12. Example Games

What We Will Cover


Illuminations

Questions from last class or the Reading?

Midterm Questions?

  • Midterm 2 statistics from Canvas
  • Code that does not compile contains an error
  • When given the tools to compile, your code must compile for a good score
  • How do you make a program with problems compile?
  • Post Midterm 2 Survey

Homework Questions?

Last Day to Withdraw

  • Last day to drop a full-term section with a grade of "W": 11/16/2019
  • See Admissions and Records: Dates and Deadlines
  • Grade calculation "what-if" available in Canvas Grades
  • Remember that there are many ways to earn XP
  • Approximate remaining XP available (NOT counting extra credit):
    Lab 11: 14
    Q11:    20
    Lab 12: 14
    Lab 13: 14
    Q13:    20
    Lab 14: 14
    Q14:    46
    Final: 350
    ----------
    Total  492 approx.
    

12.1: Making Sprites

Learner Outcomes

At the end of the lesson the student will be able to:

  • Describe the core mechanics of a platform game
  • Work with an abstract class
  • Create player characters from the Sprite class
  • Control Player movement
  • Manage player jumping, falling and landing

12.1.1: Platform Games

  • Today we look at how to make a platform game
  • A platform game requires the player to jump to and from suspended platforms
  • Also, the player may jump over obstacles or onto enemies
  • As we will see, jumping is a key part of platform games

Example Platformer

  • Here is a sequence from the game Wonder Boy (from Wikipedia)

    Wonder Boy platform sequence

  • What do you see the player doing in this platform game? Click to show answer
  • These actions that a player can make, and the rules governing them, are called game mechanics
  • In a platform game, jumping is a key activity -- a core game mechanic
  • Computer video games control the game mechanics through the programming of the game
  • Thus a platformer game provides the user with ways to walk, run and jump (among other actions)
  • To make a platformer game we will need to program ways to walk, run and jump
  • We will start constructing a platformer game in this lesson Click for link

Example Scenario

  • Looking at the classes we have:
    • GameManager: sets up the world and controls the game
    • Platform: something a character can stand on
    • Sprite: smooth moving superclass for sprites
  • We will take a closer look at Sprite in the next section

Check Yourself

  1. What a player can do and rules that define their actions are called ________ ________.
  2. In a computer video game, the actions a player can make and the rules of a game are controlled by the computer ________.
  3. What are the typical core mechanics of a platformer game? Click to show answer

12.1.2: Creating an Abstract Sprite

  • For a platform game, we need figures that will walk, run and jump
  • In games and computer graphics, characters are often called sprites
  • Greenfoot has an Actor class that handles most of the functions of a sprite
  • However, we want a sprite that moves more smoothly than the standard Actor
  • We will need smoother movement for jumping and enhanced animation, which we will discuss later
  • To enable smoother moving we start with a simple sprite class
  • The sprite class will include some parts of SmoothMover and Vector
  • Also, the class will include other methods common to all the characters in our platform game

Abstract Classes

  • When designing a program using inheritance, we often create a superclass that we do not want instantiated (created)
  • Instead, we only want subclasses of this class to be instantiated
  • The superclass contains code common to all the subclasses but should not be instantiated
  • In this case Sprite is abstract and cannot create an object
  • What would a Sprite object look like anyway?
  • What other classes have we seen before that are abstract?

Coding an Abstract Class

  • We can make any class abstract by adding the keyword abstract
  • Declaring a class abstract means that we cannot instantiate an object of the class
  • Instead, we must subclass the abstract superclass to make use of its operations
  • When a project starts we decide which actions should be common to a group of related classes
  • We develop these actions as methods and put the methods into an abstract superclass
  • Methods specific to a subclass remain in the subclass
  • As a project develops we may add more methods to the abstract superclass
  • What are common character actions in a platformer game? Click to show answer

Check Yourself

  1. You cannot create an object from a class that is declared ________.
  2. To write a class that from which you cannot create an object, use the keyword ________.
  3. True or false: an abstract superclass contains common methods shared by its subclasses.

12.1.3: Creating a Player Character

  • Our Sprite class is good for creating characters in a game
  • We can subclass the Sprite class to create either player or non-player characters (NPCs)
  • In this section we look at how to create player characters

Coding a Simple Player Class

  • We create a player character by subclassing Sprite
  • After subclassing Sprite, look at the code for the Player class and see the following class header:
    public class Player extends Sprite
    
  • The keyword extends tells us that our Player is a subclass of Sprite
  • Player inherits all the public members of Sprite
  • By using the methods of Sprite, we make the code for Player -- simpler!
  • In addition, we can add other characters to the game by simply subclassing Sprite
  • Inheritance allows us to reuse the code from Sprite in other classes

Inheritance hierarchy

Exercise: Add a Player Character (4m)

  1. Download the following scenario file, save it to a convenient location like the Desktop and unzip the file.

    Scenario file: platformer0.gfar or platformer0.zip.

  2. Double-click the project.greenfoot file in the unzipped folder to start Greenfoot and open the scenario.
  3. Right-click Sprite and add a player character named Player using the standing.gif image.

    Be careful of spelling to reduce coding problems in later exercises.

  4. Open the editor for the GameManager class and add a player to the end of the constructor with code like:
    Player player = new Player();
    addObject(player, getWidth() / 2, 209);
    
  5. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. In the class signature of the subclass, the keyword that identifies the superclass is ________.
    1. extends
    2. super
    3. super()
    4. inherits
  2. True or false: The purpose of inheritance is to reuse code.
  3. True or false: a subclass inherits only the public members of a superclass.
  4. True or false: inheritance allows subclasses to be simpler.

12.1.4: Controlling Movement

  • Now let us talk about moving the player character
  • Remember from lesson 2.2.1 that every object in a game has a position
  • We track the position in x and y screen coordinates
  • If we change the position at a certain rate of speed, the player seems to move
  • In lesson 3.5.2, we added player controls to our first game using the method:
    static boolean isKeyDown(String keyName)
    
  • We can use this same method to add player movement to our platformer scenario
  • The following example code shows one way of implementing the keypress-checking code for our Player

Example Code to Control the Player Character

public void checkKeys()
{
    if (Greenfoot.isKeyDown("right"))
    {
        setVelocityX(2.25);
    }
    else if (Greenfoot.isKeyDown("left"))
    {
        setVelocityX(-2.25);
    }
    else
    {
        setVelocityX(0.0); // stop moving
    }
}

