简单重构拆分 SimulationWorld
This commit is contained in:
+215
@@ -0,0 +1,215 @@
|
||||
using SepCore.Debugger;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace SepCore.Simulation
|
||||
{
|
||||
internal static class CollisionQuerySystem
|
||||
{
|
||||
// 读写集:R EnemyTransform / EnemyMoveConfig / EnemyIdentity / CollisionQueryData(瞬时)
|
||||
// W CollisionCandidateData(瞬时) / 敌人碰撞分桶
|
||||
public static void BuildEnemyCollisionBuckets(EnemyStore enemies,
|
||||
NativeParallelMultiHashMap<long, int> buckets, float cellSize)
|
||||
{
|
||||
buckets.Clear();
|
||||
int enemyCount = enemies.Count;
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BuildCollisionBucketsJob job = new BuildCollisionBucketsJob
|
||||
{
|
||||
Transforms = enemies.Transform.Data.AsArray(),
|
||||
Buckets = buckets.AsParallelWriter(),
|
||||
CellSize = cellSize
|
||||
};
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
|
||||
{
|
||||
job.Schedule(enemyCount, 64).Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ScheduleCandidateQuery(NativeList<CollisionQueryData> queries,
|
||||
NativeList<CollisionCandidateData> candidates, NativeParallelMultiHashMap<long, int> enemyBuckets,
|
||||
EnemyStore enemies, bool hasEnemyTargets, bool hasPlayerTarget, int playerTargetEntityId,
|
||||
float3 playerPosition, float cellSize, out JobHandle handle)
|
||||
{
|
||||
handle = default;
|
||||
if (!queries.IsCreated || !candidates.IsCreated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
candidates.Clear();
|
||||
int queryCount = queries.Length;
|
||||
if (queryCount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QueryCollisionCandidatesJob job = new QueryCollisionCandidatesJob
|
||||
{
|
||||
Queries = queries.AsArray(),
|
||||
EnemyBuckets = enemyBuckets,
|
||||
EnemyTransforms = enemies.Transform.Data.AsArray(),
|
||||
EnemyConfigs = enemies.MoveConfig.Data.AsArray(),
|
||||
EnemyIdentities = enemies.Identity.Data.AsArray(),
|
||||
Candidates = candidates.AsParallelWriter(),
|
||||
HasEnemyTargets = hasEnemyTargets,
|
||||
HasPlayerTarget = hasPlayerTarget,
|
||||
PlayerTargetEntityId = playerTargetEntityId,
|
||||
PlayerPosition = playerPosition,
|
||||
CellSize = cellSize
|
||||
};
|
||||
|
||||
handle = job.Schedule(queryCount, 64);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct BuildCollisionBucketsJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyTransformSim> Transforms;
|
||||
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
|
||||
public float CellSize;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
float3 position = Transforms[index].Position;
|
||||
int cellX = SimulationCell.ToCell(position.x, CellSize);
|
||||
int cellZ = SimulationCell.ToCell(position.z, CellSize);
|
||||
Buckets.Add(SimulationCell.Key(cellX, cellZ), index);
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct QueryCollisionCandidatesJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<CollisionQueryData> Queries;
|
||||
[ReadOnly] public NativeParallelMultiHashMap<long, int> EnemyBuckets;
|
||||
[ReadOnly] public NativeArray<EnemyTransformSim> EnemyTransforms;
|
||||
[ReadOnly] public NativeArray<EnemyMoveConfigSim> EnemyConfigs;
|
||||
[ReadOnly] public NativeArray<EnemyIdentitySim> EnemyIdentities;
|
||||
public NativeList<CollisionCandidateData>.ParallelWriter Candidates;
|
||||
public bool HasEnemyTargets;
|
||||
public bool HasPlayerTarget;
|
||||
public int PlayerTargetEntityId;
|
||||
public float3 PlayerPosition;
|
||||
public float CellSize;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
CollisionQueryData query = Queries[index];
|
||||
int centerCellX = SimulationCell.ToCell(query.Position.x, CellSize);
|
||||
int centerCellZ = SimulationCell.ToCell(query.Position.z, CellSize);
|
||||
int queryRange = math.max(1, (int)math.ceil(query.Radius / CellSize));
|
||||
int selectedCount = 0;
|
||||
bool reachedLimit = false;
|
||||
|
||||
if (HasPlayerTarget && query.SourceEntityId != PlayerTargetEntityId &&
|
||||
query.SourceOwnerEntityId != PlayerTargetEntityId)
|
||||
{
|
||||
float3 playerPosition = PlayerPosition;
|
||||
if (query.SourceType == SimulationCollision.SourceTypeArea)
|
||||
{
|
||||
playerPosition.y = query.Position.y;
|
||||
}
|
||||
|
||||
float3 playerDelta = playerPosition - query.Position;
|
||||
float playerSqrDistance = math.lengthsq(playerDelta);
|
||||
float playerRadiusSqr = query.Radius * query.Radius;
|
||||
if (playerSqrDistance <= playerRadiusSqr)
|
||||
{
|
||||
Candidates.AddNoResize(new CollisionCandidateData
|
||||
{
|
||||
QueryId = query.QueryId,
|
||||
SourceType = query.SourceType,
|
||||
SourceEntityId = query.SourceEntityId,
|
||||
SourceOwnerEntityId = query.SourceOwnerEntityId,
|
||||
TargetEntityId = PlayerTargetEntityId,
|
||||
SqrDistance = playerSqrDistance
|
||||
});
|
||||
|
||||
selectedCount++;
|
||||
if (selectedCount >= query.MaxTargets)
|
||||
{
|
||||
reachedLimit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!HasEnemyTargets || reachedLimit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int dx = -queryRange; dx <= queryRange && !reachedLimit; dx++)
|
||||
{
|
||||
for (int dz = -queryRange; dz <= queryRange && !reachedLimit; dz++)
|
||||
{
|
||||
long key = SimulationCell.Key(centerCellX + dx, centerCellZ + dz);
|
||||
if (!EnemyBuckets.TryGetFirstValue(key, out int enemyIndex,
|
||||
out NativeParallelMultiHashMapIterator<long> iterator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (enemyIndex < 0 || enemyIndex >= EnemyTransforms.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyIdentitySim identity = EnemyIdentities[enemyIndex];
|
||||
if (identity.EntityId == query.SourceOwnerEntityId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyTransformSim enemyTransform = EnemyTransforms[enemyIndex];
|
||||
float deltaY = query.SourceType == SimulationCollision.SourceTypeArea
|
||||
? 0f
|
||||
: enemyTransform.Position.y - query.Position.y;
|
||||
float3 delta = new float3(
|
||||
enemyTransform.Position.x - query.Position.x,
|
||||
deltaY,
|
||||
enemyTransform.Position.z - query.Position.z);
|
||||
float sqrDistance = math.lengthsq(delta);
|
||||
float targetRadius = query.SourceType == SimulationCollision.SourceTypeArea
|
||||
? math.max(0f, EnemyConfigs[enemyIndex].BodyRadius)
|
||||
: 0f;
|
||||
float effectiveRadius = query.Radius + targetRadius;
|
||||
if (sqrDistance > effectiveRadius * effectiveRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Candidates.AddNoResize(new CollisionCandidateData
|
||||
{
|
||||
QueryId = query.QueryId,
|
||||
SourceType = query.SourceType,
|
||||
SourceEntityId = query.SourceEntityId,
|
||||
SourceOwnerEntityId = query.SourceOwnerEntityId,
|
||||
TargetEntityId = identity.EntityId,
|
||||
SqrDistance = sqrDistance
|
||||
});
|
||||
|
||||
selectedCount++;
|
||||
|
||||
if (selectedCount >= query.MaxTargets)
|
||||
{
|
||||
reachedLimit = true;
|
||||
break;
|
||||
}
|
||||
} while (EnemyBuckets.TryGetNextValue(out enemyIndex, ref iterator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d4cc79b0ecd0c248ac2028cd928dd7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace SepCore.Simulation
|
||||
{
|
||||
internal static class EnemyMovementSystem
|
||||
{
|
||||
// 读写集:R EnemyTransform / EnemyMoveInput / EnemyAttack;W EnemyTransform / EnemyBehavior
|
||||
public static JobHandle Schedule(EnemyStore enemies, float deltaTime, float3 playerPosition,
|
||||
JobHandle dependency)
|
||||
{
|
||||
int enemyCount = enemies.Count;
|
||||
if (enemyCount == 0)
|
||||
{
|
||||
return dependency;
|
||||
}
|
||||
|
||||
EnemyMovementJob job = new EnemyMovementJob
|
||||
{
|
||||
Transforms = enemies.Transform.Data.AsArray(),
|
||||
MoveInputs = enemies.MoveInput.Data.AsArray(),
|
||||
Attacks = enemies.Attack.Data.AsArray(),
|
||||
Behaviors = enemies.Behavior.Data.AsArray(),
|
||||
DeltaTime = deltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
return job.Schedule(enemyCount, 64, dependency);
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct EnemyMovementJob : IJobParallelFor
|
||||
{
|
||||
public NativeArray<EnemyTransformSim> Transforms;
|
||||
[ReadOnly] public NativeArray<EnemyMoveInputSim> MoveInputs;
|
||||
[ReadOnly] public NativeArray<EnemyAttackSim> Attacks;
|
||||
public NativeArray<EnemyBehaviorSim> Behaviors;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
EnemyMoveInputSim moveInput = MoveInputs[index];
|
||||
EnemyAttackSim attack = Attacks[index];
|
||||
EnemyTransformSim transform = Transforms[index];
|
||||
|
||||
float attackRange = attack.AttackRange > 0f ? attack.AttackRange : EnemyAttackSim.DefaultAttackRange;
|
||||
float attackRangeSqr = attackRange * attackRange;
|
||||
|
||||
float3 currentPosition = transform.Position;
|
||||
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 && moveInput.Speed > 0f && sqrDistance > float.Epsilon;
|
||||
|
||||
float3 forward = transform.Forward;
|
||||
float3 desiredPosition = currentPosition;
|
||||
quaternion rotation = transform.Rotation;
|
||||
|
||||
if (canChase)
|
||||
{
|
||||
forward = math.normalizesafe(toPlayer, forward);
|
||||
desiredPosition = currentPosition + forward * moveInput.Speed * DeltaTime;
|
||||
if (math.lengthsq(forward) > float.Epsilon)
|
||||
{
|
||||
rotation = quaternion.LookRotationSafe(forward, math.up());
|
||||
}
|
||||
}
|
||||
|
||||
int nextState;
|
||||
if (isInAttackRange)
|
||||
{
|
||||
nextState = EnemyBehaviorSim.StateInAttackRange;
|
||||
}
|
||||
else if (canChase)
|
||||
{
|
||||
nextState = EnemyBehaviorSim.StateChasing;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextState = EnemyBehaviorSim.StateIdle;
|
||||
}
|
||||
|
||||
transform.Position = desiredPosition;
|
||||
transform.Forward = forward;
|
||||
transform.Rotation = rotation;
|
||||
Transforms[index] = transform;
|
||||
Behaviors[index] = new EnemyBehaviorSim { State = nextState };
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53c87038b72cb9c4bb51d952dabd982e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace SepCore.Simulation
|
||||
{
|
||||
internal readonly struct EnemySeparationSettings
|
||||
{
|
||||
public EnemySeparationSettings(float cellSize, float maxRadius, float pushDamping, float maxStepScale,
|
||||
bool useTangentialInAttackRange, float pushSmoothing)
|
||||
{
|
||||
CellSize = cellSize;
|
||||
MaxRadius = maxRadius;
|
||||
PushDamping = pushDamping;
|
||||
MaxStepScale = maxStepScale;
|
||||
UseTangentialInAttackRange = useTangentialInAttackRange;
|
||||
PushSmoothing = pushSmoothing;
|
||||
}
|
||||
|
||||
public float CellSize { get; }
|
||||
public float MaxRadius { get; }
|
||||
public float PushDamping { get; }
|
||||
public float MaxStepScale { get; }
|
||||
public bool UseTangentialInAttackRange { get; }
|
||||
public float PushSmoothing { get; }
|
||||
}
|
||||
|
||||
internal static class EnemySeparationSystem
|
||||
{
|
||||
// 读写集:R EnemyTransform / EnemyMoveConfig / EnemyBehavior / EnemySeparation(PreviousPush)
|
||||
// W EnemyTransform(经分离 scratch 由 Commit 写回)/ EnemySeparation(PreviousPush)
|
||||
public static JobHandle Schedule(EnemyStore enemies, NativeParallelMultiHashMap<long, int> buckets,
|
||||
NativeList<EnemyTransformSim> separatedTransforms, in EnemySeparationSettings settings,
|
||||
float3 playerPosition, JobHandle dependency)
|
||||
{
|
||||
int enemyCount = enemies.Count;
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
return dependency;
|
||||
}
|
||||
|
||||
NativeArray<EnemyTransformSim> transformArray = enemies.Transform.Data.AsArray();
|
||||
NativeArray<EnemyMoveConfigSim> configArray = enemies.MoveConfig.Data.AsArray();
|
||||
NativeArray<EnemyBehaviorSim> behaviorArray = enemies.Behavior.Data.AsArray();
|
||||
NativeArray<EnemyTransformSim> separatedTransformArray = separatedTransforms.AsArray();
|
||||
NativeArray<EnemySeparationSim> separationArray = enemies.Separation.Data.AsArray();
|
||||
|
||||
BuildEnemySeparationBucketsJob buildJob = new BuildEnemySeparationBucketsJob
|
||||
{
|
||||
Transforms = transformArray,
|
||||
MoveConfigs = configArray,
|
||||
Buckets = buckets.AsParallelWriter(),
|
||||
CellSize = settings.CellSize
|
||||
};
|
||||
JobHandle buildHandle = buildJob.Schedule(enemyCount, 64, dependency);
|
||||
EnemySeparationJob separationJob = new EnemySeparationJob
|
||||
{
|
||||
Transforms = transformArray,
|
||||
MoveConfigs = configArray,
|
||||
Behaviors = behaviorArray,
|
||||
Buckets = buckets,
|
||||
SeparatedTransforms = separatedTransformArray,
|
||||
Separations = separationArray,
|
||||
CellSize = settings.CellSize,
|
||||
MaxRadius = settings.MaxRadius,
|
||||
PlayerPosition = playerPosition,
|
||||
PushDamping = settings.PushDamping,
|
||||
MaxStepScale = settings.MaxStepScale,
|
||||
UseTangentialInAttackRange = settings.UseTangentialInAttackRange,
|
||||
PushSmoothing = settings.PushSmoothing
|
||||
};
|
||||
return separationJob.Schedule(enemyCount, 64, buildHandle);
|
||||
}
|
||||
|
||||
public static void Commit(EnemyStore enemies, NativeList<EnemyTransformSim> separatedTransforms,
|
||||
int enemyCount)
|
||||
{
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
enemies.Transform[i] = separatedTransforms[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct BuildEnemySeparationBucketsJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyTransformSim> Transforms;
|
||||
[ReadOnly] public NativeArray<EnemyMoveConfigSim> MoveConfigs;
|
||||
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
|
||||
public float CellSize;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
if (!MoveConfigs[index].AvoidOverlap)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float3 position = Transforms[index].Position;
|
||||
position.y = 0f;
|
||||
int cellX = SimulationCell.ToCell(position.x, CellSize);
|
||||
int cellZ = SimulationCell.ToCell(position.z, CellSize);
|
||||
Buckets.Add(SimulationCell.Key(cellX, cellZ), index);
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct EnemySeparationJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyTransformSim> Transforms;
|
||||
[ReadOnly] public NativeArray<EnemyMoveConfigSim> MoveConfigs;
|
||||
[ReadOnly] public NativeArray<EnemyBehaviorSim> Behaviors;
|
||||
[ReadOnly] public NativeParallelMultiHashMap<long, int> Buckets;
|
||||
public NativeArray<EnemyTransformSim> SeparatedTransforms;
|
||||
public NativeArray<EnemySeparationSim> Separations;
|
||||
public float CellSize;
|
||||
public float MaxRadius;
|
||||
public float3 PlayerPosition;
|
||||
public float PushDamping;
|
||||
public float MaxStepScale;
|
||||
public bool UseTangentialInAttackRange;
|
||||
public float PushSmoothing;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
EnemyMoveConfigSim selfConfig = MoveConfigs[index];
|
||||
if (!selfConfig.AvoidOverlap)
|
||||
{
|
||||
SeparatedTransforms[index] = Transforms[index];
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyTransformSim self = Transforms[index];
|
||||
float3 candidate = self.Position;
|
||||
candidate.y = 0f;
|
||||
float3 original = candidate;
|
||||
float3 fallback =
|
||||
math.normalizesafe(new float3(self.Forward.x, 0f, self.Forward.z), new float3(1f, 0f, 0f));
|
||||
float selfRadius = selfConfig.BodyRadius > 0f ? selfConfig.BodyRadius : 0.45f;
|
||||
int iterations = selfConfig.SeparationIterations > 0 ? selfConfig.SeparationIterations : 1;
|
||||
int queryRange = math.max(1, (int)math.ceil((selfRadius + MaxRadius) / CellSize));
|
||||
|
||||
for (int iter = 0; iter < iterations; iter++)
|
||||
{
|
||||
int cellX = SimulationCell.ToCell(candidate.x, CellSize);
|
||||
int cellZ = SimulationCell.ToCell(candidate.z, CellSize);
|
||||
float3 pushAccumulation = float3.zero;
|
||||
|
||||
for (int dx = -queryRange; dx <= queryRange; dx++)
|
||||
{
|
||||
for (int dz = -queryRange; dz <= queryRange; dz++)
|
||||
{
|
||||
long key = SimulationCell.Key(cellX + dx, cellZ + dz);
|
||||
if (!Buckets.TryGetFirstValue(key, out int otherIndex,
|
||||
out NativeParallelMultiHashMapIterator<long> iterator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (otherIndex == index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyMoveConfigSim otherConfig = MoveConfigs[otherIndex];
|
||||
if (!otherConfig.AvoidOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float otherRadius = otherConfig.BodyRadius > 0f ? otherConfig.BodyRadius : 0.45f;
|
||||
float minDistance = selfRadius + otherRadius;
|
||||
float minDistanceSqr = minDistance * minDistance;
|
||||
|
||||
float3 otherPosition = Transforms[otherIndex].Position;
|
||||
otherPosition.y = 0f;
|
||||
float3 toSelf = candidate - otherPosition;
|
||||
float sqrDistance = math.lengthsq(toSelf);
|
||||
|
||||
if (sqrDistance <= float.Epsilon)
|
||||
{
|
||||
float3 zeroDistanceAxis = GetZeroDistanceSeparationAxis(index, otherIndex);
|
||||
float directionSign = index < otherIndex ? 1f : -1f;
|
||||
pushAccumulation += zeroDistanceAxis * (selfRadius * 0.25f * directionSign);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sqrDistance >= minDistanceSqr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distance = math.sqrt(sqrDistance);
|
||||
float penetration = minDistance - distance;
|
||||
pushAccumulation += (toSelf / distance) * penetration;
|
||||
} while (Buckets.TryGetNextValue(out otherIndex, ref iterator));
|
||||
}
|
||||
}
|
||||
|
||||
if (math.lengthsq(pushAccumulation) <= float.Epsilon)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float3 resolvedPush = pushAccumulation * PushDamping;
|
||||
|
||||
float maxStep = selfRadius * MaxStepScale;
|
||||
float pushLength = math.length(resolvedPush);
|
||||
if (pushLength > maxStep && pushLength > float.Epsilon)
|
||||
{
|
||||
resolvedPush = resolvedPush / pushLength * maxStep;
|
||||
}
|
||||
|
||||
candidate += resolvedPush;
|
||||
}
|
||||
|
||||
float3 framePush = candidate - original;
|
||||
float2 previousPush2 = Separations[index].PreviousPush;
|
||||
float3 previousPush = new float3(previousPush2.x, 0f, previousPush2.y);
|
||||
float3 smoothedPush = SmoothSeparationPush(framePush, previousPush, PushSmoothing);
|
||||
|
||||
if (UseTangentialInAttackRange && Behaviors[index].State == EnemyBehaviorSim.StateInAttackRange)
|
||||
{
|
||||
smoothedPush = ProjectToTangential(smoothedPush, PlayerPosition, original);
|
||||
}
|
||||
|
||||
float maxTotalStep = selfRadius * MaxStepScale * iterations;
|
||||
float smoothedLength = math.length(smoothedPush);
|
||||
if (smoothedLength > maxTotalStep && smoothedLength > float.Epsilon)
|
||||
{
|
||||
smoothedPush = smoothedPush / smoothedLength * maxTotalStep;
|
||||
}
|
||||
|
||||
float3 finalPosition = original + smoothedPush;
|
||||
Separations[index] = new EnemySeparationSim { PreviousPush = new float2(smoothedPush.x, smoothedPush.z) };
|
||||
self.Position = new float3(finalPosition.x, self.Position.y, finalPosition.z);
|
||||
if (math.lengthsq(smoothedPush) > float.Epsilon)
|
||||
{
|
||||
self.Forward = new float3(fallback.x, self.Forward.y, fallback.z);
|
||||
}
|
||||
|
||||
SeparatedTransforms[index] = self;
|
||||
}
|
||||
|
||||
private static float3 SmoothSeparationPush(float3 framePush, float3 previousPush, float pushSmoothing)
|
||||
{
|
||||
float frameLengthSqr = math.lengthsq(framePush);
|
||||
float previousLengthSqr = math.lengthsq(previousPush);
|
||||
|
||||
if (frameLengthSqr <= float.Epsilon)
|
||||
{
|
||||
return float3.zero;
|
||||
}
|
||||
|
||||
if (previousLengthSqr <= float.Epsilon || pushSmoothing <= 0f)
|
||||
{
|
||||
return framePush;
|
||||
}
|
||||
|
||||
float frameLength = math.sqrt(frameLengthSqr);
|
||||
float previousLength = math.sqrt(previousLengthSqr);
|
||||
float3 frameDirection = framePush / frameLength;
|
||||
float3 previousDirection = previousPush / previousLength;
|
||||
float directionAlignment = math.dot(frameDirection, previousDirection);
|
||||
|
||||
if (directionAlignment >= 0.35f)
|
||||
{
|
||||
return framePush;
|
||||
}
|
||||
|
||||
float directionalFactor = math.saturate((0.35f - directionAlignment) / 1.35f);
|
||||
float smoothingStrength = pushSmoothing * directionalFactor;
|
||||
return math.lerp(framePush, previousPush, smoothingStrength);
|
||||
}
|
||||
|
||||
private static float3 ProjectToTangential(float3 push, float3 playerPosition, float3 currentPosition)
|
||||
{
|
||||
if (math.lengthsq(push) <= float.Epsilon)
|
||||
{
|
||||
return push;
|
||||
}
|
||||
|
||||
float3 toPlayer = playerPosition - currentPosition;
|
||||
float toPlayerSqr = math.lengthsq(toPlayer);
|
||||
if (toPlayerSqr <= float.Epsilon)
|
||||
{
|
||||
return push;
|
||||
}
|
||||
|
||||
float3 radialDirection = toPlayer / math.sqrt(toPlayerSqr);
|
||||
float radialOffset = math.dot(push, radialDirection);
|
||||
return push - radialDirection * radialOffset;
|
||||
}
|
||||
|
||||
private static float3 GetZeroDistanceSeparationAxis(int index, int otherIndex)
|
||||
{
|
||||
int lowIndex = math.min(index, otherIndex);
|
||||
int highIndex = math.max(index, otherIndex);
|
||||
uint pairHash = (uint)(lowIndex * 73856093) ^ (uint)(highIndex * 19349663);
|
||||
|
||||
float axisX = (pairHash & 1023u) / 511.5f - 1f;
|
||||
float axisZ = ((pairHash >> 10) & 1023u) / 511.5f - 1f;
|
||||
float3 axis = new float3(axisX, 0f, axisZ);
|
||||
return math.normalizesafe(axis, new float3(1f, 0f, 0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f125a6671430b4a45b0447eb4e6541af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace SepCore.Simulation
|
||||
{
|
||||
internal static class PickupMovementSystem
|
||||
{
|
||||
// 读写集:R/W PickupTransform / PickupState; R PickupIdentity / PickupRadius
|
||||
// W CollectedPickups
|
||||
public static JobHandle Schedule(PickupStore pickups, NativeList<int> collectedPickups,
|
||||
float deltaTime, float3 playerPosition, float absorbRange, float absorbSpeed, float collectDistance,
|
||||
JobHandle dependency)
|
||||
{
|
||||
int pickupCount = pickups.Count;
|
||||
if (pickupCount == 0)
|
||||
{
|
||||
return dependency;
|
||||
}
|
||||
|
||||
PickupMovementJob job = new PickupMovementJob
|
||||
{
|
||||
Transforms = pickups.Transform.Data.AsArray(),
|
||||
States = pickups.State.Data.AsArray(),
|
||||
Identities = pickups.Identity.Data.AsArray(),
|
||||
Radii = pickups.Radius.Data.AsArray(),
|
||||
CollectedPickups = collectedPickups.AsParallelWriter(),
|
||||
PlayerPosition = playerPosition,
|
||||
AbsorbRange = math.max(0f, absorbRange),
|
||||
AbsorbSpeed = math.max(0f, absorbSpeed),
|
||||
CollectDistance = math.max(0f, collectDistance),
|
||||
DeltaTime = deltaTime
|
||||
};
|
||||
return job.Schedule(pickupCount, 64, dependency);
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct PickupMovementJob : IJobParallelFor
|
||||
{
|
||||
public NativeArray<PickupTransformSim> Transforms;
|
||||
public NativeArray<PickupStateSim> States;
|
||||
[ReadOnly] public NativeArray<PickupIdentitySim> Identities;
|
||||
[ReadOnly] public NativeArray<PickupRadiusSim> Radii;
|
||||
public NativeList<int>.ParallelWriter CollectedPickups;
|
||||
public float3 PlayerPosition;
|
||||
public float AbsorbRange;
|
||||
public float AbsorbSpeed;
|
||||
public float CollectDistance;
|
||||
public float DeltaTime;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
PickupTransformSim transform = Transforms[index];
|
||||
PickupStateSim state = States[index];
|
||||
PickupIdentitySim identity = Identities[index];
|
||||
PickupRadiusSim radius = Radii[index];
|
||||
|
||||
float3 currentPosition = transform.Position;
|
||||
float3 horizontalPosition = new float3(currentPosition.x, 0f, currentPosition.z);
|
||||
float3 toPlayer = PlayerPosition - horizontalPosition;
|
||||
float sqrDistance = math.lengthsq(toPlayer);
|
||||
|
||||
float effectiveCollectDistance = math.max(CollectDistance, radius.Radius);
|
||||
float sqrCollectDistance = effectiveCollectDistance * effectiveCollectDistance;
|
||||
|
||||
if (sqrDistance <= sqrCollectDistance)
|
||||
{
|
||||
CollectedPickups.AddNoResize(identity.EntityId);
|
||||
return;
|
||||
}
|
||||
|
||||
bool isAbsorbing = state.State == PickupStateSim.StateAbsorbing;
|
||||
bool shouldAbsorb = isAbsorbing || (AbsorbRange > 0f && sqrDistance <= AbsorbRange * AbsorbRange);
|
||||
|
||||
if (shouldAbsorb && AbsorbSpeed > 0f && sqrDistance > float.Epsilon)
|
||||
{
|
||||
state.State = PickupStateSim.StateAbsorbing;
|
||||
float distance = math.sqrt(sqrDistance);
|
||||
float step = AbsorbSpeed * DeltaTime;
|
||||
|
||||
if (step >= distance)
|
||||
{
|
||||
transform.Position = new float3(PlayerPosition.x, currentPosition.y, PlayerPosition.z);
|
||||
CollectedPickups.AddNoResize(identity.EntityId);
|
||||
}
|
||||
else
|
||||
{
|
||||
float3 direction = toPlayer / distance;
|
||||
transform.Position = currentPosition + direction * step;
|
||||
}
|
||||
|
||||
Transforms[index] = transform;
|
||||
States[index] = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb91b00a6bb60d6429df916850580597
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace SepCore.Simulation
|
||||
{
|
||||
internal static class ProjectileMovementSystem
|
||||
{
|
||||
// 读写集:R/W ProjectileTransform / ProjectileVelocity / ProjectileLife
|
||||
public static JobHandle Schedule(ProjectileStore projectiles, float deltaTime, float3 playerPosition,
|
||||
float maxDistanceFromPlayer, float maxVerticalOffsetFromPlayer, JobHandle dependency)
|
||||
{
|
||||
int projectileCount = projectiles.Count;
|
||||
if (projectileCount == 0)
|
||||
{
|
||||
return dependency;
|
||||
}
|
||||
|
||||
float maxDistance = math.max(0f, maxDistanceFromPlayer);
|
||||
float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f;
|
||||
float maxVerticalOffset = math.max(0f, maxVerticalOffsetFromPlayer);
|
||||
|
||||
ProjectileMovementJob job = new ProjectileMovementJob
|
||||
{
|
||||
Transforms = projectiles.Transform.Data.AsArray(),
|
||||
Velocities = projectiles.Velocity.Data.AsArray(),
|
||||
Lives = projectiles.Life.Data.AsArray(),
|
||||
DeltaTime = deltaTime,
|
||||
PlayerPosition = playerPosition,
|
||||
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer = maxVerticalOffset
|
||||
};
|
||||
return job.Schedule(projectileCount, 64, dependency);
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
internal struct ProjectileMovementJob : IJobParallelFor
|
||||
{
|
||||
public NativeArray<ProjectileTransformSim> Transforms;
|
||||
public NativeArray<ProjectileVelocitySim> Velocities;
|
||||
public NativeArray<ProjectileLifeSim> Lives;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
public float MaxSqrDistanceFromPlayer;
|
||||
public float MaxVerticalOffsetFromPlayer;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ProjectileLifeSim life = Lives[index];
|
||||
if (!life.Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProjectileTransformSim transform = Transforms[index];
|
||||
float3 velocity = Velocities[index].Velocity;
|
||||
float3 nextPosition = transform.Position + velocity * DeltaTime;
|
||||
float nextAge = math.max(0f, life.Age + DeltaTime);
|
||||
bool shouldExpire = life.LifeTime > 0f && nextAge >= life.LifeTime;
|
||||
|
||||
if (!shouldExpire && MaxSqrDistanceFromPlayer > 0f)
|
||||
{
|
||||
float3 horizontalDelta =
|
||||
new float3(nextPosition.x - PlayerPosition.x, 0f, nextPosition.z - PlayerPosition.z);
|
||||
shouldExpire = math.lengthsq(horizontalDelta) > MaxSqrDistanceFromPlayer;
|
||||
}
|
||||
|
||||
if (!shouldExpire && MaxVerticalOffsetFromPlayer > 0f)
|
||||
{
|
||||
shouldExpire = math.abs(nextPosition.y - PlayerPosition.y) > MaxVerticalOffsetFromPlayer;
|
||||
}
|
||||
|
||||
transform.Position = nextPosition;
|
||||
if (math.lengthsq(velocity) > float.Epsilon)
|
||||
{
|
||||
transform.Forward = math.normalizesafe(velocity, transform.Forward);
|
||||
}
|
||||
|
||||
Transforms[index] = transform;
|
||||
life.Age = nextAge;
|
||||
life.Active = !shouldExpire;
|
||||
Lives[index] = life;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b026d9036f330c84a8f047a80e2e16b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user