Development of MMO Game Notes 12 Based on Unity 2019's Latest ECS Architecture
- MegaCity1
- Update plan
- ECS Series Catalogue
- Development of MMO Game Note 0 Based on Unity2019 Latest ECS Architecture
- Development of MMO Game Notes 1 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 2 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 3 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 4 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 5 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 6 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 7 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 8 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 9 Based on Unity 2019's Latest ECS Architecture
- Development of MMO Game Notes 10 Based on Unity 2019's Latest ECS Architecture
- MegaCity0
MegaCity1
Regarding MegaCity, I learned the SubScene streaming loading system simply yesterday, which is very suitable for application in large environment scenarios. Large environment scenarios are also a trend, as can be seen from the popular games now! For example, chicken, assassin creed, cyberpunk 2077 and so on, there will be more and more games in big scenes in the future, which will bring more rich game experience to players. So ECS technology has great potential. SubScene's loading speed is amazing. I suggest that small partners try it.
Preparations before commencement:
Download Unity Editor (Unity 2019.1.0 Beta 7 or updated version), if (downloaded) continue;
1 Click Megacity source code Download Zip compressed package; if (downloaded) continue;
2 This package has 7.11G, 17.6GB after decompression, open Unity Hub - > Project - > Add, add MegaCity_GDC2019_Release_OC to the project;
3 Open the official open source project with Unity Hub: MegaCity_GDC2019_Release_OC, waiting for Unity to compile;
Open Scenes/Megacity scenario.
Megacity's Traffic System
If you haven't downloaded or watched a scene running Megacity yet Demo Video Maybe I don't catch a cold about what I'm going to say next, Whatever, let's continue our study today.
As far as Traffic System is concerned, it can be seen from the game menu that there are two modes:
- On-Rails Flyover;
- Player Controller.
Look directly at the code below, E:
/// <summary> /// Traffic Settings /// </summary> public class TrafficSettings : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs { /// <summary> /// Section, divide the path into 100 segments /// </summary> public float pathSegments=100; public float globalSpeedFactor = 1.0f;//Global velocity parameters public int maxCars = 2000;//Maximum traffic volume public float[] speedMultipliers;//Velocity multiplier array public List<GameObject> vehiclePrefabs;//Vehicle Preset List /// <summary> /// Declarative Presupposition /// </summary> /// <param name="gameObjects">default object </param> public void DeclareReferencedPrefabs(List<GameObject> gameObjects) { for (int i = 0; i < vehiclePrefabs.Count; i++) { gameObjects.Add(vehiclePrefabs[i]); } } /// <summary> /// Conversion: Hand over data to C for storage /// </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 //Primary entities need to be called in advance before additional entities can be invoked 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); } }
There are two C's related to E, listed here:
/// <summary> /// Automobile Presupposition Data /// </summary> public struct VehiclePrefabData : IComponentData { public Entity VehiclePrefab; public float VehicleSpeed; } /// <summary> /// Traffic Setup Data /// </summary> public struct TrafficSettingsData : IComponentData { public float GlobalSpeedFactor; public float PathSegments; public float MaxCars; }
It has to be said that using Data as a naming suffix is much more straightforward than Component, and that should be the case. I had Tucao before, ECS should be renamed EDS better, and DOTS also used D, indicating that D's description is more accurate. Maybe it's official to consider the architecture of Unity Component Development and want to continue this tradition. Since we need to change, why not be more thorough? Anyway, let's look at S:
/// <summary> /// Traffic system /// </summary> [AlwaysUpdateSystem]//Always update public sealed partial class TrafficSystem : JobComponentSystem { public NativeArray<RoadSection> roadSections;//Road section bool doneOneTimeInit = false;//Is one-time initialization complete? private ComponentGroup m_CarGroup;//Vehicle-related Component Group private int numSections = 0;//Segment number public TrafficSettingsData trafficSettings;//Traffic Setup Data public NativeArray<VehiclePrefabData> vehiclePool;//Vehicle buffer pool // This is not the best way for ECS to store the player, it would be better to have component data for it //This is not the best way for ECS to cache players, because it is the object of the game and feels the embarrassment of the transition period. private GameObject _player; /// <summary> /// The physical engine of ECS is still under development. The original physical system can only be used on GameObject. /// </summary> private Rigidbody _playerRidigbody; // Will only be on the player controlled car /// <summary> /// One-time settings /// </summary> void OneTimeSetup() { //Get all sections var allRoads = GetComponentGroup(typeof(RoadSection)).ToComponentDataArray<RoadSection>(Allocator.TempJob); //Get traffic settings var settings = GetComponentGroup(typeof(TrafficSettingsData)).ToComponentDataArray<TrafficSettingsData>(Allocator.TempJob); //Acquiring Vehicle Preset 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 //Duplicate the vehicle pool as a default and add the necessary components to it 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 temporarily copy all 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> /// Execute when creating a manager /// </summary> protected override void OnCreateManager() { base.OnCreateManager(); //Get all the physical state components of the vehicle to join the train group m_CarGroup = GetComponentGroup(ComponentType.ReadOnly<VehiclePhysicsState>()); //Generating vehicle congestion = Starting to simulate entity command caching system _SpawnBarrier = World.GetOrCreateManager<BeginSimulationEntityCommandBufferSystem>(); //Recovery Block = End of Simulated Entity Command Caching System _DespawnBarrier = World.GetOrCreateManager<EndSimulationEntityCommandBufferSystem>(); // TODO: Should size this dynamics should adopt dynamic expansion _Cells = new NativeMultiHashMap<int, VehicleCell>(30000, Allocator.Persistent); _VehicleMap = new NativeMultiHashMap<int, VehicleSlotData>(30000, Allocator.Persistent); } /// <summary> /// Update: per frame execution /// </summary> /// <param name="inputDeps">dependency </param> /// <returns></returns> protected override JobHandle OnUpdate(JobHandle inputDeps) { if (!doneOneTimeInit)//Set up the first entry { OneTimeSetup(); return inputDeps; } if (vehiclePool.Length == 0 || roadSections.Length == 0) return inputDeps; #if UNITY_EDITOR && USE_DEBUG_LINES var debugLines = _DebugLines.Lines.ToConcurrent(); #endif //Vehicles in the parking queue scenario move along the queue. This primitive array should be a fleet. The location of each vehicle in each fleet is called parking space. var queueSlots = new NativeArray<Occupation>(numSections * Constants.RoadIndexMultiplier, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); // Setup job dependencies//Set task dependencies //Clean up dependencies first JobHandle clearDeps = new ClearArrayJob<Occupation> { Data = queueSlots, }.Schedule(queueSlots.Length, 512); //Clean up hash var clearHash2Job = new ClearHashJob<VehicleSlotData> {Hash = _VehicleMap}.Schedule(); // Move vehicles along path, compute banking //Path Dependence: Computed Bend Moving Vehicles Along Path 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) //The vehicle completing the turn moves to the next bend (or downhill) JobHandle pathLinkDeps = new VehiclePathLinkUpdate { RoadSections = roadSections }.Schedule(this, pathingDeps); // Move from lane to lane. PERF: Opportunity to not do for every vehicle. //From lane to lane. Performance optimization: Not every car needs to be updated 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 //Units for calculating the coverage of each vehicle (16 units per section of road) 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: Fill in 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. //Avoid rear-end chasing and send signals when changing lanes are needed JobHandle moderatorDeps = new VehicleSpeedModerate { Occupancy = queueSlots, RoadSections = roadSections, DeltaTimeSeconds = Time.deltaTime}.Schedule(this, occupationGapDeps); // Pick concrete new lanes for cars switching lanes //Select specific new lanes when changing lanes JobHandle laneSwitchDeps = new LaneSwitch { Occupancy = queueSlots, RoadSections = roadSections}.Schedule(this, moderatorDeps); // Despawn cars that have run out of road //Recovery of vehicles running the entire distance 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//Generate 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 //Clear Cache 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; } } }
Traffic system is more complex. In the following summary, I will try to draw a picture to explain some concepts.
Summary
After drawing for half a day, the painting was unsatisfactory because it was too complicated to understand.
Update plan
Author's remarks
If you like my article, you can give me some praise and support. Thank you for your encouragement. If you have any questions, please leave me a message. If there are any mistakes or omissions, please criticize and testify.
If you have technical problems to discuss, you can join the Developer Alliance: 566189328 (pay group) to provide you with limited technical support, as well as, soul chicken soup!
Of course, you are welcome to join us without technical support. You can always invite me to coffee, tea and juice! (*)