PDA

View Full Version : Player Drawing



tronied
23-02-2003, 11:55 AM
Hi

My App currently has hit a brick wall in that I cannot seem to place anything on top of my drawn map. I have created my map (256x256 grid) by drawing onto the Canvas using :

for RowCount := 1 to 256 do
for ColCount := 1 to 256 do
DXImageList1.Items[ColCount,RowCount].Draw(DXDraw1.Surface,Sprite[ColCount,RowCount].X,Sprite[ColCount,RowCount].Y,0);

Using a 2-Dimensional array slows the drawing of the Grid by about 20fps... but It will make it easier when it comes to collision detection later on. It runs at 60fps...

Anyway, on to the problem... When I try and draw a single sprite onto that surface to represent the player, my FPS drops to about 1 or 2. I have tried my hand at creating a TDirectDrawSurface but that lags even worse where you can actually see it flickering and updating the screen. Does anyone know what I am doing wrong and possibly point me in the right direction? Maybe I am using the surfaces wrong or something else.

Thanks in Advance

Alimonster
23-02-2003, 05:38 PM
I can't really help you with the DelphX side of things, but here are some tips that will severely improve your speed.

First of all, it looks like you're trying to draw the entire map each frame! Think about this:

Say we have a map that's 256 by 256 tiles. Each tile is 32 pixels. This means that your total map size in pixels is (256 * 32), (256 * 32) -- i.e., it's 8192 x 8192 pixels. Now, I don't think that your chosen screen res is that size :wink:, which means that there's wasted effort. You only want to draw what can ever be viewed on the screen - i.e., 640 x 480 pixels worth of info (for example).

The trick, then, is to calculate what tiles can be visible on the screen and only draw them. This has another advantage - the drawing speed won't depend on how many tiles are in the map :).

One method to do this is with a camera object. The camera represents the currently visible area of the overall world (screen-sized viewport/rectangle, in other words). You move this smaller rectangle about to change what's visible on the screen.

Another thing I noticed: it seems like your loops are not cache friendly. When you have loops, you always want to do this:

for outer := ....
for inner := ...
access element[outer, inner]

Your map is a 2d grid of objects. Your method, it seems, is starting on the top left. Then, it goes down a column of tiles. Once it has reached the end of that vertical line, it goes to the next column and goes down it. You want the code to do the opposite - go left to right through a row of tiles, then down onto the next row, repeat. The cache is linear - it grabs the wanted value plus some more to the right. If you go vertically, the cache won't be very effective because all the pre-emptively grabbed values will not be next in sequence, meaning slower code :(.

I think (not having checked your code thoroughly, the above might not be the case).

I'm working on a new, much better tile tutorial. I've uploaded approx 70% complete versions of the first and second parts for, with example code for the VCL and DirectDraw. You're using DirectDraw indirectly (via DelphiX), so you should be aiming for the DDraw performance - 640x480 without any speed worries at all. The VCL code is commented more thoroughly than the DDraw one - but most of the DDraw one is from the tutorial on my site, so that's not too big a deal for me yet. (Also, note the filesize - 28K for the DDraw one. That can be reduced to 16K with UPX, which goes to show how much smaller exes can be if you're willing to avoid the VCL).

Grab the work-in-progress here: http://www.alistairkeys.co.uk/tile_tutorial_2.zip (180K)

The second part will help you with the map drawing - it shows how to set up a camera class and use it to draw only the visible part of the screen, which means very large maps for you :).

Be aware that there may be one or two bugs in the code/tutorial since it's not finished. Also, the selected tilesets suck ass ;).

HTH

Sly
24-02-2003, 03:31 AM
Another thing I noticed: it seems like your loops are not cache friendly. When you have loops, you always want to do this:

for outer := ....
for inner := ...
access element[outer, inner]

Your map is a 2d grid of objects. Your method, it seems, is starting on the top left. Then, it goes down a column of tiles. Once it has reached the end of that vertical line, it goes to the next column and goes down it. You want the code to do the opposite - go left to right through a row of tiles, then down onto the next row, repeat. The cache is linear - it grabs the wanted value plus some more to the right. If you go vertically, the cache won't be very effective because all the pre-emptively grabbed values will not be next in sequence, meaning slower code :(.

I think (not having checked your code thoroughly, the above might not be the case).It seems he has rows and columns mixed up. His for loops are correct, but his arrays are back-to-front.

tronied
24-02-2003, 08:42 AM
Thanks

