【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2373

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 2837|回复: 30

[士郎] 游戏开发中的ECS 架构概述

[复制链接]  [移动端链接]
排名
3
昨日变化

7088

主题

7615

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
11121
威望
30
注册时间
2013-7-29
在线时间
3645 小时
最后登录
2018-12-17

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-9-19 16:03:58 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
0x00 何为ECS架构
ECS,即 Entity-Component-System(实体-组件-系统) 的缩写,其模式遵循组合优于继承原则,游戏内的每一个基本单元都是一个实体,每个实体又由一个或多个组件构成,每个组件仅仅包含代表其特性的数据(即在组件中没有任何方法),例如:移动相关的组件MoveComponent包含速度、位置、朝向等属性,一旦一个实体拥有了MoveComponent组件便可以认为它拥有了移动的能力,系统便是来处理拥有一个或多个相同组件的实体集合的工具,其只拥有行为(即在系统中没有任何数据),在这个例子中,处理移动的系统仅仅关心拥有移动能力的实体,它会遍历所有拥有MoveComponent组件的实体,并根据相关的数据(速度、位置、朝向等),更新实体的位置。
实体与组件是一个一对多的关系,实体拥有怎样的能力,完全是取决于其拥有哪些组件,通过动态添加或删除组件,可以在(游戏)运行时改变实体的行为。
0x01 ECS基本结构
一个使用ECS架构开发的游戏基本结构如下图所示:
1.jpg
先有一个World,它是系统和实体的集合,而实体就是一个ID,这个ID对应了组件的集合。组件用来存储游戏状态并且没有任何行为,系统拥有处理实体的行为但是没有状态。
0x02 详解ECS中实体、组件与系统1. 实体
实体只是一个概念上的定义,指的是存在你游戏世界中的一个独特物体,是一系列组件的集合。为了方便区分不同的实体,在代码层面上一般用一个ID来进行表示。所有组成这个实体的组件将会被这个ID标记,从而明确哪些组件属于该实体。由于其是一系列组件的集合,因此完全可以在运行时动态地为实体增加一个新的组件或是将组件从实体中移除。比如,玩家实体因为某些原因(可能陷入昏迷)而丧失了移动能力,只需简单地将移动组件从该实体身上移除,便可以达到无法移动的效果了。
样例:
  • Player(Position, Sprite, Velocity, Health)
  • Enemy(Position, Sprite, Velocity, Health, AI)
  • Tree(Position, Sprite)
注:括号前为实体名,括号内为该实体拥有的组件
2. 组件
一个组件是一堆数据的集合,可以使用C语言中的结构体来进行实现。它没有方法,即不存在任何的行为,只用来存储状态。一个经典的实现是:每一个组件都继承(或实现)同一个基类(或接口),通过这样的方法,我们能够非常方便地在运行时动态添加、识别、移除组件。每一个组件的意义在于描述实体的某一个特性。例如,PositionComponent(位置组件),其拥有x、y两个数据,用来描述实体的位置信息,拥有PositionComponent的实体便可以说在游戏世界中拥有了一席之地。当组件们单独存在的时候,实际上是没有什么意义的,但是当多个组件通过系统的方式组织在一起,才能发挥出真正的力量。同时,我们还可以用空组件(不含任何数据的组件)对实体进行标记,从而在运行时动态地识别它。如,EnemyComponent这个组件可以不含有任何数据,拥有该组件的实体被标记为“敌人”。
根据实际开发需求,这里还会存在一种特殊的组件,名为 Singleton Component (单例组件),顾名思义,单例组件在一个上下文中有且只有一个。具体在什么情况下使用下文系统一节中会提到。
样例:
  • PositionComponent(x, y)
  • VelocityComponent(X, y)
  • HealthComponent(value)
  • PlayerComponent()
  • EnemyComponent()
