Delphi Sprite Engine – Part 1 – The Viewport.

pacman

Introduction

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

I’ve decided to write a simple sprite engine using Delphi. I’m doing this for educational purposes more than anything else, and I thought it would make an interesting series of posts for my blog, perhaps you’d like to follow along?

I’ll point out now that this is a sprite engine, not a game engine. Adding artwork, physics, timing, audio and all the other things that are needed to create a game is a larger undertaking. So what will this engine do? Well here are some goals that we’ll be  aiming towards…

  1. Ability to display 2d images.
  2. Support transparency (transparent zones).
  3. Supported on Mac OSX, Windows, Android and iOS.
  4. Hierarchical scene composition
  5. Scrolling backdrop component.
  6. Scrolling tile map component.
  7. Reuse of image resources to reduce memory costs.
  8. Support for collision detection.
  9. Must perform sufficiently to run a simple game.

(* I’ll be referring to these goals by number later, but will remind you what they are where necessary. *)

While I refer to this as a simple sprite engine (and it really is by typical sprite engine standards), this is still a significant amount of work. In order to achieve these goals, we’ll break them down and handle them one at a time, in which ever order makes the most sense.

In this first post, We’ll be building a viewport, and providing the first requirement, the ability to display 2d images. Lets get started.

The Viewport.

Before we can begin on any of the items listed in the requirements above, we need to think about the first of them in a little more detail. Displaying 2d images is relatively trivial in RadStudio in either VCL or FMX frameworks, you simply drop a TImage control onto a form and load an image into it.

The TImage component is perhaps not the best choice for our needs, for a start it stores it’s own bitmap data internally. If we ever want to display two copies of the same image, we’d have to load the bitmap data twice, once for each. TImage is also not optimized for the kind of performance we’ll need, particularly in selecting frames of animation.

So what should we use instead? The cross platform FMX framework renders everything through OpenGL for all platforms except Microsoft Windows, for that it uses DirectX. In either case, it’s rendering to a 3D API and yet it is used to display 2D controls. How it does this is a little complicated for this post, but it basically involves altering the depth transformation of a camera in 3D space, such that everything is drawn at the same distance (removing the third dimension). It’s a common technique used in rendering 2D elements in 3D graphics systems and it’s built right in to FMX for us to take advantage of.

For reasons I don’t understand, or care to understand, FMX separates out 2D forms from 3D forms, and the 2D forms do not provide access to the functionality that we need to render sprite images. Instead, we must create a 3D form and access it’s ‘Context’ property to gain access to the rendering functions.

What we’re going to do, is write a non-visual component that we can drop onto a 3D form, which will grab access to the rendering context, and use it to display our 2D images for us. This non-visual component is our ‘viewport’ and will abstract the rendering system such that our subsequent components can use it to render their content. It’s a sort of wrapper around the rendering context.

Here’s the first draft (we’re going to enhance it later):

unit unitViewport;

interface
uses
  System.Classes, // for TComponent
  FMX.MaterialSources, // for TMaterialSource
  FMX.Forms3D; // for TForm3D

type
  TViewport = class(TComponent)
  private
    fForm: TForm3D;
    fmat: TMaterialSource;
  private
    procedure SetForm(const Value: TForm3D);
    procedure RenderSomething;

  public
    constructor Create( aOwner: TComponent ); override;
    destructor Destroy; override;

    procedure Render;

  published
    property Form: TForm3D read fForm write SetForm;
    property Material: TMaterialSource read fmat write fmat;
  end;

procedure Register;

implementation
uses
  System.Math.Vectors, // for point3D
  System.Types, // for pointF
  FMX.Types3D; // for TVertexBuffer

procedure Register;
begin
  RegisterComponents('SpriteEngine', [TViewport]);
end;

{ TViewport }

constructor TViewport.Create(aOwner: TComponent);
begin
  inherited Create(aOwner);
  // Auto assign the form if it is TForm3D, calls SetForm
  if assigned(aOwner) and (aOwner is TForm3D) then begin
    Form := TForm3D(aOwner);
  end;
end;

destructor TViewport.Destroy;
begin
  Form := nil; // causes dispose of camera and dummy.
  inherited Destroy;
end;

procedure TViewport.RenderSomething;
var
  VertexBuffer: TVertexBuffer;
  IndexBuffer: TIndexBuffer;
