关于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的场景,或观看过演示视频,可能对我接下来要说的并不感冒,Whatever,我们还是继续今天的学习吧。
关于TrafficSystem,从游戏菜单就可以看出来,分为两种模式:
下面直接看代码,E:
///
/// 交通设置
///
public class TrafficSettings : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
///
/// 路段,把路径分成100个片段
///
public float pathSegments=100;
public float globalSpeedFactor = 1.0f;//全局速度参数
public int maxCars = 2000;//最大车量
public float[] speedMultipliers;//速度乘数数组
public List<GameObject> vehiclePrefabs;//车辆预设列表
///
/// 声明预设
///
/// 预设对象
public void DeclareReferencedPrefabs(List<GameObject> gameObjects)
{
for (int i = 0; i < vehiclePrefabs.Count; i++)
{
gameObjects.Add(vehiclePrefabs[i]);
}
}
///
/// 转化:把数据交给C储存
///
///
///
///
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,这里一起列出来:
///
/// 汽车预设数据
///
public struct VehiclePrefabData : IComponentData
{
public Entity VehiclePrefab;
public float VehicleSpeed;
}
///
/// 交通设置数据
///
public struct TrafficSettingsData : IComponentData
{
public float GlobalSpeedFactor;
public float PathSegments;
public float MaxCars;
}
不得不说,用Data作为命名后缀比Component直白多了,就应该这样才对。我之前就吐槽过了,ECS应该更名为EDS更好一些,而DOTS里面也用到了D,说明D的描述更准确。也许官方是考虑到Unity组件式开发的架构吧,想要继续保留这个传统。既然要改变,何不彻底一些?Anyway,还是看看S吧:
///
/// 交通系统
///
[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;
///
/// ECS的物理引擎还在开发中,原来的物理系统只能用在游戏对象(GameObject)上
///
private Rigidbody _playerRidigbody; // Will only be on the player controlled car
///
/// 一次性设置
///
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();
}
///
/// 在创建管理器时执行
///
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);
}
///
/// 更新:每帧执行
///
/// 依赖
///
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(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)