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再返回,用的时候就很舒服了。

  • 相关阅读:
    flock对文件锁定读写操作的问题 简单
    hdu 2899 Strange Fuction(二分)
    hdu 2199 Can you solve this equation? (二分)
    poj 3080 Blue Jeans (KMP)
    poj 2823 Sliding Window (单调队列)
    poj 2001 Shortest Prefixes (trie)
    poj 2503 Babelfish (trie)
    poj 1936 All in All
    hdu 3507 Print Article (DP, Monotone Queue)
    fzu 1894 志愿者选拔 (单调队列)
  • 原文地址:https://www.cnblogs.com/yangrouchuan/p/7822341.html
Copyright © 2011-2022 走看看