Hello there!, today we are going to go straight to business by starting with the first stage of a multi-part tutorial on how to build a simple -yet very illustrative- game using the wonderful HaxeFlixel framework <3
The idea here is rather simple. We are going to create the skeleton of a 2D horizontal space shooter game, in the style of Gradius, R-Type or Thunder Force. You know the drill. But you probably have already seen lots of tutorials on teaching how to do something similar. Problem is, most of those tutorials show you how to build a game where there is no actual scenario or pattern to the game. Probably they just generate enemies and obstacles in a random way, and leave you with the simple goal of surviving and ranking up as many points as you can.
Having customized scenarios and accurate enemy placement really bring these games to life
That’s ok and really nice, but I want to give this tutorial a twist, and for that we are going to lay the groundwork for you to build a space shooter that you will be able to expand on. We are going to have custom levels that you will be able to design freely in a level editor, without having to touch a single line a code.
Are you up for it?, let’s get started then!
Setting up the project
First things first, we are going to use HaxeFlixel to build this, so you need to get all set up an installed. You got that covered right here in a very easy and detailed way, so you won’t have much trouble getting it done. Once you got everything installed and updated set up a project template by typing the following on your console/terminal:
1 |
flixel tpl -n "AwesomeGame" |
Navigate to the newly created folder and you will see a bunch of files and folders that have already been set up for you automatically (assets, source files, project.xml, etc).
If you want to test your game at any given point just type thist:
1 |
lime test neko |
This way you can quickly test your current build on your desktop. You can easily do the same to test on other platforms, but we will cover that later.
If you run the project as is you will see the HaxeFlixel logo animating playfully and then you’ll be greeted with a black screen where nothing happens. Sweet.
Getting started
Ok, now go to the source folder. You will see a bunch of premade .hx files. Main.hx is the entry point for our application, from here you can configure a few aspects of the game, such as resolution and display options, and more importantly, we get to decide which “State” class gets to be launched first. As it is the code will be instantiating an already built and empty MenuState (which explains the black screen you saw before when launching the game). We don’t want this to be like this so we are going to create a new StageState.hx file inside the source folder and we are going to modify the Main.hx class like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... class Main extends Sprite { var gameWidth:Int = 640; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom). var gameHeight:Int = 480; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom). var initialState:Class<FlxState> = StageState; // The FlxState the game starts with. var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions. var framerate:Int = 60; // How many frames per second the game should run at. var skipSplash:Bool = true; // Whether to skip the flixel splash screen that appears in release mode. var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets ... |
As you see we set the game’s resolution to a fine 640×480 and we also disabled the startup Haxeflixel loading screen.
Now open up your newly created StageState and paste the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package; import flixel.FlxG; import flixel.FlxSprite; import flixel.FlxState; import flixel.text.FlxText; import flixel.ui.FlxButton; import flixel.util.FlxMath; /** * A FlxState which can be used for the game's menu. */ class StageState extends FlxState { /** * Function that is called up when to state is created to set it up. */ override public function create():Void { super.create(); } /** * Function that is called when this state is destroyed - you might want to * consider setting all objects this state uses to null to help garbage collection. */ override public function destroy():Void { super.destroy(); } /** * Function that is called once every frame. */ override public function update():Void { super.update(); } } |
Before we move on let’s take a look at what we have here. This is the basic structure of a FlxState class, and at its core we find 3 methods:
create(): which is called when the State class is created (with new StageState(…)). This is where you create and alloc everything you will use and need on your scene.
update(): which is called on every frame (you can set up the framerate on the Main.hx class) and updates the state of the scene. This is where you will place your game’s logic.
destroy(): called when the state is going to be destroyed. Here you will free up any resources you are not going to need. We are not going to spend much time with this right now but you have to be sure you dispose of unused resources and memory properly. This isn’t Java and you don’t want to miss stuff out that could lead to nasty memory leaks.
Next up, modify the create() method and add the following:
1 2 3 4 5 |
playerBullets = new FlxTypedGroup<Bullet>(); add(playerBullets); player = new Player(100,100,playerBullets); add(player); |
Also, add the playerBullets class variable:
1 2 3 4 5 6 7 8 |
class StageState extends FlxState { //bullets public var playerBullets:FlxTypedGroup<Bullet>; ... |
This will create a Player entity and add it to the state, as well as a group (similar to a set of elements) that will contain the bullets that have been fired by the player. You will see the benefit of this scheme a bit farther down the road.
Next up, as you might be wondering, we created a player and some bullets, but we don’t have classes for those two entities, and you would be right. But we are going to fix that right now.
Player movement and firing
Create two new class files: Player.hx and Bullet.hx and set them up like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package; import flixel.FlxSprite; import flixel.FlxG; import flixel.FlxObject; import flixel.util.FlxVelocity; import flixel.util.FlxAngle; import flixel.util.FlxPoint; class Bullet extends FlxSprite { private var speed:Float; private var direction:Int; private var damage:Float; public function new(X:Float, Y:Float,Speed:Float,Direction:Int,Damage:Float) { super(X,Y); speed = Speed; direction = Direction; damage = Damage; loadGraphic(Reg.BULLET, true, 6, 6, true, "bullet"); } override public function update():Void { super.update(); if (direction == FlxObject.LEFT){ velocity.x = -speed; } if (direction == FlxObject.RIGHT){ velocity.x = speed; } if (direction == FlxObject.FLOOR){ velocity.y = speed; } if (direction == FlxObject.CEILING){ velocity.y = -speed; } } override public function destroy():Void { super.destroy(); } } |
And here is the player class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
package; import flixel.FlxSprite; import flixel.FlxG; import flixel.FlxObject; import flixel.util.FlxVelocity; import flixel.util.FlxAngle; import flixel.util.FlxPoint; import flixel.group.FlxTypedGroup; class Player extends FlxSprite { private static var SPEED:Float = 250; private var bulletArray:FlxTypedGroup<Bullet>; public function new(X:Float, Y:Float,playerBulletArray:FlxTypedGroup<Bullet>) { super(X,Y); loadGraphic(Reg.PLAYER, true, 32, 22, true, "player"); bulletArray = playerBulletArray; } override public function update():Void { velocity.x = 0; velocity.y = 0; //Input if (FlxG.keys.pressed.LEFT) { moveLeft(); } if (FlxG.keys.pressed.RIGHT) { moveRight(); } if (FlxG.keys.pressed.UP) { moveUp(); } if (FlxG.keys.pressed.DOWN) { moveDown(); } if (FlxG.keys.justPressed.A){ attack(); } super.update(); } override public function destroy():Void { super.destroy(); } private function attack():Void{ var newBullet = new Bullet(x + 32, y+15,500,FlxObject.RIGHT,10); bulletArray.add(newBullet); } private function moveRight():Void{ velocity.x += SPEED; } private function moveLeft():Void{ velocity.x -= SPEED; } private function moveUp():Void{ velocity.y -= SPEED; } private function moveDown():Void{ velocity.y += SPEED; } } |
This two classes extend from another of the most basic and important classes on HaxeFlixel: FlxSprite. This class will give us an entity that can be added to scenes, can have a set of sprites attached to it, and that can be positioned and moved around the scene. It can also be animated, transformed and collided with other FlxSprite entities. It is incredibly useful!
As you can see, it has a new method where you will assign it’s spritesheet and initialize its internal variables, such as speed, direction, health, and so on.
It also has an update method, which will be called on each frame (as with the State’s update method) and will be useful to update the Sprite’s states. Things like animation and internal AI (for enemies) will go here.
You also get a destroy method, which as you probably have correctly guessed by now is, well, called when you dispose of the sprite.
For starters we need bullets to be able to move on a set direction with a certain speed and to have some specific damage value assigned to it. As you can see we move the object by adding the speed value to the sprite’s internal velocity value. This value (along with acceleration) is checked by the state the sprite has been added to and then used to alter the sprite’s position inside it on every frame. Clean, handy and efficient.
The Player class is fairly similar. Note the way we attach graphics to the sprite:
1 |
loadGraphic(Reg.PLAYER, true, 32, 22, true, "player"); |
This Reg.PLAYER is simply a path to the .png file we assigned to our player class. It can be found on the Reg.hx file, among other values:
1 2 3 4 5 6 7 8 |
class Reg { public static inline var PLAYER:String = "assets/images/sprite/05.png"; public static inline var BULLET:String = "assets/images/sprite/blue_bullet.png"; ... |
These images are temporary, and you can store them wherever you feel like. I put them into the assets/images/sprites folder I made to keep then organized and tidy.
Also, on the player class, notice how we check for player input on the update method, is easy and straightforward, and works just they way you think it will.
We set up a method to allow the player to shoot bullets. This simply creates and adds a new bullet to the playerBullets group, which will be passed to the Player class by the state scene, so the player can access the state directly and cleanly.
The beauty of this resides on the fact that all the player bullets are stored as a group, so collision detection is very easy to perform, as well as deleting and managing the whole bunch of bullets all at once.
With this set up you should be able to run your game and move your ship around while shooting some bullets. Mine looks like this right now:
Placeholder graphics rule. Space Brick to the rescue!
It is nothing special as it is, but just you wait, because on part II we will be creating our custom scenario, with destroyable tiles and enemies!, yay!
That’s it for today, thanks a lot for reading and stay tuned for updates!
This is part 1 out of 6 of a tutorial series, you can check out the rest here:
¡Very good tutorial!