用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再返回,用的时候就很舒服了。
用C++写一个没人用的ECS的更多相关文章
- Java-集合(没做出来)第四题 (List)写一个函数reverseList,该函数能够接受一个List,然后把该List 倒序排列。 例如: List list = new ArrayList(); list.add(“Hello”); list.add(“World”); list.add(“Learn”); //此时list 为Hello World Learn reverseL
没做出来 第四题 (List)写一个函数reverseList,该函数能够接受一个List,然后把该List 倒序排列. 例如: List list = new ArrayList(); list.a ...
- 自己写一个java.lang.reflect.Proxy代理的实现
前言 Java设计模式9:代理模式一文中,讲到了动态代理,动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的.本文就自己写一个Pr ...
- Cordova webapp实战开发:(6)如何写一个iOS下获取APP版本号的插件?
上一篇我们学习了如何写一个Andorid下自动更新的插件,我想还有一部分看本系列blog的开发人员希望学习在iOS下如何做插件的吧,那么今天你就可以来看看这篇文字了. 本次练习你能学到的 学习如何获取 ...
- 分享:计算机图形学期末作业!!利用WebGL的第三方库three.js写一个简单的网页版“我的世界小游戏”
这几天一直在忙着期末考试,所以一直没有更新我的博客,今天刚把我的期末作业完成了,心情澎湃,所以晚上不管怎么样,我也要写一篇博客纪念一下我上课都没有听,还是通过强大的度娘完成了我的作业的经历.(当然作业 ...
- 写一个迷你版Smarty模板引擎,对认识模板引擎原理非常好(附代码)
前些时间在看创智博客韩顺平的Smarty模板引擎教程,再结合自己跟李炎恢第二季开发中CMS系统写的tpl模板引擎.今天就写一个迷你版的Smarty引擎,虽然说我并没有深入分析过Smarty的源码,但是 ...
- 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)
从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...
- python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)
python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...
- (转)如何学好C语言,一个成功人士的心得!
zidier111发表于 2013-1-26 08:59:05 今 天,我能够自称是一个混IT的人,并能以此谋生,将来大家能一次谋生,都要感谢两个人:克劳德.香农和约翰.冯.诺依曼,是他们发现了所 ...
- 怎样学好C语言,一个成功人士的心得!
今天,我能够自称是一个混IT的人,并能以此谋生,将来大家能一次谋生,都要感谢两个人:克劳德.香农和约翰.冯.诺依曼,是他们发现了全部的数字化信息,不论是一段程序,一封email,一部电影都是用一连串的 ...
随机推荐
- PAT 甲级 1041 Be Unique
https://pintia.cn/problem-sets/994805342720868352/problems/994805444361437184 Being unique is so imp ...
- linux 内核态调试函数BUG_ON()[转]
一些内核调用可以用来方便标记bug,提供断言并输出信息.最常用的两个是BUG()和BUG_ON(). 当被调用的时候,它们会引发oops,导致栈的回溯和错误信息的打印.为什么这些声明会导致 oops跟 ...
- 开源人脸识别face_recognition
环境:python36 1.安装dlib.face_recognition windows版 下载dlib,cp后面是py版本 下载地址:https://pypi.org/simple/dlib/ 提 ...
- bzoj1272 Gate Of Babylon(计数方法+Lucas定理+乘法逆元)
Description Input Output Sample Input 2 1 10 13 3 Sample Output 12 Source 看到t很小,想到用容斥原理,推一下发现n种数中选m个 ...
- bzoj3663/4660CrazyRabbit && bzoj4206最大团
题意 给出平面上N个点的坐标,和一个半径为R的圆心在原点的圆.对于两个点,它们之间有连边,当且仅当它们的连线与圆不相交.求此图的最大团. 点数<=2000,坐标的绝对值和半径<=5000. ...
- 【bzoj3697】采药人的路径 树的点分治
题目描述 给出一棵 $n$ 个点的树,每条边的边权为1或0.求有多少点对 $(i,j)$ ,使得:$i$ 到 $j$ 的简单路径上存在点 $k$ (异于 $i$ 和 $j$ ),使得 $i$ 到 $k ...
- Splitter Control for Dialog
原文链接地址:https://www.codeproject.com/Articles/595602/Splitter-Control-for-Dialog Introduction Yes, tha ...
- python基础----析构函数__del__
析构方法,当对象在内存中被释放时,自动触发执行. 注:此方法一般无须定义,因为Python是一门高级语言,程序员在使用时无需关心内存的分配和释放,因为此工作都是交给Python解释器来执行,所以,析构 ...
- 《JavaScript高级程序设计(第三版)》-3
相等操作符 相等和不相等 在转换不同的数据类型时,相等和不想等操作符遵循下面基本规则: 如果有一个操作符数是布尔值,则在比较相等性之前先将其转换为数值——false转换为0,而true转换为1: 如果 ...
- jsp 的 7 个动作指令
动作指令与编译指令不同,编译指令是通知 Servlet 引擎的处理消息,而动作指令只是运行时的动作.编译指令在将 JSP 编译成 Servlet 时起作用:而处理指令通常可替换成 JSP 脚本,它只是 ...