Skip to content
This repository has been archived by the owner on Mar 8, 2021. It is now read-only.

Springs #60

Open
a327ex opened this issue Jun 23, 2020 · 0 comments
Open

Springs #60

a327ex opened this issue Jun 23, 2020 · 0 comments

Comments

@a327ex
Copy link
Owner

a327ex commented Jun 23, 2020

A very simple technique for adding life to anything that's otherwise static in your game is using springs. The example below, for instance, is made completely using springs attached each UI element's scale:

There are lots of articles that go over the math of springs and you can read them by googling "gamedev springs", so I'll skip those explanations here. I'll get straight to the implementation of a Spring class which looks like this:

Spring = Object:extend()

function Spring:new(x, k, d)
  self.x = x or 0
  self.k = k or 100
  self.d = d or 10
  self.target_x = self.x
  self.v = 0
end

function Spring:update(dt)
  local a = -self.k*(self.x - self.target_x) - self.d*self.v
  self.v = self.v + a*dt
  self.x = self.x + self.v*dt
end

function Spring:pull(f, k, d)
  if k then self.k = k end
  if d then self.d = d end
  self.x = self.x + f
end

Here the variable k represents stiffness and the variable d represents dampening. The higher stiffness the more aggressively the spring will go back to its initial value, and the lower the dampening the longer the spring will oscillate for. The pull function is called whenever we want to add a force to the spring to make it oscillate. Now, to use this with an example let's assume the following code:

SpringExample = Object:extend()

function SpringExample:new()

end

function SpringExample:update(dt)

end

function SpringExample:draw()
  graphics.print("Spring example!", gw/2, gh/2, 0, 1, 1, font:getWidth("Spring example!")/2, font:getHeight()/2)
end

And this is just drawing some text to the screen:

Now suppose we want to make the text go boing boing whenever the mouse passes over it. That code would look something like this:

SpringExample = Object:extend()

function SpringExample:new()
  self.text_spring = Spring(1)
  self.w, self.h = font:getWidth("Spring example!"), font:getHeight()
end

function SpringExample:update(dt)
  self.text_spring:update(dt)

  if math.is_point_in_rectangle(cursor.x, cursor.y, gw/2, gh/2, self.w, self.h) then
    if not self.hot then
      self.hot = true
      self.text_spring:pull(0.25)
    end
  else self.hot = false end
end

function SpringExample:draw()
  graphics.print("Spring example!", gw/2, gh/2, 0, self.text_spring.x, self.text_spring.x, self.w/2, self.h/2)
end

First, we can see how the spring is actually used. We create it to the .text_spring attribute, update it, and then hook it to the text's scale (5th and 6th arguments on the graphics.print function). The spring's pull function is called whenever the cursor enters the area of the text and notice that it's called with a 0.25 value and started with a 1 value. This means that it starts at normal scale, and then we'll pull it up to 1.25 and it will oscillate back to 1 over time:

Doing this for the scale of objects in your game whenever they interact with anything goes a really long way towards making things feel juicier. Now, one additional thing to be mindful of is that you can apply a single spring to multiple objects at the same time and multiple springs at different points to the same object.

For instance, if you go back to look at the first gif in this post, all UI elements are composed of at least 3 pieces: some text, some options and some more text or an image. Yet, they're all scaled somewhat uniformly around a general central point as well as around their own central points. This was done by using push/pop functions. Take a look at this example of the OptionsButton class (resolution and monitor buttons in the gif):

This looks complicated but it's essentially applying the .button_sx/sy springs to the center of the entire UI element, and then the .text_sx/sy springs are applied to the center of each individual piece. If you don't know what the push/pop functions do this is their implementation:

function graphics.push(x, y, r, sx, sy)
  love.graphics.push()
  love.graphics.translate(x or 0, y or 0)
  love.graphics.scale(sx or 1, sy or sx or 1)
  love.graphics.rotate(r or 0)
  love.graphics.translate(-x or 0, -y or 0)
end

function graphics.pop()
  love.graphics.pop()
end

We can apply the same idea to our example. Suppose we want to print "Spring example!" and then to its side an image of some sort. We also want both the text and the image to have be affected by .text_spring at their centers, but also to be affected by .button_spring at their collective center (the average of both elements). That would look like this:

SpringExample = Object:extend()

function SpringExample:new()
  self.text_spring = Spring(1, 200, 10)
  self.button_spring = Spring(1, 100, 5)
  self.text_w, self.h = font:getWidth("Spring example!"), font:getHeight()
  self.w = self.text_w + 4*graphics.get_image("arrow_right").w + 30
end

function SpringExample:update(dt)
  self.text_spring:update(dt)
  self.button_spring:update(dt)

  if math.is_point_in_rectangle(cursor.x, cursor.y, gw/2, gh/2, self.w, self.h) then
    if not self.hot then
      self.hot = true
      self.text_spring:pull(0.15)
      self.button_spring:pull(0.15)
    end
  else self.hot = false end

  if self.hot and input.mouse_1.pressed then
    self.text_spring:pull(0.5)
    self.button_spring:pull(0.5)
  end
end

function SpringExample:draw()
  graphics.push(gw/2 - self.text_w/2 + self.w/2, gh/2, 0, self.button_spring.x, self.button_spring.x)
    graphics.print("Spring example!", gw/2, gh/2, 0, self.text_spring.x, self.text_spring.x, self.w/2, self.h/2)
    graphics.draw_image("arrow_right", gw/2 + self.text_w/2 + 30, gh/2, 0, 4*self.text_spring.x, 4*self.text_spring.x)
  graphics.pop()
end

First, we need to calculate the total width of our whole thingy here so we can figure out where its actual center is. The total width is the size of the text, the size of the spacing between text and image, and then the size of the image. The true center will then be the half-width of the entire element (self.w/2) from the left-most position of the text (gw/2 - self.text_w/2). And so that's where we apply the .button_spring! Then the text and image are drawn in their respective places using .text_spring as their own personal springs. That ends up looking like this:

Finally, if you go back to the first gif in the post you can also see that the entire frame holding all UI elements is wobbling from left to right. Springs can be attached to any variable you want, so it makes sense sometimes to also attach them to the rotation of objects. In the example of the first gif this looks like this in code:

In our example we can also try it by creating the rotation spring in our constructor, pulling it, making sure to update it in the update function, and then adding it to the rotation argument of the graphics.push call in our draw function. That ends up looking like this:

Many many games use all these techniques to great success. The most prominent one I can remember is Forager. A lot of elements in that game hooked up to springs, from UI elements to camera movement. This is a very versatile tool to have mastery over so I hope this article was useful! ^^

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

1 participant