Epoxyish, a modern spring solution

Epoxyish

Epoxyish is a generic solution to springing values unlike before. You can animate various data types and automatically have the springs animate to your instances based on values you create and change.

Everything is fully type-checked and thus should autofill.

Supported value types include:

  • Number
  • Vector2 & Vector2int16
  • Vector3 & Vector3int16
  • CFrame
  • UDim
  • UDim2
  • Color3

The ideal format of expoyish is to store Spring, Value, and Latch as variables to speed up development process, and this post shows examples assuming such.

local Epoxyish = require(ReplicatedStorage.Epoxyish)

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

:warning: This resource uses the PascalCase name formatting for all value types, including variables, properties, and functions.


Expoxyish.Spring

Below is the type definition of a spring object:

export type SpringProperties = {
	Speed : number?,
	Dampening : number?,
	Force : number?,
	Mass : number?,

	Target : {
		Set : (any) -> nil,
		Get : () -> any,
	},
	
	Position  : any?,
	Velocity : any?,
}

In other words, you can set speed, dampening, force, mass, position, velocity, and target. Required values are target, speed, and dampening.

Per your decision, you can change these values in runtime, including the data type of the target itself. For example, you can change the speed or force while the spring is calculating, and may even change the target from a number to a vector (if you have a use case for such).

Springs are object-oriented, so they should be treated similarly to Roblox instances programmatically. You can construct a spring like this, for example:

local ExampleSpring = Spring {
	Target = MyValue,
	Speed = 10,
	Dampening = 5,
	Mass = 8,
	Force = 30,
	Position = Color3.new(0, 1, 1),
}

Springs must have the target be a Expoxyish.Value object, which is explained below. You can use them independently or with Epoxyish.Latch.

If you want to detect when a spring is finished, you can do:

Spring.Completed:Connect(function()
   ...
end)

Springs sleep when they are completed. If a spring is at the target, the property Active will be false. Otherwise, it will be true.

Spring objects also have three methods: Get, Impulse, and DisconnectLatch.

Spring:Impulse(Force : any)

This method simply adds force to the current velocity of the spring. The force must match the data type of the target, otherwise, the position and velocity are reset.

Spring:Get(Property : string?)

This gets the current value of any index from the string. Property defaults to Position. For example, calling Spring:Get() will get the current position of the spring, Spring:Get("Velocity") gets the current velocity, etc.

The following values should not be read as properties and should instead be read through :Get() to be accurate:

  • Position
  • Velocity
  • Acceleration (not a property of spring anyways)

Spring:DisconnectLatch()

If you are using Latch with the spring, calling this method disconnects the spring from it.

Springs do not actively calculate through RunService, so they do not have any footprint until you read them.

:warning: Spring targets must be an Epoxish.Value type, no exceptions! Attempting to do otherwise will throw an error.

Epoxyish.Value

The Value object is a simple instance that is constructed with any type of initial user data. The only two methods are :Set() and :Get().

Below is an example using Value:

local MyValue = Value(100)

print(MyValue:Get()) -- "100"

MyValue:Set(200)

print(MyValue:Get()) -- "200"

Epoxyish.Latch

This is the gold of this resource. Rather than having to use RunService for reading and updating springs all of the time, you can just use Latch. Latch supports many instance types, such as base parts, UI objects, value objects, etc. The only limitation is the actual property type.

Take a look at the code below using Latch:

local PartPositionValue = Value(Target.Position)

local PartPositionSpring = Spring {
	Target = PartPositionValue,
	
	Speed = 5,
	Dampening = 3,
}

Latch(Part) { -- Part is, well, a part.
	Position = PartPositionSpring,
    -- Index is the property name, value is ALWAYS a spring
}

Here is an example of changing the value to animate the spring:

while true do -- While loop is bad lmao
	task.wait(1)
	PartPositionValue:Set(Vector3.new(math.random(-30, 30), 10, math.random(-30, 30)))
end
The whole example script
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Epoxyish = require(ReplicatedStorage.Epoxyish)

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

local Target = workspace.Target
local Part = workspace.Value

