© Lee Stemkoski 2018
Lee StemkoskiJava Game Development with LibGDXhttps://doi.org/10.1007/978-1-4842-3324-5_12

12. Adventure Games

Lee Stemkoski
(1)
DEPT OF MATH & CS, ADELPHI UNIVERSITY DEPT OF MATH & CS, Garden City, New York, USA
 
This chapter features the most ambitious game project in the entire book: a combat-based adventure game named Treasure Quest , inspired by classic console games such as The Legend of Zelda. This game uses new features, such as enemy combat with two different types of weapons (a sword and an arrow), non-player characters (NPCs) with messages that depend on the state of the game (such as the number of enemies remaining), and an item-shop mechanic. Figures 12-1 and 12-2 feature screenshots of in-game combat and the item shop, respectively.
A352797_2_En_12_Fig1_HTML.jpg
Figure 12-1.
Swordfighting the Flyer enemies
A352797_2_En_12_Fig2_HTML.jpg
Figure 12-2.
Purchasing items (hearts and arrows) at the Item Shop

Game Project: Treasure Quest

Treasure Quest is an adventure game where the character, called the hero, is called upon to rid the land of flying bat-like creatures (called Flyers) and is promised a treasure chest as a reward. The hero has two methods of attacking and destroying the Flyers : swinging a sword and shooting arrows (the hero begins with three). Contact with either of these weapons will destroy a Flyer in a single hit. The hero begins with three health points . Colliding with a Flyer reduces the health of the hero by one point, and if the hero’s health reaches zero, then the hero is destroyed, and the game is over. When each Flyer is destroyed, they drop a coin, which can be collected by the hero and used to purchase additional health or arrows from an item shop. There are a pair of NPCs in the game: the Shopkeeper, who informs you of the prices and quantities of items in the shop (1 health for 3 coins, 3 arrows for 4 coins), and the Gatekeeper, who guards the treasure and tells you how many Flyers remain to be defeated.
The player uses the arrow keys to move the hero, and the S and A keys to swing the sword and shoot arrows, respectively. Arrows travel in a straight line in whatever direction the player is facing. The amount of health and the numbers of coins and arrows are displayed along the top of the screen. When the player wins or loses the game, a large message will be displayed in the center of the screen. NPC messages are displayed automatically on the bottom of the screen (as seen in Figure 12-2) when the hero is within four pixels of the NPC and automatically disappear when the hero moves away.
The graphics for this game are colorful and cartoonish. Only a few animations are implemented in this project: the hero walking, the sword swinging, the enemies flying, and fading smoke that appears when an enemy is destroyed. Adding additional animations and sound effects to enhance the interaction with game-world objects would be an important and worthwhile addition to this project.
This chapter assumes that you are familiar with the material on tilemaps from Chapters 10 and 11, and that you have installed the Tiled map-editor software. In addition, you will need to be familiar with the material on dialog boxes and sign mechanics from Chapter 5, which is necessary to set up interactions with the NPCs.
Beginning this project requires the same steps as in previous projects: creating a new project, creating an assets folder and a +libs folder (the latter not being necessary if you have set up the userlib directory), copying the custom framework files you created in the first part of this book (BaseGame.java, BaseScreen.java, BaseActor.java), as well as the TilemapActor.java class you developed in Chapter 10 and the DialogBox.java class you developed in Chapter 5, and copying the graphics and audio for this project into your assets folder. As described previously, a BlueJ project named Framework has been created for your convenience and contains the necessary source code files to provide a convenient starting point. To begin the first project:
  • Download the source code files for this chapter.
  • Make a copy of the downloaded Framework folder (and its contents) and rename it to Treasure Quest.
  • Copy all the contents of the downloaded Treasure Quest project assets folder into your newly created Treasure Quest assets folder.
  • Open the BlueJ project in your Treasure Quest folder.
  • In the CustomGame class, change the name of the class to TreasureQuestGame (BlueJ will then rename the source code file to TreasureQuestGame.java).
  • In the Launcher class, change the contents of the main method to the following:
    Game myGame = new TreasureQuestGame ();
    LwjglApplication launcher = new LwjglApplication(
            myGame, "Treasure Quest", 800, 600 );

Level Setup

