[If you’re new to this series, you may wish to skip ahead to part-7, where I’ve done a partial ‘start-over’]

Welcome to part 3 of this mini series on writing a Sprite Engine in Delphi. In this part we’ll finally create an animated sprite and render it to our viewport!

This part is going to be a little lengthy, make sure you have a coffee (or other favorite beverage) to hand.

Sprite Sheets.

In part 1 of this series of posts, we looked at projecting a 2D sprite onto a flat surface in 3D space. In doing so we found something interesting in the way we texture the object, that-is, because we’re able to specify texture coordinates, we don’t have to use the entire image to texture our sprite. In fact, it’s quite common practice in many graphics engines to load multiple textures into a single image, and then using texture coordinates, to select the region of the image to display. This is a common practice for many historical and technical reasons which I’ll not go into here, but, we’ll stick with this practice for our sprite engine.

My brother very kindly generated several frames of animation for me, which I pieced into the following image…

The image contains several frames of an animation in which a pigeon (small dove) flaps it’s wings. We’re going to use this image to generate the animation in our sprite engine. I’ll include the image files, and all of the source code files in a download at the foot of this post.

In order to display any one cell of this animation we’ll need two vectors. The first vector will point at the top-left corner of the cell, and the second vector at the bottom-right corner. As we’re using the texture coordinates system, which scales to unity, we’ll need to scale the vectors also. Translating pixel locations to texture locations can be done with the following formula..

Tex = (1 / S) * P

Where S is the size of the image axis in pixels, and P is the pixel location that we want.

Lets create a class to represent an animation frame, and give it the ability to calculate our texture coordinates for us…

type TAnimationFrame = class private fTexture : TRectf ; public constructor Create ( TextureWidth , TextureHeight : int32 ; spriteX , spriteY , spriteWidth , SpriteHeight : int32 ) ; reintroduce ; public property TexCoords : TRectf read fTexture ; end ;

Has only a constructor…

constructor TAnimationFrame . Create ( TextureWidth , TextureHeight : int32 ; spriteX , spriteY , spriteWidth , SpriteHeight : int32 ) ; begin inherited Create ; // Set the texture coords fTexture . Left : = ( 1 / TextureWidth ) * SpriteX ; fTexture . Top : = ( 1 / TextureHeight ) * SpriteY ; fTexture . Right : = ( 1 / TextureWidth ) * ( SpriteX + SpriteWidth ) ; fTexture . Bottom : = ( 1 / TextureHeight ) * ( SpriteY + SpriteHeight ) ; end ;

As we create this class, we pass in the dimensions of the image which contains the animation, and the pixel coordinates of the animation frame. We may then read the TexCoords property to get the texture coordinates for the animation frame.

If we have a list of TAnimationFrame, this will give us a complete animation. Lets create a class to represent an animation.

