zoukankan      html  css  js  c++  java
  • 用C++写一个没人用的ECS

    github地址:https://github.com/yangrc1234/Resecs

    在做大作业的时候自己实现了一个简单的ECS,起了个名字叫Resecs。
    这里提一下一些实现的细节,作为回顾。
    用到最多的是C++11的可变参数模板的feature。多亏了它,很多想法可以用很少的代码实现。

    最后用这个ecs系统作为逻辑层,加上之前做的openGL练习,拼拼凑凑做了个贪吃蛇出来,当作业交了

    Components存储

    在Resecs中,Component的存储由World对象来管理。
    每当World被实例化,它就会为每种Component去申请一整块连续的内存,可容纳数量等于Entity个数上限。
    连续一整块内存的好处,就是减少cache miss,提高性能。(虽然做一个贪吃蛇并不需要什么性能)
    每个Component内存块中,相同的下标的Component组合起来,表示一个Entity。
    在Resecs中,所有自定义的Component都要继承自Component类,这个类仅有一个字段,actived,表示该Component是否被激活。
    只有当一个Component的actived为true时,才表示这个Entity的确拥有这个Component。

    这里似乎是有优化空间,我们把actived从每个Component里提出来,用一个bitset做存储。这样可以得到一些额外的内存。

    Entity表示

    在实现中,我们用2个整数去唯一表示一个Entity(唯一的意思是,删除了这个Entity,这个Entity就找不回来了,不会因为下标相同又活过来),一个是index,另一个是generation,两个数合起来我们称为EntityID(见EntityID.hpp)。其中index表示它在内存中的位置,generation需要更多解释。

    在实现这个系统的过程中,有一个需求是,当我们手里拿着一个EntityID的时候,我们需要知道它是不是已经被摧毁。

    方案一,我们仅保存一个index。然后world里保存一个alive数组,alive[index] == true,表示这个Entity活着。
    这个方法,乍看上去很美好,实际上这个方法要求一个index被使用后就不能再次使用了,否则我们在一个系统中摧毁一个Entity,再重建这个Entity,此时alive[index]==true成立,但是该Entity已经不再是原来那个Entity了。

    方案二,我们在EntityID中增加一个字段generation,world里也有一个generation数组,默认为全0。当我们创建一个Entity时,不仅把alive[index] = true,同时将generation[index]赋值给被创建的EntityID中去。
    同时,在World摧毁一个Entity的时候,我们需要把被摧毁的Entity对应的generation的数字增加1。
    当alive[index] == true,并且generation[index] == generation的时候,我们才认为这个Entity活着。
    如果同样位置被创建了一个Entity,此时我们拿着被摧毁Entity的EntityID进行比较时,因为generation不同,我们还是认为这个Entity已经被摧毁。
    这个方案,当generation数组中某个元素溢出之后会出现问题,但是这个概率,emmmmmm

    在代码中,我用uint32_t表示index,int表示generation。实际上这个大小完全可以压缩一下。

    实际使用的时候,如果每次都根据EntityID去手动设置world里对应Component,有点寒酸;我这里加了一个包装类Entity(这里是类名Entity,上文中的不是),这个类是World的内部类,通过world->GetEntityHandle(EntityID)获得,实际内容就是一个EntityID和指向world的指针;用户通过这个Entity类进行操作,就很舒服;然后把world的操作Component的方法设为private,让用户只能通过Entity来操作Component,接口就很干净了。

    Singleton Component

    实现中加了一个接口,ISingletonComponent,这个接口没有任何作用,但是通过std::is_base_of,我们可以知道一个类是否继承了这个接口。
    如果一个Component继承了该接口,我们在申请内存的时候,只为它申请大小为1的空间,于是我们就可以通过下标为来访问Singleton Component了。
    在World被创建时,会自动创建一个Entity,该Entity相当于占住了0号内存空间,来防止意外操作。

    Get<T>、Set<T>的实现细节

    在开始实现Resecs之前,因为我对模板编程并不熟悉,这是让我最担心的一部分。
    理想状态下,我们使用Get<T>,应该能直接计算出内存地址,并返回这个Component的指针,没有任何的多余操作。

    先来看一下我在World里是怎么保存这些Component的内存池的。

    		using pComponent = Component*;
    		/* all components stores here.
    		The pComponent type actually doesn't do anything. Replace the pointer with (void*) will also work. Doing so makes it easier to understand.
    		To actually get to a component, a static_cast<T*> is needed before using index.
    		*/
    		pComponent* components; 
    

    非常简单,一个二级指针,用pComponent仅仅是为了方便理解;
    在释放内存的时候,要注意先cast到对应的type的指针,再去delete[],不然就ub了

    对于每一种Component的内存池,我们把它们安排在对应的位置。在初始化中,我们有如下代码:

    		World() {
    			components = new pComponent[sizeof...(TComps)];
    			InitializeComponents<0, TComps...>();
    			memset(generation, 0, sizeof(generation));
    			memset(alive, 0, sizeof(alive));
    			CreateEntity();	//create the singleton Entity.
    		}
    
    		template<int index, class T>
    		void InitializeComponents() {
    			if (std::is_base_of<ISingletonComponent, T>::value) {
    				components[index] = static_cast<Component*>(new T[1]);
    			}
    			else {
    				components[index] = static_cast<Component*>(new T[entityPoolSize]);
    			}
    		}
    
    		template<int index, class T, class V, class... U>
    		void InitializeComponents() {
    			InitializeComponents<index, T>();
    			InitializeComponents<index + 1, V, U...>();
    		}
    

    这里用到了C++11的新特性,可变参数模板。如果你不熟悉的话,我在这里简单讲讲执行流程:
    在构造函数第一行,我们初始化components为 new pComponent[sizeof...(TComps)]。其中sizeof...(TComps)返回TComps中参数的个数。这一行相信都能懂。
    之后调用初始化InitializeComponents,该方法会递归地在对应的components下标上执行一个new,最终完成初始化。

    初始化完毕后,components里,对应下标存储着对应模板参数中的Component数据。
    比如我们创建一个World<CompA,CompB,CompC>。那么components[0],保存的是所有的CompA,components[1],保存的是所有CompB……
    那么问题来了,要怎么实现T* Get();C++并没有"给定一个T,返回T在Ts...中对应位置"的办法。

    所幸的是万能的谷歌有答案,通过模板元编程,我们是可以获得T在Ts...中对应下标的,代码如下:

    	template <typename T, typename... Ts>
    	struct Index;
    
    	template <typename T, typename... Ts>
    	struct Index<T, T, Ts...> : std::integral_constant<std::uint16_t, 0> {};
    
    	template <typename T, typename U, typename... Ts>
    	struct Index<T, U, Ts...> : std::integral_constant<std::uint16_t, 1 + Index<T, Ts...>::value> {};
    

    使用该代码的GetComponent方法:

    		template<class T>
    		T* GetComponent(EntityIndex_t entityID) noexcept {
    			auto index = Index<T, TComps...>::value;
    			T* ptr = static_cast<T*>(components[index]);
    			return ptr + entityID;
    		}
    

    同时index的求值发生在编译期,可以说是非常理想了。

    Group

    Group的实现用到了上一篇文章中的监听者系统;
    其实非常简单,World实现了Component添加删除的事件,Group去监听事件,然后对每个有状况的Entity,都去查一下是否符合条件就ok了。符合条件的,塞到自己的Hashset(unordered_set)里,不符合的,从HashSet里删掉(如果有)。
    目前来看这个实现颇为暴力,有机会想想能不能优化。
    HashSet中保存的是EntityID,但是我另外实现了一个Iterator,在迭代的时候,先用EntityID生成一个Entity再返回,用的时候就很舒服了。

  • 相关阅读:
    搭建你的Spring.Net+Nhibernate+Asp.Net Mvc 框架 (五)测试你的成果
    初识Asp.Net MVC2.0
    搭建你的Spring.Net+Nhibernate+Asp.Net Mvc 框架 (四)配置全攻略
    Asp.Net MVC2.0 Url 路由入门
    逝去的2010,期待平静的2011【续】
    搭建你的Spring.Net+Nhibernate+Asp.Net Mvc 框架 (六)写在后面的话
    初识Asp.Net MVC2.0【续】
    Nhibernate入门与demo
    Entity Framework快速入门CodeOnly POCO
    用友面试经历 续【最终遭拒】
  • 原文地址:https://www.cnblogs.com/yangrouchuan/p/7822341.html
Copyright © 2011-2022 走看看