前言
还是因为期末考试导致ECS系统的学习推迟了很久,再加上ECS的内容比较抽象,这里只是简单讲讲进阶内容
原型和chunk
在笔记一中我们介绍了chunk,我们知道IJobEntity是它的语法糖,现在,让我们正式和它见一面吧。
简单来说,chunk就是连续内存块,每一个的大小一般是16kb。它的里面装满原型相同的实体,这也是为什么ECS的速度非常快的原因之一。
上面这段话很难理解,所以我们逐一为您解释
原型是什么
原型就是所有的组件相同,共享组件(SharedComponent)的值相同的实体,我们称其为一个原型,Unity把对应的原型分成多个chunk,便于查找。
了解了原型,我们还要知道它在代码中出现的情况。通常情况下,只要你给实体添加组件,就会有对应的原型,唯一用到的地方是你要创建大量的新实体,并且实体上要添加较多组件,请不要给每一个实体AddComponent一遍,而是创建出对应的原型,再统一创建
EntityArchetype _bulletArchetype;
public void OnCreate(ref SystemState state)
{
_bulletArchetype = state.EntityManager.CreateArchetype(
typeof(BulletComponent),
typeof(LocalTransform),
typeof(LocalToWorld),
);
EntityManager.CreateEntity(_bulletArchetype, count, Allocator.Temp);
}
改变原型
既然原型由实体挂载的组件决定,那它的原型会不会改变呢?答案是肯定的,这种情况,专业术语称为结构性变化。它会导致chunk中的分布发生变化,每一次结构性变化后Unity都会帮你把新原型放到对应的chunk中。这样会很耗性能,下面几种情况会导致结构性变化:
-
删除,增加实体
-
为实体删除,增加组件
-
sharedComponent中的值发生变化
低频少量的改变对性能的影响极小,可以放心使用,如果是高频,大量的改变,可以考虑以下的方案
- 使用ecb(EntityCommandBuffer),请注意,这个是在系统的主线程中使用,它并不会让增删的性能变化,只是打上标记,在当前帧的帧末调用playback方法统一增删。只是提供安全性,并不能提供性能。playback是否手动调用取决于ecb是通过CommandBufferSystem的CreateCommandBuffer方法还是你new出来的。
- 使用ecb的AsParallelWriter()方法,它可以为你提供一支多线程安全的笔,让你在JobEntity或JobChunk中想用ecb相同的安全性
- 真正能够提供性能的,是IEnableComponent,它类似IComponentData,只是多了一个打开关闭的属性,类似Mono中的enable属性,我们可以在查找时选择性忽略某个组件被关闭的实体,而且,它的打开与关闭并不影响实体
SharedComponent组件
请不要感觉神秘,它也类似IComponentData,只不过主要用来充当不常改变的,原型相同的公共值,取值起来比一般组件更快更省内存,你可以把它当成普通组件,只要牢记一改变它的值就会发生结构性变化即可
ECS中的事件系统
回想c#中的事件系统,为我们带来了很多便利,然而,ECS世界中并没有这种便利,为了实现这样的效果,我们需要自己创建类似的机制
原理
使用类似监听与命令的方式,将事件作为一种组件,每当事件发生,就挂载到对应的实体上,再由监听系统统一处理逻辑。
我知道上面这段话很难理解,所以接下来为您举一个例子
例子
首先定义一个事件参数的组件
public struct HitEventData:IComponentData
{
public int Damage; //伤害事件
}
接下来的逻辑是满足特定情况时就给实体添加上面的事件,常见的比如碰撞
,这里为了演示方便,直接让有Player标签的一直受到Hit
public particla struct OnHitSystem:System
{
public void OnUpdate(ref SystemState state)
{
var player=SystemAPI.GetSingletonEntity<Player>();
AddComponent(player,new HitEventData()
{
Damage=100;
});//这里本来是要用ecb,我是为了演示方便
}
}
最后,只需要一个监听系统,看看谁身上有HitEventData组件,并执行相关逻辑就好了。可以是减少Health组件的值等等,这里就交由读者自行完成。
最后提一嘴,大量添加组件会导致结构性变化…………
ECS中的碰撞系统
首先,想要碰撞,请自行导入Unity.Physics包,之后可以为SubScene中的对象添加刚体和碰撞体等Mono组件,ECS在渲染时会自行为您生成对应的组件。
ICollisionEventJob与ITriggerEventJob
我们真正需要关心的是碰撞事件的产生,好在Unity为我们提供了两个接口,用来替代Mono中的OnTriggerEnter等事件。
首先,我们需要对应的Job事件,继承自标题的两种之一
[BurstCompile]
partial struct CollisionEventJob : ICollisionEventsJob
{
public ComponentLookup<Health> HealthLookup; // Lookup类似Dictionary,以实体为键
[ReadOnly]
public ComponentLookup<Enemy> EnemyLookup;
public void Execute(CollisionEvent collisionEvent)
{
Entity a = collisionEvent.EntityA;//发生碰撞的实体A
Entity b = collisionEvent.EntityB;//发生碰撞的实体B
// 根据组件判断谁是敌人,谁是子弹
Entity enemy = EnemyLookup.HasComponent(a) ? a : b;
Entity bullet = enemy == a ? b : a;
if (EnemyLookup.HasComponent(enemy) && HealthLookup.HasComponent(enemy))
{
Health hp = HealthLookup[enemy];//获取敌人的生命组件
hp.Value -= 10; // 扣血
HealthLookup[enemy] = hp;
// 子弹回收( 最好用ecb,这里演示直接删)
// EntityManager.DestroyEntity(bullet);
}
}
}
有了Job还不够,还需要一个系统
[UpdateInGroup(typeof(PhysicsSimulationGroup))] // 必须在这个组里
[UpdateAfter(typeof(PhysicsSimulationGroup))] // 等物理跑完再处理
public partial struct CollisionEventSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<SimulationSingleton>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var job = new CollisionEventJob
{
HealthLookup = SystemAPI.GetComponentLookup<Health>(false),
EnemyLookup = SystemAPI.GetComponentLookup<Enemy>(true)
};
state.Dependency = job.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(),
state.Dependency);
}
}
[UpdateInGroup(typeof(PhysicsSimulationGroup))]这个我忘了有没有在第一节提过了,这里提一嘴,UpdateInGroup可以决定系统的运行时机,这里是在物理组中,也可以在初始化组和一般组,还有FixedUpdate组等等,UpdateAfter/Before可以决定系统的执行顺序,这里也包含你自己的系统,一搬用到的情况就是读数据在写数据之后。
ECS中的对象池系统
对象池的核心,在于为池中的对象添加Disabled组件(Unity已经内置),并在需要时直接找寻对应实体并移除Disabled组件,这里简单给一个例子
为对象池初始化的,可以设置预制体和初始大小
public class BulletPoolAuthoring : MonoBehaviour
{
public GameObject prefab;
public int size;
private class Baker : Baker<BulletPoolAuthoring>
{
public override void Bake(BulletPoolAuthoring authoring)
{
Entity entity=GetEntity(TransformUsageFlags.None);
AddComponent(entity,new BulletPool()
{
Prefab = GetEntity(authoring.prefab,TransformUsageFlags.Dynamic),
PoolSize = authoring.size,
});
}
}
}
初始化对象池系统,填充对象池,只执行一次
[WorldSystemFilter(WorldSystemFilterFlags.Default)]
partial struct InitBulletPoolSystem : ISystem
{
private EntityQuery _poolQuery;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
_poolQuery = state.GetEntityQuery(ComponentType.ReadOnly<BulletPool>());
state.RequireForUpdate<BulletPool>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
state.Enabled = false;
var ecb = new EntityCommandBuffer(Allocator.Temp);
var pools=_poolQuery.ToComponentDataArray<BulletPool>(Allocator.Temp);
foreach (var pool in pools)
{
for (int i = 0; i < pool.PoolSize; i++)
{
var bullet = ecb.Instantiate(pool.Prefab);
ecb.AddComponent<Disabled>(bullet);
}
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
后续想要子弹只要查找有BulletComponent和Disabled组件的实体,并删除Disabled组件即可,想丢弃子弹就加上Disabled就好了
配置表Blob
想要实现Unity中的SO,最简单又实用的方法就是用一个IComponentData作为配置表,当然,这里有一个更麻烦的方法,但是既然它有,就必然有其存在价值,这里简单略过
首先搞一个SO资产
[CreateAssetMenu(fileName = "PlayerConfig", menuName = "SO/Config/PlayerConfig")]
public class PlayerConfig : ScriptableObject
{
[Header("基础属性")]
public float baseHealth;
public float baseMaxHealth;
public float baseSpeed;
public int baseLevel;
public int baseNeedExperience;
[Header("视角输入")]
public float viewSensitivityX = 200f;
public float viewSensitivityY = 200f;
[Header("开火设置")]
public float fireRate;
public float bulletSpeed;
public float bulletDamage;
}
再用一个结构体接受SO资产
public struct PlayerConfigBlob
{
public float BaseHealth;
public float BaseMaxHealth;
public float BaseSpeed;
public int BaseLevel;
public int BaseNeedExperience;
public float ViewSensitivityX ;
public float ViewSensitivityY;
public float FireRate;
public float BulletSpeed;
public float BulletDamage;
}
还有一个组件数据来接收
public struct PlayerConfigComponent : IComponentData
{
public BlobAssetReference<PlayerConfigBlob> ConfigBlob;//一个引用
}
再来一个辅助方法实现从SO到Blob的转化
public static BlobAssetReference<PlayerConfigBlob> ConvertPlayerConfigBlob(PlayerConfig so)
{
if (so == null)
{
Debug.LogError("没有玩家配置表");
return default;
}
using var builder = new BlobBuilder(Allocator.Temp);
ref var blobData = ref builder.ConstructRoot<PlayerConfigBlob>(); //要求获得一片内存区域的引用
blobData.BaseHealth = so.baseHealth;
blobData.BaseLevel = so.baseLevel;
blobData.BaseMaxHealth = so.baseMaxHealth;
blobData.BaseNeedExperience = so.baseNeedExperience;
blobData.BaseSpeed = so.baseSpeed;
blobData.BulletDamage = so.bulletDamage;
blobData.BulletSpeed = so.bulletSpeed;
blobData.FireRate = so.fireRate;
blobData.ViewSensitivityX = so.viewSensitivityX;
blobData.ViewSensitivityY= so.viewSensitivityY;
//冻结成只读资产并返回一个只读引用
var blobRef = builder.CreateBlobAssetReference<PlayerConfigBlob>(Allocator.Persistent);
return blobRef;
}
最后烘培即可
[SerializeField]
private PlayerConfig config;
public PlayerConfig Config => config;
private class Baker : Baker<PlayerAuthoring>
{
public override void Bake(PlayerAuthoring authoring)
{
if (authoring.Config == null)
{
Debug.LogError($"{nameof(authoring.Config)} is null");
return;
}
Entity entity=GetEntity(TransformUsageFlags.Dynamic);
var blob=ConfigBlobConverter.ConvertPlayerConfigBlob(authoring.Config);
if (!blob.IsCreated)
{
Debug.LogError($"{nameof(authoring.Config)} is not created");
return;
}
//用引用计数,防止假死
AddBlobAsset(ref blob, out var hash);
//清除警告
_ = hash;
//这个组件留着当备胎,以后需要了才用
AddComponent(entity,new PlayerConfigComponent()
{
ConfigBlob=blob,
});
}
}
绕了这么大一圈,其实真不如一个IComponentData来的快吧
结语
ECS后面应该就是学一些场景方面的内容了,看看寒假之前会不会有第三节吧

被折叠的 条评论
为什么被折叠?