Multiple Alternatives

  • Notice the structure of the if-else statements
  • The second if statement is nested in the else clause of the first if statement
  • Also, that last else clause if part of the second if statement
  • We use this structure to allow us to make a single choice among three alternatives
  • This is a common programming pattern when we want only one result from a series of test conditions

Exercise: Player Control (4m)

  1. Start Greenfoot and open the scenario from the last Exercise: Add a Player Character.
  2. Open the editor for the Player class and add the following method:
    public void checkKeys()
    {
        if (Greenfoot.isKeyDown("right"))
        {
            setVelocityX(2.25);
        }
        else if (Greenfoot.isKeyDown("left"))
        {
            setVelocityX(-2.25);
        }
        else
        {
            setVelocityX(0.0); // stop moving
        }
    }
    
  3. Call the checkKeys() method from the act() method.
  4. In addition, add a call to move() at the end of the act() method.
    public void act()
    {
        checkKeys();
        move();
    }
    
  5. Test the player control by running the scenario and moving the player.
  6. Save your updated scenario as we will be adding to it as the lesson continues.
  7. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. True or false: you can nest if statements in the if clause, the else clause, or both.
  2. In the example code above, the test condition of the first if-statement must be ________ before the second if-statement executes.
  3. True or false: in the example code above, the player only stops moving if neither the right nor left cursor keys are pressed.
  4. What is another way to write the if-statements to control player characters?

12.1.5: Gravity and Falling

  • We still need to add the ability of our player character to jump and fall
  • Objects at the earths surface fall because of gravity
  • To add realism to our game, we need to simulate gravity

Gravity

  • Gravity is a force of attraction between objects with mass
  • Newton's famous formula for gravity is shown in the following image from Wikipedia:

    F=G(m1 x m2)/r^2

  • Since the Earth's gravity is huge compared to that of a person, we can ignore the mass of the smaller object
  • Ignoring the smaller object does not significantly change the force of attraction
  • Thus we can simplify the equation for the force of gravity to:
    F = m * G
  • Where:
    • F: is the force
    • m: mass of Earth
    • G: gravitational constant

Falling

  • The distance that an object moves as it falls near the Earth is given by the formula:

    formula for distance while falling

  • Where:
    • g = 9.81 m/s2 = 32.2 ft/s2 (Earth's gravitational force)
    • t = elapsed time
  • Gravity only acts in the vertical (y) direction near the Earth

Calculating Gravity in a Game

  • To apply the gravity formula to a game, we need to keep in mind:
    • Time is measured in game cycles
    • Vertical speed (dy) is updated each game cycle
    • The gravitational constant is measured in pixels moved per game cycle
  • By calculating vertical speed every single unit of time, t = 1 and we can eliminate the t variable
  • We then multiply ½ × g (gravity) to create a single constant:
    public static final double GRAVITY = 0.7;
    
  • Thus, in programming terms, we write something like:
    velocityY = velocityY + GRAVITY;
    
  • Where:
    • velocityY is the current velocity in pixels per scenario cycle
    • GRAVITY is a gravitational constant that works for our game
  • Then we add velocityY to the y-position each game cycle to update the position
    y = y + velocityY;
    
  • Notice that y increases by velocityY every game cycle

Exercise: Adding Gravity (5m)

  1. Start Greenfoot and open the scenario from the last Exercise: Player Control
  2. Open the editor for the Player class and add a gravitational constant named GRAVITY like the following:
    public static final double GRAVITY = 0.7;
    
  3. In the Player class, add a method named applyGravity() like the following:
    public void applyGravity()
    {
        double velocityY = getVelocityY() + GRAVITY;
        setVelocityY(velocityY);
    }
    
  4. Add a call to applyGravity() in the act() method.
    public void act()
    {
        checkKeys();
        move();
        applyGravity();
    }
    
  5. Test the gravity by running the scenario in slow motion and watching how the player falls.

    Notice that we can still use the player controls to move while falling. Horizontal movement while falling is often allowed in platformer games.

  6. Save your updated scenario as we will be adding to it as the lesson continues.
  7. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. ________ is a force of attraction between objects with mass.
  2. When falling near the Earth, ________ only acts in the vertical direction.
  3. True or false: As you fall toward the ground, your speed gets faster every second that passes.
  4. When simulating falling, velocityY increases every game cycle due to ________.

12.1.6: Landing on Platforms

  • When a player falls they need to stop at some point
  • Traditionally in a platform game, a player stops falling when reaching a platform

Platforms

  • Platforms are objects in the game that a player can move and jump upon
  • In our example scenario, we use a Platform class to build platforms
  • The Platform class just contains an image for the platform, like:

    tile image

  • We simply place Platform objects anywhere we want a platform
  • If we open the editor, we see there is very little code in the Platform class

Landing

  • When the player is falling and nears a platform, we need to do two things:
    1. Move the player exactly to the top of the platform
    2. Set velocityY = 0 so the player stops falling
  • To detect when a player is nearing a platform, we use the Actor method:
    Actor getOneObjectAtOffset(int dx, int dy, Class cls)
    
  • Where:
    • dx: how far to look in the x-direction
    • dy: how far to look in the y-direction
    • cls: the type of object to look for (null for all objects)
  • Remember that we ran across this method before in lesson 8.1.3
  • To make use of the method, we write code like:
    Actor a = getOneObjectAtOffset(0, lookY, Platform.class);
    
  • If a Platform object exists within lookY distance below our player, the method returns the object
  • Otherwise, the method returns null (which means "no object")
  • We use the returned value in an if statement to decide our course of action
    1. If there is no Platform object (null is returned) then the character keeps falling
    2. If the character is near a platform then we move the character to the platform and stop falling

How Far Will We Fall?

  • To use the getOneObjectAtOffset() method effectively, we need to calculate lookY
  • To calculate lookY, we first calculate how far the player will fall during the current cycle :
    int lookY = velocityY + GRAVITY; // need more
    
  • However, Greenfoot measures location from the center of an Actor

    Falling

  • Thus we must add half the height of the sprite to the distance we check:
    int lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
    

Checking the Vertical

  • 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);
}

Exercise: Landing on a Platform (4m)

  1. Start Greenfoot and open the scenario from the last Exercise: Adding Gravity.
  2. 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
        }
    }
    
  3. Change the act() method to call checkVertical() instead of applyGravity().
    public void act()
    {
        checkKeys();
        move();
        checkVertical();
    }
    
  4. 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.

  5. 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);
    }
    
  6. 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().

  7. Save your updated scenario as we will be adding to it as the lesson continues.
  8. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. In platform games, players stop falling when they reach a ________.
  2. To detect if the character is nearing a platform, call the Greenfoot Actor method ________.
  3. True or false: to see if we are nearing a platform, we first calculate how far we will fall in one scenario cycle.
  4. The distance a character will fall is given by the formula ________.
    1. velocityY * GRAVITY
    2. velocityY * velocityY
    3. velocityY + GRAVITY
    4. velocityY + GRAVITY * GRAVITY

12.1.7: Jumping

  • While we can fall and land, we still need to add an important part of platformer games: jumping
  • Jumping is more complicated in that we cannot jump while in midair

    However, some platform games do allow double jumps, where the player character can jump again while at the top of a previous jump.

  • To control when the player can jump, we add a boolean instance variable to the Player class:
    private boolean canJump = true;
    
  • Then we add a method to control jumping like:
    public void jump()
    {
        if (canJump)
        {
            setVelocityY(-14);
            canJump = false;
        }
    }
    
  • Then when our player lands we re-enable jumping in the checkVertical() method:
    setVelocityY(0.0);
    canJump = true; // enable jumping
    
  • Finally, we add the following player control in checkKeys() method to start the jumping:
    if ((Greenfoot.isKeyDown("space") || Greenfoot.isKeyDown("up")))
    {
        jump();
    }
    
  • The space key is traditional for jumping though we can easily add more
  • Notice that the player is still able to walk through platforms
  • In the next section we discuss how to check for horizontal collision

Bumping Our Head

  • The current checkVertical() method only checks downwards
    int lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
    
  • This means that our player can jump through a platform
  • To prevent this problem, we add the ability to check for collisions while rising as well as falling
  • In checkVertical() we change our lookY calculation as follows:
    int lookY = 0;
    if (velocityY > 0) // falling
    {
        lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
    }
    else // rising
    {
        lookY = (int) (velocityY + GRAVITY - getHeight() / 2);
    }
    
  • To prevent jumping while bumping our head in checkVertical() we change our canJump to:
    setVelocityY(0.0);
    if (velocityY > 0)
    {
        canJump = true; // enable jumping
    }
    

Exercise: Add Jumping (4m)

  1. Start Greenfoot and open the scenario from the last Exercise: Landing on a Platform
  2. Open the editor for the Player class and add a boolean instance variable named canJump like:
    private boolean canJump = true;
    

    Usually instance variables are added at the top of a class, thought they may be added anywhere in the class but outside methods.

  3. Add the following method to Player for jumping:
    public void jump()
    {
        if (canJump)
        {
            setVelocityY(-14);
            canJump = false;
        }
    }
    
  4. In the checkVertical() method, add code to re-enable jumping after the setVelocityY(0.0); line:
    setVelocityY(0.0); // after this code
    if (velocityY > 0)
    {
        canJump = true; // enable jumping
    }
    
  5. Finally, add a jump control at the end of checkKeys() to control jumping:
    if ((Greenfoot.isKeyDown("space") || Greenfoot.isKeyDown("up")))
    {
        jump();
    }
    
  6. Test the jumping code by running the scenario, pressing the space bar and observing the jumping.

    Notice that we can jump up through platforms.

  7. In checkVertical(), replace the current lookY calculation:
    int lookY = (int) (velocityY + GRAVITY + getHeight() / 2); // replace this
    
    with the following:
    int lookY = 0;
    if (velocityY > 0) // falling
    {
        lookY = (int) (velocityY + GRAVITY + getHeight() / 2);
    }
    else // rising
    {
        lookY = (int) (velocityY + GRAVITY - getHeight() / 2);
    }
    
  8. There is no need to save the platformer0 scenario as we will start with platformer1 in the next exercise.
  9. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. True or false: players should be able to jump at any time.
  2. We control jumping with a ________ variable.
  3. Setting a negative y-velocity means our player character moves ________.
    1. left
    2. right
    3. up
    4. down
  4. After the character jumps, we re-enable jumping in the ________ method.

12.1.8: Horizontal Collisions

  • We use a similar approach for horizontal collisions as we did for vertical collisions:
    1. Calculate how far our player will move horizontally during the game cycle
    2. Check to see if a tile is within the distance the player will move
    3. If no collision is imminent then keep moving
    4. Otherwise, move to contact the tile and set velocity to zero
  • We can add these steps in the checkHorizontal() method of the Player class

Calculating the Look Ahead Distance

  • To detect if our player will collide with an object, we use the same Actor method:
    getOneObjectAtOffset(int dx, int dy, Class cls)
    
  • However, this time we will provide a value for dx rather than dy
  • The distance to look ahead depends on if we are moving left or right
    int lookX = 0;
    if (velocityX < 0)
    {
        lookX = (int) velocityX - getWidth() / 2;
    }
    else
    {
        lookX = (int) velocityX + getWidth() / 2;
    }
    
  • Remember that Greenfoot measures location from the center of an Actor

    Looking ahead

  • Thus we need to add half the width of the image to the lookX distance

Checking for a Collision

  • Once we have the correct values for lookX, we call the method:
    Actor a = getOneObjectAtOffset(lookX, 0, Platform.class);
    
  • If the method returns null, no object is in range and we keep moving left or right
    if (a != null) {
        moveToContactHorizontal(a);
        setVelocityX(0.0); // stop moving
    }
    
  • If the method returns an object, a collision is imminent
  • We then move to contact the actor and stop our movement

Check Yourself

  1. Given a velocityX, an Actor moves horizontally ________ during a cycle.
  2. Half the width of an actor's image is added to the look-ahead distance because Greenfoot tracks the coordinates of an actor from its ________.
  3. True or false: an actor needs to stop moving when it makes contact with a platform or wall so it does not penetrate the surface.
  4. True or false: when no horizontal collision is detected, an actor keeps moving.

Exercise 12.1: Horizontal Collisions (10m)

In this exercise, we do explore how to add horizontal collision checking to the Player class.

Specifications

  1. Download the Platformer1 scenario file and save it in the scenario folder of Greenfoot.

    Scenario file: platformer1.gfar or platformer1.zip.

    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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. To finish writing moveToContactHorizontal() add the following code in order:
    1. Sum half the values of the target and player widths:
      int w2 = (getWidth() + target.getImage().getWidth()) / 2; // sum half-widths
      
    2. Next calculate the new x-coordinate of the player:
      int newX = 0;
      if (target.getX() > getX()) // above
      {
          newX = target.getX() - w2;
      }
      else // below
      {
          newX = target.getX() + w2;
      }
      
    3. Finally, set the player in its new x-coordinate:
      setLocation(newX, getY());
      
  7. To finish writing checkCollisionHorizontal() add the following code in order:
    1. First get the current velocity
      double velocityX = getVelocityX();
      if (velocityX == 0) return;
      
    2. 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
      }
      
    3. Finally check for a collision and take action if detected:
      Actor a = getOneObjectAtOffset(lookX, 0, Platform.class);
      if (a != null) {
          moveToContactHorizontal(a);
          stopMoving();
      }
      
  8. 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.

  9. Save your Player.java file to submit to Canvas as part of assignment 10.

Listing of Horizontal Collision Methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * 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.

12.1.9: Review

Answer these questions to check your understanding. You can find more information by following the links after the question.

  1. In platform games, characters traditionally walk, run and jump on ______________. (12.1.1)
  2. What are core game mechanics of a platform game? (12.1.1)
  3. An image or animation, like a character, that is added to a larger scene is called a(n) ______________. (12.1.2)
  4. To make a class from which you cannot instantiate an object use the keyword _________. (12.1.2)
  5. The purpose of inheritance is to ________ code. (12.1.3)
  6. In the following code, when is the else clause executed? (12.1.4)
    if (Greenfoot.isKeyDown("right"))
    {
        moveRight();
    }
    else if (Greenfoot.isKeyDown("left"))
    {
        moveLeft();
    }
    else
    {
        stopMoving();
    }
    
    1. When the player presses the "right" key
    2. When the player presses the "left" key
    3. When the player presses both the "right" and "left" keys
    4. When the player does not press either the "right" or "left" keys
  7. ____________ is a force of attraction between objects with mass. (12.1.5)
  8. True or false: As you fall toward the ground, your speed gets faster every second that passes. (12.1.5)
  9. Which of the following statements simulates gravity in a platform game? (12.1.5)
    1. y = velocityY + GRAVITY;
    2. y = y + GRAVITY;
    3. velocityY = velocityY + GRAVITY;
      y = y + dy;
    4. y = y + GRAVITY;
      dy = dy + y
  10. When a character is closer to a platform than it can fall in one game cycle, it is time to __________. (12.1.6)
  11. What method can we use to detect if we are nearing a platform? (12.1.6)
  12. True or false: to see if we are nearing a platform, we first calculate how far we will fall in one scenario cycle. (12.1.6)

12.2: Animating Sprite Images

Learner Outcomes

At the end of the lesson the student will be able to:

  • Discuss animation using images
  • Write code to alternate between images
  • Use a list to shorten alternation code
  • Keep track of directions

12.2.1: Alternating Images

  • 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

Run image 1 Run image 2 Run image 3 Run image 4 Run image 5 Run image 6 Run image 7
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? Click to show answer
  • 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? Click to show answer
  • 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? Click to show answer

Check Yourself

  1. True or false: To show animations we need to display multiple images.
  2. True or false: we delay changing images by using a counting variable.
  3. What are the three steps to controlling timing over multiple scenario cycles? Click to show answer

12.2.2: Alternating Images in a List

  • 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

  1. Given the following code, the index selected for the image array is ________.
    count = 4;
    image[count / 3];
    
    1. 0
    2. 1
    3. 2
    4. 3
  2. True or false: the number three in the above code is the duration (number of game cycles) to display each image.
  3. 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.

12.2.3: Caching an Animation

  • Now let us look at applying image animation for our platformer: Click for link
  • To animate images with arrays and loops, the first step is to load the images into a GreenfootImage object
  • The usual way to make an image is by loading the image from a file (see lesson 4.3.3)
    standImage = new GreenfootImage("standing.gif");
    
  • Oftentimes we will reuse images, such as when we have multiple actors of the same type in a scenario
  • If we want to reuse images then we cache them as we did with the Proton Wave images in lesson 10.2.2
  • The images are stored in "gif" files and named "runX.gif", where the X is the number of the image

    Run 1 Run 2 Run 3 Run 4 Run 5 Run 6 Run 7
    run1 run2 run3 run4 run5 run6 run7

  • The cached images are stored in a static array
    private static GreenfootImage[] runRightImages;
    
  • After constructing the array, we use a loop with "i" as the index variable to make file names:
    for (int i = 0; i < RUN_COUNT; i++)
    {
        String fileName = "run" + (i + 1) + ".gif";
        // other code ommitted
    
  • The file name is used to load the image, which is then assigned to an array element
  • We can see the complete method below

Method for Caching Images

private static final int RUN_COUNT = 7;
private static GreenfootImage[] runRightImages;

public static void initializeImages()
{
    if (runRightImages == null)
    {
        runRightImages = new GreenfootImage[RUN_COUNT];
        for (int i = 0; i < RUN_COUNT; i++)
        {
            String fileName = "run" + (i + 1) + ".gif";
            runRightImages[i] = new GreenfootImage(fileName);
        }
    }
}

Exercise: Cache Images (5m)

  1. Start Greenfoot and open the platformer1 scenario from the last exercise.

    If you have not saved the previous exercises, then download the following scenario.

    Scenario file: platformer2.gfar or platformer2.zip.

  2. Open the editor for the Player class and add an array to cache the images like:
    private static final int RUN_COUNT = 7;
    private static GreenfootImage[] runRightImages;
    
  3. In addition, add the following method to the Player class:
    public static void initializeImages()
    {
        if (runRightImages == null)
        {
            runRightImages = new GreenfootImage[RUN_COUNT];
            for (int i = 0; i < RUN_COUNT; i++)
            {
                String fileName = "run" + (i + 1) + ".gif";
                runRightImages[i] = new GreenfootImage(fileName);
            }
        }
    }
    
  4. Type the code to add a default constructor to the Player class that calls the initializeImages() method.
    public Player()
    {
        initializeImages();
    }
    
  5. Compile the code to verify it is correct.
  6. Add an instance variable to count the scenario cycles:
    private int runCount = 0;
    
  7. In addition, add a constant to set the duration of each image:
    private static final int DURATION = 5;
    
  8. Finally, update the moveRight() method as follows:
    public void moveRight()
    {
        setVelocityX(MOVE_SPEED);
        runCount++;
        if (runCount >= runRightImages.length * DURATION)
        {
            runCount = 0; // reset the count
        }
        setImage(runRightImages[runCount / DURATION]);
    }
    

    If you do not have an existing moveRight() method, start these instructions again using the platformer2 scenario above.

  9. Compile and run your scenario to verify the player runs to the right.

    We will add moving to the left in the next section.

  10. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. True or false: the first step of creating an animation is to load the images into GreenfootImage objects.
  2. If images will be reused we should ________ the images.
  3. What are the image files loaded with the following code?
    private static final int COUNT = 3;
    private static GreenfootImage[] fooImages;
    //...
    for (int i = 0; i < COUNT; i++)
    {
        String fileName = "foo" + (i + 1) + ".gif";
        fooImages[i] = new GreenfootImage(fileName);
    }
    

12.2.4: Flipping Images

  • We loaded images for the player to display while moving to the right
  • We also need images for moving left
  • Rather than preparing and loading more images, we will use a method of GreenfootImage to create the left-facing images

    mirrorHorizontally(): flip the image around the x-axis

  • We may copy our right-facing images and then flip the copies to make left-facing images
  • For example, if we had two instance variables to hold images:
    private static GreenfootImage faceRight;
    private static GreenfootImage faceLeft;
    
  • 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

  1. 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
  2. Second line constructs a new array the same size as the parameter array
  3. The for-loop accesses each array element using an index i to place images in the flipped array
  4. Make a copy of the original image and save in the flipped array
  5. Mirror the image horizontally
  6. The method returns the array of flipped images

Exercise: Flip Images (4m)

  1. 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.

  2. Open the editor for the Player class and add an array to cache flipped images like:
    private static GreenfootImage[] runLeftImages;
    
  3. 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;
    }
    
  4. In the initializeImages() method, add the following method call just after the for-loop:
    runLeftImages = flipImages(runRightImages);
    
  5. 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]);
    }
    
  6. Compile the code to verify the syntax is correct.
  7. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. To flip an image around the x-axis, call the GreenfootImage method ________.
  2. Calling mirrorHorizontally() to create left-facing images is generally better than loading from a file because ________.
    1. the program requires less memory
    2. the program runs faster
    3. you do not need to prepare additional image files
    4. both b and c
  3. 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.

