64x64 icon dark hosting
Choose a hosting plan here and get a free year's subscription to Tuts+ (worth $180).
Advertisement

Write Once, Publish Everywhere With HaxePunk: Making a Game

by
Gift

Start a hosting plan from $3.92/mo and get a free year on Tuts+ (normally $180)

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


Click the SWF to give it focus, then press Enter to start.

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:

Now we tell haxelib to set up lime:

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

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:

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:

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:

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:

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:

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:

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.

Now let's add to the PlayScene constructor. The following code will create an instance of Player:

The player will be centered horizontally on the screen. All that's needed to center any Entity is the following:

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:

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() 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:

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:

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:

...to compile the game for a specified target, or:

...to build and run the game with one command. If you, like me, are testing with the Flash target, the command is:

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:

...and initialize it in the constructor:

Next we need to declare some more variables in the PlayScene class:

At the top of PlayScene's constructor, we will initialize these variables:

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.

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:

...to:

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:

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:

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:

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:

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:

These will be initialized in PlayScene's constructor:

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:

I added this code after the code to move the track images but before checking for input. Let's break down what happens:

  1. driverTimer and gasTimer are counted down until they are less than zero.
  2. When this happens, a new instance of Driver or Gas (depending on which timer finished counting down) is created and added to the scene in a random lane and with a given speed.
  3. HXP.rand() returns a random integer between zero and the specified number, inclusive. There are four lanes, numbered 0 to 3, so we pass the number 4 to rand().

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.

almostdone_resized

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:

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:

...and for the Driver class:

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:

Initialize them as shown (as usual, in PlayScene's constructor):

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:

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:

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:

We then create a new tween:

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:

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:

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:

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:

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:

Then we declare the variables we will use:

Then we create the Text objects and set some properties in the constructor:

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:

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:

Depending on your IDE, it might even be simpler!

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.

Advertisement