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:
- Data oriented design from Mike Acton ( https://www.youtube.com/watch?v=rX0ItVEVjHc )
- Caches from Scott Meyers ( https://www.youtube.com/watch?v=WDIkqP4JbkE )
- Entity component system from Vittorio Romeo ( https://www.youtube.com/watch?v=QAmtgvwHInM&list=PLTEcWGdSiQenl4YRPvSqW7UPC6SiGNN7e&index=5 )
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...