Electronic Games
Development Techniques
From narrative to systems
Games as illusions
Every video game shows a representation of a world (text, images or 3D graphics) and we accept it as a reality even though we know it is not (suspension of disbelief):
“avoidance of critical thinking and logic in understanding something that is unreal or impossible in reality in order to believe it for the sake of enjoying its narrative”
Games as illusions
Let's take the first Zelda as an example: when we see a small green shape on the ground we know it is a plant because of its representation and context. The developer could have done better but it would probably be an overworked solution for something that doesn't require so much depth.
Our goal as game developers is to construct a world that has the necessary depth for our purposes, but no more.
Narrative
The same happens with every situation inside the game. They are an illusion built from a set of information presented at us.
The game may or may not have simulated all the details and events that make up the context and players don’t know how deep the game goes and mostly don't care if that allow us to have a good experience playing it.
A game centered around the concept of narrative usually present the users with a set of situations and the possible outcomes designed by the creator, giving the user the possibility to choose between them.
Narrative
In this example the player understands that there is a warrior, a door and dragon in case of opening the door. The game has not modelled any of these concepts, it presented them and let the player interpret them instead.
→ Scripted game: The game creator has already defined all the possible paths and the player is left to explore them.
cout << "You warrior is in front of the dungeon's gate, do you open it? Yes/No ";
string response;
cin >> response; // Player input
if(response == "Yes")
cout << "As you enter a Dragon sees you an kills you with fire. GAME OVER\n";
else
cout << "You ignore the dungeon and follow your path.\n";
Systems
Another option could had been that the game had a representation of every relevant element of the game, with its properties and possible interactions.
When the user performs an action the system will compute the resulting state taking into account the current state of the game, using a set of rules defined by the developer.��In this case the game has much more depth, as the player could end up in situations that no other player of this game has experienced before (emergent gameplay).
Systems
In this example we have a representation in memory of the dragon, his properties (if he is ready to throw fire), the warrior (his health and skills) and the environment (if they are in line of sight).
→ Systemic game: A system evaluates the world (systemic games).
if ( dragon.canSee(player) && dragon.fireReady )
{
if (player.has_magic_shield == false)
player.energy -= FIRE_DAMAGE;
if (player.energy < 0)
player.die();
}
Scripted
Systemic
Dwarf Fortress: the most
systemic game (link)
Lots of dead cats in the taverns. The developer tracked the issue and discovered this sequence:�
The perfect spot
Scripted
Systemic
As game developers we have to balance between these two points.
Your game
Systemic tools
In this presentation we will focus on some techniques to provide the game with more systems, starting with how we define the world where the player will move, and the elements that populate it.
Remember, the richer that world is, the more interesting it will be for the player!
World maps
Our game’s world
Our game’s world
Our player will most likely move through a space, and we want that space to be rich in detail and interaction.�
Option: Load a big image with the whole world and draw it as background → not efficient and STATIC (won't change during the execution of the game, limiting the player possibilities)
That's why having a data representation of our world is very helpful, as long as it’s easy to construct, read and visualize.
Set of objects
Our first approach to have a data definition of our world could be having a struct/class that defines one object, and having an array or vector that contains all the objects in our world.
To render, update or check collisions, we just iterate through the item container and process each of the elements.
Set of objects
Iterate through all elements constantly doesn't scale well. Big maps could be a problem…
Besides that, using pointers could produce memory leaks or invalid access that could crash the game.
�Also it gets harder to know if there is an object in some specific coordinate, you must iterate through all objects in the container.
enum eCellType { EMPTY,START,WALL,DOOR,CHEST };
// Cheap approach
struct sObject {
eCellType type;
Vector2 position;
};
std::vector<sObject*> objects;
void render()
{
for(int i = 0; i < objects.size(); ++i)
renderObject( objects[i] );
}
A world in memory
A better solution: Define our world as a bidimensional matrix of cells (every cell represents the properties of an area of the world). Benefits for rendering, collisions, pathfinding, procedural generation, animations, etc. For drawing the world we iterate over every cell and draw the appropriate image for that cell from a set of predefined cells.
Cell properties
First thing is to store the type of cell (tree, door, wall, etc.). To declare all our cell types we use an enum as they are more memory efficient. You could use uint8 cells in case of having 16x16 tilesets or less (256 possible tiles).
It is important that the amount of memory we use per cell is as small as possible if we plan to have a big world.
// Don’t use uint8 for tilesets bigger
// than 16x16, since you will have more than
// 256 different tiles
enum eCellType : uint8 {
EMPTY,START,WALL,DOOR,CHEST
};
enum eItemType : uint8 {
NOTHING,SWORD,POTION
};
struct sCell {
eCellType type;
eItemType item;
};
Storing the world (1/2)
Next step is to create the GameMap, where you will store all cells.
We make an array of cells that measures exactly width per height, so all cells are well packed in memory (like an image but with cells!).
To avoid mistakes, getCell method will compute the index in the array (based on X and Y) and return that cell.
class GameMap {
public:
int width = 0;
int height = 0;
sCell* data = nullptr;
int tile_width = 0;
int tile_height = 0;
GameMap() { }
GameMap(int w, int h) {
width = w;
height = h;
data = new sCell[w * h];
}
sCell& getCell(int x, int y) {
return data[x + y * width];
}
};
Storing the world (2/2)
Since we will probably need different layers for our game map, it’s a good idea to support them in our GameMap class.
A layer is basically a container for the data so instead of having it directly in the map, we have different arrays to store information such as the main color, collisions, etc.
struct sLayer {
sCell* data;
}
class GameMap {
public:
int width = 0;
int height = 0;
int tile_width = 0;
int tile_height = 0;
sLayer* layers = nullptr;
GameMap() { }
GameMap(int w, int h) {
width = w;
height = h;
}
sCell& getCell(int x, int y, int l) {
return layers[l].data[x + y * width];
}
};
World and Cell Coordinates
Using cells, we need to convert a given world coordinate (in pixels) to know the corresponding cell. Starting in 0,0 and assuming a 8x8 sized cell, then:
int cellX = posX / 8;
In case of different origin:
int cellX = (posX - originX) / 8;
Where posX is the world coordinate to convert and originX is the place where the world map starts (its 0,0).
World generation
Now we need a way to generate and edit the world map (the cells) to define the game space. Doing it directly in the code could be awkward…�
One option is to use a text file where each char represents a cell. Within the code, a parser will read the file and interpret the data. Text files are easily manipulable and allow us to modify the game space without recompiling.
mymap.txt
*********
* C *
******D**
.
. *****
....D S *
*****
World generation: Parser example
Steps:
// Create a map
GameMap map(128,128);
// Read file to string using a framework function
std::string map_content;
readFile("data/mapa.txt", map_content);
// Iterate characters in text from file
int x = 0, y = 0;
for (int i = 0; i < map_content.size(); ++i) {
// Get character
char c = map_content[i];
eCellType type = 0;
switch (c)
{
case '*': type = WALL; break;
case 'D': type = DOOR; break;
case 'C': type = CHEST; break;
case 'S': type = START; break;
case '\n': y++; x = 0; continue;
case ',': continue;
}
// Store in cell
map.getCell(x, y).type = type;
// Increment cell position
x++;
}
Using images
We could use images and assign types depending on pixel colors:
Besides the type of cell, we can use other layers to define other map properties.
WYSIWYG
The problem of using text files or images is that during the editing process the designer doesn't get a true representation of how the map will look.
It is always good to have an editor where What You See Is What You Get, so artists have more control over the final result.
*********
* C *
******D**
.
. *****
....D S *
*****
Using tileset editors
There is even a better way to build maps: Tileset editors. There you can load tiled images and generate your world map painting with each of the tiles using the mouse.
Once the map is created, we need to code the function responsible for reading and processing it. Each editor uses its own format and some of them could be difficult to parse…
Loading maps (1/2)
This is how to parse Tiled JSON files, composed by an array of layers each one containing all the tile data.
We can use an open-source C++ library from github.com/nlohmann, read the data inside the file and build our game map cell data from it.
Note that the following function load different layers from the same map!
#include "json.hpp"
#include <fstream>
GameMap* loadGameMap(const char* filename)
{
using json = nlohmann::json;
std::ifstream f(filename);
if (!f.good())
return nullptr;
json jData = json::parse(f);
int w = jData["width"];
int h = jData["height"];
int numLayers = jData["layers"].size();
GameMap* map = new GameMap(w, h);
// Allocate memory each layer
map->layers = new sLayer[numLayers];
map->tile_width = jData["tilewidth"];
map->tile_height = jData["tileheight"];
for(int l = 0; l < numLayers; l++) {
// Allocate memory for data inside each layer
map->layers[l].data = new sCell[w * h];
json layer = jData["layers"][l];
for(int x = 0; x < map->width; x++) {
for(int y = 0; y < map->height; y++) {
int index = x + y * map->width;
int type = layer["data"][index].get<int>();
map->getCell(x,y,l).type = eCellType(type-1);
}
}
}
return map;
}
Loading maps (2/2)
Now an example of a parser for .map format files from Rogueditor.
It’s very simple, it stores a header of 16 bytes with the width and height of the map, and the number of bytes per cell (and some padding bytes for future options), and then it contains the whole block of bytes with the information of each cell, row by row.
struct sMapHeader {
int w; // Width of map
int h; // Height of map
unsigned char bytes; // Num bytes per cell
unsigned char extra[7];
};
GameMap* loadGameMap(const char* filename)
{
// Read file in binary format
FILE* file = fopen(filename,"rb");
if (file == NULL) // File not found
return NULL;
// Read header and store it in the struct
sMapHeader header;
fread( &header, sizeof(sMapHeader), 1, file);
assert(header.bytes == 1); // Control bad cases
// Allocate memory for cells (assuming 1 byte per cell!)
unsigned char* cells = new unsigned char[ header.w * header.h ];
// Read data from file directly to memory
fread( cells, header.bytes, header.w * header.h, file);
fclose(file);
// Create the map where we will store it
GameMap* map = new GameMap(header.w, header.h);
// Copy every cell data to the map
for(int x = 0; x < width; x++)
for(int y = 0; y < height; y++)
map->getCell(x,y).type = (eCellType)cells[x + y*width];
delete[] cells; // Always free memory allocated
return map;
}
Drawing maps (1/2)
Now that we have it loaded in memory, we can easily visualize it using a tileset image. We iterate through the whole game map and for each cell we paint in the framebuffer position corresponding to that cell, the region of the corresponding tileset according to the cell type.
We can apply an offset to the position on the screen if we want to paint the map in another position on the screen (useful to simulate a camera). Keep in mind that if the map is very large we do not want to paint the cells that are outside the framebuffer to save resources!
Drawing maps (2/2)
We go through each cell in the game map, look at what type it is, and based on that we calculate the x, y coordinate of that tile in the tileset (we can do it using module and divisions).
Finally we paint in the framebuffer in the corresponding position of the cell the piece of image of the tileset.
int num_tiles_x = tileset.width / tile_width;
int num_tiles_y = tileset.height / tile_height;
// For every cell
for (int x = 0; x < map->width; ++x)
for (int y = 0; y < map->height; ++y)
{
// Get cell info
sCell& cell = map->getCell(x, y, layer_id);
int type = (int)cell.type;
if(type == -1)
continue;
// Screen position with camera offset
int screenx = x * tile_width - cameraPos.x;
int screeny = y * tile_height - cameraPos.y;
// Avoid rendering out of screen stuff
if(screenx < -tile_width || screenx > (int)fb.width ||
screeny < -tile_height || screeny > (int)fb.height)
continue;
// Compute tile pos in tileset image
int tilex = (type % numtiles) * tile_width;
int tiley = floor(type / numtiles) * tile_height;
// Create tile area
Area area(tilex, tiley, tile_width, tile_height);
// Draw region of tileset inside framebuffer
fb.drawImage(tileset, screenx, screeny, area);
}
Interactive worlds
Because the world is a set of tiles, we could allow players to change tiles state while playing (when collecting stuff, open or close door/chests, etc).
We only have to check the tile type and change it for the new one (we might want to know which is the target cell depending on the position of the player!).
This makes the world more alive.
// In the update() function
// If key Z was pressed
if (Input::wasKeyPressed(SDL_SCANCODE_Z)) {
int cellX = (player.position.x) / tile_width;
int cellY = (player.position.y) / tile_height;
// Player is next to cell (Cell on the right)
eCellType& type = map->getCell(cellX + 1, cellY).type;
if (type == CLOSED_CHEST) {
printf("CHEST found!\n");
type = OPEN_CHEST;
}
}
Animating cells
One advantage of using cells is that we can dynamically represent them based on different game parameters.
For example, we can easily animate the visual representation of a cell. Depending on its type, we could paint that tile or the following ones taking into account the time.
// While rendering...
float time = getTime() * 0.001; // Time in sec
// During loop...
// Store auxiliar type in local var
int type = cell.type;
// Animate in case of animated type
if (type == WATER){
type = WATER + (int(time * 6)) % 4;
}
Dynamic Camera
In most cases, we will want to update the camera so it follows the player, but without letting the camera show areas outside the game map.
Dynamic Camera
// While updating camera position...
int marginW = framebuffer.width * 0.5f;
int marginH = framebuffer.height * 0.5f;
positionX = clamp(
playerPosition.x - player->getTileWidth()*0.5f, � marginW,� map->getMapPixelWidth() - marginW�) - marginW;
positionY = clamp(� playerPosition.y - player->getTileHeight()*0.5f,� marginH,� map->getMapPixelHeight() - marginH�) - marginH;
World collisions
2D Collisions
Our map is probably going to have certain areas the player cannot step on, so we must add collisions.
The same way we added properties to maps using images or text files, we can create the necessary data to check if the player is able to step on its next direction.
We define a way to say which cell types are non-walkable (for instance if the type value is bigger than 100), or we have two maps, one for the visuals, and one for the properties.
2D Collisions
To avoid checking recurrently if the player position is valid, you can use an testCollision(Vector2) function used after updating the possible next position:
Vector2 target = player.pos;
// Update target position using input
// ...
if (!testCollision( target )) {
player.pos = target;
}
Game state
Game state
Within our data-oriented design, we could define the entire game using the same approach.
Any relevant element in the �gameplay must be represented �in our memory structure.
Game state
Take a look at how every relevant element is represented as a struct.
At the end, every element in the game fits in a GameData instance, it's easy to modify its state, save/load from disk, debug, etc.
struct sItem {
int type;
int extra; // Store extra information
};
struct sCell {
int type;
int item; // Item located here
};
struct sWorld {
sCell cells[256*256];
};
struct sPlayer {
Vector2 position;
int energy;
int inventory[16];
};
struct sGameData {
sItem items[MAX_ITEMS];
sWorld level[16];
sPlayer players[4];
};
sGameData myGame; // Instance of the game
Indexing (1/2)
Note that there are no pointers in the previous code. To define which item is in each cell, we are using an integer representing the index of the item inside an array:
Even different cells could share the same object since an object by now is in fact an index �(1 = SWORD, 2 = POTION, etc.). This only work if objects do not have an state, but even in that case we could have different item index for different item states, like 1 = SWORD, 2 = SWORD_BROKEN, …
struct sCell {
int type;
int item;
};
Indexing (2/2)
The same index works here for the player’s inventory:
Indices have a lot of advantages:
int inventory[16];
Persistence (1/2)
In most cases the player should have the option to save the game to continue later. Since all the data is contained in a consecutive block of memory, we can export it easily.
In case of dynamic sized arrays (enemies, items, bullets, etc.), we should create a fixed size array limited by the maximum of elements of that type. Since you are possibly exporting more than necessary (plus the array size), it consumes more memory but it’s worthy since its way more comfortable.
#define MAX_ITEMS 255
#define MAX_ENEMIES 255
struct sCharacterInfo {
Vector2 position;
int type;
int energy;
};
struct sGameInfo {
sCharacterInfo player;
int num_items;
sItemInfo items[MAX_ITEMS];
int level;
int num_enemies;
sCharacterInfo enemies[MAX_ENEMIES];
};
void saveGameInfo()
{
sGameInfo game_info;
// Fill here game_info with all game data
// …
// Save to file
FILE* fp = fopen("savegame.bin",”wb”);
fwrite(&game_info, sizeof(sGameInfo),1,fp);
fclose(fp);
}
Persistence (2/2)
When reading the file, it’s enough to transfer the data to the corresponding structs in memory.
In case of exporting data with dynamic sizes, it’s possible to do it using different write and read operations. Here it’s recommended to use a header that contains all the information about how the file has been written.
void loadGameInfo()
{
sGameInfo game_info;
// Load file
FILE* fp = fopen("savegame.bin",”rb”);
if(fp == NULL) // No saved game found
return false;
fread(&game_info, sizeof(sGameInfo),1,fp);
fclose(fp);
// Transfer data from game_info to Game
// ...
}
Level from disk
We may also want to have information about each level (objective, player and enemy stats, etc.) without recompiling.
In this case, binary files would be an issue since there’s no option to edit them externally. Instead we could use text files which are more convenient and easy to edit.
level_1.txt
player 10 20
item 1 30 20
item 2 60 20
item 1 30 20
enemy 1 50 20
enemy 1 50 40
exit 60 60
time 60
Data from disk
Parsing text is sometimes annoying in C++, so it’s recommended to use libraries to ease your work.
Using the provided TextParser class we can ask for the elements of the text file one by one and it also allows us to easily parse them.
NOTE: If you want to know if you are at the end of the file you can use if( tp.eof() )
#include "textparser.h"
void loadLevel( const char* filename ) {
TextParser tp;
if( tp.create(filename) == false)
return false;
// While there are words to read
char* w;
while (w = tp.getword()) { // Returns null at end
std::string str(w);
if (str == "player") {
std::cout << "Player!" << std::endl;
int playerx = tp.getint();
int playery = tp.getint();
}
else if (str == "item") {
std::cout << "Item!" << std::endl;
int itemtype = tp.getint();
int itemx = tp.getint();
int itemy = tp.getint();
}
else {
// Unknown type
tp.nextline();
}
}
}
In-game editor
Loading levels from disk can be extended easily by supporting edition from the game (data is later saved again to disk!).
You will save lots of time during level edition and players could share its own creations!
#include <iostream>
#include <fstream>
void saveLevel() {
ofstream file;
file.open("level.txt");
file << "player ";
file << player.x << “ ” << player.y << std::endl;
for (int i = 0; i < items.size(); ++i) {
Item* item = items[i];
file << "item ";
file << item->pos.x << “ ” << item->pos.y;
file << std::endl;
}
file.close();
}
Procedural generation
We have seen some ways to create a map manually. Now it’s time to learn how create it procedurally.
The idea is to find an algorithm in charge of defining which tile goes into each position, making it visually coherent (avoiding abrupt changes). Keep in mind that every procedurally generated world is usually quite monotonous and uninteresting, and it’s our job to avoid that!
Perlin noise: Given X, Y, return a value between -1 and 1.
Perlin noise
Create an array of random numbers and a function that maps from (x,y) to [n] of the array. That could a simple perlin function!
In the following example we use an existing implementation such as stb_perlin. We can play with the scaling factor we use to define X and Y to make a more or less abrupt map.
#define STB_PERLIN_IMPLEMENTATION
#include “stb_perlin.h”
void generateLevel() {
float s = 0.01; // Scaling parameter
for (int x = 0; x < level.width; ++x)
for (int y = 0; y < level.height; ++y) {
float r = stb_perlin_noise(� x * s,
y * s,� 0, 0, 0, 0
);
r = r * 0.5 + 0.5; // -1,1 to 0,1
level.set(x, y, int(r * 4)); // 4 types
}
}
Patterns
Accessing resources
During the development, we are going to use MANY game resources (images, audio, levels, etc.), so it’s important to add an intermediate layer responsible for all the resource management:
Managers (1/2)
These type of functions are called Managers. You can use them for any type of resources, scene objects, stages, levels, etc.
Usually, you will find these functions as a static member of a resource class:
class Image{
// ...
static Image* Get(const char* name);
}
#include <map>
#include <string>
std::map<std::string, Image*> images_cache;
Image* getImage(const char* name) {
// Search image in cache
auto it = images_cache.find(name);
// Return if found
if(it != images_cache.end())
return it->second();
// Not found, then load it
Image* img = new Image();
img->loadTGA(name);
// Save in cache
images_cache[name] = img;
// Return it
return img;
}
// Fetch image by name (it won’t load more than once!)
Image* img = getImage(“enemy.png”);
Managers (2/2)
It’s important to note that we are storing pointers, not instances of the class. Remember that if a container stores class instances, each access or assignment would imply a copy from one instance to another, which is slow by definition (and could cause problems if it is GPU data). Storing pointers saves that overhead!
Stage class (1/3)
Another useful class is the Stage class. Represents a section of the game such as the loading screen, the initial menu, the intro, the game itself, the inventory, the final screen, etc. The idea of stages is to decouple code and encapsulate.
For example, when rendering (or other main function), the game can delegate that action to a class in charge of rendering exactly the section of the game where we are. This way we make sure not to mix the sections of our game.
Stage class (2/3)
In order to implement it, we should make use of inheritance in C++. Our Stage class must have the important Game methods:
Then use the inherited versions of Stage such as IntroStage, MenuStage, PlayStage, etc. and a pointer to the current Stage so in the end the delegation of the render could be written as:� current_stage->render(framebuffer);
Game
Stage
current_stage
IntroStage
MenuStage
PlayStage
Stage class (3/3)
Remember to add the virtual keyword to when overriding methods in derived classes.
We are indicating the compiler that we want to use the methods of the derived class instead of the ones in the base class.
class Stage {
public:
virtual void render(..) {}; // Empty body
virtual void update(..) {}; // Empty body
}
class IntroStage : public Stage {
public:
void render(..);
void update(..);
}
// ...
Game::Game() {
intro_stage = new IntroStage();
play_stage = new PlayStage();
current_stage = intro_stage;
}
void Game::render() {
current_stage->render(..);
}
Pools
Constantly reserving and freeing memory for those objects that are constantly created and destroyed (picking, shooting, etc.) is often problematic (consumes time, can produce memory leaks or crashes).
A solution could be reserving a fixed number of elements in advance. When we need an element, get the first free element in the pool (using a boolean to mark them). When it’s released, it is enough to mark it as free again.
#define MAX_BULLETS 1000
struct sBullet {
bool in_use;
Vector2 position;
Vector2 direction;
}
sBullet bullets_pool[MAX_BULLETS];
sBullet& findFreeBullet() {
for(int i = 0; i < MAX_BULLETS; ++i)
{
sBullet& b = bullets_pool[i];
if(b.in_use)
continue;
b.in_use = true;
return b;
}
// In case no free bullets
std::cout << "No more bullets!" << std::endl;
return bullets_pool[0];
}
// After using a bullet, remember to free it
void freeBullet(sBullet& b) {
b.in_use = false;
}
Interpolations
One way to bring our rendering to life is to avoid jerky moving objects and instead move them over time.
When changing from state A to B, the trick is to move closer to B a bit at each frame. This produces a nicer smooth transition until reaching B.
Linear vs non-linear
Using linear movements could looks very synthetic and unnatural. Following a more interesting interpolation (cubic, quadratic, etc.), the object seems to be more alive!
Linear
Non-linear
Tweening/Easing
There are libraries that give us different functions to linearly interpolate between two values using different formulas, which produces more interesting movements (called tweening of inbetweening, or easing).
The bad thing is tweening libraries require initial and final values, and the time you want it to take. Usually we don’t have this type of information since game actors are constantly changing their behavior. We can't use tweening for everything!
Easing types