基本概念
在Dots中也分为编辑模式下来编辑物体和运行模式下编辑物体。他们分别被称之为:Authoring模式与Runtime模式,由于我们大部分的时候都是用的是结构体类型,因此我们在很多地方都需要用到地址,而不是他的拷贝,所以Unity用RefRW和RefRO来帮我们封装了一层。在使用的时候也需要注意,有可能你获得的值只是个拷贝。
Authoring模式下创建物体
在Authoring模式下也就是编排模式中,我们想要编辑物体则需要创建一个SubScene,该场景用于将我们的将我们的GameObject转化为Entity。并给予各种Component。在该模式下,我们将编写的Component称之为Baker(一些自定义的数据,不包括方法)。将添加组件的行为称之为Baking。
Runtime模式下创建物体
基础介绍
在运行模式下我们需要先创建一个World,World是一个Entity的集合,我们通过world下的EntityManager来管理里面的Entity。Entity在一个World中的id是唯一的,但在不同的world下可能会相同,即A和B两个world中都有一个id为1的entity。Entity只是一个id。他并没有数据也没有方法,我们可以通过Entity来索引到他所对应的数据,即ECS中的C部分。然后我们再通过System来对他们进行操控。
Runtime模式下下的代码大概如下(来源自视频)
var world = new World("Test"); //创建world
var entityManager = world.EntityManager;//获得world中的manager
Entity entity = entityManager.CreateEntity(); //创建一个Entity
entityManager.AddComponent<CubeGeneratorByScript>(entity) //绑定Component
var generator = entityManager.GetComponentData<CubeGeneratorByScript>(entity);//获得添加的Component副本。用于后面设置数据
generator.cubeCount = CubeCount;
entityManager.SetComponentData(entity,cubePrototype);
//other----------
//System中实例化
var generator = SystemAPI.GetSingleton<CubeGeneratorByScript>();
var cube =CollectionHelper.CreateNativeArray<Enetity>(generator.cubeCount,Allocator.Temp);
state.entityManager.Instantiate(generator.cubeEntityProtoType,cubes);
cubes.Dispose();
常用方法
因为我们是拥有一个默认的世界(Default World),因此我们可以在System中直接通过该World的EntityMananger来创建物体。如下
var generator = SystemAPI.GetSingleton<WaveCubeGenerateData>();
var cubes = CollectionHelper.CreateNativeArray<Entity>(4 * generator.halfCountY * generator.halfCountX, Allocator.Temp);
state.EntityManager.Instantiate(generator.CubeProtoType, cubes);
首先声明一个空间来存放物体,然后通过Instantiate方法填充即可。
对于一个RefRW引用的变量,他的值,是在ValueRW变量下的。
//RefRW<LocalTransform> transform
transform.ValueRW.postion.y = 1;
Archetype与Chunk
Archetype为一个相同组件的原型标识。假设EntityA与EntityB都是具有Postion和Rotation两个组件,那么他们就是同一种Archetype。而EntityC只有一个Position。他就属于另一种Archetype。而我们通过这种模式,在遍历的时候也可以更好的获得相应的原型
在每个Archetype所标记的内存会被分为固定大小且连续的非托管内存块。这一个内存块被称之为Chunk。默认下每个Chunk大小为16kb,每个Chunk中包含了共享原型的Component数组以及Entity的数组,当数组没有填充满的情况下,也需要留空。以确保内存对齐。
Entity本质上只是一个索引,我们可以通过该索引来获得Component数组中所对应的组件
Structural Change:所有需要导致需要重新组织内存块或内存块内容的操作都被称之为structural Change,比如我们去给某个Entity删除或添加Component。又比如我们将某个Entity给移动到另一个Chunk块中等一系列操作内存的行为,添加或删除Entity也在其中
1.0.10补充
在Bakder下新增加了一个TransformUsageFlags类,它用于将我们自动转化为Entity时来筛选Component。且在后面的GetEntity方法中都需要该参数。
无论是将一个Prefab转化为Entity,还是将Gameobject Baker成Entity都需要参数
var entity = GetEntity(TransformUsageFlags.Dynamic);
var entity2 = GetEntity(prefab,TransformUsageFlags.None);
当参数为 None时意味着我们不需要转化任何的Component上去,又比如我们有一个房子GameObject,他是一个静态物体,因此TransformUsageFlags参数为Renderable。这说明它只需要一个LocalToWorld组件,但如果他还有个子物体窗户,这个窗户可能需要打开等。那么他的参数就应该为 Dynamic,这样就会为它添加上LocalTransform与LocalToWorld俩组件了,
总之该参数可以帮助我们在Bake时减少消耗。
Entity 包
Compnent
根据内存类型来划分大约分为 非托管Compnent与托管Compnent.对于其他的划分的话还分为了很多的类型,具体可以去文档中查看,比如单例版本的Compnent,按照访问来划分的有按照Entity访问的,按Chunk访问的等等,我们会在后面来介绍这些Component
非托管Compnent
用于存储一些常见的数据类型,具体的存储数据类型可以去给出的链接中查看,他可以存储Blittable类型,一般来说,非托管类型他们的大小或者内存是不变的。
托管Compnent
托管的Compnent可以存储任何数据,但缺点是无法通过Jobs进行访问。也无法用Brust编译,因此他与非托管Compnent相比效率较低。
对于功能性上划分的Component有很多,比如Tag Component,他虽然也是继承自IComponent,但是他一般不写任何数据,只在System中作为一个Tag使用,自然他也不会去占用内存
System
System的接口只要有两个:ISystem和SystemBase。
ISystem的接口只有值类型的对象可以继承。否则会报错,他与Burst兼容,且比SystemBase更快。而SystemBase则要比ISystem慢一些,但他要比ISystem更加的方便,ISystem提供对非托管的内存的访问,而SystemBase提供了对托管内存的访问。
对于System的使用,我们应该System 尽可能的不要定义数据,而是将数据组织成Compnoent。 在System更新的时候,尽可能的采用SystemState的GetEntityQuery,GetComponentTypeHanle<>等访问Chunk的component数组,而不是通过EntityManager等方法访问。
在system中我们想要更改某个Entity的Component的值,有很多办法,比如可以现在查询时候指定他。或者通过Aspect来包装一个方法。
也可以通过**SystemAPI.GetComponentRW
SystemGroup
SystemGroup感觉像是让我们控制每一个System的更新顺序,我们可以打开System的窗口来查看所有的System的更新顺序。同时也可以使用特性UpdateBefore与UpdateAfter来控制我们的System的顺序。同时利用UodateInGroup来了将我们的System添加到指定的Group下。
遍历与查询
有的时候我们可能会想缓存下一个Query,可以利用EntityQuery来声明一个变量。并声明一个EntityQueryBuilder实例。并用SysteState的GetEntityQuery方法填充
EntityQuery q;
var query = new EntityQueryBuilder(Allocator.Temp).WithAll<RotateSpeed,LocalTransform>();
q = state.GetEntityQuery(query);
如果一个EntityQuery包含了一个共享组件,还可以使用方法AddSharedComponentFitter方法来进行组件剔除。(普通的查询应该也可以,但是API机翻看不懂。
然后我们可以通过
query.ToEntityArray(Allocator.Temp);
来获得一个Entity的数组以供后面使用
官方实例
SystemAPI.Query
在一般的情况下我们使用**SystemAPI.Query<>**即可满足遍历需求。具体的用法如下
foreach (var (transform,speed) in SystemAPI.Query<RefRW<LocalTransform>,RefRO<RotationCube>>())
{
transform.ValueRW = transform.ValueRO.RotateY(
speed.ValueRO.rotationSpeed * deltaTime
);
}
SystemAPI.Query的参数是Entity上的Component,他们在该world中选出所有满足条件的Entity,比如上文中的就是挑选出含有LoaclTransform与RotationCube的Entity。另外他只能与foreach连着使用,无法将结果存入一个变量中在后面反复使用。
IJobEntity
我们还可以通过继承自IJobEntity接口来实现查询物体,用法如下
[BurstCompile]
public partial struct RotationCubeJob : IJobEntity
{
public float deltaTime;
public void Execute(ref LocalTransform transform,in RotationCube speed,in RotationRedCube red)
{
transform = transform.RotateY(deltaTime * speed.rotationSpeed);
}
}
//xxxx
public void OnUpdate(ref SystemState state)
{
var job = new RotationCubeJob() { deltaTime = SystemAPI.Time.DeltaTime};
job.ScheduleParallel();
}
同时我们也可以在Execute的参数中来筛选出想要的物体,在上面的例子中便是我们会挑选出带有三个组件的物体。为了避免拷贝所带来的性能影响,我们在Execute的函数中的参数上会加上in关键字来避免消耗。我们还可以通过Entity参数来获得该entity
public void Execute(Entity entity);
IJobChunk
他会遍历ArcheType Chunk,为每一个Chunk执行一次Execute。一般用于不需要遍历每个Entity的情况。比较少用。
IAspect
Dots中给我们提供了一个新的接口,它可以将我们的Component给进行一个更高级的封装,使其更符合我们oop的写法,用法如下
readonly partial struct RotationAndMoveAspects : IAspect
{
public readonly RefRW<LocalTransform> localTransform;
public readonly RefRO<RotateAndMoveSpeed> speed;
public void Move(double elapsedTime)
{
localTransform.ValueRW.Position.y = (float)math.sin(elapsedTime * speed.ValueRO.speed);
}
public void Rotate(float deltaTime) {
localTransform.ValueRW = localTransform.ValueRO.RotateY(speed.ValueRO.rotateSpeed * deltaTime);
}
public void RotateAndMove(double elapsedTime,float deltaTime)
{
localTransform.ValueRW.Position.y = (float)math.sin(elapsedTime * speed.ValueRO.speed);
localTransform.ValueRW = localTransform.ValueRO.RotateY(speed.ValueRO.rotateSpeed * deltaTime);
}
}
我们可以在里面封装出自己想要的Component。其中在声明的时候readonly关键字是必须的,partial也是必须的。对于封装的数据必须要用RefRW或者RefRO引用起来。
在SystemAPI.Query中我们也可以通过输入IAspect类型的变量来遍历出我们想要的实体。
当我们的entity存在Aspects中所有的组件的时候我们就默认他有该Aspect了
我们在该结构体内实现出自己想要的方法,然后在外面直接调用即可。
foreach (var aspect in SystemAPI.Query<RotationAndMoveAspects>())
{
//aspect.Move(elapsedTime);
//aspect.Rotate(deltaTime);
aspect.RotateAndMove(elapsedTime, deltaTime);
}
Entity的随机访问
Entity的随机访问指的是在任意的时刻通过Entity对象来访问到他的组件。通过EntityManager可以获得,但是EntityManger无法在Jobs中使用
我们可以在Job中声明一个ComponentLookup变量,然后在创建Entity时候填充他,然后我们可以通过Entity作为key来获得他的物体。
ComponentLookup<RotateAndMoveSpeed> speed;
speed = state.GetComponentLookup<RotateAndMoveSpeed>(),
在1.0.10版本中推荐在OnCreate中获得lookup变量,然后再OnUpdate里面调用update方法来更新?
public partial struct CubesMarchingSystem : ISystem{
ComponentLookup<RotateAndMoveSpeed> lookup;
public void OnCreate(ref SystemState state){
lookup = state.GetComponentLookup<RotateAndMoveSpeed>();
}
public void OnUpdate(ref SystemState state){
lookup.update(ref state);
//具体使用
var job1 = new CubesMarchingEntityJob()
{
speed = lookup,
deltaTime = SystemAPI.Time.DeltaTime,
ecb = ecbp
};
}
}
具体实例待补充
EntityCommandBuffer
EntityCommandBuffer类似于一个指令链。由于在Job中无法直接创建与销毁Entity。因此我们就需要将命令记录下来,在同步到主线程的时候来执行所记录下的指令。
因此EntityCommandBuffer 在 Job中承担的是EntityManager的工作。他们的方法也有很多相同。
EntityCommandBuffer的基础使用方法是我们在主线程,也就是System中声明一个EntityCommandBuffer变量。然后将其赋值给我们的Job。
public void OnUpdate(ref SystemState state)
{
EntityCommandBuffer ecb = new EntityCommandBuffer(Allcator.TempJob);
// xxxx
var job = new Job
{
// xxx
ecb = ecb,
}
state.Dependency = job.ScheduleParllel(state.Dependecy);
//state.Dependency = job.Schedule(cubes.length,state.Dependency);//这里分别是使用ScheduleParllel和使用Schedule的区别
state.Dependency.Comlete();//为了等待所有的job都执行完毕,我们需要让主线程等待
ecb.playback(state.EntityManager);
Cubes.Dispose();
ecb.Dispose();
}
一个简单的ecb的使用也就大概如上,我们ecb的实例化时候需要指定内存为TempJob。在Job中来负责执行一些我们像执行的命令,比如创建Entity和删除Entity。如下
//创建Entity
public struct GenerateCubesJob : IJobFor
{
[ReadOnly]
public Entity cubeProtoType;
public NativeArray<Entity> cubes;
public EntityCommandBuffer ecb;
[NativeDisableUnsafePtrRestriction]
public RefRW<RandomSingleton> random;
public void Execute(int index)
{
cubes[index] = ecb.Instantiate(cubeProtoType);
ecb.AddComponent(cubes[index],new RotateAndMoveSpeed
{
rotateSpeed = math.radians(60),
speed = 5f,
});
float2 targetPos2D = random.ValueRW.random. NextFloat2(new float2(-15, -15), new float2(15, 15));
ecb.AddComponent(cubes[index], new RandomTarget()
{
postion = new float3(targetPos2D.x, 0, targetPos2D.y)
});
}
}
//删除Entity
ecbp.DestroyEntity(chunkIndex, entity);
由于job的执行顺序是不确定的。但在我们看来某个任务他的执行顺序是不会因为先后执行顺序导致错误。比如填充数组。这时候可以利用特性NativeDisableUnsafePtrRestriction来放弃编辑器检查(错误言论,但是不知道他干嘛用的,所以暂时先不删除了),
在创建的Job中。由于我们还没有任何的Entity,所以他不能继承自IJobEntity。应该继承IJobFor。由于在job中我们并没有真正的执行命令,而是记录下了命令,因此在创建中的时候index也仅仅是在数组Cubes的一个占位符,并没有与Entity连接起来。因此我们只能在Job中来创建Entity和AddComponent。而不是去SetComponent。
在删除的例子中,ChunkIndex是一个用ChunkIndexInQuery特性修饰的变量,它可以让我们获得该Entity所在的Chunk的索引,以便于我们来并行化的操控Entity,而在普通的ECB中则不需要提供该参数
//IJobEntity的函数声明
void Execute([ChunkIndexInQuery]int chunkIndex,Entity entity,ref LocalTransform transform,in RandomTarget target,in RotateAndMoveSpeed speed)
Scheduled的执行是一个工作线程中来执行。不算真正的并行化执行任务,因此我们可以使用SchedulePallel来执行Job。在此时的时候我们便不能使用ecb了。而是使用EntityCommandBuffer.ParallerWriter类型,该类型可以通过一个ecb对象调用**AsParallerWriter()**方法来获得
EntityCommandBuffer.ParallelWriter ecbp =ecb.AsParallelWriter();
//具体的调用
state.Dependency = job.ScheduleParallel(cubes.Length, 1,state.Dependency);
ecb的一些注意内容
强烈不建议在多个Job中共享使用一个ecb。最好为每一个Job创建和使用一个ecb。
如果要多次调用ecb的Playback方法,这可能会导致异常,如果真的要调用可以在创建ecb的时候指定MultiPlayBack选项
EntityCommandBuffer ecb=new EntityCommandBuffer(Allocator.TempJob,PlaybackPolicy.MultiPlayback);
我们可以通过继承EntityCommandBufferSystem来实现一个ECBSystem。
DynamicBufferComponent
DynamicBufferComponent可以看成一个动态大小的数组,我们将自己想要的数据继承自IBufferElementData,然后在Bake中通过**AddBuffer
[InternalBufferCapacity(8)]
struct WayPoint : IBufferElementData
{
public float3 point;
}
//xxx
DynamicBuffer<WayPoint> wayPoints = AddBuffer<WayPoint>();
一个DBC的容量默认为128字节,而一个DBC的默认容量为8,也就是当我们的元素一个字节大小的时候,我们一个DBC的内存就是8字节。这么算下来那我们一个element的默认大小就是 128 / 8 = 16,刚好是一个float4的类型,我们还可以通过InternalBufferCapacity特性来更改默认大小,来调高我们的紧凑型。当发生了Structural Change的时候可能会破坏DBC,这个时候我们需要重新调用GetBuffer来获取DynamicBuffer。
一个DBC默认是在一个Chunk中的,当他其中的容量超过了的时候,他会被移出该Chunk。
普通使用
在使用上我们可以通过Entity.Manager.GetBuffer
在Job中使用DBC
待补充
ECB中使用DBC
在ECB中我们可以通过类似**Addbuffer
Enableable Componet
Eneableable Component是一个可以开关的组件类型。它允许我们将一个组件关闭,从而达到在查询中剔除的效果,当关闭该Component以后,查询的时候会将其视为没有组件的情况。这样我们就可以避免大量Addcomponent或RemoveComponent所造成的Structural Change的情况,利用Enableable Component来提到Tag Component可以减少Archetype的数量,更好的利用内存。
当我们对该实体进行组件操作的时候与其他的组件一样,该获得获得该设置设置
Enableable Component只能使用在IComponentData或IBufferElement上。在继承上述两种接口的时候,在继承实现IEnableableComponent接口
struct data :IComponentData,IEnableableComponent
在Job中的使用
在Job中关闭和启用组件是不会对组件的值造成任何影响的,因此我们可以在Job中放心的关闭和开启,当一个Job拥有写权限时候,应该避免在另一个job中来启用或关闭该Component。
SharedComponent
共享组件,属于是我们多个Entity他们在值相同的时候共享一个组件,注意并不是他们共用一个组件。SharedComponent不存在于chunk中,而是存在于其他的内存块中,而每一个Chunk块中则存储了该ShadredComponent的句柄,
当我们更改了一个组件的共享组件的值以后,他会先寻找是否有chunk块存储的是该值,如果有便将该Entity移动到该Chunk块中,如果没有则会移动到一个新的Chunk块中(Archetype没有变化)。并创建一个新的SharedComponent。并让该entity引用该值
无论如何,只要改动了SharedComponent的值,便会造成Structual Change。因此我们要尽可能的避免更改其值,同时也应避免大量有独特值的共享组件,因为这样会导致有很多利用率不高的Chunk存在。
还应该避免多个共享组件类型组合,因为会造成多个Archetype类型,导致碎片化
使用方法
在使用上我们需要根据数据的类型划分为非托管类型与托管类型。非托管与托管类型,他们分开存储。
非托管类型
让我们的Component继承字ISharedComponentData接口即可。
public struct CubeSharedComponentData : ISharedComponentData
{
public float rotateSpeed;
public float moveSpeed;
}
在添加组件的时候不再能使用AddComponent,而是使用AddSharedComponent
public struct CubeSharedComponentData : ISharedComponentData
{
public float rotateSpeed;
public float moveSpeed;
}
托管类型
待补充
Blob Asset
Blob(Binary Large Object) Asset是Unity中一块连续的内存区域,用于存储不可变的非托管二进制数据。可以用于流式传输。他是一种只读的数据结构,之后无法更改他的值。
他与ShadredComponent或ScriptObject差不多。都是用于类似享元模式的解决办法,但他么仍有一切区别,
Blob Asset | SharedComponent |
---|---|
不能有托管数据 | 可以有托管数据 |
不能运行时更改 | 可以运行时更改数据(会导致Structual Change) |
数据是只读可以多线程访问而无需安全检查 | 数据可读写,需要安全检查 |
数据定义在组件外,通过BlobAssetRef引用访问 | 直接定义在组件内 |
BlobAsset的数据只能为 BlitableType(即可以在托管代码与非托管代码之间无缝转化的类型)数据类型。BlobString ,BlobArray,BlobPtr这几种。
具体使用
假设在某个游戏中我们会有很多的炮塔,他们会有很多的属性,如生命值。护甲值,攻击力等,按照正常写法应该是这样写
public enum BuidingType //建筑类型
{
BT_Spawner, //兵营
BT_DefenderTower, //防御塔
BT_MAX
}
public enum ArmorType //护甲类型
{
AT_None = 0, //无甲
AT_Light, //轻甲
AT_Normal, //中甲
AT_Heavy, //重甲
AT_Hero, //特殊类型甲
AT_Max
}
public enum DamageType //伤害类型
{
DT_Slash = 0, //挥砍伤害
DT_Pricks, //穿刺伤害
DT_Smash, //粉碎伤害
DT_Magic, //魔法伤害
DT_Chaos, //混合型伤害
DT_Hero, //特殊类伤害
DT_Max
}
struct EntitySpawnerAllComponentData : IComponentData
{
public Entity entityProtoType; //生成的entity原型对象,用于实例化克隆 8byte
public BuidingType buildingType; //建筑类型 4byte
public int level; //当前等级 4byte
public float tickTime; //每多少秒生成一次 4byte
public int spawnCountPerTicktime; //每次生成几个entity 4byte
public float maxLife; //最大生命值 4byte
public float currentlife; //当前生命值 4byte
public ArmorType armorType; //护甲类型 4byte
public DamageType damageType; //伤害类型 4byte
public float maxDamage; //最大攻击力 4byte
public float minDamage; //最大攻击力 4byte
public float upgradeTime; //升级时间 4byte
public float upgradeCost; //升级费用 4byte
}
如果有1000个这样的塔,就会占用56kb的内存。但是他们大部分的内容都是相同的,比如护甲类型,最大攻击力与升级耗费等,只有生命值是每个独有的,因此我们可以将这些不变的内容制作为BlobAsset来共享,
我们将原先的一个ComponentData分为三份,一个是每个Entity独享的(EntitySpawnerComponentData),一个是他们共同享有的(EntitySpawnerSettings),而最后的一个就是他们所共用的内容(EntitySpawnerBlobData)
struct EntitySpawnerComponentData : IComponentData
{
public float currentlife; //当前生命值 4byte
}
struct EntitySpawnerBlobData
{
public Entity entityProtoType; //生成的entity原型对象,用于实例化克隆 8byte
public BuidingType buildingType; //建筑类型 4byte
public int level; //当前等级 4byte
public float tickTime; //每多少秒生成一次 4byte
public int spawnCountPerTicktime; //每次生成几个entity 4byte
public float maxLife; //最大生命值 4byte
public ArmorType armorType; //护甲类型 4byte
public DamageType damageType; //伤害类型 4byte
public float maxDamage; //最大攻击力 4byte
public float minDamage; //最大攻击力 4byte
public float upgradeTime; //升级时间 4byte
public float upgradeCost; //升级费用 4byte
}
struct EntitySpawnerSettings : IComponentData
{
public BlobAssetReference<EntitySpawnerBlobData> blobSettings; // 8byte
}
具体的添加如下
BlobAssetReference<EntitySpawnerBlobData> CreateSpawnerBlobSettings(EntitySpawnerAuthoring authoring)
{
var builder = new BlobBuilder(Allocator.Temp);
//创建一块堆内存的引用
ref EntitySpawnerBlobData spawnerBlobData = ref builder.ConstructRoot<EntitySpawnerBlobData>();
///相关赋值
spawnerBlobData.entityProtoType = GetEntity(authoring.protoTypePrefab, TransformUsageFlags.Dynamic);
spawnerBlobData.buildingType = authoring.buildingType;
spawnerBlobData.level = authoring.level;
spawnerBlobData.tickTime = authoring.tickTime;
spawnerBlobData.spawnCountPerTicktime = authoring.spawnCountPerTicktime;
spawnerBlobData.maxLife = authoring.maxLife;
spawnerBlobData.armorType = authoring.armorType;
spawnerBlobData.damageType = authoring.damageType;
spawnerBlobData.maxDamage = authoring.maxDamage;
spawnerBlobData.minDamage = authoring.minDamage;
spawnerBlobData.upgradeTime = authoring.upgradeTime;
spawnerBlobData.upgradeCost = authoring.upgradeCost;
//完成BlobAsset的创建并返回内存的引用
var result = builder.CreateBlobAssetReference<EntitySpawnerBlobData>(Allocator.Persistent);
//切记删除临时的BlobAsset构造器
builder.Dispose();
//返回引用
return result;
}
//---使用BlobAsset
AddComponent(entity, new EntitySpawnerComponentData
{
currentlife = authoring.maxLife
});
var settings = CreateSpawnerBlobSettings(authoring);
AddBlobAsset(ref settings, out var hash);
ddComponent(entity, new EntitySpawnerSettings
{
blobSettings = settings
});
Cleanup Component
当销毁一个包含Cleanup Component的Entity时,他会删除其他非Cleanup Component组件,但该Entity仍然存在。
他不会随实体复制到另一个world中,它主要用于在创建Entity后初始化Entity或销毁Entity时清理Entity,想要使用Cleanup Component可以继承以下接口
1.ICleanupComponentData //托管与非托管类型
2.ICleanupBufferElementData //dynamic buff 类型
3.ICleanupSharedComponentData //sharedCompoent类型
ChunkComponent
ChunkComponent是按照Chunk而非Entity来存储值的组件。当我们给某个Entity添加了一个ChunkComponent时,他会指向另一个存有该ChunkComponent的chunk原型。
即如果我 entity1 添加了 一个ChunkComponent以后,他的Chunk会有一个外部Chunk链接,在外部的chunk中存储了一个对应的组件,如果此时有一个entity2,他与entity1属于两个不同的Archetype,但他们所指向的是同一个Chunk。但他们真正的Component却不是同一个,当entity2变成与entity1的Aechetype相同以后,他们所引用的component变成了同一个。这意味着ChunkComponet是属于Chunk上的概念,在同一个chunk下的entity所包含的component是同一个,达到了共享组件的目的。另外设置ChunkComponent的值不属于一个StructureChange。因为他没有让entity发生变化,就好比设置某个组件值不属于一样。
具有相同ChunkCompoennt的Chunk都是有自己单独的副本,比如前面的Entity2与Entity1的例子,
对于ChunkComponent的使用,依然是将数据继承自IComponentData,但是添加则是通过AddChunkComponent相关API来添加与删除
Graphics
该包主要是将我们的Gameobject的MeshRender与MeshFilter在Entity下也有一个对应的Component。其中的MeshRender与MeshFilter 变为了 RenderMesh, LODGroup 变为了Mesh’LODMeshLODGroupComponent,Transform变味了LocalToWorld(localTransform的值其实是相对于父物体而言的,当没有父物体的情况下他的值才相当于世界坐标)。
当我们通过prefab来实例化entity时,他会自动转化这几个相应组件,如果我们想自己添加的话可以通过以下代码实现。主要的API为 RenderMeshUtility。尽可能的不要自己一个一个添加渲染相关的组件。最基本的使用如下。我们只用设置网格与材质即可。
public class CreateEntityWithMonobehavior : MonoBehaviour
{
public Mesh mesh;
public Material material;
void Start()
{
var world = World.DefaultGameObjectInjectionWorld;
var entityManager = world.EntityManager;
var renderMeshArray = new RenderMeshArray(new[] {material}, new []{mesh});
//渲染相关设置
var renderMeshDescription = new RenderMeshDescription
{
FilterSettings = RenderFilterSettings.Default,
LightProbeUsage = LightProbeUsage.Off,
};
var cubeEntity = entityManager.CreateEntity();
//添加组件
RenderMeshUtility.AddComponents(
cubeEntity,
entityManager,
renderMeshDescription,
renderMeshArray,
MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));
//设置世界坐标
entityManager.SetComponentData(cubeEntity, new LocalToWorld{Value = float4x4.identity});
}
}
ReadMeshUtility是一个主线程API。所以还是别拿他来创建很多的Entity。
在渲染中还有个特殊的组件——DisableRendering,它可以将我们的物体不渲染,但是该entity的Component依然会被更新。
材质属性值修改
[MaterialProperty("_BaseColor")]
public struct CustomColor : IComponentData
{
public float4 color;
}
在运行期间修改相关值
想要动态的替换材质或mesh,我们可以通过更改MaterialMeshInfo的MeshID与MaterialID信息,他们分别是已经注册好的索引BatchMeshID和BatchMaterialID。想要将材质或mesh注册进去则需要使用 EntitiesGraphicsSystem相关方法。相关代码如下
//动态变化的相关组件,因为包含了托管数据,所以他也得为class,如果是共享的话还可以用blobAsset或SharedComponent来优化
public class CustomMeshAndMaterial : IComponentData
{
public Mesh sphere;
public Mesh capsule;
public Mesh cylinder;
public Material red;
public Material green;
public Material blue;
}
// createCube.cs
// 填充相关数据
entityManager.AddComponentData(entity, new CustomMeshAndMaterial
{
sphere = changeMeshes[0],
capsule = changeMeshes[1],
cylinder = changeMeshes[2],
red = changeMaterials[0],
green = changeMaterials[1],
blue = changeMaterials[2]
});
// 更新逻辑
public partial class ChangeWaveCubesMeshAndMaterialSystem : SystemBase
{
private Dictionary<Mesh, BatchMeshID> m_MeshMapping;
private Dictionary<Material, BatchMaterialID> m_MaterialMapping;
//注册所有的网格信息
protected override void OnStartRunning()
{
RequireForUpdate<CustomMeshAndMaterial>();
var entitiesGraphicsSystem = World.GetOrCreateSystemManaged<EntitiesGraphicsSystem>();
m_MeshMapping = new Dictionary<Mesh, BatchMeshID>();
m_MaterialMapping = new Dictionary<Material, BatchMaterialID>();
Entities
.WithoutBurst()
.ForEach((in CustomMeshAndMaterial changer) =>
{
if (!m_MeshMapping.ContainsKey(changer.sphere))
m_MeshMapping[changer.sphere] = entitiesGraphicsSystem.RegisterMesh(changer.sphere);
if (!m_MeshMapping.ContainsKey(changer.capsule))
m_MeshMapping[changer.capsule] = entitiesGraphicsSystem.RegisterMesh(changer.capsule);
if (!m_MeshMapping.ContainsKey(changer.cylinder))
m_MeshMapping[changer.cylinder] = entitiesGraphicsSystem.RegisterMesh(changer.cylinder);
if (!m_MaterialMapping.ContainsKey(changer.red))
m_MaterialMapping[changer.red] = entitiesGraphicsSystem.RegisterMaterial(changer.red);
if (!m_MaterialMapping.ContainsKey(changer.green))
m_MaterialMapping[changer.green] = entitiesGraphicsSystem.RegisterMaterial(changer.green);
if (!m_MaterialMapping.ContainsKey(changer.blue))
m_MaterialMapping[changer.blue] = entitiesGraphicsSystem.RegisterMaterial(changer.blue);
}).Run();
}
//update中来动态替换相关网格与材质
protected override void OnUpdate()
{
Entities
.WithoutBurst()
.ForEach((CustomMeshAndMaterial changer, ref MaterialMeshInfo info, in LocalToWorld trans) =>
{
if (trans.Position.y < -5)
{
info.MeshID = m_MeshMapping[changer.cylinder];
info.MaterialID = m_MaterialMapping[changer.blue];
}
else if (trans.Position.y > 5)
{
info.MeshID = m_MeshMapping[changer.sphere];
info.MaterialID = m_MaterialMapping[changer.red];
}
else
{
info.MeshID = m_MeshMapping[changer.capsule];
info.MaterialID = m_MaterialMapping[changer.green];
}
}).Run();
}
}
关于在 MonoBehaviour下来创建Entity
大致操作为我们可以通过World来获得相应的world,然后通过world里面的EntityManger来创建一个空的Entity。再来给他添加相应的组件
Physics
与原版没啥区别,将物体添加上相关的Collider组件与rigidbody组件在拖拽到subscence下即可。在unity的物理模拟一帧中大约做了以下的事情。
而在我们的Physics包中,他是有以下的几个Systemgroup来完成的,我们需要了解他每一步所做的事情,才方便后面我们自己来控制物理行为
FixedStepSimulationSystemGroup
PhysicsSystemGroup
PhysicsInitializeGroup //该system用于初始化所需的物理数据
PhysicsSimulationGroup
PhysicsCreateBodyPairsGroup //查找AABB重叠部分
PhysicsCreateContactsGroup //创建接触信息
PhysicsCreateJacobiansGroup //基于接触信息来创建矩阵信息
PhysicsSolveAndIntegrateGroup //计算矩阵结果
ExportPhysicsWorld // 应用得到的数据
对于我们自己的物理操作就应放在PhysicsSimulationGroup之前,切记不要放在ExportPhysicsWorld之后,这样他才能并入到各种计算之中.
对物体属性的修改
Dots为我们提供了一个Aspect——RigidbodyAspect。它提供了一些方便我们操作Entity物理的操作,比如给一个物体一个角动量。RigidbodyAspect的数据都属于Ecs数据,因此对他的操作应该是在物理模拟(PhysicsSystemSimulationSystemGroup之前),Rigidbody变成的相关Components
[BurstCompile]
public partial struct ApplyImpulseJob : IJobEntity
{
public float DeltaTime;
public float3 ImpulseDir;
public void Execute(RigidBodyAspect rigidBodyAspect)
{
float3 impulse = -ImpulseDir;
impulse *= DeltaTime;
rigidBodyAspect.ApplyAngularImpulseWorldSpace(impulse);
}
}
[BurstCompile]
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateBefore(typeof(PhysicsSystemGroup))]
public partial struct ApplyImpluseSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<GeneratorData>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
SimulationSingleton simulation = SystemAPI.GetSingleton<SimulationSingleton>();
if (simulation.Type == SimulationType.NoPhysics)
return;
TornadoComponent tornado = SystemAPI.GetSingleton<TornadoComponent>();
state.Dependency = new ApplyImpulseJob()
{
DeltaTime = SystemAPI.Time.DeltaTime,
ImpulseDir = tornado.MoveDirection,
}.Schedule(state.Dependency);
}
}
可以达到一个自转 与其他物体碰撞从而使物体散的更开的效果。
对物理流程的修改
[BurstCompile]
//该Job是循环访问可能重叠体,pair
public struct DisableDynamicDynamicPairsJob : IBodyPairsJob
{
public int NumDynamicBodies;
public void Execute(ref ModifiableBodyPair pair)
{
//如果两个物体都有rigidbody,边禁用该次碰撞检测。
bool isDynamic = pair.BodyIndexA < NumDynamicBodies && pair.BodyIndexB < NumDynamicBodies;
if(isDynamic)
{
pair.Disable();
}
}
}
[BurstCompile]
[UpdateInGroup(typeof(PhysicsSimulationGroup))]
[UpdateAfter(typeof(PhysicsCreateBodyPairsGroup))]
[UpdateBefore(typeof(PhysicsCreateContactsGroup))]
public partial struct DisableDynamicDynamicPairsSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<SimulationSingleton>();
state.RequireForUpdate<PhysicsWorldSingleton>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
SimulationSingleton simulation = SystemAPI.GetSingleton<SimulationSingleton>();
if(simulation.Type == SimulationType.NoPhysics)
{
return;
}
var physicsWorld = SystemAPI.GetSingletonRW<PhysicsWorldSingleton>().ValueRW.PhysicsWorld;
state.Dependency = new DisableDynamicDynamicPairsJob()
{
//获得该PhysicsWorld下动态刚体的数量
NumDynamicBodies = physicsWorld.NumDynamicBodies,
}.Schedule(simulation,ref physicsWorld,state.Dependency);
}
}
PhysicsWorld是一个用于模拟物理相关的系统,我们可以通过他来获得相关的控制,等同于Physics类,里面有投射射线等一系列相关的方法。这个例子中我们介入了物理模拟流程,即获得碰撞信息之后,计算物理信息之前的流程,由于物理模拟的数据都存在于内存之中,我们只能通过相关的方法来修改。
通过PhysicsWorldSingleton我们还可以获得一个不是单例版本的类。单例版本的可以使我们的jOb不会遇到条件竞争?
碰撞查询
大致分为以下几种类型
- Raycast : 射线检测,获得射线方向上所有或最近的点
- ColliderCast :碰撞体射线检测
- ColliderDistance :碰撞体距离检测,在指定范围内找出与其他碰撞体最近的点
- PointDistance :在半径内找出最近的点
- Overlap Query : 找出碰撞体相重合的实体