begin
  VertexBuffer := TVertexBuffer.Create([TVertexFormat.Vertex, TVertexFormat.TexCoord0], 4);
  try
    IndexBuffer := TIndexBuffer.Create(6);
    try
      VertexBuffer.Vertices[0] := Point3D( 10,   10,   0);
      VertexBuffer.Vertices[1] := Point3D( 100,  10,   0);
      VertexBuffer.Vertices[2] := Point3D( 100,  100,  0);
      VertexBuffer.Vertices[3] := Point3D( 10,   100,  0);

      VertexBuffer.TexCoord0[0] := PointF(0, 0);
      VertexBuffer.TexCoord0[1] := PointF(1, 0);
      VertexBuffer.TexCoord0[2] := PointF(1, 1);
      VertexBuffer.TexCoord0[3] := PointF(0, 1);


      // Set Indices
      IndexBuffer[0] := 0;
      IndexBuffer[1] := 1;
      IndexBuffer[2] := 3;
      IndexBuffer[3] := 3;
      IndexBuffer[4] := 1;
      IndexBuffer[5] := 2;

      Form.Context.DrawTriangles(VertexBuffer,IndexBuffer,fMat.Material,1);
    finally
      IndexBuffer.Free;
    end;
  finally
    VertexBuffer.Free;
  end;
end;

procedure TViewport.Render;
begin
  if assigned(Form) then begin
    // Render something as a test.
    if Form.Context.BeginScene then begin
      try
        Form.Context.SetContextState(TContextState.cs2DScene);
        RenderSomething;
      finally
        Form.Context.EndScene;
      end;
    end;
  end;
end;

procedure TViewport.SetForm(const Value: TForm3D);
begin
  if Value<>Form then begin
    fForm := Value;
  end;
end;

end.

(* 
[EDIT] This code was written to target Delphi XE7. I've since tried to compile it using Delphi XE5 and discovered that there are some changes required, which are likely required in other previous versions also.
 1) Remove "System.Math.Vectors" from the uses list in the implementation section
 2) In the RenderSomething() method, replace the line which constructs TVertexBuffer with this: 
      VertexBuffer := TVertexBuffer.Create([TVertexFormat.vfVertex, TVertexFormat.vfTexCoord0], 4);
*)

Some instructions for use:

  • Drop this unit into a package (I named mine pkgSpriteEngine).
  • Build and install the package. It will register our TViewport component on the Tool Palette.
  • Start a new multi-device application (Firemonkey/FMX application in Delphi versions prior to XE7)
  • Select “3D Application” from the application wizard. (Creates a TForm3D)
  • Drop a TViewport control onto the form.
  • Drop a TTextureMaterialSource onto the form.
  • Load an image into the TTextureMaterialSource.Texture property.
  • Set the Texture property of TViewport to point at TextureMaterialSource1.
  • Ensure the TViewport.Form property has set it’s self to the form that the control is sitting on.
  • In the forms OnRender handler, put this code: Viewport1.Render;
  • Run the application.

In my case I see this:

ghost

A slightly misshapen image of a ghost from a popular classic video game. The ghost is the image that I loaded into the TTextureMaterialSource component, and it’s misshapen (slightly) because I have done nothing to account for the image dimensions when pasting it arbitrarily onto the screen.

The two methods of importance in the code above are .Render() and .RenderSomething().  Lets take a look at each of them in turn.

procedure TViewport.Render;
begin
  if assigned(Form) then begin
    // Render something as a test.
    if Form.Context.BeginScene then begin
      try
        Form.Context.SetContextState(TContextState.cs2DScene);
        RenderSomething;
      finally
        Form.Context.EndScene;
      end;
    end;
  end;
end;
  • We first check that Form is assigned, so that we can render to it.
  • We begin the rendering session with BeginScene, all of our sprites will be rendered within the BeginScene and EndScene calls. Note that BeginScene is a conditional method, for hardware reasons, it may not always be possible to update the rendering when we’d like to, but we can safely ignore a failure from BeginScene since the next call will be mere milliseconds later (eventually, when we have some timing device).
  • Then we’re setting the state of the rendering context to cs2DScene (since we’re not rendering 3D here).
  • We then call ‘RenderSomething()’ explained below.
  • Finally, we end the rendering session.

So we’re doing very little inside the Render method, we’re really just putting the context into a suitable state for rendering and then calling RenderSomething.

