PDA

View Full Version : Scrolling a tiled map?



neafriem
01-06-2004, 09:20 PM
I have done a tiled map where I have a character that I want to move. And that works fine until the dude walks out of the screen. I want him to be in the center at all times so that is the map that are moving. I'm quite new att game programming so try make it simple.

Alimonster
01-06-2004, 10:33 PM
You want to use a camera object. The idea is that the camera has coordinates for the currently viewed portion of the map (imagine the map being a lot bigger, and your player only ever sees a small rectangle within it). When drawing, you grab the top-left tile of the currently visible section to figure out where to start drawing (you definitely don't want to draw all the map each frame, since most of it probably won't be visible). You also need to know the offset for the first time (easiest way to visualise this: think about a tile in the top-left corner. You move slightly to the right, the tile moves slightly off-screen to the left - the value to move a tile off the screen is the offset you need).

I'll post some code now - it's from a camera class I use in my tiled graphics tutorial (work in progress, ho hum). You need to supply some constants to use it, but they should be self-evident:

unit Camera;

interface

////////////////////////////////////////////////////////////////////////////////
// //
// camera //
// //
// This contains the TCamera class, which is used to move around the tiled //
// world in an efficient manner (plus enabling other sorts of trickery later //
// on!). //
// //
// It's important to note that we will be using this class to draw __only__ //
// what is visible on the screen (a screen's worth of the tiled world). This //
// is in major contrast to many code snippets I've seen that draw the whole //
// world every frame! Don't do that -- using a camera means that your world //
// can be any size (up to a reasonable limit, of course) without changing //
// the framerate //
// //
////////////////////////////////////////////////////////////////////////////////

type
TCamera = class
private
FPixelX : Integer;
FPixelY : Integer;
FMaxWidth : Integer;
FMaxHeight: Integer;

procedure Clip;

function GetOffsetX: Integer;
function GetOffsetY: Integer;
function GetTileX : Integer;
function GetTileY : Integer;
public
constructor Create(MaxX, MaxY: Integer;
StartX: Integer = 0;
StartY: Integer = 0);

procedure CentreOnTile(TileX, TileY: Integer);
procedure CentreOnPosition(PixX, PixY: Integer);
procedure Scroll(DeltaX, DeltaY: Integer);
procedure GoToPosition(PixX, PixY: Integer);

property PixelX : Integer read FPixelX;
property PixelY : Integer read FPixelY;
property OffsetX : Integer read GetOffsetX;
property OffsetY : Integer read GetOffsetY;
property TileX : Integer read GetTileX;
property TileY : Integer read GetTileY;
property MaxWidth : Integer read FMaxWidth write FMaxWidth;
property MaxHeight: Integer read FMaxHeight write FMaxHeight;
end;

implementation

uses
Globals;

//------------------------------------------------------------------------------

//
// Create
//
// Standard constructor. This initialises the map, centring it on the pixel
// coordinates supplied
//
constructor TCamera.Create(MaxX, MaxY: Integer;
StartX: Integer = 0;
StartY: Integer = 0);
begin
inherited Create;
FPixelX := StartX;
FPixelY := StartY;
FMaxWidth := MaxX;
FMaxHeight := MaxY;

Clip;
end;

//------------------------------------------------------------------------------

//
// CentreOnTile
//
// Centres on the given **tile coordinates**
//
procedure TCamera.CentreOnTile(TileX, TileY: Integer);
begin
FPixelX := ((TileX * TILE_SIZE) + HALF_TILE_SIZE) - HALF_SCREEN_WIDTH;
FPixelY := ((TileY * TILE_SIZE) + HALF_TILE_SIZE) - HALF_SCREEN_HEIGHT;
Clip;
end;

//------------------------------------------------------------------------------

//
// Clip
//
// Ensures that the camera doesn't go out of the level's bounds
//
procedure TCamera.Clip;
begin
if FPixelX < 0 then
FPixelX := 0;

if FPixelY < 0 then
FPixelY := 0;

// gotta make sure we're not dealing with
// smaller-than-screen-size maps!
if FPixelX >= FMaxWidth - SCREEN_WIDTH then
begin
if FMaxWidth <= SCREEN_WIDTH then
FPixelX := 0
else
FPixelX := FMaxWidth - SCREEN_WIDTH - 1;
end;

// same deal for the y axis
if FPixelY >= FMaxHeight - SCREEN_HEIGHT then
begin
if FMaxHeight <= SCREEN_HEIGHT then
FPixelY := 0
else
FPixelY := FMaxHeight - SCREEN_HEIGHT - 1;
end;
end;

//------------------------------------------------------------------------------

//
// CentreOnPosition
//
// This centres on the pixel coordinates
//
procedure TCamera.CentreOnPosition(PixX, PixY: Integer);
begin
FPixelX := PixX - HALF_SCREEN_WIDTH;
FPixelY := PixY - HALF_SCREEN_HEIGHT;
Clip;
end;

