Every scenario cycle we must check our falling and decide if we need to land
We manage the vertical movement by adding a checkVertical() method shown below
Then we call the checkVertical() method from the act() method instead of calling applyGravity()
Method checkVertical()
public void checkVertical()
{
double velocityY = getVelocityY();
int lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
// Check for vertical collision this cycle
Actor a = getOneObjectAtOffset(0, lookY, Platform.class);
if (a == null)
{
applyGravity();
}
else
{
// TODO: move to vertical contact
setVelocityY(0.0); // stop falling
}
}
Moving to Vertical Contact
With the above method, we see the player slows down just before landing
For a quicker landing, we add a move to vertical contact method
In the method we first cacluate the distance from the target to the player
int h2 = (target.getImage().getHeight() + getHeight()) / 2;
Then we test to see if we are rising or falling and calculate our y-distance
Finally, we set our location with the new y-coordinate value
Method moveToContactVertical()
public void moveToContactVertical(Actor target)
{
int h2 = (target.getImage().getHeight() + getHeight()) / 2;
int newY = 0;
if (target.getY() > getY()) // test up or down
{
newY = target.getY() - h2; // up
}
else
{
newY = target.getY() + h2; // down
}
setLocation(getX(), newY);
}
Open the editor for the Player class and add the checkVertical() method.
public void checkVertical()
{
double velocityY = getVelocityY();
int lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
// Check for vertical collision this cycle
Actor a = getOneObjectAtOffset(0, lookY, Platform.class);
if (a == null)
{
applyGravity();
}
else
{
// TODO: move to vertical contact
setVelocityY(0.0); // stop falling
}
}
Change the act() method to call checkVertical() instead of applyGravity().
public void act()
{
checkKeys();
move();
checkVertical();
}
Test the landing by running the scenario, moving the player off the platform and watching how the player lands.
You will see a slow down before landing.
Add moveToContactVertical() method to the Player class
public void moveToContactVertical(Actor target)
{
int h2 = (target.getImage().getHeight() + getHeight()) / 2;
int newY = 0;
if (target.getY() > getY()) // test up or down
{
newY = target.getY() - h2; // up
}
else
{
newY = target.getY() + h2; // down
}
setLocation(getX(), newY);
}
Now add checkVertical() to the Player class.
public void checkVertical()
{
double velocityY = getVelocityY();
int lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
// Check for vertical collision this cycle
Actor a = getOneObjectAtOffset(0, lookY, Platform.class);
if (a == null)
{
applyGravity();
}
else
{
moveToContactVertical(a);
setVelocityY(0.0); // stop falling
}
}
Notice how checkVertical() calls moveToContactVertical().
Save your updated scenario as we will be adding to it as the lesson continues.
Be prepared to answer the following Check Yourself questions when called upon.
Check Yourself
In platform games, players stop falling when they reach a ________.
To detect if the character is nearing a platform, call the Greenfoot Actor method ________.
True or false: to see if we are nearing a platform, we first calculate how far we will fall in one scenario cycle.
The distance a character will fall is given by the formula ________.
It is important to use this scenario as it has minor enhancements from the exercises we completed previously:
Added two new constants
Added three simple methods: moveRight(), moveLeft() and stopMoving()
Why? So you do not have to bother with these minor details.
Start Greenfoot and open the scenario.
If you downloaded the zip file, unzip it and find the un-zipped folder. Inside the folder, double-click the project or project.greenfoot file to open the scenario.
Open the Player class and add a stub for the following method:
public void checkCollisionHorizontal() { }
A method stub is a syntactically correct method with few or no programming statements inside the method.
In the act() method, call checkCollisionHorizontal() just before the call to checkVertical().
Check to verify your code still compiles. If you have problems ask a guild member or the instructor for help.
In the Player class, add a stub for the following method:
public void moveToContactHorizontal(Actor target) { }
Check to verify your code still compiles. If you have problems ask a classmate or the instructor for help.
To finish writing moveToContactHorizontal() add the following code in order:
Sum half the values of the target and player widths:
int w2 = (getWidth() + target.getImage().getWidth()) / 2; // sum half-widths
Next calculate the new x-coordinate of the player:
To finish writing checkCollisionHorizontal() add the following code in order:
First get the current velocity
double velocityX = getVelocityX();
if (velocityX == 0) return;
Next calculate the look-ahead value in the x-direction:
int lookX = 0;
if (velocityX < 0)
{
lookX = (int) velocityX - getWidth() / 2; // left
}
else
{
lookX = (int) velocityX + getWidth() / 2; // right
}
Finally check for a collision and take action if detected:
Actor a = getOneObjectAtOffset(lookX, 0, Platform.class);
if (a != null) {
moveToContactHorizontal(a);
stopMoving();
}
Test if your Player character stops when running into a platform sideways.
If the character does not stop correctly, check your code against the listing below. If it still does not work, ask a classmate of the instructor for help.
Save your Player.java file to submit to Canvas as part of assignment 10.
/**
* Check for a horizontal collision with a platform.
*/
public void checkCollisionHorizontal() {
double velocityX = getVelocityX();
if (velocityX == 0) return;
int lookX = 0;
if (velocityX < 0)
{
lookX = (int) velocityX - getWidth() / 2;
}
else
{
lookX = (int) velocityX + getWidth() / 2;
}
Actor a = getOneObjectAtOffset(lookX, 0, Platform.class);
if (a != null) {
moveToContactHorizontal(a);
stopMoving();
}
}
/**
* Move this Actor into contact with the specified Actor in the
* horizontal (x) direction.
*
* @param target The target this sprite is approaching.
*/
public void moveToContactHorizontal(Actor target)
{
int w2 = (getWidth() + target.getImage().getWidth()) / 2;
int newX = 0;
if (target.getX() > getX())
{
newX = target.getX() - w2;
}
else
{
newX = target.getX() + w2;
}
setLocation(newX, getY());
}
As time permits, read the following sections and be prepared to answer the Check Yourself questions in the section: 12.1.8: Review.
One problem we see with our character is that his arms and legs do not move
In other words, our character is not animated
To produce an animation, we need a series of images depicting movement
For example, we could use the following images to show our character running to the right
Images for Running Toward the Right
run1
run2
run3
run4
run5
run6
run7
To run to the left we would need another seven images
What other sets of images might we need for our character in a platform game?
Walking -- both right and left
Jumping! -- both right and left
Standing
Climbing
How many images total might we need for our main character?
To control all of these images, we need to improve our animation techniques
Displaying the Images
To improve our animation techniques, let us start with the easier bugs4.gfar scenario from lesson 4
The technique we learned in lesson 4.3.5 was to use an instance variable for each image:
public class Bug extends Actor
{
private GreenfootImage image1, image2, image3;
private int flowersEaten;
public Bug()
{
image1 = new GreenfootImage("bug-a.gif");
image2 = new GreenfootImage("bug-b.gif");
image3 = new GreenfootImage("bug-c.gif");
setImage(image1);
}
// other methods omitted
}
Then in lesson 4.3.6 we wrote code in updateImage() to change the image to display:
public void updateImage()
{
if (getImage() == image1)
{
setImage(image2);
}
else if (getImage() == image2)
{
setImage(image3);
}
else
{
setImage(image1);
}
}
Sometimes we need to slow down how fast we switch the images to match other parts of the scenario
What technique have we used to control timing over multiple scenario cycles?
Add an instance variable to hold counting values and initialize the variable
private int count = 0;
Change the value of the count in each call to the act() method
count++;
Use an if-statement to test if the count has reached its goal
if (count > SOME_NUMBER)
We can add the counter to the image switching code like:
private int count = 0; // initialize counter
public void updateImage()
{
count++; // increment counter
if (count > 3 && getImage() == image1)
{
setImage(image2);
}
else if (count > 6 && getImage() == image2)
{
setImage(image3);
}
else if (count > 9)
{
setImage(image1);
count = 1; // reset the counter
}
}
How could we simplify the above code?
public void updateImage()
{
count++; // increment counter
if (count > 9)
{
setImage(image1);
count = 1; // reset the counter
}
else if (count > 6)
{
setImage(image3);
}
else if (count > 3)
{
setImage(image2);
}
}
We can control which image to display using only the count variable
Check Yourself
True or false: To show animations we need to display multiple images.
True or false: we delay changing images by using a counting variable.
What are the three steps to controlling timing over multiple scenario cycles?
Add an instance variable to hold counting values and initialize the variable
private int count = 0;
Change the value of the count during every call to act().
count++;
Use an if-statement to test if the count has reached its goal
As a compact way to store images, we may set up an array like this:
private GreenfootImage[] image;
public Bug()
{
image = new GreenfootImage[3];
image[0] = new GreenfootImage("bug-a.gif");
image[1] = new GreenfootImage("bug-b.gif");
image[2] = new GreenfootImage("bug-c.gif");
setImage(image[0]);
}
With a list, we can simplify our code to change images over multiple cycles
For instance, we may calculate the index of the current image from the count dividing by 3:
int index = count / 3; // integer division
Remember that the results of integer division is to truncate (throw away) the decimal part
Thus we end up with an integer index that changes more slowly
By calculating the image index, we may simplify our code like this:
public void updateImage()
{
count++;
if (count >= image.length * 3)
{
count = 0; // reset the count
}
setImage(image[count / 3]);
}
Adding More Images
If we used a series of if-statements to decode which image to display, our code becomes longer with every added image
However, by using a list, the code to select an image does not change
To add or delete images, we simply change the images on the list
Check Yourself
Given the following code, the index selected for the image array is ________.
count = 4;
image[count / 3];
0
1
2
3
True or false: the number three in the above code is the duration (number of game cycles) to display each image.
True or false: By calculating the index of an array of images, instead of using if-statements, the code to chose which image to display in an animation becomes more complex to write as the number of images increases.
Then in the initializeImages() method we could add code like:
// Load the right-facing image
faceRight = new GreenfootImage("standing.gif");
// Make the left-facing image
faceLeft = new GreenfootImage(faceRight);
faceLeft.mirrorHorizontally();
As well as being easier to prepare artwork, this approach runs faster because we load fewer images
Reading from main memory (RAM) is about 1000x faster than from disk and about 20x faster than SSD
Flipping Arrays of Images
We will need to flip arrays of images like the runRightImages to make left-facing images
To make the process easier we write a method named flipImages()
We call the method from initializeImages() like this:
runLeftImages = flipImages(runRightImages);
Method to Flip Images
private static GreenfootImage[] flipImages(GreenfootImage[] imgs) // 1
{
GreenfootImage[] flipped = new GreenfootImage[imgs.length]; // 2
for (int i = 0; i < imgs.length; i++) // 3
{
flipped[i] = new GreenfootImage(imgs[i]); // 4
flipped[i].mirrorHorizontally(); // 5
}
return flipped; // 6
}
Notes on the Code
First line is the method header (signature) named flipImages with:
A static keyword limiting the method to using static or local variables
GreenfootImage[] return type to return an array of flipped images
GreenfootImage[] imgs parameter from which to copy images
Second line constructs a new array the same size as the parameter array
The for-loop accesses each array element using an index i to place images in the flipped array
Make a copy of the original image and save in the flipped array
Start Greenfoot and open the scenario from the last exercise.
If you did not keep the scenario, then complete Exercise: Cache Images now and then continue with these instructions.
Open the editor for the Player class and add an array to cache flipped images like:
private static GreenfootImage[] runLeftImages;
In the Player class, add the flipImages() method.
private static GreenfootImage[] flipImages(GreenfootImage[] imgs)
{
GreenfootImage[] flipped = new GreenfootImage[imgs.length];
for (int i = 0; i < imgs.length; i++)
{
flipped[i] = new GreenfootImage(imgs[i]);
flipped[i].mirrorHorizontally();
}
return flipped;
}
In the initializeImages() method, add the following method call just after the for-loop:
runLeftImages = flipImages(runRightImages);
Update the moveLeft() method as follows:
public void moveLeft()
{
setVelocityX(-MOVE_SPEED);
runCount++;
if (runCount >= runLeftImages.length * DURATION)
{
runCount = 0; // reset the count
}
setImage(runLeftImages[runCount / DURATION]);
}
Compile the code to verify the syntax is correct.
Be prepared to answer the following Check Yourself questions when called upon.
Check Yourself
To flip an image around the x-axis, call the GreenfootImage method ________.
Calling mirrorHorizontally() to create left-facing images is generally better than loading from a file because ________.
the program requires less memory
the program runs faster
you do not need to prepare additional image files
both b and c
If each of seven image files takes 0.01 seconds to load, your program runs about ________ faster by copying and flipping images rather than by loading all images.
Inside the if-statement of the initializeImages() method, add the following code to create the images:
// Load the right-facing image
faceRight = new GreenfootImage("standing.gif");
// Make the left-facing image
faceLeft = new GreenfootImage(faceRight);
faceLeft.mirrorHorizontally();
To track direction, add the following constants and instance variable to Player:
private static final int RIGHT = 0;
private static final int LEFT = 1;
private int direction = RIGHT;
In moveRight() add the following line of code:
direction = RIGHT;
Similarly, in moveLeft() add the following line of code:
direction = LEFT;
Update the stopMoving() method as follows:
public void stopMoving()
{
setVelocityX(0.0);
if (direction == LEFT)
{
setImage(faceLeft);
}
else
{
setImage(faceRight);
}
}
Compile and run your scenario to verify all the changes work well and the player faces the correct direction when stopping.
If you have problems, ask a classmate or the instructor for help as needed.
Save your latest Player.java file to submit to Canvas as part of assignment 10.
As time permits, read the following sections and be prepared to answer the Check Yourself questions in the section: 12.2.6: Review.
For example, we added code to create two images from one as follows:
// Load the right-facing image
faceRight = new GreenfootImage("standing.gif");
// Make the left-facing image from faceRight
faceLeft = new GreenfootImage(faceRight); // make copy of faceRight
faceLeft.mirrorHorizontally(); // flip face right
To track direction, so we knew when to display some images, we added the following variables to Player:
private static final int RIGHT = 0;
private static final int LEFT = 1;
private int direction = RIGHT;
Then we used an if-statement to select the image to draw
Answer these questions to check your understanding. You can find more information by following the links after the question.
GameManager: constructs the world and controls the game
Platform (and subclasses): something a character can stand on
Sprite: smooth moving superclass for sprites
Player: the player controlled sprite
Sign: a background image (not a platform)
The tiles for the map are subclasses of Platform
The tile "engine" is in the GameManager class
Constructing the Example Tile Map
Lets open the editor for the GameManager class and look at the source code
What has changed compared to the last scenario?
Removed arrays PLATFORM_X and PLATFORM_Y
Added a new array named MAP
New instance variables: leftX, topY, player
Changes in method createPlatforms()
New method makeMapRow()
New Player instance variable named george
Most of the changes have to do with drawing the tile map
The tile map is specified in the array MAP:
private static final String[] MAP =
{
"B B",
"B B",
"B B",
"B LMMMMR LMR B",
"B P P LB",
"B P P B",
"B P LMMMR B",
"BMMR P P B",
"B PMR P LMB",
"B P P B",
"B P P B",
"B LMMMMMMMMMMMR B",
"B P B",
"B P B",
"BMMMR P LMMB",
"B P B",
"B P ",
"B P ",
"MMMMMMMMMMMMMMMR LMMMMM"
};
Each of the letters represents a type of tile:
B: block
L: left platform
M: middle platform
R: right platform
P: pole
By rearranging the letters in the MAP, we change the displayed tile map and its world
The GameManager constructor sets up the world for the scenario
public GameManager()
{
super(800, 600, 1, false); // allow actors to move outside world
leftX = TILE_WIDTH / 2; // left-most tile position
topY = TILE_HEIGHT - getHeight() % TILE_HEIGHT; // top-most tile position
createPlatforms(MAP); // start drawing the map
}
The createPlatform() method uses a loop to iterate through each string in the array MAP
private void createPlatforms(String[] MAP)
{
for (int y = 0; y < MAP.length; y++) // each string in the map
{
makeMapRow(y, MAP); // lay out each row
}
addObject(new Sign(), TILE_WIDTH * 23, getHeight() - TILE_HEIGHT * 2);
addObject(george, getWidth() / 2, 0); // add player last
}
If you downloaded the zip file, unzip it and find the un-zipped folder. Inside the folder, double-click the project or project.greenfoot file to open the scenario.
Open the GameManager class, locate the command that specifies the size of the Greenfoot world and try changing the size of the world.
super(800, 600, 1);
Change the size of the world and recompile the GameManager to see the effects.
Restore the GameManager class to its original condition.
Compile the class and verify there are no errors. Resolve any errors you find, getting help from a classmate or the instructor as needed.
In the GameManager class, locate the MAP array.
Take about five minutes to rearrange the tile map and create a different world.
Save your GameManager.java file to submit to Canvas as part of assignment 10.
As time permits, read the following sections and be prepared to answer the Check Yourself questions in the section: 12.3.6: Review.
Start Greenfoot and open the scenario from the last exercise.
If you did not keep the scenario, then complete Exercise: Scrolling the World now and then continue with these instructions.
Open the editor for the Player class and add the following move() method.
@Override public void move()
{
super.move();
double dx = getVelocityX();
GameManager w = (GameManager) getWorld();
if (w == null || dx == 0)
{
return;
}
w.scrollHorizontal(dx);
setLocation(w.getWidth() / 2, getY()); // stay in horizontal center
}
Compile and run the program to verify you implemented the changes correctly.
When the player moves, the character is supposed to stay horizontally centered on the screen while the background tiles scroll. However, the player gets off center. We will address this problem in the following sections. If you have problems ask a guild member or the instructor for help as needed.
Save your updated scenario as we will be adding to it as the lesson continues.
Be prepared to answer the following Check Yourself questions when called upon.
Check Yourself
True or false: Overriding is when a method in the superclass and subclass have the same name.
To have the compiler warn us when we do not override a method correctly add an ________ annotation before the method declaration.
To call a method of the superclass that has been overridden, use the keyword ________ and a dot before the method name.
Sometimes we may want to give certain types of actors special treatment
One example may be the Player character
To provide special treatment, we may test using the instanceof operator
Using the instanceof Operator
The instanceof operator checks if a test object is in the inheritance hierarchy of a specified class type
Syntax:
testObject instanceof ClassName
Where:
testObject: the object to test
ClassName: the class to compare again
Yhe instanceof test returns true if the testObject is a descendant of any ClassName
As an example for Player:
List<Actor> actors = getWorld().getObjects(null);
for (Actor a : actors)
{
if (a instanceof Player)
{
Player p = (Player) a;
// do something with p
}
// other code here
}
We may use the instanceof operator to test for Player objects in our loop and apply different movement on those objects
See the complete code of the move() method listed below, with changes in bold
Method scrollHorizontal() with instanceofOperator
public void scrollHorizontal(double dx)
{
List<Actor> actors = getObjects(null);
for (Actor a : actors)
{
if (a instanceof Player)
{
// Allow smooth moving
Player p = (Player) a;
double moveX = p.getExactX() - dx;
p.setLocation(moveX, p.getExactY());
}
else
{
int moveX = (int) Math.round(a.getX() - dx);
a.setLocation(moveX, a.getY());
}
}
}
Check Yourself
True or false: we can test to see if a certain object is part of an inheritance hierarchy.
To test if an object is part of an inheritance hierarchy we can use the ________ operator.
Given the following code, ________ will be printed when the test() method is called.
public class CodeTester {
public static void test() {
B b = new C();
A a = b;
if (a instanceof A) System.out.print("A");
if (a instanceof B) System.out.print("B");
if (a instanceof C) System.out.print("C");
if (a instanceof D) System.out.print("D");
}
}
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
In a 2D platform game, the map of an entire level is usually several screens wide
The player stays centered on the screen while the background moves
To scroll the background we move all the actors of the scenario at once while the player stays stationary
To scroll the background we first get a list of all the actors by calling the getObjects() method of World
Then we move all the actors by the amount of the player's velocity
A convenient way to implement the scrolling code in Player is to override the move() method from the Sprite class
To override a method we must have the same method declaration in the superclass and the subclass
To have the compiler verify we have override a method correctly, we add the @Override annotation before the signature
Sometimes we want to call a method of the superclass that was overridden in a subclass
To do so, we use the keyword super like:
super.move();
At times we may want to test the type of an object
To do so we can use the instanceof operator like:
List<Actor> actors = getWorld().getObjects(null);
for (Actor a : actors)
{
if (a instanceof Player)
{
Player p = (Player) a;
// do something with p
}
// other code here
}
The instanceof operator returns true is the object is in the inheritance hierarchy of the class
Check Yourself
Answer these questions to check your understanding. You can find more information by following the links after the question.
True or false: Most platformer games have maps that are several screen's wide. (12.4.1)
True or false: a scrolling background makes the player appear to move even though the actor stays in the center of the screen. (12.4.1)
The methods of the World class called to get a list of all the objects in the world is ________. (12.4.1)
findObjects()
allObjects()
getObjects()
listObjects()
To access all the objects in a list we code a ________ statement. (12.4.1)
The number of pixels to move the background is calculated from the ________ character's velocity. (12.4.1)
True or false: Overriding is when a method in the superclass and subclass have the same name. (12.4.2)
To have the compiler warn us when we do not override a method correctly add an ________ annotation before the method declaration. (12.4.2)
To call a method of the superclass that has been overridden, use the keyword ________ and a dot before the method name. (12.4.2)
super
this
sub
that
True or false: sometimes we need to test if a certain class is part of a list. (12.4.3)
To test if an object is a certain class type we can use the ________ operator. (12.4.3)
Given the following code, ________ will be printed when the test() method is called. (12.4.3)
public class CodeTester {
public static void test() {
B b = new C();
A a = b;
if (a instanceof A) System.out.print("A");
if (a instanceof B) System.out.print("B");
if (a instanceof C) System.out.print("C");
if (a instanceof D) System.out.print("D");
}
}
class A {}
class B extends A {}
class C extends B {}
class D extends C {}