ECS 游戏架构 实现

转载自:http://blog.csdn.net/i_dovelemon/article/details/27230719

实现 组件-实体-系统 - 博客频道

这篇文章是在我前面文章,理解组件-实体-系统,的基础上进行的。如果你还没有阅读过这篇文章,建议你去看看,这样你就会对这里要实现的内容不会那么的陌生。

先来总结下,上篇文章讲些什么内容:

  • 组件表示一个游戏对象可以拥有的数据部分
  • 实体用来代表一个游戏对象,它是多个组件的聚合
  • 系统提供了在这些组件上进行的操作

这篇文章将会讲述如何实现一个ECS系统,并且会解决一些存在的问题。所有的实例代码,我都是使用C语言来编写。

组件

在上篇文章中,我曾说过,组件实际上就是一个C结构体,只拥有简单普通的数据而已,所以我也就会使用结构体来实现组件。下面的组件,从名字上看就能够很好的明白它的作用到底是什么。在下面我会实现三种组件:

  1. Displacement(x,y)
  2. Velocity(x,y)
  3. Appearance(name)

下面的代码,演示了如何定义Displacement组件。它只是拥有两个分量的简单结构体而已:

  1. typedef struct
  2. {
  3. float x ;
  4. float y ;
  5. } Displacement ;
typedef struct
{
        float x ;
        float y ;
} Displacement ;

Velocity组件也是同样的进行定义了,显示中只是含有一个string成员而已。

除了上面定义的具体的组件类型,我们还需要一个组件标示符,用来对组件进行标示。每一个组件和系统都会拥有一个组件标示符,如何使用将会在下面详细的解释。

  1. typedef enum
  2. {
  3. COMPONENT_NONE = 0 ,
  4. COMPONENT_DISPLACEMENT = 1 << 0 ,
  5. COMPONENT_VELOCITY = 1 << 1 ,
  6. COMPONENT_APPEARANCE = 1 << 2
  7. } Component ;
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。这个结构,不仅仅保留了所有的组件,而且还保存了每一个实体的组件标示符。

  1. typedef struct
  2. {
  3. int mask[ENTITY_COUNT];
  4. Displacement displacement[ENTITY_COUNT];
  5. Velocity velocity[ENTITY_COUNT];
  6. Appearance appearance[ENTITY_COUNT];
  7. } 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来保存 的。

除了上面的结构体之外,我还定义了一些函数,来对这些实体进行创建和销毁。

  1. unsigned int createEntity(World *world)
  2. {
  3. unsigned int entity;
  4. for(entity = 0; entity < ENTITY_COUNT; ++entity)
  5. {
  6. if(world->mask[entity] == COMPONENT_NONE)
  7. {
  8. return(entity);
  9. }
  10. }
  11. printf("Error!  No more entities left!\n");
  12. return(ENTITY_COUNT);
  13. }
  14. void destroyEntity(World *world, unsigned int entity)
  15. {
  16. world->mask[entity] = COMPONENT_NONE;
  17. }
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。

  1. unsigned int createTree(World *world, float x, float y)
  2. {
  3. unsigned int entity = createEntity(world);
  4. world->mask[entity] = COMPONENT_DISPLACEMENT | COMPONENT_APPEARANCE;
  5. world->displacement[entity].x = x;
  6. world->displacement[entity].y = y;
  7. world->appearance[entity].name = "Tree";
  8. return(entity);
  9. }
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);
}

在一个真实的游戏引擎中,你的实体可能需要额外的数据来进行创建,但是这个已经不再我介绍的范围内了。尽管如此,读者还是可以看见,这样的系统将会具有多么高的灵活性。

系统

在这个实现中,系统是最复杂的部分了。每一个系统,都是对某一个组件进行操作的函数方法。这是第二次使用组件标示符了,通过组件标示符,我们来定义系统将会对什么组件进行操作。

  1. #define MOVEMENT_MASK (COMPONENT_DISPLACEMENT | COMPONENT_VELOCITY)
  2. void movementFunction(World *world)
  3. {
  4. unsigned int entity;
  5. Displacement *d;
  6. Velocity *v;
  7. for(entity = 0; entity < ENTITY_COUNT; ++entity)
  8. {
  9. if((world->mask[entity] & MOVEMENT_MASK) == MOVEMENT_MASK)
  10. {
  11. d = &(world->displacement[entity]);
  12. v = &(world->velocity[entity]);
  13. v->y -= 0.98f;
  14. d->x += v->x;
  15. d->y += v->y;
  16. }
  17. }
  18. }
#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中,可以使用一个数组来进行扩展,像下面这样:

  1. (EntityMask[0] & SystemMask[0]) == SystemMask[0] && (EntityMask[1] & SystemMask[1]) == SystemMask[1]
