ShootThem! revival

Quite some time ago I dag out the sources of an old game of mine, ShootThem! made back when I was at high-school, around 2006.

It has been over a decade ever since I made that game and I had enough inspiration to revisit the code once again.

This is a short update blog about what it used to be and what it became as of now.

2006

Back in 2006 I have barely learned some OOP and C / C++, just enough to be able to use it in school programming competitions.

The game looked awful - there were no textures in the whole game, the models were mostly hand-made by myself in some dodgy 3D editor (Calligra TrueSpace or something alike).

The code was terrible as well - static variables, huge functions, not to mention the whole game was a huge hard-coded, non-configurable mess in one main.cpp file.

But there was also a map editor, which allowed for some customization - it was basically a first-person camera and few magical buttons that allowed to define target positions for a specific level. The level mesh and output file were both provided via command-line arguments.

2015

I have dag out the sources and decided to put them on GitHub for historical reference. I have also managed to set up CMake for the thing so that it could be built on any computer without bothering too much about magical compiler options and everything.

2020

I have started the rework. First things first, I reworked the CMake configuration to a modern style and replaced the dependency “management” (essentially an instruction to manually unzip Irrlich and IrrKlang files and put them in specific directories) with CMake’s Fetch module.

Then I thought about reshaping the code, so I have split one big main.cpp file with static variables and a handful of functions into classes, encapsulating the logic - Level, Score, PlayerState, GameState. This allowed me to maintain the working state of the game whilst actually improving the state of the code (commit).

I have also extracted the settings into a separate file, settings.xml so that the application can be actually somewhat configured. Also, the magical data format for the levels was replaced with much more readable XML, levels.xml:

<levels>
  <level>
    <model>room1.dae</model>
    <entities>
      <light>
        <position x="-717.96405029296875" y="129.1875" z="200"/>
      </light>
      <light>
          <position x="0" y="125.34170532226563" z="258.18295288085938"/>
      </light>
      <target>
          <position x="-765.06158447265625" y="271.0589599609375" z="670.49334716796875"/>
      </target>
      <!-- ... -->
    </entities>
  </level>
</levels>

Next step was to reduce the coupling of code components and introduce some sort of state management. I am still unhappy with what I ended up with, but it is still infinitely better than one single main() function.

I have ended up with something similar to Redux from front-end world (at least that was an initial plan) - there is a Store, where the state is stored; there are Actions, which define a change to the state; there is a Reducer, which modifies the state of the app; and finally, there are Subscribers which react to state changes and render the state of the app.

But the issue was that there are many effects to handle like playing sound, loading and unloading the data, etc.

Along the way I have also kept maintaining a list of things I was thinking about - bugs found, improvements, refactoring ideas, etc (README.md history).

Few improvements included adding a main menu, replacing the “UI” with something more user-friendly (previously it was just an ugly line of text), improving the “drunk shooter” effect (ended up replacing it with shaders, which is not a trivial task in Irrlicht - more on that later).

I have also started reworking the 3D models for the game and decided it was a good point to just show how I meant the original art to be in the game:

The art is still far from being ready, but here’s a preview of the new training level:

The editor was also reworked… Or better being said, made from scratch - now it actually has a GUI, it allows to manage all the levels without restarting the app, it allows to place targets and move them around, it allows to place light sources on the scene and it is not a first-person camera anymore.

Lessons learned

There are quite a few lessons that I have learned.

CMake sucks. A lot.

I freaking hate CMake for a few very good reasons:

  • awful syntax, it is still same good old Make, just with syntax being wrecked
  • lack of any structure to the projects - everyone does it in their own preferred way, so no one will actually tell you the way to set up your project
  • the lack of dependency managemen - which, in 2020, seems like a huge drawback, especially if you don’t develop every single bit of your application from scratch
  • performance - yes, performance - by default it performs a clean build every time - it compiles everything from scratch every time you change anything
  • documentation - no one will tell you the way to do it; most manuals and articles still rely on CMake 2.6 or 3.0 at best; a lot of information online is so dispersed, inconsistent and out-of-date - you will be surprised!

The only reason I use CMake is because all the dependencies I use could be used with CMake. Well, except Irrlicht and IrrKlang.

I thought Bazel or Buck will be better, but they still require one to create custom build instructions for those dependencies and it is especially hard for a cross-platform libraries such as Irrlicht. Well, at least they do solve most of CMake issues I have listed above.

Irrlicht is old

It is extremely old - last bugfix version, 1.8.4, was released on 9th July 2016 (which is only 4.5 years ago as of now) and the corresponding minor version, 1.8.0 being released on 8th November 2012 (8 years ago as of now).

It might not seem like a huge deal, but here are few things that I have noticed:

  • shaders are hard to get working
  • only GLSL and HSL are supported
  • no frame buffer support
  • renderers are OpenGL 3 and DirectX 9
  • out-of-the-box tools are quite limited:
    • camera movement is either Maya-style, First-Person-Shooter-style or manual control, through matrices and vectors
    • the GUI out of the box is not suited for high-resolution monitors with fonts being pixelated
    • overall a lot of bugs with GUI (random events missed or fired)
  • ray-picking is buggy
  • textures / lighting is buggy as heck (sometimes textures are disappearing, separate mesh triangles being shaded differently from others, etc.)
  • anti-aliasing not working properly for sprites (which makes my HUD look ugly most of the time, even though it is high-res images)

