using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; using CustomDebugger; using CustomEvent; using Definition.DataStruct; using Definition.Enum; using Entity; using Entity.Weapon; using CustomUtility; using UnityGameFramework.Runtime; namespace Simulation { public sealed partial class SimulationWorld { private const int PlayerEntityId = -1; [Header("投射物模拟参数")] [Tooltip("投射物距离玩家超过该水平半径时回收。小于等于 0 表示不启用该回收条件。")] [SerializeField] private float _projectileMaxDistanceFromPlayer = 120f; [Tooltip("投射物与玩家的垂直高度差超过该值时回收。小于等于 0 表示不启用该回收条件。")] [SerializeField] private float _projectileMaxVerticalOffsetFromPlayer = 30f; [Tooltip("投射物 Broad Phase 命中查询半径。")] [SerializeField] private float _projectileCollisionQueryRadius = 0.35f; [Tooltip("每个投射物最多保留的候选目标数量。")] [SerializeField] private int _projectileMaxCandidatesPerQuery = 1; [Tooltip("投射物 Broad Phase 分桶网格尺寸。小于等于 0 时将按查询半径自动推导。")] [SerializeField] private float _projectileCollisionCellSize = 0f; [Header("投射物命中事件派发")] [Tooltip("是否派发投射物命中表现事件。")] [SerializeField] private bool _dispatchProjectileHitPresentationEvent = true; [Tooltip("命中时是否请求命中标记表现。")] [SerializeField] private bool _dispatchProjectileHitMarkerEvent = true; [Tooltip("命中时是否请求特效表现。")] [SerializeField] private bool _dispatchProjectileHitEffectEvent = true; [Tooltip("命中事件建议使用的特效实体类型 Id(<=0 表示不指定,由表现层决定)。")] [SerializeField] private int _projectileHitPresentationEffectTypeId = 0; [BurstCompile] private struct ProjectileMovementBurstJob : IJobParallelFor { [ReadOnly] public NativeArray Inputs; public NativeArray Outputs; public float DeltaTime; public float3 PlayerPosition; public float MaxSqrDistanceFromPlayer; public float MaxVerticalOffsetFromPlayer; public void Execute(int index) { ExecuteProjectileMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition, MaxSqrDistanceFromPlayer, MaxVerticalOffsetFromPlayer); } } private struct ProjectileMovementJob : IJobParallelFor { [ReadOnly] public NativeArray Inputs; public NativeArray Outputs; public float DeltaTime; public float3 PlayerPosition; public float MaxSqrDistanceFromPlayer; public float MaxVerticalOffsetFromPlayer; public void Execute(int index) { ExecuteProjectileMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition, MaxSqrDistanceFromPlayer, MaxVerticalOffsetFromPlayer); } } public bool TryEnqueueAreaCollisionQuery(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius, int maxTargets = 16) { return TryEnqueueAreaCollisionQueryInternal(sourceEntityId, sourceOwnerEntityId, in center, radius, maxTargets, CollisionShapeCircle, Vector3.forward, 180f); } public bool TryEnqueueSectorCollisionQuery(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16) { return TryEnqueueAreaCollisionQueryInternal(sourceEntityId, sourceOwnerEntityId, in center, radius, maxTargets, CollisionShapeSector, direction, halfAngleDeg); } private bool TryEnqueueAreaCollisionQueryInternal(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg) { if (!_useSimulationMovement || !_useJobSimulation) { return false; } if (sourceEntityId == 0 || radius <= 0f || maxTargets <= 0) { return false; } int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId; Vector3 normalizedDirection = direction; normalizedDirection.y = 0f; if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon) { normalizedDirection = Vector3.forward; } else { normalizedDirection.Normalize(); } _areaCollisionRequests.Add(new AreaCollisionRequestData { SourceEntityId = sourceEntityId, SourceOwnerEntityId = resolvedOwnerEntityId, Center = center, Radius = Mathf.Max(0.01f, radius), MaxTargets = Mathf.Max(1, maxTargets), ShapeType = shapeType, Direction = normalizedDirection, HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f) }); return true; } private int GetPendingAreaCollisionQueryCount() { return _areaCollisionRequests.Count; } private int EstimatePendingAreaCollisionCandidateCount() { int expectedCount = 0; for (int i = 0; i < _areaCollisionRequests.Count; i++) { expectedCount += Mathf.Max(1, _areaCollisionRequests[i].MaxTargets); } return expectedCount; } private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context) { int projectileCount = _projectileJobInputs.Length; if (projectileCount == 0) { return default; } if (context.DeltaTime <= 0f) { CopyProjectileInputToOutput(); return default; } float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer); float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f; float maxVerticalOffsetFromPlayer = Mathf.Max(0f, _projectileMaxVerticalOffsetFromPlayer); float3 playerPosition = new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z); NativeArray inputArray = _projectileJobInputs.AsArray(); NativeArray outputArray = _projectileJobOutputs.AsArray(); if (_useBurstJobs) { ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob { Inputs = inputArray, Outputs = outputArray, DeltaTime = context.DeltaTime, PlayerPosition = playerPosition, MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer, MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer }; return burstJob.Schedule(projectileCount, 64); } ProjectileMovementJob job = new ProjectileMovementJob { Inputs = inputArray, Outputs = outputArray, DeltaTime = context.DeltaTime, PlayerPosition = playerPosition, MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer, MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer }; return job.Schedule(projectileCount, 64); } private void BuildProjectileCollisionCandidates() { if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated || !_enemyCollisionBuckets.IsCreated) { ResetCollisionRuntimeStats(); ClearAreaCollisionFrameBuffers(); return; } int projectileCount = _projectileJobOutputs.Length; int areaQueryCount = _areaCollisionRequests.Count; if (projectileCount == 0 && areaQueryCount == 0) { ResetCollisionRuntimeStats(); return; } float queryRadius = Mathf.Max(0.01f, _projectileCollisionQueryRadius); int maxCandidatesPerQuery = Mathf.Max(1, _projectileMaxCandidatesPerQuery); float maxQueryRadius = queryRadius; int queryId = 0; int projectileQueryCount = 0; int builtAreaQueryCount = 0; using (CustomProfilerMarker.Collision_BuildQueries.Auto()) { for (int i = 0; i < projectileCount; i++) { ProjectileJobOutputData projectile = _projectileJobOutputs[i]; if (!projectile.Active || projectile.State != ProjectileStateActive) { continue; } AddProjectileCollisionQuery(queryId, in projectile, queryRadius, maxCandidatesPerQuery); queryId++; projectileQueryCount++; } for (int i = 0; i < areaQueryCount; i++) { AreaCollisionRequestData request = _areaCollisionRequests[i]; AddAreaCollisionQuery(queryId, request.SourceEntityId, request.SourceOwnerEntityId, in request.Center, request.Radius, request.MaxTargets, request.ShapeType, in request.Direction, request.HalfAngleDeg); queryId++; builtAreaQueryCount++; if (request.Radius > maxQueryRadius) { maxQueryRadius = request.Radius; } } } _lastProjectileCollisionQueryCount = projectileQueryCount; _lastAreaCollisionQueryCount = builtAreaQueryCount; _lastCollisionQueryCount = projectileQueryCount + builtAreaQueryCount; _lastResolvedAreaHitCount = 0; if (_collisionQueryInputs.Length == 0) { _lastCollisionCandidateCount = 0; _lastProjectileCollisionCandidateCount = 0; _lastAreaCollisionCandidateCount = 0; _lastCollisionCellSize = 0f; _lastCollisionHasEnemyTargets = _enemyJobOutputs.Length > 0; return; } float autoCellSize = maxQueryRadius * 2f; float configuredCellSize = _projectileCollisionCellSize > 0f ? _projectileCollisionCellSize : autoCellSize; float cellSize = Mathf.Max(0.1f, configuredCellSize); bool hasEnemyTargets = _enemyJobOutputs.Length > 0; _lastCollisionCellSize = cellSize; _lastCollisionHasEnemyTargets = hasEnemyTargets; using (CustomProfilerMarker.Collision_BuildBuckets.Auto()) { if (hasEnemyTargets) { BuildEnemyCollisionBucketsForProjectiles(cellSize); } } int projectileCandidateCount; int areaCandidateCount; using (CustomProfilerMarker.Collision_QueryCandidates.Auto()) { QueryProjectileCollisionCandidates(cellSize, hasEnemyTargets, out projectileCandidateCount, out areaCandidateCount); } _lastProjectileCollisionCandidateCount = projectileCandidateCount; _lastAreaCollisionCandidateCount = areaCandidateCount; _lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount; } private void ResolveProjectileCollisionCandidatesMainThread() { if (!_collisionCandidates.IsCreated) { _lastResolvedAreaHitCount = 0; ClearAreaCollisionFrameBuffers(); return; } _projectileResolvedEntityIds.Clear(); _areaCollisionHitEvents.Clear(); _areaCollisionHitDedupKeys.Clear(); if (_collisionCandidates.Length == 0) { _lastResolvedAreaHitCount = 0; ClearAreaCollisionFrameBuffers(); return; } using (CustomProfilerMarker.Collision_ResolveProjectile.Auto()) { for (int i = 0; i < _collisionCandidates.Length; i++) { CollisionCandidateData candidate = _collisionCandidates[i]; if (candidate.SourceType == CollisionSourceTypeProjectile) { int projectileEntityId = candidate.SourceEntityId; if (_projectileResolvedEntityIds.Contains(projectileEntityId)) { continue; } if (!TryGetActiveProjectileData(projectileEntityId, out _, out ProjectileSimData projectile)) { _projectileResolvedEntityIds.Add(projectileEntityId); continue; } bool shouldExpireProjectile = true; bool shouldDispatchPresentation = false; int damage = 0; Vector3 hitPosition = projectile.Position; if (TryGetTargetableEntity(candidate.TargetEntityId, out TargetableObject target)) { EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId); EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId); shouldExpireProjectile = ResolveProjectileHit(target, sourceEntity, ownerEntity, in projectile, out damage, out hitPosition, out shouldDispatchPresentation); } if (shouldDispatchPresentation) { DispatchProjectileHitPresentationEvent(projectileEntityId, candidate.SourceEntityId, candidate.SourceOwnerEntityId, candidate.TargetEntityId, damage, in hitPosition); } if (shouldExpireProjectile) { MarkProjectileExpired(projectileEntityId); _projectileResolvedEntityIds.Add(projectileEntityId); } continue; } if (candidate.SourceType == CollisionSourceTypeArea) { long dedupKey = (((long)candidate.QueryId) << 32) ^ (uint)candidate.TargetEntityId; if (!_areaCollisionHitDedupKeys.Add(dedupKey)) { continue; } _areaCollisionHitEvents.Add(new AreaCollisionHitEventData { QueryId = candidate.QueryId, SourceEntityId = candidate.SourceEntityId, SourceOwnerEntityId = candidate.SourceOwnerEntityId, TargetEntityId = candidate.TargetEntityId, SqrDistance = candidate.SqrDistance }); } } } int resolvedAreaHitCount; using (CustomProfilerMarker.Collision_ResolveArea.Auto()) { resolvedAreaHitCount = ResolveAreaCollisionHitsMainThread(); } _lastResolvedAreaHitCount = resolvedAreaHitCount; _projectileResolvedEntityIds.Clear(); ClearAreaCollisionFrameBuffers(); } private void RecycleInactiveProjectiles() { _projectileRecycleEntityIds.Clear(); for (int i = 0; i < _projectiles.Count; i++) { ProjectileSimData projectile = _projectiles[i]; if (!ShouldRecycleProjectile(projectile)) { continue; } _projectileRecycleEntityIds.Add(projectile.EntityId); } if (_projectileRecycleEntityIds.Count == 0) { return; } var entityComponent = GameEntry.Entity; for (int i = 0; i < _projectileRecycleEntityIds.Count; i++) { int entityId = _projectileRecycleEntityIds[i]; if (entityComponent != null) { entityComponent.HideEntity(entityId); } RemoveProjectileByEntityId(entityId); } _projectileRecycleEntityIds.Clear(); } private bool ResolveProjectileHit(TargetableObject target, EntityBase sourceEntity, EntityBase ownerEntity, in ProjectileSimData projectile, out int damage, out Vector3 hitPosition, out bool shouldDispatchPresentation) { damage = 0; hitPosition = projectile.Position; shouldDispatchPresentation = false; if (target == null || !target.Available || target.IsDead) { return true; } if (target.CachedTransform != null) { hitPosition = target.CachedTransform.position; } if (!TryResolveImpactSource(sourceEntity, ownerEntity, out EntityBase attacker, out ImpactData sourceImpact)) { shouldDispatchPresentation = true; return true; } ImpactData targetImpact = target.GetImpactData(); if (AIUtility.GetRelation(targetImpact.Camp, sourceImpact.Camp) == RelationType.Friendly) { return false; } damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat, targetImpact.DefenseStat, targetImpact.DodgeStat); shouldDispatchPresentation = true; if (damage <= 0) { return true; } target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, damage); return true; } private void DispatchProjectileHitPresentationEvent(int projectileEntityId, int sourceEntityId, int sourceOwnerEntityId, int targetEntityId, int damage, in Vector3 hitPosition) { if (!_dispatchProjectileHitPresentationEvent) { return; } var eventComponent = GameEntry.Event; if (eventComponent == null) { return; } eventComponent.Fire(this, ProjectileHitPresentationEventArgs.Create( projectileEntityId, sourceEntityId, sourceOwnerEntityId, targetEntityId, damage, hitPosition, _dispatchProjectileHitMarkerEvent, _dispatchProjectileHitEffectEvent, _projectileHitPresentationEffectTypeId)); } private static bool TryResolveImpactSource(EntityBase sourceEntity, EntityBase ownerEntity, out EntityBase attacker, out ImpactData impactData) { if (TryResolveImpactFromEntity(sourceEntity, out impactData)) { attacker = sourceEntity; return true; } if (TryResolveImpactFromEntity(ownerEntity, out impactData)) { attacker = ownerEntity; return true; } attacker = null; impactData = default; return false; } private static bool TryResolveImpactFromEntity(EntityBase entity, out ImpactData impactData) { if (entity is WeaponBase weapon) { impactData = weapon.GetImpactData(); return true; } if (entity is EnemyProjectile enemyProjectile) { impactData = enemyProjectile.GetImpactData(); return true; } if (entity is TargetableObject targetableObject) { impactData = targetableObject.GetImpactData(); return true; } impactData = default; return false; } private static bool TryGetTargetableEntity(int entityId, out TargetableObject target) { target = null; var enemyManager = GameEntry.EnemyManager; if (enemyManager != null && enemyManager.TryGetEnemy(entityId, out EntityBase enemyEntity)) { if (enemyEntity is TargetableObject enemyTarget && enemyTarget.Available && !enemyTarget.IsDead) { target = enemyTarget; return true; } } EntityBase entity = TryGetEntityById(entityId); if (entity is TargetableObject targetable && targetable.Available && !targetable.IsDead) { target = targetable; return true; } return false; } private static EntityBase TryGetEntityById(int entityId) { var entityComponent = GameEntry.Entity; return entityComponent != null ? entityComponent.GetGameEntity(entityId) : null; } private bool TryGetActiveProjectileData(int projectileEntityId, out int simulationIndex, out ProjectileSimData projectile) { simulationIndex = -1; projectile = default; if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int foundIndex) || foundIndex < 0 || foundIndex >= _projectiles.Count) { return false; } ProjectileSimData data = _projectiles[foundIndex]; if (!data.Active || data.State != ProjectileStateActive) { return false; } simulationIndex = foundIndex; projectile = data; return true; } private bool MarkProjectileExpired(int projectileEntityId) { if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) || simulationIndex < 0 || simulationIndex >= _projectiles.Count) { return false; } ProjectileSimData projectile = _projectiles[simulationIndex]; projectile.Active = false; projectile.State = ProjectileStateExpired; projectile.RemainingLifetime = 0f; if (projectile.LifeTime > 0f && projectile.Age < projectile.LifeTime) { projectile.Age = projectile.LifeTime; } _projectiles[simulationIndex] = projectile; return true; } private int ResolveAreaCollisionHitsMainThread() { if (_areaCollisionHitEvents.Count == 0) { return 0; } int resolvedHitCount = 0; for (int i = 0; i < _areaCollisionHitEvents.Count; i++) { AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i]; if (!TryGetCollisionQueryById(hitEvent.QueryId, out CollisionQueryData query) || query.SourceType != CollisionSourceTypeArea) { continue; } EntityBase sourceEntity = TryGetEntityById(hitEvent.SourceEntityId); if (sourceEntity == null || !sourceEntity.Available) { continue; } if (!TryGetTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target)) { continue; } if (!IsAreaTargetInsidePreciseShape(in query, target)) { continue; } AIUtility.PerformCollision(target, sourceEntity); resolvedHitCount++; } return resolvedHitCount; } private bool TryGetCollisionQueryById(int queryId, out CollisionQueryData query) { query = default; if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length) { return false; } CollisionQueryData direct = _collisionQueryInputs[queryId]; if (direct.QueryId == queryId) { query = direct; return true; } for (int i = 0; i < _collisionQueryInputs.Length; i++) { CollisionQueryData candidate = _collisionQueryInputs[i]; if (candidate.QueryId != queryId) { continue; } query = candidate; return true; } return false; } private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target) { if (target == null || target.CachedTransform == null) { return false; } Vector3 center = new Vector3(query.Position.x, query.Position.y, query.Position.z); Vector3 toTarget = target.CachedTransform.position - center; toTarget.y = 0f; float radius = Mathf.Max(0.01f, query.Radius); float radiusSqr = radius * radius; float sqrDistance = toTarget.sqrMagnitude; if (sqrDistance > radiusSqr) { return false; } if (query.ShapeType != CollisionShapeSector) { return true; } if (sqrDistance <= Mathf.Epsilon) { return true; } Vector3 forward = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z); forward.y = 0f; if (forward.sqrMagnitude <= Mathf.Epsilon) { forward = Vector3.forward; } else { forward.Normalize(); } float halfAngle = Mathf.Clamp(query.HalfAngleDeg, 0f, 180f); float angle = Vector3.Angle(forward, toTarget.normalized); return angle <= halfAngle; } private void ClearAreaCollisionFrameBuffers() { _areaCollisionRequests.Clear(); _areaCollisionHitEvents.Clear(); _areaCollisionHitDedupKeys.Clear(); } private void BuildEnemyCollisionBucketsForProjectiles(float cellSize) { _enemyCollisionBuckets.Clear(); for (int i = 0; i < _enemyJobOutputs.Length; i++) { EnemyJobOutputData enemy = _enemyJobOutputs[i]; int cellX = (int)math.floor(enemy.Position.x / cellSize); int cellZ = (int)math.floor(enemy.Position.z / cellSize); _enemyCollisionBuckets.Add(SeparationCellKey(cellX, cellZ), i); } } private void QueryProjectileCollisionCandidates(float cellSize, bool hasEnemyTargets, out int projectileCandidateCount, out int areaCandidateCount) { projectileCandidateCount = 0; areaCandidateCount = 0; bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId, out float3 playerPosition); for (int i = 0; i < _collisionQueryInputs.Length; i++) { CollisionQueryData query = _collisionQueryInputs[i]; float radiusSqr = query.Radius * query.Radius; int centerCellX = (int)math.floor(query.Position.x / cellSize); int centerCellZ = (int)math.floor(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) { playerPosition.y = query.Position.y; float3 playerDelta = playerPosition - query.Position; float playerSqrDistance = math.lengthsq(playerDelta); // Log.Info( // $"playerPos:{playerPosition} - queryPos:{query.Position} = playerSqrDistance:{playerSqrDistance}"); if (playerSqrDistance <= radiusSqr) { AddCollisionCandidate( query.QueryId, query.SourceType, query.SourceEntityId, query.SourceOwnerEntityId, playerTargetEntityId, playerSqrDistance); if (query.SourceType == CollisionSourceTypeProjectile) { projectileCandidateCount++; } else if (query.SourceType == CollisionSourceTypeArea) { areaCandidateCount++; } } } if (!hasEnemyTargets) { continue; } for (int dx = -queryRange; dx <= queryRange && !reachedLimit; dx++) { for (int dz = -queryRange; dz <= queryRange && !reachedLimit; dz++) { long key = SeparationCellKey(centerCellX + dx, centerCellZ + dz); if (!_enemyCollisionBuckets.TryGetFirstValue(key, out int enemyIndex, out NativeParallelMultiHashMapIterator iterator)) { continue; } do { if (enemyIndex < 0 || enemyIndex >= _enemyJobOutputs.Length) { continue; } EnemyJobOutputData enemy = _enemyJobOutputs[enemyIndex]; if (enemy.EntityId == query.SourceOwnerEntityId) { continue; } float3 delta = new float3( enemy.Position.x - query.Position.x, enemy.Position.y - query.Position.y, enemy.Position.z - query.Position.z); float sqrDistance = math.lengthsq(delta); if (sqrDistance > radiusSqr) { continue; } AddCollisionCandidate( query.QueryId, query.SourceType, query.SourceEntityId, query.SourceOwnerEntityId, enemy.EntityId, sqrDistance); if (query.SourceType == CollisionSourceTypeProjectile) { projectileCandidateCount++; } else if (query.SourceType == CollisionSourceTypeArea) { areaCandidateCount++; } selectedCount++; if (selectedCount >= query.MaxTargets) { reachedLimit = true; break; } } while (_enemyCollisionBuckets.TryGetNextValue(out enemyIndex, ref iterator)); } } } } private static bool TryGetPlayerCollisionTarget(out int playerEntityId, out float3 playerPosition) { playerEntityId = PlayerEntityId; playerPosition = default; if (!TryGetTargetableEntity(playerEntityId, out TargetableObject playerTarget)) { return false; } Transform playerTransform = playerTarget.CachedTransform; if (playerTransform == null) { return false; } Vector3 position = playerTransform.position; playerPosition = new float3(position.x, position.y, position.z); return true; } private static bool ShouldRecycleProjectile(in ProjectileSimData projectile) { if (!projectile.Active) { return true; } if (projectile.State == ProjectileStateExpired) { return true; } return projectile.LifeTime > 0f && projectile.Age >= projectile.LifeTime; } private static void ExecuteProjectileMovement(int index, NativeArray inputs, NativeArray outputs, float deltaTime, float3 playerPosition, float maxSqrDistanceFromPlayer, float maxVerticalOffsetFromPlayer) { ProjectileJobInputData input = inputs[index]; ProjectileJobOutputData output = 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 }; if (!input.Active) { output.State = ProjectileStateExpired; outputs[index] = output; return; } float3 position = input.Position; float3 forward = input.Forward; float3 velocity = input.Velocity; if (math.lengthsq(velocity) <= float.Epsilon && input.Speed > 0f) { float3 moveDirection = math.normalizesafe(forward, new float3(0f, 0f, 1f)); velocity = moveDirection * input.Speed; } float3 nextPosition = position + velocity * deltaTime; float nextAge = math.max(0f, input.Age + deltaTime); float nextRemainingLifetime = input.RemainingLifetime; bool shouldExpire = false; if (input.LifeTime > 0f) { nextRemainingLifetime = math.max(0f, input.LifeTime - nextAge); shouldExpire = nextAge >= input.LifeTime; } else if (input.RemainingLifetime > 0f) { nextRemainingLifetime = math.max(0f, input.RemainingLifetime - deltaTime); shouldExpire = nextRemainingLifetime <= float.Epsilon; } 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; } output.Position = nextPosition; output.Velocity = velocity; output.Age = nextAge; output.RemainingLifetime = nextRemainingLifetime; output.Active = !shouldExpire; output.State = shouldExpire ? ProjectileStateExpired : ProjectileStateActive; if (math.lengthsq(velocity) > float.Epsilon) { float3 moveForward = math.normalizesafe(velocity, forward); output.Forward = moveForward; } outputs[index] = output; } } }