To begin, start the Tiled map-editor software . Create a new map with a map size that has a width of 40 tiles and a height of 40 tiles, and a tile size with the width and height both set to 32 pixels. The resulting map will be 1280 by 1280 pixels. Click the Save As . . . button and save the file to your assets directory with the filename map.tmx.
Next, add an object layer to your map. Then, in the Tileset panel , add a new tileset using the image adventure-tiles.png (shown on the left side of Figure 12-3), set the tile size to 32 by 32, and check the box to embed the tileset in the map. Add another new tileset to the map, this time using the image object-tiles.png (shown on the right side of Figure 12-3), in the same way. Since the object tiles will be used to indicate where game-world objects should be spawned, you will need to specify the corresponding Java classes (which you will create later). In the Tileset panel , select object-tiles and click on the icon to edit the tileset. In the tab that appears, you will need to click on each tile (one at a time) and add a new custom property to each. The property should be called name; the corresponding values for the tiles (from left to right) are Bush, Rock, Coin, Treasure, Flyer, NPC, ShopHeart, and ShopArrow. In addition, for the NPC tile, add a second custom property called id with a value of default, and a third custom property called text with a value of Hello, World!. (These default values will be overridden for specific instances later on in the tilemap editor).
A352797_2_En_12_Fig3_HTML.jpg
Figure 12-3.
Tilesets for the tile layer (left) and object layer (right)
Return to the tab featuring the tilemap. In the Layers panel, select Tile Layer 1. In the Tileset panel, select adventure-tiles , and then press the B key to activate the Stamp Brush tool . Add grass tiles across the entire map (which can be greatly accelerated using the Bucket Fill tool ), then add the wooden fence tiles along the edges. Also, add two large fenced-in areas near the center of the map, each with a two-tile-wide entrance so that the hero can move through; the bottom half of the tilemap is shown in Figure 12-4.
A352797_2_En_12_Fig4_HTML.jpg
Figure 12-4.
Adding grass and fence tiles to the tile layer
As was mentioned in the previous chapter, the tile layer is being used only to simplify creating an image from a tileset. The fence tiles should be solid barriers in the actual game, and so you will add rectangles in the object layer to store the data corresponding to those regions. In the Layers panel, select Object Layer 1 and press R to activate the Rectangle tool . Draw rectangles around all the regions containing solid tiles; for simplicity, rectangles can be drawn that surround multiple tiles at once. Each time you add one of these rectangles, you must also add a custom property called name with a value of Solid (which will be used to initialize corresponding actors later). To accelerate this process, when a rectangle is selected and the Select Object tool is active, you can use the shortcut key combination Ctrl+D to duplicate the object (including the custom property) and simply reposition and resize the new object (which appears directly on top of the original object). Once you have added rectangles corresponding to all the regions containing solid tiles, add one final rectangle in the bottom center of the map to indicate the starting position of the hero; add the custom property name with a value of Start.
Next, you will add the game objects, aiming for a design similar to that seen in Figure 12-5. Press T to activate the Insert Tile tool. Add Bush and Rock tiles (from the object-tiles tileset) to wall off a region that includes the hero’s starting position and the entrances to the two fenced-in areas you created earlier; this will be used to block enemies from reaching the hero until the player decides they are ready and uses the sword to remove the bushes. In the left fenced area, place a Treasure tile in the center and an NPC tile in the middle of the entrance. It may be useful during this process to hold down the Ctrl key while positioning a tile, which allows a tile to be placed in-between grid squares . For that NPC tile, temporarily switch to the Select tool and change the custom property id value to Gatekeeper . (The text that this NPC will display will be set via code later, and so you do not need to change this property.) In the right fenced area, add an NPC tile in the top center, changing id to Shopkeeper and text to 1 heart for 3 coins! 3 arrows for 4 coins! (This will produce the message seen in Figure 12-2 .) Also add a ShopHeart tile and a ShopArrow tile in the middle, at least one grid square apart from each other. Add a Coin tile somewhere in this area for later testing purposes. Finally, outside of the closed-in area you created, add a bunch of Flyer tiles and some more scattered Bush and Rock tiles.
A352797_2_En_12_Fig5_HTML.jpg
Figure 12-5.
Adding object tiles to the object layer
When you are finished, save the tilemap, close the Tiled map editor if you wish, and open the Treasure Quest project in BlueJ (if it isn’t already open). First, to represent the solid objects with which the hero and other game entities will collide, similar to the process from Chapter 11, create a class named Solid containing the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Solid extends BaseActor
{    
    public Solid(float x, float y, float width, float height, Stage s)
    {
        super(x,y,s);
        setSize(width, height);
        setBoundaryRectangle();
    }
}
Next, in the LevelScreen class , add the following import statements:
import com.badlogic.gdx.maps.MapObject;
import com.badlogic.gdx.maps.MapProperties;
Then, add the following code to the initialize method to load the tilemap and generate the Solid objects corresponding to the rectangles in the tilemap:
TilemapActor tma = new TilemapActor("assets/map.tmx", mainStage);
for (MapObject obj : tma.getRectangleList("Solid") )
{
    MapProperties props = obj.getProperties();
    new Solid( (float)props.get("x"),     (float)props.get("y"),
               (float)props.get("width"), (float)props.get("height"),
                mainStage );
}
At this point, you can test your project, although you will only see a small part of the level, and you have yet to write the code that creates the game entities represented by the object tiles. Your first task will be to add the hero character, as will be explained in the next section.

The Hero

