【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2436

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 2300|回复: 14

[士郎] [实战]Unity 实体组件系统(ECS)——性能测试

[复制链接]  [移动端链接]
排名
1
昨日变化

7406

主题

7948

帖子

3万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
9247
威望
30
注册时间
2013-7-29
在线时间
3781 小时
最后登录
2019-3-19

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-12-7 15:08:52 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
趁着Unity前几天更新了Unity ECS,正好把之前的坑填上。


1.jpg



在上一片文章我们动手体验了一下Unity ECS(Entities),尝试了一些基础操作。


这次我们尝试用Job System优化我们的ECS实现,我们会把精力放在Job System之上,然后测试它在游戏帧数这种比较直观的参数上能拉开传统的Monobehaviour多大的差距。


如果不熟悉Unity ECS,强烈推荐瞄一眼上一篇文章,熟悉ECS的核心概念与基础用法(点击此处

1.1.gif
我们打算做一个测试小游戏——疯狂吃豆人,游戏效果如下图所示:


我们先用Monobehavior(后文简称Mono)快速实现该游戏,然后再用Unity ECS尝试优化,对比两者后再看看会发生什么。相信大家都熟悉Mono的使用,我们需要实现以下几个功能:




1.玩家,敌人移动(不使用Unity的物理系统而是直接修改Transform)
2.玩家碰撞检测(可以用一个简单的碰撞算法实现)
3.敌人随机生成与销毁(需要一个单独的关卡系统控制敌人生成的位置与生成速度)
首先我们创建两个Sphere放在场景中,然后创建两个新的Material,修改一下Shader改为Unlit/Color实现球体的扁平化风格,然后再设置一下颜色(玩家设置为橘色,敌人设置为蓝色)。




为了区分玩家敌人,我们把橘色的材质挂载到玩家上,蓝色的材质挂载到敌人上。
由于不使用Unity物理系统,所以我们移除掉两个球体上的Sphere Collider
记得设置一下他们的Tag,分别为PlayerEnemy,别忘了把玩家跟敌人做成预制体
接着修改一下相机的Position为0, 0, -10。

2.jpg


设置Size调整屏幕的显示大小,在Projection中把透视改为正交。
创建Canvsa并且添加上敌人数量(EnemyCountText)这个Text:


3.jpg

字体设置就是这样准备工作完成后画面看上去是这样的:


4.png


程序员审美,简单颜色跟纯黑背景

5.jpg


搭建好场景后我们先从创建一个Player类开始,并挂载到玩家身上来实现玩家的移动。


[AppleScript] 纯文本查看 复制代码
using UnityEngine;
public class Player : MonoBehaviour
{
    public bool Dead;
    private float speed;

    void Start() => speed = 5;

    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");
        Vector3 vector = new Vector3(x, y, 0).normalized * speed * Time.deltaTime;
        transform.position += vector;
    }
}


代码是不是异常简单,估计初学三天的同学也可以轻松实现角色的移动,但看到后面,嘿嘿嘿嘿。
还有一个更简单的摄像机跟随脚本:


[AppleScript] 纯文本查看 复制代码
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
    private GameObject player;

    void Start() => player = GameObject.FindWithTag("Player");

    void Update() => transform.position = new Vector3(player.transform.position.x, 
        player.transform.position.y, gameObject.transform.position.z);
}


接着创建一个Enemy脚本挂载在敌人预制体上,提供一个供敌人生成器调用的接口,并且利用一个求两个圆相交或相切的算法实现碰撞。


[AppleScript] 纯文本查看 复制代码
using UnityEngine;
public class Enemy : MonoBehaviour
{
    private EnemySpawn spawn;
    private float speed;
    private Player player;
    private float radius;
    private float playerRadius;

    //预留接口
    public void Init(EnemySpawn spawn, float speed, Player player)
    {
        this.spawn = spawn;
        this.speed = speed;
        this.player = player;
    }