12.2.5: Adding Direction

  • We need to display images when our player stops moving, either left or right
  • Standing facing left Standing facing right
    Left Right

  • We create the images in initializeImages() method as before
  • However, we need to decide which image to display when stopping
  • To make a decision, we need to track the direction our player moves
  • To track direction, we add the following variables to Player:
    private static final int RIGHT = 0;
    private static final int LEFT = 1;
    private int direction = RIGHT;
    
  • Then in our move methods we add code to track the direction like:
    direction = RIGHT;
    
    or
    direction = LEFT;
    
  • Then in stopMoving() we test which direction to face and display the correct image:
    if (direction == LEFT)
    {
        setImage(faceLeft);
    }
    else
    {
        setImage(faceRight);
    }
    
  • We add code to track direction in the following exercise

Exercise 12.2: Track the Player Direction (7m)

In this exercise, we track the direction of our player character and add standing images.

Specifications

  1. If you have not already completed Exercise: Cache Images, do so now.
  2. In addition, if you have not already completed Exercise: Flip Images, do so now.
  3. Add instance variables for the images to display when our player stops moving:
    private static GreenfootImage faceRight;
    private static GreenfootImage faceLeft;
    
  4. 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();
    
  5. 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;
    
  6. In moveRight() add the following line of code:
    direction = RIGHT;
    
  7. Similarly, in moveLeft() add the following line of code:
    direction = LEFT;
    
  8. Update the stopMoving() method as follows:
    public void stopMoving()
    {
        setVelocityX(0.0);
        if (direction == LEFT)
        {
            setImage(faceLeft);
        }
        else
        {
            setImage(faceRight);
        }
    }
    
  9. 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.

  10. 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.

12.2.6: Review

  • To provide animation, we need to save images in variables
    private GreenfootImage image1, image2, image3;
    
  • Rather than saving images in multiple variables, we save them in an array
    private GreenfootImage[] images;
    
  • Storing images in an array provides several benefits
  • For example, if we name our images cleverly, we can load them using a for-loop and string concatenation like:
    for (int i = 0; i < COUNT; i++)
    {
        String fileName = "run" + (i + 1) + ".gif";
        runRightImages[i] = new GreenfootImage(fileName);
    }
    
  • Another benefit of arrays is that we can make use of the array index to select which image to display
    count++;
    if (count >= image.length * 3)
    {
        count = 0; // reset the count
    }
    setImage(image[count / 3]);
    
  • To save drawing and processing time, we were able to use the a method of GreenfootImage to create the left-facing images

    mirrorHorizontally(): flip the image around the x-axis

  • 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.

  1. To show animations we need to display images. (12.2.1)
  2. True or false: As the number of images to display increases, the code to chose which image to display becomes more complex to write. (12.2.1)
  3. . (12.2.3)
  4. Calling mirrorHorizontally() to create left-facing images is generally better than loading from a file because: (12.2.3)
    1. The program requires less memory
    2. The program runs faster
    3. You do not need to prepare additional image files
    4. Both b and c
  5. If each of seven image takes 0.1 seconds to load from a file, about how much faster does your program run by copying and flipping images than by loading the images? (12.2.3)
  6. The direction of a player is tracked using an integer variable. What is another way to track the state of the player's direction? (12.2.4)

12.3: Creating New Worlds

Learner Outcomes

At the end of the lesson the student will be able to:

  • Describe the advantages of using tile maps
  • Discuss the important characteristics of tiles
  • Construct tile maps

12.3.1: Tiles and Tile Maps

  • A common approach for developing game worlds is to use tile maps
  • A tile map is a technique for generating large graphics from a number of smaller graphics
  • The technique is still popular today because:
    1. Tile maps save main computer memory
    2. Tile maps increase real-time rendering performance
  • Tile maps are not intended to produce background images for a world
  • Instead, tiles are intended as objects in a game world like doors or platforms

