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. OpenLayers 3 入门教程

    OpenLayers 3 入门教程摘要OpenLayers 3对OpenLayers网络地图库进行了根本的重新设计.版本2虽然被广泛使用,但从JavaScript开发的早期发展阶段开始,已日益现实出它 ...

  2. 解决zabbix使用中文是出现乱码的问题

       解决zabbix使用中文是出现乱码的问题 ①:上传windows的simhei.ttf字体到zabbix服务器的/usr/share/zabbix/fonts/目录下   ②:编辑/usr/sh ...

  3. 设计模式PHP篇(三)————装饰器模式

    简单的用php实现了装饰器模式: <?php /** *简单的装饰器模式 */ class PrintText { protected $decorators = []; public func ...

  4. 【前端学习笔记05】JavaScript数据存储Cookie相关方法封装

    //Cookie设置 //设置新cookie function setCookie(name,value,duration){ var date = new Date(); date.setTime( ...

  5. css 层加透明度后文字依然清晰

    background: rgba(, , , !important; /*实现FF背景透明,文字不透明*/ filter: Alpha(opacity=); background: #0a0a0a; ...

  6. java中的error该不该捕获

    写java程序时,通常会被提示捕获异常,而又有一些异常是不需要强制捕获的,这是一个被说烂了的话题.像我一样从其他语言转过来的人确实有点迷惑,那我以我的理解重新解释一遍吧. 异常的基类是Exceptio ...

  7. java执行cmd命令并获取输出结果

    1.java执行cmd命令并获取输出结果 import java.io.BufferedReader; import java.io.InputStreamReader; import org.apa ...

  8. BGP与BGP机房 国内网络运营商的主流网关解决方案

    边界网关协议(BGP)是运行于 TCP 上的一种自治系统的路由协议. BGP 是唯一一个用来处理像因特网大小的网络的协议,也是唯一能够妥善处理好不相关路由域间的多路连接的协议. BGP 构建在 EGP ...

  9. App简介及登录页面

    一. APP目录 app目录: -migrations 数据操作记录,是自动创建的.数据修改表结构 -__init__.py #在python3里面可有可无都行 -__init__.py -admin ...

  10. 【洛谷】CYJian的水题大赛【第二弹】解题报告

    点此进入比赛 T1: JerryC Loves Driving 第一题应该就是一道水分题(然而我只水了130分),我的主要做法就是暴力模拟,再做一些小小的优化(蠢得我自己都不想说了). My Code ...