注:括号前为组件名,括号内为该组件拥有的数据
3. 系统
理解了实体和组件便会发现,至此还未曾提到过游戏逻辑相关的话题。系统便是ECS架构中用来处理游戏逻辑的部分。何为系统,一个系统就是对拥有一个或多个相同组件的实体集合进行操作的工具,它只有行为,没有状态,即不应该存放任何数据。举个例子,游戏中玩家要操作对应的角色进行移动,由上面两部分可知,角色是一个实体,其拥有位置和速度组件,那么怎么根据实体拥有的速度去刷新其位置呢,MoveSystem(移动系统)登场,它可以得到所有拥有位置和速度组件的实体集合,遍历这个集合,根据每一个实体拥有的速度值和物理引擎去计算该实体应该所处的位置,并刷新该实体位置组件的值,至此,完成了玩家操控的角色移动了。
注意,我强调了移动系统可以得到所有拥有位置和速度组件的实体集合,因为一个实体同时拥有位置和速度组件,我们便认为该实体拥有移动的能力,因此移动系统可以去刷新每一个符合要求的实体的位置。这样做的好处在于,当我们玩家操控的角色因为某种原因不能移动时,我们只需要将速度组件从该实体中移除,移动系统就得不到角色的引用了,同样的,如果我们希望游戏场景中的某一个物件动起来,只需要为其添加一个速度组件就万事大吉。
一个系统关心实体拥有哪些组件是由我们决定的,通过一些手段,我们可以在系统中很快地得到对应实体集合。
上文提到的 Singleton Component (单例组件) ,明白了系统的概念更容易说明,还是玩家操作角色的例子,该实体速度组件的值从何而来,一般情况下是根据玩家的操作输入去赋予对应的数值。这里就涉及到一个新组件InputComponent(输入组件)和一个新系统ChangePlayerVelocitySystem(改变玩家速度系统),改变玩家速度系统会根据输入组件的值去改变玩家速度,假设还有一个系统FireSystem(开火系统),它会根据玩家是否输入开火键进行开火操作,那么就有 2 个系统同时依赖输入组件,真实游戏情况可能比这还要复杂,有无数个系统都要依赖于输入组件,同时拥有输入组件的实体在游戏中仅仅需要有一个,每帧去刷新它的值就可以了,这时很容易让人想到单例模式(便捷地访问、只有一个引用),同样的,单例组件也是指整个游戏世界中有且只有一个实体拥有该组件,并且希望各系统能够便捷的访问到它,经过一些处理,在任何系统中都能通过类似world->GetSingletonInput()的方法来获得该组件引用。
系统这里比较麻烦,还存在一个常见问题:由于代码逻辑分布于各个系统中,各个系统之间为了解耦又不能互相访问,那么如果有多个系统希望运行同样的逻辑,该如何解决,总不能把代码复制 N 份,放到各个系统之中。UtilityFunction(实用函数) 便是用来解决这一问题的,它将被多个系统调用的方法单独提取出来,放到统一的地方,各个系统通过 UtilityFunction 调用想执行的方法,同系统一样, UtilityFunction 中不能存放状态,它应该是拥有各个方法的纯净集合。
样例:
  • MoveSystem(Position, Velocity)
  • RenderSystem(Position, Sprite)
注:括号前为系统名,括号内为该系统关心的组件集合
0x03 ECS架构实战
接下来终于到了实战环节,这里笔者使用 游戏引擎(5.6.3p4),配合现成的 Entitas 框架来实现一个小 Demo。由于 Unity3d 游戏引擎已经为我们提供了输入类和物理引擎,因此 Demo 中有部分内容可能与上文不太一致,主要以展示整体架构为主,请读者忽略这些细节。
1. Entitas介绍
Entitas is a super fast Entity Component System Framework (ECS) specifically made for C# and Unity. Internal caching and blazing fast component access makes it second to none. Several design decisions have been made to work optimal in a garbage collected environment and to go easy on the garbage collector. Entitas comes with an optional code generator which radically reduces the amount of code you have to write andmakes your code read like well written prose.
以上是 Entitas 官方介绍,简单来说该框架提供了代码生成器,只需要按照它的规范实现组件和系统,便可以一键生成我们需要的属性和方法,同时为了方便我们在系统中获得感兴趣的组件,它还提供了强大的分组、匹配功能。多说无益,直接开始实战吧。
2. 实战
下载Unity3d游戏引擎的步骤这里就省略了,我们先从 Github 上下载 Entitas,笔者这里使用的是Entitas 0.42.4 。下载好解压后,将其 CodeGenerator 和 Entitas 目录导入到一个新的 Unity 工程(这里一切从简,创建了一个空的 2D 项目),如下图所示。
2.jpg
接着,在工具栏找到 Tools -> Entitas ->Preference 对 Entitas 进行配置,由于这只是一个演示 ECS架构的小 Demo,就不对各种配置项进行解释了,对这些感兴趣的同学可以去官网查看文档,配置如下:
3.jpg
点击绿色按钮 Generate,如果没有任何报错,则配置没有问题。接下来就可以开始写代码了。
我们 Demo 的目标是控制一个矩形进行上下左右移动。由上文可知,我们至少需要 2 个组件:PositionComponent和VelocityComponent。在 Scripts/Components 目录下分别新建这两个脚本:
[AppleScript] 纯文本查看 复制代码
// PositionComponent.cs
using Entitas;
using UnityEngine;

