Epoxyish, a modern spring solution

Yeah, that was definitely an oversight, I’ll publish a new version soon. Thanks for pointing that out!

1 Like

Hi, I’m trying to use the module to spring the CFrame of a model’s primary part & a hitbox part at the same time.
Both have their own Values, Springs & Latch’s, yet it seems that they don’t spring to their new set CFrame when it changes…
RobloxStudioBeta_LFxEaciplq

Could you show some code snippets?

2 Likes

Here’s snippets of the code. The Completed function never fires (I never see the prints) either.

-- initialization (only ran once)
local spring_values = {}
local spring_springs = {}

spring_values[ClientStatus.ObjClone] = mod_epoxyish.Value(CFrame.new())
spring_values[ClientStatus.ObjHitbox] = mod_epoxyish.Value(CFrame.new())
		
spring_springs[ClientStatus.ObjClone] = mod_epoxyish.Spring{
	Target = spring_values[ClientStatus.ObjClone],
			
	Speed = 10,
	Dampening = 5,
	Mass = 8,
	Force = 30,
}
spring_springs[ClientStatus.ObjHitbox] = mod_epoxyish.Spring{
	Target = spring_values[ClientStatus.ObjHitbox],

	Speed = 10,
	Dampening = 5,
	Mass = 8,
	Force = 30,
}
		
spring_springs[ClientStatus.ObjClone].Completed:Connect(function()
	print("spring model completed")
end)
spring_springs[ClientStatus.ObjHitbox].Completed:Connect(function()
	print("spring hitbox completed")
end)
		
mod_epoxyish.Latch(ClientStatus.ObjClone.PrimaryPart){CFrame = spring_values[ClientStatus.ObjClone]}
mod_epoxyish.Latch(ClientStatus.ObjHitbox){CFrame = spring_values[ClientStatus.ObjHitbox]}

-- updated every frame (if cframes have changed)
spring_values[ClientStatus.ObjClone]:Set(cf_obj)
spring_values[ClientStatus.ObjHitbox]:Set(cf_hit)

You are making them target each other, it seems like.

1 Like

This resource sounds great, but there is a lot of boilerplate code which I’m afraid will clutter up my codebase.

For instance, suppose I want to animate the ImageColor property and Position property of an ImageLabel, with a tool like spr, this will just amount to:

spr.target(DAMPING_RATIO, FREQUENCY, {
    ImageColor = Color3.new(1, 0, 0),
    Position = UDim2.fromScale(0.5, 0.5)
})

but with Epoxyish, on top of the existing boilerplate code, I’d still have to create an entirely new spring for each property I want to animate.

local ImageLabel = ...

local Value = Epoxyish.Value
local Latch = Epoxyish.Latch
local Spring = Epoxyish.Spring

local imageColorValue = Value(Color3.new(0, 0, 0))
local imageColorSpring = Spring {
    Target = imageColorValue,

    Speed = 5,
    Dampening = 3
}

local positionValue = Value(UDim2.fromScale(-0.5, 0))
local positionSpring = Spring {
    Target = positionValue ,

    Speed = 5,
    Dampening = 3
}

Latch(ImageLabel) {
    Position = positionSpring,
    ImageColor = imageColorSpring
}

The difference between both approaches are clearly visible. While the first one is easier to understand, read and modify, the latter looks more complicated, confusing, and well… ugly (yes you heard me right).

All in all, this is a great module whose API design might sound good on paper but in practice is a horrible one.

Rather than using variables, I recommend making the springs in-line for the latch construction.

Springs are separate since Latch might not always be something that a developer would want to use. Having the two be separate is important for that!

Here is an example of your code modified to this type of structure:

local ImageLabel = ...

local Value = Epoxyish.Value
local Latch = Epoxyish.Latch
local Spring = Epoxyish.Spring

local imageColorValue = Value(Color3.new(0, 0, 0))
local positionValue = Value(UDim2.fromScale(-0.5, 0))

Latch(ImageLabel) {
    Position = Spring {
		Target = positionValue ,
	
		Speed = 5,
		Dampening = 3
	},

    ImageColor = Spring {
		Target = imageColorValue,
	
		Speed = 5,
		Dampening = 3
	}
}

Could you clarify here?

1 Like

Omg, I have been wanting and asking for a standalone Fusion style spring since the very moment I’ve used it. This is awesome, tysm!

But I am having an issue with the wally version, the signal module seems to be missing from the “Spring” modulescript, causing an error on runtime.

Also checked the github to make sure that my wally install isn’t doing weird stuff, and it verifies that the signal module is actually missing.

Oh my, thanks for letting me know! I’ll fix that soon. For now, you should be able to find it in the Roblox model.

I have no idea how it escaped the directory though haha

2 Likes

Is there anyway to set a get a value like Fusion? Something like

local SetSize = function()
	return if Background:Get() then UDim2.fromScale(0.2, 0.05) else UDim2.fromScale(0.1, 0.05)
end

Button.MouseEnter:Connect(function()
	Background:Set(true)
end)

it’s been 9 months but epoxish should be getting updated soon since ive informed igottic about it lol
i did see your fixed version of it though, you should have linked it to your post imo :stuck_out_tongue:

