









This article is continued from 2D Animation with Direct3D Part 1 and 2D Animation with Direct3D Part 2. I would like to suggest you to read the article first.
Ok let's move on to next topic, of course it's still about 2D animation. In our previous demo, player character is displayed on the top of solid color background. This kind of background is boring. 2D games usually have scrolling background that follows movement of player character. The question now is how to display scrolling background?
Source code is available to download.
Before we answer the question, we need to discuss some issues that will affect design of background scroller that we are going to make.
Background in a game especially adventure type is typically larger than screen area to allow player to explore game level.
If those big backgrounds were saved in a huge memory block (whether it is system RAM or video RAM), there is a good chance that available free memory is not large enough to contain all data of background.
We are going to split background into smaller background pieces and we will tile it over the screen. By using smaller pieces, the chance that memory will available is bigger.
At the time of this writing, some 3D graphic cards are only able to display texture with same width and height and must be 2 power n, so valid texture size is 1x1, 2x2, 4x4, 8x8, 16x16, 32x32 and so on. Some old graphic cards such as voodoo, have maximum texture size limit 256x256, newer graphic card usually can store texture with maximum size 1024x1024.
Just like our previous demo, we create texture with D3DXCreateTextureFromFileEx. This function (just like any D3DXCreateTexture*** functions) creates texture by checking size allowed by graphic card.
Our texture sizes usually are rounded up to nearest 2 power n value, so if we create texture from image120x60, there's a good chance texture size will be rounded up to 128x128, where the rest of texture not occupied by image data will be padded with 0.
This eats more resources than we need. If we are lucky, end-user might have graphic card that support any texture sizes, so we can save resources, but as developer we must prepare for the worst case.
To save memory usage. We will split background into pieces of size nxn where n is power of two. For our background scroller this will be 64x64. It's not too big and supported by any graphic cards. If you have any preferences, you can adjust it.
A background will be wrapped in a class called TBackground, derived from T3DObject. We need to override Draw method, with code to draw background pieces. Because background consists of small pieces, we need instance of TSpriteCollection and also TTextureCollection, respectively to store pieces and texture of each pieces.
For public method other than Draw and Init which is overriden from T3DObject, TBackground has constructor Create and destructor Destroy. We add code for allocation and deallocation of instance of TSpriteCollection and TTextureCollection.
Last, we add TBackground with LoadFromFile method to load background image file. File must be D3DX supported format. LoadFromFile will split image into texture pieces. We provide TBackground with Translation, X, and Y properties that we use to change background position. To be able to access Sprite collection and Texture, we add SpriteCollection and TextureCollection properties.
In some 2D games, background is not only one, they are aligned and moved with different speed to achieve depth effect.
We will implement this feature. To be able to manage these backgrounds easily, background is stored in an collection instance called TBackgroundCollection derived from T3DCollection, because we will need Engine property of T3DCollection.
Ok, let's implement TBackground. We only talk about two main features of TBackground i.e image file loading and splitting into texture pieces and drawing those pieces.
This step is easier to be done if background is already cut into small pieces with tool such as Adobe Photoshop. But it is slower to load many small files compare to loading one big file, and sometime we can't or too lazy to cut this image into smaller ones (just like me).
LoadFromFile addresses this issue. LoadFromFile automatically split big background into small textures. The step is as follow:
Note: Surface is a representation of data block in memory. The definition of surface in Direct3D is similar to surface definition in DirectDraw. However, in Direct3D, surface is type of IDirect3DSurface8, but in DirectDraw it is IDirectDrawSurface interface. To create surface we use IDirect3DDevice8.CreateImageSurface().
function CreateImageSurface(const Width, Height : Cardinal;
const Format : TD3DFormat;
out ppSurface : IDirect3DSurface8) : HResult; stdcall;
To load image file into surface we use D3DXLoadSurfaceFromFile() function,
function D3DXLoadSurfaceFromFile(const pDestSurface : IDirect3DSurface8;
const pDestPalette : PPaletteEntry;
const pDestRect : PRect;
const pSrcFile : PAnsiChar;
const pSrcRect : PRect;
const Filter : LongWord;
const ColorKey :TD3DColor;
pSrcInfo : PD3DXImageInfo) : HResult; stdcall;
Parameters:
To copy surface to another surface, we can use D3DXLoadSurfaceFromSurface(). Step 1 is wrapped in function called GetImgInfo(). It's actually a quick hack to get image info.
We create dummy surface with size2x2 pixels (actually it doesn't have to be 2x2), then we call D3DXLoadSurfaceFromFile, to load image into surface. D3DXLoadSurfaceFromFile fills image info in pSrcInfo. We use this parameter to get its width and height. Implementation is shown in the following code snippet.
function TBackground.GetImgInfo(const filename: string): TD3DXImageInfo;
var adummy:IDirect3DSurface8;
adummyRect:TRect;
begin
ZeroMemory(@result,sizeof(TD3DXImageInfo));
if (FCollection.Engine<>nil) and
(FCollection.Engine.Direct3DDevice<>nil) then
begin
FCollection.Engine.Direct3DDevice.CreateImageSurface(2,2,
D3DFMT_R5G6B5,adummy);
if adummy<>nil then
begin
adummyRect:=Rect(0,0,1,1);
D3DXLoadSurfaceFromFile(adummy,nil,
@adummyRect,
PChar(filename),
@adummyRect,
D3DX_FILTER_NONE,
0,
@result);
end;
end;
end;
After we get image information, we make temporary surface to be used for storing image.
function TBackground.LoadSurfaceFromFile(
const filename: string): IDirect3DSurface8;
var imgInfo:TD3DXImageInfo;
begin
result:=nil;
if (FCollection.Engine<>nil) and
(FCollection.Engine.Direct3DDevice<>nil) then
begin
imgInfo:=GetImgInfo(filename);
FWidth:=imgInfo.Width;
FHeight:=imgInfo.Height;
FNumSpriteX:=imgInfo.Width div BGRSIZE;
FWidthExt:=imgInfo.Width mod BGRSIZE;
if FWidthExt<>0 then
inc(FNumSpriteX);
FNumSpriteY:=imgInfo.Height div BGRSIZE;
FHeightExt:=imgInfo.Height mod BGRSIZE;
if FHeightExt<>0 then
inc(FNumSpriteY);
FCollection.Engine.Direct3DDevice.CreateImageSurface(
imgInfo.Width,
imgInfo.Height,
D3DFMT_A8R8G8B8,
result);
if result<>nil then
begin
D3DXLoadSurfaceFromFile(result,
nil,
nil,
PChar(filename),
nil,
D3DX_FILTER_NONE,
FColorKey,
nil);
end;
end;
end;
After width and height is known we calculate number of pieces needed.
Based on width and height of image, we create surface with same size as image. Not like texture, surface doesn't have any size restrictions because they are created in system memory. We set surface format as 32 bit (A=8 bit,R=8 bit B=8 bit, G=8 bit), if you have any other preferences, please feel free to change it.
If we succeed, we call D3DXLoadSurfaceFromFile to load image file to surface. Step 3 is encapsulated in LoadFromSurface method. We place LoadFromSurface in TTexture class.
procedure TTexture.LoadFromSurface(const srcSurface: IDirect3DSurface8;
const srcRect: PRect);
var aTexCollection:TTextureCollection;
surfDesc:TD3DSurface_Desc;
texSurface:IDirect3DSurface8;
begin
aTexCollection:=Collection as TTextureCollection;
if (aTexCollection.Engine<>nil) and
(aTexCollection.Engine.Direct3DDevice<>nil) and
(srcSurface<>nil) then
begin
srcSurface.GetDesc(surfDesc);
FWidth:=srcRect.Right-srcRect.Left;
FHeight:=srcRect.Bottom-srcRect.Top;
D3DXCreateTexture(aTexCollection.Engine.Direct3DDevice,
FWidth,FHeight,
0,
surfDesc.Usage,
surfDesc.Format,
D3DPOOL_MANAGED,
FTextureObj);
FTextureObj.GetSurfaceLevel(0,texSurface);
D3DXLoadSurfaceFromSurface(texSurface,
nil,
nil,
srcSurface,
nil,
srcRect,
D3DX_FILTER_NONE,
FColorKey);
end;
end;
We call D3DXCreateTexture() to create an empty surface, then we take surface on mip level 0. Note: Texture is not always composed by one image only. Often, textures is composed by one or more image with different resolution (mipmap). Level 0 is most detail image.
We copy srcSurface into texSurface, with area copied is specified by srcRect. This is complete implementation of method LoadFromFile.
For each background piece, we arrange source rectangle based on position of piece. If piece is last piece in row or column, we check if its width or height is common multiple of BGRSIZE, if not, witdh or height modulus stored in FWidthExt or FHeightExt are used to calculate source rectangle, if it is common multiple of BGRSIZE, we set width or height of piece equal to BGRSIZE (BGRSIZE=64)
procedure TBackground.LoadFromFile(const filename: string);
var asprite:TSprite;
aTexture:TTexture;
atmpSurface:IDirect3DSurface8;
i,j:integer;
srcRect:TRect;
begin
if fileExists(filename) then
begin
atmpSurface:=LoadSurfaceFromFile(filename);
for j:=0 to FNumSpriteY-1 do
begin
srcRect.Top:=j*BGRSIZE;
if (j=FNumSpriteY-1) and (FHeightExt<>0) then
srcRect.Bottom:=srcRect.Top+FHeightExt
else
srcRect.Bottom:=srcRect.Top+BGRSIZE;
for i:=0 to FNumSpriteX-1 do
begin
srcRect.Left:=i*BGRSIZE;
if (i=FNumSpriteX-1) and (FWidthExt<>0) then
srcRect.Right:=srcRect.Left+FWidthExt
else
srcRect.Right:=srcRect.Left+BGRSIZE;
aSprite:=FSprites.Add as TSprite;
aTexture:=FTextures.Add as TTexture;
aTexture.LoadFromSurface(atmpSurface,@srcRect);
aSprite.Texture:=aTexture;
end;
end;
end;
end;
Background is displayed with overriden Draw method.
procedure TBackground.Draw;
begin
FSprites.BeginDraw;
try
DrawSprites;
finally
FSprites.EndDraw;
end;
end;
procedure TBackground.DrawSprites;
var i,j,indx,offsetx,offsety:integer;
aSprite:TSprite;
begin
indx:=0;
offsetY:=0;
for j:=0 to FNumSpriteY-1 do
begin
offsetx:=0;
for i:=0 to FNumSpriteX-1 do
begin
aSprite:=TSprite(FSprites.Items[indx]);
asprite.X:=FTranslation.x+offsetx;
asprite.Y:=FTranslation.y+offsety;
aSprite.Draw;
inc(offsetx,BGRSIZE);
inc(indx);
end;
inc(offsety,BGRSIZE);
end;
end;
The actual drawing is done by DrawSprites. DrawSprites tiles pieces on the screen.
Ok that's it, source code is available to download Demo 2D Animation with Direct3D.
Do you like this article? Help this website improve by donating. Any amounts is appreciated.
Or you can help by bookmarking this page.
Bookmark this on Delicious