Checkpoint 2 & Checkpoint 3
Checkpoint 2:Simulation 与 Job 数据通道打通 - 新增 Job 数据通道(Persistent NativeList)与 Simulation <-> NativeContainer 拷贝/回写流程 - 在生命周期接入通道管理(初始化、清空、集中释放)并接入 UseJobSimulation 分支 - 增强通道健壮性:失效容器自动重建,避免 EditMode 下 ObjectDisposedException - 增加 UseJobSimulation 分支回归用例(EditMode/PlayMode 各 1 条) Checkpoint 3:敌人移动与朝向 Job 化 - UseJobSimulation 分支正式切到 Job 路径 - 新增敌人移动/朝向 Job 实现(IJobParallelFor): - Burst 版本 + 非 Burst 版本(受 UseBurstJobs 开关控制) - Job 计算移动、朝向、状态;主线程补做分离避让回补 - 扩展数据通道能力(为 Job 输出缓冲和投射物输出同步提供接口) - 补充分支回归测试(开启 UseJobSimulation 后验证追踪与朝向)
This commit is contained in:
parent
76ed9a3e53
commit
34b1424bb6
|
|
@ -0,0 +1,242 @@
|
|||
using CustomDebugger;
|
||||
using CustomUtility;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct EnemyMovementBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemyMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private struct EnemyMovementJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemyMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void TickEnemiesJobified(in SimulationTickContext context)
|
||||
{
|
||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||
{
|
||||
SyncSimulationToJobInput();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
ExecuteEnemyMovementJob(in context);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||
{
|
||||
ApplyEnemySeparationForJobOutput();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
SyncProjectilesToJobOutput();
|
||||
ApplyJobOutputToSimulation();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
PrepareEnemyJobOutputBuffer(enemyCount);
|
||||
|
||||
if (enemyCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyEnemyInputToOutput();
|
||||
return;
|
||||
}
|
||||
|
||||
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
|
||||
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
|
||||
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
|
||||
|
||||
JobHandle handle;
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
handle = burstJob.Schedule(enemyCount, 64);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnemyMovementJob job = new EnemyMovementJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
handle = job.Schedule(enemyCount, 64);
|
||||
}
|
||||
|
||||
handle.Complete();
|
||||
}
|
||||
|
||||
private void CopyEnemyInputToOutput()
|
||||
{
|
||||
for (int i = 0; i < _enemyJobInputs.Length; i++)
|
||||
{
|
||||
EnemyJobInputData input = _enemyJobInputs[i];
|
||||
_enemyJobOutputs[i] = new EnemyJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Rotation = input.Rotation,
|
||||
Speed = input.Speed,
|
||||
AttackRange = input.AttackRange,
|
||||
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = input.EnemyBodyRadius,
|
||||
SeparationIterations = input.SeparationIterations,
|
||||
TargetType = input.TargetType,
|
||||
State = input.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEnemySeparationForJobOutput()
|
||||
{
|
||||
if (_enemyJobOutputs.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_enemySeparationAgents.Clear();
|
||||
for (int i = 0; i < _enemyJobOutputs.Length; i++)
|
||||
{
|
||||
EnemyJobOutputData output = _enemyJobOutputs[i];
|
||||
if (!output.AvoidEnemyOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 position = output.Position;
|
||||
position.y = 0f;
|
||||
_enemySeparationAgents.Add(new EnemySeparationAgent
|
||||
{
|
||||
AgentId = output.EntityId,
|
||||
Position = position,
|
||||
Radius = output.EnemyBodyRadius > 0f ? output.EnemyBodyRadius : 0.45f
|
||||
});
|
||||
}
|
||||
|
||||
if (_enemySeparationAgents.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemySeparationSolverProvider.SetSimulationAgents(_enemySeparationAgents);
|
||||
for (int i = 0; i < _enemyJobOutputs.Length; i++)
|
||||
{
|
||||
EnemyJobOutputData output = _enemyJobOutputs[i];
|
||||
if (!output.AvoidEnemyOverlap || output.State != EnemyStateChasing)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 resolvedPosition = EnemySeparationSolverProvider.ResolveSimulation(
|
||||
output.EntityId,
|
||||
output.Position,
|
||||
output.Forward,
|
||||
output.SeparationIterations > 0 ? output.SeparationIterations : 1);
|
||||
|
||||
output.Position = resolvedPosition;
|
||||
_enemyJobOutputs[i] = output;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
|
||||
NativeArray<EnemyJobOutputData> outputs, float deltaTime, float3 playerPosition)
|
||||
{
|
||||
EnemyJobInputData input = inputs[index];
|
||||
float attackRange = input.AttackRange > 0f ? input.AttackRange : DefaultAttackRange;
|
||||
float attackRangeSqr = attackRange * attackRange;
|
||||
|
||||
float3 currentPosition = new float3(input.Position.x, input.Position.y, input.Position.z);
|
||||
float3 horizontalPosition = new float3(currentPosition.x, 0f, currentPosition.z);
|
||||
float3 toPlayer = playerPosition - horizontalPosition;
|
||||
float sqrDistance = math.lengthsq(toPlayer);
|
||||
bool isInAttackRange = sqrDistance <= attackRangeSqr;
|
||||
bool canChase = !isInAttackRange && input.Speed > 0f && sqrDistance > float.Epsilon;
|
||||
|
||||
float3 forward = new float3(input.Forward.x, input.Forward.y, input.Forward.z);
|
||||
float3 desiredPosition = currentPosition;
|
||||
quaternion rotation = new quaternion(input.Rotation.x, input.Rotation.y, input.Rotation.z, input.Rotation.w);
|
||||
|
||||
if (canChase)
|
||||
{
|
||||
forward = math.normalizesafe(toPlayer, forward);
|
||||
desiredPosition = currentPosition + forward * input.Speed * deltaTime;
|
||||
if (math.lengthsq(forward) > float.Epsilon)
|
||||
{
|
||||
rotation = quaternion.LookRotationSafe(forward, math.up());
|
||||
}
|
||||
}
|
||||
|
||||
int nextState;
|
||||
if (isInAttackRange)
|
||||
{
|
||||
nextState = EnemyStateInAttackRange;
|
||||
}
|
||||
else if (canChase)
|
||||
{
|
||||
nextState = EnemyStateChasing;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextState = EnemyStateIdle;
|
||||
}
|
||||
|
||||
outputs[index] = new EnemyJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
Position = new Vector3(desiredPosition.x, desiredPosition.y, desiredPosition.z),
|
||||
Forward = new Vector3(forward.x, forward.y, forward.z),
|
||||
Rotation = new Quaternion(rotation.value.x, rotation.value.y, rotation.value.z, rotation.value.w),
|
||||
Speed = input.Speed,
|
||||
AttackRange = attackRange,
|
||||
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = input.EnemyBodyRadius,
|
||||
SeparationIterations = input.SeparationIterations,
|
||||
TargetType = input.TargetType,
|
||||
State = nextState
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 99b885bb5cfe48e7bd9dba74fe683149
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
using System;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct EnemyJobInputData
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Quaternion Rotation;
|
||||
public float Speed;
|
||||
public float AttackRange;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public float EnemyBodyRadius;
|
||||
public int SeparationIterations;
|
||||
public int TargetType;
|
||||
public int State;
|
||||
}
|
||||
|
||||
private struct EnemyJobOutputData
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Quaternion Rotation;
|
||||
public float Speed;
|
||||
public float AttackRange;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public float EnemyBodyRadius;
|
||||
public int SeparationIterations;
|
||||
public int TargetType;
|
||||
public int State;
|
||||
}
|
||||
|
||||
private struct ProjectileJobInputData
|
||||
{
|
||||
public int EntityId;
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public float Speed;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
|
||||
private struct ProjectileJobOutputData
|
||||
{
|
||||
public int EntityId;
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public float Speed;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
|
||||
private NativeList<EnemyJobInputData> _enemyJobInputs;
|
||||
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
|
||||
private NativeList<ProjectileJobInputData> _projectileJobInputs;
|
||||
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
|
||||
|
||||
private void InitializeJobDataChannels()
|
||||
{
|
||||
if (AreJobDataChannelsUsable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisposeJobDataChannels();
|
||||
_enemyJobInputs = new NativeList<EnemyJobInputData>(64, Allocator.Persistent);
|
||||
_enemyJobOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
|
||||
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
|
||||
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
||||
}
|
||||
|
||||
private void DisposeJobDataChannels()
|
||||
{
|
||||
if (_enemyJobInputs.IsCreated)
|
||||
{
|
||||
_enemyJobInputs.Dispose();
|
||||
}
|
||||
_enemyJobInputs = default;
|
||||
|
||||
if (_enemyJobOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobOutputs.Dispose();
|
||||
}
|
||||
_enemyJobOutputs = default;
|
||||
|
||||
if (_projectileJobInputs.IsCreated)
|
||||
{
|
||||
_projectileJobInputs.Dispose();
|
||||
}
|
||||
_projectileJobInputs = default;
|
||||
|
||||
if (_projectileJobOutputs.IsCreated)
|
||||
{
|
||||
_projectileJobOutputs.Dispose();
|
||||
}
|
||||
_projectileJobOutputs = default;
|
||||
}
|
||||
|
||||
private void ClearJobDataChannels()
|
||||
{
|
||||
if (!AreJobDataChannelsUsable())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_enemyJobInputs.IsCreated)
|
||||
{
|
||||
_enemyJobInputs.Clear();
|
||||
}
|
||||
|
||||
if (_enemyJobOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobOutputs.Clear();
|
||||
}
|
||||
|
||||
if (_projectileJobInputs.IsCreated)
|
||||
{
|
||||
_projectileJobInputs.Clear();
|
||||
}
|
||||
|
||||
if (_projectileJobOutputs.IsCreated)
|
||||
{
|
||||
_projectileJobOutputs.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncSimulationToJobInput()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
|
||||
EnsureCapacity(ref _projectileJobInputs, _projectiles.Count);
|
||||
|
||||
_enemyJobInputs.Clear();
|
||||
_projectileJobInputs.Clear();
|
||||
|
||||
for (int i = 0; i < _enemies.Count; i++)
|
||||
{
|
||||
_enemyJobInputs.Add(ConvertToEnemyJobInput(_enemies[i]));
|
||||
}
|
||||
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
_projectileJobInputs.Add(ConvertToProjectileJobInput(_projectiles[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncSimulationToJobOutput()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobOutputs, _enemies.Count);
|
||||
EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count);
|
||||
|
||||
_enemyJobOutputs.Clear();
|
||||
_projectileJobOutputs.Clear();
|
||||
|
||||
for (int i = 0; i < _enemies.Count; i++)
|
||||
{
|
||||
_enemyJobOutputs.Add(ConvertToEnemyJobOutput(_enemies[i]));
|
||||
}
|
||||
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
_projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareEnemyJobOutputBuffer(int enemyCount)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobOutputs, enemyCount);
|
||||
_enemyJobOutputs.Clear();
|
||||
|
||||
if (enemyCount > 0)
|
||||
{
|
||||
_enemyJobOutputs.ResizeUninitialized(enemyCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncProjectilesToJobOutput()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count);
|
||||
_projectileJobOutputs.Clear();
|
||||
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
_projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyJobOutputToSimulation()
|
||||
{
|
||||
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
_enemies[i] = ConvertToEnemySimData(_enemyJobOutputs[i]);
|
||||
}
|
||||
|
||||
int projectileCount = Mathf.Min(_projectiles.Count, _projectileJobOutputs.Length);
|
||||
for (int i = 0; i < projectileCount; i++)
|
||||
{
|
||||
_projectiles[i] = ConvertToProjectileSimData(_projectileJobOutputs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private bool AreJobDataChannelsUsable()
|
||||
{
|
||||
return IsNativeListUsable(_enemyJobInputs) &&
|
||||
IsNativeListUsable(_enemyJobOutputs) &&
|
||||
IsNativeListUsable(_projectileJobInputs) &&
|
||||
IsNativeListUsable(_projectileJobOutputs);
|
||||
}
|
||||
|
||||
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
||||
{
|
||||
if (!nativeList.IsCreated || targetCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativeList.Capacity < targetCount)
|
||||
{
|
||||
nativeList.Capacity = targetCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsNativeListUsable<T>(NativeList<T> nativeList) where T : unmanaged
|
||||
{
|
||||
if (!nativeList.IsCreated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = nativeList.Length;
|
||||
return true;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static EnemyJobInputData ConvertToEnemyJobInput(in EnemySimData enemy)
|
||||
{
|
||||
return new EnemyJobInputData
|
||||
{
|
||||
EntityId = enemy.EntityId,
|
||||
Position = enemy.Position,
|
||||
Forward = enemy.Forward,
|
||||
Rotation = enemy.Rotation,
|
||||
Speed = enemy.Speed,
|
||||
AttackRange = enemy.AttackRange,
|
||||
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = enemy.EnemyBodyRadius,
|
||||
SeparationIterations = enemy.SeparationIterations,
|
||||
TargetType = enemy.TargetType,
|
||||
State = enemy.State
|
||||
};
|
||||
}
|
||||
|
||||
private static EnemyJobOutputData ConvertToEnemyJobOutput(in EnemySimData enemy)
|
||||
{
|
||||
return new EnemyJobOutputData
|
||||
{
|
||||
EntityId = enemy.EntityId,
|
||||
Position = enemy.Position,
|
||||
Forward = enemy.Forward,
|
||||
Rotation = enemy.Rotation,
|
||||
Speed = enemy.Speed,
|
||||
AttackRange = enemy.AttackRange,
|
||||
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = enemy.EnemyBodyRadius,
|
||||
SeparationIterations = enemy.SeparationIterations,
|
||||
TargetType = enemy.TargetType,
|
||||
State = enemy.State
|
||||
};
|
||||
}
|
||||
|
||||
private static EnemySimData ConvertToEnemySimData(in EnemyJobOutputData enemy)
|
||||
{
|
||||
return new EnemySimData
|
||||
{
|
||||
EntityId = enemy.EntityId,
|
||||
Position = enemy.Position,
|
||||
Forward = enemy.Forward,
|
||||
Rotation = enemy.Rotation,
|
||||
Speed = enemy.Speed,
|
||||
AttackRange = enemy.AttackRange,
|
||||
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = enemy.EnemyBodyRadius,
|
||||
SeparationIterations = enemy.SeparationIterations,
|
||||
TargetType = enemy.TargetType,
|
||||
State = enemy.State
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileJobInputData ConvertToProjectileJobInput(in ProjectileSimData projectile)
|
||||
{
|
||||
return new ProjectileJobInputData
|
||||
{
|
||||
EntityId = projectile.EntityId,
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = projectile.Position,
|
||||
Forward = projectile.Forward,
|
||||
Speed = projectile.Speed,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileJobOutputData ConvertToProjectileJobOutput(in ProjectileSimData projectile)
|
||||
{
|
||||
return new ProjectileJobOutputData
|
||||
{
|
||||
EntityId = projectile.EntityId,
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = projectile.Position,
|
||||
Forward = projectile.Forward,
|
||||
Speed = projectile.Speed,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileSimData ConvertToProjectileSimData(in ProjectileJobOutputData projectile)
|
||||
{
|
||||
return new ProjectileSimData
|
||||
{
|
||||
EntityId = projectile.EntityId,
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = projectile.Position,
|
||||
Forward = projectile.Forward,
|
||||
Speed = projectile.Speed,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 20926de0e6e14c7b818779593f1f87bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -78,6 +78,7 @@ namespace Simulation
|
|||
base.Awake();
|
||||
_entitySync = new EntitySync(this);
|
||||
_presentation = new Presentation(this);
|
||||
InitializeJobDataChannels();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
|
|
@ -90,6 +91,7 @@ namespace Simulation
|
|||
_entitySync?.OnDestroy();
|
||||
_entitySync = null;
|
||||
_presentation = null;
|
||||
DisposeJobDataChannels();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
|
|
@ -282,12 +284,10 @@ namespace Simulation
|
|||
|
||||
if (_useJobSimulation)
|
||||
{
|
||||
// Checkpoint 1: the switch is in place; Checkpoint 3+ will replace this with jobified path.
|
||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
||||
{
|
||||
TickEnemies(in context);
|
||||
TickEnemiesJobified(in context);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -304,6 +304,7 @@ namespace Simulation
|
|||
_pickups.Clear();
|
||||
_enemySeparationAgents.Clear();
|
||||
_enemyTickWorkItems.Clear();
|
||||
ClearJobDataChannels();
|
||||
|
||||
EnemyBinding.Clear();
|
||||
ProjectileBinding.Clear();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ namespace Simulation.Tests.Editor
|
|||
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
||||
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
||||
|
||||
private static readonly MethodInfo SetUseJobSimulationMethod =
|
||||
SimulationWorldType?.GetMethod("SetUseJobSimulation", PublicInstance);
|
||||
|
||||
private static readonly MethodInfo UseGridBucketSolverMethod =
|
||||
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
|
||||
|
||||
|
|
@ -65,6 +68,7 @@ namespace Simulation.Tests.Editor
|
|||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
|
||||
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
||||
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
||||
|
||||
|
|
@ -143,6 +147,23 @@ namespace Simulation.Tests.Editor
|
|||
Assert.That((int)GetField(GetEnemyAt(1), "EntityId"), Is.EqualTo(2003));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TickEnemies_ChasesPlayer_WhenJobSimulationChannelEnabled()
|
||||
{
|
||||
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||
UpsertEnemy(CreateEnemy(entityId: 1101, position: Vector3.zero, speed: 2f, attackRange: 1f));
|
||||
|
||||
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f));
|
||||
|
||||
object enemy = GetEnemyAt(0);
|
||||
Assert.That((int)GetField(enemy, "State"), Is.EqualTo(1));
|
||||
Vector3 position = (Vector3)GetField(enemy, "Position");
|
||||
Vector3 forward = (Vector3)GetField(enemy, "Forward");
|
||||
Assert.That(position.x, Is.EqualTo(2f).Within(0.0001f));
|
||||
Assert.That(position.z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(forward.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
}
|
||||
|
||||
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
|
||||
{
|
||||
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ namespace Simulation.Tests.PlayMode
|
|||
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
||||
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
||||
|
||||
private static readonly MethodInfo SetUseJobSimulationMethod =
|
||||
SimulationWorldType?.GetMethod("SetUseJobSimulation", PublicInstance);
|
||||
|
||||
private static readonly MethodInfo UseGridBucketSolverMethod =
|
||||
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
|
||||
|
||||
|
|
@ -67,6 +70,7 @@ namespace Simulation.Tests.PlayMode
|
|||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement reflection lookup failed.");
|
||||
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
||||
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
||||
|
||||
|
|
@ -155,6 +159,24 @@ namespace Simulation.Tests.PlayMode
|
|||
yield break;
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator TickEnemies_ChasesPlayer_WhenJobSimulationChannelEnabled()
|
||||
{
|
||||
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||
UpsertEnemy(CreateEnemy(entityId: 3201, position: Vector3.zero, speed: 2f, attackRange: 1f));
|
||||
|
||||
InvokeTick(deltaTime: 1f, realDeltaTime: 1f, playerPosition: new Vector3(10f, 0f, 0f));
|
||||
|
||||
object enemy = GetEnemyAt(0);
|
||||
Assert.That((int)GetField(enemy, "State"), Is.EqualTo(1));
|
||||
Vector3 position = (Vector3)GetField(enemy, "Position");
|
||||
Vector3 forward = (Vector3)GetField(enemy, "Forward");
|
||||
Assert.That(position.x, Is.EqualTo(2f).Within(0.0001f));
|
||||
Assert.That(position.z, Is.EqualTo(0f).Within(0.0001f));
|
||||
Assert.That(forward.x, Is.EqualTo(1f).Within(0.0001f));
|
||||
yield break;
|
||||
}
|
||||
|
||||
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
|
||||
{
|
||||
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
||||
|
|
|
|||
|
|
@ -123,13 +123,13 @@
|
|||
- 约束:默认可一键回退到 P1.5 路径,避免全量切换导致定位困难。
|
||||
- 完成标准:Editor/Development Build 均可编译运行;关闭开关时行为与 P1.5 一致。
|
||||
|
||||
- [ ] Checkpoint 2:Simulation 与 Job 数据通道打通(仅建通道,不改行为)
|
||||
- [x] Checkpoint 2:Simulation 与 Job 数据通道打通(仅建通道,不改行为)
|
||||
- 为敌人/投射物建立 Job 输入输出结构(纯数据,不含 `Transform`/托管引用)。
|
||||
- 建立 `SimulationWorld -> NativeContainer -> SimulationWorld` 的拷贝与回写流程。
|
||||
- 统一生命周期:`Allocator.Persistent` 分配、集中 `Dispose`,避免泄漏。
|
||||
- 完成标准:战斗循环可稳定运行,且该通道持续帧无新增 GC Alloc 热点。
|
||||
|
||||
- [ ] Checkpoint 3:敌人移动与朝向 Job 化(第一优先)
|
||||
- [x] Checkpoint 3:敌人移动与朝向 Job 化(第一优先)
|
||||
- 将敌人移动、朝向更新迁移至 `IJobParallelFor`。
|
||||
- 输入最少包含:`position/forward/speed/targetPosition/deltaTime/state`。
|
||||
- 输出最少包含:`nextPosition/nextForward/isMoving`。
|
||||
|
|
|
|||
Loading…
Reference in New Issue