基于Unity2019最新ECS架構開發MMO遊戲筆記12
- MegaCity1
-
-
- 開始之前的準備工作:
- Megacity的交通系統
-
- 小結
-
- 更新計劃
-
- 作者的話
- ECS系列目錄
-
- ECS官方示例1:ForEach
- ECS官方案例2:IJobForEach
- ECS官方案例3:IJobChunk
- ECS官方案例4:SubScene
- ECS官方案例5:SpawnFromMonoBehaviour
- ECS官方案例6:SpawnFromEntity
- ECS官方案例7:SpawnAndRemove
- ECS進階:FixedTimestepWorkaround
- ECS進階:Boids
- ECS進階:場景切換器
- ECS進階:MegaCity0
- ECS進階:MegaCity1
- UnityMMO資源整合&伺服器部署
- UnityMMO選人流程
- UnityMMO主世界
MegaCity1
關于MegaCity,昨天簡單學習了SubScene流加載系統,非常适合應用在大環境場景,大環境場景也是一種大趨勢,從現在流行的遊戲可以看得出來!例如吃雞,刺客信條,賽博朋克2077等等,未來大場景的遊戲會越來越多,給玩家帶來更加豐富的遊戲體驗。是以ECS技術也是以大有可為,SubScene的加載速度令人驚歎,建議小夥伴們嘗試一下。
開始之前的準備工作:
0下載下傳Unity編輯器(Unity 2019.1.0 Beta 7 or 更新的版本),if(已經下載下傳了)continue;
1點選Megacity源代碼下載下傳Zip壓縮包;if(已經下載下傳了)continue;
2這個包有7.11G,解壓後17.6 GB,打開Unity Hub->項目->添加,把MegaCity_GDC2019_Release_OC添加到項目中;
3用Unity Hub打開官方開源項目:MegaCity_GDC2019_Release_OC,等待Unity進行編譯工作;
4打開Scenes/Megacity場景。
Megacity的交通系統
如果還沒有下載下傳運作過Megacity的場景,或觀看過示範視訊,可能對我接下來要說的并不感冒,Whatever,我們還是繼續今天的學習吧。
關于TrafficSystem,從遊戲菜單就可以看出來,分為兩種模式:
- 自動駕駛(On-Rails Flyover);
- 玩家控制(Player Controller)。
下面直接看代碼,E:
/// <summary>
/// 交通設定
/// </summary>
public class TrafficSettings : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
/// <summary>
/// 路段,把路徑分成100個片段
/// </summary>
public float pathSegments=100;
public float globalSpeedFactor = 1.0f;//全局速度參數
public int maxCars = 2000;//最大車量
public float[] speedMultipliers;//速度乘數數組
public List<GameObject> vehiclePrefabs;//車輛預設清單
/// <summary>
/// 聲明預設
/// </summary>
/// <param name="gameObjects">預設對象</param>
public void DeclareReferencedPrefabs(List<GameObject> gameObjects)
{
for (int i = 0; i < vehiclePrefabs.Count; i++)
{
gameObjects.Add(vehiclePrefabs[i]);
}
}
/// <summary>
/// 轉化:把資料交給C儲存
/// </summary>
/// <param name="entity"></param>
/// <param name="dstManager"></param>
/// <param name="conversionSystem"></param>
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
for (int j = 0; j < vehiclePrefabs.Count; j++)
{
// A primary entity needs to be called before additional entities can be used
//在附加實體可以被調用之前,主實體需要被提前調用
Entity vehiclePrefab = conversionSystem.CreateAdditionalEntity(this);
var prefabData = new VehiclePrefabData
{
VehiclePrefab = conversionSystem.GetPrimaryEntity(vehiclePrefabs[j]),
VehicleSpeed = j < speedMultipliers.Length ? speedMultipliers[j] : 3.0f
};
dstManager.AddComponentData(vehiclePrefab, prefabData);
}
var trafficSettings = new TrafficSettingsData
{
GlobalSpeedFactor = globalSpeedFactor,
PathSegments = pathSegments,
MaxCars = maxCars
};
dstManager.AddComponentData(entity, trafficSettings);
}
}
和E相關的有兩個C,這裡一起列出來:
/// <summary>
/// 汽車預設資料
/// </summary>
public struct VehiclePrefabData : IComponentData
{
public Entity VehiclePrefab;
public float VehicleSpeed;
}
/// <summary>
/// 交通設定資料
/// </summary>
public struct TrafficSettingsData : IComponentData
{
public float GlobalSpeedFactor;
public float PathSegments;
public float MaxCars;
}
不得不說,用Data作為命名字尾比Component直白多了,就應該這樣才對。我之前就吐槽過了,ECS應該更名為EDS更好一些,而DOTS裡面也用到了D,說明D的描述更準确。也許官方是考慮到Unity元件式開發的架構吧,想要繼續保留這個傳統。既然要改變,何不徹底一些?Anyway,還是看看S吧:
/// <summary>
/// 交通系統
/// </summary>
[AlwaysUpdateSystem]//總是更新
public sealed partial class TrafficSystem : JobComponentSystem
{
public NativeArray<RoadSection> roadSections;//路段
bool doneOneTimeInit = false;//一次性初始化是否完成
private ComponentGroup m_CarGroup;//車相關元件組
private int numSections = 0;//段數
public TrafficSettingsData trafficSettings;//交通設定資料
public NativeArray<VehiclePrefabData> vehiclePool;//車輛緩存池
// This is not the best way for ECS to store the player, it would be better to have component data for it
//這不是ECS緩存玩家的最佳方式,因為它是遊戲對象,感受到了過渡期的尴尬
private GameObject _player;
/// <summary>
/// ECS的實體引擎還在開發中,原來的實體系統隻能用在遊戲對象(GameObject)上
/// </summary>
private Rigidbody _playerRidigbody; // Will only be on the player controlled car
/// <summary>
/// 一次性設定
/// </summary>
void OneTimeSetup()
{
//擷取所有路段
var allRoads = GetComponentGroup(typeof(RoadSection)).ToComponentDataArray<RoadSection>(Allocator.TempJob);
//擷取交通設定
var settings = GetComponentGroup(typeof(TrafficSettingsData)).ToComponentDataArray<TrafficSettingsData>(Allocator.TempJob);
//擷取車輛預設
var vehicles = GetComponentGroup(typeof(VehiclePrefabData)).ToComponentDataArray<VehiclePrefabData>(Allocator.TempJob);
if (settings.Length == 0 || vehicles.Length == 0 || allRoads.Length == 0)
{
allRoads.Dispose();
vehicles.Dispose();
settings.Dispose();
return;
}
trafficSettings = settings[0];
// Copy the vehicle pool for prefabs
//複制車輛池作為預設,為其添加必要元件
vehiclePool = new NativeArray<VehiclePrefabData>(vehicles.Length, Allocator.Persistent);
for (int v = 0; v < vehicles.Length; v++)
{
if (!EntityManager.HasComponent<VehiclePathing>(vehicles[v].VehiclePrefab))
{
EntityManager.AddComponentData(vehicles[v].VehiclePrefab, new VehiclePathing());
}
if (!EntityManager.HasComponent<VehicleTargetPosition>(vehicles[v].VehiclePrefab))
{
EntityManager.AddComponentData(vehicles[v].VehiclePrefab, new VehicleTargetPosition());
}
if (!EntityManager.HasComponent<VehiclePhysicsState>(vehicles[v].VehiclePrefab))
{
EntityManager.AddComponentData(vehicles[v].VehiclePrefab, new VehiclePhysicsState());
}
vehiclePool[v] = vehicles[v];
}
// for now just copy everything 暫時複制所有
roadSections = new NativeArray<RoadSection>(allRoads.Length, Allocator.Persistent);
for (int a = 0; a < allRoads.Length; a++)
{
roadSections[allRoads[a].sortIndex] = allRoads[a];
}
numSections = roadSections.Length;
#if UNITY_EDITOR && USE_OCCUPANCY_DEBUG
OccupancyDebug.queueSlots = new NativeArray<Occupation>(numSections * Constants.RoadIndexMultiplier, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
OccupancyDebug.roadSections = new NativeArray<RoadSection>(numSections, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
for (int a = 0; a < roadSections.Length; a++)
{
OccupancyDebug.roadSections[a] = roadSections[a];
}
#endif
doneOneTimeInit = true;
allRoads.Dispose();
vehicles.Dispose();
settings.Dispose();
}
/// <summary>
/// 在建立管理器時執行
/// </summary>
protected override void OnCreateManager()
{
base.OnCreateManager();
//擷取所有含車輛實體狀态元件的都加入車組
m_CarGroup = GetComponentGroup(ComponentType.ReadOnly<VehiclePhysicsState>());
//生成車輛的阻塞 = 開始模拟實體指令緩存系統
_SpawnBarrier = World.GetOrCreateManager<BeginSimulationEntityCommandBufferSystem>();
//回收阻塞 = 結束模拟實體指令緩存系統
_DespawnBarrier = World.GetOrCreateManager<EndSimulationEntityCommandBufferSystem>();
// TODO: Should size this dynamically 應該采用動态擴容
_Cells = new NativeMultiHashMap<int, VehicleCell>(30000, Allocator.Persistent);
_VehicleMap = new NativeMultiHashMap<int, VehicleSlotData>(30000, Allocator.Persistent);
}
/// <summary>
/// 更新:每幀執行
/// </summary>
/// <param name="inputDeps">依賴</param>
/// <returns></returns>
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
if (!doneOneTimeInit)//第一次進入時先進行設定
{
OneTimeSetup();
return inputDeps;
}
if (vehiclePool.Length == 0 || roadSections.Length == 0)
return inputDeps;
#if UNITY_EDITOR && USE_DEBUG_LINES
var debugLines = _DebugLines.Lines.ToConcurrent();
#endif
//車位隊列 場景中的車輛沿着隊列行進,這個原生數組應該就是一個車隊,每個車隊中每輛車的位置稱為車位
var queueSlots = new NativeArray<Occupation>(numSections * Constants.RoadIndexMultiplier, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
// Setup job dependencies//設定任務依賴
//先清理依賴
JobHandle clearDeps = new ClearArrayJob<Occupation>
{
Data = queueSlots,
}.Schedule(queueSlots.Length, 512);
//清理哈希
var clearHash2Job = new ClearHashJob<VehicleSlotData> {Hash = _VehicleMap}.Schedule();
// Move vehicles along path, compute banking
//路徑依賴:沿着路徑,計算的彎道移動車輛
JobHandle pathingDeps = new VehiclePathUpdate { RoadSections = roadSections, DeltaTimeSeconds = Time.deltaTime * trafficSettings.GlobalSpeedFactor }.Schedule(this, JobHandle.CombineDependencies(clearDeps, inputDeps));
// Move vehicles that have completed their curve to the next curve (or an off ramp)
//完成轉彎的車輛移動到下一個彎道(或一個下坡)
JobHandle pathLinkDeps = new VehiclePathLinkUpdate { RoadSections = roadSections }.Schedule(this, pathingDeps);
// Move from lane to lane. PERF: Opportunity to not do for every vehicle.
//從車道到車道。性能優化:不必每一輛車都更新
JobHandle lanePositionDeps = new VehicleLanePosition { RoadSections = roadSections, DeltaTimeSeconds = Time.deltaTime }.Schedule(this, pathLinkDeps);
float3 playerPosition = default;
float3 playerVelocity = default;
if (_player != null)
{
playerPosition = _player.transform.position;
if (_playerRidigbody != null)
{
playerVelocity = _playerRidigbody.velocity;
}
}
// Compute what cells (of the 16 for each road section) is covered by each vehicle
//計算每輛車覆寫的單元(每段路16個單元)
JobHandle occupationAliasingDeps = new OccupationAliasing {OccupancyToVehicleMap = _VehicleMap.ToConcurrent(), RoadSections = roadSections}.Schedule(this, JobHandle.CombineDependencies(clearHash2Job, clearDeps, lanePositionDeps));
JobHandle occupationFillDeps = new OccupationFill2 {Occupations = queueSlots}.Schedule(_VehicleMap, 32, occupationAliasingDeps);
// Back-fill the information:填充資訊
// | A B |
// |AAAABBBBBBB |
JobHandle occupationGapDeps = new OccupationGapFill { Occupations = queueSlots }.Schedule(roadSections.Length, 16, occupationFillDeps);
occupationGapDeps = new OccupationGapAdjustmentJob {Occupations = queueSlots, RoadSections = roadSections}.Schedule(roadSections.Length, 32, occupationGapDeps);
occupationGapDeps = new OccupationGapFill2 {Occupations = queueSlots}.Schedule(roadSections.Length, 16, occupationGapDeps);
// Sample occupation ahead of each vehicle and slow down to not run into cars in front
// Also signal if a lane change is wanted.
//避免追尾,在需要換道時發送信号
JobHandle moderatorDeps = new VehicleSpeedModerate { Occupancy = queueSlots, RoadSections = roadSections, DeltaTimeSeconds = Time.deltaTime}.Schedule(this, occupationGapDeps);
// Pick concrete new lanes for cars switching lanes
//在車換道時選擇具體的新車道
JobHandle laneSwitchDeps = new LaneSwitch { Occupancy = queueSlots, RoadSections = roadSections}.Schedule(this, moderatorDeps);
// Despawn cars that have run out of road
//回收跑完全程的車輛
JobHandle despawnDeps = new VehicleDespawnJob { EntityCommandBuffer = _DespawnBarrier.CreateCommandBuffer().ToConcurrent() }.Schedule(this, laneSwitchDeps);
_DespawnBarrier.AddJobHandleForProducer(despawnDeps);
JobHandle spawnDeps;
var carCount = m_CarGroup.CalculateLength();
if (carCount < trafficSettings.MaxCars)
{
// Spawn new cars//生成新車
spawnDeps = new VehicleSpawnJob
{
VehiclePool = vehiclePool,
RoadSections = roadSections,
Occupation = queueSlots,
EntityCommandBuffer = _SpawnBarrier.CreateCommandBuffer().ToConcurrent()
}.Schedule(this,occupationGapDeps);
_SpawnBarrier.AddJobHandleForProducer(spawnDeps);
}
else
{
spawnDeps = occupationGapDeps;
}
#if UNITY_EDITOR && USE_OCCUPANCY_DEBUG
spawnDeps.Complete();
laneSwitchDeps.Complete();
for (int a = 0; a < queueSlots.Length; a++)
{
OccupancyDebug.queueSlots[a] = queueSlots[a];
}
#endif
JobHandle finalDeps = default;
float3 camPos = default;
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
camPos = mainCamera.transform.position;
}
JobHandle movementDeps = JobHandle.CombineDependencies(spawnDeps, despawnDeps);
int stepsTaken = 0;
float timeStep = 1.0f / 60.0f;
_TransformRemain += Time.deltaTime;
while (_TransformRemain >= timeStep)
{
var clearHashJob = new ClearHashJob<VehicleCell> {Hash = _Cells}.Schedule(movementDeps);
var hashJob = new VehicleHashJob {CellMap = _Cells.ToConcurrent()}.Schedule(this, clearHashJob);
hashJob = new PlayerHashJob {CellMap = _Cells, Pos = playerPosition, Velocity = playerVelocity}.Schedule(hashJob);
movementDeps = new VehicleMovementJob
{
TimeStep = timeStep,
Cells = _Cells,
#if UNITY_EDITOR && USE_DEBUG_LINES
DebugLines = debugLines
#endif
}.Schedule(this, hashJob);
_TransformRemain -= timeStep;
++stepsTaken;
}
JobHandle finalPosition;
if (stepsTaken > 0)
{
JobHandle finalTransform = new VehicleTransformJob {dt = timeStep, CameraPos = camPos}.Schedule(this, movementDeps);
finalPosition = finalTransform;
}
else
{
finalPosition = movementDeps;
}
finalDeps = finalPosition;
// Get rid of occupation data
//清除緩存
JobHandle disposeJob = new DisposeArrayJob<Occupation>
{
Data = queueSlots
}.Schedule(JobHandle.CombineDependencies(spawnDeps, laneSwitchDeps));
return JobHandle.CombineDependencies(disposeJob, finalDeps);
}
private float _TransformRemain;
private NativeMultiHashMap<int, VehicleCell> _Cells;
private NativeMultiHashMap<int, VehicleSlotData> _VehicleMap;
private BeginSimulationEntityCommandBufferSystem _SpawnBarrier;
private EndSimulationEntityCommandBufferSystem _DespawnBarrier;
#if UNITY_EDITOR && USE_DEBUG_LINES
[Inject] private DebugLineSystem _DebugLines;
#endif
protected override void OnDestroyManager()
{
if (doneOneTimeInit)
{
roadSections.Dispose();
vehiclePool.Dispose();
#if UNITY_EDITOR && USE_OCCUPANCY_DEBUG
OccupancyDebug.queueSlots.Dispose();
OccupancyDebug.roadSections.Dispose();
#endif
}
_VehicleMap.Dispose();
_Cells.Dispose();
}
public void SetPlayerReference(GameObject player)
{
_player = player;
var rigid = _player.GetComponent<Rigidbody>();
if (rigid != null)
{
_playerRidigbody = rigid;
}
}
}
交通系統比較複雜,下面小結中我會嘗試畫一幅圖來解釋一些概念。
小結
畫了半天,畫得不滿意,因為太複雜了,實在畫不明白。
更新計劃
作者的話
如果喜歡我的文章可以點贊支援一下,謝謝鼓勵!如果有什麼疑問可以給我留言,有錯漏的地方請批評指證!
如果有技術難題需要讨論,可以加入開發者聯盟:566189328(付費群)為您提供有限的技術支援,以及,心靈雞湯!
當然,不需要技術支援也歡迎加入進來,随時可以請我喝咖啡、茶和果汁!( ̄┰ ̄*)