    void Start()
    {
        Renderer renderer = GetComponent<Renderer>();
        Renderer playerRenderer = player.GetComponent<Renderer>();
        radius = renderer.bounds.size.x / 2;
        playerRadius = playerRenderer.bounds.size.x / 2;
    }

    void Update()
    {
        //敌人寻路
        Vector3 vector = (player.transform.position - transform.position).normalized *
            Time.deltaTime * speed;
        transform.position += vector;
        //碰撞检测
        float distance = (player.transform.position - transform.position).magnitude;
        if (distance < radius + playerRadius && !player.Dead)
        {
            Destroy(gameObject);
            spawn.EnemyCount--;
        }
    }
}


Player跟Enemy脚本都已经实现,还需要一个创建Enemy的脚本EnemySpawn放在场景中当作一个计时器,每隔一定时间就在玩家身边创建一堆敌人。


[AppleScript] 纯文本查看 复制代码
using UnityEngine;
public class EnemySpawn : MonoBehaviour
{
    [HideInInspector]
    public int EnemyCount;
    [SerializeField]
    private GameObject enemyPrefab;
    private Player player;
    private float cooldown;

    void Start() => player = GameObject.FindWithTag("Player").GetComponent<Player>();

    void Update()
    {
        if (player.Dead)
            return;
        cooldown += Time.deltaTime;
        if (cooldown >= 0.1f)
        {
            cooldown = 0f;
            Spawn();
        }
    }

    void Spawn()
    {
        Vector3 playerPos = player.transform.position;
        for (int i = 0; i < 50; i++)
        {
            GameObject enemy = Instantiate(enemyPrefab);
            EnemyCount++;

            int angle = Random.Range(1, 360);        //在玩家什么角度刷出来(1-359)
            float distance = Random.Range(15f, 25f); //距离玩家多远刷出来
            //角度与距离确定好之后算一下Enemy的初始坐标
            float y = Mathf.Sin(angle) * distance;
            float x = y / Mathf.Tan(angle);

            enemy.transform.position = new Vector3(playerPos.x + x, playerPos.y + y, 0);
            Enemy enemyScript = enemy.AddComponent<Enemy>();
            enemyScript.Init(this, 2.5f, player);
        }
    }
}


设置场景中的EnemySpawn:


6.jpg

把敌人的预制体放上去
最后把控制的UI脚本加上挂载到场景中就大功告成了。


[AppleScript] 纯文本查看 复制代码
using UnityEngine;
using UnityEngine.UI;
public class UI : MonoBehaviour
{
    private Text enemyCountText;
    private EnemySpawn enemySpawn;

    void Start()
    {
        enemyCountText = GameObject.Find("EnemyCountText").GetComponent<Text>();
        enemySpawn = GameObject.Find("EnemySpawn").GetComponent<EnemySpawn>();
    }

    void Update() => enemyCountText.text = "敌人数量:" + enemySpawn.EnemyCount;
}


我们利用Monobehavior轻车熟路地实现了我们的游戏。

运行游戏:


1.2.gif


以为这就结束了吗?



下面的才是重点加难点。


我们使用Entities实现同样的功能,最后进行性能上的比较。
在开始前确保安装上了Entities,在菜单栏Window->Package Manager->All可以找到,如果网络出现问题可以反复尝试几次。
首先我们需要想清楚ComponentSystem的关系再开始编写代码。




我们的游戏有三个关键的实体:玩家,敌人,摄像机




这里画一张图方便大家理解,从上往下依次是:实体,系统,组件,系统。他们的关系通过连线一目了然。


7.jpg



看上去有点复杂,总的思想就是不同系统需要关注不同的组件并进行相应的操作。
提高性能的关键在于脚本的并行,我们看看那些系统应该实现并行:EnemyCollisionSystem,EnemyMoveSystem,这两个系统因为是关键系统并且不存在逻辑与引用的依赖所以可以实现并行。




首先我们创建一个新的场景,Camera的设置需要从第一个场景中Copy过来使用。


然后我们再场景中创建一个空物体代表Player(Tag选择Player),并且挂上一个组件:




我们在Mesh一栏中选择Sphere圆球,然后选择之前创建的对应的材质。


Enemy也是一样,只需要在Material一栏中选择不同的材质就好了。


UI也可以从之前的场景中复制过来:


8.jpg


第一步,我们照着图来编写好我们的组件,首先创建一个名为Bootstrap的脚本,为了方便起见我们就把所有的类都放在一个文件中进行管理:


[AppleScript] 纯文本查看 复制代码
namespace MultiThread
{
    using UnityEngine;
    using UnityEngine.UI;
    using Unity.Entities;
    using Unity.Jobs;
    using Unity.Burst;
    using Unity.Rendering;
    using Unity.Transforms;
    using Unity.Mathematics;
    using Unity.Collections;
    using Random = UnityEngine.Random;

    public struct PlayerInput : IComponentData
    {
        public float3 Vector;
    }

    public struct EnemyComponent : IComponentData
    {
    }

    public struct CameraComponent : IComponentData
    {
    }

    public struct Health : IComponentData
    {
        public int Value;
    }

    public struct Velocity : IComponentData
    {
        public float Value;
    }
}



值得注意的是Unity ECS里面有一个bug导致不能在结构中声明bool类型。
以上的组件属于自定义组件,除此之外还有三个Unity提供的组件:


Position,MeshInstanceRenderer,Transform




然后我们创建一个Bootstrap类,在其中创建一个能用被Unity自动调用的方法:


[AppleScript] 纯文本查看 复制代码
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
public static void Start()
{
}


再Start方法中创建EntityManager开始,并进行初始化操作。


[AppleScript] 纯文本查看 复制代码
EntityManager manager = World.Active.GetOrCreateManager<EntityManager>();

GameObject player = GameObject.FindWithTag("Player");
GameObject enemy = GameObject.FindWithTag("Enemy");
GameObject camera = GameObject.FindWithTag("MainCamera");
Text enemyCount = GameObject.Find("EnemyCountText").GetComponent<Text>();

//获取Player MeshInstanceRenderer
MeshInstanceRenderer playerRenderer = player.GetComponent<MeshInstanceRendererComponent>().Value;
Object.Destroy(player);
//获取Enemy MeshInstanceRenderer
MeshInstanceRenderer enemyRenderer = enemy.GetComponent<MeshInstanceRendererComponent>().Value;
Object.Destroy(enemy);
//初始化玩家实体
Entity entity = manager.CreateEntity();
manager.AddComponentData(entity, new PlayerInput { });
manager.AddComponentData(entity, new Position { Value = new float3(0, 0, 0) });
manager.AddComponentData(entity, new Velocity { Value = 7 });
manager.AddSharedComponentData(entity, playerRenderer);
//初始化摄像机实体
GameObjectEntity gameObjectEntity = camera.AddComponent<GameObjectEntity>();
manager.AddComponentData(gameObjectEntity.Entity, new CameraComponent());


上面代码比较简单不做过多讲解。


创建第一个系统PlayerInputSystem,照着上面的设计图纸,关注相应的组件并进行操作就好了。


[AppleScript] 纯文本查看 复制代码
public class PlayerInputSystem : ComponentSystem
{
    struct Player
    {
        public readonly int Length;
        public ComponentDataArray<PlayerInput> playerInput;
    }

    [Inject] Player player; //加上这个标签,Unity会自动注入我们声明的结构中的属性

    protected override void OnUpdate()
    {
        for (int i = 0; i < player.Length; i++)
        {
            float3 normalized = new float3();
            float x = Input.GetAxisRaw("Horizontal");
            float y = Input.GetAxisRaw("Vertical");
            if (x != 0 || y != 0) //注意:直接归一化0向量会导致bug
                normalized = math.normalize(new float3(x, y, 0));
            player.playerInput = new PlayerInput { Vector = normalized };
        }
    }
}


以上的操作在上一篇ECS文章中有提及。

