Checkpoint 5 & Checkpoint 6
This commit is contained in:
@@ -8,7 +8,11 @@ namespace Simulation
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Vector3 Velocity;
|
||||
public float Speed;
|
||||
public float LifeTime;
|
||||
public float Age;
|
||||
public bool Active;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
|
||||
@@ -125,25 +125,44 @@ namespace Simulation
|
||||
|
||||
private void TickEnemiesJobified(in SimulationTickContext context)
|
||||
{
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
PrepareCollisionCandidateChannels(0, 0, 0);
|
||||
ResetCollisionRuntimeStats();
|
||||
ClearAreaCollisionFrameBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||
{
|
||||
SyncSimulationToJobInput();
|
||||
int projectileQueryCount = _projectiles.Count;
|
||||
int areaQueryCount = GetPendingAreaCollisionQueryCount();
|
||||
int queryCount = projectileQueryCount + areaQueryCount;
|
||||
int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery);
|
||||
int areaExpectedCount = EstimatePendingAreaCollisionCandidateCount();
|
||||
int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount);
|
||||
int bucketCapacity = Mathf.Max(256, _enemies.Count * 2 + queryCount);
|
||||
PrepareCollisionCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
ExecuteEnemyMovementJob(in context);
|
||||
ExecuteProjectileMovementJob(in context);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||
{
|
||||
ApplyEnemySeparationForJobOutput(in context);
|
||||
BuildProjectileCollisionCandidates();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
SyncProjectilesToJobOutput();
|
||||
ApplyJobOutputToSimulation();
|
||||
ResolveProjectileCollisionCandidatesMainThread();
|
||||
RecycleInactiveProjectiles();
|
||||
}
|
||||
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
@@ -413,6 +432,9 @@ namespace Simulation
|
||||
|
||||
if (sqrDistance <= float.Epsilon)
|
||||
{
|
||||
float3 zeroDistanceAxis = GetZeroDistanceSeparationAxis(index, otherIndex);
|
||||
float directionSign = index < otherIndex ? 1f : -1f;
|
||||
pushAccumulation += zeroDistanceAxis * (selfRadius * 0.25f * directionSign);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -528,6 +550,18 @@ namespace Simulation
|
||||
return ((long)x << 32) ^ (uint)z;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
|
||||
NativeArray<EnemyJobOutputData> outputs, float deltaTime, float3 playerPosition)
|
||||
{
|
||||
@@ -587,4 +621,4 @@ namespace Simulation
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Simulation
|
||||
private const string DropGroupName = "Drop";
|
||||
private const string BulletGroupName = "Bullet";
|
||||
private const string ProjectileGroupName = "Projectile";
|
||||
private const string EnemyProjectileGroupName = "EnemyProjectile";
|
||||
|
||||
private readonly SimulationWorld _world;
|
||||
|
||||
@@ -53,10 +54,11 @@ namespace Simulation
|
||||
return;
|
||||
}
|
||||
|
||||
if ((groupName == BulletGroupName || groupName == ProjectileGroupName) &&
|
||||
if ((groupName == BulletGroupName || groupName == ProjectileGroupName ||
|
||||
groupName == EnemyProjectileGroupName) &&
|
||||
args.Entity.Logic is EntityBase projectileEntity)
|
||||
{
|
||||
_world.RegisterProjectileLifecycle(projectileEntity);
|
||||
_world.RegisterProjectileLifecycle(projectileEntity, args.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +81,8 @@ namespace Simulation
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupName == BulletGroupName || groupName == ProjectileGroupName)
|
||||
if (groupName == BulletGroupName || groupName == ProjectileGroupName ||
|
||||
groupName == EnemyProjectileGroupName)
|
||||
{
|
||||
_world.UnregisterProjectileLifecycle(args.EntityId);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
@@ -43,7 +44,11 @@ namespace Simulation
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Vector3 Velocity;
|
||||
public float Speed;
|
||||
public float LifeTime;
|
||||
public float Age;
|
||||
public bool Active;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
@@ -54,11 +59,67 @@ namespace Simulation
|
||||
public int OwnerEntityId;
|
||||
public Vector3 Position;
|
||||
public Vector3 Forward;
|
||||
public Vector3 Velocity;
|
||||
public float Speed;
|
||||
public float LifeTime;
|
||||
public float Age;
|
||||
public bool Active;
|
||||
public float RemainingLifetime;
|
||||
public int State;
|
||||
}
|
||||
|
||||
// Shared broad-phase query payload for CP5 projectile collision and CP6 AOE collision candidates.
|
||||
private struct CollisionQueryData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public float3 Position;
|
||||
public float Radius;
|
||||
public int MaxTargets;
|
||||
public int ShapeType;
|
||||
public float3 Direction;
|
||||
public float HalfAngleDeg;
|
||||
}
|
||||
|
||||
// Shared candidate buffer consumed by the main thread settlement stage.
|
||||
private struct CollisionCandidateData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
|
||||
private struct AreaCollisionRequestData
|
||||
{
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public Vector3 Center;
|
||||
public float Radius;
|
||||
public int MaxTargets;
|
||||
public int ShapeType;
|
||||
public Vector3 Direction;
|
||||
public float HalfAngleDeg;
|
||||
}
|
||||
|
||||
private struct AreaCollisionHitEventData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
|
||||
private const int CollisionSourceTypeProjectile = 1;
|
||||
private const int CollisionSourceTypeArea = 2;
|
||||
private const int CollisionShapeCircle = 0;
|
||||
private const int CollisionShapeSector = 1;
|
||||
|
||||
private NativeList<EnemyJobInputData> _enemyJobInputs;
|
||||
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
|
||||
private NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs;
|
||||
@@ -66,7 +127,34 @@ namespace Simulation
|
||||
private NativeList<float2> _enemySeparationCurrentPushes;
|
||||
private NativeList<ProjectileJobInputData> _projectileJobInputs;
|
||||
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
|
||||
private NativeList<CollisionQueryData> _collisionQueryInputs;
|
||||
private NativeList<CollisionCandidateData> _collisionCandidates;
|
||||
private NativeParallelMultiHashMap<long, int> _enemySeparationBuckets;
|
||||
private NativeParallelMultiHashMap<long, int> _enemyCollisionBuckets;
|
||||
private readonly List<AreaCollisionRequestData> _areaCollisionRequests = new(16);
|
||||
private readonly List<AreaCollisionHitEventData> _areaCollisionHitEvents = new(32);
|
||||
private readonly HashSet<long> _areaCollisionHitDedupKeys = new();
|
||||
private int _lastCollisionQueryCount;
|
||||
private int _lastProjectileCollisionQueryCount;
|
||||
private int _lastAreaCollisionQueryCount;
|
||||
private int _lastCollisionCandidateCount;
|
||||
private int _lastProjectileCollisionCandidateCount;
|
||||
private int _lastAreaCollisionCandidateCount;
|
||||
private int _lastResolvedAreaHitCount;
|
||||
private float _lastCollisionCellSize;
|
||||
private bool _lastCollisionHasEnemyTargets;
|
||||
|
||||
public int CollisionCandidateCount => _collisionCandidates.IsCreated ? _collisionCandidates.Length : 0;
|
||||
public int PendingAreaCollisionRequestCount => _areaCollisionRequests.Count;
|
||||
public int LastCollisionQueryCount => _lastCollisionQueryCount;
|
||||
public int LastProjectileCollisionQueryCount => _lastProjectileCollisionQueryCount;
|
||||
public int LastAreaCollisionQueryCount => _lastAreaCollisionQueryCount;
|
||||
public int LastCollisionCandidateCount => _lastCollisionCandidateCount;
|
||||
public int LastProjectileCollisionCandidateCount => _lastProjectileCollisionCandidateCount;
|
||||
public int LastAreaCollisionCandidateCount => _lastAreaCollisionCandidateCount;
|
||||
public int LastResolvedAreaHitCount => _lastResolvedAreaHitCount;
|
||||
public float LastCollisionCellSize => _lastCollisionCellSize;
|
||||
public bool LastCollisionHasEnemyTargets => _lastCollisionHasEnemyTargets;
|
||||
|
||||
private void InitializeJobDataChannels()
|
||||
{
|
||||
@@ -83,7 +171,10 @@ namespace Simulation
|
||||
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
|
||||
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
|
||||
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
||||
_collisionQueryInputs = new NativeList<CollisionQueryData>(64, Allocator.Persistent);
|
||||
_collisionCandidates = new NativeList<CollisionCandidateData>(128, Allocator.Persistent);
|
||||
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||
_enemyCollisionBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||
InitializeEnemyTargetSpatialIndex();
|
||||
}
|
||||
|
||||
@@ -93,57 +184,94 @@ namespace Simulation
|
||||
{
|
||||
_enemyJobInputs.Dispose();
|
||||
}
|
||||
|
||||
_enemyJobInputs = default;
|
||||
|
||||
if (_enemyJobOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobOutputs.Dispose();
|
||||
}
|
||||
|
||||
_enemyJobOutputs = default;
|
||||
|
||||
if (_enemyJobSeparationOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobSeparationOutputs.Dispose();
|
||||
}
|
||||
|
||||
_enemyJobSeparationOutputs = default;
|
||||
|
||||
if (_enemySeparationPreviousPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationPreviousPushes.Dispose();
|
||||
}
|
||||
|
||||
_enemySeparationPreviousPushes = default;
|
||||
|
||||
if (_enemySeparationCurrentPushes.IsCreated)
|
||||
{
|
||||
_enemySeparationCurrentPushes.Dispose();
|
||||
}
|
||||
|
||||
_enemySeparationCurrentPushes = default;
|
||||
|
||||
if (_projectileJobInputs.IsCreated)
|
||||
{
|
||||
_projectileJobInputs.Dispose();
|
||||
}
|
||||
|
||||
_projectileJobInputs = default;
|
||||
|
||||
if (_projectileJobOutputs.IsCreated)
|
||||
{
|
||||
_projectileJobOutputs.Dispose();
|
||||
}
|
||||
|
||||
_projectileJobOutputs = default;
|
||||
|
||||
if (_collisionQueryInputs.IsCreated)
|
||||
{
|
||||
_collisionQueryInputs.Dispose();
|
||||
}
|
||||
|
||||
_collisionQueryInputs = default;
|
||||
|
||||
if (_collisionCandidates.IsCreated)
|
||||
{
|
||||
_collisionCandidates.Dispose();
|
||||
}
|
||||
|
||||
_collisionCandidates = default;
|
||||
|
||||
if (_enemySeparationBuckets.IsCreated)
|
||||
{
|
||||
_enemySeparationBuckets.Dispose();
|
||||
}
|
||||
|
||||
_enemySeparationBuckets = default;
|
||||
|
||||
if (_enemyCollisionBuckets.IsCreated)
|
||||
{
|
||||
_enemyCollisionBuckets.Dispose();
|
||||
}
|
||||
|
||||
_enemyCollisionBuckets = default;
|
||||
|
||||
DisposeEnemyTargetSpatialIndex();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ResetCollisionRuntimeStats();
|
||||
}
|
||||
|
||||
private void ClearJobDataChannels()
|
||||
{
|
||||
if (!AreJobDataChannelsUsable())
|
||||
{
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ResetCollisionRuntimeStats();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,6 +295,16 @@ namespace Simulation
|
||||
_projectileJobOutputs.Clear();
|
||||
}
|
||||
|
||||
if (_collisionQueryInputs.IsCreated)
|
||||
{
|
||||
_collisionQueryInputs.Clear();
|
||||
}
|
||||
|
||||
if (_collisionCandidates.IsCreated)
|
||||
{
|
||||
_collisionCandidates.Clear();
|
||||
}
|
||||
|
||||
if (_enemyJobSeparationOutputs.IsCreated)
|
||||
{
|
||||
_enemyJobSeparationOutputs.Clear();
|
||||
@@ -187,7 +325,29 @@ namespace Simulation
|
||||
_enemySeparationBuckets.Clear();
|
||||
}
|
||||
|
||||
if (_enemyCollisionBuckets.IsCreated)
|
||||
{
|
||||
_enemyCollisionBuckets.Clear();
|
||||
}
|
||||
|
||||
ClearEnemyTargetSpatialIndex();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ResetCollisionRuntimeStats();
|
||||
}
|
||||
|
||||
private void ResetCollisionRuntimeStats()
|
||||
{
|
||||
_lastCollisionQueryCount = 0;
|
||||
_lastProjectileCollisionQueryCount = 0;
|
||||
_lastAreaCollisionQueryCount = 0;
|
||||
_lastCollisionCandidateCount = 0;
|
||||
_lastProjectileCollisionCandidateCount = 0;
|
||||
_lastAreaCollisionCandidateCount = 0;
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
_lastCollisionCellSize = 0f;
|
||||
_lastCollisionHasEnemyTargets = false;
|
||||
}
|
||||
|
||||
private void SyncSimulationToJobInput()
|
||||
@@ -242,6 +402,18 @@ namespace Simulation
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareProjectileJobOutputBuffer(int projectileCount)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _projectileJobOutputs, projectileCount);
|
||||
_projectileJobOutputs.Clear();
|
||||
|
||||
if (projectileCount > 0)
|
||||
{
|
||||
_projectileJobOutputs.ResizeUninitialized(projectileCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncProjectilesToJobOutput()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
@@ -254,6 +426,116 @@ namespace Simulation
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyProjectileInputToOutput()
|
||||
{
|
||||
for (int i = 0; i < _projectileJobInputs.Length; i++)
|
||||
{
|
||||
ProjectileJobInputData input = _projectileJobInputs[i];
|
||||
_projectileJobOutputs[i] = new ProjectileJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
OwnerEntityId = input.OwnerEntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Velocity = input.Velocity,
|
||||
Speed = input.Speed,
|
||||
LifeTime = input.LifeTime,
|
||||
Age = input.Age,
|
||||
Active = input.Active,
|
||||
RemainingLifetime = input.RemainingLifetime,
|
||||
State = input.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareCollisionCandidateChannels(int queryCount, int expectedCandidateCount, int bucketCapacity)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _collisionQueryInputs, queryCount);
|
||||
EnsureCapacity(ref _collisionCandidates, expectedCandidateCount);
|
||||
EnsureCapacity(ref _enemyCollisionBuckets, bucketCapacity);
|
||||
|
||||
_collisionQueryInputs.Clear();
|
||||
_collisionCandidates.Clear();
|
||||
_enemyCollisionBuckets.Clear();
|
||||
}
|
||||
|
||||
private void AddProjectileCollisionQuery(int queryId, in ProjectileJobOutputData projectile, float radius,
|
||||
int maxTargets = 1)
|
||||
{
|
||||
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collisionQueryInputs.Add(new CollisionQueryData
|
||||
{
|
||||
QueryId = queryId,
|
||||
SourceType = CollisionSourceTypeProjectile,
|
||||
SourceEntityId = projectile.EntityId,
|
||||
SourceOwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = new float3(projectile.Position.x, projectile.Position.y, projectile.Position.z),
|
||||
Radius = radius,
|
||||
MaxTargets = math.max(1, maxTargets),
|
||||
ShapeType = CollisionShapeCircle,
|
||||
Direction = new float3(0f, 0f, 1f),
|
||||
HalfAngleDeg = 180f
|
||||
});
|
||||
}
|
||||
|
||||
private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg)
|
||||
{
|
||||
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 normalizedDirection = direction;
|
||||
normalizedDirection.y = 0f;
|
||||
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
normalizedDirection = Vector3.forward;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalizedDirection.Normalize();
|
||||
}
|
||||
|
||||
_collisionQueryInputs.Add(new CollisionQueryData
|
||||
{
|
||||
QueryId = queryId,
|
||||
SourceType = CollisionSourceTypeArea,
|
||||
SourceEntityId = sourceEntityId,
|
||||
SourceOwnerEntityId = sourceOwnerEntityId,
|
||||
Position = new float3(center.x, center.y, center.z),
|
||||
Radius = radius,
|
||||
MaxTargets = math.max(1, maxTargets),
|
||||
ShapeType = shapeType,
|
||||
Direction = new float3(normalizedDirection.x, normalizedDirection.y, normalizedDirection.z),
|
||||
HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f)
|
||||
});
|
||||
}
|
||||
|
||||
private void AddCollisionCandidate(int queryId, int sourceType, int sourceEntityId, int sourceOwnerEntityId,
|
||||
int targetEntityId, float sqrDistance)
|
||||
{
|
||||
if (!_collisionCandidates.IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collisionCandidates.Add(new CollisionCandidateData
|
||||
{
|
||||
QueryId = queryId,
|
||||
SourceType = sourceType,
|
||||
SourceEntityId = sourceEntityId,
|
||||
SourceOwnerEntityId = sourceOwnerEntityId,
|
||||
TargetEntityId = targetEntityId,
|
||||
SqrDistance = sqrDistance
|
||||
});
|
||||
}
|
||||
|
||||
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
@@ -358,7 +640,10 @@ namespace Simulation
|
||||
IsNativeListUsable(_enemySeparationCurrentPushes) &&
|
||||
IsNativeListUsable(_projectileJobInputs) &&
|
||||
IsNativeListUsable(_projectileJobOutputs) &&
|
||||
IsNativeMultiHashMapUsable(_enemySeparationBuckets);
|
||||
IsNativeListUsable(_collisionQueryInputs) &&
|
||||
IsNativeListUsable(_collisionCandidates) &&
|
||||
IsNativeMultiHashMapUsable(_enemySeparationBuckets) &&
|
||||
IsNativeMultiHashMapUsable(_enemyCollisionBuckets);
|
||||
}
|
||||
|
||||
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
||||
@@ -472,7 +757,11 @@ namespace Simulation
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = projectile.Position,
|
||||
Forward = projectile.Forward,
|
||||
Velocity = projectile.Velocity,
|
||||
Speed = projectile.Speed,
|
||||
LifeTime = projectile.LifeTime,
|
||||
Age = projectile.Age,
|
||||
Active = projectile.Active,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
@@ -486,7 +775,11 @@ namespace Simulation
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = projectile.Position,
|
||||
Forward = projectile.Forward,
|
||||
Velocity = projectile.Velocity,
|
||||
Speed = projectile.Speed,
|
||||
LifeTime = projectile.LifeTime,
|
||||
Age = projectile.Age,
|
||||
Active = projectile.Active,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
@@ -500,7 +793,11 @@ namespace Simulation
|
||||
OwnerEntityId = projectile.OwnerEntityId,
|
||||
Position = projectile.Position,
|
||||
Forward = projectile.Forward,
|
||||
Velocity = projectile.Velocity,
|
||||
Speed = projectile.Speed,
|
||||
LifeTime = projectile.LifeTime,
|
||||
Age = projectile.Age,
|
||||
Active = projectile.Active,
|
||||
RemainingLifetime = projectile.RemainingLifetime,
|
||||
State = projectile.State
|
||||
};
|
||||
|
||||
@@ -1,18 +1,83 @@
|
||||
using CustomEvent;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using Entity.Weapon;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public partial class SimulationWorld
|
||||
{
|
||||
[Header("投射物命中表现")] [Tooltip("是否监听投射物命中表现事件。")] [SerializeField]
|
||||
private bool _projectileHitPresentationEnabled = true;
|
||||
|
||||
[Tooltip("是否播放投射物命中标记。")] [SerializeField]
|
||||
private bool _projectileHitMarkerEnabled = true;
|
||||
|
||||
[Tooltip("命中标记尺寸。")] [SerializeField]
|
||||
private float _projectileHitMarkerSize = 0.2f;
|
||||
|
||||
[Tooltip("命中标记在目标上的高度偏移。")] [SerializeField]
|
||||
private float _projectileHitMarkerYOffset = 1.2f;
|
||||
|
||||
[Tooltip("命中标记持续时间。")] [SerializeField]
|
||||
private float _projectileHitMarkerDuration = 0.15f;
|
||||
|
||||
[Tooltip("命中标记颜色。")] [SerializeField]
|
||||
private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
|
||||
|
||||
[Tooltip("是否播放投射物命中特效实体。")] [SerializeField]
|
||||
private bool _projectileHitEffectEnabled;
|
||||
|
||||
[Tooltip("投射物命中特效实体类型 Id(<=0 表示不启用)。")] [SerializeField]
|
||||
private int _projectileHitEffectTypeId;
|
||||
|
||||
private sealed class Presentation
|
||||
{
|
||||
private readonly SimulationWorld _world;
|
||||
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
|
||||
private bool _isProjectileHitEventSubscribed;
|
||||
|
||||
public Presentation(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnStart()
|
||||
{
|
||||
if (_world == null || !_world._projectileHitPresentationEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventComponent.Subscribe(ProjectileHitPresentationEventArgs.EventId, OnProjectileHitPresentationEvent);
|
||||
_isProjectileHitEventSubscribed = true;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent != null)
|
||||
{
|
||||
eventComponent.Unsubscribe(ProjectileHitPresentationEventArgs.EventId,
|
||||
OnProjectileHitPresentationEvent);
|
||||
}
|
||||
|
||||
_isProjectileHitEventSubscribed = false;
|
||||
}
|
||||
|
||||
public void OnLateUpdate()
|
||||
{
|
||||
if (_world == null || !_world.UseSimulationMovement)
|
||||
@@ -27,9 +92,9 @@ namespace Simulation
|
||||
}
|
||||
|
||||
var enemies = enemyManager.Enemies;
|
||||
for (int i = 0; i < enemies.Count; i++)
|
||||
foreach (var enemy in enemies)
|
||||
{
|
||||
if (enemies[i] is not EnemyBase enemyEntity || !enemyEntity.Available)
|
||||
if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -41,6 +106,85 @@ namespace Simulation
|
||||
|
||||
ApplyEnemyPresentation(enemyEntity, enemyData);
|
||||
}
|
||||
|
||||
if (!_world.UseJobSimulation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var projectiles = _world._projectiles;
|
||||
for (int i = 0; i < projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectileData = projectiles[i];
|
||||
if (!projectileData.Active || projectileData.State != ProjectileStateActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityBase projectileEntity = TryGetEntityById(projectileData.EntityId);
|
||||
if (projectileEntity == null || !projectileEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyProjectilePresentation(projectileEntity, projectileData);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProjectileHitPresentationEvent(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed || _world == null ||
|
||||
e is not ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ShowHitMarker && _world._projectileHitMarkerEnabled)
|
||||
{
|
||||
PlayHitMarker(args);
|
||||
}
|
||||
|
||||
if (args.ShowHitEffect && _world._projectileHitEffectEnabled)
|
||||
{
|
||||
PlayHitEffect(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayHitMarker(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
EntityBase targetEntity = TryGetEntityById(args.TargetEntityId);
|
||||
if (targetEntity == null || !targetEntity.Available)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_projectileHitMarkerEffect ??= new HandgunHitMarkerAttackEffect(
|
||||
Mathf.Max(0.01f, _world._projectileHitMarkerSize),
|
||||
_world._projectileHitMarkerYOffset,
|
||||
Mathf.Max(0.01f, _world._projectileHitMarkerDuration),
|
||||
_world._projectileHitMarkerColor);
|
||||
_projectileHitMarkerEffect.Play(null, args.HitPosition, targetEntity, 0f);
|
||||
}
|
||||
|
||||
private void PlayHitEffect(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
int effectTypeId = args.EffectEntityTypeId > 0 ? args.EffectEntityTypeId : _world._projectileHitEffectTypeId;
|
||||
if (effectTypeId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
if (entityComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EffectData effectData = new EffectData(entityComponent.GenerateSerialId(), effectTypeId)
|
||||
{
|
||||
Position = args.HitPosition
|
||||
};
|
||||
entityComponent.ShowEffect(effectData);
|
||||
}
|
||||
|
||||
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
|
||||
@@ -64,6 +208,25 @@ namespace Simulation
|
||||
enemyTransform.forward = forward.normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
|
||||
in ProjectileSimData projectileData)
|
||||
{
|
||||
Transform projectileTransform = projectileEntity.CachedTransform;
|
||||
if (projectileTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.position = projectileData.Position;
|
||||
Vector3 forward = projectileData.Forward;
|
||||
if (forward.sqrMagnitude <= float.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.rotation = Quaternion.LookRotation(forward.normalized, Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcb6513f18404795a654500d3d2f8ef9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -15,6 +15,8 @@ namespace Simulation
|
||||
private const int EnemyStateIdle = 0;
|
||||
private const int EnemyStateChasing = 1;
|
||||
private const int EnemyStateInAttackRange = 2;
|
||||
private const int ProjectileStateActive = 0;
|
||||
private const int ProjectileStateExpired = 1;
|
||||
|
||||
private struct EnemyTickWorkItem
|
||||
{
|
||||
@@ -49,6 +51,8 @@ namespace Simulation
|
||||
private readonly List<EnemySimData> _enemies = new List<EnemySimData>();
|
||||
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
|
||||
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
|
||||
private readonly List<int> _projectileRecycleEntityIds = new List<int>();
|
||||
private readonly HashSet<int> _projectileResolvedEntityIds = new HashSet<int>();
|
||||
private readonly List<EnemySeparationAgent> _enemySeparationAgents = new List<EnemySeparationAgent>();
|
||||
private readonly List<EnemyTickWorkItem> _enemyTickWorkItems = new List<EnemyTickWorkItem>();
|
||||
|
||||
@@ -89,10 +93,12 @@ namespace Simulation
|
||||
private void Start()
|
||||
{
|
||||
_entitySync?.OnStart();
|
||||
_presentation?.OnStart();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_presentation?.OnDestroy();
|
||||
_entitySync?.OnDestroy();
|
||||
_entitySync = null;
|
||||
_presentation = null;
|
||||
@@ -216,14 +222,14 @@ namespace Simulation
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RegisterProjectileLifecycle(EntityBase projectileEntity)
|
||||
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
if (projectileEntity == null || projectileEntity.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity));
|
||||
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity, userData));
|
||||
}
|
||||
|
||||
private void UnregisterProjectileLifecycle(int entityId)
|
||||
@@ -313,6 +319,11 @@ namespace Simulation
|
||||
_enemies.Clear();
|
||||
_projectiles.Clear();
|
||||
_pickups.Clear();
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
_projectileResolvedEntityIds.Clear();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
_enemySeparationAgents.Clear();
|
||||
_enemyTickWorkItems.Clear();
|
||||
ClearJobDataChannels();
|
||||
@@ -510,6 +521,10 @@ namespace Simulation
|
||||
speed = movementComponent.Speed;
|
||||
}
|
||||
|
||||
float attackRange = enemy != null && enemy.AttackRange > 0f
|
||||
? enemy.AttackRange
|
||||
: DefaultAttackRange;
|
||||
|
||||
return new EnemySimData
|
||||
{
|
||||
EntityId = enemy.Id,
|
||||
@@ -517,7 +532,7 @@ namespace Simulation
|
||||
Forward = enemyTransform.forward,
|
||||
Rotation = enemyTransform.rotation,
|
||||
Speed = speed,
|
||||
AttackRange = 1f,
|
||||
AttackRange = attackRange,
|
||||
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
|
||||
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
|
||||
@@ -537,17 +552,52 @@ namespace Simulation
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity)
|
||||
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
Vector3 forward = projectileEntity.CachedTransform.forward;
|
||||
int ownerEntityId = 0;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
float speed = 0f;
|
||||
float lifeTime = 0f;
|
||||
|
||||
if (userData is EnemyProjectileData enemyProjectileData)
|
||||
{
|
||||
ownerEntityId = enemyProjectileData.OwnerEntityId;
|
||||
|
||||
Vector3 direction = enemyProjectileData.Direction;
|
||||
direction.y = 0f;
|
||||
if (direction.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
direction.Normalize();
|
||||
forward = direction;
|
||||
}
|
||||
else if (forward.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
forward = forward.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
forward = Vector3.forward;
|
||||
}
|
||||
|
||||
speed = Mathf.Max(0f, enemyProjectileData.Speed);
|
||||
velocity = forward * speed;
|
||||
lifeTime = Mathf.Max(0f, enemyProjectileData.LifeTime);
|
||||
}
|
||||
|
||||
return new ProjectileSimData
|
||||
{
|
||||
EntityId = projectileEntity.Id,
|
||||
OwnerEntityId = 0,
|
||||
OwnerEntityId = ownerEntityId,
|
||||
Position = projectileEntity.CachedTransform.position,
|
||||
Forward = projectileEntity.CachedTransform.forward,
|
||||
Speed = 0f,
|
||||
RemainingLifetime = 0f,
|
||||
State = 0
|
||||
Forward = forward,
|
||||
Velocity = velocity,
|
||||
Speed = speed,
|
||||
LifeTime = lifeTime,
|
||||
Age = 0f,
|
||||
Active = true,
|
||||
RemainingLifetime = lifeTime,
|
||||
State = ProjectileStateActive
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user