Simple platformer game in XNA tutorial – part three “Movement”
Previously …on xnafan.net
In the previous parts of this tutorial we looked at
- how to create a basic Sprite class (part one)
- how to specialize that class into a Tile class and create a Board of Tiles (part two)
In this part of the tutorial we will create a specialization of the Sprite class for the little blue guy
and enable him to be moved around. We will do this by storing all the necessary functionality in the Jumper class, including the math for movement, and the Keyboard input.
We will do this by
- extending the Sprite class
- add an Update() method to Jumper, so we can get keyboard input and update Movement
- add a Vector2 Movement variable to update the Position with in every Update()
The Jumper class
First I want you to go ahead and create a new class Jumper which inherits Sprite, like we did with Tile in part two of the tutorial.
Try performing the following steps without peeking at part two, so you practice recalling how to create subclasses, sending parameters to the superclass’ constructor, etc.
Add an Update method with the same signature (name, return type and parameterlist) as the Update() in the Game class. Later I will tell you why we need the GameTime parameter in Jumper, and how it helps us create smooth movement.
Add an automatic property Vector2 Movement to the class.
Add a constructor which accepts all the parameters that Sprite needs, and passes them on to the base class (Sprite).
When you’re done, take a look at the code below and make sure you have something similar
public class Jumper : Sprite { public Vector2 Movement { get; set; } public Jumper(Texture2D texture, Vector2 position, SpriteBatch spritebatch) : base(texture, position, spritebatch) { } public void Update(GameTime gameTime) { } }
Movement in computergames
It is beneficial to think of movement in computergames as an illusion created by teleporting between locations. We don’t actually move anything. Instead we show an image in a new position 30-60 times per second and our brain just fills in the blanks to maintain the illusion of smooth movement. In fact our Sprite hasn’t been to more than a couple of the positions inbetween the origin and endpoint.
This is important to remember because we may be “moving” so fast that in an Update() we may teleport our object from one side of an obstacle to another and miss the collision. Here that situation is illustrated by an acceleration while falling, where the first call to Jumper.Update() moves him 50 pixels down from top to middle position and second call is moving even faster moving him 70 pixels down and across the 64 pixel tall Tile. We never detect the collision .
Therefore we either:
have to make sure we move slowly enough that each Update() only tries to teleport us a distance shorter than the smallest obstacle on the Board
or
we have to split every Update’s “teleport” into smaller parts and test for obstacles at every little step
We are going to go with the second approach - but more about collisions in next part of this tutorial. Let’s just get something moving already!
Keyboard input
In XNA, keyboard input is obtained through the Keyboard.GetState() method. The GetState() method returns a KeyboardState which is a snapshot of the keyboard which allows you to test which keys were pressed and which weren’t at the time of the snapshot.
Every Update() we will:
- get the snapshot
- inspect the Left, Right, Up, Down keys
- change the amount to move (Movement)
- change the Position by the amount in Movement
In case you need to understand a bit more about using Vector2 for movement have a look at this tutorial (you can stop at “How to point a Texture2D in the correct direction”, since our Jumper won’t rotate ).
Updating the Update() method
First – we will get the snapshot of the keyboard state.
KeyboardState keyboardState = Keyboard.GetState();
Converting input to movement – a naïve approach
In a naïve approach we could then change the Movement property like this (NOT the way to to it!)
if (keyboardState.IsKeyDown(Keys.Left)) { Movement = new Vector2(-1,0); } if (keyboardState.IsKeyDown(Keys.Right)) { Movement = new Vector2(1, 0); } if (keyboardState.IsKeyDown(Keys.Up)) { Movement = new Vector2(0, -1); }
Q: Can you figure out what is wrong with the approach above?
A: Since all the if checks are performed in order, in case you had Right and Up pressed at the same time, Movement would first be set to +1 on the X-axis and 0 on the Y axis, and then the next if would set Movement to 0 on the x-axis and –1 on the Y axis (up). The player would not get any reaction from his right key.
Converting input to movement – a better approach
What we want to do instead of replacing the Movement is adjust it like this:
if (keyboardState.IsKeyDown(Keys.Left)) { Movement += new Vector2(-1,0); } if (keyboardState.IsKeyDown(Keys.Right)) { Movement += new Vector2(1, 0); } if (keyboardState.IsKeyDown(Keys.Up)) { Movement += new Vector2(0, -1); }
The += operator adds the value to the to the right of the operator to the variable on the left. This operator also exists in a lot of other variants, to subtract, multiply and divide (–=, *=, /=) …and more .
BTW: I haven’t added Keys.Down, since gravity will be performing that movement for us later .
This will give us the benefit of gradual acceleration/deceleration, since a fast movement left, say 5 pixels per update, wouldn’t be replaced by a sudden reversal to the right when the right key is pressed. Instead Jumper decelerates to 4 pixels/update, 3 pixels/update, etc. until he stands still and then accelerates slowly right. Right now we don’t know if a whole pixel is too much or not enough, but we can fiddle around with it later till it seems right .
In case you haven’t tried Vector math before, basically all operations are performed on both the X and the Y.
Vector2 aPosition = new Vector2(6,12); //X is set to 6 and Y to 12 aPosition /= 2; //results in X=3 (6 divided by 2) and Y=6 (12 divided by 2) aPosition += new Vector2(4, 5.3f); //results in X=7 (3 plus 4) and Y=11.3 (6 plus 5.3)
The f after the 5.3 in the third line above is necessary because decimal numbers in C# are doubles by default (larger variables than floats which Vector2 use) so we explicitly declare the 5.3 a float with the “f” suffix.
Updating Position based on Movement
To update Jumper’s Position based on the Movement variable we just add Movement to Position.
call Jumper’s Update() in every call
Changing the _jumper Sprite to a Jumper
On the SimplePlatformerGame class we had a membervariable _jumper of type Sprite:
private Sprite _jumper;
go ahead and change its type to Jumper
private Jumper _jumper;
…change the instantiation in LoadContent(), so we instantiate a Jumper instead of a Sprite:
_jumper = new Jumper(_jumperTexture, new Vector2(50, 50), _spriteBatch);
and call _jumper.Update from SimplePlatformerGame.Update
protected override void Update(GameTime gameTime) { base.Update(gameTime); _jumper.Update(gameTime); }
Now run the program and try moving Jumper around
Not what you expected?
Your Jumper will fly off screen pretty fast, because we haven’t got any collision detection in place, nor any automatic slowing down. Collision detection is next tutorial, so let’s make sure we slow Jumper down at the end of every Update().
Constraining speed of movement
If we coded a real physics engine we would have an amount of drag (wind-resistance, ground-friction, etc.) which would increase with our speed and at some point we would automatically reach a maximum speed. That would require us to introduce more math though, and the result might not be discernible to the user anyway.
And the basic rule of creating games states that:
“Don’t make it if you can fake it!”
…which we will .
So go ahead and add code to Jumper’s Update to decrease the speed by a tenth every update (right before you add Movement to Position):
Movement -= Movement * new Vector2(.1f, .1f);
Read this as “Multiply both the X and Y values of Movement by .1, subtract the result of that from Movement and store it in Movement again”.
This could also have been written as
Movement *= new Vector2(.9f, .9f);
…but I like to be able to read what I use to change something with (a tenth of the speed), as opposed to what remains (nine tenths of the motion). Suit yourself, just make sure you understand what you’re doing .
Your Update should look like this now:
public void Update(GameTime gameTime) { KeyboardState keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left)) { Movement += new Vector2(-1, 0); } if (keyboardState.IsKeyDown(Keys.Right)) { Movement += new Vector2(1, 0); } if (keyboardState.IsKeyDown(Keys.Up)) { Movement += new Vector2(0, -1); } Movement *= Movement * new Vector2(.1f, .1f); Position += Movement; }
Hit F5 and try it again … muuuuch better! (I hope? …otherwise go over your code again. If something is not working, set a breakpoint and look at the math for each Update() to see what is happening).
Smoothing movement
Though you may not notice it right now your Update is called 60 times a second. In a perfect world this would be every 1/60th of a second. But (and I hate to break it to you) it’s not a perfect world!
We may have 3/60ths of a second between two Updates and then two Updates right after each other with close to a millisecond interval to catch up. So what do we do? We compensate!
We just add the elapsed time into our math, so an Update with 1/60th uses 1/60th of the movement for a complete second, and an Update which takes three times that amount of time also performs a three times longer move (it’s a “teleport” – remember? ).
We can get the elapsed time since last Update() in the gameTime parameter in the Update method. So let’s add that to our equation. Change the last line of Update() to:
Position += Movement * (float)gameTime.ElapsedGameTime.TotalMilliseconds;
the (float) is necessary because TotalMilliseconds is a double, which we can’t multiply with a Vector2. So we cast it (change it) to a float before doing the math.
Try it again – and you’ll see that movement gets waaayy to fast (for me at least ). So divide those TotalMilliseconds with something until you’re happy. I ended up with this which suited me fine:
Position += Movement *(float)gameTime.ElapsedGameTime.TotalMilliseconds/15;
Cleaning your code
Our Update is already six lines of code and growing, let’s prune it back by using some Clean Code.
If you were a regular Joe coding along, you would add comments to your Update method and it would end up like this:
public void Update(GameTime gameTime) { //get the keyboard state KeyboardState keyboardState = Keyboard.GetState(); //change movement to reflect the keypresses if (keyboardState.IsKeyDown(Keys.Left)) { Movement += new Vector2(-1, 0); } if (keyboardState.IsKeyDown(Keys.Right)) { Movement += new Vector2(1, 0); } if (keyboardState.IsKeyDown(Keys.Up)) { Movement += new Vector2(0, -1); } //simulate friction Movement -= Movement * new Vector2(.1f, .1f); //update position based on movement Position += Movement * (float)gameTime.ElapsedGameTime.TotalMilliseconds / 15; }
BUT … since you’re NOT a regular Joe, you have realized through years and years of practical experience that:
- Comments lie! Not always, but they get out of sync with the code as we update it (which we invariably will) and forget to modify the comments to reflect the changes
- When we return to our own code (or read other people’s code) we don’t want to sift through lines and lines to find what we need, to be able to correct an error or modify functionality
So you quickly move the code from Update() into three methods
public void Update(GameTime gameTime) { CheckKeyboardAndUpdateMovement(); SimulateFriction(); UpdatePositionBasedOnMovement(gameTime); } private void CheckKeyboardAndUpdateMovement() { KeyboardState keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Left)) { Movement += new Vector2(-1, 0); } if (keyboardState.IsKeyDown(Keys.Right)) { Movement += new Vector2(1, 0); } if (keyboardState.IsKeyDown(Keys.Up)) { Movement += new Vector2(0, -1); } } private void SimulateFriction() { Movement -= Movement * new Vector2(.1f, .1f); } private void UpdatePositionBasedOnMovement(GameTime gameTime) { Position += Movement * (float)gameTime.ElapsedGameTime.TotalMilliseconds / 15; }
The benefits of this approach are
- You don’t need to keep comments in sync, and code never lies – it does what it says
- You already have a very readable description of what Update() does inside Update(), just by reading the code.
- You have a structure in Update() which makes it easy to build upon it when things get more complicated (and they will )
- You don’t need to read every line to understand what it does, if you need to change something, go to that method
- You don’t need to spend time writing comments – just stay productive and CODE!
A few pointers on Vector2
There are a few constants on the Vector2 struct which are nice to know
- Vector2.One is equivalent to new Vector2(1,1)
- Vector2.Zero is equivalent to new Vector2(0,0)
- Vector2.UnitX is equivalent to new Vector2(1,0)
- Vector2.UnitY is equivalent to new Vector2(0,1)
These are nice to know because they increase readability (once you know what they are), and they are easy to modify movement with.
Stop movement:
Movement = Vector2.Zero;
Constrain to horizontal movement by multiplying X with one and Y with zero:
Movement *= Vector2.UnitX;
Constrain to vertical movement by multiplying X with zero and Y with one:
Movement *= Vector2.UnitX;
What we’ve covered
Now hopefully you’ve store inside that pretty little noggin’ of yours, that
- Movement in computergames is “teleportation”, not real movement
- To best let the user change movement, we get the state of the controller (Keyboard in this case), we modify the movement we want to perform (accelerate, brake, block, slow, etc.) and then we update the position based on the movement
- We use the elapsed time in our movement math, to compensate for computers doing other things while we’re playing our game
- We can make our code self commenting by using the Clean Code principles.
The code for the project so far
Is available here.
Next up: Collision detection
In the fourth part, we will look at how to stop our Jumper when he collides with something.
May 8th, 2015 at 16:53
Love how the Jumper just goes up and never comes down like "I'm too good for this partial-game, BYE!!!"
February 2nd, 2017 at 14:42
Great tutorial helping me a lot but parts of the example code are wrong. It says
Movement *= Movement
thoughout the steps but it changes to
Movement -= Movement
at the clean up which is the correct version