Archive

Author Archive

Simple flash game in haXe

February 1st, 2009

Introduction

There was a time when writing a simple game was a matter of firing up basic, writing a few “print” statements and having something that was reasonably playable. Then things got complicated, requiring plenty of “grunt” work to even get a window on the screen. Now things have gone full circle and it is again easy to get a simple game going – and they look infinitely better than the basic print statements.

The simple game here is acually a remake of a 1-line basic program (it was a long line). But where the trees were once the “#” character, now they are proper bitmap graphics, there is now a scoring system, top score, difficulty increase and it can be run on any machine in a browser. While the line count may have increased, the difficulty has not.

Free Tree Ski

Use Arrows Keys to dodge trees.

Assets

This game only contains a couple of graphics. I'm no graphics artist, but it was relatively simple to make these graphics in Inkscape, and then export a bitmap (export "only selected objects" as png, to maintain transparency).

The images are placed into a single file for simplicity:

tiles

Code

The source code is about 250 lines, plus comments. Again, this is all in a single file for simplicity. You can right-click save Ski.hx, or copy + paste from below. Details of the code will be discussed below.

// Free Ski Tree
// GM2D.com haxe code for simple game.
// I am placing this code, and associated "tiles.png" in the public domain.

// Since we are dealing with flash version 9, the general graphics
//  classes are in the "flash.display" package.
import flash.display.Sprite;
import flash.display.BitmapData;
import flash.display.Bitmap;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.filters.GlowFilter;


/*
 The Blob class (BLitter OBject) is a rectangle of pixels that can be
  quickyly and easily copied to the screen.  Perhaps a better name would
  be Sprite, but that would be too confusing.
 The main purpose of this class is to do a "copyPixels", and it just gathers
  the required data together to make this very easy.
*/
class Blob
{
   var mArena:BitmapData;
   var mBits:BitmapData;
   var mRect:Rectangle;
   var mPoint:Point;
   // The "hotspot" is the logical origin of the object, with respect
   //  to the top left of its bitmap rectangle.  This allows you to deal
   //  with the position of the object, without having to worry about drawing
   //  offsets etc.
   var mHotX:Float;
   var mHotY:Float;

   // Passing the arena into the constructor is not really required,
   //  but doing this reduces the number of params we have to pass into
   //  the Draw function;
   public function new(inArena:BitmapData,inBits:BitmapData,inX:Int, inY:Int, inW:Int, inH:Int,
           ?inHotX:Null, ?inHotY:Null)
   {
      mArena = inArena;
      mBits = inBits;
      mRect = new Rectangle(inX,inY,inW,inH);
      mPoint = new Point(0,0);
      // If null is provided, assume the centre.
      mHotX = inHotX==null ? inW/2 : inHotX;
      mHotY = inHotY==null ? inH/2 : inHotY;
   }
   public function draw(inX:Float,inY:Float)
   {
      mPoint.x = inX-mHotX;
      mPoint.y = inY-mHotY;
      mArena.copyPixels(mBits,mRect,mPoint,null,null,true);
   }
}

// All games would probably have some kind of state like this...
enum SkiState { FirstRun; Playing; Dead; }

// Array of the x-coordinate of the trees.
typedef TreeRow = Array;

// This is our main class, and in this case, it contains the whole game.
// It extends the standard flash "Sprite" class, which allows child objects
//  and events.  No need for a MovieClip, because we are not using the flash
//  timelining code.
class Ski extends Sprite
{
   // Using "copyPixels" we draw into this bitmap...
   var mArena:BitmapData;

   // What to do on an update...
   var mState : SkiState;

   // Our basic drawing objects
   var mTree:Blob;
   var mPlayerDown:Blob;
   var mPlayerLeft:Blob;
   var mPlayerRight:Blob;

   // Contains the state (whether currently pressed/down) of every key
   var mKeyDown:Array;

   // Time of last step - for calculating time deltas...
   var mLastStep:Float;

   // We increate game speed by increasing steps-per-second.
   // Note that this is independent of the flash frame rate, since we can
   //  do multiple steps per flash redraw.
   var mStepsPerSecond:Float;

   // An array of trees to ski past.  Other arrangements are possible,
   //  for example only traking the visible trees and creating new
   //  ones randomly as we go
   var mTrees:Array;

   // All position are in "field" coordinates, which are logical pixels.
   // We use the modulo operator (%) to wrap the trees around
   static var mFieldHeight = 10000;
   var mPlayerX:Float;
   var mPlayerY:Float;

   // Curreny play
   var mScore:Float;
   // Current session
   var mTopScore:Float;
   // GUI items
   var mScoreText:TextField;
   var mTopScoreText:TextField;


   // All the graphics are provided in the input image (BitmapData).
   function new(inBitmap:BitmapData)
   {
      // Since this class inherits from Sprite, we must call this.
      super();
      // Haxe does not automatically add the main class to the stage (it adds
      //  a "boot" object to the stage, which becomes "flash.Lib.current").
      //  In order to see anything, this class must be on the stage, so we
      //  add ourselves as child to the haxe boot class.  Subsequent objects
      //  (eg, the TextFields) get added to ourselves.
      flash.Lib.current.addChild(this);
      mKeyDown = [];

      // These two lines of code are the key to the "copyPixels" method of
      //  flash game creation.
      // First, an offscreen buffer (BitmapData) is created to hold all the graphics.
      // Then an instance of this is placed on the stage.  We can then simply change
      //  the offscreen buffer and have the changes visible.  This does not necessarily
      //  have to be the same size as the game, but in this case it is.
      mArena = new BitmapData(640,480);
      addChild(new Bitmap(mArena) );

      // Create or Blobs (aka sprites) as subrects of the input images.
      // The rectanges were calculated when the image was created.  If there were
      //  many more Blobs, some external data file would be a better way of
      //  getting the rectangles.
      mTree = new Blob(mArena,inBitmap,4,0,54,64,null,62);
      mPlayerLeft = new Blob(mArena,inBitmap,110,16,26,40);
      mPlayerDown = new Blob(mArena,inBitmap,70,12,26,46);
      mPlayerRight = new Blob(mArena,inBitmap,148,16,26,40);

      // I have chosen to add the event listeners to stage rather then
      //  other display objects.  Since there are no objects that will take
      //  keyboard focus, all the key events will go to the stage.
      // It is best to have a single OnEnter and do all the updates from there,
      //  so it may as well be on the stage.
      stage.addEventListener(KeyboardEvent.KEY_DOWN, OnKeyDown );
      stage.addEventListener(KeyboardEvent.KEY_UP, OnKeyUp );
      stage.addEventListener(Event.ENTER_FRAME, OnEnter);

      // Allocate row
      mTrees = [];
      mTrees[mFieldHeight-1] = null;

      // Fill up tree array.
      // This will be constant throughout the session, so the player can learn
      //  the best way down.  A random number "seed" could be used if you wanted
      //  the game to be the same on different machines.
      for(i in 0...500)
      {
         // Randomise x position.  Make sure they overlap the edges so that
         //  there is no advantage from shooting down the edge.
         var x = (Math.random()*700)-30;
         // Leave a gap for the top 250 rows so the player can start in peace.
         var y = Std.int(Math.random()*(mFieldHeight-250)) + 250;
         if (mTrees[y]==null)
            mTrees[y] = [];
         mTrees[y].push(x);
      }


      // The "GUI" consists of two TextFields overlapping the arena.
      // These do not use "copyPixels", but take advantage of some of the
      //  other benefits provided by flash display list model.
      // In a "real" game, you should use a custom embedded font, rather than
      //  some crappy default.
      mScoreText = new TextField();
      mScoreText.x = 10;
      mScoreText.y = 10;
      var format:TextFormat = new TextFormat();
      format.font = "Arial";
      format.bold = true;
      format.color = 0xffffff;
      format.size = 20;
      mScoreText.defaultTextFormat = format;
      mScoreText.text = "Score:0";
      mScoreText.width = 640;
      mScoreText.filters = [ new  GlowFilter(0x0000ff, 1.0, 3, 3, 3, 3, false, false) ];
      addChild(mScoreText);

      mTopScoreText = new TextField();
      mTopScoreText.x = 100;
      mTopScoreText.y = 10;
      format.color = 0xffffff;
      mTopScoreText.defaultTextFormat = format;
      mTopScoreText.filters = [ new  GlowFilter(0xff0000, 1.0, 3, 3, 3, 3, false, false) ];
      addChild(mTopScoreText);

      // Just something small to aspire too...
      mTopScore = 0;
      CheckTopScore(1000);

      mLastStep = haxe.Timer.stamp();

      Reset();
      // Slightly different message at the beginning
      mState = SkiState.FirstRun;
   }

   // Update the top score at the end of the game, if required.
   function CheckTopScore(inScore:Float)
   {
      if (inScore>mTopScore)
      {
         mTopScore = inScore;
         var s = Std.int(mTopScore * 0.1);
         mTopScoreText.text = "TopScore:" + s + "0";
         var w = mTopScoreText.textWidth;
         mTopScoreText.width = w + 20;
         mTopScoreText.x = 620 - w;
      }
   }

   // Get ready to start the game again
   function Reset()
   {
      mPlayerX = 320;
      mPlayerY = 20;
      mScore = 0;
      mStepsPerSecond = 100;
   }

   // Update one step.
   // Is this case, we will descend one line.
   // When the game speeds up, this will get called more often.
   function Update()
   {
      // Actually need to move down ?
      if (mState==SkiState.Playing)
      {
         // This small bit of code defined the whole "mechanic" of the game.
         // Other things could be done here, eg acceleration or a "one button"
         //  mode where you either go left-or-right, but not down.
         // Also, mouse support could be added here.
         var dx = mKeyDown[ Keyboard.LEFT ] ? -1 :
                  mKeyDown[ Keyboard.RIGHT ] ? 1 : 0;
         // This effectively defines the angle you go at when you turn
         mPlayerX += dx * 0.3;
         // Limit to screen
         if (mPlayerX<12) mPlayerX = 12;
         if (mPlayerX>628) mPlayerX = 628;
         // Going down...
         mPlayerY += 1;
         // Loop around, to keep numbers from overflowing.
         if (mPlayerY > mFieldHeight)
            mPlayerY -= mFieldHeight;
         // 1 Point per row
         mScore += 1.0;

         // Get faster as we get more points
         mStepsPerSecond = 100 + mScore * 0.01;

         // Check death...
         var row = mTrees[Std.int(mPlayerY)];
         if (row!=null)
            for(x in row)
               if ( Math.abs(x-mPlayerX) < 15 )
               {
                  // We are dead.  Stop scoring.
                  CheckTopScore(mScore);
                  mState = SkiState.Dead;
               }
      }
   }

   // Update the graphics based on class variables.
   // Note that this will be called less frequently than the "Update" call.
   // inExtra is not used in this example, because scrolling seems smooth enough.
   function Render(inExtra:Float)
   {
      // Offset all the object to keep th apparent position on the player
      //  the same.  This creates a "virtual viewport" for rendering.
      var scroll_y = mPlayerY - 60;

      // The "copyPixels" method works by clearing the buffer and then drawing
      //  the bitmaps into the offscreen buffer.
      mArena.fillRect(new Rectangle(0,0,640,480),0xe0e0ff);

      var blob = mKeyDown[ Keyboard.LEFT ] ? mPlayerLeft :
                 mKeyDown[ Keyboard.RIGHT ] ? mPlayerRight :
                                             mPlayerDown;
      blob.draw(mPlayerX, mPlayerY - scroll_y);

      // These bounds ensure the top and the bottom of all potentially visible
      //  sprites are rendered.
      for(y in -10...(480+80))
      {
         // Given the pixel position, back-calculate the field row position
         //  based on the scroll position.
         var field_y = Std.int(scroll_y + y) % mFieldHeight;
         var row = mTrees[field_y];
         if (row!=null)
         {
            for(x in row)
               mTree.draw(x,y);
         }
      }

      // Update the gui message.
      if (mState==SkiState.FirstRun)
      {
         mScoreText.text = "Press any key to start";
      }
      else if (mState==SkiState.Playing)
      {
         // Round scores to nearest 10 for display purposes
         var s = Std.int(mScore * 0.1);
         mScoreText.text = "Score:" + s + "0";
      }
      else
      {
         var s = Std.int(mScore * 0.1);
         if (mScore>=mTopScore)
            mScoreText.text = "Top Score! " + s + "0" + "    Press [space] to go again";
         else
            mScoreText.text = "You scored " + s + "0" + "    Press [space] to try again";
      }
   }


   // Respond to a key-down event.
   function OnKeyDown(event:KeyboardEvent)
   {
      // When a key is held down, multiple KeyDown events are generated.
      // This check means we only pick up the first one.
      if (!mKeyDown[event.keyCode])
      {
         // Most of the game runs off the "mKeyDown" state, but the in beginning we
         //  use the transition event...
         if (mState == SkiState.FirstRun)
            mState = SkiState.Playing;
         else if (mState == SkiState.Dead && event.keyCode==Keyboard.SPACE)
         {
            Reset();
            mState = SkiState.Playing;
         }
         // Store for use in game
         mKeyDown[event.keyCode] = true;
      }
   }

   // Key-up event
   function OnKeyUp(event:KeyboardEvent)
   {
      // Store for use in game
      mKeyDown[event.keyCode] = false;
   }

   // This function gets called once per flash frame.
   // This will be approximately the rate specified in the swf, but usually a
   //  bit slower.  For accurate timing, we will not rely on flash to call us
   //  consistently, but we will do our own timing.
   function OnEnter(e:flash.events.Event)
   {
      var now = haxe.Timer.stamp();
      // Do a number of descrete steps based on the mStepsPerSecond.
      var steps = Math.floor( (now-mLastStep) * mStepsPerSecond );
      // Since the mStepsPerSecond may change in the Update call, make sure
      //  we do all our calculations before we call Update.
      mLastStep += steps / mStepsPerSecond;
      var fractional_step = (now-mLastStep) * mStepsPerSecond;

      for(i in 0...steps)
         Update();

      // This helps flash efficiently update the bitmap, batching the changes
      mArena.lock();

      // fractional_step is something we don't use, but it could be used to do some
      //  dead-reckoning in the render code to smooth out the display.
      Render(fractional_step);

      // This completes the batching
      mArena.unlock();
   }



   // Haxe will always look for a static function called "main".
   static public function main()
   {
      // There are a number of ways to get bitmap data into flash.
      // In this case, we're loading it from a file that is placed next to the
      //  game swf.  Other ways will be described later.
      // Since the downloading of the bitmap from a remote location may take some
      //  time, flash uses an asynchronous api to delivier the data.
      //  This means that the request is sent and the data only becomes valid when
      //  the callback is called.  A loading screen would be appropriate here, but
      //  that's beyond the scope of this example, as is appropriate error checking.

      // Create the request object...
      var loader = new flash.display.Loader();
      // When the image is ready, instanciate the game class...
      loader.contentLoaderInfo.addEventListener(flash.events.Event.COMPLETE,
          function(_) { new Ski(untyped loader.content.bitmapData); });
      // Fire off the request and wait...
      loader.load(new flash.net.URLRequest("tiles.png"));
   }
}

Compiling

To compile this code, you will need a recent version of "haXe", from haxe.org. This code needs to be compiled for flash9. The easiest way to do this is to create a file called, say, "compile.hxml", and put the following lines in it:

-main Ski
-swf ski.swf
-swf-version 9
-swf-header 640:480:120:e0e0ff

The meaning of these lines is quite straight-forward. The "-main Ski" means look for a class called "Ski" in the file called "Ski.hx" and compile the static function "main", along with any other code that it requires. The "-swf ski.swf" specifies the output name, and the fact that we are compiling for flash (swf). The "-swf-version 9" is required because this code uses the flash 9+ API, which is faster because things are strongly typed, and has a different package structure to earlier versions of flash. The "swf-header" is the width,height, nominal frame rate and background colour for the flash movie.

Running "haxe compile.hxml" should then generate "ski.swf", which can be viewed from a browser.
Well, almost. You must also download tiles.png and place it in the same directory as the ski.swf.

A note about case

You must be a little bit careful with the uppercase vs lowercase when dealing with haxe and the web. When I upload files here (using wordpress) the filenames get converted to lowercase, however the server is on lunix, which is case sensitive. That is why the web based stuff, ski.swf and tiles.png, are named in lowercase. However, haxe is case sensitive, so the filename must be names Ski.hx, and all class names in the files also need to be uppercase, otherwise you get a somewhat cryptic message like "Unexpected ski" at the beginning of your class declaration. So if you run into problems, it is worth ensuring that all the files have the correct case.

Where's The Main Loop?

Coming from a more traditional programming language and being confronted with the flash documentation, it's easy think that the whole thing looks foreign and quite different from the "usual" way of writing a game.

The "usual" way might be to have a function that looks something like this:

void MainLoop()
{
   while(1)
   {
       MoveAllObjects();
       for(int i=0;i<mvisibleobjects.size();i++) mvisibleobjects[i]-="">Render();
   }
}
</mvisibleobjects.size();i++)>

But this does not exist in flash. But in reality it is not that much different.

For one thing, the render pass is simply not required, because it is implicit. I think one habit that is easy to fall into in the "traditional" game loop is to do some extra processing in the Render code, such as selecting the animation frame that is required for a certain character and mix that code in with the actual rendering of the animation frame. This is quite understandable because it is probably the easiest place to do this. It requires a very slight adjustment in thinking for flash if you are using a "many DisplayObject" engine. In flash, the extra processing of selecting the animation frame is the only processing you need to, because the DisplayObject will render itself in its own time. So the function becomes "SetupRender" rather than "Render".

The remaining difference is that the actual looping is done by flash. The easiest option is to setup an event handler on the "enter frame" event, and do one iteration of your main loop in this handler. The difference is really very minor.

The copyPixels Method

The "copyPixels method" is almost identical (if such a comparison can be made) to the traditional game loop. The code presented here uses this method.

This method differs from the "many DisplayObject method", which creates one flash object per game object. While the associated flash object provides additional functionality, such as independent scaling and event handling, it also incurs additional overhead and makes some things such as z-sorting more difficult. This is not to say that additional DisplayObjects should be used anywhere. Most GUI items are probably best handled with extra objects, and indeed this code uses extra objects for the text overlays in a simple and effective way.

The copyPixels method allocates an off-screen buffer (via flash.display.BitmapData) and renders (copyPixels) rectangles from other offscreen textures to this buffer via iterating over all the game objects. Sound familiar? It is pretty much the render code from the MainLoop function above.

A flash.display.Bitmap object is then used to make this data visible on the stage.

This code provides a "Blob" class, aka BlitterObject, aka Sprite class to make the copyPixels somewhat easier, but this is not strictly required.

Input

Game input systems can either be event based or polling based. Flash has both - you can poll the mouse position (relative to some DisplayObject) and you also receive events when the mouse moves or keys are pressed. The example here convertes the keyboard events into a "key state" array that can be used by the update code. HaXe event handling is very easy because anonymous function can easily be created to tend your every need.

Timing

The requested flash rate is not reliable, and you should do your own timing. In this example, the "enter frame" handler manages a timer and calculates how much of an update is required. One possible method is to update based on a time delta (such as position += dt * velocity), but the method used here uses discrete update steps.

Updating using discrete steps works from a update period. Say updates are to be run every 10ms, and the time between the "enter frame" event is 33ms, then 3 discrete updates are done, and 3ms are carried over into the next "enter frame" event. This means that the update steps will generally outnumber the Render steps. This code is slightly more complex because it allows for the update frequency to be increased as the game progresses, speeding the game up and making it harder.

The big advantage of using fixed update steps is that we can be sure that we wont "jump over" a tree if the player is going too fast - the player always advances one row of pixels and a tree check will be done in each step.

Improvements

This code has been simplified to keep the message clear. For example, no loading screen, no error checking, no menu etc. etc.

There are many things that could be done with this code. For example the trees could be placed according to some data file, the payer could be controlled via the mouse or via some other scheme. The graphics could be changed to convert it from a ski game to a driving game or any other variation. Online Top Score would be fun too. Post a comment if you have an interesting variation.

Top Scores

I got 28200, but think I can do better.

tutorial , , , , , ,