从 C++ 基础到掌握 — 第 12 集
C++ 组织成游戏结构
该博客上撰写的文章是出于教学目的而创建的。 另外,文章中的信息可能不正确,请谨慎。
让我们将迄今为止学到的语法与实际的游戏结构联系起来。 决定哪些类和数据结构具有哪些职责比记住语法更困难。
分担责任
角色可以负责生命值和攻击力,怪物可以负责怪物状态。 DungeonGame 管理您的队伍、活动怪物、生成队列和项目数据库。
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;
};
派对角色完全由 DungeonGame 所有,因此使用了 uniqueptr。 怪物可以在队列和活动列表之间共享,因此我们使用了sharedptr。当前目标是weak_ptr,因为它不被拥有。
只需查看类型,您就可以在某种程度上判断是谁在使对象保持活动状态。
边界值处理
如果您假设只收到正常输入,则游戏代码很快就会崩溃。
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;
}
它首先检查无效索引、不存在的 ID 和 nullptr 可能性并提前返回。 您不应该在空队列上调用 front 或在空堆栈上调用 top。
生命值不应低于 0,并且已经死亡的对象不应再次处理。 保护措施并不是一个华而不实的功能,但它们可以显着减少调试时间。
与虚幻相对应的概念
一般 C++ 和 Unreal 类型中的数据结构通常具有相似的作用。
std::vector和TArraystd::unordered_map班级TMapstd::unique_ptr班级TUniquePtrstd::shared_ptr班级TSharedPtrstd::weak_ptr班级TWeakPtr
但是,std::shared_ptr 不应该仅仅因为名称相似而用于 UObject。 常规 C++ 对象的生命周期由 RAII 和智能指针管理,而 UObject 则由 Unreal 的 GC 管理。
AActor 并不像普通对象那样直接删除。Destroy(),终止过程为EndPlay()加工于 需要注意的是,一般F类型和UObject类型有不同的归属方式。
此外,如果 TArray 和 std::vector 都发生重新分配,则指向该元素的指针可能会变得无效。
最后,C++ 语法不是一个单独的函数。 类捆绑职责,容器存储状态,智能指针代表生命周期,算法执行操作。
一旦你开始看到这个结构,你将能够理解为什么代码看起来像这样,而不仅仅是记住语法。