//------------------------------------------------------------------------------

//
// Scroll
//
// Scrolls the view by the given pixel amounts. Negative x = left, positive
// x = right, negative y = up, positive y = down. E.g. the values
// DeltaX = -2, DeltaY = 3 would scroll the map two pixels left and
// 3 pixels down
//
procedure TCamera.Scroll(DeltaX, DeltaY: Integer);
begin
Inc(FPixelX, DeltaX);
Inc(FPixelY, DeltaY);
Clip;
end;

//------------------------------------------------------------------------------

//
// GoToPosition
//
// Moves the camera so that its top-left is at the specified pixel coordinates
//
procedure TCamera.GoToPosition(PixX, PixY: Integer);
begin
FPixelX := PixX;
FPixelY := PixY;
Clip;
end;

//------------------------------------------------------------------------------

//
// GetOffsetX
//
// Returns the "extra bit" to the left, in pixels. Imagine that we start with
// a tile directly against the left edge. If we scroll the map left or right,
// we **won't** have a tile directly against the edge. This is the offset
// that we want - same applies for the y value
//
function TCamera.GetOffsetX: Integer;
begin
Result := FPixelX mod TILE_SIZE;
end;

//------------------------------------------------------------------------------

//
// GetOffsetY
//
// See the comment for GetOffsetX - same deal here, but for the y axis
//
function TCamera.GetOffsetY: Integer;
begin
Result := FPixelY mod TILE_SIZE;
end;

//------------------------------------------------------------------------------

//
// GetTileX
//
// Returns the left-most tile index that's visible on the screen
//
function TCamera.GetTileX: Integer;
begin
Result := FPixelX div TILE_SIZE;
end;

//------------------------------------------------------------------------------

//
// GetTileY
//
// Returns the top-most tile index that's visible
//
function TCamera.GetTileY: Integer;
begin
Result := FPixelY div TILE_SIZE;
end;

end.
Create a camera object that stores the maximum size (in pixels) for your map. For example, if your map was 100 * 100, with tile size of 32, you'd pass in the values (3200, 3200) when creating the camera (remember to free it later, once you're done with it).

Now, when moving, you tell the camera object to centre on the player's current position. The camera class will plonk itself in the right position (and will be careful to avoid scrolling off of edge boundaries too - it only scrolls until it wedges against a side of the whole map).

YourCamera.CentreOnPosition(Player.X, Player.Y);

The real trick, of course, is integrating the camera into your map drawing. Unfortunately, I don't know anything about how you're currently drawing your map, so the best I can do here is to post example code for how I do it:

//
// Draw
//
// The interesting bit!
//
procedure TLevel.Draw;
var
x : Integer;
y : Integer;
DrawHere: TRect;
FirstX : Integer;
FirstY : Integer;
LastX : Integer;
LastY : Integer;
begin
FirstX := FCamera.TileX;
FirstY := FCamera.TileY;

// figure out the right-most tile we need to display
// the if..else catches smaller-than-screen-size maps
if FirstX >= FMapWidth - HORIZONTAL_TILES_ON_SCREEN then
LastX := FMapWidth - 1
else
LastX := FirstX + HORIZONTAL_TILES_ON_SCREEN;

// figure out the bottom-most tile we need to draw
if FirstY >= FMapHeight - VERTICAL_TILES_ON_SCREEN then
LastY := FMapHeight - 1
else
LastY := FirstY + VERTICAL_TILES_ON_SCREEN;

// now we draw all the tiles on the screen
DrawHere.Top := -FCamera.OffsetY; // y position on screen of current tile
DrawHere.Bottom := DrawHere.Top + TILE_SIZE;

// for every row we need...
for y := FirstY to LastY do
begin
DrawHere.Left := -FCamera.OffsetX; // x position on screen of current tile
DrawHere.Right := DrawHere.Left + TILE_SIZE;

// for every tile on this row...
for x := FirstX to LastX do
begin
// splat down the tile
ddraw_utils.Draw(Secondary, FTileset[FMap[y,x].PictureIndex], @DrawHere);

// we move one tile's width to the right on the screen for the next tile
Inc(DrawHere.Left, TILE_SIZE);
Inc(DrawHere.Right, TILE_SIZE);
end;

// move one tile's height down on the screen for the next row
Inc(DrawHere.Top, TILE_SIZE);
Inc(DrawHere.Bottom, TILE_SIZE);
end;
end;
The draw_utils.draw is where you'd splat down the actual tile to be drawn (depends on your map). The code there will change depending on what API you use (the VCL, DirectDraw, OpenGL, or whatever). It basically translates to "draw a tile picture from the tileset at the wanted rectangle".

