2D Game Development with Heaps.io and Haxe

This post is meant for people who have experience in the Haxe high-level programming language, intending to make GPU-accelerated, high performance 2 dimensional games in Heaps.io. This article details what Heaps is, how to get started, and many functions that one requires as a game developer. I will be adding my own experiences here and there for clarity. Note that this guide is only for 2D Heaps.io, 3D you will have to look elsewhere.

What is Heaps.io?

Heaps is a 2D + 3D game development framework with hardware graphics acceleration support. Designed to be easily portable to new platforms, Heaps currently runs on Windows, macOS, Linux, HTML5, Android, Xbox One, Playstation 4, and Nintendo Switch. The 2D API in Heaps is similar to the Flash API (the scene graph is implemented as a Display List, just like in Flash), and at the same time, in Heaps, you can work with lower-level objects (shaders, buffers, vertices, etc.);

Personal Note

If you've worked with frameworks/editors like LibGDX, Unity, or UE4, Heaps.io is a leap in a different direction. As a nitty-gritty framework, you will have to invent specific mechanics yourself that are already included in others, such as physics and a camera (Heaps actually has a built-in camera, but it isn't very developed).

Heaps.io also isn't documented very well, but the source code is extremely legible and concise, so it almost makes up for that aspect. Many popular games like Dead Cells, Northgard, and even SpellBreak are developed in Heaps.io Haxe, so surpassing the steep learning curve is possible.

Heaps.io Package Structure

Heaps classes are divided into 4 categories (packages):

  • h2d - 2D API
  • h3d - 3D API
  • hxd - generic classes for working with events, sounds, resources, etc.
  • hxsl - classes related to the hxsl shader language

2D Package Base

  • h2d.Tile - whole area or texture
  • h2d.Sprite - Sprite, base class for displayable objects. The sprite itself cannot directly draw anything, but acts as a container for other objects
  • h2d.Bitmap - an object that displays a texture on the screen or a specified area from a texture
  • h2d.Text - respectively, an object that displays text on the screen

3D Package Base

  • h3d.scene.Object - similarly to a sprite, it can act as a container for other objects in the 3d scene, but it does not itself store information about the geometry of objects on the scene
  • h3d.Camera - a virtual camera through which you can watch the scene
  • h3d.scene.Mesh - an object that has a geometry (for example, a cube or a sphere), as well as a material that describes how this geometry should be rendered, what shaders and textures should be used

Working with 2D and 3D Together

Here are a few similar methods both rendering structures use:

  • To add a nested object to the scene, both for 2D and 3D use the addChild() method
  • 2D and 3D objects have a number of properties of the same name (visible, scaleX, scaleY, name, etc.)
  • hxd.Res - macro-based resource management subsystem. It is a virtual file system with the ability to load data from different sources (from disk, network, data embedded in the application itself)
  • Built-in event system (input events using mouse, keyboard, gamepad, network events).
  • System of shaders written in HxSL (Haxe Shader Language). HxSL shaders are essentially Haxe code and are directly embedded into the application code (without any quotes). Using macros, shaders are converted to the language supported on a specific target platform (glsl, hlsl, pssl).
  • Customizable rendering with support for modern graphic effects (for example, PBR). One of the ideas that was followed when creating Heaps is that any element of it could be easily changed. Therefore, the rendering in Heaps can be completely redone for your purposes, without making any changes to the code of the engine itself.

Compile Targets

As already mentioned, Heaps can compile for different platforms:

  • Flash (used by Stage3D API) - You should not compile to this anymore, the platform is deprecated on major browsers
  • HTML5 (version 1.3.0 added support for WebGL 2)
  • HashLink - A virtual machine used to run native apps. You can choose between SDL and DirectX, which are used to create the application window, draw graphics and handle events. On other desktop operating systems, only SDL is available. HashLink (and with it projects on Heaps) can also be built for modern game consoles (access to the code that provides support for these platforms is free, for access you need to contact Nicolas Cannasse). HashLink can compile to C code, making the developed game very fast.

Heaps IDEs

