Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


Home > >

22. April 2013

In the previous post we looked at the basic structure of a program in Haxe/NME running on a number of different platforms.  Now I am going to check out the graphics portion of NME, specifically sprite/bitmap rendering.  I am going to need a graphic to use for this example and I chose to use these, specifically this one.  It's a sprite sheet from Mech Commander 2, a game Microsoft released for free, including all of the source code and assets.  Of course the images themselves are still copy protected, so do not use them in a commercial project!  Of course, you can use whatever image you want, just be aware that i've hard coded the dimensions in the following examples to match the dimensions of these images.

 

I've taken a single frame of animation and blown it up for the first sample.

MechwarriorFrameScaled

We are now going to look at the code required to display this guy on screen.  Since I am using FlashDevelop, I am following their folder structure.  So I've added my image to the folder assets/img as MechWarriorFrameScaled.png.  Your layout should look something like this:

Burp

 

Now let's take a look at the code:

 

 

package gfs;

import nme.Assets;
import nme.display.Bitmap;
import nme.display.Sprite;
import nme.events.Event;
import nme.Lib;
import nme.text.TextField;
import nme.text.TextFormat;

class Main extends Sprite
{
	public function new()
	{
		super();
		var mech:Bitmap = new Bitmap(Assets.getBitmapData("img/mechwarriorFrameScaled.png"));
		Lib.current.stage.addChild(mech);
	}
	
	public static function main()
	{
		// static entry point
		Lib.current.stage.align = nme.display.StageAlign.TOP_LEFT;
		Lib.current.stage.scaleMode = nme.display.StageScaleMode.NO_SCALE;
		Lib.current.addChild(new Main());
	}
}

 Pretty straight forward, in our constructor new() we create a new nme.Display.Bitmap by using the nme.Assets class's getBitmapData function, passing in our file name and directly ( below assets in the hierarchy, you don't need to specify the assets folder in the path ).  We then simply add the bitmap to the stage, which you can get with the static value Lib.current.stage.  We run this code and:

GFX1

 

Well, that was easy enough.  That code works just fine on every single platform I tested, which is most of them.

 

Often what you want to group a number of similar sprites, such as animation frames, together in a single sprite sheet, or in NME parlance, a tile sheet.  Let's take a look at drawing a single frame from a sprite sheet.  The following is an exploded view of our spritesheet with the single sprite we are interested bounded in blue.

GFX2

 

Let's take a look at the code required to load our sprites and display a single one.

 

 

 

package gfs;

import nme.Assets;
import nme.display.Bitmap;
import nme.display.Sprite;
import nme.display.Tilesheet;
import nme.events.Event;
import nme.geom.Rectangle;
import nme.Lib;
import nme.text.TextField;
import nme.text.TextFormat;

class Main extends Sprite
{
	public function new()
	{
		super();
		var sprites:Tilesheet = new Tilesheet(Assets.getBitmapData("img/mechwarrior.png"));
		sprites.addTileRect(new Rectangle(0, 0, 90, 80));
		
		var tileData = new Array<Float>();
		tileData = [0, 0, 0];
		sprites.drawTiles(graphics, tileData);
	}
	
	public static function main()
	{
		// static entry point
		Lib.current.stage.align = nme.display.StageAlign.TOP_LEFT;
		Lib.current.stage.scaleMode = nme.display.StageScaleMode.NO_SCALE;
		Lib.current.addChild(new Main());
	}
}

 

 

Instead of creating a bitmap, we instead create a nme.display.Tilesheet, once again using Assets to load the image file.  Next we the position of our sprite/tile in the sheet.  This is done by calling addTileRect and passing in a rectangle, which are the coordinates within the sheet that our individual sprite.  In this case, at 0,0 (top left), 90 wide and 80 pixels tall.

Next up we create a Float array called tileData.  This one will make a bit more sense in a second.  The array is basically composed of x then y position on screen where you want to draw the tile, followed by the index of the rect in the Tilesheet.  Again, this will make more sense in the future… for now just realize that we are saying to draw the 0th (first) tile in the tile sheet to the position 0,0 on screen.  That is exactly what we do when we call drawTiles.  This is optimized to make drawing tiles from the same sheet as fast as possible by minimizing the OpenGL overhead, something often referred to as sprite batching.

We run the application and we see:

GFX3

 

Our single tile from our tile sheet is rendered in the top left corner of the screen.  Again, this code runs on every platform I tested it.

 

Now drawing a single graphic from a tilesheet is all nice and good, but not really useful.  Often what you are going to want to do is draw an entire level composed of tiles.  Let's take a look how that works ( still using our mechwarrior tilesheet )

 

 

package gfs;

import nme.Assets;
import nme.display.Bitmap;
import nme.display.Sprite;
import nme.display.Tilesheet;
import nme.events.Event;
import nme.geom.Rectangle;
import nme.Lib;
import nme.text.TextField;
import nme.text.TextFormat;

class Main extends Sprite
{
	public function new()
	{
		super();
		var sprites:Tilesheet = new Tilesheet(Assets.getBitmapData("img/mechwarrior.png"));
		var tileData = new Array<Float>();
		
		for (i in 0...11)
		{
			sprites.addTileRect(new Rectangle(0, i * 80, 90, 80));
		}
		
		tileData = [
			0,   0,  0,
			90,  0,  1,
			180, 0,  2,
			270, 0,  3,
			360, 0,  4,
			450, 0,  5,
			540, 0,  6,
			0,   80, 0,
			90,  80, 1,
			180, 80, 2,
			270, 80, 3,
			360, 80, 4,
			450, 80, 5,
			540, 80, 6,
			
			];
						
		sprites.drawTiles(graphics, tileData);
	}
	
	public static function main()
	{
		// static entry point
		Lib.current.stage.align = nme.display.StageAlign.TOP_LEFT;
		Lib.current.stage.scaleMode = nme.display.StageScaleMode.NO_SCALE;
		Lib.current.addChild(new Main());
	}
}

 

 

The code is remarkably similar to before, except this time we are creating a number of tile rectangles within our tilesheet.  We are going to create a rectangle for all of the sprites in the first column of our sprite sheet, as shown:

GFX4

 

This time we are going to draw two rows worth of sprites on our screen, so we have a much more complicated tileData array, but it follows exactly the same logic as before:

0, 0, 0,
90, 0, 1,
180, 0, 2,
270, 0, 3, //snip

Considering the above snippet from the array for example, what we are saying is draw the tile at index 0 in the tilesheet to 0,0 on screen, then the tile at index 1 to to 90x,0y then the tile 3rd tile to 180,2, etc… if you were drawing a level, you would obviously have a much larger array.

We run this code and:

GFX5

 

Pretty cool, that's pretty much all you need to know to draw a simple tile based background.  Once again, this code works just fine on every platform I tested.  You may be wondering at this point what determines your screen resolution?  The answer is once again, the application.nmml file.  If you look in yours, you should see something similar to:

<window background="#000000" fps="60" />
<window width="800" height="480" unless="mobile" />
<window orientation="landscape" vsync="true" antialiasing="0" if="cpp" />

This is saying make our window resolution 800x480 unless mobile ( in which case it will be the device resolution ), we set some additional settings if the target is cpp ( iOS, Android, Mac and Windows are ).

 

Since we have a sprite sheet full of animations, let's take a look at doing a simple animation!

 

 

 

package gfs;

import nme.Assets;
import nme.display.Bitmap;
import nme.display.DisplayObject;
import nme.display.Sprite;
import nme.display.Tilesheet;
import nme.events.Event;
import nme.geom.Rectangle;
import nme.Lib;
import nme.text.TextField;
import nme.text.TextFormat;
import nme.events.TimerEvent;


class Main extends Sprite
{
	private var currentFrame: Int = 0;
	public var timer:nme.utils.Timer;
	private  var sprites: Tilesheet;
	
	public function new()
	{
		super();
		sprites = new Tilesheet(Assets.getBitmapData("img/mechwarrior.png"));
		var tileData = new Array<Float>();
		
		for (i in 0...11)
		{
			sprites.addTileRect(new Rectangle(0, i * 80, 90, 80));
		}
		
		sprites.drawTiles(graphics, [0,0,0,6], false, Tilesheet.TILE_SCALE);
		timer = new nme.utils.Timer(100);
		
		timer.addEventListener(TimerEvent.TIMER, onTimerTick);
		
		timer.start();
	}
	
	public function onTimerTick(e:TimerEvent): Void {
		graphics.clear();
		if (++currentFrame == 11) currentFrame = 0;
			sprites.drawTiles(graphics, [0, 0, currentFrame, 6], false, Tilesheet.TILE_SCALE);
	
	}
	public static function main()
	{
		// static entry point
		Lib.current.stage.align = nme.display.StageAlign.TOP_LEFT;
		Lib.current.stage.scaleMode = nme.display.StageScaleMode.NO_SCALE;
		Lib.current.addChild(new Main());
	}
}

 

At this point most of the code should be quite familiar.  We create tiles for each frame of animation from the left column of the tilesheet, which represents a mech walk animation. One thing that might look kinda odd is our tileData.  Once again we are only drawing a single tile at the location 0,0, but you may notice there is a 4th value [0,0,0,6].  If you notice in the drawTiles call we pass the flag Tilesheet.TILE_SCALE, that is telling  drawTiles that we will be passing a 4th value representing the amount to scale by ( 6x ).  There are a number of additional parameters you can pass drawTiles, here is an excerpt from the docs:

You can also set flags for TILE_SCALE, TILE_ROTATION, TILE_RGB and TILE_ALPHA.
Depending on which flags are active, this is the full order of the array:
x, y, tile ID, scale, rotation, red, green, blue, alpha, x, y ...

So, for each flag you set, you have to pass an additional value in the float array you pass in to drawTiles.  Next up we create a timer that will fire ever 100 milliseconds.  We then create an event listener that will fire when the timer "ticks" calling the method onTimerTick.  Finally we start the timer.  In onTimerTick, each frame of animation we clear the screen and draw the next frame of animation  by incrementing the index within of the tile we want drawn by drawTiles.  If our animation exceeds the number of frames, we roll back to 0.

 

We run this code and:

Mechwalkcycle

 

Pretty cool!  This one however didn't work on every platform. :(  The timer event doesn't work on HTML.  Unfortunately the timer is only called once instead of every 100milliseconds.  This kind of stuff is rather troubling if you are trying to support multiple platforms at once, especially if you don't know Javascript well enough to troubleshoot the problem!  The key with Haxe development seems to be test early, often and on every platform you want to target.


EDIT: This bug has been fixed in the github version of NME as of April 24/th 2013.  So hopefully it will no longer effect you!

 

Fortunately there is an easy work around for this particular problem.  There is a timer class in the base Haxe library you can use.

 

 

 

public function new()
{
	super();
	sprites = new Tilesheet(Assets.getBitmapData("img/mechwarrior.png"));
	var tileData = new Array<Float>();
	
	for (i in 0...11)
	{
		sprites.addTileRect(new Rectangle(0, i * 80, 90, 80));
	}
	
	sprites.drawTiles(graphics, [0,0,0,6], false, Tilesheet.TILE_SCALE);
	
	timer = new Timer(100);
	
	timer.run = function(){
	graphics.clear();
	if (++currentFrame == 11) currentFrame = 0;
		sprites.drawTiles(graphics, [0, 0, currentFrame, 6], false, Tilesheet.TILE_SCALE);
	}
}

 

With the exception of one thing not working cross platform, I am pretty impressed by Haxe/NME's performance in the graphics category.  Rendering bitmaps, tile sheets and performing frame based animation is all rather simple. Moving on we will next look at handling input with Haxe and NME.

The next part on handling input is now online.

 

blog comments powered by Disqus

Month List

Popular Comments