在说ECS之前

我们先说说常用的OOP(面向对象编程)的不好。

1.二异性,例如如果要造成伤害,伤害逻辑是写在攻击者还是受击者上(我比较喜欢在攻击者处理伤害逻辑,受害者处理伤害造成的数据),亦或者是水陆空战机属于那个基类(多重继承的冲突),这就是二异性的问题,而且每个人的处理也不同,导致了后期的维护的困难 2.性能低,主要是OOP下数据十分的分散,函数的频繁跳转都会造成性能过多消耗在读写中

3.debug困难,因为数据都是在类里,对单独的数据测试会比较困难

因此DOP(面向数据编程)孕育而生

什么是ECS

ECS即 Entity-Component-System(实体-组件-系统) 的缩写。

Entiy:实体,游戏内的每一个基本单元都是一个实体,每个实体由一个或多个组件构成,其实极为类似于unity中的GameObject(以下缩写为go)go上挂载着很多component(unity或其他游戏引擎中的component不等于ecs中的component)例如位置(transfrom组件),渲染,脚本等,可以称之为组件的集合,在代码层上往往只是个ID。

Component:组件,包括了一个或多个数据(int,float这些),但是要注意它不同于unity中的component,组件中是不能处理数据的,是没有逻辑的纯数据的集合,例如我们可以定义一个组件为MoveComponent里面有移动方向,移动速度,移动时间等数据,但是不能处理移动,仅仅是个数据(通俗一点来说就是仅仅是一个结构体而不是类)。

System:系统,与组件相反,系统仅仅含有逻辑,不含有数据,系统是用来实现游戏逻辑的,比如说移动,就由MoveSystem来对MoveComponent和PositionComponent进行数据操作,并且一个系统只对一件事关系,移动系统就仅仅处理移动系统,系统之间也不能通信。

这样的ECS有什么好处

1.性能高,ECS是DOP的,是数据友好型能获得优越的性能,迎合现代CUP架构设计理念,但是展开讲比较困难,如果有兴趣了解可以观看GAMES104中面向数据编程部分。(但是实际有限,游戏对象越多提升越明显,但是性能瓶颈不一定都在这,比如说在渲染,但是P社的游戏应该非常适合)

2.低耦合,对象(实体)完全通过组合构建,系统内部高内聚,维护起来爽的飞起

3.极容易debug,毕竟数据是单独一块。

4.非常适合网络同步,毕竟数据都在单独一块,而且也方便回滚。

5.拓展性强。

ECS的弊端

正所谓,计算机里没有银弹,ECS也是有其弊端的。

1.反直觉,将数据和处理分开是非常反直觉的(其实还好),没有OOP那样直观。

2.一定要注意System的执行顺序,这部分还是比较麻烦的,多个系统对同一个数据处理可能会导致一些难以注意到的bug。

3.前期写起来很恶心,对于小型游戏完全没有必要去使用ECS,基本享受不到ECS的好处。

4.oop中一些容易实现的功能,ECS实现起来很麻烦,输入UI事件等部分我都觉得应该混用OOP

ECS简单教程

由于unity官方的DOTS进展缓慢这里我使用第三方的Entitas。

Entitas是干嘛的

entitas实际上就是帮你生成那些琐碎重复的代码,使得你可以简单方便的使用ECS框架来开发,接下来我将简单来介绍下如何使用。

安装

Home · sschmid/Entitas Wiki (github.com)

具体请百度

下载Entitas和Jenny两个包

其中Enitas里的内容放到Assets文件夹里,Jenny放到根目录里

然后在根目录执行以下代码

dotnet .\Jenny\Jenny.Generator.Cli.dll(命令行)

选择 Use Jenny,select all,Save and continue

然后都选第2个

构建Junney

使用以下代码

dotnet .\Jenny\Jenny.Generator.Cli.dll server

如果有问题(没法找到程序集) 修改jenny.properties(把该文件从Jenney 提到上一层)

Jenny.SearchPaths = Assets\Entitas\Entitas,
Jenny\Plugins\Entitas,
Jenny\Plugins\Jenny

对这一串 更改或添加 ()

基本架构

Untitled.png

在GameControl中是初始化数据和调用系统

在ObjectComponent 中编写所有和object有关的组件

在ObjectSystem中编写所有和object有关的系统

组件

Untitled.png

[Game]//是什么Context可以加上,Unique表示该context下只能有一个这个组件 所以可以用context.component来调用这个组件相当于单例模式
public sealed class NameComponent:IComponent{//继承组件接口 父类是Entitas提供的
	public Kind value;//字段 可以有多个 这里就是组件的数据 也可以为空 空组件相当于个tag/flag
	public Kind value;//字段 可以有多个......
}