public class PositionComponent : IComponent
{
    public Vector2 Value;
}
// VelocityComponent.cs
using Entitas;
using UnityEngine;

public class VelocityComponent : IComponent {
    public Vector2 Value;
}

由于在我们 Demo 中,玩家只能操控一个矩形,我们需要对其进行标记,告诉系统这个实体是玩家的代表,于是我们还要加上一个PlayerComponent来进行标记。
[AppleScript] 纯文本查看 复制代码
// PlayerComponent.cs
using Entitas;

public class PlayerComponent : IComponent { }

它不需要任何数据,仅仅用自身就可以实现标记的效果,拥有该组件的实体便是我们玩家控制的代表了。
实现完这 3 个组件后,我们需要利用 Entitas 框架提供的代码生成器,生成一下相应的代码,Tools -> Entitas -> Generate 或者快捷键control + shift + g。
4.jpg
没有看到任何报错,很好我们继续。
接着我们要实现ChangePlayerVelocitySystem,它每一帧都会运行,根据玩家是否输入w、a、s、d来改变矩形的速度。
[AppleScript] 纯文本查看 复制代码
// ChangePlayerVelocitySystem.cs
using Entitas;
using UnityEngine;

public class ChangePlayerVelocitySystem : IExecuteSystem
{
    // 每一帧都会执行
    public void Execute()
    {
        // 得到拥有 Player、Position、Velocity 组件的实体集合
        var playerCollection = Contexts.sharedInstance.game.GetGroup(
            GameMatcher.AllOf(
                GameMatcher.Player,
                GameMatcher.Position,
                GameMatcher.Velocity));

        var velocity = Vector2.zero;
        if (Input.GetKey(KeyCode.W))
        {
            velocity.y += 1;
        }

        if (Input.GetKey(KeyCode.S))
        {
            velocity.y -= 1;
        }

        if (Input.GetKey(KeyCode.A))
        {
            velocity.x -= 1;
        }

        if (Input.GetKey(KeyCode.D))
        {
            velocity.x += 1;
        }

        foreach (var player in playerCollection)
        {
            player.ReplaceVelocity(velocity);
        }
    }
}

这里实现了IExecuteSystem接口,每一帧其Execute方法都会执行。
至此,我们每一帧都会根据用户的输入去改变矩形的速度,还需要一个ChangePositionSystem,它会根据实体身上速度组件的值,去改变位置组件的值。
[AppleScript] 纯文本查看 复制代码
// ChangePositionSystem.cs
using System.Collections.Generic;
using Entitas;
using UnityEngine;

public class ChangePositionSystem : ReactiveSystem<GameEntity>
{
    public ChangePositionSystem(Contexts contexts) : base(contexts.game)
    {
    }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.AllOf(GameMatcher.Position, GameMatcher.Velocity));
    }

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasPosition && entity.hasVelocity;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (var entity in entities)
        {
            var velocity = entity.velocity.Value;
            var newPosition = entity.position.Value + velocity * Time.deltaTime;

            entity.ReplacePosition(newPosition);
        }
    }
}