In this section, you will create the code for the hero character. Since this game features a top-down perspective, the hero will have four different animations corresponding to walking in each of the four compass directions (north, south, east, and west). The main responsibilities of this class are to initialize the animations and display the correct animation depending on the actual angle of motion. It will also be important for this class to be able to determine the angle at which the character is currently facing (which will be a multiple of 90 degrees).
For adventure games or role-playing games, many spritesheets containing top-down character-walking animations typically contain the animation frames for all four directions in a single spritesheet, one direction per row, as illustrated in Figure 12-6. This layout standard has been popularized in particular by the game engine software RPG Maker. To extract the animation frames from the individual rows and create the corresponding animation will require the use of the TextureRegion class split method, as you will see.
A352797_2_En_12_Fig6_HTML.jpg
Figure 12-6.
A single spritesheet containing walking animations for four directions
To begin, create a class called Hero with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.utils.Array;
public class Hero extends BaseActor
{
    Animation north;
    Animation south;
    Animation east;
    Animation west;
    float facingAngle;
    public Hero(float x, float y, Stage s)
    {
        super(x,y,s);
        String fileName = "assets/hero.png";
        int rows = 4;
        int cols = 4;
        Texture texture = new Texture(Gdx.files.internal(fileName), true);
        int frameWidth  = texture.getWidth()  / cols;
        int frameHeight = texture.getHeight() / rows;
        float frameDuration = 0.2f;
        TextureRegion[][] temp = TextureRegion.split(texture, frameWidth, frameHeight);
        Array<TextureRegion> textureArray = new Array<TextureRegion>();
        for (int c = 0; c < cols; c++)
            textureArray.add( temp[0][c] );
        south = new Animation(frameDuration, textureArray, Animation.PlayMode.LOOP_PINGPONG);
        textureArray.clear();
        for (int c = 0; c < cols; c++)
            textureArray.add( temp[1][c] );
        west = new Animation(frameDuration, textureArray, Animation.PlayMode.LOOP_PINGPONG);
        textureArray.clear();
        for (int c = 0; c < cols; c++)
            textureArray.add( temp[2][c] );
        east = new Animation(frameDuration, textureArray, Animation.PlayMode.LOOP_PINGPONG);
        textureArray.clear();
        for (int c = 0; c < cols; c++)
            textureArray.add( temp[3][c] );
        north = new Animation(frameDuration, textureArray, Animation.PlayMode.LOOP_PINGPONG);
        setAnimation(south);
        facingAngle = 270;
        setBoundaryPolygon(8);
        setAcceleration(400);
        setMaxSpeed(100);
        setDeceleration(400);
    }
}
Next, you need to select the correct animation according to the direction of movement . This is a straightforward calculation, as visualized in Figure 12-7. For example, if the angle of motion is between 45 and 135 degrees, then the player’s movement is mainly to the north, and the hero is facing in the 90-degree direction. The eastern direction is slightly more complicated, as it corresponds to an angle that is either between 0 and 45 degrees or between 315 and 360 degrees, and so this case is left until last in the corresponding set of if-else statements.
A352797_2_En_12_Fig7_HTML.jpg
Figure 12-7.
Ranges for the angle of motion and the corresponding animation
To implement this functionality—to handle activating the correct animation according to the direction of movement, as well as setting and retrieving the angle at which hero is currently facing—add the following two methods to the Hero class:
public void act(float dt)
{
    super.act(dt);
    // pause animation when character not moving
    if ( getSpeed() == 0 )
        setAnimationPaused(true);
    else
    {
        setAnimationPaused(false);
        // set direction animation
        float angle = getMotionAngle();
        if (angle >= 45 && angle <= 135)
        {
            facingAngle = 90;
            setAnimation(north);
        }
        else if (angle > 135 && angle < 225)
        {
            facingAngle = 180;
            setAnimation(west);
        }
        else if (angle >= 225 && angle <= 315)
        {
            facingAngle = 270;
            setAnimation(south);
        }
        else
        {
            facingAngle = 0;
            setAnimation(east);
        }
    }
    alignCamera();
    boundToWorld();
    applyPhysics(dt);
}
public float getFacingAngle()
{
    return facingAngle;
}
Next, you will add the hero to the game as well as keyboard controls for moving the hero.
In the LevelScreen class, add the following import statements:
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
Then, add the following variable declaration:
    Hero hero;
In the initialize method , add the following code, which determines the starting position of the hero from the tilemap data and initializes the object:
MapObject startPoint = tma.getRectangleList("start").get(0);
MapProperties startProps = startPoint.getProperties();
hero = new Hero( (float)startProps.get("x"), (float)startProps.get("y"), mainStage);
Finally, in the update method , to move the hero with the arrow keys and stop the hero from moving through solid objects, add the following:
// hero movement controls
if (Gdx.input.isKeyPressed(Keys.LEFT))
        hero.accelerateAtAngle(180);
if (Gdx.input.isKeyPressed(Keys.RIGHT))
        hero.accelerateAtAngle(0);
if (Gdx.input.isKeyPressed(Keys.UP))
        hero.accelerateAtAngle(90);
if (Gdx.input.isKeyPressed(Keys.DOWN))
        hero.accelerateAtAngle(270);
for (BaseActor solid : BaseActor.getList(mainStage, "Solid"))
{
    hero.preventOverlap(solid);
}
This is a good point at which to test your project to make sure that the hero moves as intended.

The Sword

