Cell colony game in C++

Cell colony game in C++

Writing a simple game / simulation, render it with OpenGL using piksel graphics library

Intro

I started my programming path from the book "C++ for Dummies". Then I solved programming tasks and made my first steps in different computer science areas using this language.

I have my favorite game: "Half-Life 2", and during high school / first year in university, I tried making mods for it. And because I was really bad at modelling and level design, I practiced modifying the game logic, adding new entities and implementing some features in the game code. And the C++ code of the "Source Engine" (game engine developed by Valve company and used in Half-Life games) was a really nice example of best practices in objective-oriented programming. I was very impressed with how logical and straightforward that code is. After typical examples in programming books and articles, it was an absolutely new view. I realized, that the code could be abstract, human-readable and simple. And operate with objects, representing real-world entities, instead of just variables.

That's why I feel really warm about C++, game development / simulations, and graphics visualization. And in this article, I will implement and describe a simple game about cell colonies.

Game rules

The idea for the game comes from John Conway's "the game of life", Spore and just the nature of bacterial columns.

Fig. 1

There are 2 objects: a food base and a cell / bacteria.

The food base has a limited amount of supply and a limited amount of cells, that could feed from it. It has an "active radius", so the cell can take food from the base when being within this radius. The limited capacity and amount of food will motivate cells to expand the colony and move toward other bases.

Cell / bacteria have these features:

  • Limited lifetime, after that cell dies

  • Limited health, after some attacks cell dies

  • After eating a needed amount of food, the cell splits, creating 2 new cells

  • When hungry, the cell moves to a nearby food base

  • When seeing other colony's cells, attacks them

  • Collides with other cells

So generally, the cell is a basic unit, like in every RTS game.

Objects

Let's finally put that rules into the code.

Since both game entities have common props: position, a link to the world, they could go into a base class Entity:

class Entity {
    public:
    Entity(World* world, Point2 pos);
    virtual void Process() = 0;
    inline Point2 GetPosition();

    protected:
    Point2 position;
    World* world;

    friend class World;
};

The Process function is triggered by the world update and should handle the object's internal state changes.

For food, need to describe the amount, radius and track cells, that enter or leave the food base:

class Food : public Entity {
    public:
    Food(World* world, Point2 pos, int maxAmount, float radius, int maxCellsCount);

    void Process();
    void Die();

    inline float GetRadius();

    inline int GetMaxCellsCount();
    inline int GetCurrentCellsCount();
    inline bool HasFreeSpots();
    inline bool HasAmountAvailable();

    bool IsCellInActiveZone(Cell* cell);
    bool HasAvailableAmount();
    void HandleEaten();
    void CellLeave(Cell* cell);
    void CellOccupy(Cell* cell);

    protected:
    int currentAmount = 0;
    int maxAmount = 0;
    std::vector<Cell*> cells;
    int maxCellsCount = 0;
    float radius = 0;

    friend class World;
};

Cells need functions and variables to track health, starvation, attacks, split, etc. So the class declaration is bigger, but the implementation of each function should be simple and obvious, so all this logic is assembled piece-by-piece, which is a nice benefit of abstract programming

class Cell : public Entity {
    public:
    Cell(World* world, Point2 pos, int ownerId, std::map<std::string, std::string> params);

    void Process();

    bool CanSplit();
    void Split();

    bool IsTooHungry();
    bool IsTooOld();
    inline bool IsOutOfHealth();
    void Die();

    bool IsInCooldownFromAttack();
    bool CanAttack(Cell* otherCell);
    void Attack(Cell* otherCell);
    void TakeDamage(float amount);

    bool CanEat();
    void Eat();
    void OccupyFoodBase(Food* food);
    void LeaveFoodBase();
    bool IsWithinFoodBase();
    bool IsInCooldownFromFeed();

    inline float GetRadius() {return this->radius;}
    inline int GetUserId() {return this->userId;}

    // === Should go to AI module ===
    enum Intention {
        Nothing,
        WannaFeed,
        WannaAttack,
        Patrolling
    };

    void FormDecission();
    bool HasReachedThePoi();
    void MoveToPoint(Point2 poi, float poiRadius);
    void StopActivity();

    protected:
    // eat
    Food* feedBase = nullptr;

    // health
    float healthCurrent = 0;
    float healthMax = 0;
    float birthTime = 0;
    float lifetime = 0;

