In the last tutorial, we set up the groundwork for our tile engine by implementing the image manager and a Tile class, and then used those to draw a tile to the screen. Well, if drawing one tile was our goal in the last tutorial, our goal this tutorial is going to be drawing a whole bunch of tiles.
I want to make it clear that the way I am setting up this engine is in no way definitive. There are many ways to design a tile engine, and this is just how I've decided to design it this time. I hope that these tutorials get your mind thinking of cool new ways to design a tile engine, ways to make it faster, etc.
Solving an Issue
The most common use for a top-down 2D tile engine is for an rpg, and that is going to be the target "audience" for our tile engine. So, we have an issue that we need to consider. An RPG map can be large, hundreds of tiles wide and hundreds of tiles tall. So how do we draw all of that?
The answer is, we don't. Instead, we draw only a portion of the map on any frame. How do we decide which tiles to draw and where? The solution I like to use actually comes from 3D programming. We're going to implement a Camera.
The Camera will represent a viewport through which the user sees the map. It will have a position somewhere on our map with absolute pixel coordinates (i.e. if the x position is 200, it's 200 pixels to the right of the map origin, which is the upper left corner). It will also have a size, which we can use to determine how many tiles are visible through it. These two properties together will allow us to keep track of which tiles to draw and where.
I'd also like to implement another effect we can accomplish with a camera class, scrolling. We'll keep a target position, and move the camera towards the target slowly over time.
If that didn't make any sense, compile and take a look at the program included at the end of this tutorial.
The Camera Class
Now let's go ahead and create our Camera class in a new header file called "Camera.h":
#ifndef _CAMERA_H
#define _CAMERA_H
#include <SFML\Graphics.hpp>
#include "Tile.h"
class Camera
{
private:
//Absolute position of camera (number of
//pixels from origin of level map)
sf::Vector2f position;
//Target position camera is moving towards
sf::Vector2f target;
//Size of camera
sf::Vector2i size;
//Speed of camera, a value between 0.0 and 1.0
float speed;
public:
Camera(int w, int h, float speed);
~Camera();
//Moves camera immediately to coordinates
void Move(int x, int y);
void MoveCenter(int x, int y);
//Sets camera target
void GoTo(int x, int y);
void GoToCenter(int x, int y);
//Updates camera position
void Update();
sf::Vector2i GetPosition() { return sf::Vector2i((int)position.x, (int)position.y); }
//Helper function for retreiving camera's offset from
//nearest tile
sf::Vector2i GetTileOffset(int tileSize) { return sf::Vector2i((int)(position.x) % tileSize, (int)(position.y) % tileSize); }
//Helper function for retreiving a rectangle defining
//which tiles are visible through camera
sf::IntRect GetTileBounds(int tileSize);
};
#endif
Hopefully nothing too complicated. We have members for the camera's position and target, as well as it's size and speed. The speed variable will be used in the Update method to allow the user of the class (whoever that may be) to have some control over how quickly the camera scrolls to the target.
There are two methods for changing the camera's position. The two Move methods will set the camera's position, moving it instantly to the specified coordinates. The two GoTo methods however, will set the camera's target, causing the camera to scroll to the specified coordinates over time.
There's a Center version of both Move and GoTo, that will center the camera or target on the specified coordinates rather than set the camera's upper left corner (the camera's origin). This will be useful for centering over the player later on, and encapsulates the camera's size away from the rest of the engine.
We've implemented inline versions of GetPosition and GetTileOffset, since they are fairly trivial functions and this will remove any unnecessary overhead. GetPosition simply returns the camera's position, while GetTileOffset return the camera's position relative to the nearest tile's origin. Both of these will be useful when drawing our tiles and moving the camera around.
Alright, time to implement all this fun stuff, so let's do so in a new code file called "Camera.cpp":
#include <SFML\Graphics.hpp>
#include <math.h>
#include "Camera.h"
Camera::Camera(int w, int h, float speed)
{
size.x = w;
size.y = h;
if(speed < 0.0)
speed = 0.0;
if(speed > 1.0)
speed = 1.0;
this->speed = speed;
}
Camera::~Camera()
{
}
//Moves camera to coordinates
void Camera::Move(int x, int y)
{
position.x = (float)x;
position.y = (float)y;
target.x = (float)x;
target.y = (float)y;
}
//Centers camera at coordinates
void Camera::MoveCenter(int x, int y)
{
x = x - (size.x / 2);
y = y - (size.y / 2);
position.x = (float)x;
position.y = (float)y;
target.x = (float)x;
target.y = (float)y;
}
//Sets target to coordinates
void Camera::GoTo(int x, int y)
{
target.x = (float)x;
target.y = (float)y;
}
//Centers target at coordinates
void Camera::GoToCenter(int x, int y)
{
x = x - (size.x / 2);
y = y - (size.y / 2);
target.x = (float)x;
target.y = (float)y;
}
//This function allows us to do a cool camera
//scrolling effect by moving towards a target
//position over time.
void Camera::Update()
{
//X distance to target, Y distance to target, and Euclidean distance
float x, y, d;
//Velocity magnitudes
float vx, vy, v;
//Find x and y
x = (float)(target.x - position.x);
y = (float)(target.y - position.y);
//If we're within 1 pixel of the target already, just snap
//to target and stay there. Otherwise, continue
if((x*x + y*y) <= 1)
{
position.x = target.x;
position.y = target.y;
}
else
{
//Distance formula
d = sqrt((x*x + y*y));
//We set our velocity to move 1/60th of the distance to
//the target. 60 is arbitrary, I picked it because I intend
//to run this function once every 60th of a second. We also
//allow the user to change the camera speed via the speed member
v = (d * speed)/60;
//Keep v above 1 pixel per update, otherwise it may never get to
//the target. v is an absolute value thanks to the squaring of x
//and y earlier
if(v < 1.0f)
v = 1.0f;
//Similar triangles to get vx and vy
vx = x * (v/d);
vy = y * (v/d);
//Then update camera's position and we're done
position.x += vx;
position.y += vy;
}
}
sf::IntRect Camera::GetTileBounds(int tileSize)
{
int x = (int)(position.x / tileSize);
int y = (int)(position.y / tileSize);
//+1 in case camera size isn't divisible by tileSize
//And +1 again because these values start at 0, and
//we want them to start at one
int w = (int)(size.x / tileSize + 2);
int h = (int)(size.y / tileSize + 2);
//And +1 again if we're offset from the tile
if(x % tileSize != 0)
w++;
if(y % tileSize != 0)
h++;
return sf::IntRect(x, y, w, h);
}
Most of this code should be self explanatory, except for maybe the Update method. Unfortunately, there is a bit of math involved (oh noes!), but nothing too difficult. I've commented the code enough that anyone with any knowledge of algebra should understand it. If not, then I suggest reading any of the great math primers available here on Dream.In.Code. Hopefully the rest of this code will make sense after we implement a new RenderFrame method.
The Level Class
Alright, we now have a way of determining which tiles to draw and where. Now we need a way of storing each of the tiles. Since we're implementing the framework for an RPG engine, let's call our next class Level. Go ahead and create a new header file, call it "Level.h", and fill it with the following code:
#ifndef _LEVEL_H
#define _LEVEL_H
#include <vector>
#include "Tile.h"
class Level
{
private:
//A 2D array of Tile pointers
std::vector<std::vector<Tile*> > map;
//Width and height of level (in tiles)
int w;
int h;
void SetDimensions(int w, int h);
public:
Level(int w, int h);
~Level();
void AddTile(int x, int y, Tile* tile);
Tile* GetTile(int x, int y);
void LoadLevel();
int GetWidth();
int GetHeight();
};
#endif
We're going to store our map data in a 2D array. While we could have accomplished this with Tile* map[MAXWIDTH][MAXHEIGHT];, or something similar, we want our engine to be as versatile as possible. Therefore, we want a dynamic array. Unfortunately, there isn't a very simple way of doing that, but this is as simple as it gets. What we're using is a vector of a vector of Tile pointers. The cool part about this, is we can actually access individual Tile pointers with the familiar map[x][y] syntax.
In case it's ever requested later on, we go ahead and keep track of the map's width and height. The map's width and height are changed by the SetDimensions method, which is private right now since it's only needed by the Level constructor. We then have an AddTile method for putting a tile into the map, and a GetTile method for retrieving a tile from the map.
The LoadLevel method will be implemented in our next tutorial, and will load level data from a file on the harddrive.
Alright, time to implement this class in a new code file called "Level.cpp":
#include <vector>
#include "Level.h"
#include "Tile.h"
Level::Level(int w, int h)
{
SetDimensions(w, h);
this->w = w;
this->h = h;
}
Level::~Level()
{
}
int Level::GetHeight()
{
return h;
}
int Level::GetWidth()
{
return w;
}
void Level::SetDimensions(int w, int h)
{
//w rows
map.resize(w);
//Each row has h columns of null Tile pointers
for(int i = 0; i < w; i++)
{
map.at(i).resize(h, 0);
}
}
void Level::AddTile(int x, int y, Tile* tile)
{
map[x][y] = tile;
}
Tile* Level::GetTile(int x, int y)
{
return map[x][y];
}
void Level::LoadLevel()
{
//Eventually we'll write code to load level data from a
//file, but for now we'll just make it all up.
}
This is all very simple actually. The constructor takes in a width and height for defining the dimensions of the Level, which then uses the SetDimensions method to resize the map vector. Notice how we must first resize the map vector, which is a vector of vectors, with the width and then resize each of the vectors in that vector, which are vectors of Tile pointers. We also make sure that we initialize the vector with NULL pointers, so we don't have any nasty access violation exceptions later on.
Using These New Classes in Our Engine
Great, we now have a way to store the level data, and a way to determine how we're going to draw our frames. Now it's time to put this together to create a test demo. First, let's rewrite our RenderFrame method in Engine.cpp:
void Engine::RenderFrame()
{
//Camera offsets
int camOffsetX, camOffsetY;
Tile* tile;
window->Clear();
//Get the tile bounds we need to draw and Camera bounds
sf::IntRect bounds = camera->GetTileBounds();
//Figure out how much to offset each tile
camOffsetX = camera->GetOffset().x;
camOffsetY = camera->GetOffset().y;
//Loop through and draw each tile
//We're keeping track of two variables in each loop. How many tiles
//we've drawn (x and y), and which tile on the map we're drawing (tileX
//and tileY)
for(int y = 0, tileY = bounds.Top; y < bounds.Height; y++, tileY++)
{
for(int x = 0, tileX = bounds.Left; x < bounds.Width; x++, tileX++)
{
//Get the tile we're drawing
tile = currentLevel->GetTile(tileX, tileY);
tile->Draw((x * tileSize) - camOffsetX, (y * tileSize) - camOffsetY, window);
}
}
window->Display();
}
This will be the method we'll stick to for most of this tutorial series. What this method does is asks the camera which tiles are currently in view as well as how much the camera is offset from those tiles. Then it loops through each row of tiles and each column, starting from bounds.Top and drawing bounds.Height rows, and starting from bounds.Left and drawing bounds.Width columns. Inside the loop we get the current tile we're drawing, and tell it to draw at the current row/column, taking into account the camera's offset.
If it seems complicated, try working through it manually to see if you can figure out how it works. The following method, which we've added to Engine, is a temporary method that generates a simple level for us to look at:
void Engine::LoadLevel()
{
//Temporary level for testing
currentLevel = new Level(40, 40);
Tile* tile;
for(int y = 0; y < 40; y++)
{
for(int x = 0; x < 40; x++)
{
if(y % 4 == 0)
tile = new Tile(imageManager.GetImage(1));
else
tile = new Tile(imageManager.GetImage(0));
currentLevel->AddTile(x, y, tile);
}
}
}
Feel free to modify this if you want. All it does is create a 40x40 level, and fill it with our first sprite (index 0), and put a line of our second sprite every 4th row.
Here are the ProcessInput and Update functions we're using for now. These are to show off the camera "scrolling" effect we implemented with the Camera::Update method.
void Engine::ProcessInput()
{
sf::Event evt;
//Loop through all window events
while(window->PollEvent(evt))
{
if(evt.Type == sf::Event::Closed)
window->Close();
if((evt.Type == sf::Event::MouseButtonPressed) && (mouseDown == false))
{
int x = camera->GetPosition().x + window->GetInput().GetMouseX();
int y = camera->GetPosition().y + window->GetInput().GetMouseY();
camera->GoToCenter(x, y);
mouseDown = true;
}
if(evt.Type == sf::Event::MouseButtonReleased)
mouseDown = false;
}
}
void Engine::Update()
{
camera->Update();
}
We're letting the user click on a new location to center the camera over, and use GoTo so it scrolls there slowly instead of using Move to do it instantly. Then we remember to call camera->Update(); in the Update function.
Conclusion
Phew! That was a lot of code, but our objective was to draw a lot of tiles, and we have. Unfortunately (and I promise this will never happen again!), I forgot to save a copy of this tutorial's source code before beginning to write the code for the next tutorial, so you'll just have to check out that tutorial! See you soon!




MultiQuote





|