{# Represents an animation with a list of TAnimationFrame } TAnimation = class private fName : string ; fFrames : TList ; function GetCount : int32 ; function GetFrame ( idx : int32 ) : TAnimationFrame ; public constructor Create ( aName : string ) ; reintroduce ; destructor Destroy ; override ; function AddFrame ( aFrame : TAnimationFrame ) : int32 ; procedure Clear ; public property Name : string read fName ; property Count : int32 read GetCount ; property Frames [ idx : int32 ] : TAnimationFrame read GetFrame ; end ;

Implementation….

constructor TAnimation . Create ( aName : string ) ; begin inherited Create ; fName : = aName ; fFrames : = TList . Create ; end ; destructor TAnimation . Destroy ; begin Clear ; fFrames . Free ; inherited Destroy ; end ; function TAnimation . AddFrame ( aFrame : TAnimationFrame ) : int32 ; begin Result : = fFrames . Add ( aFrame ) ; end ; procedure TAnimation . Clear ; var idx : int32 ; begin for idx : = pred ( Count ) downto 0 do begin TAnimationFrame ( fFrames [ idx ] ) . Free ; end ; fFrames . Clear ; end ; function TAnimation . GetCount : int32 ; begin Result : = fFrames . Count ; end ; function TAnimation . GetFrame ( idx : int32 ) : TAnimationFrame ; begin Result : = TAnimationFrame ( fFrames [ idx ] ) ; end ;

This class represents an animation by storing a list of TAnimationFrame classes, each of which store the texture coordinates of a frame of animation. Notice that this class has a name property. The name property will be used as a unique identifier for the animation. Our sprite class will look-up an animation by name, which gives us the option to give them useful names.

Now we need to bind the animation data with the image data. Lets create one more class, the all important TSpriteSheet…

type TSpriteSheet = class ( TComponent ) private fImage : TBitmap ; fAnimations : TList ; function GetAnimation ( idx : int32 ) : TAnimation ; function GetAnimationByName ( name : string ) : TAnimation ; function GetCount : int32 ; public constructor Create ( aOwner : TComponent ) ; override ; destructor Destroy ; override ; procedure Clear ; function AddAnimation ( anAnimation : TAnimation ) : int32 ; public property Image : TBitmap read fImage ; // count of animations.. property Count : int32 read GetCount ; property Animation [ idx : int32 ] : TAnimation read GetAnimation ; property AnimByName [ name : string ] : TAnimation read GetAnimationByName ; end ;

and implementation…

function TSpriteSheet . AddAnimation ( anAnimation : TAnimation ) : int32 ; begin if not assigned ( GetAnimationByName ( anAnimation . Name ) ) then begin Result : = fAnimations . Add ( anAnimation ) ; end else begin raise Exception . Create ( 'TSpriteSheet.AddAnimation: Animation name must be unique.' ) ; end ; end ; procedure TSpriteSheet . Clear ; var idx : int32 ; begin for idx : = pred ( Count ) downto 0 do begin TAnimation ( fAnimations [ idx ] ) . Free ; end ; fAnimations . Clear ; end ; constructor TSpriteSheet . Create ( aOwner : TComponent ) ; begin inherited Create ( aOwner ) ; fAnimations : = TList . Create ; fImage : = TBitmap . Create ; end ; destructor TSpriteSheet . Destroy ; begin Clear ; fAnimations . Free ; fImage . Free ; inherited ; end ; function TSpriteSheet . GetAnimation ( idx : int32 ) : TAnimation ; begin Result : = TAnimation ( fAnimations [ idx ] ) ; end ; function TSpriteSheet . GetAnimationByName ( name : string ) : TAnimation ; var utName : string ; idx : int32 ; Ref : TAnimation ; begin Result : = nil ; utName : = Uppercase ( Trim ( name ) ) ; if utName = '' then exit ; for idx : = 0 to pred ( Count ) do begin Ref : = TAnimation ( fAnimations [ idx ] ) ; if Uppercase ( Trim ( Ref . Name ) ) = utName then begin Result : = Ref ; Exit ; end ; end ; end ; function TSpriteSheet . GetCount : int32 ; begin Result : = fAnimations . Count ; end ;

I went right ahead and added all three of these classes to the same code unit, named unitSpriteSheet, and added it to the SpriteEngine package, also registering the TSpriteSheet component on the Tool Palette.

As it stands right now, we have to create sprite sheets manually. We’d create an instance of TSpriteSheet, load our image into it’s ‘Image’ property, and for each animation add an instance of TAnimation, and for each frame of animation an instance of TAnimationFrame. This is fine for now, but later we will add methods to save a sprite sheet to a stream, and to load a sprite sheet from a stream.

Finally, lets add the spite class and build up a test application…

unit unitSprite ; interface uses classes , // for TComponent FMX . Types3D , // for TVertexBuffer FMX . Materials , unitSpriteSheet , // for TSpriteSheet unitViewportContext , unitSpriteScene ; type TSprite = class ( TSceneComponent ) private fVertexBuffer : TVertexBuffer ; fIndexBuffer : TIndexBuffer ; fSpriteSheet : TSpriteSheet ; private fMaterial : TTextureMaterial ; fFrame : int32 ; fAnimationName : string ; fAnimation : TAnimation ; fWidth : int32 ; fX : int32 ; fY : int32 ; fHeight : int32 ; procedure DoInitialize ; procedure DoFinalize ; procedure RecalculateVertices ; procedure SetAnimationName ( const Value : string ) ; procedure SetFrame ( const Value : int32 ) ; procedure SetHeight ( const Value : int32 ) ; procedure SetWidth ( const Value : int32 ) ; procedure SetX ( const Value : int32 ) ; procedure SetY ( const Value : int32 ) ; procedure RecalculateTexture ; procedure SetSpriteSheet ( const Value : TSpriteSheet ) ; public constructor Create ( aOwner : TSceneComponent ) ; override ; destructor Destroy ; procedure Render ( Context : TViewportContext ) ; override ; published property SpriteSheet : TSpriteSheet read fSpriteSheet write SetSpriteSheet ; property X : int32 read fX write SetX ; property Y : int32 read fY write SetY ; property Width : int32 read fWidth write SetWidth ; property Height : int32 read fHeight write SetHeight ; property Animation : string read fAnimationName write SetAnimationName ; property Frame : int32 read fFrame write SetFrame ; end ; implementation uses System . Math . Vectors , // for point3D System . Types ; // for pointF { TSprite } constructor TSprite . Create ( aOwner : TSceneComponent ) ; begin inherited Create ( aOwner ) ; DoInitialize ; Width : = 200 ; Height : = 200 ; end ; destructor TSprite . Destroy ; begin DoFinalize ; inherited Destroy ; end ; procedure TSprite . DoFinalize ; begin fMaterial . Free ; fIndexBuffer . Free ; fVertexBuffer . Free ; end ; procedure TSprite . DoInitialize ; begin fVertexBuffer : = TVertexBuffer . Create ( [ TVertexFormat . Vertex , TVertexFormat . TexCoord0 ] , 4 ) ; fIndexBuffer : = TIndexBuffer . Create ( 6 ) ; fMaterial : = TTextureMaterial . Create ; fAnimation : = nil ; // initial values fX : = 0 ; fY : = 0 ; fWidth : = 0 ; fHeight : = 0 ; RecalculateVertices ; // Set Indices fIndexBuffer [ 0 ] : = 0 ; fIndexBuffer [ 1 ] : = 1 ; fIndexBuffer [ 2 ] : = 3 ; fIndexBuffer [ 3 ] : = 3 ; fIndexBuffer [ 4 ] : = 1 ; fIndexBuffer [ 5 ] : = 2 ; // Recalculate texture co-ords SpriteSheet : = nil ; end ; procedure TSprite . RecalculateVertices ; begin fVertexBuffer . Vertices [ 0 ] : = Point3D ( X , Y , 0 ) ; fVertexBuffer . Vertices [ 1 ] : = Point3D ( X + Width , Y , 0 ) ; fVertexBuffer . Vertices [ 2 ] : = Point3D ( X + Width , Y + Height , 0 ) ; fVertexBuffer . Vertices [ 3 ] : = Point3D ( X , Y + Height , 0 ) ; end ; procedure TSprite . Render ( Context : TViewportContext ) ; begin if assigned ( SpriteSheet ) then begin Context . Context3D . DrawTriangles ( fVertexBuffer , fIndexBuffer , fMaterial , 1 ) ; end ; inherited Render ( Context ) ; // draw children end ; procedure TSprite . RecalculateTexture ; var X1 : double ; Y1 : double ; X2 : double ; Y2 : double ; begin if assigned ( fAnimation ) then begin if ( fAnimation . Count > 0 ) then begin // Check that the frame index is valid if ( fFrame < 0 ) then begin fFrame : = fAnimation . Count ; end else if ( fFrame > = fAnimation . Count ) then begin fFrame : = 0 ; end ; // Copy the animation frame coordinates. X1 : = fAnimation . Frames [ fFrame ] . TexCoords . Left ; Y1 : = fAnimation . Frames [ fFrame ] . TexCoords . Top ; X2 : = fAnimation . Frames [ fFrame ] . TexCoords . Right ; Y2 : = fAnimation . Frames [ fFrame ] . TexCoords . Bottom ; fVertexBuffer . TexCoord0 [ 0 ] : = PointF ( X1 , Y1 ) ; fVertexBuffer . TexCoord0 [ 1 ] : = PointF ( X2 , Y1 ) ; fVertexBuffer . TexCoord0 [ 2 ] : = PointF ( X2 , Y2 ) ; fVertexBuffer . TexCoord0 [ 3 ] : = PointF ( X1 , Y2 ) ; end ; end ; end ; procedure TSprite . SetAnimationName ( const Value : string ) ; begin fAnimationName : = Value ; fAnimation : = fSpriteSheet . AnimByName [ Animation ] ; fFrame : = 0 ; RecalculateTexture ; end ; procedure TSprite . SetFrame ( const Value : int32 ) ; begin fFrame : = Value ; RecalculateTexture ; end ; procedure TSprite . SetHeight ( const Value : int32 ) ; begin fHeight : = Value ; RecalculateVertices ; end ; procedure TSprite . SetSpriteSheet ( const Value : TSpriteSheet ) ; begin fSpriteSheet : = Value ; if assigned ( SpriteSheet ) then begin fMaterial . Texture : = TContext3D . BitmapToTexture ( SpriteSheet . Image ) ; fAnimation : = SpriteSheet . AnimByName [ Animation ] ; end else begin fAnimation : = nil ; end ; fFrame : = 0 ; RecalculateTexture ; end ; procedure TSprite . SetWidth ( const Value : int32 ) ; begin fWidth : = Value ; RecalculateVertices ; end ; procedure TSprite . SetX ( const Value : int32 ) ; begin fX : = Value ; RecalculateVertices ; end ; procedure TSprite . SetY ( const Value : int32 ) ; begin fY : = Value ; RecalculateVertices ; end ; end .

There you have it. Derived from the TSceneComponent that we wrote in the previous part, this TSprite component can easily be added to your scene through it’s constructor.

The sprite class has a ‘SpriteSheet’ property which provides the sprite with a reference to the image data, as well as the data describing the animation frames. The ‘Animation’ property is a string in which we put the name of the animation that we’d like to use from the sprite sheet, and the ‘Frame’ property indexes the relevant cell of animation to display. There are three methods of interest within the class, lets take a look at them.

procedure TSprite . RecalculateVertices ; begin fVertexBuffer . Vertices [ 0 ] : = Point3D ( X , Y , 0 ) ; fVertexBuffer . Vertices [ 1 ] : = Point3D ( X + Width , Y , 0 ) ; fVertexBuffer . Vertices [ 2 ] : = Point3D ( X + Width , Y + Height , 0 ) ; fVertexBuffer . Vertices [ 3 ] : = Point3D ( X , Y + Height , 0 ) ; end ;

RecalculateVertices is a private method which is called whenever the X or Y position, or Width or Height properties are changed. If we alter the position or size of our sprite, we need to adjust the surface that we display the sprite on, and that’s what we’re doing here. You’ll remember the vertex buffer from part 1, well its now a private member of our sprite class.

procedure TSprite . RecalculateTexture ; var X1 : double ; Y1 : double ; X2 : double ; Y2 : double ; begin if assigned ( fAnimation ) then begin if ( fAnimation . Count > 0 ) then begin // Check that the frame index is valid if ( fFrame < 0 ) then begin fFrame : = fAnimation . Count ; end else if ( fFrame > = fAnimation . Count ) then begin fFrame : = 0 ; end ; // Copy the animation frame coordinates. X1 : = fAnimation . Frames [ fFrame ] . TexCoords . Left ; Y1 : = fAnimation . Frames [ fFrame ] . TexCoords . Top ; X2 : = fAnimation . Frames [ fFrame ] . TexCoords . Right ; Y2 : = fAnimation . Frames [ fFrame ] . TexCoords . Bottom ; fVertexBuffer . TexCoord0 [ 0 ] : = PointF ( X1 , Y1 ) ; fVertexBuffer . TexCoord0 [ 1 ] : = PointF ( X2 , Y1 ) ; fVertexBuffer . TexCoord0 [ 2 ] : = PointF ( X2 , Y2 ) ; fVertexBuffer . TexCoord0 [ 3 ] : = PointF ( X1 , Y2 ) ; end ; end ; end ;

RecalculateTexture is a private method which is called any time the animation is altered in some way. Perhaps we set a new sprite sheet, or name a new animation, or alter the frame number, in all of these cases we call RecalculateTexture so that it can reference the sprite sheet data to select the appropriate piece of animation to display. We’re not actually performing any calculations here, at this point in the code we’re expecting the calculations to have already been done when the animation frames were loaded into the sprite sheet. In fact, we’ll see how that’s done a little later.

Finally, there’s the render method:

procedure TSprite . Render ( Context : TViewportContext ) ; begin if assigned ( SpriteSheet ) then begin Context . Context3D . DrawTriangles ( fVertexBuffer , fIndexBuffer , fMaterial , 1 ) ; end ; inherited Render ( Context ) ; // draw children end ;

By now, it should be quite obvious to you what this method is doing. It’s rendering our two triangles onto the context, passing the vertex buffer and index buffer (each of which is a private member of this class and pre-calculated), and then the material (prepared from the sprite sheet), in order to draw our frame of animation. You’ll notice that we still have a dangling literal 1 as a parameter for the opacity setting, we could now easily expose that as a public/published property of TSprite. After drawing our animation frame, the Render method calls the inherited Render(), which you’ll recall causes the sprite to render any children that it has.

With these new classes, we’re finally able to render an animation in our application. Lets look at the steps required to build the sample application.

Start a new Multidevice Delphi application (Firemonkey/FMX) Select a 3D application from the wizard. Drop a TViewport component onto your form. Drop a TSpriteScene component onto your form. Drop a TSpriteSheet component onto your form. Drop a TTimer component onto your form. Ensure Viewport1.Form is set to point at your main form. Set Viewport1.Scene to point at SpriteScene1 Set the timer interval to 50 Add unitSpriteSheet to your uses list. Add a private member to the form: “S: TSprite;” In the OnTimer event add “Viewport1.Render; S.Frame := S.Frame + 1;” On the forms OnCreate add the following code.. var i : int32 ; begin // Populate sprite sheet SpriteSheet1 . Image . LoadFromFile ( 'pigeon.png' ) ; i : = SpriteSheet1 . AddAnimation ( TAnimation . Create ( 'flap' ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 1 , 1 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 168 , 1 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 335 , 1 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 502 , 1 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 1 , 158 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 1 , 315 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 1 , 472 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 1 , 629 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 168 , 158 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 335 , 158 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 502 , 158 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 168 , 315 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 168 , 472 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 168 , 629 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 335 , 315 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 502 , 315 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 335 , 472 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 335 , 629 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 502 , 472 , 165 , 155 ) ) ; SpriteSheet1 . Animation [ i ] . AddFrame ( TAnimationFrame . Create ( SpriteSheet1 . Image . Width , SpriteSheet1 . Image . Height , 502 , 629 , 165 , 155 ) ) ; // Create backdrop TBackdrop . Create ( SpriteScene1 . Root ) ; // Create sprite S : = TSprite . Create ( SpriteScene1 . Root ) ; S . SpriteSheet : = SpriteSheet1 ; S . Animation : = 'flap' ; S . X : = 100 ; S . Y : = 100 ; end ;

In order to make this sample work, you’ll need to drop the pigeon sprite sheet into the executable directory. You can download it here: pigeon.

So this new initialization in the form’s OnCreate event, what are we doing here?

We start by loading the pigeon.png file into the sprite sheet with SpriteSheet1.Image.LoadFromFile().

On the very next line, we initialize an animation named ‘flap’, and then on subsequent lines we add frames of animation.

Each frame is specified with six parameters:

The width of the sprite sheet image. (SpriteSheet1.Image.Width) The height of the sprite sheet image, (SpriteSheet1.Image.Height) The X position (in pixels) of the animation cell within the SpriteSheet image. The Y position (in pixels) of the animation cell within the SpriteSheet image. The Width (in pixels) of the animation cell. The Height (in pixels) of the animation cell.

We then build up our scene by adding a TBackdrop, and a TSprite which we configure to use the SpriteSheet, the animation named ‘flap’, and we position the sprite at 100 x 100 within our scene.

Notice that we’re using the private class global member ‘S: TSprite;’ to keep a reference to our sprite. We’re doing this so that we can manually advance the sprite’s animation frame within the timer event.

Go ahead and run the application, you should see the pigeon flapping it’s wings….

You can download the source for the sprite engine package, and the test application here: SpriteEngine

As an experiment, I went ahead and added two sprites, sharing the same sprite sheet and offset their locations. As you can see in the following image, the transparent regions of each sprite are correctly rendered. This demonstrates sharing the sprite resources among sprites, and that transparency is working, at least on windows!

So you now have a sprite engine which is able to clear the screen, and play animations from a sprite sheet. It’s a little rough-and-ready, but it works as a proof of concept. Lets see which of our goals we’ve accomplished…

Ability to display 2d images. [DONE] Support transparency (transparent zones). [DONE, works on windows, to test on others.] Supported on Mac OSX, Windows, Android and iOS. [DONE, test this.] Hierarchical scene composition [DONE] Scrolling backdrop component. [To Be Done] Scrolling tile map component. [To Be Done] Reuse of image resources to reduce memory costs. [DONE] Support for collision detection. [To Be Done] Must perform sufficiently to run a simple game. [To Be Done]

We’re a little over half way there! In my next post we’ll look at creating a scrolling backdrop of some kind. Until then…

Thanks for reading!