PlayerMoveSystem应该关注相应的组件并且进行相应的操作:


[AppleScript] 纯文本查看 复制代码
public class PlayerMoveSystem : ComponentSystem
{
    struct Player
    {
        public readonly int Length;
        public ComponentDataArray<Position> positions;
        public ComponentDataArray<PlayerInput> playerInput;
        public ComponentDataArray<Velocity> velocities;
    }

    [Inject] Player player;

    protected override void OnUpdate()
    {
        float deltaTime = Time.deltaTime;
        for (int i = 0; i < player.Length; i++)
        {
            //Read
            Position position = player.positions;
            PlayerInput input = player.playerInput;
            Velocity velocity = player.velocities;

            position.Value += new float3(input.Vector * velocity.Value * deltaTime);
            //Write
            player.positions = position;
        }
    }
}


现在我们就已经可以控制我们的玩家小球移动了,趁热打铁继续深入。


CameraMoveSystem与上面的系统在实现上不会有太大差别。


[AppleScript] 纯文本查看 复制代码
[UpdateAfter(typeof(PlayerMoveSystem))] //存在依赖关系, 我们控制该系统的更新在PlayerMoveSystem之后
public class CameraMoveSystem : ComponentSystem
{
    struct Player
    {
        public readonly int Length;
        public ComponentDataArray<PlayerInput> playerInputs;
        public ComponentDataArray<Position> positions;
    }
    struct Cam
    {
        public ComponentDataArray<CameraComponent> cameras;
        public ComponentArray<Transform> transforms;
    }
    [Inject] Player player;
    [Inject] Cam cam;

    protected override void OnUpdate()
    {
        if (player.Length == 0) //玩家死亡
            return;
        float3 pos = player.positions[0].Value;
        //相机跟随
        cam.transforms[0].position = new Vector3(pos.x, pos.y, cam.transforms[0].position.z);
    }
}


UI系统还算比较好理解的,不做阐述细节了:


[AppleScript] 纯文本查看 复制代码
[AlwaysUpdateSystem] //持续更新系统
public class UISystem : ComponentSystem
{
    Text enemyCount;

    public void Init(Text enemyCount) => this.enemyCount = enemyCount;

    struct Player
    {
        public readonly int Length;
        public ComponentDataArray<PlayerInput> playerInputs;
    }
    struct Enemy
    {
        public readonly int Length;
        public ComponentDataArray<EnemyComponent> enemies;
    }
    [Inject] Player player;
    [Inject] Enemy enemy;

    protected override void OnUpdate()
    {
        if (player.Length == 0) //玩家死亡
            return;

        enemyCount.text = "敌人数量:" + enemy.Length;
    }
}


敌人生成系统中使用了一个生成的小算法,以玩家为原点在圆球的周长上随机一个点生成敌人


9.jpg


[AppleScript] 纯文本查看 复制代码
public class EnemySpawnSystem : ComponentSystem
{
    EntityManager manager;
    MeshInstanceRenderer enemyLook;
    float timer;

    public void Init(EntityManager manager, MeshInstanceRenderer enemyLook)
    {
        this.manager = manager;
        this.enemyLook = enemyLook;
        timer = 0;
    }

    struct Player
    {
        public readonly int Length;
        public ComponentDataArray<PlayerInput> playerInputs;
        public ComponentDataArray<Position> positions;
    }

    [Inject] Player player;

    protected override void OnUpdate()
    {
        timer += Time.deltaTime;
        if (timer >= 0.1f)
        {
            timer = 0;
            CreatEnemy();
        }
    }

