在实时策略游戏中处理“单位” - c ++

4

我正在使用C++制作一个简单的实时战略游戏。

我想知道如何处理在游戏中创建新的单位(例如通过兵营创建机枪手)。我该如何存储这些单位呢?

我考虑过创建一个名为“unit”的类,然后让特定类型的单位继承它(例如机枪手、火蝠等),但如果我为这些单位创建数组(例如Marines myMarines[20]),那么这将对单位数目设立硬性上限。

我应该如何创建这样一个可以随意扩展的数组呢? 谢谢!


谢谢大家的快速和准确的回答!我之前不知道向量,只知道一点智能指针。我会好好研究一下的!再次感谢你们的回答,非常感激。 - SamHLec
6个回答

9
标准库提供了 std::vector 模板用于动态调整大小的数组。一个 std::vector<Marine> 将是 Marines myMarines[20] 最直接的替代品。
然而,您可能不想为每种单位类型单独创建列表。很可能您希望将所有单位存储在同一列表中,而不考虑它们的类型。 std::vector<Unit> 似乎是显而易见的解决方案,但实际上并不是。问题在于 std::vector 通过值存储对象。下面的代码将无法正常工作:
std::vector<Unit> v;
v.push_back(Marine("John Doe"));

问题在于Marine对象将被复制到Unit对象中,这是向量存储的内容。这种复制会导致所谓的slicing:所有特定于Marine的成员都将丢失,只有存在于Unit中的成员才会被存储。
解决此问题的一种方法是在向量中存储指针,因为复制指针不会改变它们指向的对象。但这会带来其他问题。要存储指针,这意味着您需要动态分配对象。而这意味着现在您需要手动销毁这些对象。这是一个繁琐且容易出错的任务。
解决方案是在向量中存储能够自动销毁动态分配对象的对象,而不是指针。这些对象被称为智能指针。标准库中存在的最简单的智能指针是std::unique_ptr
std::vector<std::unique_ptr<Unit>> v;
v.emplace_back(new Marine("John Doe"));

这是C++11的一个特性。如果您的编译器不支持它,您可以在Boost库中找到替代方案。Boost甚至包括一个容器,其作用几乎类似于std::vectorstd::unique_ptrboost::ptr_vector。那将是另一种选择。

6
@bobobobo,我不喜欢假设完全陌生的人无法理解“高级”的东西。我觉得这种说法很居高临下。 - R. Martinho Fernandes
2
@bobobobo 这种思维方式导致了很多 C++ 代码存在泄漏和问题。 - Etienne de Martel
4
@bobobobo 猜猜看,有人问了这样一个问题,但并没有一脸茫然地盯着我。相反,他们继续进行了一些研究和学习。我认为这是一次胜利,我不会再参与这个讨论了。 - R. Martinho Fernandes
4
你的回答非常好,Martinho。谢谢,我很快就理解了。 - SamHLec
2
@bobobobo 一个 vector<Unit*> 比一个 vector<unique_ptr<Unit>> 更难正确处理。为什么初学者要从难的开始呢?学习 char * 先于 std::string 的时代早已过去。 - fredoverflow
显示剩余4条评论

4

在这里使用std::vector可能会对您有所帮助。这将允许您随意添加和删除项目,并且在内部处理动态内存分配(无需关心琐碎的细节!)。

假设您想要存储一份海军名单(用以下示例中的虚构类CMarine表示):

std::vector<CMarine> marinesList;

现在只需要简单地执行以下步骤即可添加海洋元素:
marinesList.push_back( CMarine( <whatever-its-constructor-takes> ) );

要访问这个Marine,您可以像这样操作:
CMarine& marine = marinesList.at( 0 );
marine.someVar = 33;
marine.doMethod();

我使用引用是因为 CMarine 可能太过笨重,无法高效地通过值传递。

你也可以使用迭代器遍历所有的海军士兵,像这样:

for ( std::vector<CMarine>::iterator _it = marinesList.begin();
                                     _it != marinesList.end(); ++_it );
{
    CMarine& marine = *_it;
    // Now you can do something with this marine reference
}

更新:

如果CMarine是多态的,也就是说,它继承自一个超类(可能类似于你的情况中的CUnit),并且你有一个“全局”的向量存储所有单位 - Georg Fritzsche正确地指出,如果我们按值存储,则可能会发生对象切片。相反,你最好使用像这样的CUnit(智能)指针向量:

std::vector<std::unique_ptr<CUnit>> unitsList;
// To add a marine:
unitsList.push_back( new CMarine( <whatever-its-constructor-takes> ) );

在这里阅读更多关于向量的信息。


4
如果这里将它们按值存储在向量中,那么这是一个BadIdea™ - 如果它们是多态的,则它们将受到对象切片 - Georg Fritzsche
确实,你说得没错,存储指针的向量可能更好。我会修改我的答案来注明这一点。 - Alex Z
1
请不要在 vector 中存储原始指针,如果该 vector 所拥有的内存。如果任何内容与半满的 vector 抛出异常,那么想象一下会引起多大的头痛。unique_ptr ftw!对于 C++98/03,请使用 ptr_vector。 - stinky472

1

很有可能你不想为每个单位类型单独创建一个容器。因此,你需要进行一些泛化,使用类似于组件化设计的东西。一旦你完成了这个步骤,你就需要在第一种情况下使用std::vector<GameUnit*>std::list<GameUnit*>,在第二种情况下使用std::vector<GameUnit>std::list<GameUnit>。无论哪种方式,你都应该使用标准库容器来存储东西。

你可以在http://cppreference.com上找到更多关于std::vectorstd::list的信息,尽管你的书本应该已经涵盖了它们。另外,请参见


1
首先,我会创建一个Unit类,然后从它派生出你的单位,这样你就不必处理一堆单独的列表。然后,我会在以下位置存储指向单位的指针:
std::list< Unit * > unitList

列表可以让你追加任意数量的对象,虽然它不允许快速访问列表的任意成员,但是你可以轻松地迭代它而不必担心当你从其中删除某个元素时会尝试移动大量内存。

我喜欢做的一件事是让一个单元在其构造函数内自动向单位列表注册自己。所以假设Marine是Unit的子类,我需要做的只是这样说:

new Marine(x_pos, y_pos);

此时,一个新的Marine将自动创建并添加到列表中。

在此时,每一帧,您可以遍历unitList中的每个单位,并运行该单位的更新函数(这是一个虚函数,对于每个子类都不同)。

在更新循环之后,运行清理循环再次遍历unitsList,找到所有被摧毁的单位,并将它们从列表中移除并删除它们。


0

我说 std::vector!通常我会创建一个基类 unit(或者我喜欢称之为 GameObject)这是我会做的:

class GameObject {} // Maybe has virtual methods for the Size, Location and Image?

class Barrack
{
   std::vector< GameObject > gameUnits;

public:
   // code
   void AddUnit() { gameUnits.push_back( GameObject() ); }
   void DestroyUnit(int index); 

   // etc. etc.
}

然而,如果你不想过度依赖继承,即你有不同种类的单位,它们并不都继承自一个基类,那么你可以尝试一下我几天前实现的vector_any类来保存我RPG游戏中的精灵:

struct element
{
    element( void* data, const std::type_info& info ) : value(data), type( &info ) {}
    void* value;
    const std::type_info* type;
};

class type_conversion_exception : exception {};

class linked_vector
{
    vector< element > stack;
public:
    linked_vector() {}
    template< typename T > void add_item( T& item )
    {
        stack.push_back( element( static_cast< void* >( &item ), typeid(item) ) );
    }
    template< typename T > T& get_item( int index )
    {
        if ( *( stack[index].type ) == typeid( T ) )
        {
            return *( static_cast< T* >( stack[index].value ) );
        }

        else throw type_conversion_exception();
    }

};

你可以像这样在游戏单位中使用它。

linked_vector gameUnits;
MilitaryUnit mUnit;
AirUnit aUnit;

gameUnits.add_item( mUnit );
gameUnits.add_item( aUnit );

try{ draw( gameUnits.get_item< MilitaryUnit >(0) ); }
catch( type_conversion_exception e ) { /* error handling */ }
// etc. etc.

0
在向量和列表之间做出选择是一个棘手的问题。每次你在向量上执行push_back()操作时,整个向量都会被重新分配和复制。列表没有这个问题。尽管如此,你可以预先分配向量并设置一个单位容量 - 除非你希望在地图上拥有有效的“无限”单位,否则这很好用,但你可能不需要。
至于查找,向量对于任何索引具有恒定时间的查找,但你有多少次想要跳到特定的索引?当涉及到遍历整个列表或向量时,我认为性能没有区别。
此外,当你想从向量中删除一个单位(当它被杀死时)时,你将面临更多的重新分配和洗牌问题,而列表可以更有效地删除任何项。
我个人倾向于使用列表。
正如已经提到的,你选择的容器应该保存指向单位基类的指针。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接