context:上下文,是环境的意思,就比如我们做语文需要结合上下文来推断作者想说什么,程序也要通过context来推断你写的代码是什么意思 Entitas中默认的context有game input等 你可以只有添加

系统

本身是靠外部调用的,在哪里调用由你来定,按原来的unity的执行方法来调用,在后面我会讲。

系统合集

是很多系统的集合,因为系统会很多,用个系统的集合把一组的系统合集方便管理

public class InputSystems : Feature { //这是所有Input系统的合集 Feature是要继承的类 entitas提供
		public InputSystems(Contexts contexts) : base(nameof(InputSystems)) {//传入对应的contexts
		//添加Input的所有系统 注意调用顺序
			Add(new EmitInputSystem(contexts));//这里就是具体的系统 有4种
			Add(new MouseHoverRaycastSystem(contexts));
			Add(new MouseDownRaycastSystem(contexts));
			Add(new MouseUpSystem(contexts));
			Add(new CleanupClickedSystem(contexts));
		}
	}

这个系统合集是在GameControl中调用

系统的主要结构是:收集该系统感兴趣的实体,对实体的组件操作这两部分。

各个系统的区别是何时调用

执行系统

Execute systems run once per frame. They implement the interface IExecuteSystem, which defines the method Execute(). This is where you put code that needs to run every frame, similar to Unity’s Update() method.

//对指定的实体操作
public class JumpSystem : IExecuteSystem {
        readonly IGroup<GameEntity> entities;//要操作的实体的集合,是动态的

        public JumpSystem(Contexts contexts) {
            entities = contexts.game.GetGroup(GameMatcher.Jump);//在game的context下获取有Jump的实体的集合
        }

        public void Execute() {
            foreach (var entity in entities.GetEntities()) {//entities就是一个集合可以直接遍历,如果你要对entites增删的话就得用GetEntities拷贝一份,否则就会因为迭代器引用报错
                entity.gameObject.gameObject.GetComponent<Rigidbody>().velocity += new Vector3(0, 5, 0);//entity.gameObject.gameObject:获取这个实体下的gameObject组件中的gameObject数据
                entity.gameObject.gameObject.GetComponent<Rigidbody>().AddForce(entity.jump.force * Vector3.up);
                entity.RemoveJump();
            }
        }
    }

反应系统

//反应系统 监听其他系统对实体的某些操作(添加或删除组件) 
//Executes when the observed group changed
public class MouseUpSystem : ReactiveSystem<InputEntity> {//名字和context 父类依旧是由Entitas提供
		public MouseUpSystem(Contexts contexts) : base(contexts.input) { }//固定写法
		//设置触发器(监听) 设置后会一直监听 直到该系统被移除
		protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context){
			return context.CreateCollector(InputMatcher.MouseHeld.Removed());
			//上面代表在Input的上下文(game上下文的话就是GameMatcher)中的MouseHeld组件被移除时
			//收集这些移除了该组件的实体(准备调用系统)
		}
		//过滤 
		protected override bool Filter(InputEntity entity) {
			//检查收集到的entity是否符合条件 下列代码的条件是
			//是否有 MouseHeld 和 MouseHeldEntity这两组件(前者是空组件使用的直接判断是否为真即等于是否存在 同理给其赋值也是用真假)
			return !entity.isMouseHeld && entity.hasMouseHeldEntity;
		}
		//执行 对搜集到的实体进行操作 
		protected override void Execute(List<InputEntity> entities) {
			foreach (var entity in entities) {//收集到的实体
				var target = entity.mouseHeldEntity.Entity;
				if (target.hasDragging) target.RemoveDragging();
				else target.isClicked = true;
				entity.RemoveMouseHeldEntity();
			}
		}
	}

只以这两个重要的系统为例其他系统请参考

Systems · sschmid/Entitas Wiki (github.com)

调用系统

//在GameControl脚本中,你可以在任何地方来执行
void Awake() {
            contexts = Contexts.sharedInstance;//获取上下文
            systems = new Feature()
                .Add(new ViewSystems(contexts));//系统的集合可以链式添加 按添加顺序调用
            systems.Initialize();//调用systems中的系统集合中的初始化系统
        }

void Update() {
            systems.Execute();///调用systems中的系统集合中的执行系统
            systems.Cleanup();///调用systems中的系统集合中的销毁系统
        }

以上就是entitas的简单介绍了,主要是介绍了书写的格式,可以很方便的入门具体详细细节请阅读Home · sschmid/Entitas Wiki (github.com)

文章作者: 妖白与茶色
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 妖白与茶色的博客
喜欢就支持一下吧