Unity技术分享Unity3d进阶unity

Unity下一轮最大的变革-Entity Component S

2018-05-22  本文已影响79人  Angeladaddy
ECS+jobs实现的酷炫效果

新一代Entity Component System(ECS)将会彻底改变Unity的底层概念(GameObject-Component 系统)和现有工作方式。MonoBehavious、Update、GameObject....这些概念已经过时了!

1. 什么是ECS?

ECS,中文:实体-组件系统。并不是什么新鲜玩意,它是在游戏架构中广泛采用的一种架构。在游戏中,每个物体是一个Entity(实体),比如,敌人、子弹、车辆等。每个实体具有一个或多个组件,赋予这个实体不同的行为或功能。所以,一个实体的行为可以在游戏运行时通过加减组件进行改变。ECS经常和数据驱动模式一起使用。 wiki链接

从上面介绍可以看到,Unity中的GameObject好像扮演着实体的角色,但,它不够纯粹!

struct MyComponent: IComponentData
{} 

和旧的组件系统不同的是,IComponentData只存储数据,可随时添加到Entity或者移去。

2.为什么要引入 ECS

传统的 GameObject/MonoBehaviour 系统七宗罪:(深有体会)

Entity-component-system的出现,就是解决这些问题:

3. Hello world

说了半天,我到底怎么用ECS?
unity世界的helloworld莫过于沿着y轴旋转一个方块,先来看看我们的老朋友MonoBehavior :

using UnityEngine;

class Rotator : MonoBehaviour
{
    void Update()
    {
        transform.rotation *= Quaternion.AngleAxis(Time.deltaTime * speed, Vector3.up);
    }
}

多么熟悉的代码啊!伴随了我们将近10年的时间,一想到就要老去,竟然有些伤感...
伤感归伤感,但是上面的代码,一直就有巨大的问题,十几年来我们只不过视而不见罢了。
我们在unity中新建一个脚本,要挂在GameObject上,就必须继承MonoBehaviour,而MonoBehaviour 本身就继承自很多父类,父类中定义的很多字段、属性,在我们的小脚本中根本用不到,可还是不得不继承。白白的浪费了内存。

下面我们来试试ECS:

  1. 目前ECS尚处于开发阶段,我们需要以下前提条件才能开启:
    • Unity2018.1版本以上
    • Build Settings - Player Settings ,设置c# runtime:


      image.png
    • 打开项目目录下packages/ manifest.json 文件,加入以下内容:
{
    "dependencies": {
        "com.unity.entities":"0.0.11"
    },
    "testables": [
        "com.unity.collections",
        "com.unity.entities",
        "com.unity.jobs"
    ],
    "registry": "https://staging-packages.unity.com"
}
  1. 场景中建立一个cube,新建Rotator.cs脚本,并拖给cube:
public class Rotator : MonoBehaviour {
        //是的,没看错,只有数据
     public float speed;
}
  1. 新建RotatorSystem.cs脚本:
using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

/// <summary>
/// ComponentSystem管理游戏逻辑(类比以前的MonoBehavior)
/// 该类只有一个OnUpdate方法需要复写
/// </summary>
class RotatorSystem : ComponentSystem
{
    /// <summary>
    /// 简单的Group结构体,规定Entity必须包含哪些ComponentData
    /// </summary>
    struct Group
    {
        public Transform transform;
        public Rotator rotator;
    }
    protected override void OnUpdate()
    {
        //遍历场景中同时包含transform和Rotator的Entity,执行操作
        foreach (var item in GetEntities<Group>())
        {
            item.transform.rotation *= Quaternion.AngleAxis(item.rotator.speed * Time.deltaTime, Vector3.up);
        }
    }
}

这个脚本不用拖拽给任何场景中的物体,运行时它会自动遍历场景中符合条件的Entity。
但此时执行游戏,不会有任何变化,下一步,需要在cube上再挂一个GameObjectEntity组件,告诉ComponentSystem这是一个GameObject类型的实体。

ECS ships with the GameObjectEntity component. On OnEnable, the GameObjectEntity component creates an entity with all components on the GameObject. As a result the full GameObject and all its components are now iterable by ComponentSystems.

Thus for the time being you must add a GameObjectEntity component on each GameObject that you want to be visible / iterable from the ComponentSystem.

运行游戏,voila!cube开始旋转。

4. 好吧,看起来很炫,但这对我的游戏开发有什么意义?

仔细想想:切换到ECS,我们需要做的只是:从MonoBehavior中把逻辑剥离出来放到ComponentSystem的OnUpdate里。实际上,以上有关ECS的代码示例只是‘Hybird’模式,对于大量已经开发的工程,这是一种无痛解决方案。unity这次的变化太大了,所以必须要有这么一种过渡阶段。

那这样做有什么好处吗?

ok,那在Hybird模式下使用ECS有什么损失呢?

SIMD,Single instruction, multiple data,计算机在多核处理器上同时进行同种运算的能力。数据处理是并行的,但不是并发的。也就是,CPU单进程的并发计算。

5. 纯ECS解决方式:

欢迎进入Unity的未来:ECS+IComponentData+c# jobs

让我们再次转动这个cube:

  1. 场景中再建一个cube,写以下代码:
using System;
using Unity.Entities;

/// <summary>
/// 一个简单的结构体(ComponentData)
/// </summary>
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float value;
}

可以看到,和我们一开始说的一样,这就是一个继承自IComponentData的简单结构体,对应过去的Component,此结构体可以从Entity上增加、删除。
但此时直接拖拽给新的cube,提示不能添加:


image.png

...没继承MonoBehavior当然不能添加。
加入一行代码就可以了:

using System;
using Unity.Entities;

/// <summary>
/// 一个简单的结构体(ComponentData)
/// </summary>
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float value;
}

/// <summary>
/// 现阶段这个wrapper是为了能够把IComponentData添加给GameObject,
/// 将来会被移去
/// </summary>
public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { }

image.png

再写一个RotationSpeedSystem,此脚本不用赋给任何物体。

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;//c# jobs
using Unity.Mathematics;//新的命名空间
using Unity.Transforms;//新的命名空间
using UnityEngine;

public class RotationSpeedSystem : JobComponentSystem
{
    /// <summary>
    /// 使用IJobProcessComponentData遍历符合条件的所有Entity。
    /// 此过程是单进程的并行计算(SIMD)
    /// IJobProcessComponentData 是遍历entity的简便方法,并且也比IJobParallelFor更高效
    /// </summary>
    [ComputeJobOptimization]
    struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
    {
        /// <summary>
        /// deltaTime
        /// </summary>
        public float dt;
        /// <summary>
        /// 实现接口,在Excute中实现旋转
        /// </summary>
         public void Execute(ref Rotation rotation, ref RotationSpeed speed)
        {
            //读取speed,进行运算后,赋值给rotation
            rotation.Value = math.mul(math.normalize(rotation.Value), math.axisAngle(math.up(), speed.value * dt));
        }
    }

    /// <summary>
    /// 我们在这里,只需要声明我们将要用到那些job
    /// JobComponentSystem 携带以前定义的所有job
    /// 最后别忘了返回jobs,因为别的job system 可能还要用
    /// 完全独立于主进程,没有等待时间!
    /// </summary>
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotationSpeedRotation() { dt = Time.deltaTime };
        return job.Schedule(this, 64, inputDeps);
    }
}

cube 又转了。

本系列文章99.9%的内容来自于官方github文档,其余为原创

上一篇 下一篇

猜你喜欢

热点阅读