Categories
blog

An entity system

Here’s a cross-post from my Moonman devlog.

Update: Well it’s been a while, but here’s my first update for 2014. I haven’t touched MM code for a month but instead have been designing and implementing a cleaner and simpler entity system. I’ve been meaning to do this for quite a while now, but after using Unity and looking at other bits of code like entityx I decided to finally attempt it. This is helped in part by the new c++ support in VS2013. The system is also data-oriented — all components and entities are tightly packed in memory. I use a similar free list setup as in MM. I also had to use some c++ techniques I haven’t used before, such as variadic templates and typelists. But that is all behind the scenes, this is what the API looks like:

[cpp]
EntitySystem es;

// Create an entity
// and add some components
Entity& e = es.create();
e.add(Transform(4,5));
e.add(Health(10));
e.add(Physics(0,-10));
e.add(ShortDescription(“Ben”));
e.add(Description(“An architecture-obsessed programmer.”));

// Create another entity
// with different components
// this time using intializer_list shorthand
Entity& chest = es.create();
chest.add(Transform(-4.5f, 0.8f));
chest.add(Inventory{
 { Item::SWORD },
 { Item::POTION, 4 },
 { Item::POTION, 3 },
 { Item::ARROW, 64 }
});

// If we need to keep a reference to an entity
// then we use ID’s (uint32s)
ID chestId = chest.id;

// Then later on somewhere we can get the entity
// and do something with it, e.g.,
if (es.has(chestId)){
 Entity& ch = es.lookup(chestId);

 // Shift the chest one unit horizontally
 Transform& tr = ch.get();
 tr.x += 1;
}

// Iterating through all entities
// can be done with a range-based for loop
for (Entity& e : es.entities()){
 std::cout << e; } // Likewise, we can iterate through all // components of a particular type. For // example a PhysicsSystem might want to // process all the Physics components. for (auto p: es.components()){
 // Apply viscocity
 p.vy *= 0.9f;
 p.vx *= 0.9f;

 // Move entity
 Entity& e = es.lookup(p.entity);
 Transform& tr = e.get();
 tr.x += p.vx;
 tr.y += p.vy;
}

// Everything is done with references
// if e doesn’t contain C, then
// e.get() returns a blank component
// (which can be checked for validity)
Entity& e = es.lookup(id);
Health& health = e.get();
health.health = 666;
if (health){
 // It’s valid
}

// Components themselves are just structs
// The CRTP gives them a unique class id
// that is used to store them in EntitySystem
struct Health: public Component {  
 float health;
 bool poisoned;
 Health(float health = 0.f, bool poisoned = false) :health(health), poisoned(poisoned){}

 std::string what() const; // human readable rep
 static const char* Name(); // name
};

// The logic of a component is implemented
// in a system. e.g., the HealthSystem might
// be responsible for decreasing a character’s
// health if they are poisoned.

class HealthSystem : public ISystem {
public:
 bool implements(int componentIndex) override {
   return Health::Index()==componentIndex;
 }

 void setup(Entity& e) override {
   e.get().health = 100;
 }