Next, you will add a sword to the game; pressing the S key will make the sword appear. The hero will appear to swing the sword in an arc, and then the sword will disappear. The first challenge in this process is making the sword appear in the correct location, consistent with the animation currently being displayed. In this game, you will assume the hero is right handed, and so the hilt of the sword should appear at the location of the hero’s right hand , as shown in Figure 12-8. This position, relative to the bottom-left corner of the hero image, will be stored as a percentage of the width and height of the hero graphic.
A352797_2_En_12_Fig8_HTML.jpg
Figure 12-8.
The position of the hero’s right hand , marked with an X, in each directional image
In addition, once the sword has been placed correctly, it will be swung through a 90-degree arc, as illustrated in Figure 12-9 . This can be accomplished by setting the initial rotation to 45 degrees less than the angle at which the hero is facing, then using an action to rotate the sword by 90 degrees (counter-clockwise). Since the sword should rotate around its hilt (not the center), the x-coordinate of the origin should be set to 0.
A352797_2_En_12_Fig9_HTML.jpg
Figure 12-9.
Arc of rotation used when swinging the sword
Finally, instead of spawning a new sword object every time the player presses the S key, there will be a single instance of the sword object, visible only when the hero is swinging the sword. In addition, while swinging the sword, the hero will stop moving, and the sword cannot be swung a second time until the first swing is completed (indicated by the sword becoming invisible again). To implement all these features, begin by creating a new class named Sword with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Sword extends BaseActor
{
    public Sword(float x, float y, Stage s)
    {
        super(x,y,s);
        loadTexture("assets/sword.png");
    }
}
Next, in the LevelScreen class , add the following import statements:
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
Then, add the following variable declaration:
Sword sword;
In the initialize method , set up the sword as follows:
sword = new Sword(0,0, mainStage);
sword.setVisible(false);
In the update method , to stop the hero from moving during a sword swing, locate the if statements corresponding to hero acceleration and enclose them in a conditional statement as follows:
if ( !sword.isVisible() )
{
    // hero movement controls
    // (code omitted below)
}
To handle the actual swinging of the sword, add the following method to the LevelScreen class:
public void swingSword()
{
    // visibility determines if sword is currently swinging
    if ( sword.isVisible() )
        return;
    hero.setSpeed(0);
    float facingAngle = hero.getFacingAngle();
    Vector2 offset = new Vector2();
    if (facingAngle == 0)
        offset.set( 0.50f, 0.20f );
    else if (facingAngle == 90)
        offset.set( 0.65f, 0.50f );
    else if (facingAngle == 180)
        offset.set( 0.40f, 0.20f );
    else // facingAngle == 270
        offset.set( 0.25f, 0.20f );
    sword.setPosition( hero.getX(), hero.getY() );
    sword.moveBy( offset.x * hero.getWidth(), offset.y * hero.getHeight() );
    float swordArc = 90;
    sword.setRotation(facingAngle - swordArc/2);
    sword.setOriginX(0);
    sword.setVisible(true);
    sword.addAction( Actions.rotateBy(swordArc, 0.25f) );
    sword.addAction( Actions.after( Actions.visible(false) ) );
    // hero should appear in front of sword when facing north or west
    if (facingAngle == 90 || facingAngle == 180)
        hero.toFront();
    else
        sword.toFront();
}
Finally, to enable the player to swing the sword, as this is a discrete action, you need to add a keyDown method to the LevelScreen class:
public boolean keyDown(int keycode)
{
    if (keycode == Keys.S)    
        swingSword();
    return false;
}
At this point, your code is ready to test again. Walk around the screen and try swinging the sword in each direction. However, there is nothing to swing at yet; this will be remedied in the next section.

Bushes and Rocks

At this point, you will add the code to create the bush and rock objects that you entered in the tilemap earlier. Bushes and rocks will be solid objects, and therefore their classes will extend the Solid class. The bushes will be destroyed when hit by the sword, while the rocks will not. The rocks are being implemented as their own object (rather than as a tile) so that a non-square boundary polygon can be set.
To begin, create a class named Bush with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Bush extends Solid
{
    public Bush(float x, float y, Stage s)
    {
        super(x,y,32,32,s);
        loadTexture("assets/bush.png");
        setBoundaryPolygon(8);
    }
}
Then, create a class named Rock with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Rock extends Solid
{
    public Rock(float x, float y, Stage s)
    {
        super(x,y,32,32,s);
        loadTexture("assets/rock.png");
        setBoundaryPolygon(8);
    }
}
To create these objects from the tilemap data, in the LevelScreen class, add the following code to the initialize method:
for (MapObject obj : tma.getTileList("Bush") )
{
    MapProperties props = obj.getProperties();
    new Bush( (float)props.get("x"), (float)props.get("y"), mainStage );
}
for (MapObject obj : tma.getTileList("Rock") )
{
    MapProperties props = obj.getProperties();
    new Rock( (float)props.get("x"), (float)props.get("y"), mainStage );
}
Since the Bush and Rock classes extend the Solid class, overlap with these objects is already prevented in the update method by the for loop that iterates over the list of Solid objects. For the sword to destroy the bushes, add the following code to the update method:
if ( sword.isVisible() )
{
    for (BaseActor bush : BaseActor.getList(mainStage, "Bush"))
    {
        if (sword.overlaps(bush))
            bush.remove();
    }
}
At this point, you can test your code again, slashing bushes with the hero’s sword to your heart’s content.

