ECS框架的初步探究

  欢迎参与讨论,转载请注明出处。
  本文转载自:https://musoucrow.github.io/2018/03/19/ecs_first/

前言

  在阅读这篇文章之前,你需要了解一下何为ECS框架。关于ECS框架,其实近年来一直想去尝试,终于在近日有所体悟,遂有此文。

详解

  ECS框架的存在实际上很早就出现了(我记得最初在2003年),近年随着《守望先锋》架构设计与网络同步一文出现后瞬间成了炙手可热的新星。
  ECS框架与帧同步锁定类似,皆只是拥有一个概念,但无确切的实现标准。但事实上已经不少现成的实现(如Entitas),不过我觉得Entitas在与Unity的结合上不符合我的审美,于是自己动手造了个轮子。
  ECS框架的概念其实相当直观:Entity-Component-System三件套。
* Entity即实体,作为Component的经纪人,可拥有多个Component。
* Component即组件,作为数据存储的容器,原则上只包含内部数据自处理的函数。Component以Entity作为标识,以此判断所属。
* System即系统,作为业务函数的集合,会与Component对接实现业务运行(System处理Component)。

  以上三点可谓看过相关文章的都懂,只是落实到具体实现上仍会有不少不明不白之处(Entity是作为容器还是标识符?Component可否嵌套Component?System之间可否相互调用?)。以上问题并没有确切的答案,只能是落实实现时根据需求而定。

实现

  所谓实践出真知,在此之前我写了个贪吃蛇,这是个不错的素材,于是便将其ECS化。这下也可将两者进行对比,品味其中区别。

Entity

  由于这款游戏是使用Unity制作的,那么自然最好与Unity本身相结合。我首先考虑到的便是与Unity本身的GameObject-Behavior(其实是Component,为防误解,特此改称)框架结合(业务环境下有调用它们的需求),于是选择将Entity做成一个Behavior:

using System;
using UnityEngine;

namespace Game.Core {
    public class Entity : MonoBehaviour {
        public static event Action<Entity> NewTickEvent;
        public static event Action<Entity> DestroyTickEvent;

        protected void Start() {
            if (Entity.NewTickEvent != null) {
                Entity.NewTickEvent(this);
            }
        }

        protected void OnDestroy() {
            if (Entity.DestroyTickEvent != null) {
                Entity.DestroyTickEvent(this);
            }
        }
    }
}

  可以看出,Entity的生命周期也与GameObject进行了捆绑,并且设置了两个event令System可以进行监控。
  再来看看Entity的具体实例:

using UnityEngine;

namespace Game.Entitys {
    using Core;
    using Components;

    public class Food : Entity {
        public Position position = new Position();

        protected void Awake() {
            this.position.Init(this);
        }

        protected new void OnDestroy() {
            base.OnDestroy();

            this.position.Destroy();
        }
    }
}

food

  可以看出Food实体创建了一个Position组件,托Unity编辑器的服,我们可以清晰地看到Position的数据构成,并可方便地进行编辑(包括运行时)。当然可以看得出这里Component的创建方式相当别扭(实例化后仍需Init),这是为了对接Unity的序列化功能,若不这么做的话,某些数据将会序列化失败(如Collision Slot)。

Component

  Component的初始实现便很简单了,只需要对接Entity以及预留Init与Destroy接口即可:

using System;

namespace Game.Core {
    [Serializable]
    public class Component {
        [NonSerialized]
        public Entity entity;

        public virtual void Init(Entity entity) {
            this.entity = entity;
        }

        public virtual void Destroy() {}
    }
}

  这里令Component拥有entity是为了便于识别身份,[Serializable]标识表示该对象可序列化(与编辑器交互),[NonSerialized]标识表示不让该变量序列化(没有显示在编辑器的需求)。接下来看看Position组件的具体实现:

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Game.Components {
    using Core;
    using Solts;

    public class Position : Component {
        public static Dictionary<Entity, Position> Map = new Dictionary<Entity, Position>();
        public static List<Position> List = new List<Position>();

        public Vector2Int value;
        public Collision collsionSlot;

        public override void Init(Entity entity) {
            base.Init(entity);

            Position.Map.Add(entity, this);
            Position.List.Add(this);
        }

        public override void Destroy() {
            Position.Map.Remove(this.entity);
            Position.List.Remove(this);
        }
    }
}

  关于ECS框架有一个很普遍的问题:在System要如何获取到Component?我的解决方法便是为有获取需求的Component设立存储容器,当然这种写法有点死板,应该专门设立容器管理类进行自动化处理,这是个可改善的方向。

System

  System纯粹来看便是个函数集,在Entitas的实现是专门设立Behavior装载System以运行。而我选择分离:System即Behavior,两者倒没什么根本上的区别,全凭个人喜好罢了。在以Behavior的实现下并不需要System基类,以下以涉及到坐标与碰撞的Field系统为例:

using System.Collections.Generic;
using UnityEngine;

namespace Game.Systems {
    using Core;
    using Components;
    using Entitys;

    public class Field : MonoBehaviour {
        public const float SIZE = 0.32f;

        private static List<Entity> SyncList = new List<Entity>();

