让每个实体拥有唯一ID——Entity和EntityManager类的封装

时间:2021-10-30 21:53:15

为什么要唯一的ID?
拥有一个唯一的ID是一件很好的事,特别是在网络中传输数据时,需要指定某个玩家,或者需要操作某个战斗单位,要做的只是传输一个int类型的ID即可。对游戏中的每个实体进行唯一标示,这就是拥有一个Entity的意义。

Entity类
我们创建一个Entity类,它拥有一个m_nID成员,并且在构造函数中用一个static的计数器来为m_nID创建唯一的ID值。

不必担心ID值会超过int的上限,如果你确实有所顾虑,那么在快达到2*10^9的时候将它重新置1即可。虽然说这样仍然不算严谨,因为有可能在新的一轮计数时,之前ID较小的实体并没有被销毁,这时就可能存在冲突。我们这里认为这种情况不会发生,如果你需要做一个宏伟的项目,那么需要另行考虑。

有了唯一ID,如何找到对应的实体?
服务端从网络接收到一个命令,ID为某个值的战斗单位要向前移动一步。这时需要通过ID找到对应的实例。这个需求很容易用STL里的map来实现。

EntityManager类
这里我们将它封装为EntityManager类,EntityManager类保存一个map,并向外提供GetEntityByID的方法。这里,我将Entity实例的new和delete的职责也交付给EntityManager,原因是EntityManager可以在Entity创建和销毁时,同时移除map中的映射,这样就不用担心被delete的实例指针仍然能在EntityManager中找到。

配套使用
下面给出一个例子,如何配套使用这两个类。

这里创建一个简化的战斗单位的类Unit,Unit类继承于Entity,并且UnitManager继承于EntityManager。

注意这个Unit类有一个重载的构造函数。在编写Unit类时父类Entity不会对它有任何的制约,只是继承了父类的一个ID而已,需要做的只是专注于Unit本身的逻辑而已。Unit类的cpp文件不做任何事情,只是简单的在构造函数中初始化变量。

重点是UnitManager需要做一些事情。
第一是实现父类的纯虚函数CEntity* New(),并且将返回值修改为CUnit*。别忘了在new的语句之后调用AddEntity将其加入到map的映射中。
第二件事情是提供一个GetUnitByID方法。虽然父类已经提供了GetEntityByID方法能满足需求,这样做的目的只是实现类型转换,在语义上也更明确一些。
除此之外最后一件事情就是,如果Unit类提供了其他的重载构造函数,那么在UnitManager中也要有对应的New方法。

后面两点虽然不是强制的,但是这样做会使得这个UnitManager类更好用。 编写EntityManager子类时,应该要记住做这三件事。

小结
就是这样,很简单的一个封装但实用。在我最近写的项目中大量用到这种结构,使得查找和管理实例相当容易。

最后注意几个原则:
1.只能通过UnitManager的New方法来创建Unit实例。
2.谁调用New方法获取了Unit实例,谁就要负责调用UnitManager的Delete方法销毁它。