    void CreatEnemy()
    {
        if (player.Length == 0) //玩家死亡
            return;
        float3 playerPos = player.positions[0].Value;

        for (int i = 0; i < 50; i++)
        {
            Entity entity = manager.CreateEntity();

            int angle = Random.Range(1, 360);        //在玩家什么角度刷出来
            float distance = Random.Range(15f, 25f); //距离玩家多远刷出来
            //计算该点的x, y分量
            float y = Mathf.Sin(angle) * distance;
            float x = y / Mathf.Tan(angle);
            float3 positon = new float3(playerPos.x + x, playerPos.y + y, 0);
            //初始化敌人及属性
            manager.AddComponentData(entity, new EnemyComponent { });
            manager.AddComponentData(entity, new Health { Value = 1 });
            manager.AddComponentData(entity, new Position { Value = positon });
            manager.AddComponentData(entity, new Velocity { Value = 1 });
            manager.AddSharedComponentData(entity, enemyLook);
        }
    }
}


到这里我们已经实现了玩家的输入,移动,摄像机跟随,UI,与敌人生成。还没完,最关键的并行系统:EnemyMove跟EnemyCollision还没有实现。
难点中的难点来了,EnemyMoveSystem需要继承JobComponent系统来实现并行。


[AppleScript] 纯文本查看 复制代码
public class EnemyMoveSystem : JobComponentSystem
{
    ComponentGroup enemyGroup;   //由一系列组件组成
    ComponentGroup playerGroup;

    protected override void OnCreateManager() //系统创建时调用
    {
        //声明该组所需的组件,包括读写依赖
        enemyGroup = GetComponentGroup
        (
            ComponentType.ReadOnly(typeof(Velocity)),
            ComponentType.ReadOnly(typeof(EnemyComponent)),
            typeof(Position)
        );
        playerGroup = GetComponentGroup
        (
            ComponentType.ReadOnly(typeof(PlayerInput)),
            ComponentType.ReadOnly(typeof(Position))
        );
    }

    [BurstCompile] //使用Burst编译
    struct EnemyMoveJob : IJobParallelFor //继承该接口实现并行
    {
        public float deltaTime;
        public float3 playerPos;
        //记得声明读写关系
        public ComponentDataArray<Position> positions;
        [ReadOnly] public ComponentDataArray<Velocity> velocities;

        public void Execute(int i) //会被不同的线程调用,所以方法中不能存在引用类型。
        {
            //Read
            float3 position = positions.Value;
            float speed = velocities.Value;
            //算出朝向玩家的向量
            float3 vector = playerPos - position;
            vector = math.normalize(vector);

            float3 newPos = position + vector * speed * deltaTime;
            //Wirte
            positions = new Position { Value = newPos };
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps) //每帧调用
    {
        if (playerGroup.CalculateLength() == 0) //玩家死亡
            return base.OnUpdate(inputDeps);

        float3 playerPos = playerGroup.GetComponentDataArray<Position>()[0].Value;

        EnemyMoveJob job = new EnemyMoveJob
        {
            deltaTime = Time.deltaTime,
            playerPos = playerPos,
            positions = enemyGroup.GetComponentDataArray<Position>(), //声明了组件后,Get时会进行组件的获取
            velocities = enemyGroup.GetComponentDataArray<Velocity>()
        };
        return job.Schedule(enemyGroup.CalculateLength(), 64, inputDeps); //第一个参数意味着每个job.Execute的执行次数
    }
}

上面这个系统比较复杂但却是Unity ECS的核心,特别是OnUpdate中进行的操作,返回的JobHandle会被不同线程执行,理解这一点是关键。
EnemyCollisionSystem在实现上几乎与上述系统一致:
[AppleScript] 纯文本查看 复制代码
[UpdateAfter(typeof(PlayerMoveSystem))] //逻辑上依赖于玩家移动系统,所以声明更新时序
public class EnemyCollisionSystem : JobComponentSystem
{
    float playerRadius;
    float enemyRadius;
    public void Init(float playerRadius, float enemyRadius)
    {
        this.playerRadius = playerRadius;
        this.enemyRadius = enemyRadius;
    }

    ComponentGroup enemyGroup;
    ComponentGroup playerGroup;