Laying Out Tile Maps

  • A tile map breaks down a game world into a grid as shown in the diagram below
  • Each cell in the grid contains either a small tile object or nothing

  • Tile-based maps are like creating a game world with building blocks
  • We use only a few different blocks but we can use as many of each block as we want
  • Each tile object in the map contains a reference to the image that belongs in a cell of the grid
  • When Greenfoot draws the tile at a location, it takes the referenced image and draws it on the screen
  • The program thus uses the same image at many places on the screen
  • By reusing the image many times, the program needs to store fewer images
  • Thus we only need a few small images to draw an entire scenario

Check Yourself

  1. For the above image, and ignoring the grid lines, you need ________ image(s) to draw the tile map.
  2. In the above tile map, the same image is displayed ________ times.
  3. Of the following statements, "________" correctly describes how a tile maps saves memory space.
    1. tiles are arranged in a two-dimensional grid to create a larger image.
    2. not all grids locations are filled with tile objects.
    3. the program draws the same small image at multiple places in the world
    4. each tile object is smaller than the entire world.

More Information

12.3.2: Creating Tiles

  • We may draw our own tiles or use a tile set (or sprite sheet)
  • A tile set is a collection of tiles arranged within a single larger image
  • Many artists have created tile sets and a few are available royalty free
  • The following tile set was provided for free by Ari Feldman (see: SpriteLib)

Example Tile Set from SpriteLib GPL

Example tile set

Creating Your Tiles

  • Notice that all the tiles are the same size
  • Having all tiles the same size makes drawing a tile map easier
  • To extract tiles from a tile map (or draw our own) we need an image processing program like:
  • To get us started, I cut out a few tiles from the above tile sheet
  • We will use these tiles to develop a tile map in the next sections

Tiles for Example Tile Map

Rock tile
rock-block.png
Left tile
rock-left.png
Middle tile
rock-mid.png
Right tile
rock-right.png
Pole
rock-pole.png

Check Yourself

  1. True or false: tiles in a tile set should all be the same size.
  2. To draw your own tiles, you need an ________ processing program.
  3. To extract tiles from a tile set, you need an ________ processing program.

More Information

12.3.3: Making a Tile Map

  • Let us look at how to create a tile map
  • We start by downloading and installing the Platformer 3 scenario:
  • Scenario file: platformer3.gfar

  • Looking at the classes we have:
    • 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? Click to show answer
  • 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
    }
    
  • We discussed arrays of strings in lesson 7.3.3 and how to use loops with arrays in lesson 7.3.4
  • To understand how the makeMapRow() method works, we need to discuss characters and string methods

Check Yourself

  1. The tile "engine" for the example map is located in the ________ class.
  2. The tile map is contained in the ________ variable.
  3. The two methods that convert the map into tiles are ________ and ________.

