关于 ECS 我想大家都不陌生了,比较Unity已经强推这么久了,如果你还不知道的话,的确有点孤陋寡闻了(其实我也是才知道的LOL)。我是因为看到MegaCity的视频才了解到ECS的,当时被庞大复杂的场景震撼到了,一直以来Unity官方出的视频都带给我强大的视觉冲击。第一部让我自发宣传的是《Adam》,后面连续出了续集2和3,然后戛然而止,却意犹未尽。Unity不开发游戏真是太可惜了,这样好的创意和功力,无论做什么都会让人拍手称赞的。至于最新的《异教徒》,让我不知道说什么好了,真希望赶快放续集,这个视频创意比亚当三部曲还要赞,千万别只出三部啊!
跑题了,我是发散性思维,超级喜欢跑题。话说回来,ECS虽然现在还没有完善,但是真是值得期待。我陆陆续续在网上找了很多相关的知识来看,越看越觉得有意思,不知不觉中越陷越深了,原本只是看了一个视频而已。
我理解的ECS是相对于OOP的,OOP就是Object Oriented Program啦,面向对象编程,格言就是Everything is Object,一切都是对象,这种设计其实是来源于现实世界的,道法自然嘛,整个虚拟世界都是在模仿现实世界,编程思维和设计也不例外。
面向对象老生常谈了,我觉得没有必要多讲了,就说说ECS吧,E是Entity实体,相对于Object对象的存在;C是Component组件,其实我认为用Data来表达更好一些,C代表的就是数据;S是System系统,相对于原来的Controller控制器。
总体的意思就是用数据来驱动实体的系统,可以这么理解,反正我是这么狭隘的认为的。
ECS相对于OOP有什么优势吗?
国外的大佬写了教学和性能比对的文章Unity* 实体组件系统 (ECS)、C# 作业系统和突发编译器入门水木本源大佬转了过来,看了实在受益匪浅。
说白了,ECS比OOP更快,而且不是一般的快,在大量测试的情况下快了上百倍。
我总结了ECS带来的一些改变:
啰里啰唆了这么多,不如直接上代码来的直观,前戏虽有必要,但是我的大刀早已饥渴难耐!
0下载Unity编辑器(2019.1.0f1 or 更新的版本),if(已经下载了)continue;
1打开Git Shell输入:
git clone https://github.com/Unity-Technologies/EntityComponentSystemSamples.git --recurse
or 点击Unity官方ECS示例下载代码
if(已经下载了)continue;
2用Unity Hub打开官方的项目:ECSSamples
3在Assets目录下找到HelloCube/1. ForEach,并打开ForEach场景
场景里总共有四个游戏对象,呃,等下就不是那么回事儿了!一切都源于一个神奇的脚本,我们来找到它:
打开RotatingCube上挂的ConvertToEntity脚本,这就是我们要找的神奇脚本,它将Unity的游戏对象GameObject转化成Entity,从而让游戏运行更加高效,详细的原理看一开始推荐国外大佬那篇文章。
下面我们来看看ConvertToEntity脚本是如何完成这项工作的:
///
/// 将游戏对象转化成ECS的实体Entity
///
[DisallowMultipleComponent]//不允许多组件
public class ConvertToEntity : MonoBehaviour
{
///
/// 转化模式枚举
///
public enum Mode
{
ConvertAndDestroy,//转化并摧毁,该模式下GameObject在被转化成实体Entity后原游戏对象被摧毁
ConvertAndInjectGameObject//转化并注入游戏对象,这个模式不会摧毁原来的游戏对象
}
///
/// 转化模式
///
public Mode ConversionMode;
void Awake()
{
if (World.Active != null)//这里新增了世界的概念,目的是划分OOP世界和ECS世界,游戏对象只存在于面向对象的世界中,实体同理
{
// Root ConvertToEntity is responsible for converting the whole hierarchy
//根节点的转化脚本负责转化整个层级,举个栗子:这个脚本挂在父节点RotatingCube上,那么子节点ChildCube也会被转化成实体
if (transform.parent != null && transform.parent.GetComponentInParent<ConvertToEntity>() != null)
return;//一个层级中只能在根节点上挂这个脚本,否则会在这里返回出去
//根据不同的模式调用不同的方法
if (ConversionMode == Mode.ConvertAndDestroy)
ConvertHierarchy(gameObject);
else
ConvertAndInjectOriginal(gameObject);
}
else
{
UnityEngine.Debug.LogWarning("ConvertEntity failed because there was no Active World", this);
}
}
///
/// 注入原始组件
///
/// 实体管理器
/// 实体
/// 变化组件
static void InjectOriginalComponents(EntityManager entityManager, Entity entity, Transform transform)
{
foreach (var com in transform.GetComponents<Component>())
{//这里遍历所有组件,并将实体注入到原始组件中,详询底层的代码
if (com is GameObjectEntity || com is ConvertToEntity || com is ComponentDataProxyBase)
continue;
//我们可以不关心更深层次的实现,只需了解这个表层的脚本功能即可,需要使用的时候把这个脚本当作组件来用
entityManager.AddComponentObject(entity, com);
}
}
///
/// 添加递归,把根节点下所有子节点都添加到实体管理器中
///
/// 实体管理器
/// 变化组件
public static void AddRecurse(EntityManager manager, Transform transform)
{
GameObjectEntity.AddToEntityManager(manager, transform.gameObject);
var convert = transform.GetComponent<ConvertToEntity>();
if (convert != null && convert.ConversionMode == Mode.ConvertAndInjectGameObject)
return;
foreach (Transform child in transform)
AddRecurse(manager, child);
}
///
/// 注入原始组件
///
/// 源游戏对象世界
/// 模拟世界,实体管理器所操控的实体世界
/// 变化组件
/// 循环递归直到层级中的所有对象都注入完,True:根节点
public static bool InjectOriginalComponents(World srcGameObjectWorld, EntityManager simulationWorld, Transform transform)
{
var convert = transform.GetComponent<ConvertToEntity>();
if (convert != null && convert.ConversionMode == Mode.ConvertAndInjectGameObject)
{
var entity = GameObjectConversionUtility.GameObjectToConvertedEntity(srcGameObjectWorld, transform.gameObject);
InjectOriginalComponents(simulationWorld, entity, transform);
transform.parent = null;
return true;
}
for (int i = 0; i < transform.childCount;)
{
if (!InjectOriginalComponents(srcGameObjectWorld, simulationWorld, transform.GetChild(i)))
i++;
}
return false;
}
///
/// 转化层级
///
/// 根节点
public static void ConvertHierarchy(GameObject root)
{
using (var gameObjectWorld = new GameObjectConversionSettings(World.Active, GameObjectConversionUtility.ConversionFlags.AssignName).CreateConversionWorld())
{
AddRecurse(gameObjectWorld.EntityManager, root.transform);
//关键方法,此游戏对象转化工具会将游戏对象世界转化成实体世界
GameObjectConversionUtility.Convert(gameObjectWorld);
InjectOriginalComponents(gameObjectWorld, World.Active.EntityManager, root.transform);
Destroy(root);
}
}
///
/// 转化并注入源
///
/// 根节点
public static void ConvertAndInjectOriginal(GameObject root)
{
using (var gameObjectWorld = new GameObjectConversionSettings(World.Active, GameObjectConversionUtility.ConversionFlags.AssignName).CreateConversionWorld())
{
GameObjectEntity.AddToEntityManager(gameObjectWorld.EntityManager, root);
GameObjectConversionUtility.Convert(gameObjectWorld);
var entity =GameObjectConversionUtility.GameObjectToConvertedEntity(gameObjectWorld, root);
InjectOriginalComponents(World.Active.EntityManager, entity, root.transform);
}
}
}
代码添加了详细注释,相信大家都大概明白了,就像注释里面说的那样,我们不必关心底层实现,毕竟精力有限,知道怎么使用即可。我们只需将这个脚本添加到游戏对象上,就可以把原来的游戏对象根据需求转化成实体。
World:世界这个概念以后会有很多应用场景,think about 多元世界 or 平行宇宙!
RotatingCube上还有一个负责传入旋转速度的脚本RotationSpeedAuthoring_ForEach:
using Unity.Entities;//依赖实体
using Unity.Mathematics;//数学
using UnityEngine;
///
/// 旋转速度写入遍历中
///
[RequiresEntityConversion]//必须实体转化
public class RotationSpeedAuthoring_ForEach : MonoBehaviour, IConvertGameObjectToEntity
{//这里继承了IConvertGameObjectToEntity接口,并实现了Convert转化方法,该方法会自动调用
///
/// 旋转速度:度每秒 °/s
///
public float DegreesPerSecond;
// The MonoBehaviour data is converted to ComponentData on the entity.
// We are specifically transforming from a good editor representation of the data (Represented in degrees)
// To a good runtime representation (Represented in radians)
///
/// 这里的Mono数据在实体上被转化成组件数据,我们特意把一个在编辑器上好表述的数据(度)转化成实时数据表述(弧)
/// 我们只需实现这个方法,并将数据传入即可,传入的数据会在运行的时候被使用
/// 实体
/// 目标实体管理器
/// 转化系统
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var data = new RotationSpeed_ForEach { RadiansPerSecond = math.radians(DegreesPerSecond) };
dstManager.AddComponentData(entity, data);
}
}
第一个脚本其实我们可以不管,直接拖拽使用即可,我们需要编写的是这个脚本,它需要引入实体和数学两个组件才能工作。我们会把方块旋转的速度通过这里传递给Component数据组件,该数据会在那里储存,并在System中被调用,在我们看完所有脚本之后再理清ECS的开发思路。下面我们来看组件脚本RotationSpeed_ForEach:
using System;
using Unity.Entities;//依赖实体
// Serializable attribute is for editor support.
///
/// 这个脚本继承IComponentData组件,它的功能就是储存数据
///
[Serializable]//可序列化特性是为了支持编辑器
public struct RotationSpeed_ForEach : IComponentData
{
///
/// 每秒旋转的弧度
///
public float RadiansPerSecond;
}
这个脚本最纯粹,它就是储存数据给System使用,下面我们来看系统RotationSpeedSystem_ForEach:
using Unity.Entities;//依赖实体和数学
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
// This system updates all entities in the scene with both a RotationSpeed_ForEach and Rotation component.
///
///这个系统脚本会每帧更新所有场景中同时带有RotationSpeed_ForEach和Rotation的实体
///
public class RotationSpeedSystem_ForEach : ComponentSystem
{
///
/// 重写父类ComponentSystem的方法,每帧调用
///
protected override void OnUpdate()
{
// Entities.ForEach processes each set of ComponentData on the main thread. This is not the recommended
// method for best performance. However, we start with it here to demonstrate the clearer separation
// between ComponentSystem Update (logic) and ComponentData (data).
// There is no update logic on the individual ComponentData.
///Entities.ForEach这个方法会处理在主线程上的每一组组件数据(ComponentData)
/// 这不是推荐的最佳性能实现方法。但是我们从这里开始表明更加干净的逻辑和数据分离,所谓解耦。
/// 没有任何更新逻辑会出现在单独的数据组件上。
Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) =>
{
var deltaTime = Time.deltaTime;
rotation.Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
});
}
}
我们来看看这个小案例是如何实现ECS架构的:
ECS | Scripts | 继承 |
---|---|---|
Entity | RotationSpeedAuthoring_ForEach | IConvertGameObjectToEntity |
Component | RotationSpeed_ForEach | IComponentData |
System | RotationSpeedSystem_ForEach | ComponentSystem |
通过上面这个对照表是不是理清开发思路了?下面通过逻辑图表来看看解耦的模式:
我大概是这么理解的,建议把Component换成Data,更容易看懂。:
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)