(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系统的主要原则,并且能够独立的使用你自己的熟练的语言来实现它。

时间: 2024-08-15 19:56:01

ECS 游戏架构 实现的相关文章

《游戏架构设计与策划基础》笔记 第一章 游戏策划概述(上)

1.1 什么是游戏策划 游戏的目的就是通过玩来获得娱乐,因此,设计游戏既需要艺术家一样的创造力,也需要工程师一样的精心规划.游戏设计是一门手艺,就像是好莱坞的电影摄像或服装设计一样.一个游戏既含有艺术要素,也含有功能要素:它必须能给人以美的享受,同时又必须能很好地运行,让游戏者享受到快乐.具备这两种特点的游戏才是好的游戏. 1.2 游戏策划的任务 游戏策划根据自己的创作理念,结合市场调研得来的数据,参考其他开发人员的意见和建议,在开发条件允许的基础上,将游戏创意以及游戏内容和规则细化完整,形成策

[笔记]《游戏架构设计与策划基础》第三章 游戏概念及原型设计

概念设计的过程:产生创意.加工创意和创建游戏概念设计文档. 3.1 创意的来源 (1)大胆设想 (2)利用现有的娱乐资源 (3)利用现有的游戏体系 (4)收集创意 3.2 加工创意 (1)合成--需要考虑如何将两个概念融合而成一款游戏,带给玩家新的游戏体验. (2)共鸣--含有协作的意思,它使故事和主题内容对游戏玩家能够产生更加深刻的影响. 3.3 游戏概念设计文档 一般包括以下要素的部分或全部:      标题--游戏的名称.      平台--游戏适合的平台.      种类--游戏的种类.

【转载】U3D 游戏引擎之游戏架构脚本该如何来写

原文:http://tech.ddvip.com/2013-02/1359996528190113.html Unity3D 游戏引擎之游戏架构脚本该如何来写 2013-02-05 00:48:48     发表评论 这篇文章MOMO主要想大家说明一下我在Unity3D游戏开发中是如何写游戏脚本的,对于Unity3D这套游戏引擎来说入门极快,可是要想做好却非常的难.这篇文章的目的是让哪些已经上手Unity3D游戏引擎的朋友学会如何更好的写游戏脚本,当然本文这紧紧是我这么多年对游戏开发的认知,你也

U3D 游戏引擎之游戏架构脚本该如何来写

这篇文章MOMO主要想大家说明一下我在Unity3D游戏开发中是如何写游戏脚本的,对于Unity3D这套游戏引擎来说入门极快,可是要想做好却非常的难.这篇文章的目的是让哪些已经上手Unity3D游戏引擎的朋友学会如何更好的写游戏脚本,当然本文这紧紧是我这么多年对游戏开发的认知,你也可以有你自己的看法.首先我们看看游戏主要是由哪几部分组成的,如下图所示,任何平台下的任何游戏核心都是由:数据.逻辑.渲染三大部分组成. 当你写过>=2个平台下的游戏时你会发现其实游戏开发很“容易”,为什么“容易”呢?因

游戏架构之二(转)

棋牌类游戏常用架构: 我从事过4年的棋牌类游戏开发,使用过的架构大致如上,各模块解释如下. LoginServer: 登陆服务器,主要负责player 的登陆请求,验证player的合法性,为合法的player分配session,与cilent 采用短连接方式,可以有多个来进行负载均衡.验证player通过后,LoginServer找到一个合适的GateWay发送给client. GateWay: 网关服务器,有多个来做负载均衡,与client 使用长连接方式,client发送的消息都通过Gat

游戏架构之一(转)

RPG游戏经典的系统架构设计 : bigword 游戏引擎就是使用这种架构,我认识的很多rpg游戏公司的同事也大致采用了这种架构方式. loginapp : 登陆服务器,主要负责player 的登陆请求,验证player的合法性,为合法的player分配session,与cilent 采用短连接方式,可以有多个loginapp来负载均衡.验证player通过后,loginapp通过baseappmgr找到一个合适的baseapp发送给client. baseapp: 我们可以叫做网关服务器,有多

Unity3D心得之游戏架构设计和属性的运用

由于Unity是一个脚本化开发的引擎,所以实现一个具体功能的代码往往非常简短. 我刚开始写的时候总会碰到一个头疼的问题,当游戏变得越来越大,场景内的物体越来越多,各种繁杂的脚本互相交错,最后当需求变更或者发现BUG的时候,我发现有茫茫多的代码需要检查和修改. 关于怎么样来架构Unity3D中的脚本,详细的可以看一下雨松momo的这篇文章 http://www.xuanyusong.com/archives/1851 总结来讲主要是三个点: 1.运用单例脚本来控制一类工作 2.一个脚本不要管和自己

`cocos2dx非完整` 游戏架构缩影 添加启动流程

这期的话题可能不是很好, 我没有想到很好的词句去更好的表达. 我一直都是很固执的认为, 同一类型的游戏,在开发做的前期工作上面其实都是可以复用的,也就是大同小异的.从游戏启动,启动日志,启动检查,检查更新,到进入游戏.这些都是那一套东西,我想把这些东西抽象一下,概括出一个叫做"流程"的概念. 我的想法就是流程是顺序执行的, 就像我喜欢画图,先做什么,然后做什么,做完什么做什么.其实从一款app启动到进入游戏,这之间的过程都是流程化进行的.还有一个很经典的例子,新手引导,其实新手引导就是

游戏架构其九:光线投射和天空 { Raycast and Sky }

光线和天空能够大大增强游戏的画面效果,以下是实现: 1. 光线效果 Raycast #pragma once //======================================================================== // Raycast.h - implements a raycast into the rendered scene //==========================================================