12.3.4: Characters and String Methods

  • Recall from lesson 3.5.1 that a string is a sequence of characters
  • We specify string variables using the String data type
  • We can work with character data as well as strings
  • A character is a single letter, digit or special symbol (like: !@#$%)
  • We specify a character literal by enclosing it in single quotes (')
    • The quote marks are not part of the data
  • For example:
    'a'   'b'   'Z'   '3'   'q'   '$'   '*'
    
  • To declare a character variable, use the char data type like:
    char letter;
    letter = 'A';
    char letterB = 'B';
    
  • As shown in the example above, we can assign single character to a char variable

String Methods

Some Methods We Need

  • length(): Returns the number of characters in a string
    String str = "Hello";
    int numChars = str.length(); // returns 5
    
  • charAt(index): Returns a single character at index
    String str = "Hello, World!";
    char letterH = str.charAt(0); // returns 'H'
    char letterE = str.charAt(1); // returns 'e'
    char letterL = str.charAt(2); // returns 'l'
    
  • The index numbers in a string start at 0; thus the last character is always: str.length() - 1
    H e l l o , W o r l d !
    0 1 2 3 4 5 6 7 8 9 10 11 12

Method makeMapRow()

private void makeMapRow(int y, String[] MAP)
{
    int tileY = topY + TILE_HEIGHT * y;  // 1
    String row = MAP[y];  // 2
    for (int x = 0; x < row.length(); x++) // 3
    {
        int tileX = leftX + TILE_WIDTH * x; // 4
        char tileType = row.charAt(x);  // 5
        if (tileType == 'B')  // 6
        {
            addObject(new Platform(), tileX, tileY); // 7
        }
        else if (tileType == 'L') // 8
        {
            addObject(new PlatformLeft(), tileX, tileY); // 9
        }
        // other tile types omitted...  // 10
        else if (tileType != ' ')  // 11
        {
            System.out.println("Wrong tile type: " + tileType); // 12
        }
    }
}

Notes on the Code

In makeMapRow() each character of the string is converted into a tile and added to the world.

  1. Calculate the y-coordinate of the first tile in the row
  2. Extract string from array
  3. Loop through each character of the string
  4. Calculate the x-coordinate of the tile
  5. Extract the character (char) from the string
  6. Test if the extracted char equals a 'B'
  7. If the test is true then add a Platform object to the world
  8. If the test was false, test again to see if the char is an 'L'
  9. If the test is true then add a PlatformLeft object
  10. Keep testing for other tile symbols and adding them to the world when found
  11. Space characters are ignored so test if any remaining char is not a space
  12. If the test is true then print an error message

Check Yourself

  1. The type of delimiter used to enclose single characters of type char is the ________.
  2. True or false: "A" and 'A' are the same type.
  3. Write the code to declare a char variable named myChar and assign it a value of 'A'.

    answer

  4. Write the code to get the first character from the string named "test" and store it in a previously declared variable named myChar.

    answer

  5. The method used to determine the length of a string is ________.
  6. True or false: the 'B' tile is a block of type Platform.
  7. True or false: no platform tile is constructed for a ' ' (space).
  8. If you want an unbounded world, set the fourth parameter of the World constructor, which is called using super(), to ________.
  9. The pixel coordinates of a tile are given by the formulas ________.
    1. (TILE_WIDTH * x, TILE_HEIGHT * y)
    2. (leftX + TILE_WIDTH * x, topY + TILE_HEIGHT * y)
    3. (x + TILE_WIDTH * leftX, y + TILE_HEIGHT * topY)
    4. (leftX + TILE_HEIGHT * x, topY + TILE_WIDTH * y)

12.3.5: Changing the World

  • Recall that the MAP array specifies which tiles are placed and where they are placed
  • The MAP array provides us with a simple map editor
  • A map editor is a tool used to design levels, maps, campaigns, and virtual worlds for a video game
  • To update our world, we rearrange the tiles in MAP
  • If we want more types of tiles:
    1. add an image of the correct size to the images folder
    2. Assign a char to represent the image
    3. Add the char to the map
  • For an example, we add another block
  • Example: rock-block2.png

Example Tile to Add to MAP

Rock tile 2
rock-block2.png

Adding a New Tile

  • To add a new tile, we first select an image of the correct size (32 x 32 pixels)
  • For an example, we choose the rock-block2.png image above
  • We make a Platform subclass named Block with the new image
  • Then we assign a char for the actor like 'b' and add the following code to makeMapRow()
    else if (tileType == 'b')
    {
        addObject(new Block(), tileX, tileY);
    }
    
  • Then we update the MAP to use our new block 'b'

ASCII and Unicode

  • Notice that upper and lower case characters have different values
  • This is because each character is represented by a different number
  • Starting in 1963, most computers used a character set known as ASCII
  • However, ASCII has since been extended to include all languages
  • The extended ASCII code is called Unicode

More Information

  • ASCII: ASCII Introduction and History from Harvard CS50.tv (8:02)
  • ASCII: Wikipedia article
  • World: Greenfoot API documentation

Check Yourself

  1. True or false: 'A' and 'a' are the same character.

Exercise 12.3: Construct a Game World (10m)

In this exercise, we construct a new game world.

Specifications

  1. Download the Platformer3 scenario file and save it in a convenient location like the Desktop
  2. Scenario file: platformer3.gfar

  3. 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.

  4. 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);
    
  5. Change the size of the world and recompile the GameManager to see the effects.
  6. 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.

  7. In the GameManager class, locate the MAP array.
  8. Take about five minutes to rearrange the tile map and create a different world.
  9. 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.

12.3.6: Review

Answer these questions to check your understanding. You can find more information by following the links after the question.

  1. What are three core game mechanics of a platform game? (12.3.1)
  2. True or false: A for-loop is often used to iterate a fixed number of times. (12.3.1)
  3. True or false: A class can be of multiple types. (12.3.1)
  4. True or false: Casting is a way to specify a more precise type than one the compiler knows about. (12.3.1)
  5. Generating a large graphics from a number of smaller graphics is known as a _______ map. (12.3.2)
  6. True or false: Tile map images are arranged in a grid to make a larger image. (12.3.2)
  7. Which of the following statements correctly describes how a tile maps saves memory space: (12.3.2)
    1. Tiles are arranged in a two-dimensional grid to create a larger image.
    2. Not all grids locations are filled with tile objects.
    3. The program draws the same small image at multiple places in the world.
    4. Each tile object is smaller than the entire world.
  8. True or false: Tiles in a tile map should all be the same size. (12.3.3)
  9. True or false: The example tile map is implemented as an array of strings. (12.3.4)
  10. What type of delimiter is used to enclose single characters? (12.3.5)
  11. What is the data type of variables that store a single character? (12.3.5)
  12. Are "A" and 'A' the same? Why or why not? (12.3.5)
  13. Given a string variable named test, what code do you write to determine its length? (12.3.5)
  14. Given a string variable named test, what code do you write to get the first character? (12.3.5)
  15. To add a new tile to the tile map engine, what changes must be made? (12.3.6)

12.4: Scrolling Worlds

Learner Outcomes

At the end of the lesson the student will be able to:

  • Discuss how to scroll a scenario
  • Write code to horizontally scroll a scenario

12.4.1: Scrolling the Background

  • In a 2D platform game, the map of an entire level is usually several screens wide
  • Here is a diagram of the concept:

  • The player stays centered on the screen
  • Even though the main character appears to move, the player is actually stationary
  • The other actors, including the tiles, move together (scroll) in the background
  • This scrolling background gives the illusion that the player is moving

Scrolling Backgrounds

  • We will create a scrolling background from the Platformer 3 scenario
  • The first step is to allow the actors to move outside the screen area
  • To allow actors outside the world, we change the GameManager
    super(800, 600, 1, false);
    
  • To scroll the background we need to move all the actors of the scenario every scenario cycle except the player
  • To move all the actors, we first call the method:

    getObjects(Class cls): Returns a list of all the objects in the world.

  • The method returns a List of Actor objects currently in the world
  • Since we will work with a List, we need to import the List interface
    import java.util.List;
    
  • After getting all the actors in the world, we use a loop to access each one in the list
  • After accessing an actor, we move it in the opposite direction the player is moving
  • We calculate the amount to move eacg object from the velocity of the player character
    double dx = getVelocityX();
    ...
    scrollHorizontal(dx);
    
  • Then in scrollHorizontal() we move each object in the world
  • Since the world is scrolling, we need to keep the Player in the center of the screen

World Method to Horizontally Scroll All Actors

public void scrollHorizontal(double dx) // 1
{
    List<Actor> actors = getObjects(null);  // 2
    for (Actor a : actors)  // 3
    {
        int moveX = (int) Math.round(a.getX() - dx); // 4
        a.setLocation(moveX, a.getY());  // 5
    }
}

Notes on the Code

  1. Method scrollHorizontal() parameter dx is the distance moved by the player
  2. Get a list of all objects in the world
  3. Loop through each object in the world
  4. Calculate the distance to move each object
  5. Set each object in its new location

Exercise: Scrolling the World (4m)

  1. Download the Platformer3 scenario file and save it in a convenient location like the Desktop, if you do not already have the scenario.
  2. Scenario file: platformer3.gfar

  3. Double-click the project.greenfoot file in the unzipped folder to start Greenfoot and open the scenario.
  4. Open the editor for the GameManager class and add an import statement for List:
    import java.util.List;
    
  5. Change the call to super to allow actors to move outside the world.
    super(800, 600, 1, false);
    
  6. Add the scrollHorizontal() method to the GameManager class.
    public void scrollHorizontal(double dx)
    {
        List<Actor> actors = getObjects(null);
        for (Actor a : actors)
        {
            int moveX = (int) Math.round(a.getX() - dx);
            a.setLocation(moveX, a.getY());
        }
    }
    
  7. Compile the program to verify you implemented the new method correctly.
  8. To test the method, right-click on the world background and select void scrollHorizontal(double dx), entering an argument on the amount to scroll.

    You should see the background scroll by the number of pixels you enter. Negative numbers scroll right and positive numbers scroll left.

  9. Save your updated scenario as we will be adding to it as the lesson continues.
  10. When finished, please help those around you.
  11. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. True or false: Most platformer games have maps that are several screen's wide.
  2. True or false: a scrolling background makes the player appear to move even though the actor stays in the center of the screen.
  3. The methods of the World class called to get a list of all the objects in the world is ________.
    1. findObjects()
    2. allObjects()
    3. getObjects()
    4. listObjects()
  4. To access all the objects in a list we code a ________ statement.
  5. The number of pixels to move the background is calculated from the velocity of the ________.
  6. If there are 42 actors in the world, the following code will move how many of them? (answer)
    List<Actor> actors = getObjects(null);
    for (Actor a : actors)
    {
        int moveX = (int) Math.round(a.getX() - getVelocityX());
        a.setLocation(moveX, a.getY());
    }
    
  7. True or false: In the above code, to scroll vertically we need to change the y-argument, currently "a.getY()", of the setLocation() method call.

12.4.2: Overriding Methods

  • In our scenario, we have a move() method in the Sprite class
    public void move()
    {
        setLocation(x + velocityX, y + velocityY);
    }
    
  • We have been calling this move() method from the Sprite subclasses including Player
    public void act()
    {
        // ... other method calls omitted
        move();
    }
    
  • However, the move() method in Sprite is not correct for scrolling
  • To implement scrolling, we need to change the way the move() method works in the Player class
  • We can change the way a superclass method works by overriding the method
  • We change the behavior of the Player by writing a new move() method in Player

Method move() in the Sprite Superclass

public void move()
{
    setLocation(x + velocityX, y + velocityY);
}

Method move() in the Player Subclass

@Override public void move()  // 1
{
    super.move();  // 2
    double dx = getVelocityX();  // 3
    GameManager w = (GameManager) getWorld();  // 4
    if (w == null || dx == 0)
    {
        return;
    }
    w.scrollHorizontal(dx);  // 5
    setLocation(w.getWidth() / 2, getY());  // 6
}

Notes on move() in Player Subclass

  1. Method move() with @Override compiler directive
  2. Call the superclass's move() method
  3. Get the Player's velocity to calculate distance to move
  4. Get a reference to the current world
  5. Call the scrollHorizontal() method of the world with the player's velocity
  6. Keep the Player in the center of the world

Comparing the Superclass and Subclass Methods

  • Notice that both the superclass and subclass have a move() method
  • Also both methods have the same name and the same parameters (that is, none)
  • Thus, they have the same signature
  • The method from a subclass overrides the superclass method if it has the same signature
  • Note that a subclass method will NOT override a superclass method if the parameters are different

Using @Override

  • The compiler will verify that a method is overridden if the @Override annotation is included
  • This prevents common mistakes of misspelling a method name or not matching the parameters
  • Without the @Override annotation the compiler will not report a problem
  • Another benefit is the annotation makes our code easier to understand since it is more obvious which methods are overridden

Calling a Superclass Method

  • Note that we can call an overridden method of the superclass from a subclass
  • To do so, we use the keyword super like:
    super.move();
  • Sometimes people confuse overriding with overloading
  • Here is a table showing the similarities and differences

Overriding Verses Overloading

Overriding Overloading
  • Same method name
  • Same method name
  • Same signature
  • One method in superclass, one in subclass
  • Different signature
  • Both methods can be in same class

Exercise: Overriding move() (3m)

  1. 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.

  2. 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
    }
    
  3. 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.

  4. Save your updated scenario as we will be adding to it as the lesson continues.
  5. Be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. True or false: Overriding is when a method in the superclass and subclass have the same name.
  2. To have the compiler warn us when we do not override a method correctly add an ________ annotation before the method declaration.
  3. To call a method of the superclass that has been overridden, use the keyword ________ and a dot before the method name.
    1. super
    2. this
    3. sub
    4. that

