The video essay version of this blog post - including a few extra examples that work better in video.
Good “Game Feel” is often the difference between an okay game and a great game. An okay game has working buttons. A great game makes mashing majestic. But adding all of this detail can be daunting when it’s hard enough to get a game working in the first place! An ideal tool would keep implementation simple while providing oodles of feedback. Which brings us to springs!
Part 1: Springs, Explained
Officially, springs are a mechanism that produce a restoring force when stretched or compressed. For our purposes, springs keep an object at a specified position. We can imagine the object and the destination are connected by a literal spring - if we move the object to the left or right, the spring will apply forces until the object reaches the goal position. We can set this goal position whenever and wherever we want, and the spring will update accordingly. This is the true beauty of springs - we get to code in terms of A and B, while the spring does all of the detail work in between.
Codewise we love Ryan Juckett’s implementation - linked here. This function has six parameters. The first two - position and velocity - track the state of the spring. Since they use the “ref” keyword, we expect the spring code to make changes to these values. After calling the spring code, we can use these values as is - the function will keep the position and velocity up to date.
Next we have the equilibrium position, which is the spring’s goal position. Delta time is, well, delta time - we pass this in so that the spring can perform the necessary math.
The last two values - damping and frequency - are what we’re most interested in. These two determine how the spring behaves.
Damping controls how “spring-y” the movement should feel. When the damping is 1 there will be no overshooting or rebounding - the object will land exactly at the rest position. When the damping is 0 the spring will bounce indefinitely - the object will never come to rest.
Frequency controls how “strong” the movement is. A low frequency means the spring is less reactive when making changes. A high frequency will cause the spring to explode at the slightest change.
Though it’s only two parameters, each has a distinct flavor. Should the motion be bouncier, or not? Should the motion have more power, or less? This is great for fine tuning the behavior since there’s no guesswork! We either blame the damping or frequency and then make the change. With some practice you’ll develop a springy sixth sense of what a damping of 0.75 means or what a frequency of 5 “feels” like.
Part 2: Button Spring
Enough theory - let’s implement something! Chances are you’ll eventually need a button in your game. A spring will add lots of personality and thrives on player input, all while keeping our code blissfully unaware.
We’ll start with the pressed state. When the button is down, we want the red button section to move down as well - this motion will be controlled by the spring. We’ll use this same state to set the height - when the button is down we’ll use the depressed height, and when it’s up we’ll use the raised height.
Now we need to turn this value into motion. We can directly map the spring value onto a vertical position, and it works! With some damping changes (let’s lower it for some more bounciness) and some frequency changes (let’s turn this up for more oomph), we suddenly have a pretty satisfying motion. If we press or release the button during motion, the animation doesn’t miss a beat.
Now let’s give the button some squash and stretch. Here we want the button to flatten out when pressed. Using a new spring, we’ll update the values in roughly the same place as before - when the button is down we want the smaller scale, and when the button is up we want to return to normal.
Springside we’ll map the value onto the Y scale. We can amplify this feeling by stretching out X and Z as Y gets smaller. Here we take the Y delta and apply it in the opposite direction to X and Z. Once again we need to fine tune the spring values - lower damping for more bounce, up the frequency for more power - and we got ourselves one boisterous button.
Part 3: Every Spring, Everywhere
Though springs may seem limited at first, you’ll soon start to see applications just about everywhere.
Let’s go back to our button example and add an “attract” animation - like bouncing the button to get the player’s attention. We can actually just reuse the existing squash and stretch animation by “nudging” the spring’s velocity - no new motion code required! For this we add a “nudge” value to the current velocity, and then the next spring update will run with it. Because this doesn’t affect the rest position, we can run the attract code without worrying about the pressed state. The button can be bounced while up or down or anywhere in between.
Springs also allow us to animate objects beyond just two values. For instance if we wanted this button to slide along a track, we can set up a spring that moves it left and right. When the player moves their finger, we set the spring to the X position every frame, and watch it move right along. If we want the button to “slot” into specific positions when released, we can round the spring value on the up event. Again we get to maximize the fidelity with next to no impact on the code’s state.
Another useful pattern is setting up “follow” springs. Here the spring is updated to match an object’s position every frame, creating a satisfying trail motion. We can also use this spring’s velocity and lag for secondary animations. The delta could be used to rotate the object, or even used to squash and stretch.
So far these examples have been in one direction - by adding another spring we can move in two! We can make an object follow the player’s mouse, or we can give that object any destination. This is great for camera systems, since we can frame whatever we want and not worry about jerky transitions. Plus we can nudge this value for some screen shake.
Using 2D springs in local space is a great way to add fidelity to a blocky system, like cells in a Match 3 grid. When a finger is down we can match the position, and when the finger is lifted (or a cell is swapped) we can return to (0, 0). And yet again we can reuse this simple spring for grid-wide effects like explosions - take the delta between the two objects and push in that direction. Then we can use the distance to adjust the strength and timing of the nudge until we get the grid feeling just right.
We can use multiple 2D springs for more complicated animations - like attaching one to each end of a meter. Depending on where objects hit the meter, we can push on either side as needed. On the visual side, we’ll place the meter at the middle point between the two ends and rotate to match the orientation. And since springs are so granular, we can even adjust the overall strength depending on the fill. That way a fuller meter has a better chance of catching the player’s eye.
Part 4: Spring Smarter, Not Harder
Now that we have a better handle on what springs can do, there’s a mental model shift that will make springs dramatically more reusable. Previously when we wanted to move something 500 units right, we set the spring’s resting position to 500. But if we want to update the object’s rotation, scale or alpha with that motion, we would need a spring for each component.
Instead we want to drive this entire sequence with a single spring, and move the specifics into different scripts. But we need to rethink the motion in terms between 0 and 1. In this case, we'll animate an object from -1 (beginning of the intro) to 0 (resting / default position) to 1 (end of the outro).
The movement script is as simple as multiplying the spring value by a factor. This makes for easy tweaks as we fine tune the effect - by changing this factor we can make the element move closer or further away.
For scaling we’ll need to massage the spring values a little more. If we just use a factor like before, we’ll notice that the behavior is inverted - everything starts backwards, then scales to 0, and then becomes normal. A negative factor just reverses the effect. Instead we can get rid of the upside down portion by taking the absolute value of the spring before applying the scale. And because we know the spring value will always be between 0 and 1, we can reverse the scale using a lerp with the first value being the default scale and the second the intro/outro scale.
We can use this same methodology to adjust the alpha. We’ll take the absolute value of the spring, remap the values with a lerp, and we’re all set! If we want more control we can use an animation curve to fine tune the effect - a nice big section of full alpha around 0, with rapid drop offs at -1 and 1.
Getting in the habit of “normalizing” your spring values between 0 and 1 essentially turns springs into APIs that any number of other scripts can plug into! We can mix and match these motions as needed - using just one to move, using just one to scale, or combining them all for a compound effect.
It should be clear by now - we love springs! They make adding detail easy and are second-to-none for handling player input.
It’s worth saying out loud that game dev is hard work - you barely have enough time to get things functional, much less having the spare cycles to properly polish! Finding reliable tools that simplify implementation and amplify iteration are essential. When changes are easy to make we’re more likely to actually make those changes! A million tiny adjustments often takes a game from feeling okay to feeling great.
BTW some of these examples work better in video - so if you're curious be sure to take a look at our video version of this post (YouTube)