SPEAKER: All right.
In Mario5, we added the ability for our camera to track the player as opposed
to just sort of scrolling independently of where the player was being rendered.
In this example, we're going to dive into a couple of things.
So this is called the animation update.
And in order to get into animation, it's going
to be a somewhat complicated process.
So the first thing that you're going to notice
is that we have our avatar there.
We have a frame of animation as they're going towards the left.
You can see they're very clearly looking towards the left.
And it'll be the case that the avatar will also be able to look to the right.
They'll have a walking animation.
And then when we don't actually move, we want
them to go back to their idle animation where they're sort of looking dead on.
And this kind of helps us tie into a couple of concepts.
So the first thing that we want to look at is sprite animation.
And a sprite animation is really kind of like a flip-book
like you might have seen where you have one picture on one page,
then you have another picture on the next page,
and another picture on the next page.
And by flipping through them sort of quickly,
it gives you the appearance of it being something that's actually moving.
And this is, of course, the principle behind animation.
2D and 3D animation, as we know it, is just a series of still frames
that together form, or at least it tricks our minds into thinking,
our eyes into thinking that we're watching something really moving
and doing something on the screen.
And we're going to be doing the same exact thing here
with sprite animation where we take those quads
that we've been chopping up before, and we put them together
in a sort of a package.
And then we loop through them, depending on some sort of timer.
We're going to keep track of using DT how much time has elapsed.
Over the course of several frames, we're going to cumulatively add that time.
When it passes a certain amount of time, let's say 0.15 seconds,
we'll say that it's time to go to the next frame.
At which point, our animation class that we're going to use says,
OK, this is the current frame of the animation.
This is the current frame of the animation.
And once the whole entire animation has elapsed,
we've gone through all the frames, we'll loop back to the first frame
over and over again, which will give us the appearance of sort of a looping
animation.
Another thing that we're going to take a look at,
which is really important in this context, is state.
Once again, we looked at state before in the context of Pong
where we had different game states.
But now, we have an entity that's going to behave somewhat
similar to what we're seeing here where we actually do have a standing state.
We have a moving state.
We have a jumping state.
Moving isn't actually diagrammed here.
But if we are standing, and we move a direction,
we should transition to the moving state, the walking state.
If we're walking, and we stop moving, we should
transition to the standing still state.
And this should also reflect on the animation
that's currently being presented.
We should have two different animations-- one
for moving and one for standing still.
And if we're moving, we should play the moving animation.
If we're standing still, we should play the standing still animation.
So pretty straightforward.
That's all the real framework that we'll lay
before we actually get started here.
The first thing that I want to do, probably,
is start with the animation class.
So let's go ahead and create Animation.lua.
So animation is going to be equal to a class.
I'm going to go ahead and preemptively load that here in Player.
So I'm going to go ahead and require animation, just
so I don't forget to do that later.
And the animation is going to have an init, just like anything we've
seen thus far.
But the init is going to take a series of parameters.
And the parameters are what's going to define not only the frames, but also
the interval through which the animation is going
to take place, the sort of amount of time
in between each frame that's going to tell our timer how long we should wait
in order to go to the next frame.
So let's go ahead and say that this params is going to carry with it
a texture.
And this params is ultimately going to be a table.
So we're going to index into it using dot syntax like this.
And we're going to very easily be able to sort of pass
this sort of a configuration object in that sense.
So we're going to get self.texture.
We're going to have some frames.
And we're going to expect that we're going
to have the frames sort of attribute on params, such that we
can worry about passing in the quads from wherever
we instantiate the animation.
Next, we're going to declare an interval.
And this is going to be the amount of time that
should pass between each frame.
And so we're going to say params.interval,
or we're going to have a default of 0.05.
So this or just means that if we don't get a params.interval within our table
object that gets passed in, it's going to default to this 0.05 value.
Now what we're going to do is declare also a self.timer.
Now, this, it's going to be starting off as a zero value,
but it's going to be incremented by delta time each and every frame.
And we're going to have an update function down here that
actually takes care of this.
So I'm going to go ahead and say this update.
And the very first thing we're going to do in here
is say self.timer is equal to self.timer plus delta time.
And what we're going to do pretty soon is,
as we get delta time incremented onto our timer, all we have to do
is check to see whether the timer is greater than the interval.
And if that's the case, well, then the current frame
is going to be needed to set to the next value.
And accordingly, we need to have a self.current frame.
By default, that's going to be one.
And so this frames table here is essentially
going to be reliant on this self.current frame.
This will be the index into which we get the current frame from self.frames.
So let's go ahead and write Animation:getCurrentFrame.
This will be very useful when we're actually using this within the Player
class to draw the Player.
Depending on what stage the animation is in,
we're going to use this getCurrentFrame function, which
is going to return self.frames at self.currentFrame, just like so.
And then one thing we should also have is a restart function just in case
we want to transition between different states,
but we don't want to start in the middle of, let's say, the walk state.
Or if we have a state that's 20 frames long,
or an animation that's 20 frames long, we
don't want to start in the middle of it if we go back and forth.
Because these animations are going to sort of
maintain their state across transitions.
In other words, if we transition from a walking state to a standing state,
the walking state is going to kind of still
have this timer information and whatnot stored within it.
But we'd rather probably like to have it restart from scratch every time
we transition between walking and standing and so forth.
So in order to restart this, all I should do
is say self.timer is equal to zero and self.currentFrame is equal to one.
Now, let's go ahead and finish the timer class here.
So there is a nice little shorthand for getting
the length of a table inside of Lua.
And that's this hash symbol.
So if we say if hash of self.frames is equal to one, meaning if the length--
it's like the leng function in Python--
if the length of the self.frames table is equal to one,
well, we don't really have much to do in terms of an animation in that case.
So we'll just return self.currentFrame like so.
Else, and this is where we're going to do the bulk of the actual math
behind calculating the next stage of the animation.
So what we can do is we can say while self.timer is greater
than self.interval do.
And what this just means is if we reach the point where our timer has actually
gone past the interval, then we need to take
care of incrementing the current frame, pointing to the next frame,
and looping back to the start if we've gone
past the sort of end of the animation.
In other words, if we've gone to frame three,
and it's time to go to the next frame, we
should go to frame one, assuming that our animation is, let's say,
three frames long.
So I'm going to go ahead and say self.timer
should be equal to self.timer minus self.interval.
And notice that we're not just setting it to zero and going to the next frame.
There could feasibly be a situation in which a lot of time
passes between one frame and another frame.
And this is greater than the interval, or maybe even two times
the integral of the animation.
So if it's the case that we've passed two or three or four
x frames beyond just one, well, we want to take this into consideration
by putting this in a while loop and decrementing timer by the interval,
not by just setting it back to zero, which might
be the first instinctive thing to do.
So self.currentFrame should then be set to self.currentFrame.
Though, this is where we end up looping the animation back to one.
So we're going to say self.currentFrame plus one modulus
from C, as you've probably seen before, the number of self.frames plus one.
And we're doing this plus one such that when we get to frame three,
we don't immediately loop back to frame one, if we have three frames.
We want to actually sort of do it plus one so
that we go all the way to the third frame,
and then we calculate it as if we are incrementing beyond into the next frame
beyond the edge of the frame table, let's say
frame four if we had three frames.
Because otherwise we would get to frame three and immediately loop back
to frame one and completely skip it.
We don't want to do that.
We want to sort of account for the next frame, the next invisible frame,
to give us that buffer.
And then what we want to do is say if self.currentFrame
is equal to zero, which is what's going to happen when we use modulus,
then self.currentFrame should be equal to one, because, remember,
we should have tables in Lua be one index.
That's just how it works by default. But when you start messing with modulo,
it does go back to zero, so we want to make sure that this
gets set appropriately back to one.
Because there is no zero frame in our table, so this wouldn't work.
And that's basically it for the animation class,
actually, so pretty compact.
A little bit of information there.
But that's it in terms of actually getting something
that will work to animate stuff.
So let's go back to the Player class here.
Now what we want to do is, previously, we
don't have any animation data so we want to start adding that.
So we're going to add a self.animations.
We're going to make that a table.
And we're going to index into two different animations.
So we have an idle animation, which really isn't much of an animation.
And we're going to say that it's an animation with this curly bracket
syntax.
Now, the reason we can use this curly bracket syntax
is because in Lua if the only argument to a function is a table,
then you don't have to use, say, this syntax, which you would instinctively
think to use like that, right?
Because it only takes in a single table as its only argument.
Lua gives you the shorthand to actually get rid of the parentheses
there just like that.
So we're going to use that.
So idle is going to be an animation as will walking.
So again, the idle animation isn't going to be much of an animation,
but it's still capable of being treated like one.
Our animation class does account for any animation that is only one frame long.
So we're going to do that.
And the animation class takes as an argument the table of parameters.
So again, it takes in textures.
So we're going to pass in a self.texture.
It also takes in frames.
So we're going to say frames is going to be equal to.
And then this is another table.
And we're going to specify quads here, which
we've gotten already into self.frames.
And I specifically want to make sure to grab the right frames here.
So I have one, two, three, four, five, six, seven, eight,
it looks like 9, 10, and 11 make like a pretty good walking animation.
So that'll be my walking frames.
And then I only need frame one for my idle frame.
So 9, 10, and 11 for the walking.
And one for the idle animations.
Let's go back here for idle frames is only going to be self.frames one,
but walking is going to be, again, texture equals self.texture frames
being equal to self.frames of 9, 10, and 11.
OK.
And the other thing that these are going to need too is an interval.
Now, the interval for idle doesn't really
matter because it's just a single texture.
And it doesn't actually get used, but we'll say interval of one.
And then for walking, why don't we go ahead
and say that this is going to be an interval of, let's say, 0.15.
That should be good.
Cool.
So another thing that we're going to end up getting to,
and this ties back into the idea of state,
is currently we could just put things into sort of an IF statement.
We could say if the player state is equal to x, then do this.
Otherwise, if it's equal to this, then do this.
You know?
If it's equal to standing, then we'd check for keyboard input
and so on and so forth.
It gets a little bit cumbersome.
And we're not going to save a tremendous amount of work by doing it this way.
But there's an interesting thing we can do.
We can actually model a state machine pretty effectively as
by if we say self.behaviors is equal to a table.
And let's just say we have in idle behavior.
And I'm just going to give it a function like that.
So I'm actually storing a function as a value
inside of this table with the key idle.
And I'm going to do the same thing for walking equals a function.
And this might look a little bit strange to you,
but this is something that you can get with Lua.
It has first class functions, functions that can be treated just
like data, like objects, like strings.
But in this case, they're functions.
And so we can assign them to this key here idle and walking.
And then as by, for example, going into, let's say,
update and saying self.behaviors at self.state, where self.state
is going to be either idle or walking.
We can then just call it like that and pass in delta time
like it were a function.
Because we're returning a function from these keys.
So a pretty cool feature of Lua.
This exists in languages like JavaScript as well and Python to an extent.
But super-cool first class thing.
When you get into functional programming in maybe a later course,
this kind of ties into that as well.
So pretty neat.
OK.
So when we're in the idle state, really what we want to do
is just check to see this right here.
If the keyboard is down, where we're pressing A or D,
we should move left or right.
So we'll do that.
And really, with walking, it's the same thing.
Now, one thing we need to make sure we do is actually keep track of the state,
so self.state.
We'll set this to idle as our default. And then now then by default,
we'll just always be triggering this here.
And if love.keyboard is down A or D, then
our x is going to move minus mu speed times delta time
or plus move speed times delta time, depending on which key we press.
Same thing for walking.
Not much has really changed.
Why don't we, as a sanity check, just make sure
this works before we go too much further.
So it looks like I can move left and right.
And everything is still completely functionally appropriate.
So that's great.
Everything looks fantastic.
Now, we aren't actually considering the animation in our code yet,
but we will very soon.
So what I'm going to do, actually, is in render, we
should be getting the current animation and its current frame.
So really, this is going to be not self.frames one anymore,
but self.animation, which means we need to have that as a field.
And then we need getCurrentFrame like so.
OK?
Perfect.
So let's go up here then, self.animation.
And we're going to say that's going to be
equal to self.animations idle for now.
Now, when we get to the point where we want to walk with our character,
we're going to need to set this to walking,
so that'll be important to do when we're down here.
And we can do that as by going down over here and saying
self.animation is equal to self.animations walking.
OK?
We'll do that here, here, here, and here.
So that's great.
So now, animation is going to be walking.
It's no longer going to be idle.
Let's test this just to make sure we don't
have any bugs as we keep going on.
OK?
Oh, and it looks like we have a frame rendering from the animation, which
is perfect, but we are not actually animating because we're not
updating the animation.
So this is very important.
Part of the update function involves us going to self.animation update
delta time.
Let's go ahead and run that.
And we can indeed see that we are moving.
The character is moving in its animation.
Now, we haven't transitioned out of walking yet when we're idle.
And we also haven't taken into consideration
the direction through which our entity is facing, which is very important.
And this is actually going to get into talking
about origin offsetting of drawing our character too.
So let's go ahead and get into that.
So if we're not moving, if it's the case that we aren't pressing A or D,
then what we should do is say self.animation is
equal to self.animations idle like so.
And we'll do the same thing here.
I'm going to copy this line of code.
Animations idle.
Let's go ahead and rerun that.
Make sure it works.
I'm walking.
And I stand still.
And it's perfect.
OK.
Excellent.
That's great.
Now, we do have a little bit of blurring of the character
because we're drawing our self.x and self.y at fractional values
it looks like.
So I'm just going to call a math.floor on both self.x
and self.y just to get rid of that issue.
Perfect.
And the next thing we need to do is take into consideration the fact
that we are flipping directions.
And this is very important.
Currently, the animation, if we look back at our frames,
the animation is specifically drawn as if the character
is looking to the right.
But it is the case that our character will be facing to the left
if we're moving to the left.
So we need to take this into consideration.
So in order to do this, we're going to have a self.direction.
And this is by default going to be right because that's
the direction of the animation by default.
And if I press A, which is to the left, then I should set self.direction left.
Copy this.
And I'm just going to paste this down here.
And self.direction should be right in the context of pressing D.
So I'm going to do that right here and right here.
Perfect.
So that's not going to do anything in and of itself.
Now what we need to do is determine whether or not
we should flip the animation.
And this is an interesting thing that we can add to love.graphics.draw call.
It takes in a few extra arguments at the end,
which we haven't really explored yet.
And some of those are the rotation, but also
whether it's offset from its origin and whether it's
flipped on one axis or another.
So what we can do is say 01.
Let me just confirm the order of the arguments here.
So if we say zero--
and that's the initial rotation, which we don't.
We don't want any rotation.
But we do want to flip our sprite on a particular axis.
So what we can do is we can say let's say scale x will be the variable.
So we'll say scale x.
So that's the scale factor really of our sprite.
And actually, if you scale something by a negative number, negative one,
it has the effect of flipping.
Now, normally scaling just means stretching something.
So if I scale something by say two on the x-axis,
it'll make it twice as long, or on the y-axis, twice as tall.
If I scale something by a negative number,
and specifically a negative one, it'll actually
scale it in the opposite direction, but also flip it, such that it's mirrored.
And that's exactly what we want.
And what I want to do is I want to say if self.direction is equal to right,
then else.
And if it is equal to right, then scale, let's say, local scaleX right here.
Let's pre-declare it.
And we'll say scaleX is equal to one.
Otherwise, it's going to be equal to negative one.
So in the case that it's left, our scale factor is going to be negative one.
Now, a weird sort of byproduct of scaling something by negative one
is it also shifts the texture over to that direction.
It actually flips it, but it flips it, and it
makes it such that now it's drawing its relative to its right corner.
We don't want that at all.
What we really want to do is flip some thing such
that it's flipping from its center point.
And normally, that is not the default behavior in love.
Normally, things are relative to the top left corner.
So to flip something by its center point,
we have to essentially move what's called the origin point of our texture.
And normally, the origin point, like I said,
is right up here at the top left corner of the sprite.
We want to set the origin point here because, otherwise, we
would be flipping it relative to its left side here.
We don't want to do that.
We want to flip it such that it stays in place
and has the appearance of changing direction.
So that's what we get by changing the origin by specifically moving it
to the center of the sprite.
So I'm going to say scaleX and one because I
don't want a scaleY factor at all.
I just want to scale it normally based on y.
And here, what I want to do, essentially,
is add the offset of whatever half of its width and height are.
So we'll just say self.width divided by two and self.height divided by two.
So just like that.
So that'll end up being 8 and 10, respectively.
So that takes care of that.
Let me just make sure everything here looks like it should.
Let's run it just to give it a test.
Oh, and I forgot one other thing that's very important.
You need to make sure that you also shift the drawing of the sprite
once that is finished.
Because since it's relative to the center point,
it's drawing it at the center point.
We need to account for this origin shift by actually incrementing.
We're adding to self.x the self.width divided by two and the self.y,
we need to do self.height divided by two, just like that.
OK?
We'll run that.
So now it's back on the ground.
So notice that it looks like I have a slight bug here.
So I am only moving to the left no matter what.
So let me just go ahead and verify that this is appropriate.
Self.direction is equal to right.
OK.
Oh, because all of these are left.
OK.
So if it's A, then it needs to be left, but if it's D, it needs to be right.
And same thing down here.
If it's D, it needs to be right.
Let's go ahead and run this one more time.
Let's move.
And I do indeed see the avatar moving in place,
not flipping and shifting over immediately.
And the animation is running perfectly.
And when I stop moving, our character is idle on the screen.
So that was a lot, but it was very smooth, I think.
Join me in Mario7 as we take a break away from that stuff,
and we do something a little simpler where
we add an ability to jump and, therefore also, the notion of gravity,
which is very important in platforming games.
So see you soon in Mario7.