The RenderSomething() method in this case is a temporary method. I added RenderSomething() so that when we run the test program there’s something displayed on the screen, we’ll be replacing RenderSomething() later, with calls to render our scene. There are however, some interesting points being made within the RenderSomething() method, lets take a look.

procedure TViewport.RenderSomething;
var
  VertexBuffer: TVertexBuffer;
  IndexBuffer: TIndexBuffer;
begin
  VertexBuffer := TVertexBuffer.Create([TVertexFormat.Vertex, TVertexFormat.TexCoord0], 4);
  try
    IndexBuffer := TIndexBuffer.Create(6);
    try
      VertexBuffer.Vertices[0] := Point3D( 10,   10,   0);
      VertexBuffer.Vertices[1] := Point3D( 100,  10,   0);
      VertexBuffer.Vertices[2] := Point3D( 100,  100,  0);
      VertexBuffer.Vertices[3] := Point3D( 10,   100,  0);

      VertexBuffer.TexCoord0[0] := PointF(0, 0);
      VertexBuffer.TexCoord0[1] := PointF(1, 0);
      VertexBuffer.TexCoord0[2] := PointF(1, 1);
      VertexBuffer.TexCoord0[3] := PointF(0, 1);


      // Set Indices
      IndexBuffer[0] := 0;
      IndexBuffer[1] := 1;
      IndexBuffer[2] := 3;
      IndexBuffer[3] := 3;
      IndexBuffer[4] := 1;
      IndexBuffer[5] := 2;

      Form.Context.DrawTriangles(VertexBuffer,IndexBuffer,fMat.Material,1);
    finally
      IndexBuffer.Free;
    end;
  finally
    VertexBuffer.Free;
  end;
end;

In order to render something as simple as a rectangular image on the screen, there’s quite a lot of code here! This code is really all a consequence of the fact that we are using a three dimensional rendering API (OpenGL or DirectX) to render our two dimensional image. We have to tell the underlying API about a surface in space on which we’re going to paste our image, and that surface, though flat, is still three dimensional. Those API’s also render using ‘polygons’ or, less precisely, triangles.

So what we need to do is tell the system that we want to render two triangles which are tacked together to form a rectangle. Each triangle corner (vertex) has to be defined in three dimensional space, however, we’re rendering with no depth, so we can ignore the Z-coordinate and simply set it to zero…

tirangles

Our first triangle, in blue, is made up of vertices 0, 1 and 3. Our second triangle is made up of vertices 3, 1 and 2. So we have four vertices to provide coordinates for…

      VertexBuffer.Vertices[0] := Point3D( 10,   10,   0);
      VertexBuffer.Vertices[1] := Point3D( 100,  10,   0);
      VertexBuffer.Vertices[2] := Point3D( 100,  100,  0);
      VertexBuffer.Vertices[3] := Point3D( 10,   100,  0);

We’re using coordinates here without specifying the coordinates system. Well, ignoring the Z coordinates the X and Y datum is at the top left corner of the screen, making the top left corner (0 x 0). I’d like to tell you that each point from there on is an integer increment per pixel, but that may not be the case! We’ll take a closer look later, but for now, we simply use the seemingly arbitrary coordinates system.

Although we now have a rectangle defined in three dimensional space, at zero on the Z axis, to be projected onto the screen without depth, what will we see? The answer is nothing, we’ve not yet given the instruction to draw these triangles that make up the rectangle, and even if we had, we’ve not specified how the triangles should be drawn. We could draw the edges only, and we could select a solid color or a gradient of colors, or we could draw the faces with coloring options, but what we want here is to texture them.

When we give the instruction to draw these triangles, we’ll be providing the vertex buffer which contains the vertex location data for two triangles. In that one call, the rendering system is going to render the two triangles as a ‘batch’, and it’ll consider them as one object in 3d space. We’re also going to provide the image data with that rendering call. There’s a problem though. The API doesn’t know the size of our object in advance of being told to draw it, and it doesn’t know the size of our image either, so how does the API know how to scale the two dimensional image data over the three dimensional object (in our case a flat rectangle). Well, we have to tell the API how the image data maps onto the geometry, and we do so using texture coordinates….

      VertexBuffer.TexCoord0[0] := PointF(0, 0);
      VertexBuffer.TexCoord0[1] := PointF(1, 0);
      VertexBuffer.TexCoord0[2] := PointF(1, 1);
      VertexBuffer.TexCoord0[3] := PointF(0, 1);