local PartPositionValue = Value(Target.Position)

local PartPositionSpring = Spring {
	Target = PartPositionValue,
	
	Speed = 5,
	Dampening = 3,
}

Latch(Part) {
	Position = PartPositionSpring
}

while true do
	task.wait(1)
	PartPositionValue:Set(Vector3.new(math.random(-30, 30), 10, math.random(-30, 30)))
end

And here is the result:

wAVZPhfH

Again, notice how I never used RunService to read the spring. It just works.


Get Epoxyish

Epoxyish is available on the Roblox Marketplace and Wally.

These resources take time and effort to make! Please support me for just $1 a month on my Patreon.

Shoutout to Eltobb, the formatting of the library is inspired by Fusion.

70 Likes

The fact that it can have multiple value types, very good :slight_smile:

1 Like

Incredible as usual, just what I expected from iGottic! Excellent job as usual!

1 Like
Could you compare this to the elastic easing style? I'm sure this module provides a much more customizable easing style, but I just want to make sure!

Good.

Tweening via TweenService is already a not-so-great thing, to start with.

That being said, the elastic tweeting style is incredibly limited. Tweening styles are always the exact same, meaning you can not change how the style behaves:

Springs are superior not only because they are so customizable, but also because they also replicate non-elastic behavior with a high dampening value.

2 Likes

Very clean module. I’m quite impressed with the uses of this. Well done.

1 Like

This feels very Fusion-like, which is a good thing. Especially how you’ve written out your example script, this is generally just an amazing resource. Unlike built-in functional springs like chriscerie/roact-spring or Fusion’s built-in spring, this is a standalone, and even could be incorporated into aforementioned projects. Obviously this is no doubt not the first time a standalone spring resource has been executed, but this has been executed extremelt well.

2 Likes

You got me wildin’ when I saw it supported CFrames :heart_eyes:.

I am curious though, what’s the benefit of using Latch? Is there a downside to RunService that gets fixed by Latch? I’d love to hear your reasons!

1 Like

Using RunService is epic, but when you have alternative to it (RunService and while loops), you should use that for efficiency. I use Latch and it has given me promising results:

1 Like

Using Latch simplifies your code and helps you just focus on what you would like as results.

2 Likes

Is there a way I can tween a camera CFrame for Gun Recoil? If so, how?

Yes. The recoil itself can be a CFrame, and every render frame can multiply the camera cframe by the spring value.

Currently using UserInputService and then detecting a mouse button down, and not using RenderStepped. Most Springs, go off of delta time which I cannot use for UserInputService, though input.Delta gives me inaccurate results.

How would I go about doing it with UserInputService?

Currently, trying out this module…Am I doing something wrong? All I want it the camera to go up, and not do this.

External Media
local Epoxyish = require(game.ReplicatedStorage.Epoxyish)

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

local Value = Epoxyish.Value 

local recoilValue = Value(workspace.CurrentCamera.CFrame + Vector3.new(0, 1, 0))

local RecoilSpring = Spring {
	Target = recoilValue,
	--Speed = 2, 
	--Dampening = 0
}

game:GetService("UserInputService").InputBegan:Connect(function(input, GPE)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then 
		Latch(workspace.CurrentCamera){
			CFrame = RecoilSpring
		}
	end
end)

You want to use RunService as well, without latch.

I would rather use UserInputService, and not RunService because it’s easier for me, and I don’t have to backtrace my code. Any ideas on how I would do this with UserInputService?

I mentioned both. You have to use RunService because you have to multiply the camera’s CFrame every render frame, hence why I mentioned not using Latch. You only use UserInputService for, well, user input.

There is not any way around this.

RunService inside of the UserInputService or the other way around? I got the Latch part,

Without using Latch part, how would I use the Spring? (I’m a visual kind of guy, so code examples would help, please don’t spoonfed ty)

game.RunService.RenderStepped:Connect(function(dt)
     -- userinputservice here with the checks
end)

Neither.

Use :Get() on the render frame

would use bt I would like if you could create a roblox-ts package.