ECS
在说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
对这一串 更改或添加 ()
基本架构
在GameControl中是初始化数据和调用系统
在ObjectComponent 中编写所有和object有关的组件
在ObjectSystem中编写所有和object有关的系统
组件
[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)