blog | Page 2

Pixel Swamp

A pixelly swamp based on Roger Dean's Greenslade cover

A pixelly swamp based on Roger Dean’s Greenslade cover

(No Comments)

Sketch

A few creature sketches.

A few creature sketches.

(No Comments)

Crash

Here’s a new game I made for the cyberpunk game jam over at itch.io. I made it over 5 days with Ben Weatherall doing the cyber-art and Tim Shiel cranking the cyber-music. It’s about hacking robots while plummeting to your doom, and it requires fast typing and hacking skills. Play it now. You can also listen to the official sound trackĀ over here.

Crash

(No Comments)

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)