This line adds half a pixel of vertical movement (downward) using the constant UnitY

(which is (0,1) you may recall :)). We may have to adjust this later, but for now we have something to work with ;).

I'm stuck!!

Now you can see that Jumper will fall whenever he is unsupported, but we also get some unwanted sideeffects, the most annoying being - we can't move once we've hit the floor :(, and sometimes we don't even hit the floor:

In situations like this one, it is a good idea to add some debug info to your game so you can see what is going on.

Adding debug info to the game

To write something onscreen you will need a SpriteFont in your content project. So go ahead and add a SpriteFont to your contentproject:

and name it "DebugFont"

Add a SpriteFont membervariable to the SimplePlatformerGame class

private SpriteFont _debugFont;

and load the font in the LoadContent method of SimplePlatformerGame

_debugFont = Content.Load<SpriteFont>("DebugFont");

Update Draw to show debug info

Then add some lines to the SimplePlatformerGame's Draw method to show where Jumper is, and where he is headed:

If you are unfamiliar with String.Format(), have a look here. Basically you pass it a string with placeholders named "{0}", "{1}", etc. for all the variables you want inserted into it. This makes it easier to read the format of the string, since formatting and data are kept separate.

You can add conversion information inside the brackets, using a separating colon.

E.g. {0:c} for currency or {0:0.0} for one decimal.

When .NET renders the result on screen you will see that the decimal separator on my screendumps is a comma, not a period, as my PC uses european (da-DK) culture :).

Now we can see what is going on

Jumper still has a desired vertical downward movement of 4.5, which makes him want to bury himself.

Q: Why is Jumper sometimes floating?

A: Same reason why we couldn't get close to the wall when moving sideways until we let go of the left/right key: the movement we're attempting would make Jumper end up inside a blocked Tile, and the Board object's HasRoomForRectangle() won't allow that move.

Q: Why can't we move sideways?

A: We haven't stopped Jumper's downward movement when he landed on the ground, which means that even if we try to move sideways, Jumper would still try to move downward and sideways.

Let's fix the floating first by improving how we handle leftover movement for Jumper.

Improving Jumper's movement

Here you can see what we want to accomplish.

In the illustration above, Jumper is moving diagonally down and left. The current Movement for Jumper wants to move him to the end of the diagonal red arrow in this Update().

We want that movement to be stopped right when he hits the horizontal row of blocks, and have the leftover motion carry over into horizontal movement, which only terminates when he hits the vertical wall of blocks a bit later.

Algorithm

What we're going to implement is a function WhereCanIGetTo on the Board class which

gets the origin and destination of a Rectangle

breaks down that movement into a number of half pixel steps

checks for every step whether it is blocked

if it is blocked, tries to carry over any diagonal movement into vertical or horizontal movement

Creating the WhereCanIGetTo() method

The reason we don't send the bounding rectangle of Jumper to the method, is that Rectangle uses ints for positioning, and we need more finegrained movement here.

We're going to do some vector math here, so if you're not used to that, have a look at this :).

Before we begin stepping along the path from origin to destination, we need some variables. These variables will be used to store

the complete movement we want to try (the distance from origin to destination)

the furthest available location we've found so far

the direction only (without distance)

the direction and length of one step

the number of steps we want to break movement into

Add some code to calculate these values. First calculate the movement from origin to destination:

Vector2 movementToTry = destination - originalPosition;

This means that if Jumper was at wants to go to (80, 120) and starts out at (100,100), the movement he wants to carry out is (80 - 100, 120 - 100) = (-20, 20), or in other words, -20 on the x-axis (meaning left) and 20 on the y-axis meaning down.

we assume that the originalPosition is in a nonblocked area, so we use the original position as the furthest possible location we have found along the path we want to travel

Vector2 furthestAvailableLocationSoFar = originalPosition;

to figure out how many steps we want to break the movement into, we multiply the length of the movement by 2 (so we approximately try once per half pixel), and add one, to make sure we at least try one step for very small movements

In the example mentioned above ((-20,20).Length() being approximately 28.3) this would work out at

(28.3 * 2) ≈ 57 + 1 = 58 small steps

And finally figure out how far one step is by dividing the entire move by the number of steps

Vector2 oneStep = movementToTry / numberOfStepsToBreakMovementInto;

Each step would be (-20,20) / 58 ≈ (-0.34, 0.34)

One small step at a time

Now that we have these values we can make a loop where we:

keep trying the next step along the movementToTry and see if we can go there. We do this by creating a Rectangle at that position and asking HasRoomForRectangle whether it is blocked

if that move was unblocked, we store that position in furthestAvailableLocationSoFar and continue

If that place is blocked, we exit the loop and return furthestAvailableLocation

