1 of 62

Electronic Games

Development Techniques

2 of 62

From narrative to systems

3 of 62

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”

4 of 62

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.

5 of 62

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.

6 of 62

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";

7 of 62

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).

8 of 62

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();

}

9 of 62

Scripted

Systemic

  • Easy to develop
  • Total narrative control
  • Not much freedom
  • Bad gameplay
  • Bad replayability

  • Good gameplay
  • More freedom
  • Emergent gameplay =�Great replayability
  • Hard to develop
  • Less narrative control

10 of 62

Dwarf Fortress: the most

systemic game (link)

Lots of dead cats in the taverns. The developer tracked the issue and discovered this sequence:�

  • If a dwarf is drinking and is request to work, he throws his drink to the ground, and runs off.
  • The liquid gets spilled in the floor.
  • When a walking character crosses the floor his feet gets wet from the liquid.
  • Cats lick their feet constantly, if the feet is wet they drink the liquid.
  • Drinking too much alcohol will kill the character.

11 of 62

The perfect spot

Scripted

Systemic

As game developers we have to balance between these two points.

  • More systems → more complexity and richer feeling to the player, but less control about the player experience.
  • More scripted → less freedom for the player but more control over experiences.

Your game

12 of 62

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!

13 of 62

World maps

14 of 62

Our game’s world

  • The Time Hallows

15 of 62

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.

16 of 62

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.

17 of 62

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] );

}

18 of 62

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.

19 of 62

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;

};

20 of 62

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];

}

};

21 of 62

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];

}

};

22 of 62

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).

23 of 62

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 *

*****

24 of 62

World generation: Parser example

Steps:

  • Get a string from the file
  • Iterate each character and convert it to the corresponding type
  • Assign the type to the cell in that position

// 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++;

}

25 of 62

Using images

We could use images and assign types depending on pixel colors:

  • EMPTY
  • WALL
  • ENEMY
  • WATER
  • EXIT
  • START

Besides the type of cell, we can use other layers to define other map properties.

26 of 62

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 *

*****

27 of 62

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…

28 of 62

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;

}

29 of 62

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;

}

30 of 62

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!

31 of 62

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);

}

32 of 62

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;

}

}

33 of 62

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;

}

34 of 62

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.

35 of 62

Dynamic Camera

  1. Center on the player’s position (center of the sprite)
  2. Clamp within map limits
  3. Offsetting by half the screen size to match rendering coordinates.

// 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;

36 of 62

World collisions

37 of 62

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.

38 of 62

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:

  • Get target position
  • Check if it’s a collision
  • If it isn’t, set it

Vector2 target = player.pos;

// Update target position using input

// ...

if (!testCollision( target )) {

player.pos = target;

}

39 of 62

Game state

40 of 62

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.

41 of 62

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

42 of 62

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;

};

43 of 62

Indexing (2/2)

The same index works here for the player’s inventory:

Indices have a lot of advantages:

  • Reduce complexity (no memory leaks)
  • Render more efficient (group by item)
  • Serialization! Game data cannot have pointers since memory addresses are broken once exported.

int inventory[16];

44 of 62

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);

}

45 of 62

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

// ...

}

46 of 62

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

47 of 62

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();

}

}

}

48 of 62

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();

}

49 of 62

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.

50 of 62

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

}

}

51 of 62

Patterns

52 of 62

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:

  • Resource variables: Get resources from its name and avoid creating a variable per resource.
  • Memory and performance: Avoid loading resources more than once, that could affect performance and it’s a waste of memory!

53 of 62

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”);

54 of 62

Managers (2/2)

  1. Check if resource is already loaded
    1. If loaded, return the resource instance pointer
    2. If not, load it from disk and store it in the manager cache. Return it after that!

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!

55 of 62

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.

56 of 62

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:

  • Render (framebuffer reference)
  • Update (delta time)
  • Input event 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

  • Render
  • Update

Stage

current_stage

IntroStage

  • Render
  • Update

MenuStage

  • Render
  • Update

PlayStage

  • Render
  • Update

57 of 62

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(..);

}

58 of 62

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;

}

59 of 62

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.

60 of 62

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

61 of 62

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!

62 of 62

Easing types