Check these screenshots for example:

It is the same scene, with lighting being messed up for all target models on the latter screenshot.

With the subset of APIs that are needed for my game, I am quite seriously considering just sticking to lower-level graphics APIs (OpenGL / DirectX) and stepping away from Irrlicht.

C++ is not easy

With the idea of having redux-like state management and the strong type system of C++ I thought it would be a good idea to leverage the compiler and language features to do some compile-time checks for the actions that are being dispatched. I started with templates, but then figured out (the most painful way possible) that templates are merely hints for compiler.

Also, one can not define method overrides for the base class of the hierarchy by specifying child class as method param.

So my initial design like below fell apart very quickly:

class BaseAction {};

class PlaySoundAction : public BaseAction {
  public:
    std::string filename;
};

class DestroyEntityAction : public BaseAction {
  public:
    std::shared_ptr<Entity> entity;
};

// ---

class BaseReducer {
  public:
    virtual void processAction(BaseAction* action) = 0;
};

class Reducer : public BaseReducer {
  public:
    // error: 'processAction' marked 'override' but does not override any member functions
    virtual void processAction(DestroyEntityAction* action) override {
      // ...
    }

    // error: 'processAction' marked 'override' but does not override any member functions
    virtual void processAction(PlaySoundAction* action) override {
      // ...
    }
};

// ---

class Store {
  private:
    std::unique_ptr<BaseReducer> reducer;
    std::queue<BaseAction*> actionQueue;

    void processQueue() {
      while (!actionQueue.empty()) {
        auto action = actionQueue.front();

        actionQueue.pop();

        // error: no matching member function for call to 'processAction'
        reducer->processAction(action);
      }
    }
};

Templates won’t solve the issue either:

class BaseReducer {
  public:
    // only this method will be called every time
    template <class T>
    void processAction(T action) {
      std::cout << "processing unknown action" << std::endl;
    }
};

class Reducer : public BaseReducer {
  public:
    void processAction(DestroyEntityAction* action) {
      BaseReducer::processAction(action);
      std::cout << "destroying an object" << std::endl;
    }

    void processAction(PlaySoundAction* action) {
      BaseReducer::processAction(action);
      std::cout << "playing a sound" << std::endl;
    }
};

Also, a template method can’t be virtual.

So here comes another trick front-end developers use, the type field for actions and switch..case statement in reducer:

class BaseReducer {
  public:
    virtual void processAction(BaseAction* action) = 0;
};

class Reducer : public BaseReducer {
  public:
    virtual void processAction(BaseAction* action) override {
      switch (action->type) {
        case ActionType::PLAY_SOUND:
          std::cout << "playing a sound" << std::endl;
        break;

        case ActionType::DESTROY_ENTITY:
          std::cout << "destroying an object" << std::endl;
        break;

        default:
          std::cout << "unknown action" << std::endl;
      }
    }
};

Editor is really important

My game has multiple levels. Each level requires its own lighting and target placement. It is quite hard to do properly without the editor. And the first version of my “editor” has proven that.

Even the new version of editor lacks lots of features - one still can’t rotate the targets or see the actual targets, there are no particle emitters available, there are no scene-specific events that would have allowed some neat mechanics.

And finally, it is hard to see the result in the editor itself - there will be differences with the game.

However, I did consider using GoDot engine for the game.

There are features that I really liked about it.

I18n support

Configurable user actions

This way you can have same handler for gamepad buttons’ and keyboard/mouse input events:

Attaching scripts to any object on the scene

This allows for a magnitude of interesting mechanics.

For a number of reasons I didn’t choose it - there were some user experience issues significantly reducing my productivity.

Asset management

Sspecifically the neccesity to specify every single texture for every single object on a scene. Check out this chicken model:

It is just an OBJ file with every material specified:

mtllib chicken.mtl

# 262 vertex positions
v  18.556948 41.779099 -12.354659
v  20.53804 50.283123 -13.547608

# ...

# Mesh '0' with 68 faces
g 0
usemtl Material #1
f  1/1/1 2/2/2 3/3/3
f  3/3/3 2/2/2 4/4/4

# ...

Yet in the editor you’ll have to manually assign every single material to every single sub-mesh:

Not really clear scene node types

Now which node do I use for kinematic physical body?

Just what the heck is “spatial” and why does it have both “camera” and “collision shape” next to “audio stream”?

Lack of any sort of pre-made solutions

So that every time you need an FPS camera - you have to define it from zero, using the matrix transformations. This increases the amount of boilerplate code and actually reduces the productivity even compared to Irrlicht:

This is not the end

I am not done with this project revival just yet - I still want it to build under OSX and I still have few assets to put into the game. Also, the plot is still not very clear, so I’d better come up with something rather interesting. Stay tuned for more updates!