The loop itself is based on the same idea as the TStringGrid.Cells[ColCount,RowCount]...

Now, this is the way I look at it but I could be wrong... using the two loops with having the Column Count in the middle loop with the Row Count on the outer it allows me to go through one row by counting horizontally until the rowcount is increased and it can move on to the next row.

I am taking your advice Alimonster about only drawing the tiles which are needed... I am attempting to do this by using the following :

var
CurrentX, CurrentY : Integer;
begin
for RowCount := 1 to 256 do //Sorry about the dodgy loop again :-)
for ColCount := 1 to 256 do
with DXDraw do
begin
if (Height div 2 > Sprite[ColCount,RowCount].Y) and
(Height div 2 < Sprite[ColCount,RowCount + 1].Y) then
CurrentY := RowCount;
if (Width div 2 > Sprite[ColCount,RowCount].X) and
(Width div 2 < Sprite[ColCount + 1,RowCount].X) then
CurrentX := ColCount;
end;
end;

That could be wrong as I am not taking it from Delphi. At the moment the X and Y's give the right value although the Y seems to be delayed for about 3 squares to update. Anyway, when I have got the correct tile I am centered on I will then use that to only draw the tiles which are visible i.e. 8 tiles out or more based on the height and width.

Thanks again

Alimonster
24-02-2003, 10:02 AM
Your drawing loop is still doing too much ;).

To draw the currently visible area of the map, we can do this:

Figure out the first tile that is visible on the screen, at the top-left position.
Calculate how far off the screen it is. Think about this: with a camera positioned at the very top-left of the world, the top-left tile would be (0,0) and it would fit snugly without any offset. However, if you scroll half a tile to the right, the top-left tile would still be [0,0], just moved a bit off the screen (i.e., you could only see its right half). If you scrolled a complete tile to the right, then the first tile would be [y=0, x=1] and there would be no offset.

So, when drawing the map, you need the following variables:

the tile x and y values in your map of the top-left-most tile
x and y values (in pixels) for calculating how far off the screen the first tile is drawn.

At that point, you know how many tiles are going to be visible on the screen (the screen size doesn't change after all!), so you can do two loops. Once you've calculated the top-left tile and offsets, you **know immediately** what tiles will be visible :). This means that your code can be simplified to a few ifs (less than 10), versus 256 * 256 * 2 ;).


for y &#58;= start_tile_y to start_tile_y + vertical_tiles_visible_on_screen
begin
for x &#58;= start_tile_x to start_tile_x + horizontal_tiles_visible_on_screen do
begin
//todo&#58; draw the current tile at wanted offset
end;
end;

You could hard-code the values for horizontal_tiles_on_screen and vertical_tiles_on_screen. In my ddraw one for 640x480 and 32x32 tiles, I used:

HORIZONTAL_TILES_ON_SCREEN = 20;
VERTICAL_TILES_ON_SCREEN = 16;

As long as the values are roughly correct then it doesn't matter. Clipping will take out anything slightly over the edge. Simply change the values until you get a complete screen of tiles (remembering about the initial offset-off-screen sometimes!). For example, try using the ddraw example code. If you change the horizontal tiles constant to 10, you'll see that only half of the screen gets filled with tiles.

The above is pseudocode-ish. Your current code is still checking over every tile - this ain't necessary, remember :).

You can nick the camera class from my tutorial (probably copy-and-paste directly from the tutorial :)). Take the ddraw example code and use the camera class there. You can remove the bit at the top of the file about waypoints and the FInternalX and FInternalY values - they're not used at present.

To use it, you'd create the camera per-level with the max width of your level:

FCamera := TCamera.Create(MapWidthInPixels, MapHeightInPixels);

(Remember to free it later.)

To get the map width/height in pixels, just multiply the width and height by your tile size (e.g. 256 * 32) for 32x32 tiles.

Other things you'll need for the code to work:

SCREEN_WIDTH - just a constant for the screen x res (e.g. 640)
SCREEN_HEIGHT - another constant for y screen res (480)
TILE_SIZE - a power of two (16 or 32, probably).
TILE_SHIFT - two-to-the-power-what = TILE_SIZE? (i.e., tile_shift = 3 for 8 pixel tiles, tile_Shift = 4 for 16 pixel tiles, or 5 for 32 pixel tiles). This is a worthless micro-optimisation.

The camera class assumes that you're using square tiles (16x16, 32x32, or whatever) and that the dimensions are powers of two. If your tiles don't fit into that category then let me know and I'll tell you how to change the code.