    protected override void OnCreateManager()
    {
        enemyGroup = GetComponentGroup
        (
            ComponentType.ReadOnly(typeof(EnemyComponent)),
            typeof(Health),
            ComponentType.ReadOnly(typeof(Position))
        );
        playerGroup = GetComponentGroup
        (
            ComponentType.ReadOnly(typeof(PlayerInput)),
            ComponentType.ReadOnly(typeof(Position))
        );
    }

    [BurstCompile]
    struct EnemyCollisionJob : IJobParallelFor
    {
        public int collisionDamage; //碰撞对双方造成的伤害
        public float playerRadius;
        public float enemyRadius;
        public float3 playerPos;
        [ReadOnly] public ComponentDataArray<Position> positions;
        public ComponentDataArray<Health> enemies;

        public void Execute(int i)
        {
            float3 position = positions[i].Value;
            float x = math.abs(position.x - playerPos.x);
            float y = math.abs(position.y - playerPos.y);
            //距离
            float magnitude = math.sqrt(x * x + y * y);

            //圆形碰撞检测
            if (magnitude < playerRadius + enemyRadius)
            {
                //Read
                int health = enemies[i].Value;
                //Write
                enemies[i] = new Health { Value = health - collisionDamage };
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        if (playerGroup.CalculateLength() == 0) //玩家死亡
            return base.OnUpdate(inputDeps);

        float3 playerPos = playerGroup.GetComponentDataArray<Position>()[0].Value;

        EnemyCollisionJob job = new EnemyCollisionJob
        {
            collisionDamage = 1,
            playerRadius = this.playerRadius,
            enemyRadius = this.enemyRadius,
            playerPos = playerPos,
            positions = enemyGroup.GetComponentDataArray<Position>(),
            enemies = enemyGroup.GetComponentDataArray<Health>()
        };
        return job.Schedule(enemyGroup.CalculateLength(), 64, inputDeps);
    }
}

最后别忘了加上移除死亡的敌人的系统,按照Unity官方的说法我们需要使用如下的格式进行实体的移除,要注意的是IJobProcessComponentData接口,继承这个接口可以获得所有的带有指定组件的实体。


[AppleScript] 纯文本查看 复制代码
public class RemoveDeadBarrier : BarrierSystem
{
}
public class RemoveDeadSystem : JobComponentSystem
{
    struct Player
    {
        public readonly int Length;
        [ReadOnly] public ComponentDataArray<PlayerInput> PlayerInputs;
    }
    [Inject] Player player;
    [Inject] RemoveDeadBarrier barrier;

    [BurstCompile]
    struct RemoveDeadJob : IJobProcessComponentDataWithEntity<Health>
    {
        public bool PlayerDead;
        public EntityCommandBuffer Command;

