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的更多相关文章

  1. 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 ...

  2. 自己写一个java.lang.reflect.Proxy代理的实现

    前言 Java设计模式9:代理模式一文中,讲到了动态代理,动态代理里面用到了一个类就是java.lang.reflect.Proxy,这个类是根据代理内容为传入的接口生成代理用的.本文就自己写一个Pr ...

  3. Cordova webapp实战开发:(6)如何写一个iOS下获取APP版本号的插件?

    上一篇我们学习了如何写一个Andorid下自动更新的插件,我想还有一部分看本系列blog的开发人员希望学习在iOS下如何做插件的吧,那么今天你就可以来看看这篇文字了. 本次练习你能学到的 学习如何获取 ...

  4. 分享:计算机图形学期末作业!!利用WebGL的第三方库three.js写一个简单的网页版“我的世界小游戏”

    这几天一直在忙着期末考试,所以一直没有更新我的博客,今天刚把我的期末作业完成了,心情澎湃,所以晚上不管怎么样,我也要写一篇博客纪念一下我上课都没有听,还是通过强大的度娘完成了我的作业的经历.(当然作业 ...

  5. 写一个迷你版Smarty模板引擎,对认识模板引擎原理非常好(附代码)

    前些时间在看创智博客韩顺平的Smarty模板引擎教程,再结合自己跟李炎恢第二季开发中CMS系统写的tpl模板引擎.今天就写一个迷你版的Smarty引擎,虽然说我并没有深入分析过Smarty的源码,但是 ...

  6. 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)

    从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...

  7. python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)

    python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...

  8. (转)如何学好C语言,一个成功人士的心得!

    zidier111发表于 2013-1-26 08:59:05   今 天,我能够自称是一个混IT的人,并能以此谋生,将来大家能一次谋生,都要感谢两个人:克劳德.香农和约翰.冯.诺依曼,是他们发现了所 ...

  9. 怎样学好C语言,一个成功人士的心得!

    今天,我能够自称是一个混IT的人,并能以此谋生,将来大家能一次谋生,都要感谢两个人:克劳德.香农和约翰.冯.诺依曼,是他们发现了全部的数字化信息,不论是一段程序,一封email,一部电影都是用一连串的 ...

随机推荐

  1. Robot 模拟操作键盘 实现复制粘贴功能;

    1.代码逻辑 : a.封装一个粘贴的方法体:setAndctrlVClipboardData(String string);参数string是需要粘贴的内容 : b.声明一个StringSelecti ...

  2. Additinal Dependencies和#pragma comment(lib,"*.lib")的分析

     网上.一些书上也写道,这两种方式作用一样.其实仔细分析,它们两者还是有非常大的差异的. Additinal Dependencies和#pragma comment(lib,"*.lib& ...

  3. HTML常用标签查询

    JAVA开发避免不了要接触前端,所以我不得不从0开始学习前端内容!下面分享我自己总结的HTML常用标签查询代码:将下面代码复制粘贴到文本文档,然后另存为html格式;通过file:///文档保存路径的 ...

  4. 方法调用时候 传入this 谁调用 传入谁

    方法调用时候 传入this 谁调用 传入谁

  5. solr服务器的查询过程

    SolrDispatchFilter的作用 This filter looks at the incoming URL maps them to handlers defined in solrcon ...

  6. 【JavaScript&jQuery】省市区三级联动

    HTML: <%@page import="com.mysql.jdbc.Connection"%> <%@ page language="java&q ...

  7. Linux正确的关机方式

    本人还未入门,仅看书所得. Linux不建议的是直接关电源.Linux后台可能有多人在工作,直接关电源可能造成文件的毁坏. 正常关机之前应该干两件事:一.查看一下谁在线:二.通知一下别人啦,通知别人可 ...

  8. BZOJ 3786: 星系探索 解题报告

    3786: 星系探索 Description 物理学家小C的研究正遇到某个瓶颈. 他正在研究的是一个星系,这个星系中有n个星球,其中有一个主星球(方便起见我们默认其为1号星球),其余的所有星球均有且仅 ...

  9. spark(四)

    一. spark 2  版本 相对于以前版本的变化 spark core  : Accumulators (累加器):性能更好,页面上也可以看到累加器的信息 spark sql: 1. 2.DataS ...

  10. 【bzoj4810】【ynoi2018】由乃的玉米田

    4810: [Ynoi2017]由乃的玉米田 Time Limit: 30 Sec  Memory Limit: 256 MBSubmit: 1090  Solved: 524[Submit][Sta ...