1
\$\begingroup\$

I'm studying computer science at university. So far I've created several programs for school projects and homeworks in C++. They were usually smaller programs up to 1000 lines of code.

Now I want to create a simple 2D turn-based strategy game (TBS game) similar to Advance War.

If I decide to use OOP principle, then game design would seem quite straightforward in some aspects. I'd create class such as Unit, Tile, GameBoard... But several questions are arrising in a moment.

Tile should probably contain:

  • information about type of terrain (probably enum type),
  • information if there is a unit (probably pointer to unit)
    ===> needed for searching possible moves in a map of tiles

Unit should probably contain:

  • information about type of unit (probably enum type)
  • where unit is standing (probably pointer to Tile / position coordinate?)
    ===> if I want to search space for a specific unit I must know where searching should start.

Should I use pointer to Tile or (x,y) coordinates for Unit class? Is it bad to have circular dependency (Unit<-->Tile) in this specific situation?

However I am not so sure whether OOP is good way to go after watching several videos. I've seen videos about:

In the video about data oriented design you can hear sentences like:

00:09:20 "Multiple inheritance... that's not even a guestion. That's just dumb. Runtime type information... that's off the table."

01:16:00 "Show me your code, I won't know what you're doing. Show me your tables (how you organise your data), I won't need to see your code"

01:18:00 "You don't care how long it takes. Great. But People who don't care about how long it takes are also a reason why I have to wait 30 seconds for Word to boot."

Since methods like attack and move for individual units (infantry, mech, transporter) differ only with values I can avoid using virtual functions for attack/move quite trivially and have only one universal class Unit.

#include <iostream>
#include <string>
#include <array>
#include <bitset>

enum class TerrainType   { Grass, Mountain, Forest, size};
enum class TransportType { Foot, Boot, Tread, size  };
enum class UnitType      { InfantryType, MechType, TransporterType, size };

const size_t COUNT_UNIT = (size_t) UnitType::size;
const size_t COUNT_TRANSPORT = (size_t) TransportType::size;
const size_t COUNT_TERRAIN = (size_t) TerrainType::size;
const size_t UNREACHABLE = 100;

const size_t damageTable[COUNT_UNIT][COUNT_UNIT] =
{
    {5, 2, 1},    // infantry
    {50, 20, 10}, // mech
    {0, 0, 0}     // transporter
};

const size_t moveCostTable[COUNT_TERRAIN][COUNT_TRANSPORT] =
{
    {1, 1, 1},              // Grass
    {2, 1, UNREACHABLE},    // Mountain
    {1, 1, 2}               // Forest
};

const size_t numberOfMovesTable[COUNT_UNIT] =
{
    3, // infantry
    2, // mech
    6  // transporter
};


struct CUnit
{
    size_t health;
    size_t fuel;
    UnitType type;

    //------------------------------------------------------------
    CUnit(size_t hpParam, size_t fuelParam, UnitType typeParam)
        : health(hpParam), fuel(fuelParam), type(typeParam) {}

    void attack(CUnit& unit) const
    {
        size_t baseDamage = damageTable[(size_t)type][(size_t)(unit.type)];
        unit.health -= baseDamage;
    }
};


int main(int argc, char const *argv[])
{
    CUnit inftry {99, 99, UnitType::InfantryType};
    CUnit mech {99, 99, UnitType::MechType};

    std::cout << "inftryHP: " << inftry.health << std::endl;
    std::cout << "mechHP: " << mech.health << std::endl;

    inftry.attack(mech);
    mech.attack(inftry);
    std::cout << "after attack\n";
    std::cout << "inftryHP: " << inftry.health << std::endl;
    std::cout << "mechHP: " << mech.health << std::endl;

    return 0;
}

But there are units that behave distinctly and have additional moves such as transporting and supplying other units. Then I have to create somewhere for example additional information about what units can a tranporter load, create information about its capacity, array where to save transported units and so on. Then my universal class Unit immediately create extra memory overhead because Infantry or Mech do not need those additional information.

I've come up with an idea creating somewhere a constant tuple of unique unitSheets (infantrySheet, TransporterSheet...). Each unitSheet would contain specific behaviour and values. Then CUnit would have ID and health,fuel... just like before, but specific behaviour would be access via tuple using CUnit's ID. Unfortunately it is only possible call get(tuple) with specific ID in compile-time.

So I do not know which direction should I go? I've spent several days on thinking about how to implement the game but the results are minimal. You can argue that TBS games is not area where I should worry with performance. However I'd like to experiment with the game using evolution algorithm and neural network to train AI in the future. So quick operations might come in handy.

EDIT #1 (after @Philipp answer):

Does a tile need to know its unit? (So only unit knows its tile)

Definitely yes. But let's assume "no". How could I check if an unit standing on a tileA can enter a tileB? Is a different unit already standing on the tileB? I am unable to examine the tileB directly because of its absence of pointer to unit. I am forced to go through every unit and its tile to check if it is the tileB.

Does a unit need to know its tile? (So only tile knows its unit)

Probably not. If player wants to choose a unit, he is still clicking on a tile. So we can determine unit via tile's pointer quite easily. How to find player's unit on a game map? If I store array of units, it won't tell me nothing about their position because of absence of pointer to tile. If I store array of tiles (probably pointer to tile) where player's unit stand, I will know unit's position. However it creates also several drawbacks... I am accesing unit indirectly via another pointer. If an unit moves to other tile, I must reflect that in player's array of tiles. Is "only tile knows its unit" better than having circular dependency? I don't know...

\$\endgroup\$
2
  • \$\begingroup\$ It will be hard to provide a concise answer to this multifaceted question. For now, it is a bit broad and can lead to debate (which is not ideal on an SE site). Concretely, thinking this early on about design patterns and about data-oriented design is not productive. Refine your design and mechanics on paper. Then worry about the minor details. Unless you will write numerically-intensive algorithms, cache misses and DoD will be the lest of your worries. Once you have a refined view of your whole game mechanics, you may consider redesigning your code. \$\endgroup\$
    – teodron
    Commented Sep 16, 2018 at 16:26
  • \$\begingroup\$ Regarding the problem you mentioned about different units having extra movements or whatever, that's exactly what Entity-Component System is perfect for. There's a little overhead but don't worry about optimizing for now, you will have time to improve things later if you ever need to. \$\endgroup\$ Commented Sep 17, 2018 at 4:59

2 Answers 2

1
\$\begingroup\$

First things first: If this is your first game and you dont plan to expand on it (except maybe for Neural Network Learing), then use your learned OOD. There is nothing wrong in doing it a certain way. Your first game wont be a Battlefield or Dota, not even a Final Fantasy Tactics. So dont worry about the performance and whatnot before even having a design plan. Why worry about memory overhead, if you have not implemented a single class.

i would suggest to go with either one, OOD or ECS, but dont mix them. For you OOD might be the more natural way, so if making a game is just a stepping stone for more complex topics, stick to OOD. If you want to make a game to lear new concepts and work with those, go ECS. Any hybrid thingy may make it worse.

Now second, do you understand why polymorphism and inheritance is arguably bad for games? You cited the videos (which i couldnt watch yet) and some quotes, but just after that you want to have a certain table filled with every detail, and possibly link it --> Cache miss. If you want a good read about these topics, i advice you to look for Game Programming Pattern.

Maybe im wrong - quite possible that i am - but even then you should plan some designs you could implement and gather pros and cons for them. If you think you are done, post them here and ask others what they think.

After you thought about those two things, you can start concepting if your tiles need to know the units and the units need to know the tiles.

\$\endgroup\$
0
\$\begingroup\$

Should I use pointer to Tile or (x,y) coordinates for Unit class?

When looking up the tile of a coordinate-pair is cheap (which it usually is if your map is an array of tiles with array indexes mapping to coordinates), then it does not make much of a difference if you get the tile from the coordinates or the coordinates from the tile. So the unit knowing its coordinates and the unit knowing its tile is pretty much the same thing.

Is it bad to have circular dependency (Unit<-->Tile) in this specific situation?

The real question here is "does a unit needs to know its tile" and "does a tile need to know its unit"? I could imagine cases where both navigations might be useful. So I would recommend to maintain that circular dependency. However, in order to avoid inconsistencies, make up your mind about who is "the boss in the relationship". When a unit moves from one tile to another, are the tiles responsible for updating the unit or is the unit responsible for updating the tiles? Both are reasonable approaches, but you need to decide on one and stick to it, or there is a high risk of bugs if tile and unit do not agree with each other.

There are two special cases which might or might not be relevant for your game:

  1. Can a unit occupy more than one tile?
  2. Can a tile host more than one unit?

If either is the case, then it might not be a good idea to have the 1 in the 1..n relationship know its n, because it might be ambiguous.

I've come up with an idea creating somewhere a constant tuple of unique unitSheets (infantrySheet, TransporterSheet...). Each unitSheet would contain specific behaviour and values.

This sounds like you just reinvented classes. You won't get around the performance problems you get from performance-critical code using polymorphism by inventing your own polymorphic system and calling a class a "Unit Sheet".

Units with unique abilities appears like a good application for the Entity - Component - System (ECS) pattern. A unit which can transport, gets a Transporter component. A unit which can supply gets a Supplier component, and so on.

The components are separate objects which contain all the data relevant to that functionality. So the Transporter component would contain the information about the loaded units, while the Supplier component contains what resources it can supply to which units and the remaining stock.

The memory overhead problem you mentioned can be avoided by not instantiating any component objects for entities which do not have them.

In a pure ECS, you don't actually have a class CUnit. All the data is in the components and all the code is in the systems. An entity is just a size_t id which is used to find out which components belongs to the same entity. But if you don't want to go that far, you can also have components without systems. In such an architecture, your class CUnit has a pointer for every possible optional component an unit can have. When a unit does not have a specific component, that pointer gets the value null (or points to a static NULL-object if you want to avoid runtime errors caused by accidentally dereferencing null pointers).

I am looking forward to playing your game.

\$\endgroup\$
2
  • \$\begingroup\$ Yes, it's some kind of my own polymorhism. It should be probably tested if it's better than calling C++ virtual functions. Anyway I am unable to create that polymorphism with tuple. We are still paying for some abstraction with ECS. Every unit doesn't need Transporter or Supplier. I know I need at least 4 components in my game. It's additional 4 * 8 bytes = 32 bytes per unit on 64bit system. But I still feel urge it can be done better. Well, maybe I want a Holy Grail that doesn't exist. Currently using non-pure ECS seems to me as a good way to go. Thanks for your answer and edit :) \$\endgroup\$
    – kocourOggy
    Commented Sep 16, 2018 at 14:37
  • \$\begingroup\$ @kocourOggy If you're concerned with memory overhead you can easily bring that down. Say if you don't expect to have more than 65000 Transporter instances in your game at once, you can store a 2-byte index into your collection of transporters, rather than an 8-byte pointer. Then you can fit four component references in the space it takes to point to your unit sheet. \$\endgroup\$
    – DMGregory
    Commented Sep 17, 2018 at 12:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .