Here are step-by-step instructions for programming a simple platform game--a side-view game in which a player moves left/right and jumps onto platforms.
Exercise 1: Starter Code
Download this code. Compile and run it. Try it out. Then look through the code to get a sense of how it works. How does the program know where to draw the player?
Exercise 2: Coordinates
Currently, the player's location is hard-coded. Let's modify the code to use variables to keep track of the player's location.
Declare instance variables of type double named x and y. (We're using doubles to allow our player to occupy positions between integer coordinates.)
In the beginning of the constructor, initialize x and y to appropriate values.
In the paintComponent method, find the line of code that draws the player at (175, 50). Modify this line to draw the player at (x, y) instead. The drawImage method requires int arguments, so you will need to pass (int)x and (int)y.
Test that the player appears, and that changing the initial values of x and y changes where the player appears.
Exercise 3: Movement
Find the infinite loop at the end of the constructor. Insert the following lines inside the loop.
x += -0.5;
y += 1;
Test that you now see the player moving. Try changing the numbers added to x and y to change the player's direction and speed.
Exercise 4: Velocity
The amount we add to x is the player's velocity in the x direction, and the amount we add to y is the player's velocity in the y direction. We want to be able to change these velocities.
Declare instance variables of type double named xvel and yvel, representing the x- and y-components of the velocity.
In the beginning of the constructor, initialize xvel and yvel to appropriate values.
In the infinite loop, replace the numbers added to x and y with xvel and yvel. Each time the body of the loop runs, x should increase by xvel and y should increase by yvel.
Test that the player moves, and that changing the initial values of xvel and yvel changes where the player's direction and speed.
Exercise 5: Keyboard
Initialize xvel and yvel to 0, so that the player does not move.
The keyPressed method is called whenever a key is pressed.
In the keyPressed method, modify the code so that pressing the left arrow key (37) subtracts 1 from x, and pressing the right arrow key (39) adds 1 to x.
You should now be able to move left and right. Try adding or subtract more than 1.
What happens when you press and hold down one of the arrow keys?
Exercise 6: Smooth Movement
Currently, whenever we press an arrow key, we change the value of x. We're going to change that now in order to make the movement smoother.
Declare a boolean instance variable for each key: leftPressed and rightPressed. A true value indicates that the key is currently being pressed.
In the beginning of the constructor, initialize leftPressed and rightPressed to false.
In the keyPressed method, instead of changing x, simply set the appropriate instance variable to true.
In the keyReleased method, set the appropriate instance variable to false.
In the infinite loop, test if each instance variable is true. When leftPressed is true, decrease x. When rightPressed is true, increase x.
The movement should feel smoother now. If it's too fast or slow, try adding/subtracting a different amount.
Exercise 7: Continued Movement
Right now, the player moves whenever we hold down a key, but this is not the correct behavior for a platformer. When we introduce jumping, we'll want the player to continue to move horizontally in whatever direction it was moving before--even when we're not pressing a key.
In the infinite loop, modify the code that runs when leftPressed is true. Instead of decreasing x, simply set xvel to a negative number. Likewise, when rightPressed is true, instead of increasing x, set xvel to a positive number.
Now, when you press an arrow key, the player should continue to drift in that direction until you press another arrow key.
Exercise 8: Friction
When you press an arrow key, we don't want the player to drift in that direction forever. Therefore, we will introduce friction to gradually slow the player down. This is simply a matter of repeatedly multiplying xvel by a constant between 0.0 and 1.0. For example, repeatedly multiplying xvel by 0.99 will largely leave xvel unchanged, corresponding to a small amount of friction, and leaving the player to drift longer. On the other hand, repeatedly multiplying xvel by 0.25 will quickly reduce the value of xvel, corresponding to a large amount of friction, and quickly stopping the player's movement.
Insert code in the infinite loop to multiply xvel by the constant of your choice.
Test that your player stops moving (eventually) when you stop pressing a key. Try multiplying xvel by different constants until you find one you like.
Exercise 9: Gravity
We wish to introduce gravity. Suppose we initialize y to 50 and yvel to 2. Then Java will draw the player at a y-coordinate of 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, etc. Yes, the player will move down the screen, but at a constant speed. It will look more like drifting than falling.
Real gravity causes objects to fall faster and faster--not at a constant speed. We can achieve this by repeatedly adding a small constant to yvel. For example, suppose we start with a yvel of 0, and then we repeatedly add 0.1 to yvel. The following table shows how the value of y changes as we do this. Notice it begins to increase faster and faster.
In the infinite loop, insert code to add a small constant to yvel.
Test that the player falls faster and faster now. Try adding different constants to yvel until you find one you like.
Exercise 9: Landing
Right now, the player falls off the screen. Let's fix that so that they land at the bottom of the screen.
In the infinite loop, insert code to test if the player's y places it below the screen. If so, set y so that the player appears at the bottom of the screen, and set yvel to 0 (to stop them from falling off the screen again).
Hint: Remember that y corresponds to the top of the player. If the screen is 400 pixels tall, and you set y to 400, then the top of the player will align with the bottom of the screen, and the rest of the player will be below the screen--which is not what you want.
Test that your player now lands on the bottom of the screen.
Exercise 10: Jumping
Making the player jump is simply a matter of assigning the correct value to yvel whenever the jump key is pressed.
In the keyPressed method, add code to test if the jump key is pressed (e.g. 32 for the space bar or 38 for the up arrow). When that key is pressed, assign an appropriate value to yvel.
Test that your player can jump now. Try assigning different values to yvel unti you find one you like.
Exercise 11: Eliminating Double-Jumps
You probably discovered that your player can jump even when they are in mid-air. We only want them to jump off a platform. We will fix this using a variable called airTime, which will keep track of how long the player has been in the air (since the last time they jumped/fell off a platform).
Declare an instance variable called airTime of type int.
At the very beginning of the infinite loop, add one to airTime.
In the infinite loop, find the code that moves the player back onto the screen when they fall below it. Whenever the player is moved back onto the screen, reset airTime to 0 (because they are now on the ground).
In the keyPressed method, only allow the player to jump when airTime is less than 3. (You might wonder why we don't simply test if airTime is 0. Allowing the player to jump when airTime is a low value helps to prevent the player from getting stuck.)
Background: Representing Platforms
There are two common ways to represent platforms, walls, obstacles, etc.:
by color: In this approach, we designate a particular color to represent a platform. We draw all the platforms offscreen in the same color. Whenever the player tries to move, we determine if specific points on the player would correspond to locations with that color in the offscreen image. We usually test several points on the player (e.g. a point at the top of their head, a point on the bottom of their feet, etc). If we don't test enough points, the player may get stuck when a platform slips between test points. This approach is best when we have weird-shaped platforms in a finite world. It tends to run faster, but it can be more challenging to code in Java.
by location: In this approach, we store the coordinates of all platforms, and we limit ourselves to rectangular platforms. Whenever the player tries to move, we loop through all the platforms and determine if any of those rectangles overlap with the rectangular outline of the player. Because this approach is easier to understand, this is the approach we will use.
Exercise 12: Platforms
In this exercise, we'll draw platforms.
Create a class representing a single Platform. It should store the x and y values of its top-left corner, along with its width and height--all as integers. The constructor should take in initial values for x, y, width, and height. Provide methods for getting the values of x, y, width, and height.
In the Game class, add an instance variable named platforms of type ArrayList, which will store a list of all the Platform objects.
In the beginning of the Game class's constructor, initialize the platforms instance variable and add 3 Platform objects with the coordinates/dimensions of your choice.
In Game's paintComponent method, loop over the platforms list. Use the Graphics object's setColor and fillRect methods to draw each platform, using the platform's coordinates and dimensions.
Test that the platforms appear on the screen. The player won't interact with them just yet.
Exercise 13: Touching One Platform
In this exercise, we will write a method to test if the player is touching (overlaps with) a given platform. To keep this simple, we will treat both the player and the platform as rectangles. Determining if two rectangles overlap can be tricky. Happily, determining that two rectangles do NOT overlap is fairly straightforward. Here's how.
If the player is to the left of the platform, then they do NOT overlap.
If the player is to the right of the platform, then they NOT not overlap.
If the player is above the platform, then they do NOT overlap.
If the player is below the platform, then they do NOT overlap.
Otherwise, the player and the platform DO overlap.
When we say that the player is to the left of the platform (for example), we mean the entire player is to the left of the entire platform, as in the image below. To determine this, we test if the right edge of the player (player's x + player's width) is less than the left edge of the platform (platform's x).
In the Game class, implement the following method, which tests if the player is touching the given platform.
private boolean isTouching(Platform p)
In the Game class's paintComponent method, find the code that draws each platform. When you draw a platform, call isTouching. If isTouching returns true, draw the platform in a different color (using fillRect) or with an outline around it (using drawRect). This will allow you to see if isTouching works correctly.
Test that platforms now appear differently whenever they overlap with the player.
Exercise 14: Touching Any Platform
Currently, we can determine if the player is touching a particular platform. We'll now write code to determine if the player is touching ANY platform (as opposed to touching NO platform).
In the Game class, implement the following method, which tests if the player is touching any platform.
private boolean isTouchingAny()
In the Game class's paintComponent method, find the code that draws the background. Call isTouchingAny. If isTouchingAny returns true, draw the background in a different color. This will allow you to see if isTouchingAny works correctly.
Test that the background color changes whenever the player overlaps with any platform.
Exercise 15: Preventing Overlap
To prevent the player from passing through platforms, we'll test if a move would cause them to touch any platform. If so, we'll simply undo that move as follows:
In the Game constructor's infinite loop, find the code that adds xvel to x. Immediately afterward, test if the player is touching any platform. If so, (1) set x back to its previous value, and (2) set xvel to 0.
In the Game constructor's infinite loop, find the code that adds yvel to y. Immediately afterward, test if the player is touching any platform. If so, (1) set y back to its previous value, (2) set yvel to 0, and (3) set airTime to 0.
You should now be able to jump onto and off of platforms. Where you go from here is up to you!