    // move
    bool inMove = false;
    Point2 poi;
    float poiRadius;
    Vector2 velocity;
    float speed = 0;
    float radius = 0;

    // feed & fission variables
    // when reach feedMax - self-dublicate
    int feedCurrent = 0, feedMax = 0;
    float lastFeedTime = 0; // last global time when cell feeded
    float feedCooldown = 0; // cooldown between feed actions (when cell cannot act)
    float feedInterval = 0; // min interval before feed actions
    float maxTimeWithoutFood = 0;
    float foodDetectRadius = 0;

    // attack variables
    float attackRange = 0;
    float attackPower = 0;
    float lastAttackTime = 0;
    float attackCooldown = 0;
    float enemiesDetectRadius = 0;

    // belonging
    int userId = 0;

    // === Should go to AI module ===
    Intention intention;

    friend class World;
};

Ideally, all the decision-making parts should go into a separate AI class, that would handle only intension-forming logic.

It is a lot of code, but just take a look at the Process function, it is so abstract and human-readable:

void Cell::Process() {
    if (this->feedBase && !this->IsWithinFoodBase()) {
        this->LeaveFoodBase();
    }

    if (this->IsOutOfHealth() || this->IsTooHungry() || this->IsTooOld()) {
        return this->Die();
    }

    if (this->IsInCooldownFromAttack() || this->IsInCooldownFromFeed()) {
        this->inMove = false;
        return;
    }

    if (this->CanSplit()) {
        return this->Split();
    }

    this->FormDecission();
}

And the main object - World which is just a container for cells and food, that runs the simulation and implements functions to manipulate it (but not to render it! Which is important, to split those 2 processes)

class World {
    public:
    World(float width, float height);
    inline float GetCurrentTime();
    void Step(float delta);

    Cell* CreateBacteria(Point2 position, int ownerId);
    Food* CreateFood(Point2 position, int maxAmount, float radius, int maxCellsCount);
    void DestroyCell(Cell* cell);
    void DestroyFood(Food* food);

    const std::vector<Cell*>& GetCells();
    const std::vector<Food*>& GetFood();

    protected:
    float width, height;

    std::vector<Cell*> cells;
    std::vector<Food*> food;

    float currentTime = 0;
};

The main idea here - you have a special class, that can do the simulation, and it is separated from render. Potentially, that could even be used to train a neural network with a positive reinforcement algorithm, that will process cell decisions.

Also, basic cell collisions were implemented. In real usage, better to trust that logic to a separate physics engine (like box2d), but for a simple demo, it is fine to remember a simple 2D math and process collisions manually.

The article can even end here, but for the game, you need user input to manipulate the world, plus it needs to be rendered.

Render

When it comes to rendering hardware-accelerated graphics, you usually have to write a lot of extra code, before you can actually draw something. Such as:

  • A code to create a window and obtain an OpenGL context

  • OpenGL code to bind vertex buffer, vertex and fragment shaders, map coordinates and many other different instructions

That's why I would like to use any library, that will encapsulate all this logic and give a simple API to draw objects and handle keyboard and mouse input. Plus, a nice bonus if it is cross-platform (including web). I searched a little bit and found a perfectly matching library - piksel.

Everything that you need is just to inherit the BaseApp class and implement needed functions:

class App : public piksel::BaseApp {
public:
    App(World* w, int width, int height);
    void setup();
    void draw(piksel::Graphics& g);
    void keyPressed(int keycode);
    void keyReleased(int keycode);
    void mousePressed(int code);
    void mouseMoved(int x, int y);

protected:
    void ProcessCamera(float step);
    bool IsSeenByCamera(const Point2& point);

    void DrawCells(piksel::Graphics& g);
    void DrawFood(piksel::Graphics& g);

    World* world;
    std::chrono::system_clock::time_point lastUpdateTime;
    Point2 cameraPos;
    int pressedKeys;
    int mouseX, mouseY;
};

To draw primitives, you can use simple functions g.rect, g.ellipse, etc. To simplify, let's just draw cells as circles (of a different color) and food as white rect.

As a result, we can have cell colonies epic battles:

So the resulting project structure is as follows:

The source code is available on GitHub.

Maybe when having some time, I will continue to develop that game. There are different possible directions:

  • Use box2d as a physics engine (it would be a nice experience)

  • Play more with the best cells and food params

  • Use sprites for game entities

  • Implement UI, cells selection and control (like in RTS games)

  • Implement different levels with complicated relief and a path search algorithm to allow cells to move there