User Interface

Next, you will set up the user interface, which displays the amount of health remaining and the number of coins and arrows currently being held by the player. To keep the interface simple and minimal, images will be used rather than words, as shown in Figure 12-10 . The user interface will also contain a label that displays a “Game Over” message when appropriate, as well as a dialog box for discussions with NPCs (although this will not be used until later).
A352797_2_En_12_Fig10_HTML.jpg
Figure 12-10.
Images used in the user interface to represent health, coins, and arrows
To begin, add the following import statements to the LevelScreen class:
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
Then, add the following variables to the class:
int health;
int coins;
int arrows;
boolean gameOver;
Label healthLabel;
Label coinLabel;
Label arrowLabel;
Label messageLabel;
DialogBox dialogBox;
Next, to set up all these variables , add the following code to the initialize method:
health = 3;
coins = 5;
arrows = 3;
gameOver = false;
healthLabel = new Label(" x " + health, BaseGame.labelStyle);
healthLabel.setColor(Color.PINK);
coinLabel  = new Label(" x " + coins,  BaseGame.labelStyle);
coinLabel.setColor(Color.GOLD);
arrowLabel = new Label(" x " + arrows, BaseGame.labelStyle);
arrowLabel.setColor(Color.TAN);
messageLabel = new Label("...", BaseGame.labelStyle);
messageLabel.setVisible(false);
dialogBox = new DialogBox(0,0, uiStage);
dialogBox.setBackgroundColor( Color.TAN );
dialogBox.setFontColor( Color.BROWN );
dialogBox.setDialogSize(600, 100);
dialogBox.setFontScale(0.80f);
dialogBox.alignCenter();
dialogBox.setVisible(false);
To create the icons used in the interface, add these statements:
BaseActor healthIcon = new BaseActor(0,0,uiStage);
healthIcon.loadTexture("assets/heart-icon.png");
BaseActor coinIcon = new BaseActor(0,0,uiStage);
coinIcon.loadTexture("assets/coin-icon.png");
BaseActor arrowIcon = new BaseActor(0,0,uiStage);
arrowIcon.loadTexture("assets/arrow-icon.png");
To arrange all these items in the uiTable , add the following code as well:
uiTable.pad(20);
uiTable.add(healthIcon);
uiTable.add(healthLabel);
uiTable.add().expandX();
uiTable.add(coinIcon);
uiTable.add(coinLabel);
uiTable.add().expandX();
uiTable.add(arrowIcon);
uiTable.add(arrowLabel);
uiTable.row();
uiTable.add(messageLabel).colspan(8).expandX().expandY();
uiTable.row();
uiTable.add(dialogBox).colspan(8);
To keep the labels updated with the correct values of the variables, add the following code to the update method :
healthLabel.setText(" x " + health);
coinLabel.setText(" x " + coins);
arrowLabel.setText(" x " + arrows);
In addition, when the game is over, you will no longer want to move the player or swing the sword, so add the following code at the beginning of the update method:
if ( gameOver )
    return;
Similarly, at the beginning of the keyDown method , add the following:
if ( gameOver )
    return false;
This is a good point at which to add a few more game objects: the coins that can be collected and the treasure chest that you find to win the game. First, create a new class named Coin with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Coin extends BaseActor
{
    public Coin(float x, float y, Stage s)
    {
       super(x,y,s);
       loadTexture("assets/coin.png");
    }    
}
Then, create a class named Treasure with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Treasure extends BaseActor
{
    public Treasure(float x, float y, Stage s)
    {
        super(x,y,s);
        loadTexture("assets/treasure-chest.png");
    }
}
Since there will only ever be one treasure object in the game, for simplicity in the code that will follow, add the following variable declaration to the LevelScreen class:
Treasure treasure;
To create the objects from the map data, add the following to the initialize method :
for (MapObject obj : tma.getTileList("Coin") )
{
    MapProperties props = obj.getProperties();
    new Coin( (float)props.get("x"), (float)props.get("y"), mainStage );
}
MapObject treasureTile = tma.getTileList("Treasure").get(0);
MapProperties treasureProps = treasureTile.getProperties();
treasure = new Treasure( (float)treasureProps.get("x"), (float)treasureProps.get("y"),
    mainStage );
To handle overlap with these objects, in the update method , add the following code:
for ( BaseActor coin : BaseActor.getList(mainStage, "Coin") )
{
    if ( hero.overlaps(coin) )
    {
        coin.remove();
        coins++;
    }
}
if ( hero.overlaps(treasure) )
{
    messageLabel.setText("You win!");
    messageLabel.setColor(Color.LIME);
    messageLabel.setFontScale(2);
    messageLabel.setVisible(true);
    treasure.remove();
    gameOver = true;
}
In addition, this is a good time to add the code that handles losing the game, which happens when the hero runs out of health (even though this is not possible yet), so also add the following:
if ( health <= 0 )
{
    messageLabel.setText("Game over...");
    messageLabel.setColor(Color.RED);
    messageLabel.setFontScale(2);
    messageLabel.setVisible(true);
    hero.remove();
    gameOver = true;
}
Once again, you are ready to test your game. Collect coins (if you have added any) and collect the treasure to see the “You win!” message appear on the screen.

Enemies

Every game should have an obstacle that makes it difficult to reach or achieve the goal. In Treasure Quest, you must use your weapons to destroy the enemies that fly around the screen, which are named Flyers. Flyers are somewhat random: each has a random speed, and at random times they change their direction of motion to a random angle. Create a new class called Flyer that contains the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.math.MathUtils;
public class Flyer extends BaseActor
{
    public Flyer(float x, float y, Stage s)
    {
        super(x,y,s);
        loadAnimationFromSheet( "assets/enemy-flyer.png", 1, 4, 0.05f, true);
        setSize(48,48);
        setBoundaryPolygon(6);
        setSpeed( MathUtils.random(50,80) );
        setMotionAngle( MathUtils.random(0,360) );
    }
    public void act(float dt)
    {
        super.act(dt);
        if ( MathUtils.random(1,120) == 1 )
            setMotionAngle( MathUtils.random(0,360) );
        applyPhysics(dt);
        boundToWorld();
    }
}
When a Flyer is destroyed, it will disappear in a puff of smoke. To this end, create a new class called Smoke that contains the following code, which creates an image that fades out and then removes itself from the game:
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
public class Smoke extends BaseActor
{
    public Smoke(float x, float y, Stage s)
    {
        super(x,y,s);
        loadTexture("assets/smoke.png");
        addAction( Actions.fadeOut(0.5f) );
        addAction( Actions.after( Actions.removeActor() ) );
    }
}
As usual, you need to create these new objects in the LevelScreen class initialize method, as follows:
for (MapObject obj : tma.getTileList("Flyer") )
{
    MapProperties props = obj.getProperties();
    new Flyer( (float)props.get("x"), (float)props.get("y"), mainStage );
}
Flyers will also be blocked by solid objects and change their direction on collision, so in the update method, locate the for loop that iterates over the Solid objects and in the corresponding block of code add the following:
for (BaseActor flyer : BaseActor.getList(mainStage, "Flyer"))
{
    if (flyer.overlaps(solid))
    {
        flyer.preventOverlap(solid);
        flyer.setMotionAngle( flyer.getMotionAngle() + 180 );
    }
}
The sword can destroy enemies when it is visible (which indicates it is in the process of being swung). In this case, if the sword overlaps an enemy, smoke will be created and a coin will be spawned (rewarding the player for their victory). In the update method, locate the conditional statement that checks if the sword is visible, and in the corresponding block of code add the following:
for (BaseActor flyer : BaseActor.getList(mainStage, "Flyer"))
{
    if (sword.overlaps(flyer))
    {
        flyer.remove();
        Coin coin = new Coin(0,0, mainStage);
        coin.centerAtActor(flyer);
        Smoke smoke = new Smoke(0,0, mainStage);
        smoke.centerAtActor(flyer);
    }
}
Finally, the hero should lose health when he comes into contact with a Flyer. Checking for overlap is straightforward, but there is a subtle point that needs to be addressed: the hero and the Flyer should be “pushed apart” as much as possible because otherwise the hero could overlap the Flyer multiple times in rapid succession, and the player would quickly lose the game. Therefore, after a collision occurs, the Flyer will start moving in the opposite direction, and the hero will be pushed away from the enemy, which is also sometimes called knockback. To calculate the angle of motion, you calculate the vector from the hero to the Flyer by subtracting their positions and then use the angle method of the Vector2 class. To implement these features , add the following code to the update method:
for (BaseActor flyer : BaseActor.getList(mainStage, "Flyer"))
{
    if ( hero.overlaps(flyer) )
    {
        hero.preventOverlap(flyer);                
        flyer.setMotionAngle( flyer.getMotionAngle() + 180 );
        Vector2 heroPosition  = new Vector2(  hero.getX(),  hero.getY() );
        Vector2 flyerPosition = new Vector2( flyer.getX(), flyer.getY() );
        Vector2 hitVector = heroPosition.sub( flyerPosition );
        hero.setMotionAngle( hitVector.angle() );
        hero.setSpeed(100);
        health--;
    }
}
At this point, you have finished implementing the Flyer enemies. Test your project, slash the enemies with your sword, and collect the coins they drop.

Arrows

