PostApocalypse DevDiary: Week 1

by admin

Recently I’ve picked up work again on my indie game: PostApocalypse.

Beginnings

Last summer, I needed to stay busy. You don’t want to come out of your summer holiday with a set of blunt tools, so you need to continue using them. It was actually my father who gave me inspiration for this game. My dad is an accountant who specializes in automating business processes. Yes, it’s as dull as it sounds. But he was complaining that if the world were to end, his skills wouldn’t be of use to anyone. I disagreed, because even after the apocalypse food supplies will need to be kept track of, trading will still have to be organized and eventually governments are to be rebuilt.

And that… sounded like a pretty cool game to me.

What helped was that at the time, I was playing boatloads of Mount & Blade: Warband. If you’ve never heard of it, it’s a game where you start with a lonely fighter on a horse and end up with a mighty army. But what I especially enjoyed was the trading, because you can make a lot of money with very little effort. Some settlements will have a surplus of one material, but lack another. For instance, grain will be cheap in city A, but expensive in city B. If you can keep track of those differences (for instance, on a piece of paper) you can send caravans from one city to the other and reap mountains of gold.

Mount & Blade: Warband

It took me forever to find this screenshot of Mount & Blade: Warband. Apparently nobody else enjoyed the boring bits as much as I did.

So my very first design goal became: if you don’t need paper to keep track of “hidden” game state, the design has failed.

False start

Another thing I wanted to try was to build a game completely data-oriented. Previously I have blogged about data-oriented design and how it could help your game run faster, but I wanted to see what would happen if I did everything that way.

It was a disaster.

One thing I decided early on was that I wanted hexagonal grid cells as a landscape. So I had a “Landscape” class and it had “DataChunks” and “DataCells”. The cells themselves would have to be built every frame, using the center position as a reference. Using some math you can then derive the normals for each triangle of the cell. That’s all fine and dandy, but the function to do that (_GenerateNormals) assumes cells are in one chunk. When you add more chunk, it becomes increasingly complex.

Basically: the code architecture was horrible to work with and impossible to add features to once written. I had coded myself into a corner.

I abandoned the project and started focusing on school projects again.

Reboot

That was half a year ago and now I know better than to try and build a game using data-oriented design. I now (almost) have enough credits to go on an internship. For the first time in a long while, I have some free time. So this week, I decided to take a look at PostApocalypse again.

Yup, still as bad as I remembered. I could reuse maybe 5% of the old code, but threw the rest away.

Progress

Right now, the game looks like this:

So what currently works?

  • Generating a landscape using Perlin noise
  • Generating a default mesh with a default texture
  • Loading DDS textures (the grass)
  • Rotating the camera using the keyboard
  • Rendering the landscape and the units using OpenGL
  • Placing new units on the landscape
  • Selecting and deselecting units
  • Pathfinding between points on the landscape
  • Moving units across the map
  • Saving and loading game state.

I’d say that’s pretty good for one week of development. :D

And now for the cool part: explaining the game’s architecture!

New architecture

The solution is divided into three projects:

  • Game – The actual game code
  • Render – The rendering library
  • Tools – Static library both projects link to

Game links to the Render project, but obviously doesn’t do any of the rendering. This will make it easier in the long run to port the project to other systems. Because both the Render project and the Game project need to log to the same file, the logging class is put into the Tools project. Ever wanted to use a singleton across the DLL-Executable barrier? Here’s how you do it:

static Log& Get()
{
	if (!s_Instance)
	{

#ifdef PROJECT_DLL

		typedef Log* (*GetLogFn)();
		HMODULE mod = GetModuleHandle(NULL);
		GetLogFn GetLog = (GetLogFn)::GetProcAddress(mod, "LogGetInstance");
		s_Instance = GetLog();

#else

		s_Instance = new Log;

#endif

	}

	return *s_Instance;
}

And then in the Game project:

extern "C" __declspec(dllexport) Log* LogGetInstance()
{
	return &Log::Get();
}

So how does that voodoo magic work? Well, the Render project defines PROJECT_DLL, while the Game project does not. So when the Game class compiles the header, it creates a new instance of the Log class and returns it. But when the Render class compiles it, it calls a function inside the executable exposed to the DLL’s that returns that very instance. Now, both the executable and the DLL have the same pointer to the Log class. :D

RenderManager

Because I’m trying to avoid OpenGL deprecated calls, I’m using a RenderManager class. This keeps track of global state like matrices, the current material and the current shader. It makes it easy to abstract all kinds of rendering operations without losing performance.

I built the first version of the rendering architecture for my Procedural Generation class. I wanted to experiment with using “new” OpenGL (> 2.1) and had to figure out how I could still have global state.

The epitome of modern OpenGL

