Buildings in a 2D tilebased RPG

Started by
6 comments, last by ollyb342 14 years, 11 months ago
Hey guys, I'm programming my first game using SDL and C++, I'm attempting to create a 2D RPG/action game. At the minute I've dubbed the game as "Pokélda" due to the fact that I want the gameplay to be like the original NES version of Zelda, but I'd like a story/role like the Gameboy Pokémon games. Anyway, the problem I have is as follows. I have a 2D integer array to handle each of the tiles, and a seperate boolean array which holds thier collision values (for example ROCK tile is solid, while GRASS tile is not). using this format, how would I manage buildings and caves etc? for example there would be a door on the map, when the user steps through the door it should transport them to another room, then when they go back through the door it should take them to the map where they were previously, to the exact same X and Y coordinates as before. Any ideas guys? As it stands I'm really enjoying this, I've written it all in one source file atm and it's not object oriented yet! It will be open source, so if anyone fancies having a browse at my source then let me know. Ollie
Advertisement
How about, instead of having an array of bools for walls, have an array of structs.

That struct contains several different types of invisible tiles, like 'WALL', 'TELEPORTER', or 'NONE'. When the player moves, don't check if the bool is true, check if the invisible tile is set to 'WALL'. You'll also have to store information in the struct, for if it IS a teleporting, where the teleporter goes.

This also makes it easy for you to expand; if you want a new type of invisible tile, like 'BEGIN_BATTLE' for Cukoo trainers challenging the player, you just add the new type, and then add what happens when the player tries to step on that tile.

Alternatively, you could keep your array of bools, just for walls (since walls will be most common), and have a std::vector of something like this:
struct SpecialTile{    int x, y; //The location on the map of this special tile.        TILE_TYPE type; //A enum of the type of the special tile. (TELEPORTER, BEGIN_BATTLE, STORE_OWNER, etc...)}
When the player moves, first check if he collides with the walls, using your array of bools. If he doesn't, loop through the vector, and see if he's stepping on one of the special tiles, by checking the x and y location of that tile.

You'll also need to store some parameters for the different tiles. For instance, TELEPORTER will need to know where it teleports to. Since you are using C++, you can do this with polymorphism. 'SpecialTile' would be a base class from which 'TeleporterTile' inherits, with a virtual function 'OnPlayerTouch()' and their own parameters.

If you aren't yet familiar with class inheritance, you could also do it in different ways, like using a Union in the struct, or keeping several pointers to different special tiles parameter structs, but leaving them NULL, unless it's that type of tile.
struct SpecialTile{    int x, y; //The location on the map of this special tile.        TILE_TYPE type; //A enum of the type of the special tile. (TELEPORTER, BEGIN_BATTLE, STORE_OWNER, etc...)    TeleporterParameters *teleporterParameters; //Is NULL, unless 'type' is 'TELEPORTER'    BeginBattleParameters *beginBattleParameters; //Is NULL, unless 'type' is 'BEGIN_BATTLE'    //more parameters for more types of tiles.}
Wow.. that sounds really really complicated.

How would I actually define that a tile is a transporter AND that it's got a certain texture attached to it.. I'm a bit stumped about this.

If it would help at all I can post all of my source code to date so you could try to show me in context?

I really appreciate the help. cheers for this

Ollie.
Hey guys, another shameless bump thread!

I've commented my code a little bit, and I'll post it below.

If anyone could offer me a method of having buildings in the game (in context with my code) then I'd really appreciate it.

Code:

#include <iostream>#include <SDL/SDL.h>#include <SDL/SDL_image.h>#include <fstream>#include <sstream>#include "Timer.h"using namespace std;//window propertiesconst int width = 800;const int height = 600;const int bpp = 32;//person propertiesconst int personWidth = 17;const int personHeight = 20;const int tileSize = 32;//the number of different  types of tileconst int numTiles = 11;const int FPS = 30;//the various tile typesconst int black = 0;const int tree = 4;const int rockCorner = 6;const int rock = 2;const int grass = 1;const int dirt = 3;const int dirtTopLeftCorn = 7;const int dirtTopRightCorn = 9;const int dirtBottomLeftCorn = 8;const int dirtBottomRightCorn = 10;const int water = 5;//wanky idea I had to set the initial x and y positionconst int persOrigin = 53;//map arrayint map[50][60];//collision arraybool collide[numTiles];									SDL_Rect personRect;SDL_Rect camera;SDL_Rect personClipRect;SDL_Rect enviroClipRect;SDL_Surface *screen = NULL;SDL_Surface *person = NULL;SDL_Surface *enviroSprites = NULL;SDL_Surface *canvas = NULL;bool done;//surface loaderSDL_Surface* loadImages(string filename){	SDL_Surface *temp = IMG_Load(filename.c_str());	SDL_Surface *optimo = SDL_DisplayFormat(temp);	return optimo;}void draw(){	for(int y = 0; y < 50;y++)	{		for(int x = 0; x < 60; x++)		{			SDL_Rect rect;			rect.x = x * tileSize;			rect.y = y * tileSize;						enviroClipRect.x = 0;            //blit the grass to the screen			SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			if(map[y][x] == rock)			{				enviroClipRect.x = 32;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == rockCorner)			{				enviroClipRect.x = 32 * (rockCorner - 1);				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == tree)			{				enviroClipRect.x = 96;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == dirtTopLeftCorn)			{				enviroClipRect.x = (dirtTopLeftCorn - 1) * 32;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == dirtTopRightCorn)			{				enviroClipRect.x = (dirtTopRightCorn - 1) * 32;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == dirtBottomLeftCorn)			{				enviroClipRect.x = (dirtBottomLeftCorn - 1) * 32;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == dirtBottomRightCorn)			{				enviroClipRect.x = (dirtBottomRightCorn - 1) * 32;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == dirt)			{				enviroClipRect.x = 64;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == water)			{				enviroClipRect.x = 128;				SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);			}			else if(map[y][x] == black)			{				SDL_FillRect(canvas,&rect,SDL_MapRGB(screen->format,0,0,0));							}					}	}}//the map loadervoid loadMap(string mapName){	ifstream mapFile(mapName.c_str());			if(mapFile == NULL)	{		cout << "PHAIL";		SDL_Quit();	}		for(int y = 0; y < 50; y++)	{		for(int x = 0; x < 60; x++)		{			SDL_Rect rect;			rect.x = x * tileSize;			rect.y = y * tileSize;						mapFile >> map[y][x];						//again, the wanky way of setting the persons x and y initially			if(map[y][x] == persOrigin)			{				personRect.x = rect.x;				personRect.y = rect.y;							}		}	}}//up = 1; right = 2; down = 3; left = 4//little function to handle the movement and collisionsvoid handleInput(int dir){		//up	if(dir == 1)	{				personRect.y -= 10;		if(collide[map[(personRect.y) / tileSize][(personRect.x / tileSize)]] == true)		{			personRect.y += 10;			return;		}		if(collide[map[(personRect.y) / tileSize][(personRect.x + (personWidth - 2)) / tileSize]] == true)		{			personRect.y += 10;			return;		}			}		//right	if (dir == 2)	{		personRect.x += 10;			if(collide[map[personRect.y / tileSize][(personRect.x + personWidth) / tileSize]] == true)			{				personRect.x -= 10;				return;			}				if(collide[map[(personRect.y + (personHeight - 2)) / tileSize][(personRect.x + personWidth) / tileSize]] == true)			{				personRect.x -= 10;				return;			}			}		//down	if (dir == 3)	{		personRect.y += 10;		if(collide[map[(personRect.y + (personHeight-2)) / tileSize][(personRect.x / tileSize)]] == true)		{			personRect.y -= 10;			return;		}				if(collide[map[(personRect.y + (personHeight-2)) / tileSize][(personRect.x + (personWidth - 2)) / tileSize]] == true)		{			personRect.y -= 10;			return;		}	}		//left	if (dir == 4)	{				personRect.x -= 10;				if(collide[map[personRect.y / tileSize][(personRect.x)/ tileSize]] == true)		{			personRect.x += 10;			return;		}				if(collide[map[(personRect.y + (personHeight - 2)) / tileSize][(personRect.x) / tileSize]] == true)		{			personRect.x += 10;			return;		}			}	}//move the scrolling cameravoid setCamera(){		camera.x = (personRect.x + (personWidth /2)) - (width / 2);		camera.y = (personRect.y + (personHeight /2)) - (height / 2);				if (camera.x < 0)		{			camera.x = 0;		}		if(camera.x >= (60*tileSize) - width)		{			camera.x = (60*tileSize) - width;		}		if(camera.y < 0)		{			camera.y = 0;		}		if(camera.y >= (50*tileSize) - height)		{			camera.y = (50*tileSize) - height;		}				return;}//do the initializationvoid init(){    /* Initialize SDL */    if (SDL_Init (SDL_INIT_VIDEO) < 0)    {        exit (1);    }    atexit (SDL_Quit);    screen = SDL_SetVideoMode (width, height, bpp, SDL_SWSURFACE | SDL_DOUBLEBUF);    if (screen == NULL)    {        exit (2);    }    SDL_WM_SetCaption ("PokéZelda", NULL);    SDL_EnableKeyRepeat(1,0);    person = loadImages("images/person.png");    enviroSprites = loadImages("images/environmentSprites.png");        canvas = SDL_CreateRGBSurface(SDL_SWSURFACE,60 * tileSize, 50 * tileSize, 32,0,0,0,0);            SDL_SetColorKey(enviroSprites,SDL_SRCCOLORKEY, SDL_MapRGB(enviroSprites -> format,255,0,255));    SDL_SetColorKey(person,SDL_SRCCOLORKEY, SDL_MapRGB(person -> format,255,0,255));             personRect.x = 60;	personRect.y = 350;	camera.x = 0;	camera.y = 0;	camera.w = 800;	camera.h = 600;    done = false;        personClipRect.w = 15;    personClipRect.h = 20;        enviroClipRect.x = 0;    enviroClipRect.y = 0;    enviroClipRect.w = 32;    enviroClipRect.h = 32;        //load "level4" initially	loadMap("level4.map");	}//set the collision types initiallyvoid setCollisions(){collide[black] = false;collide[grass] = false;collide[rock] = true;collide[dirt] = false;collide[tree] = true;collide[water] = false;collide[rockCorner] = true;collide[dirtTopLeftCorn] = false;collide[dirtBottomLeftCorn] = false;collide[dirtTopRightCorn] = false;collide[dirtBottomRightCorn] = false;}int main (int argc, char *argv[]){	Timer fps;		init();	setCollisions();	    //set all keys to false, or unpressed		bool keys[256];	for(int i = 0; i < 256; i++)	{		keys = false;	}    SDL_Event event;    while (!done)    {				fps.start();        while (SDL_PollEvent (&event))        {            switch (event.type)            {		    case SDL_KEYDOWN:													keys[event.key.keysym.sym] = true;				break;								case SDL_KEYUP:				keys[event.key.keysym.sym] = false;				break;			            case SDL_QUIT:                done = 1;                break;            default:                break;            }                    }        			//move player/check collision and set the sprite in the sprite sheet			if(keys[SDLK_UP] == true)			{				handleInput(1);				if(personClipRect.x == 75)				{					personClipRect.x = 90;				}				else				{					personClipRect.x = 75;				}			}			else if(keys[SDLK_RIGHT] == true)			{				handleInput(2);				if(personClipRect.x == 105)				{					personClipRect.x = 120;				}				else				{					personClipRect.x = 105;				}							}			else if(keys[SDLK_LEFT] == true)			{;				handleInput(4);									if(personClipRect.x == 45)				{					personClipRect.x = 60;				}				else				{					personClipRect.x = 45;				}			}			else if(keys[SDLK_DOWN] == true)			{				handleInput(3);				if(personClipRect.x == 15)				{					personClipRect.x = 30;				}				else				{					personClipRect.x = 15;									}			}			draw();		setCamera();        SDL_BlitSurface(person,&personClipRect,canvas,&personRect);        SDL_BlitSurface(canvas,&camera,screen,&screen -> clip_rect);        SDL_UpdateRect(screen, camera.x,camera.y,width,height);        SDL_Flip(screen);                if(fps.getTicks() < (1000 / FPS))        {			SDL_Delay(( 1000 / FPS) - fps.getTicks());		}    }    return 0;}


Let me know what you think!

Ollie.
My TileMap system has the following types:

enum TileType{  Empty = 0,  ... etc}


TileType is tied pretty much directly to the Sprite I use for the tile - Empty means none.

I then have the following information in a lookup (I use C#, you could use an array)).

public class TileTypeInfo{  public TileType Type { get; set; }  public bool Collidable { get; set; }  public bool Destructible { get; set; }}


This holds information about the tile itself - such as whether it's collidable, destructible or not - you can add all sorts of stuff here if you wanted.

I then have the MapTile2D entry itself - in my system it's a struct that holds more information (I have per-tile overrides), but you could simply refer to your tiles as TileType integers.

TileType []tileData = new TileType[colCount,RowCount];


Or in more C++ like terms:

unsigned char *tileData = new unsigned char[numcols * numrows];TileTypeInfo tileInfo[] = {{ 0, 0, 0 },{ 1, 1, 0 },// etc};


You would then do a lookup to test if the tile is collidable, using something like:

int currentTile = somenumber;if (tileInfo[currentTile].Collidable == true){  // collision stuff}else{  // no collision}



I'm using XNA and C#, but the principle is the same.

There's a few mistakes in your code, which you might want to fix. I'll just list them real quick:

First, you shouldn't call SDL_Quit() to exit when an error occurs; you should free your resources. For every SDL_Surface you load, you should call SDL_FreeSurface() on it when your game ends.

Instead of calling SDL_Quit(), you could do something like this: (Not the best way to do things, but it's easy enough for you to use in your program)
void ExitProgram(){    //Free your images.    SDL_FreeSurface(person);    SDL_FreeSurface(grassTile);    ... more stuff to free ...        //Close SDL.    SDL_Quit();        //Exit the program.    exit(0);}

Instead of calling SDL_Quit(), or instead of calling exit(), you can call ExitProgram() to exit in a better way (no memory leaks). Also, call ExitProgram() before 'return 0' in 'main()'.

You also need to call SDL_FreeSurface() on the temporary image you pass to SDL_DisplayFormat().
SDL_Surface* loadImages(string filename){	SDL_Surface *temp = IMG_Load(filename.c_str());	SDL_Surface *optimo = SDL_DisplayFormat(temp);        SDL_FreeSurface(temp);	return optimo;}


If those are too complicated, just forget it. Sometimes it's best just to get your program running, otherwise you'll lose motivation if your program doesn't work.

Also, instead of keeping all your tiles in one image, it'll be easier for you to manage if you have them each in their own image. Example:
SDL_Surface *grassTile;SDL_Surface *dirtTile;SDL_Surface *sandTile;

Instead of 'enviroSprites'. This way, you don't need to do any weird math.
Instead of this:
else if(map[y][x] == dirtTopRightCorn){	enviroClipRect.x = (dirtTopRightCorn - 1) * 32;	SDL_BlitSurface(enviroSprites,&enviroClipRect,canvas,&rect);}

You can do this:
else if(map[y][x] == dirtTopRightCorn){	SDL_BlitSurface(dirtTopRightCornerTile, NULL, canvas, &rect);}

This way, you're less likely to make mistakes, and it's easier for you to manage. Easier to read, is easier to expand later. But if your way is easy to understand, then whatever works.

In answer to your question, 'how do I make a transporter', you could do it the right way, but as you said, it's complicated at your current level. Instead, might I suggest a tacked on, sloppy way? [smile] It'll be better for you to use a sloppy way you understand, then for you to use a 'correct' way, that you don't understand; as long as you realize that it's sloppy, because at least you'll learn from it.

(Make sure you save a copy of your program before you make any changes, incase anything goes wrong)

I recommend you handle it in the same way you are handling the player's spawn point: have a special tile type, called 'transporter'. Let's assume you'll only four transporters into and out of any map (you could add more later). If we take your code for the player's starting position, You could expand it for transporters as well:
const int persOrigin = 53;const int transporter1 = 101;const int transporter2 = 102;const int transporter3 = 103;const int transporter4 = 104;

We'll use these tiles for warping to and for warping from.

But we also need to store what map/level they warp the player to. So we need four more variables, that are not 'const'.
int transporterMap1 = 0;int transporterMap2 = 0;int transporterMap3 = 0;int transporterMap4 = 0;


You'll need a new global variable, called 'lastTransporterEntered', to know which transporter the player walked in. Make sure, when starting the game, you have it set to 0, so the player spawns at 'persOrigin' instead of a transporter. We'll also create a new variable, that's a 'bool' called 'changingMap'. We set it to true when the player steps on a transporter, so we know to change the map. Make sure to set it to false.
int lastTransporterEntered = 0;bool changingMap = false;


In the 'loadMap()' function, you do this:
//You already had this code:if(map[y][x] == persOrigin) //If this tile is the player's origin...{	if(lastTransporterEntered == 0) //AND if the player didn't walk into a transporter...	{		personRect.x = rect.x;		personRect.y = rect.y;	}}//But this part is new:if(map[y][x] == transporter1) //If this tile is transporter 1...{	if(lastTransporterEntered == transporter1) //AND if the player entered transporter 1 on the map he just left...	{		personRect.x = rect.x;		personRect.y = rect.y;		mapFile >> transporterMap1; //Get the map number to transport the player to, when he steps on this transporter.	}}//Again but for transporter 2:if(map[y][x] == transporter2) //If this tile is transporter 2...{	if(lastTransporterEntered == transporter2) //AND if the player entered transporter 2 on the map he just left...	{		personRect.x = rect.x;		personRect.y = rect.y;		mapFile >> transporterMap2;	}}//Do the same thing for transporter 3 and 4.


There! That makes the player start at the 'transporter1' tile on a map, when a map is loaded, but only if 'lastTransporterEntered' equals 1. Understand? (If not, ask questions!)

In your 'level.map' files, transporters have an extra number! Most of your tiles have one number, like '5' for your water tile. But transporters have two numbers, so be careful! First, you have the transporter number, like 102 (for transporter2). Then, right after it, you have another number and that's the level.map that transporter2 warps you to. (This doesn't mess up your 'int map[50][60]' array, so don't worry)

Now we need to make it so when the player walks into a transporter, we know what transporter to warp him to. To do so, we're going to rewrite some of your 'void handleInput(int dir)' function.
        //I rewrote 'dir == 1'; you can rewrite the other 3 easily.        if(dir == 1)	{		personRect.y -= 10;				int tile = map[(personRect.y) / tileSize][(personRect.x / tileSize)];				if(collide[tile] == true)		{			personRect.y += 10;			return;		}		if(collide[tile] == true)		{			personRect.y += 10;			return;		}		if(tile == transporter1)		{			lastTransporterEntered = transporter1;			changingMap = true; //Let us know to change the map.		}		if(tile == transporter2)		{			lastTransporterEntered = transporter2;			changingMap = true;		}		if(tile == transporter3)		{			lastTransporterEntered = transporter3;			changingMap = true;		}		if(tile == transporter4)		{			lastTransporterEntered = transporter4;			changingMap = true;		}	}


And finally, right after all your 'if(keys[SDLK_DOWN] == true)' where you check if the arrow keys are held down, and right before the drawing code, you add this:
if(changingMap == true){	int mapToWarpTo = 0;		if(lastTransporterEntered == transporter1)	{		mapToWarpTo = transporterMap1;	}	if(lastTransporterEntered == transporter2)	{		mapToWarpTo = transporterMap2;	}	if(lastTransporterEntered == transporter3)	{		mapToWarpTo = transporterMap3;	}	if(lastTransporterEntered == transporter4)	{		mapToWarpTo = transporterMap4;	}		//Change the map number, from an 'int' to a 'std::string', so we can load the map as a filename.	std::string mapname = "level" + IntToString(mapToWarpTo) + ".map";	loadMap(mapname);	lastTransporterEntered = 0;	changingMap = false; //Set changingMap to false again, otherwise the instant the map is loaded, it'll try to warp us again.}


There! *dusts off hands*

I warned you it was sloppy, and this is very sloppy indeed. It could be written alot better, but it'd require completely rewriting your code; this is your project and not mine, so it wouldn't make sense to rewrite it in a way you can't yet understand.

Also, like I already said, it's better for you to keep working on your project, even if it means writing sloppy code, as long as you learn something from it. Your first few projects will be like that, probably; mine certainly were. Infact, some of mine were worse! One of my first projects was a text-based adventure game, where every one of the 50-ish rooms were hardcoded into the source code! At least you are loading your maps from files. [wink]

If you didn't understand anything I did, just ask so I can explain it. I tried to keep it as simple as possible, but I also tried to write it while trying to change your code as little as possible. Some 'wanky' coding was the result. [grin]
Servant of the lord; you are a God amongst men.

If you were sat here right now I would probably shake your hand to be honest!

The solution you gave above may well be sloppy, but I understand it completely and it seems logical to me.

Cheers for all of your help =)

Now, go forth and spread the word "wanky" as much as possible; it's a beautiful word!

And thanks for the tips evolutional, I guess it would make far more sense to split everything up into proper objects rather than just having random mismatches of things everywhere.

Cheers again dude =P

Heh.
Sorry for double post, but do you realise Servant Of The Lord that your last post was nearly 1,300 words long :O

That's amazing lol!

Thanks again. heh.

This topic is closed to new replies.

Advertisement