C++ from Basics to Mastery — Part 12
C++ organized into game structure
The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.
Let’s connect the grammar we’ve learned so far to the actual game structure. Deciding which classes and data structures have which responsibilities are more difficult than memorizing the grammar.
sharing responsibility
Characters can be responsible for health and attack, and Monsters can be responsible for monster status. DungeonGame manages your party, active monsters, spawn queue, and item database.
class DungeonGame
{
public:
void AddCharacter(std::unique_ptr<Character> character)
{
if (character)
{
party.push_back(std::move(character));
}
}
void QueueMonster(std::shared_ptr<Monster> monster)
{
if (monster)
{
spawnQueue.push(std::move(monster));
}
}
private:
std::vector<std::unique_ptr<Character>> party;
std::vector<std::shared_ptr<Monster>> activeMonsters;
std::queue<std::shared_ptr<Monster>> spawnQueue;
std::stack<int> killHistory;
std::unordered_map<int, Item> itemDatabase;
std::weak_ptr<Monster> currentTarget;
};
Party characters are solely owned by DungeonGame, so uniqueptr was used. Monsters can be shared between the queue and active list, so we used sharedptr. The current target is weak_ptr because it is not owned.
Just by looking at the type, you can tell to some extent who is keeping the object alive.
Boundary value processing
If you assume that only normal input is received, the game code will quickly break down.
bool AttackMonster(std::size_t characterIndex, int monsterId)
{
if (characterIndex >= party.size())
{
return false;
}
auto iter = std::find_if(
activeMonsters.begin(),
activeMonsters.end(),
[monsterId](const auto& monster)
{
return monster && monster->GetId() == monsterId;
}
);
if (iter == activeMonsters.end())
{
return false;
}
(*iter)->TakeDamage(party[characterIndex]->Attack());
return true;
}
It first checks for invalid indexes, non-existent IDs, and nullptr possibilities and returns early. You should not call front on an empty queue or top on an empty stack.
Health should not go below 0, and already dead objects should not be processed again. Safeguards aren't a flashy feature, but they significantly reduce debugging time.
Concepts corresponding to Unreal
Data structures in general C++ and Unreal types often have similar roles.
std::vectorandTArraystd::unordered_mapclassTMapstd::unique_ptrclassTUniquePtrstd::shared_ptrclassTSharedPtrstd::weak_ptrclassTWeakPtr
However, std::shared_ptr should not be used for UObject just because the name is similar. While regular C++ objects have their lifetime managed by RAII and smart pointers, UObjects are managed by Unreal's GC.
AActor is not deleted directly like a regular object.Destroy(), and the termination process isEndPlay()Processed in It should be noted that the general F type and UObject type have different ownership methods.
Additionally, if reallocation of both TArray and std::vector occurs, the pointer pointing to the element may become invalid.
In the end, C++ syntax is not a separate function. Classes bind responsibilities, containers store state, smart pointers represent lifetimes, and algorithms perform operations.
Once you start to see this structure, you will be able to understand why the code looks like this rather than just memorizing the grammar.