Many combat-centric games feature multiple weapons to appeal to multiple styles of gameplay. You will now add an arrow weapon to this game, which allows you to attack enemies from a distance. To make sure the player doesn’t rely on this weapon exclusively, the number of arrows is limited (although additional arrows may be purchased at the item shop that will be created near the end of this chapter). The player can shoot an arrow by pressing the A key, and if the hero has any arrows remaining, an arrow will be spawned and travel in the direction the hero is facing. If the arrow hits an enemy, the enemy will be destroyed; if the arrow hits a solid object, then the arrow will stop moving and fade out. To begin, create a new class called Arrow with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class Arrow extends BaseActor
{
    public Arrow(float x, float y, Stage s)
    {
        super(x,y,s);
        loadTexture("assets/arrow.png");
        setSpeed(400);
    }
    public void act(float dt)
    {
        super.act(dt);
        applyPhysics(dt);
    }
}
Shooting arrows will be handled by the following method, which should be added to the LevelScreen class:
public void shootArrow()
{
    if ( arrows <= 0 )
        return;
    arrows--;
    Arrow arrow = new Arrow(0,0, mainStage);
    arrow.centerAtActor(hero);
    arrow.setRotation( hero.getFacingAngle() );
    arrow.setMotionAngle( hero.getFacingAngle() );
}
In the keyDown method, add the following code after the code that causes the hero to swing their sword:
if (keycode == Keys.A)      
    shootArrow();
Finally, to specify how the arrows interact with the Flyers and with solid objects, add the following code to the update method:
for (BaseActor arrow : BaseActor.getList(mainStage, "Arrow"))
{
    for (BaseActor flyer : BaseActor.getList(mainStage, "Flyer"))
    {
        if (arrow.overlaps(flyer))
        {
            flyer.remove();
            arrow.remove();
            Coin coin = new Coin(0,0, mainStage);
            coin.centerAtActor(flyer);
            Smoke smoke = new Smoke(0,0, mainStage);
            smoke.centerAtActor(flyer);
        }
    }
    for (BaseActor solid : BaseActor.getList(mainStage, "Solid"))
    {
        if (arrow.overlaps(solid))
        {
            arrow.preventOverlap(solid);
            arrow.setSpeed(0);
            arrow.addAction( Actions.fadeOut(0.5f) );
            arrow.addAction( Actions.after( Actions.removeActor() ) );
        }
    }
}
This completes the arrow-shooting mechanic . Test your game and make sure that arrows destroy Flyer enemies, arrows stop when colliding with a solid, the number of arrows decreases when they are shot, and once the number of arrows reaches 0, no more arrows can be shot.

Non-Player Characters

The next feature you will implement is the addition of non-player characters (NPCs). They are very closely related to the sign objects introduced in Chapter 5; you may want to reread the corresponding section before continuing. Similar to signs, approaching an NPC causes the dialog box in the user interface to display an associated message. Unlike signs, however, each NPC will have a distinct appearance, using the identification (id) data set in the tilemap. In addition, each NPC’s ID will be checked before displaying the associated message, which gives you the opportunity to display different messages depending on the state of the game. In particular, the game designed in this chapter features two NPCs, one called the Gatekeeper, the other called the Shopkeeper. The Gatekeeper initially blocks access to the treasure, provides instructions to the player (destroy all the Flyers to get the treasure!), informs you how many Flyers remain, and fades out and disappears once you have destroyed them all, thus enabling you to collect the treasure. The Shopkeeper does not feature dynamic text; they simply inform the hero of the prices and quantities of items available for sale in the item shop (which will be created in the next section).
To begin, create a new class called NPC with the following code. Note that the image for the character is not set in the constructor; rather, it is set when the ID is set.
import com.badlogic.gdx.scenes.scene2d.Stage;
public class NPC extends BaseActor
{
    // the text to be displayed
    private String text;
    // used to determine if dialog box text is currently being displayed
    private boolean viewing;
    // ID used for specific graphics
    //   and identifying NPCs with dynamic messages
    private String ID;
    public NPC(float x, float y, Stage s)
    {
        super(x,y,s);
        text = " ";
        viewing = false;
    }
    public void setText(String t)
    {  text = t;  }
    public String getText()
    {  return text;  }
    public void setViewing(boolean v)
    {  viewing = v;  }
    public boolean isViewing()
    {  return viewing;  }  
    public void setID(String id)
    {  
        ID = id;  
        if ( ID.equals("Gatekeeper") )
            loadTexture("assets/npc-1.png");
        else if (ID.equals("Shopkeeper"))
            loadTexture("assets/npc-2.png");
        else // default image
            loadTexture("assets/npc-3.png");
    }
    public String getID()
    {  return ID;  }
}
To set up the NPCs, in the initialize method of the LevelScreen class, add the following code; note that you need to also retrieve the properties named id and text that you created when designing the tilemap. Note that the NPC position will be slightly offset from where it was placed in the Tiled map editor, because the NPC size is larger than the icon used to represent it in the tileset. You may find that you wish to adjust the NPC positions in Tiled later.
for (MapObject obj : tma.getTileList("NPC") )
{
    MapProperties props = obj.getProperties();
    NPC s = new NPC( (float)props.get("x"), (float)props.get("y"), mainStage );
    s.setID( (String)props.get("id") );
    s.setText( (String)props.get("text") );
}
Finally, in the update method, add the following code. If the hero is within four pixels of an NPC, the hero is considered to be nearby, and the corresponding message will be displayed. Once the hero is no longer near the NPC whose message is being viewed, the dialog box will disappear. The majority of the following code is used to create the dynamic messages for the Gatekeeper NPC, which depend on the number of Flyers remaining in the game. Once all the Flyers are destroyed, the Gatekeeper will slowly fade out and then move off-screen, thus enabling the hero to reach the treasure.
for ( BaseActor npcActor : BaseActor.getList(mainStage, "NPC") )
{
    NPC npc = (NPC)npcActor;
    hero.preventOverlap(npc);
    boolean nearby = hero.isWithinDistance(4, npc);
    if ( nearby && !npc.isViewing() )
    {
        // check NPC ID for dynamic text
        if ( npc.getID().equals("Gatekeeper") )
        {
            int flyerCount = BaseActor.count(mainStage, "Flyer");
            String message = "Destroy the Flyers and you can have the treasure. ";
            if ( flyerCount > 1 )
                message += "There are " + flyerCount + " left.";
            else if ( flyerCount == 1 )
                message += "There is " + flyerCount + " left.";
            else // flyerCount == 0
            {
                message += "It is yours!";
                npc.addAction( Actions.fadeOut(5.0f) );
                npc.addAction( Actions.after( Actions.moveBy(-10000, -10000) ) );
            }
            dialogBox.setText(message);
        }
        else
        {
            dialogBox.setText( npc.getText() );
        }
        dialogBox.setVisible( true );
        npc.setViewing( true );
    }
    if (npc.isViewing() && !nearby)
    {
        dialogBox.setText( " " );
        dialogBox.setVisible( false );
        npc.setViewing( false );
    }
}
At this point, the NPCs are fully functional. Test your game and talk to each of the NPCs. Try to destroy the Flyers, and then make sure that the Gatekeeper vanishes as expected.

