您当前的位置: 首页 > 

CloudHu1989

暂无认证

  • 5浏览

    0关注

    89博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

用ECS做HexMap:重构地图系统

CloudHu1989 发布时间:2019-08-27 14:08:20 ,浏览量:5

基于Unity2019最新ECS架构开发MMO游戏笔记20
    • 概述
    • 概念
      • 原型Archetypes
      • 内存块
      • 实体查询EntityQuery
      • 任务Jobs
      • 系统组织
    • 优化地图系统
      • 主世界
      • 六边形单元生成系统
      • 六边形单元系统
  • 更新计划
    • 作者的话
  • ECS系列目录
    • ECS官方示例1:ForEach
    • ECS官方案例2:IJobForEach
    • ECS官方案例3:IJobChunk
    • ECS官方案例4:SubScene
    • ECS官方案例5:SpawnFromMonoBehaviour
    • ECS官方案例6:SpawnFromEntity
    • ECS官方案例7:SpawnAndRemove
    • ECS进阶:FixedTimestepWorkaround
    • ECS进阶:Boids
    • ECS进阶:场景切换器
    • ECS进阶:MegaCity0
    • ECS进阶:MegaCity1
    • UnityMMO资源整合&服务器部署
    • UnityMMO选人流程
    • UnityMMO主世界
    • UnityMMO网络同步
    • 用ECS做HexMap:自动生成地图系统
    • 用ECS做HexMap:利用RenderMesh绘制六边形
    • 用ECS做HexMap:利用RenderMesh为六边形涂色
    • 用ECS做HexMap:六边形单元的颜色混合
    • 用ECS做HexMap:重构地图系统
    • 用ECS做HexMap:鼠标点击六边形单元涂色

概述

花了两天时间想要精简冗余的代码,结果都以失败告终,而且我很怀疑自己对ECS的理解和使用方式是否真的正确。很多地方虽然这样写了,但是却觉得有些本末倒置,究竟怎样做才是合理的设计? 抱着困惑,我决定沉下心来好好看看UnityECS官方文档,也许可以从中得到一些更深层次的理解。

ECS ECS全称”实体组件系统“(Entity Component System,简称ECS),是Unity面向数据堆栈技术(Data-Oriented Tech Stack,简称DOTS)的核心。ECS顾名思义由以下三部分组成:

  • 实体,一切构成你游戏世界的事物都是实体,相对于面向对象编程(Object Oriented Programming,简称OOP)中的对象(Object),在ECS之前,Unity中的一切都是OOP架构的,所以贯彻的是GameObject游戏对象这个概念,所有的代码都是围绕Object对象展开的,不仅如此,我所接触到的编程思想都秉持着一切皆是对象(Everything is Object)的原则来设计。夸张的说,一个程序员无论走到哪里,无论在做什么,他的大脑中始终无法释怀的就是对象。
  • 组件,和实体相关的数据就是组件,在OOP中也有数据,但是这些数据都是通过对象来调用的,例如你想知道玩家这个对象的名字/血量/力量/暴击等等,一切数据都是这个对象的属性,通过访问对象来获取,也可以通过对象的公共方法来影响这些数据,总而言之,对象和实体是关联的。而实体和数据则完全是解耦的,数据由组件自身来组织和管理。OOP也有MVC这样的设计模式来解决耦合问题,但是和ECS的设计思想又不一样,具体的区别后面再详细看吧。
  • 系统,System才是用来操作数据的,它以实体为索引,然后操作与其关联的所有数据。这么看来,我把六边形单元作为实体,用系统来操作其数据似乎并没有错。比较使我感到困惑的是,如果你的数据量比较大时,当涉及到数组/列表时,你无法在组件中进行定义,虽然有DynamicBuffer这种类似于数组/列表的定义,可是一点都不好用。
概念

ECS的架构分为索引(实体)、数据(组件)和行为(系统),这样的架构是聚焦于数据的。系统通过读取由实体索引的组件数据流,将数据从输入状态转换为输出状态。 下面的图表将表明三者是如何一起运作的: ECS架构 在上图中,系统读取了Translation(用来把OOP的信息转化成ECS的数据)和Rotation组件,将两者相乘,然后更新LocalToWorld组件,即公式:L2W=T*R。 实际上,实体A和B有Render组件,实体C却没有。这里吐槽一下,目前ECS封装了相对于OOP的MeshRender,它改了个名字叫RenderMesh,这是个共享组件,不能在Job中使用,我觉得用起来不爽。 实体上的组件可以作为系统刷选实体的依据,在Job执行的过程中,它可以指定组件。系统在查询实体时,也是通过组件来刷选的,就像图中所示,系统如果指定了Render组件,那么就只有AB受影响。

原型Archetypes

一句话解释就是组件的排列组合。 原型 组件的数量越多,你的实体原型将几何倍增。 这无疑可以大大扩展游戏实体的丰富性,就像杂交培育一样,DNA作为数据,将其拆分成很多个组件,通过排列组合,嘣,物种大爆炸。这个世界就是这样,猫和老虎,豹子和狮子,狗和狼,猩猩、猴子和人…… 原型就是组件的组合。