This is really good except for one thing. CFrames. The rotation of CFrames I really really cant get to work quite right with this, it just seems to cause really weird behavior sending an object really far away for no reason.

1 Like

CFrames definitely don’t work as expected… :cry:

local Epoxyish = require(script.Parent.Epoxyish)
local Value = Epoxyish.Value
local Spring = Epoxyish.Spring
local Latch = Epoxyish.Latch

--\\ Private Methods

local function getCFrame()
	return workspace.Baseplate.Attachment.WorldCFrame
end

--\\ Connections

local spring = Spring {
	Target = Value(getCFrame()),
	Speed = 5,
	Dampening = 3,
}

Latch (script.Parent) {
	CFrame = spring
}

while task.wait() do
	spring.Target:Set(getCFrame())
end

Position seems to work alright, but when Rotation gets involved… brace yourself.

Video on Streamable

Thanks for the report! Will look into it.

1 Like

I should that add this seems like what @ianfinity01 was describing from a month ago!

2 Likes

After experimenting a bit, I found something that may be very important in figuring out how to solve this.

1: IterationForce calculated incorrectly?

Here is the formula:

It makes sense! The force is target - position. However, with CFrames, an unexpected value is made with this.

{E0E27DDC-0A1E-4FB3-8385-841C1DA3CF97}

Since the Y for both these CFrames is the same, you would expect the new Y to be zero. It turns into 0.95 for some reason! And the Z for both is zero, but it turns into -3…? The RX value seems to be correct though. 38.38 - 2.38 is indeed 36. What happened here?

Here’s the original CFrame Add function:

{0E1B6096-C270-4BCE-A8CC-B8AAE83546FE}

Here’s my fixed version:

This fixes the IterationForce and the “flying” bug, but Acceleration has another problem. This is where I got majorly stumped, and I bet you will too…

2: Acceleration problem

At this point, the “flying” bug is fixed. It’s when the position flies off into infinity at most angles. This problem describes why the spring doesn’t rotate correctly.

Here is the formula for acceleration:

Again, this makes sense! F = ma, and we’re solving for a, so the new formula is a = F/M, which can be seen above as Acceleration = F1 * F2 * 1 / M. What went wrong?

This issue lies in the Multiply CFrame function:

Here, Number is set to 50 which is the spring’s force. RotationX is set to 36, and 36 * 50 is 1800. So, our rotation X acceleration is going up by 1800 radians per second. Checks out.

Or does it??

local angles = CFrame.Angles(math.rad(36) * 50, 0 * 50, 0 * 50) 
print(angles:ToOrientation()) --> -5.56283680452907e-07 0 0

No. The acceleration is (basically) 0 radians per second, so it doesn’t rotate. :cry:

This is where I hit the end of my knowledge. CFrames are very confusing. All I can tell is that 1800 is divisible by 360, so maybe that has something to do with it.

If I set the force really low, like to 2, it looks like it wants to work…

…then it starts doing backflips. Working with the orientation values seems to be pretty unreliable. :sweat_smile:

The only solution I can think of is not using CFrames to represent CFrames. Instead, two Vector3s representing position and orientation respectively. I have no idea if it would work though. Maybe I’ll try later in another reply?

You can try the test I used here (with my current fixed code):

EpoxyishCFrameTest.rbxm (15.8 KB)

1 Like

Thanks a ton! This is mega helpful.

1 Like

I will use the Quaternions spring module to solve the issue. Basically, I plan to just integrate it in Epoxyish when your asking to make a CFrame spring. When I tried solving the CFrames issues, I came across ‘Gimble Lock’. :cry:

I’ll undo everything in my old post because this approach seems more convenient. Tomorrow. It’s 11 PM. :3

Old Post

I was able to fix this! Sort of.

https://youtu.be/xUbtEVBGis0

Epoxyish Fixed CFrame Test (16.2 KB)

By representing CFrames as a list of 6 numbers (X, Y, Z, RX, RY, RZ) it works good enough.

The target, position, and velocity are all “decompiled” into something usable. This could be the same value, but if it’s a CFrame, it’s turned into the list.

Since CFrames were lists now, I had to make a new method to get the type of values.

{A3775D9B-1619-4562-B99A-3232F0165438}

The Multiply, Add, and AsNumber methods had to also account for these lists, so I changed that in those modules:

Sidenote

The logic behind making springs sleep was a bit buggy, so I fixed that too. It made it seem like springs stopped abruptly, randomly. The new logic is as follows:
(edit): Is it actually fixed…? I will have to actually test. Lol.

The highlighted line is the one I added!


This solution fixes the acceleration problem, since these numbers have no limits.

The video shows that the RX spring test behaviors a bit weirdly…

When you rotate the rotor between 90 and 270 degrees, the test fails.

image

We can look into this later, I’m going to have stuffed shells!

Summary

1 Like

I’ve implemented the Quaternion Springs to fix the issue!

I will still need to battle test this to ensure its bug-free, but this is good news!
If I get lazy and you want to test it instead, you can try with this model:

EpoxyishWithQuaternions.rbxm (43.4 KB)

1 Like

This is awesome, thank you so much!

1 Like