Item Shop

The final mechanic you will implement in the Treasure Quest game is the item shop, which consists of two tiles that the hero can stand on, and if the player presses the B key (to buy) and has enough coins, the player will receive the quantity of items stated by the Shopkeeper. This gives value to the coins collected by the player, rather than their just being an abstract number of points or measure of progress. To begin, create a new class named ShopHeart with the following code:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class ShopHeart extends BaseActor
{
    public ShopHeart(float x, float y, Stage s)
    {
        super(x,y,s);
        loadTexture("assets/heart-icon.png");
    }
}
Also create a class named ShopArrow as follows:
import com.badlogic.gdx.scenes.scene2d.Stage;
public class ShopArrow extends BaseActor
{
    public ShopArrow(float x, float y, Stage s)
    {
        super(x,y,s);
        loadTexture("assets/arrow-icon.png");
    }
}
Since there will only be one instance of each of these objects, in the LevelScreen class, add the following variables:
ShopHeart shopHeart;
ShopArrow shopArrow;
To create these objects , add the following to the initialize method:
MapObject shopHeartTile = tma.getTileList("ShopHeart").get(0);
MapProperties shopHeartProps = shopHeartTile.getProperties();
shopHeart = new ShopHeart( (float)shopHeartProps.get("x"), (float)shopHeartProps.get("y"),
    mainStage );
MapObject shopArrowTile = tma.getTileList("ShopArrow").get(0);
MapProperties shopArrowProps = shopArrowTile.getProperties();
shopArrow = new ShopArrow( (float)shopArrowProps.get("x"), (float)shopArrowProps.get("y"),
    mainStage );
Since buying items is a discrete action, add the following code to the keyDown method after the code that activates the weapons:
if (keycode == Keys.B)
{
    if (hero.overlaps(shopHeart) && coins >= 3)
    {
        coins -= 3;
        health += 1;
    }
    if (hero.overlaps(shopArrow) && coins >= 4)
    {
        coins -= 4;
        arrows += 3;
    }
}
That’s all there is to it! Try out your game again, destroy some Flyers , and spend your hard-earned coins.
Once you have reached this point, congratulations are in order, as you have completed the longest project in this book!

Summary and Next Steps

In this chapter, you created the game Treasure Quest, a combat-based adventure game, which included new features such as combat with multiple types of weapons, NPCs with dynamic text, and an item shop. While implementing these features, you also handled subtle details such as sword placement, the interaction of enemies and arrows with solid objects, and knockback resulting from hero-enemy collisions.
As always, you should add features such as menus and sounds. If you wish, you could add new types of weapons, such as bombs, that create explosions that destroy both rocks and enemies. Bombs would likely be a limited resource, similar to arrows, and could perhaps be purchased at the item shop as well. Additionally, you could create a new kind of enemy that follows the player for added difficulty. You could change the goal of the game: instead of destroying Flyers, perhaps you have to collect a certain number of coins to proceed. You could remove the sword from the game, or even remove weapons entirely, making the game about dodging and avoiding enemies. The possibilities are endless!
At this point, you have completed the second part of the book. The final part of this book contains advanced techniques and algorithms that explain how to use gamepad controllers, advanced graphics, and more, which can be used to improve all of the games you have worked on up to this point.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset