官方案例解析6
開始之前的準備工作:
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/6. SpawnFromEntity ,並打開6. SpawnFromEntity場景
6. SpawnFromEntity
這個案例演示了我們如何使用預設遊戲對象來生成實體和組件,場景將由此生成一大堆旋轉的方塊對,貌似和上一個案例一樣,實則不是,下面一起來一探究竟吧:
- Main Camera ……主攝像機
- Directional Light……光源
- Spawner……旋轉方塊生成器
我們注意到這個Spawner生成器相對於案例5多了一個ConvertToEntity腳本,這個腳本是我們最早接觸到的,還記得它的功能嗎?就是把遊戲對象轉化成實體,由此我們就明白這個案例的用意了。相對於案例5的從遊戲對象上生成實體,我們這一次先將Spawner轉化成實體,再由實體來生成實體:
/// <summary>
/// 先將自己轉換成實體,再由預設生成新的實體
/// </summary>
[RequiresEntityConversion]
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject Prefab;
public int CountX;
public int CountY;
// Referenced prefabs have to be declared so that the conversion system knows about them ahead of time
/// <summary>
/// IDeclareReferencedPrefabs接口的實現,聲明引用的預設,好讓轉化系統提前知道它們的存在
/// </summary>
/// <param name="referencedPrefabs">引用的預設</param>
public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
{
referencedPrefabs.Add(Prefab);
}
// Lets you convert the editor data representation to the entity optimal runtime representation
/// <summary>
/// 我們將編輯器的數據表述轉化成實體最佳的運行時表述
/// </summary>
/// <param name="entity">實體</param>
/// <param name="dstManager">目標實體管理器</param>
/// <param name="conversionSystem">轉化系統</param>
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Spawner_FromEntity
{
// The referenced prefab will be converted due to DeclareReferencedPrefabs.
// So here we simply map the game object to an entity reference to that prefab.
//被引用的預設因爲做了聲明將被轉化成實體
//所以我們這裏只是將遊戲對象標記到一個引用該預設的實體上
Prefab = conversionSystem.GetPrimaryEntity(Prefab),
CountX = CountX,
CountY = CountY
};
dstManager.AddComponentData(entity, spawnerData);
}
}
因爲是將Spawner先轉化成實體,再由Spawner實體來生成旋轉方塊兒實體,所以這裏需要組件Spawner實體的ECS結構。
/// <summary>
/// 我是Spawner的組件,我只儲存數據
/// </summary>
public struct Spawner_FromEntity : IComponentData
{
public int CountX;
public int CountY;
public Entity Prefab;
}
Component和實體的代碼都非常簡單,所以後期開發的時候,我會寫一個工具來自動生成代碼,從而大大提高工作效率。既然我們是面向數據的編程模式,那麼我認爲可以直接通過策劃的表格或數據庫來直接生成實體和組件,我們要做的只是編寫各種System系統。先這麼構思好了,後面很快就能製作出對應的工具來的,我會把工具放到開發者聯盟的羣文件共享,如果有需要的話可以自行下載。稍後我會把製作代碼生成工具排進計劃中,不如明天好了。
跑題了,我們繼續來看看Spawner的System腳本SpawnerSystem_FromEntity:
// JobComponentSystems can run on worker threads.
// However, creating and removing Entities can only be done on the main thread to prevent race conditions.
// The system uses an EntityCommandBuffer to defer tasks that can't be done inside the Job.
/// <summary>
/// 任務組件系統(JobComponentSystems)可以在工作線程上運行,但是創建和移除實體只能在主線程上做,從而防止線程之間的競爭
/// Jobs系統使用一個實體命令緩存(EntityCommandBuffer)來延遲那些不能在任務系統內完成的任務。
/// </summary>
[UpdateInGroup(typeof(SimulationSystemGroup))]//標記更新組爲模擬系統組
public class SpawnerSystem_FromEntity : JobComponentSystem
{
// BeginInitializationEntityCommandBufferSystem is used to create a command buffer which will then be played back
// when that barrier system executes.
// Though the instantiation command is recorded in the SpawnJob, it's not actually processed (or "played back")
// until the corresponding EntityCommandBufferSystem is updated. To ensure that the transform system has a chance
// to run on the newly-spawned entities before they're rendered for the first time, the SpawnerSystem_FromEntity
// will use the BeginSimulationEntityCommandBufferSystem to play back its commands. This introduces a one-frame lag
// between recording the commands and instantiating the entities, but in practice this is usually not noticeable.
/// <summary>
/// 開始初始化實體命令緩存系統(BeginInitializationEntityCommandBufferSystem)被用來創建一個命令緩存,
/// 這個命令緩存將在阻塞系統執行時被回放。雖然初始化命令在生成任務(SpawnJob)中被記錄下來,
/// 它並非真正地被執行(或“回放”)直到相應的實體命令緩存系統(EntityCommandBufferSystem)被更新。
/// 爲了確保transform系統有機會在新生的實體初次被渲染之前運行,SpawnerSystem_FromEntity將使用
/// 開始模擬實體命令緩存系統(BeginSimulationEntityCommandBufferSystem)來回放其命令。
/// 這就導致了在記錄命令和初始化實體之間一幀的延遲,但是該延遲實際通常被忽略掉。
/// </summary>
BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;
/// <summary>
/// 在這個字段中緩存BeginInitializationEntityCommandBufferSystem,這樣我們就不需要每一幀去創建
/// </summary>
protected override void OnCreate()
{
// Cache the BeginInitializationEntityCommandBufferSystem in a field, so we don't have to create it every frame
m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
}
/// <summary>
/// 生成實體任務,實現了IJobForEachWithEntity接口
/// </summary>
struct SpawnJob : IJobForEachWithEntity<Spawner_FromEntity, LocalToWorld>
{
/// <summary>
/// 當前實體命令緩存
/// </summary>
public EntityCommandBuffer.Concurrent CommandBuffer;
/// <summary>
/// 這裏循環實例化實體
/// </summary>
/// <param name="entity">實體</param>
/// <param name="index">索引</param>
/// <param name="spawnerFromEntity">生成器實體</param>
/// <param name="location">相對位置</param>
public void Execute(Entity entity, int index, [ReadOnly] ref Spawner_FromEntity spawnerFromEntity,
[ReadOnly] ref LocalToWorld location)
{
for (var x = 0; x < spawnerFromEntity.CountX; x++)
{
for (var y = 0; y < spawnerFromEntity.CountY; y++)
{
var instance = CommandBuffer.Instantiate(index, spawnerFromEntity.Prefab);
// Place the instantiated in a grid with some noise
var position = math.transform(location.Value,
new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));
CommandBuffer.SetComponent(index, instance, new Translation {Value = position});
}
}
CommandBuffer.DestroyEntity(index, entity);
}
}
/// <summary>
/// 任務系統OnUpdate每幀調用
/// </summary>
/// <param name="inputDeps">輸入依賴</param>
/// <returns></returns>
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
//Instead of performing structural changes directly, a Job can add a command to an EntityCommandBuffer to perform such changes on the main thread after the Job has finished.
//Command buffers allow you to perform any, potentially costly, calculations on a worker thread, while queuing up the actual insertions and deletions for later.
//取代直接執行結構的改變,一個任務可以添加一個命令到EntityCommandBuffer(實體命令緩存),從而在主線程上完成其任務後執行這些改變
//命令緩存允許在工作線程上執行任何潛在消耗大的計算,同時把實際的增刪排到之後
// Schedule the job that will add Instantiate commands to the EntityCommandBuffer.
//把將要添加實例化命令到EntityCommandBuffer的任務加入計劃
var job = new SpawnJob
{
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent()
}.Schedule(this, inputDeps);
// SpawnJob runs in parallel with no sync point until the barrier system executes.
// When the barrier system executes we want to complete the SpawnJob and then play back the commands (Creating the entities and placing them).
// We need to tell the barrier system which job it needs to complete before it can play back the commands.
///生成任務並行且沒有同步機會直到阻塞系統執行
///當阻塞系統執行時,我們想完成生成任務,然後再執行那些命令(創建實體並放置到指定位置)
/// 我們需要告訴阻塞系統哪個任務需要在它能回放命令之前完成
m_EntityCommandBufferSystem.AddJobHandleForProducer(job);
return job;
}
}
這個腳本涉及到的線程知識比較多,所以我額外解釋一下:
- 任務系統Jobs是C#爲了讓我們安全地使用多線程而封裝的;
- 不能濫用任務系統,否則會引起線程之間的競爭,例如你有四個線程,但是現在有三個被佔用,卻有五個任務要完成,這時就會五個任務去爭奪一個線程,從而造成線程安全問題;
- 任務組件系統可以在工作線程上運行,但是創建和移除實體只能在主線程上做,從而防止線程之間的競爭;
- 爲了確保任務可以完成,這裏引入了命令緩存機制,就在先把任務緩存起來,等待主線程完成工作後,再進行增刪實體的操作。
- 關於阻塞系統,是爲了確保安全而生,當線程在執行任務的時候,將其阻塞起來,避免其他任務誤入,等任務完成之後,再執行下一個任務,從而有序進行。
- 有的任務由於等待太久而錯過時機怎麼辦,Play Back回放,大概是把沒有執行的任務重新添加到隊列(我猜的)
- 這些多線程的東西,知道一些常識即可,然後按照常識操作。
小結
這裏因爲Spawner生成器轉化成實體之後生成的是案例二的旋轉方塊實體,所以列出來做個類比,生成的方塊實體會按照案例二制定的規則自動運行。但凡組合成ECS都會這樣,生成之後就會按照既定System系統執行。
案例二:
ECS | Scripts | Interface |
---|---|---|
Entity | RotationSpeedAuthoring_IJobForEach | IConvertGameObjectToEntity |
Component | RotationSpeed_IJobForEach | IComponentData |
System | RotationSpeedSystem_IJobForEach | JobComponentSystem |
案例六:
ECS | Scripts | Interface1 | Interface2 |
---|---|---|---|
Entity | SpawnerAuthoring_FromEntity | IConvertGameObjectToEntity | IDeclareReferencedPrefabs |
Component | Spawner_FromEntity | IComponentData | |
System | SpawnerSystem_FromEntity | JobComponentSystem |
這裏Spawner因爲要轉化成實體,再生成實體,所以相對會多實現一個接口IDeclareReferencedPrefabs,來聲明其引用的預設。看起來會繁瑣一些,實際上卻是我們不得不做的,例如敵方AI弓箭手陣營不斷射箭,那麼首先弓箭手就是實體,射出來的箭也是實體,這就是實體生成實體了,弓箭手就得聲明箭是自己的預設!
DOTS 邏輯圖表
Spawn流程大體如下:
DOTS系統:
這一篇主要是多線程的地方要注意一下線程安全問題,其他的都是之前梳理過的。
更新計劃
作者的話
如果喜歡我的文章可以點贊支持一下,謝謝鼓勵!如果有什麼疑問可以給我留言,有錯漏的地方請批評指證!
如果有技術難題需要討論,可以加入開發者聯盟:566189328(付費羣)爲您提供有限的技術支持,以及,心靈雞湯!
當然,不需要技術支持也歡迎加入進來,隨時可以請我喝咖啡、茶和果汁!( ̄┰ ̄*)