这里我们用到了ReactiveSystem<GameEntity>基类,稍微讲解一下,它应该算是一种特殊的IExecuteSystem接口实现,它也会每一帧都执行,但它会帮助我们监听我们感兴趣的组件,只有当这些组件发生变化时,它的Execute方法才会被调用,GetTrigger和Filter两个方法相当于过滤器,具体就不细讲了,可以去官网查看一下文档。
由于使用了 Unity3d 游戏引擎,我们的框架需要由引擎来驱动,因此我们还要添加一个继承自MonoBehaviour的GameController脚本,在其中的Start方法里实例化各个系统,Update方法里调用Excute。
[AppleScript] 纯文本查看 复制代码
// GameController.cs
using UnityEngine;
using Entitas;

public class GameController : MonoBehaviour
{

    private Systems _systems;

    private void Start()
    {
        Contexts contexts = Contexts.sharedInstance;

        // 创建系统
        _systems = CreateSystems(contexts);

        // 创建我们的玩家实体
        var player = contexts.game.CreateEntity();
        // 为其添加相应的组件
        player.isPlayer = true;
        player.AddPosition(Vector2.zero);
        player.AddVelocity(Vector2.zero);

        // 初始化系统
        _systems.Initialize();
    }

    private void Update()
    {
        _systems.Execute();
        _systems.Cleanup();
    }

    private void OnDestroy()
    {
        _systems.TearDown();
    }

    private Systems CreateSystems(Contexts contexts)
    {
        // Feature 是 Entitas 框架提供的在 Editor 下进行调试的类
        return new Feature("Game")
            .Add(new ChangePlayerVelocitySystem())
            .Add(new ChangePositionSystem(contexts));
    }
}

在场景中新建一个名为“GameController”的空物体,将该脚本添加上去,运行游戏,在“Hierarchy”页签下就可以看到我们创建的系统和实体了,如下图:
5.jpg
当我们按下w、a、s、d时,可以看到左侧 Position 下面的数值和 Velocity 下面的数值都根据我们的输入产生了对应的变化,这说明功能实现的没有问题。
至此,虽然还没有图形显示在场景中,但一个可操控的 Demo 已经完成了。

为了节省篇幅,SpriteComponent(精灵组件)和RenderSystem(渲染系统),这里就不再展示了,完整项目可以在我的 Github 里查看。
0x04 后记
到此,整篇文章也进入了尾声,不知读者是否对 ECS 架构有了自己的理解,其实笔者也是最近这段时间才开始使用该架构编写一些小项目,还未在商业项目中使用过,因此有些地方的理解可能存在一定的偏差,欢迎大家讨论与指正,感谢大家的阅读。

知乎@杨睿涵

评分

参与人数 1鲜花 +5 收起 理由
seany321 + 5 赞一个!

查看全部评分

[发帖际遇]: 清风 发帖时在路边捡到 1 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

2初来乍到
101/150
排名
34094
昨日变化
18

0

主题

54

帖子

101

积分

Rank: 2Rank: 2

UID
102309
好友
1
蛮牛币
1
威望
0
注册时间
2015-5-22
在线时间
39 小时
最后登录
2018-11-27
发表于 2018-9-19 16:54:12 | 显示全部楼层
太棒了这代码写的

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1001/1500
排名
5508
昨日变化
1

0

主题

579

帖子

1001

积分

Rank: 6Rank: 6Rank: 6

UID
254705
好友
1
蛮牛币
1070
威望
0
注册时间
2017-11-16
在线时间
174 小时
最后登录
2018-12-14
发表于 2018-9-19 17:06:44 | 显示全部楼层
6666666666666666666666666666

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1372/1500
排名
2414
昨日变化
1

5

主题

427

帖子

1372

积分

Rank: 6Rank: 6Rank: 6

UID
54335
好友
2
蛮牛币
2039
威望
0
注册时间
2014-11-9
在线时间
370 小时
最后登录
2018-12-15
发表于 2018-9-19 18:13:26 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
900/1000
排名
3419
昨日变化
3

0

主题

212

帖子

900

积分

Rank: 5Rank: 5

UID
2623
好友
0
蛮牛币
1576
威望
0
注册时间
2013-8-26
在线时间
266 小时
最后登录
2018-12-17
发表于 2018-9-19 22:10:34 | 显示全部楼层
[发帖际遇]: zuoyamin 乐于助人,奖励 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