Before we begin coding that, we need functionality to create a new Rectangle at a given position, so we have something to test each step with. So add a new function to the Board class which receives a Vector2 and a width and height, and creates a Rectangle at that position.

We will lose some precision in converting the floats from positionToTry to ints, but we will finetune that later.

Now we have the basic skeleton of our improved collision detection up and running. So update the WhereCanIGetTo method. I suggest you don't copy and paste the code, but code it while making sure you understand every step of it, but suit yourself .

Also update the MoveIfPossible name to MoveAsFarAsPossible, to better reflect that it is no longer a do/don't move decision, but a movement within the confines of the possible. Now your MoveIfPossible method should look like this:

Recycle leftover diagonal movement as horizontal or vertical movement

Right now our movement ends as soon as we take a step which ends in a blocked position.

What we want to do is:

If we get blocked before finishing a move:

find out if we we're moving diagonally when we got blocked, and if we were:

try to move as far horizontally and/or vertically as possible and return the farthest possible location

We can illustrate it like this: Here we hit a blocked Tile about halfway into the move the Jumper is trying to make. When that happens we check whether it is a diagonal move (neither of the X and Y part of the movement vector is zero), and then we test movement using the remaining movement along the X and Y axis.

Remember you can turn a movement vector into horizontal movement by multiplying it by Vector2.UnitX (thereby setting its movement on the y-axis to zero) and into vertical movement by multiplying it byVector2.UnitY (setting its x-axis movement to zero).

"If we get blocked before finishing a move"

The place to add code for this case is inside the else part of the loop in the WhereCanIGetTo() method

"If it is a diagonal move"

Create a boolean variable to store whether it is a diagonal move, and add it to the beginning of the else in WhereCanIGetTo(). As you can see, we store a true if neither the x- nor the y-movement is zero (think about it! :)).

To calculate the steps left, we have to subtract the step we just tried, as that moved us into a blocked area (if HasRoomForRectangle() returned true we wouldn't be down here handling all the messy details :)), and subtract the result from however many steps we were supposed to take on the entire path.

int stepsLeft = numberOfStepsToBreakMovementInto - (i - 1);

Example: We want to move 10 steps in this complete Update(). When testing step 7, we find it to be blocked, so we subtract 1 from 7 to find the last valid position (7-1 = 6), and then subtract that step from the entire trip (10 - 6 = 4) to find out how many steps we still need to try.

"try to move as far horizontally and/or vertically as possible"

We're almost there now - we can see the finishing line ... so let's perform the final sprint!

As mentioned earlier, to get only the horizontal/vertical movement part of a vector, we multiply by Vector2.UnitX or Vector2.UnitY respectively. So for each type of movement, we calculate the remaining movement in that direction, find out where we want to end up of we completed that movement by adding the remaining movement to furthestAvailableLocationSoFar.

Now we have the position to start from, and where we want to end up ... if only there were some way of finding out how far little Jumper could get to along that path...? 😉

"But there IS!" (I hear you cry!)

"Just feed those two positions right back into WhereCanIGetTo(), and it'll tell you!".

Right you are - so here is the final part of our if (isDiagonalMove) {... }

The calling of a function from itself is called recursion. In a lot of cases the calling can be nested many times deep, but we only ever call two layers deep. When calling functions recursively it is very important to have a criteria for when to stop, otherwise the program enters an infinite loop. The reason our calling stops is that the recursion is only performed when movement is diagonal, and the parameters to the second call to WhereCanIGetTo is never diagonal.

Go ahead and try it out, you will see that little Jumper no longer sticks to walls or floors, and slides right along - YAY! Go celebrate with a cup of coffee/cola/juice/water... 😀

The final version of WhereCanIGetTo

Read it through and see if there is something you still don't understand. If there is, now is the time to scroll back up and read the explanation again

This method is too long for my tastes. But simplifying it would mean having to put some of the code into other methods and passing along a lot of parameters (which is also not optimal for readability), or encapsulating the functionality in a small class. At the end of the next and final part of this tutorial, I will show you how that can be achieved.

What we've covered

After this part of the tutorial you should have learnt that

Gravity can be implemented simply by adding a downward momentum in every Update

You can benefit from adding debug information in your games to let you know why something is happening

You can reuse a method inside itself using recursion, and you should always ensure you have a stopping condition, so the program doesn't enter an infinite loop

The code so far

Next up...

In the final part of this tutorial we will have a look at how to make Jumper jump, and how to stop his Movement when he hits something.

This entry was posted
on Saturday, April 27th, 2013 at 16:46 and is filed under Uncategorized.
You can follow any responses to this entry through the RSS 2.0 feed.
Both comments and pings are currently closed.