You'd centre on the players position using the camera's CentreOnPosition (in pixel coordinates) or CentreOnTile (in tile coordinates) method.

Both the example code zips show how to draw a map based on the current camera position - this is the same method as in the second tutorial part. Have a look at the link above, dude, and most of your questions will be answered.

Useless Hacker
24-02-2003, 10:41 AM
for RowCount := 1 to 256 do
for ColCount := 1 to 256 do
DXImageList1.Items[ColCount,RowCount].Draw(DXDraw1.Surface,Sprite[ColCount,RowCount].X,Sprite[ColCount,RowCount].Y,0);

That code won't even compile, since DXImageList.Items takes a single index.

Also, I assume you are using separate images for each tile. Since all your tiles are the same size it would be better to have one large image and use the PatternIndex parameter to the Draw functions to specify which one to draw.

tronied
24-02-2003, 11:13 AM
for RowCount := 1 to 256 do
for ColCount := 1 to 256 do
DXImageList1.Items[ColCount,RowCount].Draw(DXDraw1.Surface,Sprite[ColCount,RowCount].X,Sprite[ColCount,RowCount].Y,0);

That code won't even compile, since DXImageList.Items takes a single index.

Also, I assume you are using separate images for each tile. Since all your tiles are the same size it would be better to have one large image and use the PatternIndex parameter to the Draw functions to specify which one to draw.

Whoops, my mistake... I put it in wrong. I have got some interesting reading to do tonight, thanks all.

Traveler
24-02-2003, 12:34 PM
I've mixed up a tutorial in preparation of my platform tutorial a couple weeks ago. It does exactly what you want to do.

Have a look in the tutorial section of my website (http://www.gameprogrammer.net)if you're interested...

tronied
24-02-2003, 03:43 PM
I've mixed up a tutorial in preparation of my platform tutorial a couple weeks ago. It does exactly what you want to do.

Have a look in the tutorial section of my website (http://www.gameprogrammer.net)if you're interested...

Will do, thanks

tronied
26-02-2003, 12:25 PM
Hi again

Sorry to be a pain but I have another problem :(

I have now got it running at 170fps while not moving and 110fps while moving... Not bad... Anyway, my question is that when I draw the main character on top of the grid it isnt transparent. I have set the `DXImageList.Items[CharPic].Transparent = true` but it hasnt made any difference. Is there a particular colour that it recognises for changing it transparent...? currently I am using white.

Thanks

Traveler
26-02-2003, 12:30 PM
I'm not sure which drawing methode you are using, but I think the following should work:

Dxdraw1.Surface.Draw(0, 0, yourspritesurface.clientrect, yourspritesurface, true);

User137
26-02-2003, 12:57 PM
The best transparent color is Black. I've seen other colors having problems, when changing color manually. I think proper color would come when you get pixel from DXSurfaceCanvas of picture (not from DXSurface.Pixels).

These colors may not be in form $FFFFFF which normally would be white.

BojZ
26-02-2003, 02:02 PM
The best transparent color is Black. I've seen other colors having problems, when changing color manually. I think proper color would come when you get pixel from DXSurfaceCanvas of picture (not from DXSurface.Pixels).

These colors may not be in form $FFFFFF which normally would be white.

In my experience the best color for transparency is MAGENTA as it is easily replaced by other colors. Black, on the other hand, is used a lot, to border the sprites, for example. I've also encountered problems using black for transparency on WinME, dunno why, though, as the game worked fine on other OSs.

I select the transparent color by checking the color of the pixel[1,1] and it works fine.

Traveler
26-02-2003, 02:18 PM
I think is something we can debate about. In my opinion it doesn't really matter which color you choose as a background color. It all depends on the image itself. If you have one with a lot of purple in it, then magenta isn't the color either...

Anyway, I discovered that selecting a transparency color doesn't always work at diferent color depths.

When selecting the color by looking at pixel[x,y] you might want to try typecasting it to a word

if (word(YourSurface.Pixels[X,Y]) = 65535) then showmessage('you have clicked on a white pixel');

Useless Hacker
27-02-2003, 12:54 PM
The TransparentColor property of TDXPictureCollectionItem is a Delphi TColor, so you can simply use $00FF00FF for magenta and it should work. For TDirectDrawSurface, however, TransparentColor is in the format of the surface. You can use the ColorMatch method to set the TransparentColor of a surface to a TColor value, like so:
Surface.TransparentColor := Surface.ColorMatch($00FF00FF);