内存块

实体的原型决定了实体在内存中储存的位置。一块内存由一个原型块对象来代表,原型块中总是包含了相同原型的对象。 内存块不是动态扩容的,当一块内存满了,新的内存块会被分配来装新的同种原型。 如果你改变实体的组件构成,新增或删除组件,都会导致实体从一个内存块移动到对应实体原型的内存块。 内存块 这种一对多的设计机制有利于查询实体,实体的数量很多,但是原型的数量相对较少。 组件在内存块中是无序的,当某个原型的实体被新建时,它将被放到第一个有空间的对应原型内存块中。 存放方式也不是按照顺序来的,而是见缝插针的方式,以保持内存块的紧密性。

实体查询EntityQuery

你可以通过实体查询来帮助系统识别哪些实体是你想操作的,实体查询会搜索满足条件的实体,还可以根据需要使用下列条件:

  • All,实体必须满足所有你需要的组件。
  • Any,只要有任何一个满足条件都可以。
  • None,一个都不包含。
任务Jobs

任务系统就是用来利用多线程的,这个可以这样理解,当你在跑步的时候,也可以同时听音乐;当我想写一部小说的时候,我先不动笔,而是尽情去玩耍,等玩累了,小说的情节也有了,其实我们的大脑也是多线程的。 详细怎么使用后面有代码示例,只要知道这个概念即可。

系统组织

我大概画了一下系统的组织架构,如下所示: 系统组织 后面的内容我就不翻译了,毕竟现在只是半成品,等时机成熟了,官方更新正式版了再研究研究。 看完官方文档后,我觉得自己的理解有些问题,于是开始重构代码。

优化地图系统

HexMap架构 这是我简单绘制的架构图,服务器端暂时还没有做,客户端分为ECS世界和OOP世界,因为ECS还没有物理引擎支撑,所以现在只能进行混合开发。也有大神做Pure ECS的纯粹开发,我大概看了下物理的写法,很复杂,需要很多运算。我物理不是很好,数学也很勉强,还是等官方出ECS的物理组件吧。

主世界

于是我把地图的渲染权还给OOP,地图的计算仍然交给ECS来做,详细的还是看代码吧:

/// 
/// 主世界
/// 
public class MainWorld : MonoBehaviour
{
    /// 
    /// 地图材质
    /// 
    //public Material material;

    /// 
    /// 地图宽度(以六边形为基本单位)
    /// 
    [SerializeField]
    private int MapWidth = 6;

    /// 
    /// 地图长度(以六边形为基本单位)
    /// 
    [SerializeField] private int MapHeight = 6;

    /// 
    /// 地图颜色
    /// 
    [SerializeField] private Color defaultColor = Color.white;

    //单例模式
    public static MainWorld Instance = null;
    private World m_HexMapWorld;
    private CellSpawnSystem m_CellSpawnSystem;
    private EntityManager m_EntityManager;
    //private Entity m_Mesh;
    private Entity m_Builder;
    private Mesh m_Mesh;
    private MeshCollider m_MeshCollider;

    #region Mono

    //Make this single
    private void Awake()
    {
        Instance = this;
        //初始化
        Initialize();
    }

    // Update is called once per frame
    void Update()
    {
        m_CellSpawnSystem.Update();
    }

    #endregion

    #region Init初始化
    /// 
    /// 构造函数
    /// 
    private MainWorld() { }

    /// 
    /// 初始化
    /// 
    private void Initialize()
    {
        //0.get the active world or new one
        m_HexMapWorld = World.Active != null ? World.Active : new World("HexMap");
        //1.get the entity Manager
        m_EntityManager = m_HexMapWorld.EntityManager;
        //2.Create Builder Entity;
        EntityArchetype builderArchetype = m_EntityManager.CreateArchetype(typeof(Data),typeof(OnCreateTag));
        m_Builder = m_EntityManager.CreateEntity(builderArchetype);
        //3.Setup Map;  Todo:get map data from server and SetupMap,now we just use default data
        SetupMap(MapWidth, MapHeight, defaultColor);

        //4.Create Mesh entity for map and setup RenderMesh
        GetComponent().mesh = m_Mesh = new Mesh();
        m_Mesh.name = "Hex Mesh";
        m_MeshCollider=gameObject.AddComponent();
        //5.Create System to spawn cells
        m_CellSpawnSystem = m_HexMapWorld.CreateSystem();
    }

    #endregion

    #region Public Function公共方法

    /// 
    /// 渲染地图
    /// 
    public void RenderMesh()
    {

        //暴力获取所有实体,如果有系统外的实体就糟糕了,Todo:只获取Cell单元实体
        NativeArray entities = m_EntityManager.GetAllEntities();
        if (entities.Length             
关注
打赏
1664096582
查看更多评论
0.0436s