Personal Note

I recommend using Visual Studio Code to develop with Haxe and Heaps. It has a great plugin to aid debugging and development. IntelliJ IDEA and HaxeDevelop are other IDEs used to create Haxe applications.

Currently, HashLink applications are profiled by profiling the C code that is output (for example, using MS Visual Studio).

To work with the network, Nicolas recommends using his own hxbit library, an example of working with which is available in the examples distributed with Heaps - Network.hx.

Installing and Configuring the Heaps Environment

At the moment, the documentation says that Haxe version 3.4.2 or higher is required to work with Heaps. However, if you want to build a project for HashLink, then there may be incompatibilities between the Haxe and HashLink versions. Therefore, I will immediately make a reservation that when writing this material, I used the following versions of Haxe, HashLink and libraries:

Haxe 4.0.0-preview.4
HashLink 1.7
heaps 1.3.0
hlsdl 1.7.0
hldx 1.7.0
hlopenal 1.5.0 (the audio library that hlsdl and hldx depend on)

After downloading and unpacking HashLink, you need to add the path to its folder to the path system variable (otherwise you won't be able to run hl files in VS Code). If you installed on Mac or Linux, then you're good to go, no PATH modification necessary.

Heaps can be installed either from haxelib:

haxelib install heaps

Or install from the git version of the library:

haxelib git heaps https://github.com/HeapsIO/heaps.git

A set of samples is distributed with Heaps (see the samples folder). It is highly recommended that you familiarize yourself with them for a better understanding of the library.

In this article, we will be using VS Code as an editor.

To work with Haxe in VS Code, use the “Haxe Extension Pack”:

Setting Up a Project

Launch VS Code, open the project folder you created (create one if you didn't) (File -> Open folder). Create two folders: src and out at the top-level.

Here are a few files you need to create:

  1. Main.hx - Place this inside the src folder.
    The main (and only in our case) project class inherited from hxd.App (the base class for Heaps applications):
class Main extends hxd.App {
    
    static function main() {
        new Main();
    }
}

Note the capitalization difference for Main and main. Haxe, like any other language, is case-sensitive.

  1. build.hxml - Place this outside the src folder, at top level.
    The file with settings for project compilation. Set the following parameters:
# imports the heaps library
-lib heaps
# uses SDL (Simple DirectMedia Layer), but if you're on Windows, feel free to change it to "hldx"
-lib hlsdl
# set the output path
-hl out/hashlink/main.hl
# set window size
-D windowSize=1366x768
# set window title
-D windowTitle=Test App
# set path where main file resides
--class-path src
# set main class name
-main Main

After creating these files, this should be your file structure:

projectName
---out
---src
------Main.hx
---build.hxml

Make sure the structure looks like this, then press Ctrl-Shift-B. A dialog should pop up to select a task. Select “haxe: build.hxml”, and compile the project.

After that, the file main.hl should appear in the file tree inside the out/hashlink folder.

If this does not happen, then this most likely means that some process is frozen and you need to restart VS Code, the easiest way to do this is by calling the "Reload window" command - for this we call the command panel (Ctrl + Shift + P), type "reload", and press enter.

Also, if your autocomplete or code jumps stop working, then most likely you just need to restart the Language Server - an auxiliary process that calls on the Haxe compiler to perform these tasks. The Language Server can also be restarted through the command bar (Ctrl + Shift + P) with the command “Haxe: Restart Language Server”.

Now let's try to automate the launch of our hl file, and for this we will create a corresponding task in the project. Press F5 and select HashLink as the environment:

In the generated code, you need to correct the name of the hxml file if it does not say build.hxml. Rename it to build.hxml otherwise.

Now we need to set up the default build task (Tasks -> Set up default build task ...) by choosing game.hxml as the task:

In the generated task, you need to give it the name “Build” by adding the “label” field with the appropriate value (since a task with this name is searched for and launched before launching the application - it is set in launch.json by the “preLaunchTask” field):

Having configured all the tasks, we can finally launch the application by pressing F5 - an empty window with a black background will open.

Besides the fact that VS Code has support for the Haxe syntax, working with Heaps and HashLink in VS Code is also convenient because the HashLink debugger is included in the extension package.

Haxe Program Structure

Personal Note

If you've worked with other game frameworks, you may be familiar with the init() and update() methods.

init() is what is called when the program starts up, and only then. It is never called after, so this function is where you create all your variables and initialize other classes.

update() is called multiple times a second (anywhere from 100-500), so this is where your main game logic goes. It basically functions as a while loop, keeping the game running, updating sprites and delta time, detecting events, etc.

Adding Visuals

Let's now add visual objects to the scene. Let's add a 2d image - a Bitmap object whose constructor takes 2 parameters: tile and parent.

Tile describes a certain area of ​​the Texture (many tiles can be created from one texture). In this example, we will create a tile programmatically - from a texture filled with red. Let's add our object to a 2d scene, which is automatically created by the engine - s2d (it is a property of the main class - h2d.App, beside it, a 3d scene is created in the application - s3d):


override function init() {
    trace("Hello World!");

    var b = new h2d.Bitmap(h2d.Tile.fromColor(0xff0000, 60, 60), s2d);
    b.x = 50;
    b.y = 100;

    b.tile = b.tile.center();
    b.rotation = Math.PI / 4;
}

In the above code, we are centering the tile (give it an anchor point). This way all transformations of the object on the screen will be calculated relative to the center of the tile. If this method is not called, then the tile will be transformed relative to its top left point. We also rotate the tile, and the rotation is given in radians.

You can also center the tile using the dx and dy properties of the tile.

Coloring

Objects in a 2D scene have many properties similar to those of DisplayObject in Flash, for example, color transformations can be configured as follows:

b.color.set(1.0, 0.0, 0.0);

b.colorAdd = new h3d.Vector(0.0, 0.0, 1.0, 0.0);
b.adjustColor({hue: Math.random()});

b.colorKey = 0xffffffff;

Let's add dynamics - let our object change its position over time. To do this, h2d.App has an update (dt: Float) method that I mentioned earlier that's called every frame. Let's override it:

override function update(dt:Float) {
    b.rotation += 0.01;
}

Now, if we compile the example (Ctrl-Shift-B, then f5), we will see a red rotating square.

Interaction and User Events

Let's now look at how Heaps handles user input events.

In Heaps, unlike Flash, objects in the scene cannot themselves handle mouse events. For this, objects of type h2d.Interactive are created, which are added as child objects to the scene. Add the following code at the end of the init() method:

var i = new h2d.Interactive(b.tile.width, b.tile.height, b);

i.x = -0.5 * b.tile.width;
i.y = -0.5 * b.tile.height;

i.onOver = function(event : hxd.Event) {
    b.alpha = 0.5;
} 
i.onOut = function(event : hxd.Event) {
    b.alpha = 1.0;
} 

As you can see, the interactive object takes a rectangular area to detect mouse input. The event detection is a handler system, with the function assigned to the event being called every time the mouse activates that event.

Now, when you hover the mouse over the red square, it will become semi-transparent, and when you move the mouse out, it will become opaque again.

In order to override the behavior of an Interactive object (the area in which mouse events are triggered), you can override its onCheck() method.
Also in Heaps, instead of a rectangular validation area, you can set it to an ellipse-shaped area; for this, the Interactive object has a boolean property isEllipse.

Handling keyboard button clicks:

override function update(dt:Float) {
    if (hxd.Key.isDown(hxd.Key.RIGHT)) {
        g.x++;
    }
    else if (hxd.Key.isDown(hxd.Key.LEFT)) {
        g.x--;
    }
}

To organize work with different tasks, the hxd.WaitEvent class has been added to Heaps, which executes the function passed to it until it returns true. Also, WaitEvent has several predefined functions, for example, for deferred method invocation:

var w:hxd.WaitEvent;

override function init() {
    w = new hxd.WaitEvent();
    w.wait(2, function() {
        trace("Kept you waiting, huh?");
    });
    w.add(everyFrameRoutine);
}

override function update(dt:Float) {
    w.update(dt);
}

function everyFrameRoutine(dt:Float):Bool {
    trace("everyFrameRoutine");
    return false;
}

Images and Textures

Let's try to use an image as a texture.

Add a folder “res” to the project (by default, a folder with this name is used as a resource store, but if necessary, the folder name can be redefined in the project by setting the resourcesPath compilation flag as such: -D resourcesPath=assets/).

This should be the folder structure of the project now, after adding this image to the assets folder:

projectName
---assets
------logo.png
---out
------hashlink
---------main.hl
---src
------Main.hx
---build.hxml

Note how the above structure omits the .vscode folder, this is intentional, do not delete that folder.

Now, change this line of code:

b = new h2d.Bitmap(h2d.Tile.fromColor(0xff0000, 60, 60), s2d);

To this:

b = new h2d.Bitmap(hxd.Res.logo.toTile(), s2d);

In the above sample, we are calling hxd (the underlying heaps library) to access the resources folder designated, find the PNG with the name "logo", and convert it to a tile.

Resource Management System Backend

Due to the fact that the resource management system is based on macros, the names of files with resources do not need to be specified as strings - they become fields of the hxd.Res class, which avoids typos in file names (messages about such errors are displayed at the compilation stage of the application).

In addition, based on file extensions, the resource system independently determines the type of resource (for example, jpg and png are processed as images).

The table of correspondence between file extensions and resource types is defined in the hxd.res.Config class and can be changed because is a public variable:

public static var extensions = [
    "jpg,png,jpeg,gif,tga" => "hxd.res.Image",
    "fbx,hmd" => "hxd.res.Model",
    "ttf" => "hxd.res.Font",
    "fnt" => "hxd.res.BitmapFont",
    "wav,mp3,ogg" => "hxd.res.Sound",
    "tmx" => "hxd.res.TiledMap",
    "atlas" => "hxd.res.Atlas",
    "grd" => "hxd.res.Gradients",
    #if hide
    "prefab,fx,l3d" => "hxd.res.Prefab"
    #end
];

In an extreme case, resources can be loaded by file name, but the typing of resources is lost and you need to independently convert the resulting resource to its type:

hxd.Res.load("logo.png").toImage().toTile();

You can also create a resource from a byte array and cast it to the required type:

hxd.res.Any.fromBytes(...);

But in order for the texture to actually load, it is necessary to initialize the resource system. This is done before calling the application constructor at the beginning of the app. Place the following line of code before calling new Main() in the main() method, and replace the BitMap declaration:

class Main extends hxd.App {
    
    var b:h2d.Bitmap;
    
    static function main() {
        hxd.Res.initLocal();
        new Game();
    }

    override function init() {
        b = new h2d.Bitmap(hxd.Res.load("haxeLogo.png").toImage().toTile(), s2d);
        ...
    }

In this example, we are calling the Res.initLocal() method, which means that resources will be read from the local disk system as separate files. In addition to this method, methods are available:

initEmbed() - all resources will be embedded in the application file;

initPak() - resources will be packed into pak files (resource archives that can be grouped according to some attribute, for example, resources of a certain game level).

In addition, you can independently determine how resources will be loaded (for example, over the network). Heaps implements a live reload mechanism for resources, which is activated by the pipeline (before initializing the resource system):

hxd.res.Resource.LIVE_UPDATE = true;

And to process resource changes, you need to assign a callback, which will be called at the same time:

hxd.Res.logo.entry.watch(changeCallback);

Here the entry property of hxd.Res.logo is an object of the virtual file system, with which the resource system works as an abstraction. Entry has its own interface, with which you can determine if entry is a folder or file, read its bytes, etc.

It should also be said that in Heaps unused resources are automatically removed from memory, but there are also manual methods for destroying them, for example, tiles have a dispose() method that removes the texture it uses from memory.

Text Processing and Display

Here are a few more important classes based around text processing:

  • For buttons and other "stretching" elements (using 9 slice), Heaps has the h2d.ScaleGrid class.
  • The h2d.Flow component can be used for layout elements.
  • Text rendering can be done either using the h2d.Text class, or using h2d.HtmlText, which supports markup using html tags.
  • An inputable text field is implemented in the h2d.TextInput class.

In these classes, BitMap fonts are used as fonts, which can be prepared, for example, in the bmfont program (on the net you can find many of its counterparts with a wide range of capabilities). Also, if the target platform is Flash, JS or Lime (not officially supported), then BitMap fonts can be generated programmatically from vector using the hxd.res.FontBuilder class:

var fnt = hxd.res.FontBuilder.getFont("Verdana", 24);

Here's the simplest example of using the h2d.Text class (place this in the init() function):

var t = new h2d.Text(hxd.res.DefaultFont.get(), s2d);
t.scale(10);

t.text = "Hello Haxe";

This created a Text object with the default font, attaches it to the 2D scene, scales it up by 10, and sets the text. Building/running the project again shows the text in the window.

h2d.HtmlText inherits from h2d.Text and working with it is no different:

var t = new h2d.HtmlText(hxd.res.DefaultFont.get(), s2d);
t.scale(10);

t.text = "Hello <font color='#ff0000'>Haxe</font>";

h2d.InputText adds the ability to handle text input from the keyboard, as well as the ability to get information about the selected text fragment (using the cursorIndex and selectionRange properties):

var t = new h2d.TextInput(hxd.res.DefaultFont.get(), s2d);
t.scale(10);

t.text = "Input Here";

Tile Batching

Next up, tile batching! There are 2 classes for batching tiles in Heaps:

  • h2d.TileGroup - for static groups whose elements do not change their relative position
  • h2d.SpriteBatch - for dynamic groups

An example of using h2d.TileGroup:

var g:h2d.TileGroup;

override function init() {
    var tiles = hxd.Res.logo.toTile().gridFlatten(10, 0, 0);

    g = new h2d.TileGroup(tiles[0], s2d);
    g.blendMode = None;

    for (i in 0...1000) {
        g.add(Std.random(s2d.width), Std.random(s2d.height), tiles[i % tiles.length]);
    }
}

override function update(dt:Float) {
    g.rotation += 0.01;
}

Since the TileGroup class is optimized for rendering static elements, transformations of the entire group cost almost nothing in terms of performance! That is, you can change the position, scale, and rotation of the group without slowing down the application speed, no matter how many elements there are.

Consider creating simple 2d animations using the h2d.Anim class:

var animationSpeed = 15;
b = new h2d.Anim(hxd.Res.logo.toTile().split(10, false), animationSpeed, s2d);

Here we split the texture into 10 horizontal frames, set the animation speed and add an animated sprite to the scene.

Of course, for an animated sprite, you can manually change the animation speed, set the current frame, pause the animation, etc. (in general, do whatever is expected of animation):

b.currentFrame = 1;
b.pause = true;
b.speed = 10;

Shape Rendering / Geometric Primitives

To draw geometric primitives in Heaps, such as shapes, objects of the h2d.Graphics type are used (there is also h3d.scene.Graphics, which works with a 3D scene), which are similar to flash.display.Graphics in Flash, but at the same time are an element of the scene, and not a property of sprites:

var g:h2d.Graphics;

override function init() {
    g = new h2d.Graphics(s2d);

    g.beginFill(0xff00ff, 0.5);
    g.drawCircle(150, 150, 100);
    g.endFill()
}

At the same time, unlike OpenFL (another Haxe game development library), all primitives are rendered entirely by means of the GPU, without consuming additional memory, and are also stored in a buffer (that is, each frame is not recalculated).

Done!

Hopefully after going through this guide, you've become more acquainted and proficient with 2D Heaps.io. With a library like this, understanding the documentation can be critical to your game, and I hope I've cleared some things up.

Have fun with game development! Feel free to share any cool projects with me at rohan@rbansal.dev.


Permalink: https://blog.rbansal.dev/gamedev-heaps-and-haxe/