If you don't understand anything above then please let me know. I realise that I've dumped down a lot of code, but it's the best I could manage without knowing more info about how you're doing things.

Also, I have some example projects (work in progress). If you want any (they're using DirectDraw) then drop your email address and I'll mail them to you - assuming I'm still connected to the Internet by the time I read the email (I'm living on borrowed time here and will probably lose my home net connection sometime this week for a while).

neafriem
01-06-2004, 10:43 PM
Thanks a lot. It will take som time to go through, but I probebly manage. Thanks again :D

Septimus
24-06-2004, 09:45 AM
I was just about ask that exact same question.
The camera works great, but I need one more simple procedure to save myself a lot of space.

My current project is a PC version of a dungeon hack RPG type board game. To make things easy on myself, and to be loyal to the board game, I've gotten scans of the board pieces.
These fit together randomly as the party progresses through the dungeon.

My problem is that the board pieces need to be rotated depending on which direction the dungeon is unfolding. IE, there's a corner piece that I need to use to turn north and east, north and west, south and east, south and west, ect.

I figured the easiest way to do this was to just rotate and save the bitmap with Paint Shop and load that into the image list. It works fine, but it just means that the exe goes from around 1.45mb to over 3AČ?mb. And that's only with a few of the 16+ board pieces loaded. That or the 9+ extra mb they take up in a subdirectory.

I've probably gotten into too much detail now, so here's my problem:
I found a few tutorials and code pieces showing how to rotate bitmaps to every angle under the sun. I'm really not the most mathematically inclined person so I can't figure out what the hell to do with them.
All I need is just something simple that will rotate a bitmap 90AČ?, 180AČ? and 270AČ?.

Oh, and I'm just working with the basic delphi components at the moment. I know DelphiX has DrawRotate or something (I looked that up in the forum), but I'd like to know how to do it without any extra components, even if it's more difficult.

Paulius
24-06-2004, 11:03 AM
procedure Rotate90Deg(BmpIn, BmpOut: TBitmap);
Var
InX, InY: integer;
Output, Input: pRGBTriple;
OutNext, OutBack, InPlus: integer;
begin
BmpOut.Width:= BmpIn.Height;
BmpOut.Height:= BmpIn.Width;

Output:= BmpOut.ScanLine[0];
OutNext:= Integer(BmpOut.Scanline[1]) - Integer(Output);
OutBack:= BmpIn.Width * OutNext + SizeOf(TRGBTriple);
inc(Output, BmpOut.Width-1);

Input:= BmpIn.ScanLine[0];
InPlus:= Integer(BmpIn.Scanline[1]) - Integer(Input) - BmpIn.Width * SizeOf(TRGBTriple);

for InY:= 0 to BmpIn.Height-1 do
begin
for InX:= 0 to BmpIn.Width-1 do
begin
Output^ := Input^;
Inc(Integer(Output), OutNext);
Inc(Input);
end;
Dec(Integer(Output), OutBack);
Inc(Integer(Input), InPlus);
end;
end;

Do this three times and you'll have all youre 90 180 270 degree rotated bitmaps.

Septimus
26-06-2004, 06:53 AM
Thanks Paulius, I appreciate the code but I can't get it to work.
The first bitmap is fine (I just used a LoadFromFile for that one), the second and third bitmaps (the first 2 that I pass through the procedure) end up just blank white, though the dimensions are correct.
The final bitmap has proper length/width also but only has black 'noise' covering the left 2 thirds of the picture.

This is how I've been calling it


...
BmpFireChasm&#58; Array&#91;0..3&#93; of TBitmap; //one for each direction
...
// rotate Fire Chasm.bmp
BmpFireChasm&#91;0&#93;.LoadFromFile&#40;'Dungeon\Objective Room - Fire Chasm.bmp'&#41;;
for i &#58;= 1 to 3 do
Rotate90Deg&#40;BmpFireChasm&#91;i - 1&#93;, BmpFireChasm&#91;i&#93;&#41;;


I've recently gotten http://delphi.about.com/cs/adptips2001/a/bltip1201_4.htm to work somewhat. It rotates the image fine, but this also happens to anything not square (getting worse over the next 2 passes):
http://home.iprimus.com.au/brm79/zfirechasm1.jpg

I'm wondering if I should just put up with the extra 8mb or use DelphiX, though I'd still like to do it this way.
:roll:

Paulius
26-06-2004, 03:54 PM
Works fine for me and you're code looks ok, but ... looks like trouble, a guest would be you're not specifying pf24bit bitmap format, if I'm mistaken post more code.

Septimus
26-06-2004, 09:56 PM
Dah. I should have thought of that. I'd previously tried saving the bitmaps in 16/24/32bit, but I didn't think to set it in Delphi.

Works perfectly now of course, thanks Paulius.
I can keep on going now.
:D