7日久生情
1589/5000
排名
1435
昨日变化
1

0

主题

468

帖子

1589

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
87577
好友
0
蛮牛币
5366
威望
0
注册时间
2015-3-31
在线时间
271 小时
最后登录
2018-12-16

锦衣玉食

发表于 2018-9-20 08:30:33 | 显示全部楼层
too good too strong!

回复 支持 反对

使用道具 举报

4四处流浪
413/500
排名
5920
昨日变化
25

0

主题

56

帖子

413

积分

Rank: 4

UID
288982
好友
0
蛮牛币
696
威望
0
注册时间
2018-7-8
在线时间
131 小时
最后登录
2018-12-17
发表于 2018-9-20 08:39:48 | 显示全部楼层
谢谢楼主分享

回复

使用道具 举报

7日久生情
2341/5000
排名
2508
昨日变化
6

1

主题

1490

帖子

2341

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
119154
好友
0
蛮牛币
2628
威望
0
注册时间
2015-8-21
在线时间
296 小时
最后登录
2018-12-16
发表于 2018-9-20 08:50:47 | 显示全部楼层
谢谢楼主大大。
[发帖际遇]: boy840102 乐于助人,奖励 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

5熟悉之中
651/1000
排名
3935
昨日变化

2

主题

43

帖子

651

积分

Rank: 5Rank: 5

UID
261068
好友
1
蛮牛币
1666
威望
0
注册时间
2017-12-26
在线时间
240 小时
最后登录
2018-12-17
发表于 2018-9-20 09:15:30 | 显示全部楼层
谢谢分享

回复

使用道具 举报

5熟悉之中
575/1000
排名
4944
昨日变化

1

主题

183

帖子

575

积分

Rank: 5Rank: 5

UID
245227
好友
0
蛮牛币
703
威望
0
注册时间
2017-9-21
在线时间
109 小时
最后登录
2018-12-17
发表于 2018-9-20 09:21:05 | 显示全部楼层
这种东西不封装起来的话用起来就像在写汇编一样坑

回复 支持 反对

使用道具 举报

4四处流浪
457/500
排名
5437
昨日变化
2

0

主题

82

帖子

457

积分

Rank: 4

UID
282652
好友
1
蛮牛币
2586
威望
0
注册时间
2018-5-24
在线时间
121 小时
最后登录
2018-12-17
发表于 2018-9-20 09:48:09 | 显示全部楼层
ECS架构?可以的

回复

使用道具 举报

5熟悉之中
530/1000
排名
4627
昨日变化
1

1

主题

40

帖子

530

积分

Rank: 5Rank: 5

UID
253412
好友
0
蛮牛币
642
威望
0
注册时间
2017-11-9
在线时间
183 小时
最后登录
2018-12-17
发表于 2018-9-20 10:03:58 | 显示全部楼层
谢谢分享!

回复

使用道具 举报

4四处流浪
344/500
排名
7944
昨日变化

0

主题

66

帖子

344

积分

Rank: 4

UID
253699
好友
1
蛮牛币
1149
威望
0
注册时间
2017-11-10
在线时间
126 小时
最后登录
2018-12-17
发表于 2018-9-20 10:52:38 | 显示全部楼层
666666666666666666666666666666

回复 支持 反对

使用道具 举报

排名
164
昨日变化

9

主题

409

帖子

3843

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
7264
好友
1
蛮牛币
16363
威望
0
注册时间
2013-11-6
在线时间
1189 小时
最后登录
2018-12-7

VIP社区QQ达人活力之星

QQ
发表于 2018-9-20 11:16:55 | 显示全部楼层
每天进步一点点  

回复 支持 反对

使用道具 举报

排名
164
昨日变化

9

主题

409

帖子

3843

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
7264
好友
1
蛮牛币
16363
威望
0
注册时间
2013-11-6
在线时间
1189 小时
最后登录
2018-12-7

VIP社区QQ达人活力之星

QQ
发表于 2018-9-20 11:17:06 | 显示全部楼层
每天进步一点点  每天进步一点点  

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表