        //该方法会获取所有带有Health组件的实体。
        public void Execute(Entity entity, int index, [ReadOnly] ref Health health)
        {
            if (health.Value <= 0 || PlayerDead)
                Command.DestroyEntity(entity);
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        bool playerDead = player.Length == 0;

        RemoveDeadJob job = new RemoveDeadJob
        {
            PlayerDead = playerDead,
            Command = barrier.CreateCommandBuffer(),
        };
        return job.ScheduleSingle(this, inputDeps); //这里使用ScheduleSingle可以不需要指定Execute的指定顺序。
    }
}


最后,当然别忘了在Bootstrap.Start中初始化这三个系统:


[AppleScript] 纯文本查看 复制代码
//初始化UI系统
UISystem uISystem = World.Active.GetOrCreateManager<UISystem>();
uISystem.Init(enemyCount);


//初始化敌人生成系统
EnemySpawnSystem enemySpawnSystem = World.Active.GetOrCreateManager<EnemySpawnSystem>();
enemySpawnSystem.Init(manager, enemyRenderer);


//初始化敌人碰撞系统
EnemyCollisionSystem collisionSystem = World.Active.GetOrCreateManager<EnemyCollisionSystem>();
collisionSystem.Init(playerRenderer.mesh.bounds.size.x / 2, enemyRenderer.mesh.bounds.size.x / 2);



当这些系统都完成之后我们运行游戏看一下效果:


1.3.gif



效果是分毫不差的

终于,我们用两种方式都已经实现了该游戏,用Unity Profiler简单测试一下这两种方式的性能:


测试机器的CPU(四核)与内存(8GB):




10.jpg


测试环境:关闭了绝大部分进程,CPU空闲的情况下使用Unity2019.1 Editor运行游戏。
测试方法:使用玩家小球朝着一个特定的方向移动。


首先来看一下基于Monobehavior的实现:


11.jpg



在敌人数量在17000左右时,游戏帧数掉到了30帧。由于没有使用Unity的物理系统,所以基本上是脚本跟渲染两大块占用CPU的性能。




12.jpg



在主线程中一帧的时间已经超过30毫秒了,其中脚本执行就占用了几乎27毫秒。
我们可以看到Job System上的线程也帮我们分担了不少渲染上的负担:




13.jpg


值得一提的是在unity2017之后加入了Job System,所以Unity的渲染也会被分配到不同的线程中去执行,在一定程度上提高了整体运行效率。
但在该游戏最吃性能的还是脚本,而我们希望在Job Sytem的不同工作线程中也能分担主线程中的脚本运行。


所以我们测试一下加上了Job Sytem的Unity ECS实现:


14.jpg



同样在17000左右,ECS实现依然能维持85帧
Job System的工作线程的确为主线程分担了相当一部分负担,并行化的脚本分配到了不同的工作线程上,利用了多核的性能。




15.jpg


在不同Worker Thread中有蓝色的代码执行片段
我们测试一下ECS的极限,看看实例化多少个单位会下降到30帧:


16.jpg

在敌人数量超过65000个时帧数下降到30帧
差不多是惊人的65000个,几乎是Mono的4倍(因为充分利用了四个处理器核心)。


从中我们可以看到,如果使用Unity开发某个拥有非常多相似的单位或是模型的游戏的时候使用Unity ECS会是不二之选。
知乎@ProcessCA



点评

使用Unity开发某个拥有非常多相似的单位或是模型的游戏的时候使用Unity ECS会是不二之选。  发表于 2018-12-8 00:55

评分

参与人数 2鲜花 +10 收起 理由
RyeCat + 5
soure + 5 很有帮助

查看全部评分

[发帖际遇]: 清风 乐于助人,奖励 2 蛮牛币. 幸运榜 / 衰神榜

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

7日久生情
2195/5000
排名
1402
昨日变化
5

0

主题

681

帖子

2195

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
135463
好友
0
蛮牛币
290
威望
0
注册时间
2016-1-23
在线时间
628 小时
最后登录
2019-3-19
发表于 2018-12-7 18:00:27 | 显示全部楼层
hgchbnb nbvjhvj

回复

使用道具 举报

5熟悉之中
745/1000
排名
4359
昨日变化
1

2

主题

152

帖子

745

积分

Rank: 5Rank: 5

UID
63685
好友
1
蛮牛币
863
威望
0
注册时间
2014-12-26
在线时间
257 小时
最后登录
2019-3-17
发表于 2018-12-7 18:07:45 | 显示全部楼层
赞一个,很好的ECS实例代码,最近正好最近看到相关的文章。谢谢分享

回复 支持 反对

使用道具 举报

7日久生情
2451/5000
排名
791
昨日变化
3

1

主题

685

帖子

2451

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
56496
好友
0
蛮牛币
7550
威望
0
注册时间
2014-11-19
在线时间
553 小时
最后登录
2019-3-19
发表于 2018-12-10 09:04:15 | 显示全部楼层
在敌人数量超过65000个时帧数下降到30帧,几乎是Mono的4倍(因为充分利用了四个处理器核心)  手机上是不是要烧鸡了

回复 支持 反对

使用道具 举报

4四处流浪
467/500
排名
5230
昨日变化
22

0

主题

42

帖子

467

积分

Rank: 4

UID
281427
好友
0
蛮牛币
601
威望
0
注册时间
2018-5-16
在线时间
151 小时
最后登录
2019-3-19
发表于 2018-12-10 09:14:43 | 显示全部楼层
感谢,在这里学习了,今后有需求还会来看看

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1136/1500
排名
2017
昨日变化
9

4

主题

128

帖子

1136

积分

Rank: 6Rank: 6Rank: 6

UID
161338
好友
0
蛮牛币
1429
威望
0
注册时间
2016-8-9
在线时间
328 小时
最后登录
2019-3-19
QQ
发表于 2018-12-10 09:24:30 | 显示全部楼层

回复

使用道具 举报

7日久生情
1793/5000
排名
1203
昨日变化
5

0

主题

514

帖子

1793

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
87577
好友
0
蛮牛币
6220
威望
0
注册时间
2015-3-31
在线时间
307 小时
最后登录
2019-3-19
发表于 2018-12-10 13:50:32 | 显示全部楼层
too good too strong!

回复 支持 反对

使用道具 举报

5熟悉之中
629/1000
排名
5318
昨日变化
37

2

主题

192

帖子

629

积分

Rank: 5Rank: 5

UID
208267
好友
0
蛮牛币
1316
威望
0
注册时间
2017-2-24
在线时间
165 小时
最后登录
2019-3-19
发表于 2018-12-13 09:55:06 | 显示全部楼层
想起了她的温柔

回复

使用道具 举报

4四处流浪
424/500
排名
5706
昨日变化
1

0

主题

46

帖子

424

积分

Rank: 4

UID
175798
好友
0
蛮牛币
314
威望
0
注册时间
2016-10-15
在线时间
132 小时
最后登录
2019-2-24
发表于 2018-12-13 13:31:18 | 显示全部楼层
这个示例代码不错,可以有个对比,有个从mono转到ecs的参考了
[发帖际遇]: sysoutyop 在网吧通宵,花了 1 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

5熟悉之中
939/1000
排名
16942
昨日变化
4

0

主题

632

帖子

939

积分

Rank: 5Rank: 5

UID
199204
好友
1
蛮牛币
430
威望
0
注册时间
2017-1-5
在线时间
267 小时
最后登录
2019-3-19
发表于 2018-12-17 23:15:00 | 显示全部楼层
谢谢分享

回复

使用道具 举报

3偶尔光临
221/300
排名
8901
昨日变化
2

1

主题

19

帖子

221

积分

Rank: 3Rank: 3Rank: 3

UID
77357
好友
1
蛮牛币
1087
威望
0
注册时间
2015-3-7
在线时间
69 小时
最后登录
2019-2-14
发表于 2018-12-18 14:58:16 | 显示全部楼层
感谢非常好。很强大!

回复 支持 反对

使用道具 举报

5熟悉之中
640/1000

0

主题

446

帖子

640

积分

Rank: 5Rank: 5

UID
109564
好友
0
蛮牛币
10
威望
0
注册时间
2015-6-20
在线时间
194 小时
最后登录
2019-3-6
发表于 2019-1-3 10:38:25 | 显示全部楼层
6666666666666666666666

回复 支持 反对

使用道具 举报

3偶尔光临
150/300
排名
14214
昨日变化
5

0

主题

61

帖子

150

积分

Rank: 3Rank: 3Rank: 3

UID
296951
好友
0
蛮牛币
24
威望
0
注册时间
2018-9-13
在线时间
31 小时
最后登录
2019-3-13
发表于 2019-2-13 09:53:31 | 显示全部楼层
楼主真会玩,66666666666666666666666

回复 支持 反对

使用道具 举报

7日久生情
2640/5000
排名
2234
昨日变化
10

1

主题

1677

帖子

2640

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
119154
好友
0
蛮牛币
2717
威望
0
注册时间
2015-8-21
在线时间
334 小时
最后登录
2019-3-18
发表于 2019-2-17 11:29:55 | 显示全部楼层
谢谢楼主大大。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表