Each of the four texture coordinates, from zero through three, correspond to vertices zero through three, and each contain a two dimensional location vertex.

texcoords

Each of the texture coordinates is given a value between zero and one to represent it’s position in the 2d space of the image, the unit of measure for these coordinates being image-widths in the horizontal, or image-heights in the vertical. For instance, vertex number two of the triangles is texture coordinate number two, and is given the values (1 x 1). Suppose the image is 300 pixels wide, well the X coordinate zero is at pixel coordinate zero, but the X coordinate 1 is at 300 pixels (1*image width in pixels). So if you only wanted to display the top left quarter of the image, you’d supply  PointF(0.5,0.5) where 0.5*image width in pixels = 150, so 50% of the way across the image. Get it?

I’m not sure I explained that last part very well, if you didn’t get it, try reading the previous paragraph a couple more times.

In any case, this ability to texture only a portion of the image onto our rectangle is going to come in handy for asset management when we want to animate our sprites later. We’ll supply the API with an image containing all frames of animation, and simply manipulate these texture coordinates to pick out the animation cell that we want to display. Why do it this way? I’m pleased you asked…

The 3D rendering API’s function as the client side of a client-server relationship with the graphics card, which serves as the server side. So, when we want to display relatively large chunks of data such as images, we have to upload them into the graphics card through the API. This means sending image data across the data-bus on your motherboard, which is an expensive operation in terms of time. In situations where there are hundreds or thousands of large images to be uploaded (unlikely in our sprite engine, but common in large scale game engines), it would simply take too long to upload all of the image data from memory to the graphics card for every single frame of animation, which reduces the frame rates achievable. A better strategy would be to upload all of those images to the graphics card before the rendering begins (perhaps during the loading screen of a game level), and then simply refer to the images with each frame that is rendered. The image data simply remains in the graphics card until no longer needed, and is then discarded. Our engine is not likely to need such high data loads (though I’m sure we could overload it if we tried), however, it’s still worth while observing this practice so that our CPU is freed up from the task of feeding the GPU. This is especially important on the potentially more limited hardware of a cell phone or tablet.

Finally, before drawing our triangles, we supply the API with an index buffer. This index buffer is really irrelevant in two dimensional rendering, however, it is required for 3d space…

      // Set Indices
      IndexBuffer[0] := 0;
      IndexBuffer[1] := 1;
      IndexBuffer[2] := 3;
      IndexBuffer[3] := 3;
      IndexBuffer[4] := 1;
      IndexBuffer[5] := 2;

Effectively, we’re telling the API which vertices make up the triangles by supplying the index of each vertex in the vertex buffer which makes up the triangles. You’ll remember when we defined the triangles, the first of them had vertices 0,1,3 well there are the first three entries into the index buffer. The last three entries being 3,1,2 for the second triangle. You should always supply these indices to FMX in clockwise order.

It’s time to draw our image!

Form.Context.DrawTriangles(VertexBuffer,IndexBuffer,fMat.Material,1);

We make a call to the rendering context DrawTriangles() method, supplying the vertices and texture coordinates in the vertex buffer, the indices in the index buffer, the image data from the private member fMat.Material (the TTextureMaterialSource), and then a value for the alpha channel of the rendering system. Tip: The alpha value sets the transparency of the object being drawn, values range from zero (transparent), to one (opaque).

Conclusion

That concludes this first post aimed at building a sprite engine. We’ve accomplished our first goal, and in fact, though you’ve not seen it demonstrated yet we’ve also accomplished goals 2 and 3. Lets remind ourselves what those first three goals were..

  1. Ability to display 2d images. – [DONE]
  2. Support transparency (transparent zones). – [DONE] Tried it, it works, will demonstrate later.
  3. Supported on Mac OSX, Windows, Android and iOS. – [DONE] I’ve been able to deploy to all four, but this post is not about deployment, that’ll be an exercise for the reader. That said, in a later post we may look at bundling resources with the binary executable for an easier deployment.

For now,

Thank you for reading!

Facebooktwittergoogle_plusredditpinterestlinkedintumblrmail

1 Response

  1. 2017-05-12

    […] dann komplexer wird, dann nennt man das eine Sprite-Engine. So was findest Du beispielsweise hier http://chapmanworld.com/2015/02/27/d…-the-viewport/ cu […]

Leave a Reply