You've probably had this experience before: you hear about an awesome game, but then you find out that it's only coming out on the one platform that you don't own. It doesn't have to be this way. In this tutorial, you will learn how to use Haxe to make a game in one development platform—Haxe—that can target multiple gaming platforms including Linux, Mac, Windows, iOS, Android, and Flash.
Introduction
This tutorial is about cross-compiling games using the Haxe programming language. If you don't know Haxe but you understand basic coding principles, you should be fine; Haxe's syntax is not difficult to learn (in fact, if you know ActionScript syntax you already know most of it). Knowledge of the OpenFL or HaxePunk libraries is not necessary.
To demonstrate this, I'll show you how to make a simple 2D drag racing game from start to finish. Other cars will spawn, and they will have to be avoided by switching lanes. Gasoline cans will also spawn, and collecting them will allow the player to race longer.
We will then compile and optimise this game for multiple platforms in a future tutorial. Let's get started!
Setting Up Haxe, OpenFL, and HaxePunk
First, you will need to get Haxe and some libraries. Visit http://haxe.org/download and download the correct version for your operating system. I'll be doing everything on a Linux machine, but it's all essentially the same if you are on Windows or a Mac.
After downloading and running the Haxe installer, open a command line or prompt window. We're going to use Haxelib (a command line tool) to install everything else we need.
OpenFL, a cross-platform framework, is helpful for making games, but we will make development even simpler by using HaxePunk. This is a Haxe port of the popular FlashPunk game engine, and lets us start making games quicker than we could by using only OpenFL.
Run the following command to install HaxePunk:
haxelib install lime
Now we tell haxelib to set up lime:
haxelib run HaxePunk setup
This will tell HaxePunk to download and set up the other tools we need, including OpenFL and lime (the backend to OpenFL). However, we aren't quite ready to move on. We still need to prepare a development environment. This is where things differ slightly depending on your platform. Run the command
lime setup <target platform>
For example, to compile for Linux, run openfl setup linux
; for Android, openfl setup android
. This command will guide you through installing (if necessary) and configuring tools for the target platform. On Windows, Visual Studio will be used; on Mac, Xcode; and on Linux, gcc.
After setting up one or more target platforms, it's time to create a new project for our game.
Create a New Project
Now that we have our environment prepared, we can create a new project. If you are using an IDE that supports Haxe (FlashDevelop is a good choice for Windows users), there should be an easy way to create a new project. If that is not the case, you can also create a project from a command line:
haxelib run HaxePunk new <project name>
This will create a folder in the current directory with the name you specified and copy the standard OpenFL template to the folder. This template is great, but I like my projects to have a slightly different directory structure, with separate folders for different types of resources. I'll show you what it looks like soon. For now, let's create a new project called MachRacer
.
You should see an XML file in the project folder. This file is used to configure many different aspects of your game. Here is my (modified) version:
<?xml version="1.0" encoding="utf-8"?> <project> <meta title="MachRacer" package="package" version="1.0.0" company="company" /> <app main="MachRacer" path="bin" file="MachRacer" /> <window background="#00ff33" fps="60" /> <window width="800" height="600" unless="mobile" /> <window vsync="false" antialiasing="0" if="cpp" /> <window orientation="landscape" /> <source path="src" /> <haxelib name="HaxePunk" /> <icon path="assets/HaxePunk.svg" /> <assets path="assets/gfx" rename="gfx" type="image" include="*.png" /><!-- We won't use these for this game, but I've added them to show you that these types exist --><assets path="assets/audio" rename="audio" include="*.mp3" if="flash" /><assets path="assets/audio" rename="audio" include="*.wav|*.ogg" unless="flash" /><assets path="assets/font" rename="font" include="*.ttf" /></project>
Here is a quick explanation of my customizations:
After the XML declaration at the beginning, we have have information about the game. You should change the package
and company
properties to something like "com.yourname.machracer"
and "YourName"
respectively. Next is the output folder and filename. I changed the filename from Main
to match the game's name.
The next section defines the dimensions of the game, the background color, graphical options, and whether the game should run in portrait or landscape mode on mobile devices. Next we define our source path, which is simply src
.
After this is where we tell the compiler what libraries we want to use. In this case, we will use HaxePunk.
The final part of our XML file is more path declaration. We give a path to use to get an icon for our game, and then we define paths for images, sound effects, music, and fonts. Instead of throwing all our assets into a folder called assets
, we can be much more organized! Now we have assets/gfx
, assets/audio
, and so on. At the end of each of these lines we say what file types we want to use from these folders: typically, we would use .png for images, .wav for sound effects, .ogg for music (OpenFL dropped support for .mp3 recently, for licensing reasons), and .ttf for fonts.
If you want to use a different structure for your project folder, that's fine. As I said, it comes down to personal preference. If you change it, however, I suggest that you use the rename
property in the XML file to make the folders appear to have the same names I am using. This way, the code from this tutorial will not need to be changed to look for assets in a different folder.
That was a lot of work! But now you should have a good grasp on how customizable your Haxe projects can be. Let's start making the actual game.
Getting Something Moving On-Screen
The first thing we need is our Main.hx
file. This is where execution of code begins. We will import a couple of classes:
import com.haxepunk.Engine; import com.haxepunk.HXP;
These lines of code pull in the Engine
class, which is the base game engine needed by every HaxePunk game, and a class that handles a lot of useful variables. We will also create the actual Main
class:
class Main extends Engine { override public function init() { HXP.console.enable() HXP.screen.scale = 1; HXP.scene = new PlayScene(); } public static function main() { new Main(); } }
First we see that the Main
class extends Engine
.
The init()
function, as might be guessed, initializes some values. The first line in the function enables the HaxePunk console, which is useful for debugging. The console will be overlaid on top of the game while it is running, and will show traces, the number of Entities
in the current scene, and more.
The next two lines set the scale of the game to 1
("normal scaling") and set the current scene to a new instance of PlayScene
, respectively. What is PlayScene? It's where the main part of the game actually takes place! We will create that in a moment.
The last function in this class, main()
, creates a new instance of the entire game, calling the init()
function. It's pretty simple.
Next, let's create a new file called PlayScene.hx
as follows:
import com.haxepunk.Scene; import com.haxepunk.HXP; class PlayScene extends Scene { public function new() { super(); } }
PlayScene
extends the Scene
class, which is a world or screen in the game. This scene will handle the actual playing of the game, and later we will create another scene for the title screen.
Right now, our scene is completely empty. Let's make another file (this is the last one for now, I promise) called Player.hx
, with the following code:
import com.haxepunk.Entity; import com.haxepunk.HXP; import com.haxepunk.graphics.Image; class Player extends Entity { public var health:Int; //the health of the player's vehicle public var gas:Float; //the amount of gas the player has left private var image:Image; //the player sprite public function new() { super(); image = new Image("gfx/player.png"); graphic = image; layer = 9; setHitbox(Std.int(image.scaledWidth), Std.int(image.scaledHeight)); type = "player"; health = 3; gas = 100; } }
We have several variables and a basic constructor to initialize them. gfx/player.png
is the location of the player image, which we then set to the Entity's graphic
property to display it. We want the player to be drawn on top of other things like the road and gas cans, so we set its image layer to 9
, as higher layer numbers will be drawn first.
Next, we set the hitbox of the player. All that happens is the width and height (multiplied by the scale of the game, which is very useful when dealing with different screen sizes) is cast from a Float
to an Int
and passed as the width and height of the hitbox. We set a type to check collisions against, but this is unnecessary for the player, as we will use the other objects' types for this.
Now we just need to create an instance of the player in our scene. This variable needs to be accessed throughout the entire PlayScene
class, so add it inside the class but outside any functions.
private var player:Player;
Now let's add to the PlayScene
constructor. The following code will create an instance of Player
:
player = new Player(); player.x = (HXP.screen.width * .5) - player.halfWidth; player.y = HXP.screen.height * .75; add(player); //adds an entity to the scene
The player will be centered horizontally on the screen. All that's needed to center any Entity
is the following:
(HXP.screen.width * .5) -- entity.halfWidth;
Very easy! The player's y position is three-quarters of the way down the screen. The last line simply adds the given Entity
to the current scene.
Now we should add two more import statements to the PlayScene
file:
import com.haxepunk.utils.Input; import com.haxepunk.utils.Key;
These two imports will allow us to handle various inputs, such as keyboard keys. We need to add two more lines to the constructor before we can make movement possible:
Input.define("left", [Key.A, Key.LEFT]); Input.define("right", [Key.D, Key.RIGHT]);
Input.define()
takes a string name (in this case, "left"
and "right"
) and an array of Keys. It will let us use a single name to represent multiple keys on the keyboard. This way, players can control their vehicle with either the left and right arrow keys or the A and D keys.
Let's add a function to handle movement. It should be added to the PlayScene
class:
private function move(dir:String):Void { if(dir == "left") { player.x -= 140 * HXP.elapsed; } else //assuming that dir == "right" { player.x += 140 * HXP.elapsed; } }
This function takes a String
parameter, which will be either "left"
or "right"
, and will tell the function which direction to move the player. The actual movement is done by changing the player's x position.
Tip: What is this strange calculation we are using? The quick explanation is that not all computers will run our game at the same speed. Some will take longer to process code and render a frame than others. To deal with this, we move the player by some amount expressed in pixels per second (140 in this case) multiplied by the amount of time the frame took to finish. Slower computers means a longer time spent on each frame, which means that the player will move farther, which means that the player will move at the same rate regardless of how fast the game is running.
Now we need to check if the player is pressing one of the defined keys and, if so, call our move()
function. Every Scene
object has an inherited function called update()
, which is run every frame. We will override this function so we can add our own code to it. This will also be added to the PlayScene
class:
override public function update():Void { super.update(); if(Input.check("left")) { move("left"); } if(Input.check("right")) { move("right"); } }
Every frame, we check whether a key that we have defined as "left"
is being pressed or a key we have defined as "right"
is being pressed. If one of these keys is held down, the move()
function is called with the direction to move the player in.
I think it's time to test the game! There are many targets we can build for, but compiling to the Flash target is quickest and therefore great for testing. If you are using an IDE such as FlashDevelop there is likely a keyboard shortcut to build and run the game. (In FlashDevelop, simply pressing F5 will build and run the game for the currently selected target—this shortcut can be changed via a drop-down menu at the top of the screen.)
If you are working from a command line like me, you can type:
lime build <target>
...to compile the game for a specified target, or:
lime test <target>
...to build and run the game with one command. If you, like me, are testing with the Flash target, the command is:
lime test flash
If you are using a different IDE/editor, check the documentation to see if you are able to build projects from within it.
After running the game, you should see a red car on a green background, and pressing A and D or the left and right arrow keys should move it across the screen! While this is a good start, the game feels... empty.
Creating the Game World Around the Player
Let's create an actual track for the game to take place on. Add a variable to the Player
class:
public var curLane:Int;
...and initialize it in the constructor:
curLane = 1;
Next we need to declare some more variables in the PlayScene
class:
private var track1:Image; //two images for the track to private var track2:Image; //give the illusion of an infinite track private var laneWidth:Float; //the width of an individual lane private var trackRightEdge:Float; //the position of the rightmost edge of the track private var laneX:Array<Float>; //an array of the x positions of each lane private var gameSpeed:Float; //the current speed at which to move the track images
At the top of PlayScene
's constructor, we will initialize these variables:
track1 = new Image("gfx/track.png"); track1.y = 0; //placed at the top of the screen addGraphic(track1, 15); //adds an image to the scene, similar to add() for entities track2 = new Image("gfx/track.png"); track2.y = -track2.scaledHeight; //placed immediately above the first track image addGraphic(track2, 15); laneWidth = 80; laneX = [20, 120, 220, 320]; trackRightEdge = laneX[3] + laneWidth + 20; gameSpeed = 240;
I'll explain what's happening. We want this game to go on for as long as the player collects gas and avoids drivers. This could be a long time! We want the player to be driving along a track, but we also want the player to believe the vehicles are actually moving. To do this, we create two instances of the track images. Both images will scroll downward at the speed defined by gameSpeed
, which will slowly increase. Once an image is below the bottom of the screen, it is moved to just above the top of the screen, and continues to scroll.
As for the other variables, the width of a lane is 80 pixels, so we store that in laneWidth
. The positions of the beginning of each lane (not counting the yellow lines at the edges) are stored in the array laneX
. The position of the right edge of the whole track is the rightmost lane plus 20 pixels (the width of the yellow lines). gameSpeed
is set to 240
, which is a decently slow speed for the game to begin at.
I explained how the track images will work. Now we should add them to the top of PlayScene
's update()
function, so they will be moved every frame.
//scroll track images to create the illusion of movement track1.y += gameSpeed * HXP.elapsed; track2.y += gameSpeed * HXP.elapsed; if(track1.y > HXP.screen.height) { track1.y = track2.y - track1.scaledHeight; } else if(track2.y > HXP.screen.height) { track2.y = track1.y - track2.scaledHeight; }
Tip: Again, using HXP.elapsed
will allow us to move things in the game without worrying about how well the game will run on different computers.
After moving the track images, we check whether one of them is below the bottom of the screen. If so, we put it above the other image to make the two images flow smoothly together.
Go ahead and test the game again. It looks great, doesn't it? However, the player isn't limited to staying on the track, or even on the screen. Let's change this. Change the line that sets the player's x position, from:
player.x = (HXP.screen.width * .5) -- player.halfWidth;
...to:
player.x = laneX[1] + (laneWidth * .5) - player.halfWidth; //start in second lane
This will center the player in the second lane.
Now, find the lines in PlayScene
's update()
function that check if the player is pressing a key to move their vehicle to the left or right. We are currently using Input.check()
to test for this, which will look for a key that is held down. Now we will want to check if the key has only been pressed on this frame, using Input.pressed()
. Here is what the code to check for input will now look like:
if(Input.pressed("left")) { move("left"); } if(Input.pressed("right")) { move("right"); }
The last thing to change is the move()
function. Instead of moving a few pixels to the left or right, we want the player to move one lane over. The new move()
function will look like this:
private function move(dir:String):Void { if(dir == "left") { if(player.curLane > 0) { player.curLane -= 1; player.x = laneX[player.curLane] + (laneWidth * .5) - player.halfWidth; } } else //assuming that dir == "right" { if(player.curLane < laneX.length - 1) { player.curLane += 1; player.x = laneX[player.curLane] + (laneWidth * .5) - player.halfWidth; } } }
We check the direction that the player wants to move in just like before. After that, we check whether moving in that direction would move the player off the track. If it wouldn't, then we change the player's curLane
variable, and set the player's x position to the lane to the left or right, depending on which direction the player is moving in.
Build and test again, and you'll see that the player is now limited to staying on the track! Our game is getting better, but it's a bit lonely in its current state, don't you think?
Adding Cars and Gas Cans
We will now add some more entities to the game so that the player has something to do. We will begin by creating a new class for the other drivers. Create a new file named Driver.hx
with this code:
import com.haxepunk.Entity; import com.haxepunk.HXP; import com.haxepunk.graphics.Image; class Driver extends Entity { private var image:Image; //the driver sprite private var speed:Float; //the speed at which to move public function new(xx:Float, speed:Float) { super(); image = new Image("gfx/driver.png"); graphic = image; setHitbox(Std.int(image.scaledWidth), Std.int(image.scaledHeight)); type = "driver"; x = xx - halfWidth; //- halfWidth to center it in the lane y = -image.scaledHeight; //start just above the screen this.speed = speed; } override public function update():Void { if(y < HXP.screen.height) { //still onscreen, so move downward y += speed * HXP.elapsed; } else { //offscreen, so we can remove it HXP.scene.remove(this); } } }
Most of this should be clear to you by now, but I'll explain what is happening in the update()
function. The player will not actually be moving vertically in this game, so everything else must move instead. As long as the driver is on the screen, it will move downwards, giving the impression that the player is slowly driving past. Once it reaches the bottom of the screen, it is removed.
The gas cans will be very similar. Place this code in a file named Gas.hx
:
import com.haxepunk.Entity; import com.haxepunk.HXP; import com.haxepunk.graphics.Image; class Gas extends Entity { private var image:Image; //the gas can sprite private var speed:Float; //the speed at which to move public function new(xx:Float, speed:Float) { super(); image = new Image("gfx/gascan.png"); graphic = image; layer = 12; setHitbox(Std.int(image.scaledWidth), Std.int(image.scaledHeight)); type = "gas"; x = xx - halfWidth; //- halfWidth to center it in the lane y = -image.scaledHeight; //start just above the screen this.speed = speed; } override public function update():Void { if(y < HXP.screen.height) { //still onscreen, so move downward y += speed * HXP.elapsed; } else { //offscreen, so we can remove it HXP.scene.remove(this); } } }
As you can see, it is almost identical to the Driver
class. It is created on the track, it moves downward, and it will be removed when it is below the screen.
Now that we have made the classes, we can actually add drivers and gas cans to the game. We will need some new variables defined in PlayScene
so we can keep track (lame pun intended) of when to put these objects on the track:
private var baseDriverTimer:Float; //driverTimer starts at this amount private var driverTimer:Float; //counts down to add a new driver private var baseGasTimer:Float; //gasTimer starts at this amount private var gasTimer:Float; //counts down to add a new gas can
These will be initialized in PlayScene
's constructor:
baseDriverTimer = 4.5; driverTimer = 2; baseGasTimer = 8.75; gasTimer = baseGasTimer;
These numbers are in units of seconds. This means that at the beginning of the game, driverTimer
will finish counting down in two seconds, and gasTimer
will finish in 8.75 seconds.
In PlayScene
's update()
function, the following code will be responsible for determining when to add drivers and gas cans:
if(driverTimer > 0) { driverTimer -= HXP.elapsed; } else { var d:Driver = new Driver(laneX[HXP.rand(4)] + (laneWidth * .5), gameSpeed - 60); add(d); baseDriverTimer -= .05; driverTimer += baseDriverTimer; } if(gasTimer > 0) { gasTimer -= HXP.elapsed; } else { var g:Gas = new Gas(laneX[HXP.rand(4)] + (laneWidth * .5), gameSpeed); add(g); baseGasTimer -= .05; gasTimer += baseGasTimer; }
I added this code after the code to move the track images but before checking for input. Let's break down what happens:
driverTimer
andgasTimer
are counted down until they are less than zero.- When this happens, a new instance of
Driver
orGas
(depending on which timer finished counting down) is created and added to the scene in a random lane and with a given speed. HXP.rand()
returns a random integer between zero and the specified number, inclusive. There are four lanes, numbered0
to3
, so we pass the number4
torand()
.
Why do gas cans have a speed equal to gameSpeed
but drivers have a speed that is less than this? Remember, this speed is the speed at which the objects will move down the screen. The gas has the same speed as the track images, so it will not appear to be moving forward or backward, relative to the track. The drivers move slower than this, so they appear to be moving forward, relative to the track. After this, baseDriverTimer
and baseGasTimer
are made slightly smaller so that objects will start to be added more and more quickly as the game goes on.
It's time to build and test the game again! Play for a while and watch the drivers and gas cans move down the screen. We are not testing for collisions between anything, so nothing will happen if the player's vehicle touches a driver or gas can.
This makes the game rather easy.
Adding Lose Conditions
Let's add a way for the player to actually lose. What do we want the lose condition to be? There are two ways a loss can happen: the player can run out of gas, or the player can hit other vehicles three times. Before we make this happen, let's test for collisions with drivers and gas cans.
In the Player
class, override the update()
function like we have done before:
override public function update():Void { var collobj:Entity = collide("driver", x, y); if(collobj != null) { //collided with a driver health -= 1; HXP.scene.remove(collobj); } collobj = collide("gas", x, y); if(collobj != null) { //collided with a gas can gas += 50; if(gas > 100) { gas = 100; } HXP.scene.remove(collobj); } }
The collide()
function tests whether the object it is called on collides with a specified type at a certain position. In the first case, we test if the player collides with entities with a type of "driver"
at its current position. Remember setting the type property for these entities? That's what collide()
is checking for. If the entity does indeed collide with the specified type, collide()
returns the entity we collided with. We store this in collobj
, so if collobj
is not null
, we know that there was a collision. In that case, we reduce the player's health by 1
and remove the entity it collided with (a driver).
After this, we check for collisions with objects of type "gas"
. If there is a collision, we increase the gas in the player's vehicle by 50
and remove the entity (a gas can).
Now we will add a function to both the Driver
class and the Gas
class. The function will not be exactly the same for each class. Here is the function for the Gas
class:
public function gameOver():Void { speed = 0; //the gas can will appear to be sitting still on the track }
...and for the Driver
class:
public function gameOver():Void { speed *= -1; //the vehicle will appear to drive away }
These functions will be called when the player loses. When this happens, we want the gas cans to stop moving downward and appear to simply be sitting on the track. In the case of the other drivers, however, we want them to drive away. Let's add some variables to the PlayScene
class to handle the player losing:
private var gameover:Bool; //whether or not the game has ended private var endTimer:Float; //the amount of time to wait after the game ends
Initialize them as shown (as usual, in PlayScene
's constructor):
gameover = false; endTimer = 3;
Now we will make a large change to PlayScene
's update()
function. As it is now, we move the track images, add drivers and gas cans, and handle the player's input. In most games, we don't want to do all this if the game is actually over, so we will only move the track images (and so on) if the gameover
variable is false
. The whole update()
function will be changed, so I'll just show you the new version:
override public function update():Void { super.update(); if(gameover) { if(endTimer > 0) { endTimer -= HXP.elapsed; } else { HXP.scene.removeAll(); HXP.scene = new PlayScene(); } } else { //scroll track images to create the illusion of movement track1.y += gameSpeed * HXP.elapsed; track2.y += gameSpeed * HXP.elapsed; if(track1.y > HXP.screen.height) { track1.y = track2.y - track1.scaledHeight; } else if(track2.y > HXP.screen.height) { track2.y = track1.y - track2.scaledHeight; } if(driverTimer > 0) { driverTimer -= HXP.elapsed; } else { var d:Driver = new Driver(laneX[HXP.rand(4)] + (laneWidth * .5), gameSpeed - 60); add(d); baseDriverTimer -= .05; driverTimer += baseDriverTimer; } if(gasTimer > 0) { gasTimer -= HXP.elapsed; } else { var g:Gas = new Gas(laneX[HXP.rand(4)] + (laneWidth * .5), gameSpeed); add(g); baseGasTimer -= .05; gasTimer += baseGasTimer; } if(Input.pressed("left")) { move("left"); } if(Input.pressed("right")) { move("right"); } if(player.gas > 0 && player.health > 0) { gameSpeed += .05 * HXP.elapsed; player.gas -= 5 * HXP.elapsed; if(player.gas < 0) { player.gas = 0; } } else { gameover = true; var drivers:Array<Driver> = []; var gascans:Array<Gas> = []; getType("driver", drivers); for(d in drivers) { d.gameOver(); } getType("gas", gascans); for(g in gascans) { g.gameOver(); } } } }
Our function has grown quite a bit since we created it! Now we only move the tracks, add entities, and get player input if the player has not lost. Otherwise, we collect all the drivers, and then all the gas cans, and call their gameOver()
function. The gameover
variable is set to true
, we count down the endTimer
variable, and we restart the game!
Currently, the player instantly jumps to another lane when input is given. In a lot of games, gradual movement will look better than instant teleportation. Tweening (short for inbetweening) is a useful technique to use for this situation. To use a tween for the player, we start by importing the tween we want to use in the PlayScene
class:
import com.haxepunk.tweens.motion.LinearMotion;
Have you guessed that we are doing a lot in this class? Linear motion makes the most sense for a vehicle switching lanes. We will need to add another variable:
private var playerTween:LinearMotion; //the tween used for moving the player
We then create a new tween:
playerTween = new LinearMotion(); playerTween.x = player.x; playerTween.y = player.y; player.addTween(playerTween);
We add this tween to the player because it affects the player. Next, we need to make a change to the move()
function. Currently, it changes the player's x position, but we want the tween to handle the player's movement. To do this, we can replace the lines where player.x
was being set with:
playerTween.setMotion(player.x, player.y, laneX[player.curLane] + (laneWidth * .5) - player.halfWidth, player.y, .25);
This function takes a starting x and y position, and ending x and y position, and the amount of time in seconds to move from one to the other. Now the tween's x position will be changed when the player presses a key to move the vehicle. There's still one more thing to do before the tween will actually move the player. Add this line to PlayScene
's update()
function after input is checked:
player.x = playerTween.x; //this actually moves the player during the tween
That's it! Tweens aren't very difficult, are they?
Going the Distance and Showing It
The player doesn't have much incentive to keep playing in the game's current state. Let's add a sort of score. Add another variable to PlayScene
:
private var distance:Float; //the distance the player has traveled
It should be set to zero when the game begins. At the bottom of the update()
function, we check whether the player's gas and health are greater than zero. If they are, the game speed is increased and the player's gas is decreased.
Add this line so that the distance will also increase:
distance += 10 * HXP.elapsed;
The distance traveled will increase by ten every second. We should probably show this (and other information) to the player. Displaying this via onscreen text is easy. First, we import the Text
class:
import com.haxepunk.graphics.Text;
Then we declare the variables we will use:
private var healthText:Text; //text to display the player's health private var gasText:Text; //text to display the amount of gas left private var distanceText:Text; //text to display the distance traveled
Then we create the Text
objects and set some properties in the constructor:
healthText = new Text("Health: 3"); healthText.color = 0xffff00; healthText.size = 28; healthText.x = HXP.screen.width - ((HXP.screen.width - trackRightEdge) * .5) - (healthText.scaledWidth * .5); healthText.y = (HXP.screen.height * .25) - (healthText.scaledHeight * .5); addGraphic(healthText); gasText = new Text("Gas: 100"); gasText.color = 0xffff00; gasText.size = 28; gasText.x = HXP.screen.width - ((HXP.screen.width - trackRightEdge) * .5) - (gasText.scaledWidth * .5); gasText.y = (HXP.screen.height * .5) - (gasText.scaledHeight * .5); addGraphic(gasText); distanceText = new Text("Distance: 0"); distanceText.color = 0xffff00; distanceText.size = 28; distanceText.x = HXP.screen.width - ((HXP.screen.width - trackRightEdge) * .5) - (distanceText.scaledWidth * .5); distanceText.y = (HXP.screen.height * .75) - (distanceText.scaledHeight * .5); addGraphic(distanceText);
It's not a small amount of code, but almost all of it is the same: We create a new Text
object, passing the text we would like to display; we set a color with a hexadecimal value (yellow in this case); we set a font size; we set the x position (centered on the right-most part of the screen) and y position; and we add it to the scene. All of this is optional. If we didn't want to give it any text to display, we could give it an empty string. If we didn't set a color, it would default to black (0x000000
).
Now that these text objects are onscreen, we can use them to give updated information on the game state. To do that, we will update the text property of these objects in PlayScene
's update()
function. A good place to put the following code is after the player movement but before the game checks for a gameover
state:
//update the text entities healthText.text = "Health: " + player.health; healthText.x = HXP.screen.width - ((HXP.screen.width - trackRightEdge) * .5) - (healthText.scaledWidth * .5); gasText.text = "Gas: " + HXP.round(player.gas, 1); gasText.x = HXP.screen.width - ((HXP.screen.width - trackRightEdge) * .5) - (gasText.scaledWidth * .5); distanceText.text = "Distance: " + HXP.round(distance, 1); distanceText.x = HXP.screen.width - ((HXP.screen.width - trackRightEdge) * .5) - (distanceText.scaledWidth * .5);
I'll use the healthText
object as an example of what is happening. The text property is updated to say "Health: "
followed by whatever the player's health is. The text is then centered on the right-most part of the screen, because the width of the text might not be exactly the same once the health value is added.
The other two text objects are updated in the same way. The only difference is that player.gas
and distance
are not integers, but floating point numbers, so they are rounded to make the text look nicer (and so that they don't fill too much of the screen!)
Take a Breath and Relax
That was a lot of work, but the results are great! We have a simple game, and we've compiled it for a single platform. What's next? Compiling for more platforms! Up until this point, you've probably been building a flash version of the game and testing that. This is perfectly fine. The more adventurous of you might have already done some experimenting with other targets. That's great! Remember, compiling for a different target from a command line is simple:
lime build <whatever target you want to build for>
Depending on your IDE, it might even be simpler!
I think this is enough for one tutorial. In the next tutorial I'll show you how to make your game work optimally on various platforms, and leave you with some tips for cross-platform development.