Tip #10 – Flixels Internal Structure and Performance Tips
If you’ve ever wondered just what Flixel does when it starts-up or runs its main loop, then wonder no more Here’s the full gory details, with some take-away performance tips at the end.
The Instantiation Process
All games in Flixel extend the FlxGame class, which in turn extends Sprite. When the game is created the following process happens, in the following order:
- It hides the system mouse cursor
- Calls FlxG.init which clears the bitmap cache, creates a new Sprite (flashGfxSprite) and creates an empty cameras Array
- Sets the internal game frame rate
- Sets the Flash Player frame rate
- Adds an ENTER_FRAME Event Listener which triggers FlxGame.create
- Sets the step to be 1000 / Game Frame Rate. So a rate of 30 updates per second would equal a step of 33.
- Sets the Maximum Accumulation (maxAcc) value to 2000 / Flash Player Frame Rate (fps) – 1.
- So an fps rate of 30 would equal a maxAcc of 66. The maxAcc can never be less than step.
This function is only run once. It’s main responsibilities are creating the various event listeners that Flixel uses:
- It sits in a loop until it has access to root. Once it does the ENTER_FRAME event listener (created above) is removed
- The Stage scale mode is set to NO_SCALE, alignment to TOP_LEFT and the Flash Frame Rate is set (once again!)
- Mouse Up, Down and Mouse Wheel listeners are started
- Keyboard Up and Down listeners are started
- A new empty sprite called _mouse is created an added to the display list (it will contain the bitmap mouse cursor later)
- The FlxDebugger is created and added to the display list
- Creates the Sound Tray
- Creates the Lost Focus Screen (the large play arrow with the Flixel icon top left) and starts Event.ACTIVATE and DEACTIVATE listeners
- Adds an ENTER_FRAME for FlxG.onEnterFrame
Processing ends here until the next Enter Frame event fires. At that point we’re out of the creation phase and into the main loop.
The Main Loop – FlxGame.onEnterFrame
The guts of Flixel. Everything happens from here, one way or another.
- It starts by setting a var called mark to equal getTimer. The elapsedMS value is then calculated from this. That is the number of milliseconds that have elapsed since the last enter frame event occurred.
- The Sound Tray is updated
- If the SWF file has focus then it will process the core of this function, otherwise it bails out.
Core Loop Actions
- If the FlxDebugger has been created and is currently paused then it checks to see if a debug step was requested. If it was it runs step once.
- If the FlxDebugger is not paused, then the game is running normally and as such it will work out the Accumulator value. It does this by adding the elapsedMS total to it. If it’s higher than the maxAccumulator set in the creation phase then it’s set to equal maxAccumulator.
- Something very important happens here. If Accumulator is >= step then it calls the step function. It then reduces the value of Accumulator by step and checks again. It will literally loop this until the accumulator is no longer large enough to warrant running another step call. You’ll notice that when your game first starts up, due to default values step is likely to be called several times. I’ve seen the accumulator go 150, 117, 84, 51, 18 – at which point it stops. So it’ll literally run step 5 times. Typically in a normal game this isn’t the case. As the values balance out step is usually only called once or twice per loop. On some occasions, usually after some heavy garbage collection, step fails to be called at all. This is quite undesirable imho, but is how it works.
So what happens when Step is called? First it checks to see if there is a Reset Request pending. A Reset Request would occur if you had asked Flixel to change to a new FlxState. It will also always happen the very first time your game is started.
- It creates a new instance of the given FlxState
- Resets the Replay Timer
- Calls FlxG.reset
- Clears the Bitmap Cache (again :))
- Resets Input
- Destroys all sounds
- Resets various internal arrays and variables to zero
- Sets the TimeScale to 1.0
- Seeds the random number generator
- Sets the World Bounds to -10, -10, Width + 20, Height + 20
- Sets World Divisions to 6
- Creates the DebugPathDisplay Plugin
Step, continued …
After reset has finished it carries on with the Step:
- If a Replay or Replay Recording is requested, it will process that (this will never happen on the first run)
- If there is a new state (different from the current State) then switch to it (again, doesn’t happen on the first run)
- Set the ACTIVE COUNT to zero
- If Replaying it steps through the replay log, otherwise:
- Update Input: If a Key is pressed it retains the Boolean for it. If the Mouse has moved, it updates the mouse variables.
- If Recording, it records the frame
- Calls Update
- Sets the Mouse Wheel values to zero
- Calculates the Mark and FlxG.elapsed values based on the timescale. FlxG.elapsed = FlxG.timeScale * (step / 1000)
- Calls FlxG.updateSounds – Which handles panning, proximity and amplitude changes in sounds
- Calls FlxG.updatePlugins – Iterates through the plugins list and if active calls the plugin update method
- Calls FlxState.update – See below
- Calls FlxG.updateCameras – Iterates through the cameras Array (on first pass it’ll just have the one default camera in there). If the camera exists and is active it calls update on it, and the cameras flashSprite is moved based on the new x/y coordinates (if changed) or set in / visible.
- Finally if the FlxDebugger is visible it updates the timer on it.
Remember that this can all happen multiple times. Probably the most important part of it is #4 when it calls FlxState.update. FlxStates extend FlxGroup, they don’t have update functions of their own – but FlxGroup does. So every single object you add into your state (FlxSprites, FlxTileMap, etc) are all added into this one “master” group. Even other groups, Flixel doesn’t care how deeply nested they are.
FlxState.update iterates through every object in members array. Providing the object has exists = true and active = true, it calls 3 functions on it:
Different objects perform different tasks in these three functions. For example an FlxSprite in its preUpdate call will track the last known coordinates for collision purposes, and also check if the sprite was moving on a path. In postUpdate the FlxSprite will update its motion and animation if there is any. Depending on the type of Flixel object depends what happens in these 3 functions.
It’s also well worth mentioning that typically in the update function of your FlxState is where you normally put commands such as FlxG.collide and FlxG.overlap. We will cover this in more detail in a later tutorial, but it’s still useful to know exactly when in the flow they are handled.
FlxGame.onEnterFrame, continued …
After running step and update as many times as the Accumulator wants, it then calls draw once, and ends the onEnterFrame function.
The draw operation is the most expensive part of Flixel, as it’s where it renders all of the cameras, sprites and objects that build-up the visual display. It does so in the following sequence:
FlxG.lockCameras is called, iterating through all active cameras. If useBufferLocking is true, then it will lock the bitmapData per camera (note that the default for this is false). It then calls FlxCamera.fill. This does a fillRect on the fill bitmapData object using a colour value of zero, effectively wiping it clean. It then does a copyPixels from fill to FlxCamera.buffer and finally sets FlxCamera.screen.dirty to true.
An FlxState doesn’t have a draw method of its own, although you can over-ride i should you need to do something after all of the children draw operations have finished. FlxState extends FlxGroup, which does have a draw method. Whenever you add something to your State, you are effectively adding it to one giant FlxGroup.
The FlxGroup draw method iterates through all objects in the groups members Array. If the object has exists = true and visible = true then it calls their own draw function. Depending on the data type of the object depends what happens next. The following is the draw process for an FlxSprite:
- The FlxSprite iterates through all of the cameras
- For every camera it checks if the sprite is visible within that camera
- If it is, it then renders the sprite in one of two ways:
- If the sprite isn’t rotated, or is using baked rotation, and NO scaling or blend modes have been applied – then it does a copyPixel from the FlxSprites framePixels direct to the camera buffer.
- If the FlxSprite is rotated, scaled, or has a blend mode then it creates a Transform Matrix. This is then translated, scaled and rotated accordingly. Once the matrix is ready is runs FlxCamera.buffer.draw using framePixels and the matrix. This is a considerably more expensive operation, so please try and always used baked rotation.
- Finally if visualDebug is on it also draws the debug data (such as the bounding box) to FlxG.flashGfxSprite
It will do the above sequence for every single FlxSprite used in your State! And of course not just sprites – but tile maps, buttons, text, etc. So do try and be careful about what you leave laying around in memory. Always make sure you set exists or visible to false when you’re done with an object. Although Flixel is clever enough to not bother doing a copyPixel if the object isn’t in any of the cameras views anyway, it will still avoid all of those checks. And if you’ve got thousands of sprites, that’s a lot of extra baggage you can shed.
However the draw process isn’t finished yet! We’ve just locked and cleaned all the cameras, rendered all the sprites and other objects, so the final two things it does are:
- FlxG.drawPlugins. This iterates through all of the plugins, and if they are active it will call their draw methods. It’s important that when creating a plugin you decide when it’s going to process whatever it does. As you can see from this flow plugins are updated FIRST, before all of the game objects (such as your FlxSprites) are. So if you have a plugin that modifies the x/y coordinates of an FlxSprite it’s important to realise that will happen before the calls to things like updateMotion or FlxG.collide – perhaps un-doing the work of your plugin in the process. The same is true of visual changes to sprites. Basically, test, test, test! Sometimes it’s better to have plugins perform graphical changes here, in their draw function. And sometimes it’s better to do it in their update.
- Once the plugins have all drawn FlxG.unlockCameras is run. As you can imagine this iterates through all the cameras, only this time if they are visible it will call their FlxCamera.drawFX functions. This internal function handles the flash, fade and shake effects built into Flixel. If any of them are running they are applied right here. Personally I think they ought to be moved to plugins
- After it’s all shaken and faded the cameras bitmapDatas are actually unlocked. But as you may recall bufferLocking is disabled by default, so in practise this doesn’t actually do much.
All of the above happens every single frame in your game! That could be 30 or even 60 times per second. So it’s easy to see that if you aren’t careful you can be making Flixel do lots more work than it actually needs to.
My take-aways from this post are:
- If you find the Delta Timer trouble-some it’s very easy to disable. Just edit the FlxGame.onEnterFrame method and remove the _accumulator checks, and just make sure that step() is run once, and draw() is run once. This is how earlier versions of Flixel used to work, and you may find it improves performance significantly.
- Be tidy! Re-use (pool) objects. Don’t leave references laying around. If a Sprite isn’t needed, make sure you kill it properly (active / exists / visible to false) to stop it being rendered.
- If you have loads of items in a FlxGroup, but have done something like stick a great big picture over the top, then disable that group until the picture is removed. It’ll save processing on all of the group items that are obscured from view but will be rendered regardless.
- Plan your plugins carefully. It makes a significant difference if they are update or draw based. Sometimes moving the processing logic to draw actually helps.
- Don’t set your game or flash frame rates too high. Especially if you are not using a zoom mode / have a larger resolution game. Keep them at 30/30 if you can.
- Rotated FlxSprites are expensive if you don’t use baked rotation!
- Scaling and Blend Modes are expensive! Use sparingly. If you just need a sprite to be say x4 larger in size than it actually is, and the scale won’t change during play, then consider either rendering that sprite x4 bigger in your art package and having 2 versions of it – or scaling it during the create phase of your game, then using the bitmapData to make the actual game sprites from. Basically do what you can to avoid that call to draw() with the matrix transform.
- Don’t be scared to dig deep into Flixels guts and change it There are definitely things that can be improved in there!
If you’ve made it this far well done! Relax by looking at some beautiful pixel art
12 ResponsesLeave a comment
Make yourself heard
All about Photon Storm and our
HTML5 game development services
Filter our Content
- Cool Links
- Flash Game Dev Tips
- Game Development
- Geek Shopping
- In the Media
- Phaser 3