blog | Page 2

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:

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<Transform>();
  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<Physics>()){
  // Apply viscocity
  p.vy *= 0.9f;
  p.vx *= 0.9f;
 
  // Move entity
  Entity& e = es.lookup(p.entity);
  Transform& tr = e.get<Transform>();
  tr.x += p.vx;
  tr.y += p.vy;
}

// Everything is done with references
// if e doesn't contain C, then 
// e.get<C>() returns a blank component
// (which can be checked for validity)
Entity& e = es.lookup(id);
Health& health = e.get<Health>();
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<Health> {  
  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>().health = 100;
  }

  void update(EntitySystem& es, double dt) override {
    for (Health& h : es.components<Health>()){
      if (h.poisoned){
        h.health -= 0.1f * (float)dt;
        if (h.health <= 0){
          // Create KILL EVENT
        }
      }
    }
  }
};

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:

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<Health>();
    if (health) health.poisoned = false;
    delete duration;
    delete timer;
  };

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

  poisoner->update = [timer, duration](Entity& e, double dt){
    *duration += (float) dt;
    Health& health = e.get<Health>();
    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;
}

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

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

Into nicely autocompleting code that looks like this:

e.get<Transform>().x += 0.1f; 
// or with shorthand
e.transform().x += 0.1f;
// and
bool isOnGround = e.get<Physics>().isOnGround;
bool hasPhysics = e.has<Physics>();
(3 Comments)

A basic tile-based game in SFML

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.

#include <string>
#include <iostream>
#include <list>
#include <cstdlib>
#include <ctime>
#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
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<TILES_WIDE;i++){
			for(int j=0;j<TILES_HIGH;j++){
				char c = mapString[i + j*TILES_WIDE];
				Tile tile;
				switch (c){
				case ' ': tile = TILE_DIRT; break;
				case '~': tile = TILE_WATER; break;
				default: tile = TILE_INVALID; break;
				}
				setTile(i, j, tile);
			}
		}
	}

	Tile tile(int i, int j){
		if (isValidIndex(i,j)){			
			return mTiles[i + j*TILES_WIDE];
		}
		else return TILE_INVALID;
	}

	void setTile(int i, int j, Tile t){
		if (isValidIndex(i,j)){
			mTiles[i + j*TILES_WIDE] = t;
		}
	}

protected:

	bool isValidIndex(int i, int j){
		return (i>=0 && i<TILES_WIDE && j>=0 && j<TILES_HIGH);
	}

	Tile mTiles[TILES_WIDE*TILES_HIGH];
};

// Base class for all moveable things
struct Entity {
	// Actual position
	int i, j;

	// these are used for position interpolation
	bool isMoving;
	int oldI, oldJ; 
	float pi, pj;
};

struct Player: Entity {
	
};

enum MonsterType {RED_ZOMBIE, GREEN_ZOMBIE};
struct Monster: Entity {
	MonsterType type;
};

enum GameState {
	WAITING_FOR_PLAYER_INPUT,
	UPDATING_PLAYER,
	UPDATING_MONSTERS,

	// Other game states may be
	// TITLE_SCREEN
	// DEATH_SCREEN
	// UPDATING_BOMBS
	// etc..
};

struct Game {
	GameState state;
	sf::Clock clock;
	Map map;
	Player player;
	list<Monster> 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<NUM_MONSTERS;i++){		
		MonsterType type = GREEN_ZOMBIE;

		double random = (1.0*rand())/RAND_MAX;
		if (random > 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<TILES_WIDE;i++){
			for(int j=0;j<TILES_HIGH;j++){
				sf::RectangleShape tileRectangle(sf::Vector2f(TILE_WIDTH, TILE_HEIGHT));

				// Convert i,j to top-left screen position of this tile
				int x = i * TILE_WIDTH;
				int y = j * TILE_HEIGHT;
				tileRectangle.setPosition(x, y);

				// Set rect colour based on tile type
				sf::Color colour;
				Tile tile = game.map.tile(i,j);
				switch (tile){
					case TILE_DIRT: colour = sf::Color::Black; break;
					case TILE_WATER: colour = sf::Color::Blue; break;
					default: colour = sf::Color::Magenta; break;
				}
				tileRectangle.setFillColor(colour);
				window.draw(tileRectangle);
			}
		}

		// Draw player
		{
			static const int PLAYER_SIZE = TILE_WIDTH/2;
			sf::RectangleShape playerRectangle(sf::Vector2f(PLAYER_SIZE, PLAYER_SIZE));
			int x = game.player.pi * TILE_WIDTH + (TILE_WIDTH - PLAYER_SIZE)/2;
			int y = game.player.pj * TILE_HEIGHT + (TILE_HEIGHT - PLAYER_SIZE)/2;
			playerRectangle.setPosition(x, y);
			playerRectangle.setFillColor(sf::Color::White);
			window.draw(playerRectangle);
		}

		// Draw monsters
		for(auto monster: game.monsters){
			static const int MONSTER_SIZE = TILE_WIDTH - 10;
			sf::RectangleShape monsterRectangle(sf::Vector2f(MONSTER_SIZE, MONSTER_SIZE));
			int x = monster.pi * TILE_WIDTH + (TILE_WIDTH - MONSTER_SIZE)/2;
			int y = monster.pj * TILE_HEIGHT + (TILE_HEIGHT - MONSTER_SIZE)/2;
			monsterRectangle.setPosition(x, y);

			// Choose colour based on type
			sf::Color color;
			if (monster.type==RED_ZOMBIE) color = sf::Color::Red;
			else if (monster.type==GREEN_ZOMBIE) color = sf::Color::Green;
			monsterRectangle.setFillColor(color);
			window.draw(monsterRectangle);
		}

		window.display();
    }
    return 0;
}
(4 Comments)

Tetraopticon

Today I stumbled across Dart, a new web programming language that seems pretty neat. I hacked their sunflower demo and here’s what I ended up with: The Tetraopticon! It will only run in the latest browsers so here’s some pics if you can’t run it. Now I really should get back to making games.

(more…)

(No Comments)

30 min pixels

The silo

The silo

(No Comments)

Tiny font

When mocking up things, for Moonman or other games, I sometimes write little notes to myself. I find that instead of using the text tool it’s sometimes quicker just to write the note with the draw tool, and so I’ve developed a small font 4 pixels high. My pixel art exercise for today was to explore what the complete font might look like, and so here it is in a range of sizes. At a height of 4 pixels all the characters are unique.

A tiny bitmap font

A tiny bitmap font

(No Comments)

pixel tree

pixel tree

pixel tree

(No Comments)

30 min pixels

small pixel faces

small pixel faces

(No Comments)

GGJ Tram pixel

Today’s sketch is a logo for Melbourne’s arm of the Global Game Jam. Trams are iconic in Melbourne, but have absolutely nothing to do with games. :D

The global game jam tram!

The global game jam tram!

(No Comments)

30 min mini

mini

mini

(No Comments)

30 min marc

a drawing of a friend

a drawing of a friend

(No Comments)