12.4.3: The instanceof Operator

  • 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 instanceof Operator

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

  1. True or false: we can test to see if a certain object is part of an inheritance hierarchy.
  2. To test if an object is part of an inheritance hierarchy we can use the ________ operator.
  3. 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 {}
    
    1. A will be printed
    2. AB will be printed
    3. ABC will be printed
    4. ABCD will be printed

Exercise 12.4: Special Treatment (5m)

  1. Start Greenfoot and open the scenario from the last exercise.

    If you did not keep the scenario, then complete Exercise: Overriding move() now and then continue with these instructions.

  2. Open the editor for the GameManager class and update the scrollHorizontal() method as follows:
    public void scrollHorizontal(double dx)
    {
        List<Actor> actors = getObjects(null);
        for (Actor a : actors)
        {
            if (a instanceof Player)
            {
                // Allow smooth moving of Players
                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());
            }
        }
    }
    
  3. Compile and run the program to verify you implemented the changes correctly.

    When the player moves, the character should stay horizontally centered on the screen while the background tiles scroll.

  4. Be prepared to answer the following Check Yourself questions when called upon.

12.4.4: Review

  • 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.

  1. True or false: Most platformer games have maps that are several screen's wide. (12.4.1)
  2. 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)
  3. The methods of the World class called to get a list of all the objects in the world is ________. (12.4.1)
    1. findObjects()
    2. allObjects()
    3. getObjects()
    4. listObjects()
  4. To access all the objects in a list we code a ________ statement. (12.4.1)
  5. The number of pixels to move the background is calculated from the ________ character's velocity. (12.4.1)
  6. True or false: Overriding is when a method in the superclass and subclass have the same name. (12.4.2)
  7. To have the compiler warn us when we do not override a method correctly add an ________ annotation before the method declaration. (12.4.2)
  8. To call a method of the superclass that has been overridden, use the keyword ________ and a dot before the method name. (12.4.2)
    1. super
    2. this
    3. sub
    4. that
  9. True or false: sometimes we need to test if a certain class is part of a list. (12.4.3)
  10. To test if an object is a certain class type we can use the ________ operator. (12.4.3)
  11. 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 {}
    
    1. A will be printed
    2. AB will be printed
    3. ABC will be printed
    4. ABCD will be printed

Wrap Up

Due Next:
Q11: Project Proposal (11/14/19)
Lab 12: Initial Prototype (11/19/19)
  • When class is over, please shut down your computer
  • You may complete unfinished lesson exercises at any time before the due date.
Last Updated: December 01 2019 @14:41:24