        public static Vector2 ToPosition(int x, int y) {
            return new Vector2(x * SIZE + SIZE * 0.5f, y * SIZE + SIZE * 0.5f);
        }

        public static void AdjustPosition(Position position, Transform transform=null) {
            transform = transform == null ? position.entity.transform : transform;
            transform.position = Field.ToPosition(position.value.x, position.value.y);
        }

        private static void Collide(Position a, Position b) {
            if (a.value == b.value) {
                if (a.collsionSlot != null) {
                    a.collsionSlot.Run(a.entity, b.entity);
                }

                if (b.collsionSlot != null) {
                    b.collsionSlot.Run(b.entity, a.entity);
                }
            }
        }

        private static void Sync(Position position, Joint joint) {
            joint.laterPos = position.value;
        }

        protected void Awake() {
            Entity.NewTickEvent += this.NewTick;
            Entity.DestroyTickEvent += this.DestroyTick;
            Director.UpdateTickEvent += this.UpdateTick;
        }

        private void NewTick(Entity entity) {
            bool hasPos = Position.Map.ContainsKey(entity);
            bool hasJoi = Joint.Map.ContainsKey(entity);

            if (hasPos) {
                Field.AdjustPosition(Position.Map[entity]);
            }

            if (hasPos && hasJoi) {
                Field.SyncList.Add(entity);
            }
        }

        private void UpdateTick() {
            for (int i = 0; i < Position.List.Count; i++) {
                for (int j = i + 1; j < Position.List.Count; j++) {
                    Field.Collide(Position.List[i], Position.List[j]);
                }
            }

            foreach (var entity in Field.SyncList) {
                Field.Sync(Position.Map[entity], Joint.Map[entity]);
            }
        }

        private void DestroyTick(Entity entity) {
            if (Field.SyncList.Contains(entity)) {
                Field.SyncList.Remove(entity);
            }
        }
    }
}

  可以看出,继承Behavior的System可以很方便地使用自带的各种回调函数(如Awake),业务函数也变得清晰无比,只需要提供相应Component即可(如AdjustPosition)。对于一些需要复合组件的业务(如Sync),则会专门设立容器(SyncList)进行存储,对Entity的NewTickEvent与DestroyTickEvent进行监控便可筛选出合适的对象,且所有组件可通过Entity从组件容器进行获取,十分方便。
  当然也不要忘记与编辑器结合的优势,System也可以将变量序列化与编辑器交互:
system
  当然Unity可进行序列化的部分只有实例变量,所以需要作此处理:

public class Test : MonoBehaviour {
    private static Test Instance;

    public static int Get() {
        return Instance.value;
    }

    public int value;

    protected void Awake() {
        Food.Instance = this;
    }
}

  因为System是单例Behavior,所以这么做是安全的。如此便可操作实例对象了。

后记

  总的而言,ECS框架主要是一种对OOP思想的反思,甚至可以说是一种复古(函数式编程风格)。也是一种彻底的组件模式实现,彻底地奉行数据-逻辑分离。它使得我们更容易地去抽象、描述游戏事物。当然我认为它在某种程度上是反直觉的、抽象的(某些只会属于某个对象所属的业务却要分开写,并且用组件去涵盖)。所以我认为它更适用于某些场景下,如动作游戏里的地图单位,分为多种样式(物件、道具、战斗单位、NPC、飞行道具等),这种时候使用传统的继承+子对象写法确实不如ECS来得好了。再比如UI方面,我认为还是MVC框架更为王道。所以切忌教条主义,一切跟着实际需求走。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
JavaScript ECS框架是一种用于游戏开发的框架,它基于实体-组件-系统的设计模式。在这种框架中,游戏对象被表示为实体,每个实体由一组组件组成,而系统则负责处理这些实体和组件之间的交互。 在给出的引用中,可以看到一个简单的JavaScript ECS框架的实现。在这个框架中,主要的文件结构包括游戏逻辑入口文件(main.js)、角色系统(character.js)、碰撞系统(collision.js)、衰减系统(decay.js)、敌人系统(enemy.js)、渲染系统(render.js)、四叉树划线辅助系统(sketch.js)、时间系统(time.js)、用户输入系统(userInput)等。这些系统负责处理不同的游戏逻辑,例如角色的移动、碰撞检测、渲染等。 在加载框架时,首先通过异步加载main.js文件,并获取到canvas和主角图片等对象。然后,加载完毕后调用gameStart函数来启动ECS框架。gameStart函数接受canvas、dom和img作为参数,用于初始化ECS框架ECS框架的核心思想是将游戏对象分解为实体和组件,通过系统来处理它们之间的交互。实体是游戏对象的容器,组件是实体的属性或行为。系统则负责处理特定类型的组件,例如渲染系统负责处理渲染相关的组件。 通过使用ECS框架,开发者可以更好地组织和管理游戏逻辑,提高代码的可维护性和可扩展性。同时,ECS框架也能够提供更好的性能,因为它可以更有效地处理游戏对象之间的交互。 总结起来,JavaScript ECS框架是一种用于游戏开发的框架,基于实体-组件-系统的设计模式,通过将游戏对象分解为实体和组件,并通过系统来处理它们之间的交互,提供了更好的代码组织和管理方式,以及更好的性能。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值