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();
|
base.Awake();
|
||||||
_entitySync = new EntitySync(this);
|
_entitySync = new EntitySync(this);
|
||||||
_presentation = new Presentation(this);
|
_presentation = new Presentation(this);
|
||||||
|
InitializeJobDataChannels();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
|
|
@ -90,6 +91,7 @@ namespace Simulation
|
||||||
_entitySync?.OnDestroy();
|
_entitySync?.OnDestroy();
|
||||||
_entitySync = null;
|
_entitySync = null;
|
||||||
_presentation = null;
|
_presentation = null;
|
||||||
|
DisposeJobDataChannels();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LateUpdate()
|
private void LateUpdate()
|
||||||
|
|
@ -282,12 +284,10 @@ namespace Simulation
|
||||||
|
|
||||||
if (_useJobSimulation)
|
if (_useJobSimulation)
|
||||||
{
|
{
|
||||||
// Checkpoint 1: the switch is in place; Checkpoint 3+ will replace this with jobified path.
|
|
||||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
using (CustomProfilerMarker.TickEnemies.Auto())
|
||||||
{
|
{
|
||||||
TickEnemies(in context);
|
TickEnemiesJobified(in context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,6 +304,7 @@ namespace Simulation
|
||||||
_pickups.Clear();
|
_pickups.Clear();
|
||||||
_enemySeparationAgents.Clear();
|
_enemySeparationAgents.Clear();
|
||||||
_enemyTickWorkItems.Clear();
|
_enemyTickWorkItems.Clear();
|
||||||
|
ClearJobDataChannels();
|
||||||
|
|
||||||
EnemyBinding.Clear();
|
EnemyBinding.Clear();
|
||||||
ProjectileBinding.Clear();
|
ProjectileBinding.Clear();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ namespace Simulation.Tests.Editor
|
||||||
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
||||||
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo SetUseJobSimulationMethod =
|
||||||
|
SimulationWorldType?.GetMethod("SetUseJobSimulation", PublicInstance);
|
||||||
|
|
||||||
private static readonly MethodInfo UseGridBucketSolverMethod =
|
private static readonly MethodInfo UseGridBucketSolverMethod =
|
||||||
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
|
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
|
||||||
|
|
||||||
|
|
@ -65,6 +68,7 @@ namespace Simulation.Tests.Editor
|
||||||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||||
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement 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(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||||
Assert.NotNull(EnemiesProperty, "Enemies property 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));
|
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)
|
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
|
||||||
{
|
{
|
||||||
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ namespace Simulation.Tests.PlayMode
|
||||||
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
private static readonly MethodInfo SetUseSimulationMovementMethod =
|
||||||
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
SimulationWorldType?.GetMethod("SetUseSimulationMovement", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo SetUseJobSimulationMethod =
|
||||||
|
SimulationWorldType?.GetMethod("SetUseJobSimulation", PublicInstance);
|
||||||
|
|
||||||
private static readonly MethodInfo UseGridBucketSolverMethod =
|
private static readonly MethodInfo UseGridBucketSolverMethod =
|
||||||
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
|
EnemySeparationSolverProviderType?.GetMethod("UseGridBucketSolver", PublicStatic);
|
||||||
|
|
||||||
|
|
@ -67,6 +70,7 @@ namespace Simulation.Tests.PlayMode
|
||||||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||||
Assert.NotNull(SetUseSimulationMovementMethod, "SetUseSimulationMovement 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(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||||
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
||||||
|
|
||||||
|
|
@ -155,6 +159,24 @@ namespace Simulation.Tests.PlayMode
|
||||||
yield break;
|
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)
|
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange)
|
||||||
{
|
{
|
||||||
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
object enemy = System.Activator.CreateInstance(EnemySimDataType);
|
||||||
|
|
|
||||||
|
|
@ -123,13 +123,13 @@
|
||||||
- 约束:默认可一键回退到 P1.5 路径,避免全量切换导致定位困难。
|
- 约束:默认可一键回退到 P1.5 路径,避免全量切换导致定位困难。
|
||||||
- 完成标准:Editor/Development Build 均可编译运行;关闭开关时行为与 P1.5 一致。
|
- 完成标准:Editor/Development Build 均可编译运行;关闭开关时行为与 P1.5 一致。
|
||||||
|
|
||||||
- [ ] Checkpoint 2:Simulation 与 Job 数据通道打通(仅建通道,不改行为)
|
- [x] Checkpoint 2:Simulation 与 Job 数据通道打通(仅建通道,不改行为)
|
||||||
- 为敌人/投射物建立 Job 输入输出结构(纯数据,不含 `Transform`/托管引用)。
|
- 为敌人/投射物建立 Job 输入输出结构(纯数据,不含 `Transform`/托管引用)。
|
||||||
- 建立 `SimulationWorld -> NativeContainer -> SimulationWorld` 的拷贝与回写流程。
|
- 建立 `SimulationWorld -> NativeContainer -> SimulationWorld` 的拷贝与回写流程。
|
||||||
- 统一生命周期:`Allocator.Persistent` 分配、集中 `Dispose`,避免泄漏。
|
- 统一生命周期:`Allocator.Persistent` 分配、集中 `Dispose`,避免泄漏。
|
||||||
- 完成标准:战斗循环可稳定运行,且该通道持续帧无新增 GC Alloc 热点。
|
- 完成标准:战斗循环可稳定运行,且该通道持续帧无新增 GC Alloc 热点。
|
||||||
|
|
||||||
- [ ] Checkpoint 3:敌人移动与朝向 Job 化(第一优先)
|
- [x] Checkpoint 3:敌人移动与朝向 Job 化(第一优先)
|
||||||
- 将敌人移动、朝向更新迁移至 `IJobParallelFor`。
|
- 将敌人移动、朝向更新迁移至 `IJobParallelFor`。
|
||||||
- 输入最少包含:`position/forward/speed/targetPosition/deltaTime/state`。
|
- 输入最少包含:`position/forward/speed/targetPosition/deltaTime/state`。
|
||||||
- 输出最少包含:`nextPosition/nextForward/isMoving`。
|
- 输出最少包含:`nextPosition/nextForward/isMoving`。
|
||||||
|
|
@ -241,4 +241,4 @@
|
||||||
|
|
||||||
## 测试命令
|
## 测试命令
|
||||||
- PlayMode: `Unity -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log`
|
- PlayMode: `Unity -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log`
|
||||||
- EditMode: `Unity -batchmode -nographics -projectPath . -runTests -testPlatform EditMode -testResults Logs/editmode-test-results.xml -logFile Logs/editmode-tests.log`
|
- EditMode: `Unity -batchmode -nographics -projectPath . -runTests -testPlatform EditMode -testResults Logs/editmode-test-results.xml -logFile Logs/editmode-tests.log`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue