You are on page 1of 12

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


This game only contains a couple of graphics. Im 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 t transparency). The images are placed into a single file for simplicity:

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 click Ski.hx will be discussed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 // 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 display.Sprite; import flash.display display.BitmapData; import flash.display display.Bitmap; import flash.geom.Rectangle Rectangle; import flash.geom.Poi Point; import flash.events events.Event;

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

import import import import import

flash.events.KeyboardEvent; flash.ui.Keyboard; flash.text.TextField; flash.text.TextFormat; 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<Float>, ?inHotY:Null<Float>) { 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; }

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127

// Array of the x-coordinate of the trees. typedef TreeRow = Array<Float>; // 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<Bool>; // 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<TreeRow>; // 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;

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

// 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 = [];

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241

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);

242 mLastStep = haxe.Timer.stamp(); 243 244 245 Reset(); 246 // Slightly different message at the beginning 247 mState = SkiState.FirstRun; 248 } 249 250 // Update the top score at the end of the game, if required. function CheckTopScore(inScore:Float) 251 252 { 253 if (inScore>mTopScore) 254 { 255 mTopScore = inScore; var s = Std.int(mTopScore * 0.1); 256 mTopScoreText.text = "TopScore:" + s + "0"; 257 var w = mTopScoreText.textWidth; 258 259 mTopScoreText.width = w + 20; 260 mTopScoreText.x = 620 - w; 261 } 262 } 263 264 // Get ready to start the game again function Reset() 265 266 { 267 mPlayerX = 320; 268 mPlayerY = 20; 269 mScore = 0; 270 mStepsPerSecond = 100; 271 } 272 273 // Update one step. 274 // Is this case, we will descend one line. 275 // When the game speeds up, this will get called more often. function Update() 276 277 { 278 // Actually need to move down ? 279 if (mState==SkiState.Playing) 280 { 281 // This small bit of code defined the whole "mechanic" of the 282 game. 283 // Other things could be done here, eg acceleration or a "one 284 button" 285 // mode where you either go left-or-right, but not down. 286 // Also, mouse support could be added here. var dx = mKeyDown[ Keyboard.LEFT ] ? -1 : 287 288 mKeyDown[ Keyboard.RIGHT ] ? 1 : 0; 289 // This effectively defines the angle you go at when you turn 290 mPlayerX += dx * 0.3; 291 // Limit to screen 292 if (mPlayerX<12) mPlayerX = 12; 293 if (mPlayerX>628) mPlayerX = 628; 294 // Going down... 295 mPlayerY += 1; 296 // Loop around, to keep numbers from overflowing. 297 if (mPlayerY > mFieldHeight) 298 mPlayerY -= mFieldHeight;

299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355

// 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); } }

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412

// 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" + " [space] to go again"; else mScoreText.text = "You scored " + s + "0" + " [space] to try again"; } }

Press

Press

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

413 // This will be approximately the rate specified in the swf, but 414 usually a 415 // 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.

Wheres The Main Loop?


Coming from a more traditional programming language and being confronted with the flash documentation, its 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(); } }

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.

You might also like