Sei sulla pagina 1di 9

Delphi Sprite Engine Part 1 The Viewport.

by Craig Chapman Published 2015-02-27 Updated 2015-09-22

Introduction
[If youre new to this series, you may wish to skip ahead to part-7, where Ive done a partial start-over]
Ive decided to write a simple sprite engine using Delphi. Im doing this for educational purposes more than anything else, and I thought it
would make an interesting series of posts for my blog, perhaps youd like to follow along?
Ill 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 well be aiming towards
1.
2.
3.
4.
5.
6.
7.
8.
9.

Ability to display 2d images.


Support transparency (transparent zones).
Supported on Mac OSX, Windows, Android and iOS.
Hierarchical scene composition
Scrolling backdrop component.
Scrolling tile map component.
Reuse of image resources to reduce memory costs.
Support for collision detection.
Must perform sufficiently to run a simple game.

(* Ill 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, well break them down and handle them one at a time, in which ever order makes the most sense.
In this first post, Well 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 its own bitmap data internally. If we ever want to
display two copies of the same image, wed have to load the bitmap data twice, once for each. TImage is also not optimized for the kind of
performance well 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, its 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). Its a common technique used in rendering 2D elements in 3D graphics systems
and its built right in to FMX for us to take advantage of.
For reasons I dont 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 its Context property to gain access
to the rendering functions.
What were 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. Its a sort of wrapper around the rendering context.
Heres the first draft (were 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]
VertexBuffer.TexCoord0[1]
VertexBuffer.TexCoord0[2]
VertexBuffer.TexCoord0[3]
// Set Indices
IndexBuffer[0]
IndexBuffer[1]
IndexBuffer[2]
IndexBuffer[3]
IndexBuffer[4]
IndexBuffer[5]

:=
:=
:=
:=
:=
:=

:=
:=
:=
:=

PointF(0,
PointF(1,
PointF(1,
PointF(0,

0);
0);
1);
1);

0;
1;
3;
3;
1;
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 its 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:

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 its 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 wed 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 were setting the state of the rendering context to cs2DScene (since were not rendering 3D here).
We then call RenderSomething() explained below.
Finally, we end the rendering session.

So were doing very little inside the Render method, were 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 theres
something displayed on the screen, well 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]
VertexBuffer.TexCoord0[1]
VertexBuffer.TexCoord0[2]
VertexBuffer.TexCoord0[3]
// Set Indices
IndexBuffer[0]
IndexBuffer[1]
IndexBuffer[2]
IndexBuffer[3]
IndexBuffer[4]
IndexBuffer[5]

:=
:=
:=
:=
:=
:=

:=
:=
:=
:=

PointF(0,
PointF(1,
PointF(1,
PointF(0,

0);
0);
1);
1);

0;
1;
3;
3;
1;
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, theres 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 were going to paste our image, and that surface, though flat, is still three
dimensional. Those APIs 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, were rendering with no depth, so we can ignore the Z-coordinate and
simply set it to zero

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]
VertexBuffer.Vertices[1]
VertexBuffer.Vertices[2]
VertexBuffer.Vertices[3]

:=
:=
:=
:=

Point3D(
Point3D(
Point3D(
Point3D(

10,
100,
100,
10,

10,
10,
100,
100,

0);
0);
0);
0);

Were 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). Id like to tell you that each point from there on is an integer increment per pixel, but
that may not be the case! Well 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, weve not yet given the instruction to draw these triangles that make up the rectangle, and even if
we had, weve 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, well 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 itll consider them as one object in 3d
space. Were also going to provide the image data with that rendering call. Theres a problem though. The API doesnt know the size of our
object in advance of being told to draw it, and it doesnt 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]
VertexBuffer.TexCoord0[1]
VertexBuffer.TexCoord0[2]
VertexBuffer.TexCoord0[3]

:=
:=
:=
:=

PointF(0,
PointF(1,
PointF(1,
PointF(0,

0);
0);
1);
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.

Each of the texture coordinates is given a value between zero and one to represent its 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, youd supply PointF(0.5,0.5) where 0.5*image width in pixels = 150, so 50% of the way across the image. Get it?
Im not sure I explained that last part very well, if you didnt 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. Well 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? Im pleased you asked
The 3D rendering APIs 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 Im sure we could overload it if we tried), however, its 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]
IndexBuffer[1]
IndexBuffer[2]
IndexBuffer[3]
IndexBuffer[4]
IndexBuffer[5]

:=
:=
:=
:=
:=
:=

0;
1;
3;
3;
1;
2;

Effectively, were 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. Youll 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.

Its 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. Weve accomplished our first goal, and in fact, though youve not seen it
demonstrated yet weve 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] Ive been able to deploy to all four, but this post is not about deployment, thatll
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!

Leave a Reply
You must be logged in to post a comment.

Follow Us

Sharing

Potrebbero piacerti anche