-
Notifications
You must be signed in to change notification settings - Fork 144
Timer and Tween Functions in Lua #48
Comments
One bug I had using tweens like this is that the tweening value never really reach the target value. It's problematic with cyclics behaviors : local platform = {x = 1000}
after(6, function()
tween(3, platform,{x = platform.x +500}, 'in-out-cubic', function()
tween(3, platform,{x = platform.x -500}, 'in-out-cubic')
end)
end, true, "moving_p")
What I did to prevent that is simply to make the tweening value equal to the target value when the tween end. Also to prevent nesting functions for complicated behaviors you can use coroutines ( Thanks for the nice article :) |
�Would you please tell how to run this code ? |
@nvcken This is designed for Lua, typically in the Love2D environment. |
I think that my article about making cutscenes can be somewhat relevant here. delay(0.5)
cat:goTo(meetingPoint)
delay(0.25)
-- do something else You can even have parallel actions happening like this (especially useful if you have multiple parts moving at once): function cutscene(cat, girl, meetingPoint)
local c1 = coroutine.create(
function()
cat:goTo(meetingPoint)
end)
local c2 = coroutine.create(
function()
girl:goTo(meetingPoint)
end)
c1.resume()
c2.resume()
-- synchronization
waitForFinish(c1, c2)
-- cutscene continues
cat:say("meow")
...
end |
@eliasdaler hump.timer seems to handle this in a fairly straightforward manner (https://hump.readthedocs.io/en/latest/timer.html#Timer.script, https://github.com/vrld/hump/blob/master/timer.lua#L96) and something like that could be added here: function Timer:script(f)
local co = coroutine.wrap(f)
co(function(t)
self:after(t, co)
coroutine.yield()
end)
end I just personally never ended up using this myself because the |
Yeah, it's a similar idea, I see. I like coroutines, because they allow you to wrap even more complex actions in coroutines, e.g. "goTo" executes until the entity arrives at destination. "say" shows dialogue box until you press a button, and so on. The possibilities are endless. :) |
Timer and tween functions are some of the most fundamental and useful functions for gameplay code in my opinion and so in this article I'll go over getting those properly implemented in Lua. If you use other languages you can probably use this article as a reference but some implementation details will likely have to be different.
Motivation
The standard way of doing something after n seconds in a game looks something like this:
And so after 5 seconds from when
timer_current
was first set to 0 the action will be performed. Doing things this way is mostly fine but it's noisy and cumbersome and gets even more so when you want to do timers that are bit more involved, like chained actions. So the way we'll solve this is...after
We'll use a function called
after
, which takes in a delay and a function, and then performs the function after the given delay. It looks like this:This simplifies the problem quite a bit because now we don't have to care about additional variables or updating things, we just say that we want this to happen and it will. In Lua this works fine because Lua allows for functions to be passed as arguments like this, but depending on your language this approach might be harder.
Either way, let's get into the details of implementing the
after
function. The high level way it will work is that we'll keep an internal table of all timers that have been registered (theafter
function could be calledregister
too, and some timer libraries do call it something like that) and then we'll update all registered timers and perform their functions when appropriate.So here what we're doing is just that: adding a structure that contains the delay and the action to be performed to a
timers
table, and then updating each of those structures. What that update looks like would be something like this:And so in this way we have a simple
after
function that works. One simple example of it in use is this:We have some kind of hit function that gets called when the player is hit, it sets some flag saying that the player was just hit to true, and then 1 second after that flag is set to false. This flag can then be used to make the player flash white, for instance.
Tags
One necessary thing that comes with this setup is a way to cancel existing timers. Think of what happens in the previous example when the player is hit once and then again after 0.3 seconds. The behavior we want is that
just_hit
is set to true again on the second hit, and 1 second after that second hit it's set to false, but what will actually happen is that as the 1 second is over for the first timerjust_hit
will be set to false, 0.3 seconds earlier than it should have been. Most of the time when I use timers in my game I actually want to cancel the previous timer that performs that same action.We can solve this problem by identifying a timer by its tag (a string or number) and then whenever we add a new timer with a tag we automatically cancel any timer that already exists with the same tag:
And so here we use the fact that tables in Lua can also be used as dictionaries and then we use the tag as the key, and the table we were creating previously as the value. Whenever we call this function with a tag defined we will overwrite the previous value set in
timers[tag]
, and so we get our desired outcome. The way to call this now would be something like this:And so here this particular timer created in the player's hit function will always have the
just_hit
tag attached to it, meaning that any time a new timer is created here, a previous one that is currently running will be automatically cancelled.There are all sorts of details to solve with this setup, like the fact that all tags have to be unique across multiple objects for this to work properly, or that tags can be optional so we have to change the
after
function slightly to deal with when the user doesn't provide a tag, and so on. I'm not sure if this is the ideal solution to this problem, but it's a problem that happens often enough and it's a solution that has worked well for me so far.Repeatable after
Another type of behavior that's really useful is repeatable behavior. Say we want something to make the player flash really fast whenever he gets hit. One way to do it is being able to define repeatable behavior in the after function, something like this:
And here the argument after the action is a boolean that will signal that the action should be repeated indefinitely. In this example, every 0.04 seconds
just_hit
will change between true and false, which means that in our draw function we can make the player appear and disappear for small amounts of time, which gives the desired "flash really fast" effect. In any case, the way this idea looks like implementation wise:Here the only differences are that we add the
repeatable
key to our timer structure, and then whenever we perform our action we check if this attribute is set to true, and if it is we setcurrent_time
to 0, which will restart the counter and make the action happen again afterdelay
.If we want to control how many times we should repeat an action we can make the
repeatable
attribute also be valid as a number, so, for instance,after(1, function() end, 5)
would repeat the function every 1 second for a total of 5 times. The way this would look implemented would be something like this:Here the only additional thing we do is checking the type of the
repeatable
variable, if it is a number then we subtract 1 from it and then resetcurrent_time
, and if that number gets to 0 then we destroy this timer instead, since it has already repeated the action for the required amount of times. Destroying the timer can be as simple as doingtimers[tag] = nil
.One last additional thing we can do is add an optional function that gets called when the repeatable timer is done. And so our function would have the following description
after(delay, action, repeatable, after, tag)
, with the last 3 arguments being optional. One possible use of this, using the player hit example from above:And so in this example the
just_hit
attribute will oscillate between true and false 25 times over 1 second (25*0.04), and then once that's done we'll make sure that it's set to false.tween
Now for the next function which is the tween one. The way I've settled on this function generally is like this:
And so this will make the attributes
self.sx
andself.sy
go to 0 over 2 seconds using thecubin_in_out
tweening method. Because of the way Lua tables work we can very easily change attributes like this and so this became my preferred way of doing it. The implementation looks like this:The main difference here from the
after
function is that we need to get the initial values as they are on the source and store them in another table. So using the example above,initial_values
will have the keyssx
andsy
as the values ofself.sx
andself.sy
when thetween
function was called. We need these initial values to perform the tween in the update function, which looks like this:There are a number of things that are pretty Lua specific here so let's go step by step. The first line accesses
_G[timer.method]
._G
is Lua's global environment table, which holds a reference to all global variables. If you define a global function in Lua then you can also access it via the_G
table. For instance, we just defined thetween
function, and instead of callingtween(2, self, {sx = 0, sy = 0}, 'cubic_in_out')
we could also do_G['tween'](2, self, {sx = 0, sy = 0}, 'cubic_in_out')
. Those are exactly the same thing. Which means that themethod
attribute is being used as the name of some function we defined, in this case the tween method. In any tweening library there are numerous tween methods available, for instance, here'scubic_in_out
:All these methods receive a value from 0 to 1 and return a value from 0 to 1 with some given curve being applied to it. The source code at the end of this article has all tween methods I defined, and you should be able to find these methods implemented in many languages. I got mine from this tween library which is written in Haxe, but this is pretty standard code that should be nearly the same across most tween library implementations.
In any case, we get the modified value from the function by passing in
timer.current_time/timer.delay
, which is how far along in our tween we are currently. For instance, if the delay is 5 and the current time is 2.5, then we currently are at 50%, which is 0.5 in a 0-1 range. After this we do the main work:Here we go over all keys and values in the source table, apply a lerp from the initial value of a particular key
timer.initial_values[k]
to the desired valuev
, and then apply that totimer.target[k]
.So, for instance, in our previous example our
source
table was{sx = 0, sy = 0}
, so first going throughsx
we'd be doingself.sx = lerp(t, initial value of sx, desired value of sx which is 0)
, and ast
increases we getself.sx
closer and closer to the desired value. We'd then repeat the same forsy
given that we're going over all keys intimer.source
. The code after this part simply runs a function after the tween is done if one is defined and that's it!Examples
Having
after
andtween
defined in this manner we can do lots of cool things with them. I'll go over some examples of how I've used them before.Visualizations
In the previous article I wrote all visualizations are powered by the
after
function. This is what the visualization looks like:And so, for instance, the code to generate the first part this (the physics circles) looks like this:
And if the code is left like this it simply generates all circles at the same time. But with a very small change like this:
We're making it so that we generate one circle every 0.05 seconds. And that looks like this:
All the next steps follow the same idea, where I simply wrapped action being done into an
after
call that is offset properly based on the index of the for loop. The fact that it's so easy to do this also means it's a great way to debug these algorithms that can get a bit opaque and blackboxy after a while.Event scale
These functions are very useful when juicing up your game in general but one particular use that's easy to get across is scaling entities on important enough events. The way it goes is roughly like this:
The player's
sx
andsy
variables are naturally at 1, since the player isn't being scaled in any way by default. But when he gets hit, they will go up to 1.4 and then decay down back to 1 over a small amount of time, like 0.15 seconds. This gives the hit a bit more oomph and makes it feel nicer. The same idea can be applied to when the player attack, or when enemies get hit, or when a new item is acquired, and so on.Chained timers
Some more complicated uses involve lots of chained timers and cancelling of previous timers on weird conditions and so on. About 2 years ago I was playing around with doing some animations using timers only and one example of it can be seen here:
This looks fairly simple but the code for it looks something like this:
And it just keeps going and going. But as you can see, there's a fair bit of use of the
after
andtween
functions to do various things. If I were to do this again today I'd probably not choose this route but it shows that these two functions alone can create some OK things with some creative use.Source code
The source code for what was implemented in this article can be found here and it's implemented in Lua. Most of these ideas can carry over to most modern languages without significant drawbacks, but for some languages you might wanna choose something different altogether because the drawbacks outweigh the benefits. Good luck!
The text was updated successfully, but these errors were encountered: