Unity下的ECS框架 (Entitas框架)
Unity下的ECS框架 (Entitas框架)
组件-实体-系统 (ECS \CES)游戏编程模型:
组件
一个组件可以使用C中的结构体来进行设计。它没有方法,只是用来存储一些数据,并不在它之上进行动作。一个经典的实现方式是,每一个不同的组件都继承至一个抽象的Componet类,通过这样的方法我们能够在运行时动态的添加组件,识别组件。每一个组件都描述了实体的某个属性特征。当他们单独存在的时候,实际上是没有任何意义的,但是当多个组件通过系统的方式组织在一起,就能够发挥强大的力量。我们可以使用空的组件来对实体进行标记,从而能够在运行时动态的识别它。
实体
一个实体指的是存在于你的游戏世界中的物体。实体在代码上就是一个组件的列表。由于实体的结构实在是太简单了,所以很多实现都没有专门的设计一个实体的数据结构。相反的,一个实体就是一个ID,所有组成这个实体的组件将会被这个ID给标记,从而明确的知道哪些组件是属于哪个实体的。如果你想的话,你可以在运行时,动态的将组件从实体中移除或者增加一个或多个你感兴趣的组件。比如说,如果玩家发出了一个冰系魔法,将敌人冻住,你只要简单的将它的速度组件移除,那么敌人就静止住了。
系统
注意,我在上面没有提到任何和游戏逻辑相关的话题。游戏逻辑是系统需要进行的工作。一个系统就是对所有相关联的组件记性操作,比如说,同一个实体的组件。举个例子,人物的移动系统可能会对位置(Position),速度(Velocity),碰撞(Collider),和输入(Input)进行操作。每一个系统,都会在每一帧中按照逻辑上的顺序进行更新。如果要让一个角色跳起来,我们只要检测下Input中的keyJump按键是否被按下,如果是,那么系统就会查看下载Collider中是否有一个接触了地面,如果是,就将这个实体的Velocity的y速度设置一下,让这个物体跳起来。
由于系统只会对相关联的组件进行操作,所以组件就定义了一个实体所应该具有的行为。比如说,如果一个实体有一个Position组件,但是没有Velocity组件,那么我们就知道,这个物体是静止不动的,系统就不会对这个实体的Position组件进行操作了。当我们对这个实体增加了一个Velocity组件的时候,系统就会使用Velocity组件来对物体进行移动。这样的行为可以使用被标记的组件来进行,被标记的组件能够重复的使用在不同的上下文中。对一个实体,增加一个空的Player组件,将会为这个实体打上了Player的标签,那么PlayerControl系统,就会寻找带有这个标签的所有组件,然后使用Input中的数据,进行操作。
- 组件表示一个游戏对象可以拥有的数据部分
- 实体用来代表一个游戏对象,它是多个组件的聚合
- 系统提供了在这些组件上进行的操作
实现
组件
在上篇文章中,我曾说过,组件实际上就是一个C结构体,只拥有简单普通的数据而已,所以我也就会使用结构体来实现组件。下面的组件,从名字上看就能够很好的明白它的作用到底是什么。在下面我会实现三种组件:
Displacement(x,y)
Velocity(x,y)
Appearance(name)
下面的代码,演示了如何定义Displacement组件。它只是拥有两个分量的简单结构体而已:
typedef struct
{
float x ;
float y ;
} Displacement ;
Velocity组件也是同样的进行定义了,显示中只是含有一个string成员而已。
除了上面定义的具体的组件类型,我们还需要一个组件标示符,用来对组件进行标示。每一个组件和系统都会拥有一个组件标示符,如何使用将会在下面详细的解释。
typedef enum
{
COMPONENT_NONE = 0 ,
COMPONENT_DISPLACEMENT = 1 << 0 ,
COMPONENT_VELOCITY = 1 << 1 ,
COMPONENT_APPEARANCE = 1 << 2
} Component ;
定义组件标示符是很简单的事情。在实体的上下文中,我们使用组件标示符来表示这个实体拥有哪些组件。如果这个实体,拥有Displacement和Appearance组件,那么这个实体的组件标示符将会是 COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE 。
实体
实体本身不会被明确的定义为一个具体的数据类型。我们并不会使用面向对象的方法来对实体进行一个类的定义,然后让它拥有一系列的成员属性。因此,我们将会将组件加入到内存中去,创建一个结构数组。这样会提高缓冲效率,并且会有助于迭代。所以,为了实现这个,我们使用这些结构数组的下标来表示实体。这个下标,就表示是实体的一个组件。
我称这个结构数组为World。这个结构,不仅仅保留了所有的组件,而且还保存了每一个实体的组件标示符。
typedef struct
{
int mask[ENTITY_COUNT];
Displacement displacement[ENTITY_COUNT];
Velocity velocity[ENTITY_COUNT];
Appearance appearance[ENTITY_COUNT];
} World;
ENTITY_COUNT在我的测试程序中,被定义为100,但是在一个真实的游戏中,这个值应该更加的大。在这个实现中,最大数值就被限制在100.实际上,我更加喜欢在栈中实现这个结构数组,而不是在堆中实现,但是考虑到读者可能会使用C++来实现这个World,它也是可以使用vector来保存的。
除了上面的结构体之外,我还定义了一些函数,来对这些实体进行创建和销毁。
unsigned int createEntity(World *world)
{
unsigned int entity;
for(entity = 0; entity < ENTITY_COUNT; ++entity)
{
if(world->mask[entity] == COMPONENT_NONE)
{
return(entity);
}
}
printf("Error! No more entities left!\n");
return(ENTITY_COUNT);
}
void destroyEntity(World *world, unsigned int entity)
{
world->mask[entity] = COMPONENT_NONE;
}
实际上,create方法并不是创建一个实体,而是返回World中第一个为空的实体下标。第二个方法,只是简单的将实体的组件表示符设置为COMPONENT_NONE而已。把一个实体设置为空的组件是很直观的表示方法,因为它为空的话,就表示没有任何的系统将会在这个实体上进行操作了。
我还编写了一些用来创建完整实体的代码,比如下面的代码将会创建一个Tree,这个Tree只拥有Displacement和Appearance。
unsigned int createTree(World *world, float x, float y)
{
unsigned int entity = createEntity(world);
world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE;
world->displacement[entity].x = x;
world->displacement[entity].y = y;
world->appearance[entity].name = "Tree";
return(entity);
}
在一个真实的游戏引擎中,你的实体可能需要额外的数据来进行创建,但是这个已经不再我介绍的范围内了。尽管如此,读者还是可以看见,这样的系统将会具有多么高的灵活性。
系统
在这个实现中,系统是最复杂的部分了。每一个系统,都是对某一个组件进行操作的函数方法。这是第二次使用组件标示符了,通过组件标示符,我们来定义系统将会对什么组件进行操作。
#define MOVEMENT_MASK (COMPONENT_DISPLACEMENT | COMPONENT_VELOCITY)
void movementFunction(World *world)
{
unsigned int entity;
Displacement *d;
Velocity *v;
for(entity = 0; entity < ENTITY_COUNT; ++entity)
{
if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK)
{
d = &(world->displacement[entity]);
v = &(world->velocity[entity]);
v->y -= 0.98f;
d->x += v->x;
d->y += v->y;
}
}
}
这里就显示出了组件标示符的威力了。通过组件标示符,我们能够在函数中确定这个实体是否具有这样的属性,并且速度很快。如果将每一个实体定义为一个结构体的话,那么确定它是否有这些组件,这样的操作将会非常耗时。
这个系统,会自动的添加重力,然后对Displacement和Velocity进行操作。如果所有的实体都是正确的进行了初始化,那么每一个进行这样操作的实体,都会有一个有效的Displacement和Velocity组件。
对于这个组件标示符的一个缺点就是,这样的组合是有限的。在我们这里的实现中,它最多只能是32位的,也就是说最多只能够拥有32个组件类型。C++提供了一个名为std::bitset<n>的类,这个类可以拥有N位的类型,而且我确信,如果你使用的是其他的编程语言的话,也会有这样的类型提供。在C中,可以使用一个数组来进行扩展,像下面这样:
(EntityMask[0] & SystemMask[0]) == SystemMask[0] && (EntityMask[1] & SystemMask[1]) == SystemMask[1] // && ...
问题
这样的系统在我的一些程序中能够很好的进行工作,并且这样的系统能够很容易的进行扩展。它也能够很容易的在一个主循环中进行工作,并且只要添加少量的代码就能够从外部读取文件来创建实体对象。
这一小节,将会讲述在游戏机制中可能出现的一些问题,还会讲述一些这个系统所具有的高级特性。
升级和碰撞过滤
这个问题是在上篇文章中,网友Krohm提出来得。他想知道,在这样的系统中,如何实现游戏特殊行为了。他提出,如果在升级的时候,想要避免和某种类型的物体进行碰撞,该如何进行。
解决这样的问题,我们使用一个叫做动态组件的东西。我们来创建一个组件,叫做GhostBehavior,这个组件拥有一个限定符列表,我们通过这个列表来判断,哪些实体可以让物体穿越过去。比如说,一个组件标示符的列表,或者是材质下标的列表。任何的组件,都可以在任何时候,任何地方被移除出去。当玩家,拾取到了一个升级包,GhostBehavior组件将会增加到玩家实体的列表中去。我们还可以为这个组件创建一个内置的定时器,一旦时间到了,就自动的将自己从列表中移除出去。
为了不进行某些碰撞,我们可以使用物理引擎中的一个经典的碰撞回应。在大部分的物理引擎中,第一步都是先进行碰撞检测,然后产生接触,在然后,为某一个物体添加一个接触力。我们假设,这些工作都是在一个系统中实现的,但是有一个组件能够对每一个实体的碰撞接触进行跟踪记录,这个组件叫做Collidable。
我们创建一个新的系统,同时对GhostBehavior和Collidable进行处理。在上面介绍的两个步骤之间,我们将实体之间的接触删除掉,这样他们就不会产生力,也就不会产生碰撞,让物体穿越过去了。这样的效果,就会产生一个无效的碰撞。同样的系统也能够用来将GhostBehavior进行移除。
同样的策略,也能够用来处理,当发生了碰撞时,我们希望进行某种特定的操作的情况。对于每一个特定的行为,我们都可以创建一个系统,或者同一个系统可以同时处理多个特定的动作。不管怎么样,系统都要先判断两个物体是否发生了碰撞,然后才能够进行特定的行为。
消灭所有怪物
另外一个问题,就是如何通过一个指令,来秒杀所有的怪物。
解决这个问题的关键地方是实现一个系统,这个系统将会在主循环的外面进行。任何一个实体,如果它是怪物的话,那么它就应该有一个同样的组件标示符。比如说,同时拥有AI和血量的实体,就是怪物,这样的判断可以简单的使用组件标示符来进行判断。
还记得我们在上面说过的,每一个系统实际上就是对某个组件标示符进行操作的函数。我们将秒杀技能定义为一个系统。这个系统将会用一个函数来实现。在这个函数中,,最核心的操作就是调用destroyMonster函数了,但是同时可能也会创建一个粒子特效,或者播放一段音乐等。这个系统的组件标示符可能是这样的COMPONENT_HEALTH COMPONENT_AI。
在前面一篇文章中,我讲述过了每一个实体都能够拥有一个或者多个输入组件,这些输入组件将会包括一个boolean值,或者真实的值,用来表示不同的输入。我们创建一个MagicInputComponet组件,这个组件只有一个bool值,一旦将这个组件加入到实体中去,每一个实体都会对这个组件进行处理,从而消灭所有的怪物。
每一个秒杀技能都有一个独特的ID,这个ID将会用来对查找表进行查找。一旦在查找表中,找到了这个ID,那么就调用这个ID对应的函数,让这个函数,来运行这个系统消灭所有的怪物。
结论
记住,这里的实现只是一个非常简单的方法。它仅仅对我们这里的测试程序有效而已,对于一个完整的游戏来说,它并没有那个能力来驱动它。然后,我希望,通过这个例子,你已经明白了设计ECS系统的主要原则,并且能够独立的使用你自己的熟练的语言来实现它。
上一篇: 一个简单的神经网络的实现
下一篇: 本地缓存同步的一个简单方案
推荐阅读
-
Unity下的ECS框架 (Entitas框架)
-
真Unity3d_比较5个帧同步框架的实现
-
企业应用下的业务组件开发实践 博客分类: 架构乱弹 企业应用应用服务器OSGI框架EJB
-
企业应用下的业务组件开发实践 博客分类: 架构乱弹 企业应用应用服务器OSGI框架EJB
-
Tomcat下多域名的配置 博客分类: others TomcatStrutsIE框架浏览器
-
Windows下Java+MyBatis框架+MySQL的开发环境搭建教程
-
Windows下Java+MyBatis框架+MySQL的开发环境搭建教程
-
ssi架构下使用struts2的标签的一个小问题 博客分类: web框架
-
springboot学习之一(Idea环境下搭建基于Maven的SpringBoot框架)
-
基于hibernate框架在eclipse下的配置方法(必看篇)