For instance, this is the Render method of the Model class:

void Model::Render()
{
	RenderManager::Get().SetShader(m_Shader);

	for (unsigned int i = 0; i < m_MeshTotal; i++)
	{
		if (!m_Mesh[i].m_VertexArray) { continue; }

		Mesh* curr = &m_Mesh[i];

		RenderManager::Get().SetMaterial(curr->m_Material);
		RenderManager::Get().Begin();

		curr->m_VertexArray->Enable();
			glDrawArrays(GL_TRIANGLES, 0, curr->m_VertexFilled); CGLE();
		curr->m_VertexArray->Disable();

		RenderManager::Get().End();
	}
}

You only have to set the shader once, so we can do that outside the loop. It’s almost starting to look like DirectX calls, so perhaps those guys are on to something. ;)

Coincidentally, I could copy the Perlin class, the Sphere generator and the texture generator directly from my Procedural Generation assignment. Sometimes it can be nice to have clean code. B)

Model-View-Controller pattern

Another thing I wanted to try this time was the Model-View-Controller (MVC) pattern. For instance, let’s say we need to keep track of people. Here’s what you start with:

struct DataPerson
{
	std::string name;
	int age;
};

Okay, pretty cool, but we need to keep track of all people, not just the one. How do we do that?

class ControllerPerson
{

public:

	void SetName(DataPerson* a_Person, const char* a_Name);
	void SetAge(DataPerson* a_Person, int a_Age);

protected:

	std::vector<DataPerson*> m_Persons;

};

Aha, that makes sense. You might want to read data from a Person, but if you want write data to a Person, you will have to use the Controller. So how do we display the information to the screen?

class ViewPerson
{

public:

	void DrawOnScreen(DataPerson* a_Person);

};

Pretty sweet. But what if want find a person by name in our database? We’ll add a method to the Controller:

	DataPerson* GetByName(const char* a_Name);

MVC in PostApocalypse

This pattern is the best I have ever seen for game development. Because you keep everything separated, it becomes a breeze to add new logic or update the rendering. Currently, there are three controllers:

  • ControllerLandscape – Keeps track of the landscape, the chunks and the cells.
  • ControllerUnit – Keeps track of the units on the landscape.
  • ControllerPath – Combines data from the landscape and the units to find a path for a unit between two points.

When you place a unit, you need to know what cell to add it to. So first you do a raycast into the scene to get the cell:

tb::Vec2 mousepos = Mouse::Get().GetPosition();
tb::Vec3 ray_start = RenderManager::Get().ProjectInWorld(mousepos.x, mousepos.y, 0.f);
tb::Vec3 ray_end = RenderManager::Get().ProjectInWorld(mousepos.x, mousepos.y, -1.f, 2.f);
tb::Vec3 ray_dir = (ray_end - ray_start).Normalize();

g_CellSelected = ControllerLandscape::Get().SelectCellRaycast(ray_start, ray_dir);

Then you can create a new unit:

if (Keyboard::Get().Released(KEY_1) && !g_UnitAdd)
{
	g_UnitAdd = ControllerUnit::Get().CreateUnit(g_CellSelected);
	g_UnitSelected = g_UnitAdd;
}

And the highlight the path from the selected unit to the cell under the cursor:

ControllerPath::Get().GetPathTo(g_Path, g_UnitSelected->m_Cell, g_CellSelected);

On the screen:

Beginnings of pathfinding using A*

I can’t stress enough how easy I got this working. I spent two hours doing pathfinding and an hour to put it into its own controller, but when I was done it was completely abstract.

Saving the game

What I’m currently working on is trying to save the game state to a file and loading it back in. You might think this is a bit early in the project, because I don’t have that much state left to save. But I’m doing it this early because it’s easier to keep adding to a save game implementation than it is to retrofit it when the game is done. All you have to do is set up the basics and make sure it continues to work throughout the project.

So I have two classes: GameSave and GameLoad. When you press F5, the following happens:

if (Keyboard::Get().Released(KEY_F5))
{
	GameSave* autosave = new GameSave("autosave.sav");

	autosave->StartSection("Camera");
	autosave->WriteValue("Position", cam->GetPosition());
	autosave->WriteValue("Orientation", cam->GetOrientation());
	autosave->EndSection("Camera");

	autosave->Finish();
}

And when you press F9:

if (Keyboard::Get().Released(KEY_F9))
{
	GameLoad* autosave = new GameLoad("autosave.sav");

	autosave->AddCallback("Camera", LoadCameraState);

	autosave->Parse();
}

It’s a bit clunky, it’s a bit messy, it uses XML and it doesn’t save everything. But it’s a start.

Next week

That’s it for this week, I’ll try and think of stuff to add for next week. :D Maybe… gameplay?

Naww.