 void update(EntitySystem& es, double dt) override {
   for (Health& h : es.components()){
     if (h.poisoned){
       h.health -= 0.1f * (float)dt;
       if (h.health <= 0){          // Create KILL EVENT        }      }    }  } }; [/cpp] Besides this I've also been thinking about having Script components. These will be a special type of component that uses traditional polymorphism and lambdas to offer a concise and easy way to implement special behaviours. For example, I could attach a custom c++ poison script to a entity like this: [cpp] Script* newPoisonerScript(Entity& e){  // variables to be captured by the lambdas  float* duration = new float(0.f);  float* timer = new float(0.f);  auto poisoner = new Script("poisoner");  poisoner->destroy = [duration, timer](Entity& e){
   Health& health = e.get();
   if (health) health.poisoned = false;
   delete duration;
   delete timer;
 };

 poisoner->start = [](Entity&e){
   Health& health = e.get();
   if (health) health.poisoned = true;
 };

 poisoner->update = [timer, duration](Entity& e, double dt){
   *duration += (float) dt;
   Health& health = e.get();
   if (health){
     if (health.poisoned){
       *timer -= (float) dt;
       if (*timer < 0){          health.health -= 1;          *timer = 1.0f;        }      }    }  };  poisoner->finished = [duration](Entity& e){return *duration>1.5f;};
 return poisoner;
}
[/cpp]

Anyway, once I’ve finalised the new system it’s going to take a couple of days to incorporate it into Moonman, but I definitely think it’s worth the deviation. Basically it means that I’ll turn macro’ed code that looks like this (see devlog for explanation of how it works):

[cpp]
ATTRIB(e, x) += 0.1f;
bool isOnGround = ATTRIB_OR(e, is_on_ground, false);
bool hasPhysics = HAS_ATTRIB(e, is_physics);
[/cpp]

Into nicely autocompleting code that looks like this:

[cpp]
e.get().x += 0.1f;
// or with shorthand
e.transform().x += 0.1f;
// and
bool isOnGround = e.get().isOnGround;
bool hasPhysics = e.has();
[/cpp]

Categories
blog

A basic tile-based game in SFML

Whoops, the code below has been mangled by wordpress, sorry! I’ll keep it in the post but here’s a link to the source also.

Hey there! Here’s the source for a simple tile-based game in SFML. You move the white player around a small world that contains randomly moving red and green zombies. The code demonstrates basic gamedev and SFML concepts such as collision detection, game state, movement interpolation, and uses a simple data-oriented design.

The player is the white square. Water is blue. Zombies are green and red.
The player is the white square. Water is blue. Zombies are green and red.

[cpp]
#include
#include
#include #include
#include
#include
#include
using namespace std;

static const int WINDOW_WIDTH = 512, WINDOW_HEIGHT = 512;
static const int TILES_WIDE = 8, TILES_HIGH = 8;
static const int TILE_WIDTH = WINDOW_WIDTH/TILES_WIDE, TILE_HEIGHT = WINDOW_HEIGHT/TILES_HIGH;

enum Tile {
TILE_WATER,
TILE_DIRT,

TILE_INVALID,
};

class Map {
public:
Map(){}
Map(string mapString){
// Convert map string into tiles
if (mapString.length()!=TILES_WIDE*TILES_HIGH){
cerr << "map string is wrong length!"; return; } for(int i=0;i=0 && i=0 && j monsters;
};

// Checks to see if theres a monster here
bool isMonsterHere(Game& game, int i, int j){
for(auto monster: game.monsters){
if (monster.i==i && monster.j==j) return true;
}
return false;
}

// Checks to see if theres a player here
bool isPlayerHere(Game& game, int i, int j){
if (game.player.i==i && game.player.j==j) return true;
else return false;
}

// Linear interpolation
float lerp(float a, float b, float t){
return a*(1-t) + b*t;
}

int main(){
srand(time(NULL)); // seed randomiser
sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), “Game”);

// Setup game
Game game;
game.state = WAITING_FOR_PLAYER_INPUT;

// Setup map
string mapString =
” ”
” ~~~ ~~ ”
” ”
” ~ ~ ”

” ~ ~ ”
” ~ ~ ”
” ~~ ~~~ ”
” “;
game.map = Map(mapString);

// Setup player
game.player.i = 0;
game.player.j = 0;
game.player.isMoving = false;
game.player.oldI = 0;
game.player.oldJ = 0;
game.player.pi = 0.0f;
game.player.pj = 0.0f;

// Create some monsters
const int NUM_MONSTERS = 6;
for(int i=0;i 0.8){
type = RED_ZOMBIE;
}

// Find an empty dirt spot to place the monster
int placeI = 0, placeJ = 0;
while(true){
int ri = rand()%TILES_WIDE;
int rj = rand()%TILES_HIGH;

// Check for collision with player or other zombies
bool canPlaceHere = true;
canPlaceHere = !isPlayerHere(game, ri, rj);
canPlaceHere = canPlaceHere && !isMonsterHere(game, ri, rj);
canPlaceHere = canPlaceHere && game.map.tile(ri, rj)==TILE_DIRT;
if (canPlaceHere){
placeI = ri;
placeJ = rj;
break;
}
// else keep trying
}

Monster monster;
monster.type = type;
monster.i = placeI;
monster.j = placeJ;
monster.isMoving = false;
monster.oldI = monster.i;
monster.oldJ = monster.j;
monster.pi = (float) placeI;
monster.pj = (float) placeJ;

game.monsters.push_back(monster);
}

// Start loop
game.clock.restart();
while (window.isOpen())
{
// Manage Input

sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed){
window.close();
}

if (game.state==WAITING_FOR_PLAYER_INPUT){
if (event.type==sf::Event::KeyPressed){
int di = 0, dj = 0; // the direction to move in
switch (event.key.code){
case sf::Keyboard::Left: di = -1; break;
case sf::Keyboard::Right: di = 1; break;
case sf::Keyboard::Up: dj = -1; break;
case sf::Keyboard::Down: dj = 1; break;
default: break;
}

if (di!=0 || dj!=0){
// Player wants to move so try to move him
int i = game.player.i + di;
int j = game.player.j + dj;

// Check targe tile for collision
bool collision = false;
Tile t = game.map.tile(i,j);
switch (t){
case TILE_WATER: case TILE_INVALID: collision = true; break;
default: break;
}
collision = collision || isMonsterHere(game, i, j);

if (!collision){
// Can move there
game.player.oldI = game.player.i;
game.player.oldJ = game.player.j;
game.player.i = i;
game.player.j = j;
game.player.isMoving = true;
game.state = UPDATING_PLAYER;
game.clock.restart();
}
}
}
}
}

// Update other game state etc
switch (game.state){
case WAITING_FOR_PLAYER_INPUT:
{
// Player gets only a short time to make a move
// The logic of movement is handled in the event handled
static const float MAX_TIME_TO_MOVE = 2.0f;
float t = game.clock.getElapsedTime().asSeconds();
if (t >= MAX_TIME_TO_MOVE){
game.state = UPDATING_PLAYER;
game.clock.restart();
}
break;
}
case UPDATING_PLAYER:
{
static const float MOVE_SPEED = 0.2f; // seconds it takes to move
float t = game.clock.getElapsedTime().asSeconds();
t = min(MOVE_SPEED, t);

if (game.player.isMoving){
game.player.pi = lerp(game.player.oldI, game.player.i, t/MOVE_SPEED);
game.player.pj = lerp(game.player.oldJ, game.player.j, t/MOVE_SPEED);
}

if (t >= MOVE_SPEED){
game.player.pi = game.player.i;
game.player.pj = game.player.j;
game.player.oldI = game.player.i;
game.player.oldJ = game.player.j;
game.player.isMoving = false;
game.state = UPDATING_MONSTERS;
game.clock.restart();

// We now update all monsters
for(auto& monster: game.monsters){
// Monsters move randomly
// and sometimes stay still
double random = (1.0*rand())/RAND_MAX;
if (random > 0.6){
// stay still
continue;
}

// Here’s a list of possible directions (di,dj)
static const int DIRS[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};

// randomly index into the list
int randomDirIndex = rand()%4;

// And then try each direction in turn
for(int dir=0;dir<4;dir++){ int index = (dir + randomDirIndex)%4; int di = DIRS[index][0]; int dj = DIRS[index][1]; int i = monster.i + di; int j = monster.j + dj; // Check targe tile for collision bool collision = false; Tile t = game.map.tile(i,j); switch (t){ case TILE_WATER: case TILE_INVALID: collision = true; break; default: break; } collision = collision || isPlayerHere(game, i, j) || isMonsterHere(game, i, j); if (!collision){ // Can move there monster.oldI = monster.i; monster.oldJ = monster.j; monster.i = i; monster.j = j; monster.isMoving = true; break; } } } } break; } case UPDATING_MONSTERS: { static const float MOVE_SPEED = 0.4f; // seconds it takes a zombie to move float t = game.clock.getElapsedTime().asSeconds(); t = min(MOVE_SPEED, t); for(auto& monster: game.monsters){ if (monster.isMoving){ monster.pi = lerp(monster.oldI, monster.i, t/MOVE_SPEED); monster.pj = lerp(monster.oldJ, monster.j, t/MOVE_SPEED); } } if (t >= MOVE_SPEED){
for(auto& monster: game.monsters){
monster.oldI = monster.i;
monster.oldJ = monster.j;
monster.isMoving = false;
}
game.state = WAITING_FOR_PLAYER_INPUT;
game.clock.restart();
}
break;
}
}

// Draw
window.clear(sf::Color::Black);

// Draw map
for(int i=0;i