简单重构拆分 SimulationWorld

This commit is contained in:
2026-09-18 17:43:41 +08:00
parent 1e4d3ef8d7
commit 92e1ea0d55
109 changed files with 2605 additions and 1817 deletions
+5
View File
@@ -0,0 +1,5 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore
+2 -1
View File
@@ -1,4 +1,4 @@
# AGENTS.md — SBE(Seek-Battle-Evacuate)
# AGENTS.md
单人 3d 类吸血鬼幸存者游戏原型。Unity 2022.3.62f3,框架为 GameFramework(UGF,源码在 `Assets/GameFramework/`),配置表用 Luban,插件有 UniTask、DOTween 等。
@@ -36,6 +36,7 @@
- 不做静态回退(不用默认值/兜底逻辑静默继续);异常路径直接 `Log.Error(...)` 抛出(UGF 的 `Log`,支持格式串,如 `Log.Error("Entity data is invalid.")`)。
- UI 的序列化字段(`[SerializeField]` 及 View 生成的引用)**不做判空检查**,默认已在 Inspector 中赋值。
- GameEntry 中提供的静态入口不对其进行判空,默认其不为空。
- 对于从 Luban 中取出的数据,默认其是有效且不为空的,代码里不对其做任何有效性检测
## 代码风格
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 67ad529203c03d8419375e2342eb485f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace SepCore.Simulation
{
internal struct EnemyAttackSim : ISimulationComponent
{
public const float DefaultAttackRange = 1f;
public float AttackRange;
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 59ae2375305641a6a36d1bea0bf1abc2
guid: 35d153869faa1494c9fbb7218bbdd5b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,11 @@
namespace SepCore.Simulation
{
internal struct EnemyBehaviorSim : ISimulationComponent
{
public const int StateIdle = 0;
public const int StateChasing = 1;
public const int StateInAttackRange = 2;
public int State;
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b2c8675358384b21925e6f660ce61a79
guid: 23b79d0c84ef6904588ff8da30bf4108
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,7 @@
namespace SepCore.Simulation
{
internal struct EnemyIdentitySim : ISimulationComponent
{
public int EntityId;
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 57a3f582d9c04803908205e0cd4b5b75
guid: f89e25340a7475048a94caab1c77e745
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,9 @@
namespace SepCore.Simulation
{
internal struct EnemyMoveConfigSim : ISimulationComponent
{
public bool AvoidOverlap;
public float BodyRadius;
public int SeparationIterations;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa8be64dc4a8e0c4598f5b0be7fa7e03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace SepCore.Simulation
{
internal struct EnemyMoveInputSim : ISimulationComponent
{
public float Speed;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8883455a6bb9c94eba26c839184ca0b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
internal struct EnemySeparationSim : ISimulationComponent
{
public float2 PreviousPush;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eaeb6060e86ea424f956a2a478bc6126
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
namespace SepCore.Simulation
{
internal sealed class EnemyStore : EntityComponentStore
{
private Column<EnemyIdentitySim> _identity;
private Column<EnemyTransformSim> _transform;
private Column<EnemyMoveInputSim> _moveInput;
private Column<EnemyMoveConfigSim> _moveConfig;
private Column<EnemyAttackSim> _attack;
private Column<EnemyBehaviorSim> _behavior;
private Column<EnemySeparationSim> _separation;
public Column<EnemyIdentitySim> Identity => _identity;
public Column<EnemyTransformSim> Transform => _transform;
public Column<EnemyMoveInputSim> MoveInput => _moveInput;
public Column<EnemyMoveConfigSim> MoveConfig => _moveConfig;
public Column<EnemyAttackSim> Attack => _attack;
public Column<EnemyBehaviorSim> Behavior => _behavior;
public Column<EnemySeparationSim> Separation => _separation;
public void Initialize(int capacity)
{
if (IsCreated)
{
return;
}
Dispose();
_identity = Register<EnemyIdentitySim>(capacity);
_transform = Register<EnemyTransformSim>(capacity);
_moveInput = Register<EnemyMoveInputSim>(capacity);
_moveConfig = Register<EnemyMoveConfigSim>(capacity);
_attack = Register<EnemyAttackSim>(capacity);
_behavior = Register<EnemyBehaviorSim>(capacity);
_separation = Register<EnemySeparationSim>(capacity);
MarkInitialized();
}
public int Add(in EnemyIdentitySim identity, in EnemyTransformSim transform, in EnemyMoveInputSim moveInput,
in EnemyMoveConfigSim moveConfig, in EnemyAttackSim attack, in EnemyBehaviorSim behavior)
{
int index = AppendRow(identity.EntityId);
_identity.Data.Add(identity);
_transform.Data.Add(transform);
_moveInput.Data.Add(moveInput);
_moveConfig.Data.Add(moveConfig);
_attack.Data.Add(attack);
_behavior.Data.Add(behavior);
_separation.Data.Add(default);
return index;
}
public override void Dispose()
{
base.Dispose();
_identity = null;
_transform = null;
_moveInput = null;
_moveConfig = null;
_attack = null;
_behavior = null;
_separation = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21d84c6a187090e46a1ec7637519c5a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
internal struct EnemyTransformSim : ISimulationComponent
{
public float3 Position;
public float3 Forward;
public quaternion Rotation;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d8966797f1b08648886988017716ea3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,164 @@
using System.Collections.Generic;
using Unity.Collections;
namespace SepCore.Simulation
{
internal abstract class EntityComponentStore
{
internal interface IColumn
{
bool IsCreated { get; }
void EnsureCapacity(int count);
void SwapBackRemove(int index, int lastIndex);
void Clear();
void Dispose();
}
internal sealed class Column<T> : IColumn where T : unmanaged, ISimulationComponent
{
private NativeList<T> _data;
public Column(int capacity)
{
_data = new NativeList<T>(capacity, Allocator.Persistent);
}
public NativeList<T> Data => _data;
public bool IsCreated => _data.IsCreated;
public ref T this[int index] => ref _data.ElementAt(index);
public void EnsureCapacity(int count)
{
if (_data.IsCreated && _data.Capacity < count)
{
_data.Capacity = count;
}
}
public void SwapBackRemove(int index, int lastIndex)
{
if (index != lastIndex)
{
_data[index] = _data[lastIndex];
}
_data.RemoveAt(lastIndex);
}
public void Clear()
{
if (_data.IsCreated)
{
_data.Clear();
}
}
public void Dispose()
{
if (_data.IsCreated)
{
_data.Dispose();
}
_data = default;
}
}
private readonly EntityBinding _binding = new();
private readonly List<IColumn> _columns = new();
private int _count;
public int Count => _count;
public bool IsCreated { get; private set; }
public void EnsureCapacity(int count)
{
for (int i = 0; i < _columns.Count; i++)
{
_columns[i].EnsureCapacity(count);
}
}
public bool TryGetSimulationIndex(int entityId, out int index)
{
return _binding.TryGetSimulationIndex(entityId, out index);
}
public bool RemoveByEntityId(int entityId)
{
if (!_binding.TryGetSimulationIndex(entityId, out int index))
{
return false;
}
int lastIndex = _count - 1;
bool hasMovedRow = index != lastIndex;
int movedEntityId = 0;
if (hasMovedRow)
{
_binding.TryGetEntityId(lastIndex, out movedEntityId);
}
for (int i = 0; i < _columns.Count; i++)
{
_columns[i].SwapBackRemove(index, lastIndex);
}
_binding.UnbindByEntityId(entityId);
if (hasMovedRow)
{
_binding.RemapIndex(movedEntityId, index);
}
_count--;
return true;
}
public void Clear()
{
for (int i = 0; i < _columns.Count; i++)
{
_columns[i].Clear();
}
_binding.Clear();
_count = 0;
}
public virtual void Dispose()
{
for (int i = 0; i < _columns.Count; i++)
{
_columns[i].Dispose();
}
_columns.Clear();
_binding.Clear();
_count = 0;
IsCreated = false;
}
protected Column<T> Register<T>(int capacity) where T : unmanaged, ISimulationComponent
{
Column<T> column = new Column<T>(capacity);
_columns.Add(column);
return column;
}
protected void MarkInitialized()
{
IsCreated = true;
}
protected int AppendRow(int entityId)
{
int index = _count;
_binding.Bind(entityId, index);
_count++;
return index;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea8b87c1fbddf6743a7ce3339dc943a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,6 @@
namespace SepCore.Simulation
{
internal interface ISimulationComponent
{
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f605b392ae908a439b2028cf5a4eb05
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace SepCore.Simulation
{
internal struct PickupIdentitySim : ISimulationComponent
{
public int EntityId;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be2030806e57ecc4f9276b506c800542
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
namespace SepCore.Simulation
{
internal struct PickupRadiusSim : ISimulationComponent
{
public float Radius;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5d8bf9445be1f14bba1ccf4093045f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace SepCore.Simulation
{
internal struct PickupStateSim : ISimulationComponent
{
public const int StateIdle = 0;
public const int StateAbsorbing = 1;
public int State;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e505617febb37c54bbec57f8882d3a8a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
namespace SepCore.Simulation
{
internal sealed class PickupStore : EntityComponentStore
{
private Column<PickupIdentitySim> _identity;
private Column<PickupTransformSim> _transform;
private Column<PickupRadiusSim> _radius;
private Column<PickupStateSim> _state;
public Column<PickupIdentitySim> Identity => _identity;
public Column<PickupTransformSim> Transform => _transform;
public Column<PickupRadiusSim> Radius => _radius;
public Column<PickupStateSim> State => _state;
public void Initialize(int capacity)
{
if (IsCreated)
{
return;
}
Dispose();
_identity = Register<PickupIdentitySim>(capacity);
_transform = Register<PickupTransformSim>(capacity);
_radius = Register<PickupRadiusSim>(capacity);
_state = Register<PickupStateSim>(capacity);
MarkInitialized();
}
public int Add(in PickupIdentitySim identity, in PickupTransformSim transform, in PickupRadiusSim radius,
in PickupStateSim state = default)
{
int index = AppendRow(identity.EntityId);
_identity.Data.Add(identity);
_transform.Data.Add(transform);
_radius.Data.Add(radius);
_state.Data.Add(state);
return index;
}
public override void Dispose()
{
base.Dispose();
_identity = null;
_transform = null;
_radius = null;
_state = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88ed8cf0bf6d9cb459e071ff5b93dc54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
internal struct PickupTransformSim : ISimulationComponent
{
public float3 Position;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8046df9b31763ac448c52a088f9fd9d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
namespace SepCore.Simulation
{
internal struct ProjectileIdentitySim : ISimulationComponent
{
public int EntityId;
public int OwnerEntityId;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52b2efbbc6a2ee94a81016767ef6a6bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace SepCore.Simulation
{
internal struct ProjectileLifeSim : ISimulationComponent
{
public float Age;
public float LifeTime;
public bool Active;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b7fb93b432358948ae81a913129bcf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
namespace SepCore.Simulation
{
internal sealed class ProjectileStore : EntityComponentStore
{
private Column<ProjectileIdentitySim> _identity;
private Column<ProjectileTransformSim> _transform;
private Column<ProjectileVelocitySim> _velocity;
private Column<ProjectileLifeSim> _life;
public Column<ProjectileIdentitySim> Identity => _identity;
public Column<ProjectileTransformSim> Transform => _transform;
public Column<ProjectileVelocitySim> Velocity => _velocity;
public Column<ProjectileLifeSim> Life => _life;
public void Initialize(int capacity)
{
if (IsCreated)
{
return;
}
Dispose();
_identity = Register<ProjectileIdentitySim>(capacity);
_transform = Register<ProjectileTransformSim>(capacity);
_velocity = Register<ProjectileVelocitySim>(capacity);
_life = Register<ProjectileLifeSim>(capacity);
MarkInitialized();
}
public int Add(in ProjectileIdentitySim identity, in ProjectileTransformSim transform,
in ProjectileVelocitySim velocity, in ProjectileLifeSim life)
{
int index = AppendRow(identity.EntityId);
_identity.Data.Add(identity);
_transform.Data.Add(transform);
_velocity.Data.Add(velocity);
_life.Data.Add(life);
return index;
}
public override void Dispose()
{
base.Dispose();
_identity = null;
_transform = null;
_velocity = null;
_life = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6de5ba3f6e96dc845b50ca960aa5ae32
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
internal struct ProjectileTransformSim : ISimulationComponent
{
public float3 Position;
public float3 Forward;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59b7c7f9782759b498e3b1d9a507ca81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
internal struct ProjectileVelocitySim : ISimulationComponent
{
public float3 Velocity;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01a59e095dd7381469d25c36b4a7e878
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -7,17 +7,18 @@ namespace SepCore.Simulation
{
#region Collision Transient
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;
public SimulationStats Stats => new SimulationStats(
_collisionCandidates.IsCreated ? _collisionCandidates.Length : 0,
_areaCollisionRequests.Count,
_lastCollisionQueryCount,
_lastProjectileCollisionQueryCount,
_lastAreaCollisionQueryCount,
_lastCollisionCandidateCount,
_lastProjectileCollisionCandidateCount,
_lastAreaCollisionCandidateCount,
_lastResolvedAreaHitCount,
_lastCollisionCellSize,
_lastCollisionHasEnemyTargets);
private void ResetCollisionRuntimeStats()
{
@@ -45,8 +46,8 @@ namespace SepCore.Simulation
_enemyCollisionBuckets.Clear();
}
private void AddProjectileCollisionQuery(int queryId, in ProjectileJobOutputData projectile, float radius,
int maxTargets = 1)
private void AddProjectileCollisionQuery(int queryId, in ProjectileTransformSim transform,
in ProjectileIdentitySim identity, float radius, int maxTargets = 1)
{
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
{
@@ -56,14 +57,14 @@ namespace SepCore.Simulation
_collisionQueryInputs.Add(new CollisionQueryData
{
QueryId = queryId,
SourceType = CollisionSourceTypeProjectile,
SourceEntityId = projectile.EntityId,
SourceOwnerEntityId = projectile.OwnerEntityId,
SourceType = SimulationCollision.SourceTypeProjectile,
SourceEntityId = identity.EntityId,
SourceOwnerEntityId = identity.OwnerEntityId,
SourceWasActiveAtQueryTime = true,
Position = projectile.Position,
Position = transform.Position,
Radius = radius,
MaxTargets = math.max(1, maxTargets),
ShapeType = CollisionShapeCircle,
ShapeType = SimulationCollision.ShapeCircle,
Direction = new float3(0f, 0f, 1f),
HalfAngleDeg = 180f,
HalfWidth = 0f,
@@ -94,7 +95,7 @@ namespace SepCore.Simulation
_collisionQueryInputs.Add(new CollisionQueryData
{
QueryId = queryId,
SourceType = CollisionSourceTypeArea,
SourceType = SimulationCollision.SourceTypeArea,
SourceEntityId = sourceEntityId,
SourceOwnerEntityId = sourceOwnerEntityId,
SourceWasActiveAtQueryTime = sourceWasActiveAtQueryTime,
@@ -0,0 +1,23 @@
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
#region Enemy Separation Buffers
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
{
InitializeJobDataChannels();
EnsureCapacity(ref _enemySeparationTransforms, enemyCount);
_enemySeparationTransforms.Clear();
if (enemyCount > 0)
{
_enemySeparationTransforms.ResizeUninitialized(enemyCount);
}
EnsureCapacity(ref _enemySeparationBuckets, bucketCapacity);
_enemySeparationBuckets.Clear();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a7f58c069d920f4698662060cb2db0b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,91 +0,0 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
#region Enemy Separation Temporal
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
{
InitializeJobDataChannels();
EnsureCapacity(ref _enemyJobSeparationOutputs, enemyCount);
_enemyJobSeparationOutputs.Clear();
if (enemyCount > 0)
{
_enemyJobSeparationOutputs.ResizeUninitialized(enemyCount);
}
EnsureCapacity(ref _enemySeparationBuckets, bucketCapacity);
_enemySeparationBuckets.Clear();
EnsureCapacity(ref _enemySeparationPreviousPushes, enemyCount);
EnsureCapacity(ref _enemySeparationCurrentPushes, enemyCount);
if (_enemySeparationPreviousPushes.Length < enemyCount)
{
int oldLength = _enemySeparationPreviousPushes.Length;
_enemySeparationPreviousPushes.ResizeUninitialized(enemyCount);
for (int i = oldLength; i < enemyCount; i++)
{
_enemySeparationPreviousPushes[i] = float2.zero;
}
}
else if (_enemySeparationPreviousPushes.Length > enemyCount)
{
_enemySeparationPreviousPushes.ResizeUninitialized(enemyCount);
}
_enemySeparationCurrentPushes.Clear();
if (enemyCount > 0)
{
_enemySeparationCurrentPushes.ResizeUninitialized(enemyCount);
}
}
private void CommitEnemySeparationTemporalBuffers(int enemyCount)
{
if (!_enemySeparationPreviousPushes.IsCreated || !_enemySeparationCurrentPushes.IsCreated)
{
return;
}
int copyCount = math.min(enemyCount,
math.min(_enemySeparationPreviousPushes.Length, _enemySeparationCurrentPushes.Length));
for (int i = 0; i < copyCount; i++)
{
_enemySeparationPreviousPushes[i] = _enemySeparationCurrentPushes[i];
}
}
private void OnEnemyAddedToSeparationTemporalBuffers()
{
if (_enemySeparationPreviousPushes.IsCreated)
{
_enemySeparationPreviousPushes.Add(float2.zero);
}
if (_enemySeparationCurrentPushes.IsCreated)
{
_enemySeparationCurrentPushes.Add(float2.zero);
}
}
private void OnEnemyRemovedFromSeparationTemporalBuffers(int removedIndex)
{
if (_enemySeparationPreviousPushes.IsCreated && removedIndex >= 0 &&
removedIndex < _enemySeparationPreviousPushes.Length)
{
_enemySeparationPreviousPushes.RemoveAtSwapBack(removedIndex);
}
if (_enemySeparationCurrentPushes.IsCreated && removedIndex >= 0 &&
removedIndex < _enemySeparationCurrentPushes.Length)
{
_enemySeparationCurrentPushes.RemoveAtSwapBack(removedIndex);
}
}
#endregion
}
}
@@ -5,14 +5,6 @@ namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
// Shared channel constants plus compatibility fields still reflected by tests.
private const int CollisionSourceTypeProjectile = 1;
private const int CollisionSourceTypeArea = 2;
private const int CollisionShapeCircle = 0;
private const int CollisionShapeSector = 1;
private const int CollisionShapeRectangle = 2;
// Kept as top-level fields because current regression tests reflect them directly.
private NativeList<CollisionQueryData> _collisionQueryInputs;
private readonly List<AreaCollisionRequestData> _areaCollisionRequests = new(16);
}
@@ -1,151 +0,0 @@
using Unity.Mathematics;
using UnityEngine;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
#region Job Data Conversion
private void SyncSimulationStateToJobInputs()
{
InitializeJobDataChannels();
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
EnsureCapacity(ref _projectileJobInputs, _projectiles.Count);
_enemyJobInputs.Clear();
_projectileJobInputs.Clear();
foreach (EnemySimData data in _enemies)
{
_enemyJobInputs.Add(ConvertToEnemyJobInput(data));
}
foreach (ProjectileSimData data in _projectiles)
{
_projectileJobInputs.Add(ConvertToProjectileJobInput(data));
}
}
private void PrepareEnemyJobOutputBuffer(int enemyCount)
{
InitializeJobDataChannels();
EnsureCapacity(ref _enemyJobOutputs, enemyCount);
_enemyJobOutputs.Clear();
if (enemyCount > 0)
{
_enemyJobOutputs.ResizeUninitialized(enemyCount);
}
}
private void PrepareProjectileJobOutputBuffer(int projectileCount)
{
InitializeJobDataChannels();
EnsureCapacity(ref _projectileJobOutputs, projectileCount);
_projectileJobOutputs.Clear();
if (projectileCount > 0)
{
_projectileJobOutputs.ResizeUninitialized(projectileCount);
}
}
private void CopyProjectileInputsToOutputs()
{
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 static EnemyJobInputData ConvertToEnemyJobInput(in EnemySimData enemy)
{
return new EnemyJobInputData
{
EntityId = enemy.EntityId,
Position = new float3(enemy.Position.x, enemy.Position.y, enemy.Position.z),
Forward = new float3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z),
Rotation = new quaternion(enemy.Rotation.x, enemy.Rotation.y, enemy.Rotation.z, enemy.Rotation.w),
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 = new Vector3(enemy.Position.x, enemy.Position.y, enemy.Position.z),
Forward = new Vector3(enemy.Forward.x, enemy.Forward.y, enemy.Forward.z),
Rotation = new Quaternion(enemy.Rotation.value.x, enemy.Rotation.value.y, enemy.Rotation.value.z,
enemy.Rotation.value.w),
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 = new float3(projectile.Position.x, projectile.Position.y, projectile.Position.z),
Forward = new float3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z),
Velocity = new float3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z),
Speed = projectile.Speed,
LifeTime = projectile.LifeTime,
Age = projectile.Age,
Active = projectile.Active,
RemainingLifetime = projectile.RemainingLifetime,
State = projectile.State
};
}
private static ProjectileSimData ConvertToProjectileSimData(in ProjectileJobOutputData projectile)
{
return new ProjectileSimData
{
EntityId = projectile.EntityId,
OwnerEntityId = projectile.OwnerEntityId,
Position = new Vector3(projectile.Position.x, projectile.Position.y, projectile.Position.z),
Forward = new Vector3(projectile.Forward.x, projectile.Forward.y, projectile.Forward.z),
Velocity = new Vector3(projectile.Velocity.x, projectile.Velocity.y, projectile.Velocity.z),
Speed = projectile.Speed,
LifeTime = projectile.LifeTime,
Age = projectile.Age,
Active = projectile.Active,
RemainingLifetime = projectile.RemainingLifetime,
State = projectile.State
};
}
#endregion
}
}
@@ -1,6 +1,5 @@
using System;
using Unity.Collections;
using Unity.Mathematics;
namespace SepCore.Simulation
{
@@ -16,70 +15,44 @@ namespace SepCore.Simulation
}
DisposeJobDataChannels();
_enemyJobInputs = new NativeList<EnemyJobInputData>(64, Allocator.Persistent);
_enemyJobOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
_enemyJobSeparationOutputs = new NativeList<EnemyJobOutputData>(64, Allocator.Persistent);
_enemySeparationPreviousPushes = new NativeList<float2>(64, Allocator.Persistent);
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
_enemies.Initialize(64);
_projectiles.Initialize(64);
_pickups.Initialize(32);
_enemySeparationTransforms = new NativeList<EnemyTransformSim>(64, Allocator.Persistent);
_collectedPickups = new NativeList<int>(64, Allocator.Persistent);
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, 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();
}
private void DisposeJobDataChannels()
{
if (_enemyJobInputs.IsCreated)
_enemies.Dispose();
_projectiles.Dispose();
_pickups.Dispose();
if (_enemySeparationTransforms.IsCreated)
{
_enemyJobInputs.Dispose();
_enemySeparationTransforms.Dispose();
}
_enemyJobInputs = default;
_enemySeparationTransforms = default;
if (_enemyJobOutputs.IsCreated)
if (_collectedPickups.IsCreated)
{
_enemyJobOutputs.Dispose();
_collectedPickups.Dispose();
}
_enemyJobOutputs = default;
_collectedPickups = default;
if (_enemyJobSeparationOutputs.IsCreated)
if (_enemySeparationBuckets.IsCreated)
{
_enemyJobSeparationOutputs.Dispose();
_enemySeparationBuckets.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;
_enemySeparationBuckets = default;
if (_collisionQueryInputs.IsCreated)
{
@@ -95,13 +68,6 @@ namespace SepCore.Simulation
_collisionCandidates = default;
if (_enemySeparationBuckets.IsCreated)
{
_enemySeparationBuckets.Dispose();
}
_enemySeparationBuckets = default;
if (_enemyCollisionBuckets.IsCreated)
{
_enemyCollisionBuckets.Dispose();
@@ -127,24 +93,14 @@ namespace SepCore.Simulation
return;
}
if (_enemyJobInputs.IsCreated)
if (_enemySeparationTransforms.IsCreated)
{
_enemyJobInputs.Clear();
_enemySeparationTransforms.Clear();
}
if (_enemyJobOutputs.IsCreated)
if (_collectedPickups.IsCreated)
{
_enemyJobOutputs.Clear();
}
if (_projectileJobInputs.IsCreated)
{
_projectileJobInputs.Clear();
}
if (_projectileJobOutputs.IsCreated)
{
_projectileJobOutputs.Clear();
_collectedPickups.Clear();
}
if (_collisionQueryInputs.IsCreated)
@@ -157,21 +113,6 @@ namespace SepCore.Simulation
_collisionCandidates.Clear();
}
if (_enemyJobSeparationOutputs.IsCreated)
{
_enemyJobSeparationOutputs.Clear();
}
if (_enemySeparationPreviousPushes.IsCreated)
{
_enemySeparationPreviousPushes.Clear();
}
if (_enemySeparationCurrentPushes.IsCreated)
{
_enemySeparationCurrentPushes.Clear();
}
if (_enemySeparationBuckets.IsCreated)
{
_enemySeparationBuckets.Clear();
@@ -191,13 +132,11 @@ namespace SepCore.Simulation
private bool AreJobDataChannelsUsable()
{
return IsNativeListUsable(_enemyJobInputs) &&
IsNativeListUsable(_enemyJobOutputs) &&
IsNativeListUsable(_enemyJobSeparationOutputs) &&
IsNativeListUsable(_enemySeparationPreviousPushes) &&
IsNativeListUsable(_enemySeparationCurrentPushes) &&
IsNativeListUsable(_projectileJobInputs) &&
IsNativeListUsable(_projectileJobOutputs) &&
return _enemies.IsCreated &&
_projectiles.IsCreated &&
_pickups.IsCreated &&
IsNativeListUsable(_enemySeparationTransforms) &&
IsNativeListUsable(_collectedPickups) &&
IsNativeListUsable(_collisionQueryInputs) &&
IsNativeListUsable(_collisionCandidates) &&
IsNativeMultiHashMapUsable(_enemySeparationBuckets) &&
@@ -1,40 +0,0 @@
using UnityEngine;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
#region Job Output Commit
private void ApplyJobOutputsToSimulationState()
{
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
bool hasEnemyPositionChanged = false;
for (int i = 0; i < enemyCount; i++)
{
EnemyJobOutputData output = _enemyJobOutputs[i];
if (!hasEnemyPositionChanged)
{
Vector3 currentPosition = _enemies[i].Position;
hasEnemyPositionChanged = currentPosition.x != output.Position.x ||
currentPosition.z != output.Position.z;
}
_enemies[i] = ConvertToEnemySimData(output);
}
if (hasEnemyPositionChanged)
{
MarkEnemyTargetSpatialIndexDirty();
}
int projectileCount = Mathf.Min(_projectiles.Count, _projectileJobOutputs.Length);
for (int i = 0; i < projectileCount; i++)
{
_projectiles[i] = ConvertToProjectileSimData(_projectileJobOutputs[i]);
}
}
#endregion
}
}
@@ -0,0 +1,35 @@
using UnityEngine;
namespace SepCore.Simulation
{
public interface ISimulationWorld
{
SimulationStats Stats { get; }
void Tick(in SimulationTickContext context);
void ClearSimulationState();
bool TryGetNearestEnemyEntityId(Vector3 origin, float maxSqrRange, out int enemyEntityId);
bool TryRequestAreaCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius,
int maxTargets = 16);
bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center, float radius,
in Vector3 direction, float halfAngleDeg, int maxTargets = 16);
bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float halfWidth, float halfLength, in Vector3 direction, int maxTargets = 16);
void SyncPlayerMovementInput(Transform playerTransform, bool isMoving, in Vector3 direction, float speed);
void UnregisterPlayerMovement(Transform playerTransform);
void SyncEnemyMovementInput(int entityId, bool isMoving, in Vector3 direction, float speed,
bool avoidEnemyOverlap, float enemyBodyRadius, int separationIterations);
void SyncPlayerAbsorbSettings(float absorbRange, float absorbSpeed, float collectDistance);
void TriggerFullMapMagnet();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f0174cbb31d284749a20c973c0153ca9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,14 +1,11 @@
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
internal struct AreaCollisionHitEventData
{
private struct AreaCollisionHitEventData
{
public int QueryId;
public int SourceEntityId;
public int SourceOwnerEntityId;
public int TargetEntityId;
public float SqrDistance;
}
public int QueryId;
public int SourceEntityId;
public int SourceOwnerEntityId;
public int TargetEntityId;
public float SqrDistance;
}
}
@@ -2,21 +2,18 @@ using UnityEngine;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
internal struct AreaCollisionRequestData
{
private struct AreaCollisionRequestData
{
public int SourceEntityId;
public int SourceOwnerEntityId;
public bool SourceWasActiveAtQueryTime;
public Vector3 Center;
public float Radius;
public int MaxTargets;
public int ShapeType;
public Vector3 Direction;
public float HalfAngleDeg;
public float HalfWidth;
public float HalfLength;
}
public int SourceEntityId;
public int SourceOwnerEntityId;
public bool SourceWasActiveAtQueryTime;
public Vector3 Center;
public float Radius;
public int MaxTargets;
public int ShapeType;
public Vector3 Direction;
public float HalfAngleDeg;
public float HalfWidth;
public float HalfLength;
}
}
@@ -1,48 +0,0 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct BuildEnemySeparationBucketsBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> Inputs;
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
public float CellSize;
public void Execute(int index)
{
BuildEnemySeparationBucket(
index,
Inputs,
Buckets,
CellSize
);
}
}
private static void BuildEnemySeparationBucket(
int index,
NativeArray<EnemyJobOutputData> inputs,
NativeParallelMultiHashMap<long, int>.ParallelWriter buckets,
float cellSize
)
{
EnemyJobOutputData output = inputs[index];
if (!output.AvoidEnemyOverlap)
{
return;
}
float3 position = output.Position;
position.y = 0f;
int cellX = (int)math.floor(position.x / cellSize);
int cellZ = (int)math.floor(position.z / cellSize);
buckets.Add(SeparationCellKey(cellX, cellZ), index);
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e833e91677bb49a4a458c9c8b90099e9
timeCreated: 1771901500
@@ -1,15 +1,12 @@
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
internal struct CollisionCandidateData
{
private struct CollisionCandidateData
{
public int QueryId;
public int SourceType;
public int SourceEntityId;
public int SourceOwnerEntityId;
public int TargetEntityId;
public float SqrDistance;
}
public int QueryId;
public int SourceType;
public int SourceEntityId;
public int SourceOwnerEntityId;
public int TargetEntityId;
public float SqrDistance;
}
}
@@ -2,23 +2,20 @@ using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
internal struct CollisionQueryData
{
private struct CollisionQueryData
{
public int QueryId;
public int SourceType;
public int SourceEntityId;
public int SourceOwnerEntityId;
public bool SourceWasActiveAtQueryTime;
public float3 Position;
public float Radius;
public int MaxTargets;
public int ShapeType;
public float3 Direction;
public float HalfAngleDeg;
public float HalfWidth;
public float HalfLength;
}
public int QueryId;
public int SourceType;
public int SourceEntityId;
public int SourceOwnerEntityId;
public bool SourceWasActiveAtQueryTime;
public float3 Position;
public float Radius;
public int MaxTargets;
public int ShapeType;
public float3 Direction;
public float HalfAngleDeg;
public float HalfWidth;
public float HalfLength;
}
}
@@ -1,22 +0,0 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
private struct EnemyJobInputData
{
public int EntityId;
public float3 Position;
public float3 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;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 171936b8a6b84a8fbacc7651ce473b98
timeCreated: 1771901146
@@ -1,22 +0,0 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
private struct EnemyJobOutputData
{
public int EntityId;
public float3 Position;
public float3 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;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 34ef6fbf123f45ca88d8e7ab8311d543
timeCreated: 1771901204
@@ -1,93 +0,0 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace SepCore.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 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 = input.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 && input.Speed > 0f && sqrDistance > float.Epsilon;
float3 forward = input.Forward;
float3 desiredPosition = currentPosition;
quaternion rotation = input.Rotation;
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 = desiredPosition,
Forward = forward,
Rotation = rotation,
Speed = input.Speed,
AttackRange = attackRange,
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
EnemyBodyRadius = input.EnemyBodyRadius,
SeparationIterations = input.SeparationIterations,
TargetType = input.TargetType,
State = nextState
};
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: abd5519779ed41d4947280eec326ffb1
timeCreated: 1771901472
@@ -1,245 +0,0 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct EnemySeparationBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> Inputs;
[ReadOnly] public NativeParallelMultiHashMap<long, int> Buckets;
[ReadOnly] public NativeArray<float2> PreviousPushes;
public NativeArray<EnemyJobOutputData> Outputs;
public NativeArray<float2> CurrentPushes;
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)
{
ExecuteEnemySeparation(
index,
Inputs,
Buckets,
Outputs,
CellSize,
MaxRadius,
PlayerPosition,
PushDamping,
MaxStepScale,
UseTangentialInAttackRange,
PreviousPushes,
CurrentPushes,
PushSmoothing
);
}
}
private static void ExecuteEnemySeparation(
int index,
NativeArray<EnemyJobOutputData> inputs,
NativeParallelMultiHashMap<long, int> buckets,
NativeArray<EnemyJobOutputData> outputs,
float cellSize,
float maxRadius,
float3 playerPosition,
float pushDamping,
float maxStepScale,
bool useTangentialInAttackRange,
NativeArray<float2> previousPushes,
NativeArray<float2> currentPushes,
float pushSmoothing
)
{
currentPushes[index] = float2.zero;
EnemyJobOutputData self = inputs[index];
if (!self.AvoidEnemyOverlap)
{
outputs[index] = self;
return;
}
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 = self.EnemyBodyRadius > 0f ? self.EnemyBodyRadius : 0.45f;
int iterations = self.SeparationIterations > 0 ? self.SeparationIterations : 1;
int queryRange = math.max(1, (int)math.ceil((selfRadius + maxRadius) / cellSize));
for (int iter = 0; iter < iterations; iter++)
{
int cellX = (int)math.floor(candidate.x / cellSize);
int cellZ = (int)math.floor(candidate.z / cellSize);
float3 pushAccumulation = float3.zero;
for (int dx = -queryRange; dx <= queryRange; dx++)
{
for (int dz = -queryRange; dz <= queryRange; dz++)
{
long key = SeparationCellKey(cellX + dx, cellZ + dz);
if (!buckets.TryGetFirstValue(key, out int otherIndex,
out NativeParallelMultiHashMapIterator<long> iterator))
{
continue;
}
do
{
if (otherIndex == index)
{
continue;
}
EnemyJobOutputData other = inputs[otherIndex];
if (!other.AvoidEnemyOverlap)
{
continue;
}
float otherRadius = other.EnemyBodyRadius > 0f ? other.EnemyBodyRadius : 0.45f;
float minDistance = selfRadius + otherRadius;
float minDistanceSqr = minDistance * minDistance;
float3 otherPosition = other.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 = previousPushes[index];
float3 previousPush = new float3(previousPush2.x, 0f, previousPush2.y);
float3 smoothedPush = SmoothSeparationPush(framePush, previousPush, pushSmoothing);
if (useTangentialInAttackRange && self.State == EnemyStateInAttackRange)
{
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;
currentPushes[index] = 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);
}
outputs[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));
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 94a77477c3a8410cb58b5028dd5d2c63
timeCreated: 1771901543
@@ -1,22 +0,0 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
private struct ProjectileJobInputData
{
public int EntityId;
public int OwnerEntityId;
public float3 Position;
public float3 Forward;
public float3 Velocity;
public float Speed;
public float LifeTime;
public float Age;
public bool Active;
public float RemainingLifetime;
public int State;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 334ca3869d7940058256ba5419573637
timeCreated: 1771901235
@@ -1,22 +0,0 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
private struct ProjectileJobOutputData
{
public int EntityId;
public int OwnerEntityId;
public float3 Position;
public float3 Forward;
public float3 Velocity;
public float Speed;
public float LifeTime;
public float Age;
public bool Active;
public float RemainingLifetime;
public int State;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b5c4a144914548379d9991d8ef061da7
timeCreated: 1771901266
@@ -1,118 +0,0 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct ProjectileMovementBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<ProjectileJobInputData> Inputs;
public NativeArray<ProjectileJobOutputData> 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 static void ExecuteProjectileMovement(
int index,
NativeArray<ProjectileJobInputData> inputs,
NativeArray<ProjectileJobOutputData> 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;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 49001e849c1b4afdaa37d851b9b76866
timeCreated: 1771901884
@@ -1,132 +0,0 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
[BurstCompile]
private struct QueryCollisionCandidatesBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<CollisionQueryData> Queries;
[ReadOnly] public NativeParallelMultiHashMap<long, int> EnemyBuckets;
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
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 = (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)
{
float3 playerPosition = PlayerPosition;
if (query.SourceType == CollisionSourceTypeArea)
{
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 = SeparationCellKey(centerCellX + dx, centerCellZ + dz);
if (!EnemyBuckets.TryGetFirstValue(key, out int enemyIndex,
out NativeParallelMultiHashMapIterator<long> iterator))
{
continue;
}
do
{
if (enemyIndex < 0 || enemyIndex >= EnemyOutputs.Length)
{
continue;
}
EnemyJobOutputData enemy = EnemyOutputs[enemyIndex];
if (enemy.EntityId == query.SourceOwnerEntityId)
{
continue;
}
float deltaY = query.SourceType == CollisionSourceTypeArea
? 0f
: enemy.Position.y - query.Position.y;
float3 delta = new float3(
enemy.Position.x - query.Position.x,
deltaY,
enemy.Position.z - query.Position.z);
float sqrDistance = math.lengthsq(delta);
float targetRadius = query.SourceType == CollisionSourceTypeArea
? math.max(0f, enemy.EnemyBodyRadius)
: 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 = enemy.EntityId,
SqrDistance = sqrDistance
});
selectedCount++;
if (selectedCount >= query.MaxTargets)
{
reachedLimit = true;
break;
}
} while (EnemyBuckets.TryGetNextValue(out enemyIndex, ref iterator));
}
}
}
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 81aa43bb35f547c48b867eafa17a3857
timeCreated: 1771914779
@@ -1,8 +1,5 @@
using SepCore.Debugger;
using SepCore.Entity;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
@@ -24,7 +21,7 @@ namespace SepCore.Simulation
return;
}
int projectileCount = _projectileJobOutputs.Length;
int projectileCount = _projectiles.Count;
int areaQueryCount = _areaCollisionRequests.Count;
if (projectileCount == 0 && areaQueryCount == 0)
{
@@ -42,13 +39,15 @@ namespace SepCore.Simulation
{
for (int i = 0; i < projectileCount; i++)
{
ProjectileJobOutputData projectile = _projectileJobOutputs[i];
if (!projectile.Active || projectile.State != ProjectileStateActive)
if (!_projectiles.Life[i].Active)
{
continue;
}
AddProjectileCollisionQuery(queryId, in projectile, queryRadius, maxCandidatesPerQuery);
ProjectileTransformSim projectileTransform = _projectiles.Transform[i];
ProjectileIdentitySim projectileIdentity = _projectiles.Identity[i];
AddProjectileCollisionQuery(queryId, in projectileTransform, in projectileIdentity, queryRadius,
maxCandidatesPerQuery);
queryId++;
projectileQueryCount++;
}
@@ -80,14 +79,14 @@ namespace SepCore.Simulation
_lastProjectileCollisionCandidateCount = 0;
_lastAreaCollisionCandidateCount = 0;
_lastCollisionCellSize = 0f;
_lastCollisionHasEnemyTargets = _enemyJobOutputs.Length > 0;
_lastCollisionHasEnemyTargets = _enemies.Count > 0;
return;
}
float autoCellSize = maxQueryRadius * 2f;
float configuredCellSize = _projectileCollisionCellSize > 0f ? _projectileCollisionCellSize : autoCellSize;
float cellSize = Mathf.Max(0.1f, configuredCellSize);
bool hasEnemyTargets = _enemyJobOutputs.Length > 0;
bool hasEnemyTargets = _enemies.Count > 0;
_lastCollisionCellSize = cellSize;
_lastCollisionHasEnemyTargets = hasEnemyTargets;
@@ -95,13 +94,17 @@ namespace SepCore.Simulation
{
if (hasEnemyTargets)
{
BuildEnemyCollisionBuckets(cellSize);
CollisionQuerySystem.BuildEnemyCollisionBuckets(_enemies, _enemyCollisionBuckets, cellSize);
}
}
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
{
_collisionCandidateQueryScheduled = ScheduleCollisionCandidateQueryJob(cellSize, hasEnemyTargets,
bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId,
out float3 playerPosition);
_collisionCandidateQueryScheduled = CollisionQuerySystem.ScheduleCandidateQuery(
_collisionQueryInputs, _collisionCandidates, _enemyCollisionBuckets, _enemies, hasEnemyTargets,
hasPlayerTarget, playerTargetEntityId, playerPosition, cellSize,
out _collisionCandidateQueryHandle);
}
}
@@ -120,78 +123,6 @@ namespace SepCore.Simulation
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
}
private void BuildEnemyCollisionBuckets(float cellSize)
{
_enemyCollisionBuckets.Clear();
int enemyCount = _enemyJobOutputs.Length;
if (enemyCount <= 0)
{
return;
}
BuildCollisionBucketsBurstJob job = new BuildCollisionBucketsBurstJob
{
EnemyOutputs = _enemyJobOutputs.AsArray(),
Buckets = _enemyCollisionBuckets.AsParallelWriter(),
CellSize = cellSize
};
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
{
JobHandle handle = job.Schedule(enemyCount, 64);
handle.Complete();
}
}
[BurstCompile]
private struct BuildCollisionBucketsBurstJob : IJobParallelFor
{
[ReadOnly] public NativeArray<EnemyJobOutputData> EnemyOutputs;
public NativeParallelMultiHashMap<long, int>.ParallelWriter Buckets;
public float CellSize;
public void Execute(int index)
{
EnemyJobOutputData enemy = EnemyOutputs[index];
int cellX = (int)math.floor(enemy.Position.x / CellSize);
int cellZ = (int)math.floor(enemy.Position.z / CellSize);
Buckets.Add(SeparationCellKey(cellX, cellZ), index);
}
}
private bool ScheduleCollisionCandidateQueryJob(float cellSize, bool hasEnemyTargets, out JobHandle handle)
{
handle = default;
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated)
{
return false;
}
_collisionCandidates.Clear();
int queryCount = _collisionQueryInputs.Length;
if (queryCount == 0)
{
return false;
}
bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId, out float3 playerPosition);
QueryCollisionCandidatesBurstJob job = new QueryCollisionCandidatesBurstJob
{
Queries = _collisionQueryInputs.AsArray(),
EnemyBuckets = _enemyCollisionBuckets,
EnemyOutputs = _enemyJobOutputs.AsArray(),
Candidates = _collisionCandidates.AsParallelWriter(),
HasEnemyTargets = hasEnemyTargets,
HasPlayerTarget = hasPlayerTarget,
PlayerTargetEntityId = playerTargetEntityId,
PlayerPosition = playerPosition,
CellSize = cellSize
};
handle = job.Schedule(queryCount, 64);
return true;
}
private void CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount)
{
projectileCandidateCount = 0;
@@ -205,11 +136,11 @@ namespace SepCore.Simulation
for (int i = 0; i < _collisionCandidates.Length; i++)
{
CollisionCandidateData candidate = _collisionCandidates[i];
if (candidate.SourceType == CollisionSourceTypeProjectile)
if (candidate.SourceType == SimulationCollision.SourceTypeProjectile)
{
projectileCandidateCount++;
}
else if (candidate.SourceType == CollisionSourceTypeArea)
else if (candidate.SourceType == SimulationCollision.SourceTypeArea)
{
areaCandidateCount++;
}
@@ -11,14 +11,14 @@ namespace SepCore.Simulation
float radius, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
maxTargets, CollisionShapeCircle, Vector3.forward, 180f, 0f, 0f);
maxTargets, SimulationCollision.ShapeCircle, Vector3.forward, 180f, 0f, 0f);
}
public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
maxTargets, CollisionShapeSector, direction, halfAngleDeg, 0f, 0f);
maxTargets, SimulationCollision.ShapeSector, direction, halfAngleDeg, 0f, 0f);
}
public bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
@@ -28,7 +28,7 @@ namespace SepCore.Simulation
float safeHalfLength = Mathf.Max(0.01f, halfLength);
float boundingRadius = Mathf.Sqrt(safeHalfWidth * safeHalfWidth + safeHalfLength * safeHalfLength);
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, boundingRadius,
maxTargets, CollisionShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength);
maxTargets, SimulationCollision.ShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength);
}
private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId,
@@ -36,7 +36,7 @@ namespace SepCore.Simulation
for (int i = 0; i < _collisionCandidates.Length; i++)
{
CollisionCandidateData candidate = _collisionCandidates[i];
if (candidate.SourceType == CollisionSourceTypeProjectile)
if (candidate.SourceType == SimulationCollision.SourceTypeProjectile)
{
int projectileEntityId = candidate.SourceEntityId;
if (_projectileResolvedEntityIds.Contains(projectileEntityId))
@@ -44,7 +44,7 @@ namespace SepCore.Simulation
continue;
}
if (!TryGetActiveProjectileSimData(projectileEntityId, out _, out ProjectileSimData projectile))
if (!TryGetActiveProjectileIndex(projectileEntityId, out int projectileIndex))
{
_projectileResolvedEntityIds.Add(projectileEntityId);
continue;
@@ -53,14 +53,15 @@ namespace SepCore.Simulation
bool shouldExpireProjectile = true;
bool shouldDispatchPresentation = false;
int damage = 0;
Vector3 hitPosition = projectile.Position;
Vector3 projectilePosition = ToVector3(_projectiles.Transform[projectileIndex].Position);
Vector3 hitPosition = projectilePosition;
if (TryGetAliveTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
{
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId);
shouldExpireProjectile = ResolveProjectileHitAgainstTarget(target, sourceEntity,
ownerEntity,
in projectile,
in projectilePosition,
out damage, out hitPosition, out shouldDispatchPresentation);
}
@@ -79,7 +80,7 @@ namespace SepCore.Simulation
continue;
}
if (candidate.SourceType == CollisionSourceTypeArea)
if (candidate.SourceType == SimulationCollision.SourceTypeArea)
{
long dedupKey = (((long)candidate.QueryId) << 32) ^ (uint)candidate.TargetEntityId;
if (!_areaCollisionHitDedupKeys.Add(dedupKey))
@@ -112,11 +113,11 @@ namespace SepCore.Simulation
private bool ResolveProjectileHitAgainstTarget(TargetableObject target, EntityBase sourceEntity,
EntityBase ownerEntity,
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
in Vector3 projectilePosition, out int damage, out Vector3 hitPosition,
out bool shouldDispatchPresentation)
{
damage = 0;
hitPosition = projectile.Position;
hitPosition = projectilePosition;
shouldDispatchPresentation = false;
if (target == null || !target.Available || target.IsDead)
@@ -167,7 +168,7 @@ namespace SepCore.Simulation
{
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
if (!TryGetCollisionQueryByQueryId(hitEvent.QueryId, out CollisionQueryData query) ||
query.SourceType != CollisionSourceTypeArea)
query.SourceType != SimulationCollision.SourceTypeArea)
{
continue;
}
@@ -238,9 +239,9 @@ namespace SepCore.Simulation
return 0f;
}
if (target is EnemyBase && TryGetEnemyData(target.Id, out EnemySimData enemyData))
if (target is EnemyBase && _enemies.TryGetSimulationIndex(target.Id, out int enemyIndex))
{
return Mathf.Max(0f, enemyData.EnemyBodyRadius);
return Mathf.Max(0f, _enemies.MoveConfig[enemyIndex].BodyRadius);
}
MovementComponent movementComponent = target.GetComponent<MovementComponent>();
@@ -267,7 +268,7 @@ namespace SepCore.Simulation
return false;
}
if (query.ShapeType == CollisionShapeRectangle)
if (query.ShapeType == SimulationCollision.ShapeRectangle)
{
Vector3 forwardRect = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
forwardRect.y = 0f;
@@ -288,7 +289,7 @@ namespace SepCore.Simulation
return Mathf.Abs(forwardDistance) <= halfLength && Mathf.Abs(lateralDistance) <= halfWidth;
}
if (query.ShapeType != CollisionShapeSector)
if (query.ShapeType != SimulationCollision.ShapeSector)
{
return true;
}
@@ -314,47 +315,34 @@ namespace SepCore.Simulation
return angle <= halfAngle;
}
private bool TryGetActiveProjectileSimData(int projectileEntityId, out int simulationIndex,
out ProjectileSimData projectile)
private bool TryGetActiveProjectileIndex(int projectileEntityId, out int simulationIndex)
{
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)
if (!_projectiles.TryGetSimulationIndex(projectileEntityId, out int foundIndex) ||
!_projectiles.Life[foundIndex].Active)
{
return false;
}
simulationIndex = foundIndex;
projectile = data;
return true;
}
private bool MarkProjectileAsExpired(int projectileEntityId)
{
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
if (!_projectiles.TryGetSimulationIndex(projectileEntityId, out int simulationIndex))
{
return false;
}
ProjectileSimData projectile = _projectiles[simulationIndex];
projectile.Active = false;
projectile.State = ProjectileStateExpired;
projectile.RemainingLifetime = 0f;
if (projectile.LifeTime > 0f && projectile.Age < projectile.LifeTime)
ProjectileLifeSim life = _projectiles.Life[simulationIndex];
life.Active = false;
if (life.LifeTime > 0f && life.Age < life.LifeTime)
{
projectile.Age = projectile.LifeTime;
life.Age = life.LifeTime;
}
_projectiles[simulationIndex] = projectile;
_projectiles.Life[simulationIndex] = life;
return true;
}
@@ -1,5 +1,5 @@
using SepCore.Debugger;
using Unity.Collections;
using SepCore.Debugger;
using SepCore.Entity;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
@@ -8,7 +8,36 @@ namespace SepCore.Simulation
{
public sealed partial class SimulationWorld
{
// Orchestrates per-tick simulation pipeline and enemy movement/separation jobs.
// Orchestrates the per-tick simulation pipeline over the component systems.
private float _playerAbsorbRange = -1f;
private float _playerAbsorbSpeed = -1f;
private float _playerCollectDistance = -1f;
public void SyncPlayerAbsorbSettings(float absorbRange, float absorbSpeed, float collectDistance)
{
_playerAbsorbRange = absorbRange;
_playerAbsorbSpeed = absorbSpeed;
_playerCollectDistance = collectDistance;
}
private float ResolveAbsorbRange()
{
if (_playerAbsorbRange >= 0f) return _playerAbsorbRange;
return GameEntry.Luban != null ? GameEntry.Luban.Global.DefaultAbsorbRange / 1000f : 2f;
}
private float ResolveAbsorbSpeed()
{
if (_playerAbsorbSpeed >= 0f) return _playerAbsorbSpeed;
return GameEntry.Luban != null ? GameEntry.Luban.Global.AbsorbSpeed / 1000f : 8f;
}
private float ResolveCollectDistance()
{
if (_playerCollectDistance >= 0f) return _playerCollectDistance;
return GameEntry.Luban != null ? GameEntry.Luban.Global.CollectDistance / 1000f : 0.5f;
}
[Header("敌人互斥参数")]
[Tooltip("敌人互斥分桶使用的网格尺寸。小于等于 0 时,将根据敌人体积半径自动计算。")]
[SerializeField]
@@ -46,32 +75,31 @@ namespace SepCore.Simulation
JobHandle enemySeparationHandle = default;
bool hasEnemySeparationJob = false;
bool hasEnemySeparationCandidates = false;
int enemySeparationCount = 0;
bool hasMovingEnemy = false;
int enemyCount = 0;
float enemySeparationMaxRadius = 0.45f;
// 2. BuildInput 阶段:
// 把 _enemies/_projectiles 同步到 Native 输入,准备输出缓冲;
// 统计是否需要敌人分离 Job;
// 预估并准备碰撞查询/候选缓冲。
// 扫描敌人组件统计是否需要分离系统以及碰撞查询容量。
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
{
SyncSimulationStateToJobInputs();
int enemyCount = _enemyJobInputs.Length;
int projectileCount = _projectileJobInputs.Length;
PrepareEnemyJobOutputBuffer(enemyCount);
PrepareProjectileJobOutputBuffer(projectileCount);
enemyCount = _enemies.Count;
enemySeparationCount = enemyCount;
for (int i = 0; i < enemyCount; i++)
{
EnemyJobInputData input = _enemyJobInputs[i];
if (!input.AvoidEnemyOverlap)
if (_enemies.MoveInput[i].Speed > 0f)
{
hasMovingEnemy = true;
}
EnemyMoveConfigSim config = _enemies.MoveConfig[i];
if (!config.AvoidOverlap)
{
continue;
}
hasEnemySeparationCandidates = true;
float radius = input.EnemyBodyRadius > 0f ? input.EnemyBodyRadius : 0.45f;
float radius = config.BodyRadius > 0f ? config.BodyRadius : 0.45f;
if (radius > enemySeparationMaxRadius)
{
enemySeparationMaxRadius = radius;
@@ -90,34 +118,53 @@ namespace SepCore.Simulation
int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery);
int areaExpectedCount = EstimatePendingAreaCollisionCandidateCountFromRequests();
int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount);
int bucketCapacity = Mathf.Max(256, _enemies.Count * 2 + queryCount);
int bucketCapacity = Mathf.Max(256, enemyCount * 2 + queryCount);
PrepareCollisionQueryAndCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
int pickupCount = _pickups.Count;
if (pickupCount > 0)
{
EnsureCapacity(ref _collectedPickups, pickupCount);
_collectedPickups.Clear();
}
}
// 3. StateUpdate 阶段(调度两个移动 Job)
// 3. StateUpdate 阶段:敌人/投射物/掉落物移动系统
JobHandle pickupMovementHandle = default;
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
{
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
float3 planarPlayerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
float3 playerPosition = new float3(context.PlayerPosition.x, context.PlayerPosition.y,
context.PlayerPosition.z);
enemyMovementHandle = EnemyMovementSystem.Schedule(_enemies, context.DeltaTime, planarPlayerPosition,
default);
projectileMovementHandle = ProjectileMovementSystem.Schedule(_projectiles, context.DeltaTime,
playerPosition, _projectileMaxDistanceFromPlayer, _projectileMaxVerticalOffsetFromPlayer, default);
if (_pickups.Count > 0)
{
float absorbRange = ResolveAbsorbRange();
float absorbSpeed = ResolveAbsorbSpeed();
float collectDistance = ResolveCollectDistance();
pickupMovementHandle = PickupMovementSystem.Schedule(_pickups, _collectedPickups, context.DeltaTime,
planarPlayerPosition, absorbRange, absorbSpeed, collectDistance, default);
}
}
// 4. Schedule 阶段(可选分离 Job)
// 如果有需要分离的敌人,调度:
// BuildEnemySeparationBucketsBurstJob(依赖 EnemyMovement)
// EnemySeparationBurstJob(依赖分桶 Job)
// 然后把“敌人链路 handle”和“投射物移动 handle”合并。
// 4. Schedule 阶段:可选敌人分离系统
JobHandle simulationHandle;
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
{
hasEnemySeparationJob = TryScheduleEnemySeparationFromJobOutput(
hasEnemySeparationJob = TryScheduleEnemySeparationJob(
in context,
enemyMovementHandle,
hasEnemySeparationCandidates,
enemySeparationCount,
enemyCount,
enemySeparationMaxRadius,
out enemySeparationHandle);
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
JobHandle movementHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
simulationHandle = JobHandle.CombineDependencies(movementHandle, pickupMovementHandle);
}
// 5. Complete 阶段:等待上述 Job 全部完成。
@@ -126,14 +173,19 @@ namespace SepCore.Simulation
simulationHandle.Complete();
}
// 6. 主线程后处理阶段:
// - 把分离结果覆盖回敌人输出(如果有分离 Job)
// - 构建碰撞候选(投射物查询 + area 请求查询 + 网格筛选)
// 6. 主线程提交阶段:
// - 分离结果写回敌人 Transform 组件
// - 有位移时标记目标选择索引失效
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
{
if (hasEnemySeparationJob)
{
CommitEnemySeparationFromJobOutput(enemySeparationCount);
EnemySeparationSystem.Commit(_enemies, _enemySeparationTransforms, enemyCount);
}
if (hasMovingEnemy || hasEnemySeparationJob)
{
MarkEnemyTargetSpatialIndexDirty();
}
}
@@ -144,137 +196,131 @@ namespace SepCore.Simulation
}
// 7. MainThreadCommit 阶段
// - ApplyJobOutputsToSimulationState(写回 _enemies/_projectiles)
// - ResolveCollisionCandidatesOnMainThread(命中结算、事件、范围碰撞)
// - RecycleInactiveAndExpiredProjectiles(隐藏并移除失效投射物)
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
{
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
{
ApplyJobOutputsToSimulationState();
ResolveCollisionCandidatesOnMainThread();
RecycleInactiveAndExpiredProjectiles();
ResolveCollectedPickupsOnMainThread();
}
}
}
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
private void ResolveCollectedPickupsOnMainThread()
{
int enemyCount = _enemyJobInputs.Length;
if (enemyCount == 0)
if (!_collectedPickups.IsCreated || _collectedPickups.Length == 0)
{
return default;
return;
}
if (context.DeltaTime <= 0f)
Player player = TryGetPlayer();
for (int i = 0; i < _collectedPickups.Length; i++)
{
CopyEnemyInputsToOutputs();
return default;
}
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
{
Inputs = inputArray,
Outputs = outputArray,
DeltaTime = context.DeltaTime,
PlayerPosition = playerPosition
};
return burstJob.Schedule(enemyCount, 64);
}
private void CopyEnemyInputsToOutputs()
{
for (int i = 0; i < _enemyJobInputs.Length; i++)
{
EnemyJobInputData input = _enemyJobInputs[i];
_enemyJobOutputs[i] = new EnemyJobOutputData
int entityId = _collectedPickups[i];
EntityBase entity = TryGetEntityById(entityId);
if (entity == null || !entity.Available)
{
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
};
continue;
}
if (entity is CoinEntity coin)
{
if (player != null)
{
coin.TryCollect(player);
}
else
{
var entityComponent = GameEntry.Entity;
if (entityComponent != null)
{
entityComponent.HideEntity(coin);
}
}
}
else if (entity is ExpEntity exp)
{
if (player != null)
{
exp.TryCollect(player);
}
else
{
var entityComponent = GameEntry.Entity;
if (entityComponent != null)
{
entityComponent.HideEntity(exp);
}
}
}
else
{
var entityComponent = GameEntry.Entity;
if (entityComponent != null)
{
entityComponent.HideEntity(entity);
}
}
}
_collectedPickups.Clear();
}
private bool TryScheduleEnemySeparationFromJobOutput(in SimulationTickContext context, JobHandle dependency,
private Player TryGetPlayer()
{
if (_playerMovementTransform != null)
{
Player player = _playerMovementTransform.GetComponent<Player>();
if (player != null && player.Available)
{
return player;
}
}
var entityComponent = GameEntry.Entity;
if (entityComponent != null && GameEntry.Luban != null)
{
var group = entityComponent.GetEntityGroup(GameEntry.Luban.Global.PlayerGroupName);
if (group != null && group.EntityCount > 0)
{
var entities = group.GetAllEntities();
for (int i = 0; i < entities.Length; i++)
{
if (entities[i] is UnityGameFramework.Runtime.Entity ugfEntity &&
ugfEntity.Logic is Player player && player.Available)
{
return player;
}
}
}
}
return null;
}
private bool TryScheduleEnemySeparationJob(in SimulationTickContext context, JobHandle dependency,
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
{
separationHandle = dependency;
if (enemyCount <= 0 || !hasSeparationCandidates)
{
separationHandle = dependency;
return false;
}
float autoCellSize = maxRadius * 2f;
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
float cellSize = Mathf.Max(0.1f, configuredCellSize);
EnemySeparationSettings settings = new EnemySeparationSettings(
Mathf.Max(0.1f, configuredCellSize),
maxRadius,
Mathf.Clamp(_enemySeparationPushDamping, 0f, 2f),
Mathf.Max(0.1f, _enemySeparationMaxStepScale),
_enemySeparationUseTangentialInAttackRange,
Mathf.Clamp01(_enemySeparationPushSmoothing));
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
float pushDamping = Mathf.Clamp(_enemySeparationPushDamping, 0f, 2f);
float maxStepScale = Mathf.Max(0.1f, _enemySeparationMaxStepScale);
bool useTangentialInAttackRange = _enemySeparationUseTangentialInAttackRange;
float pushSmoothing = Mathf.Clamp01(_enemySeparationPushSmoothing);
NativeArray<EnemyJobOutputData> inputArray = _enemyJobOutputs.AsArray();
NativeArray<EnemyJobOutputData> separatedOutputArray = _enemyJobSeparationOutputs.AsArray();
NativeArray<float2> previousPushes = _enemySeparationPreviousPushes.AsArray();
NativeArray<float2> currentPushes = _enemySeparationCurrentPushes.AsArray();
BuildEnemySeparationBucketsBurstJob buildJob = new BuildEnemySeparationBucketsBurstJob
{
Inputs = inputArray,
Buckets = _enemySeparationBuckets.AsParallelWriter(),
CellSize = cellSize
};
JobHandle buildHandle = buildJob.Schedule(enemyCount, 64, dependency);
EnemySeparationBurstJob separationJob = new EnemySeparationBurstJob
{
Inputs = inputArray,
Buckets = _enemySeparationBuckets,
PreviousPushes = previousPushes,
Outputs = separatedOutputArray,
CurrentPushes = currentPushes,
CellSize = cellSize,
MaxRadius = maxRadius,
PlayerPosition = playerPosition,
PushDamping = pushDamping,
MaxStepScale = maxStepScale,
UseTangentialInAttackRange = useTangentialInAttackRange,
PushSmoothing = pushSmoothing
};
separationHandle = separationJob.Schedule(enemyCount, 64, buildHandle);
separationHandle = EnemySeparationSystem.Schedule(_enemies, _enemySeparationBuckets,
_enemySeparationTransforms, in settings, playerPosition, dependency);
return true;
}
private void CommitEnemySeparationFromJobOutput(int enemyCount)
{
if (enemyCount <= 0)
{
return;
}
CommitEnemySeparationTemporalBuffers(enemyCount);
for (int i = 0; i < enemyCount; i++)
{
_enemyJobOutputs[i] = _enemyJobSeparationOutputs[i];
}
}
private static long SeparationCellKey(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
}
}
@@ -1,7 +1,4 @@
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine;
namespace SepCore.Simulation
{
@@ -16,45 +13,6 @@ namespace SepCore.Simulation
[SerializeField]
private float _projectileMaxVerticalOffsetFromPlayer = 30f;
#region Projectile Movement Job
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
{
int projectileCount = _projectileJobInputs.Length;
if (projectileCount == 0)
{
return default;
}
if (context.DeltaTime <= 0f)
{
CopyProjectileInputsToOutputs();
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<ProjectileJobInputData> inputArray = _projectileJobInputs.AsArray();
NativeArray<ProjectileJobOutputData> outputArray = _projectileJobOutputs.AsArray();
ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob
{
Inputs = inputArray,
Outputs = outputArray,
DeltaTime = context.DeltaTime,
PlayerPosition = playerPosition,
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
};
return burstJob.Schedule(projectileCount, 64);
}
#endregion
#region Projectile Cleanup
private void RecycleInactiveAndExpiredProjectiles()
@@ -62,13 +20,13 @@ namespace SepCore.Simulation
_projectileRecycleEntityIds.Clear();
for (int i = 0; i < _projectiles.Count; i++)
{
ProjectileSimData projectile = _projectiles[i];
if (!ShouldRecycleProjectileSimData(projectile))
ProjectileLifeSim life = _projectiles.Life[i];
if (life.Active && (life.LifeTime <= 0f || life.Age < life.LifeTime))
{
continue;
}
_projectileRecycleEntityIds.Add(projectile.EntityId);
_projectileRecycleEntityIds.Add(_projectiles.Identity[i].EntityId);
}
if (_projectileRecycleEntityIds.Count == 0)
@@ -91,21 +49,6 @@ namespace SepCore.Simulation
_projectileRecycleEntityIds.Clear();
}
private static bool ShouldRecycleProjectileSimData(in ProjectileSimData projectile)
{
if (!projectile.Active)
{
return true;
}
if (projectile.State == ProjectileStateExpired)
{
return true;
}
return projectile.LifeTime > 0f && projectile.Age >= projectile.LifeTime;
}
#endregion
}
}
@@ -32,40 +32,53 @@ namespace SepCore.Simulation
continue;
}
if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData))
if (!_world._enemies.TryGetSimulationIndex(enemyEntity.Id, out int simulationIndex))
{
continue;
}
ApplyEnemyPresentation(enemyEntity, enemyData);
EnemyTransformSim enemyData = _world._enemies.Transform[simulationIndex];
ApplyEnemyPresentation(enemyEntity, in enemyData);
}
}
var projectiles = _world._projectiles;
for (int i = 0; i < projectiles.Count; i++)
{
ProjectileSimData projectileData = projectiles[i];
if (!projectileData.Active || projectileData.State != ProjectileStateActive)
if (!projectiles.Life[i].Active)
{
continue;
}
EntityBase projectileEntity = TryGetEntityById(projectileData.EntityId);
EntityBase projectileEntity = TryGetEntityById(projectiles.Identity[i].EntityId);
if (projectileEntity == null || !projectileEntity.Available)
{
continue;
}
ApplyProjectilePresentation(projectileEntity, projectileData);
ProjectileTransformSim projectileData = projectiles.Transform[i];
ApplyProjectilePresentation(projectileEntity, in projectileData);
}
var pickups = _world._pickups;
for (int i = 0; i < pickups.Count; i++)
{
EntityBase pickupEntity = TryGetEntityById(pickups.Identity[i].EntityId);
if (pickupEntity == null || !pickupEntity.Available)
{
continue;
}
pickupEntity.CachedTransform.position = ToVector3(pickups.Transform[i].Position);
}
}
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemyTransformSim enemyData)
{
Transform enemyTransform = enemyEntity.CachedTransform;
enemyTransform.position = enemyData.Position;
enemyTransform.position = ToVector3(enemyData.Position);
Quaternion rotation = enemyData.Rotation;
Quaternion rotation = ToQuaternion(enemyData.Rotation);
float rotationMagnitude = Mathf.Abs(rotation.x) + Mathf.Abs(rotation.y) + Mathf.Abs(rotation.z) +
Mathf.Abs(rotation.w);
if (rotationMagnitude > float.Epsilon)
@@ -74,7 +87,7 @@ namespace SepCore.Simulation
return;
}
Vector3 forward = enemyData.Forward;
Vector3 forward = ToVector3(enemyData.Forward);
forward.y = 0f;
if (forward.sqrMagnitude > float.Epsilon)
{
@@ -83,7 +96,7 @@ namespace SepCore.Simulation
}
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
in ProjectileSimData projectileData)
in ProjectileTransformSim projectileData)
{
Transform projectileTransform = projectileEntity.CachedTransform;
if (projectileTransform == null)
@@ -91,8 +104,8 @@ namespace SepCore.Simulation
return;
}
projectileTransform.position = projectileData.Position;
Vector3 forward = projectileData.Forward;
projectileTransform.position = ToVector3(projectileData.Position);
Vector3 forward = ToVector3(projectileData.Forward);
if (forward.sqrMagnitude <= float.Epsilon)
{
return;
@@ -13,7 +13,6 @@ namespace SepCore.Simulation
public bool AvoidEnemyOverlap;
public float EnemyBodyRadius;
public int SeparationIterations;
public int TargetType;
public int State;
}
}
@@ -9,11 +9,8 @@ namespace SepCore.Simulation
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;
}
}
@@ -0,0 +1,17 @@
using Unity.Mathematics;
namespace SepCore.Simulation
{
internal static class SimulationCell
{
public static long Key(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
public static int ToCell(float value, float cellSize)
{
return (int)math.floor(value / cellSize);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2908180e0131e63428d1bfdaed65e1ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
namespace SepCore.Simulation
{
internal static class SimulationCollision
{
public const int SourceTypeProjectile = 1;
public const int SourceTypeArea = 2;
public const int ShapeCircle = 0;
public const int ShapeSector = 1;
public const int ShapeRectangle = 2;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c39f2a2274ea06347970351f25e70c28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
namespace SepCore.Simulation
{
public readonly struct SimulationStats
{
public SimulationStats(int collisionCandidateCount, int pendingAreaCollisionRequestCount,
int lastCollisionQueryCount, int lastProjectileCollisionQueryCount, int lastAreaCollisionQueryCount,
int lastCollisionCandidateCount, int lastProjectileCollisionCandidateCount,
int lastAreaCollisionCandidateCount, int lastResolvedAreaHitCount, float lastCollisionCellSize,
bool lastCollisionHasEnemyTargets)
{
CollisionCandidateCount = collisionCandidateCount;
PendingAreaCollisionRequestCount = pendingAreaCollisionRequestCount;
LastCollisionQueryCount = lastCollisionQueryCount;
LastProjectileCollisionQueryCount = lastProjectileCollisionQueryCount;
LastAreaCollisionQueryCount = lastAreaCollisionQueryCount;
LastCollisionCandidateCount = lastCollisionCandidateCount;
LastProjectileCollisionCandidateCount = lastProjectileCollisionCandidateCount;
LastAreaCollisionCandidateCount = lastAreaCollisionCandidateCount;
LastResolvedAreaHitCount = lastResolvedAreaHitCount;
LastCollisionCellSize = lastCollisionCellSize;
LastCollisionHasEnemyTargets = lastCollisionHasEnemyTargets;
}
public int CollisionCandidateCount { get; }
public int PendingAreaCollisionRequestCount { get; }
public int LastCollisionQueryCount { get; }
public int LastProjectileCollisionQueryCount { get; }
public int LastAreaCollisionQueryCount { get; }
public int LastCollisionCandidateCount { get; }
public int LastProjectileCollisionCandidateCount { get; }
public int LastAreaCollisionCandidateCount { get; }
public int LastResolvedAreaHitCount { get; }
public float LastCollisionCellSize { get; }
public bool LastCollisionHasEnemyTargets { get; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 69cb78d487cd9f54d979790a9b24ee39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -4,15 +4,13 @@ namespace SepCore.Simulation
{
public readonly struct SimulationTickContext
{
public SimulationTickContext(float deltaTime, float realDeltaTime, Vector3 playerPosition)
public SimulationTickContext(float deltaTime, Vector3 playerPosition)
{
DeltaTime = deltaTime;
RealDeltaTime = realDeltaTime;
PlayerPosition = playerPosition;
}
public float DeltaTime { get; }
public float RealDeltaTime { get; }
public Vector3 PlayerPosition { get; }
}
}
@@ -1,5 +1,6 @@
using SepCore.Components;
using SepCore.Entity;
using Unity.Mathematics;
using UnityEngine;
namespace SepCore.Simulation
@@ -25,7 +26,7 @@ namespace SepCore.Simulation
float attackRange = enemy.AttackRange > 0f
? enemy.AttackRange
: DefaultAttackRange;
: EnemyAttackSim.DefaultAttackRange;
return new EnemySimData
{
@@ -38,8 +39,7 @@ namespace SepCore.Simulation
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
TargetType = 0,
State = EnemyStateIdle
State = EnemyBehaviorSim.StateIdle
};
}
@@ -83,12 +83,9 @@ namespace SepCore.Simulation
Position = projectileEntity.CachedTransform.position,
Forward = forward,
Velocity = velocity,
Speed = speed,
LifeTime = lifeTime,
Age = 0f,
Active = true,
RemainingLifetime = lifeTime,
State = ProjectileStateActive
Active = true
};
}
@@ -98,11 +95,34 @@ namespace SepCore.Simulation
{
EntityId = pickupEntity.Id,
Position = pickupEntity.CachedTransform.position,
PickupRadius = 0.35f,
State = 0
PickupRadius = 0.35f
};
}
#endregion
#region Component Conversion
private static float3 ToFloat3(Vector3 value)
{
return new float3(value.x, value.y, value.z);
}
private static Vector3 ToVector3(float3 value)
{
return new Vector3(value.x, value.y, value.z);
}
private static quaternion ToQuaternion(Quaternion value)
{
return new quaternion(value.x, value.y, value.z, value.w);
}
private static Quaternion ToQuaternion(quaternion value)
{
return new Quaternion(value.value.x, value.value.y, value.value.z, value.value.w);
}
#endregion
}
}
@@ -50,21 +50,21 @@ namespace SepCore.Simulation
ClearPlayerMovementState();
}
private Vector3 ResolvePlayerPositionForTick(in SimulationTickContext context)
private Vector3 ResolvePlayerPositionForTick(Vector3 fallbackPlayerPosition, float deltaTime)
{
if (_playerMovementTransform == null)
{
return context.PlayerPosition;
return fallbackPlayerPosition;
}
if (!_playerMovementActive || _playerMovementSpeed <= 0f ||
_playerMovementDirection.sqrMagnitude <= Mathf.Epsilon || context.DeltaTime <= 0f)
_playerMovementDirection.sqrMagnitude <= Mathf.Epsilon || deltaTime <= 0f)
{
_playerMovementPosition = _playerMovementTransform.position;
return _playerMovementPosition;
}
_playerMovementPosition += _playerMovementDirection * (_playerMovementSpeed * context.DeltaTime);
_playerMovementPosition += _playerMovementDirection * (_playerMovementSpeed * deltaTime);
_playerMovementPosition.y = _playerMovementTransform.position.y;
_playerMovementTransform.position = _playerMovementPosition;
return _playerMovementPosition;
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace SepCore.Simulation
@@ -17,23 +16,14 @@ namespace SepCore.Simulation
private readonly CollisionPipelineRuntimeState _collisionPipelineRuntime = new();
private readonly TargetSelectionRuntimeState _targetSelectionRuntime = new();
private List<EnemySimData> _enemies => _simulationState.Enemies;
private List<ProjectileSimData> _projectiles => _simulationState.Projectiles;
private List<PickupSimData> _pickups => _simulationState.Pickups;
private EnemyStore _enemies => _simulationState.Enemies;
private ProjectileStore _projectiles => _simulationState.Projectiles;
private PickupStore _pickups => _simulationState.Pickups;
private List<int> _projectileRecycleEntityIds => _simulationState.ProjectileRecycleEntityIds;
private HashSet<int> _projectileResolvedEntityIds => _collisionPipelineRuntime.ProjectileResolvedEntityIds;
private EntityBinding EnemyBinding => _simulationState.EnemyBinding;
private EntityBinding ProjectileBinding => _simulationState.ProjectileBinding;
private EntityBinding PickupBinding => _simulationState.PickupBinding;
private ref NativeList<EnemyJobInputData> _enemyJobInputs => ref _jobDataRuntime.EnemyJobInputs;
private ref NativeList<EnemyJobOutputData> _enemyJobOutputs => ref _jobDataRuntime.EnemyJobOutputs;
private ref NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs => ref _jobDataRuntime.EnemyJobSeparationOutputs;
private ref NativeList<float2> _enemySeparationPreviousPushes => ref _jobDataRuntime.EnemySeparationPreviousPushes;
private ref NativeList<float2> _enemySeparationCurrentPushes => ref _jobDataRuntime.EnemySeparationCurrentPushes;
private ref NativeList<ProjectileJobInputData> _projectileJobInputs => ref _jobDataRuntime.ProjectileJobInputs;
private ref NativeList<ProjectileJobOutputData> _projectileJobOutputs => ref _jobDataRuntime.ProjectileJobOutputs;
private ref NativeList<EnemyTransformSim> _enemySeparationTransforms => ref _jobDataRuntime.EnemySeparationTransforms;
private ref NativeList<int> _collectedPickups => ref _jobDataRuntime.CollectedPickups;
private ref NativeList<CollisionCandidateData> _collisionCandidates => ref _jobDataRuntime.CollisionCandidates;
private ref NativeParallelMultiHashMap<long, int> _enemySeparationBuckets => ref _jobDataRuntime.EnemySeparationBuckets;
private ref NativeParallelMultiHashMap<long, int> _enemyCollisionBuckets => ref _jobDataRuntime.EnemyCollisionBuckets;
@@ -101,24 +91,16 @@ namespace SepCore.Simulation
private sealed class SimulationStateStore
{
public readonly List<EnemySimData> Enemies = new();
public readonly List<ProjectileSimData> Projectiles = new();
public readonly List<PickupSimData> Pickups = new();
public readonly EnemyStore Enemies = new();
public readonly ProjectileStore Projectiles = new();
public readonly PickupStore Pickups = new();
public readonly List<int> ProjectileRecycleEntityIds = new();
public readonly EntityBinding EnemyBinding = new();
public readonly EntityBinding ProjectileBinding = new();
public readonly EntityBinding PickupBinding = new();
}
private sealed class JobDataRuntimeState
{
public NativeList<EnemyJobInputData> EnemyJobInputs;
public NativeList<EnemyJobOutputData> EnemyJobOutputs;
public NativeList<EnemyJobOutputData> EnemyJobSeparationOutputs;
public NativeList<float2> EnemySeparationPreviousPushes;
public NativeList<float2> EnemySeparationCurrentPushes;
public NativeList<ProjectileJobInputData> ProjectileJobInputs;
public NativeList<ProjectileJobOutputData> ProjectileJobOutputs;
public NativeList<EnemyTransformSim> EnemySeparationTransforms;
public NativeList<int> CollectedPickups;
public NativeList<CollisionCandidateData> CollisionCandidates;
public NativeParallelMultiHashMap<long, int> EnemySeparationBuckets;
public NativeParallelMultiHashMap<long, int> EnemyCollisionBuckets;
@@ -1,4 +1,5 @@
using SepCore.Entity;
using UnityEngine;
namespace SepCore.Simulation
{
@@ -16,11 +17,8 @@ namespace SepCore.Simulation
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
_fixedStepAccumulator = 0d;
ClearJobDataChannels();
EnemyBinding.Clear();
ProjectileBinding.Clear();
PickupBinding.Clear();
ClearPlayerMovementState();
}
@@ -28,46 +26,96 @@ namespace SepCore.Simulation
#region Enemy Simulation State
private int AddEnemy(in EnemySimData simData)
{
int simulationIndex = _enemies.Count;
_enemies.Add(simData);
EnemyBinding.Bind(simData.EntityId, simulationIndex);
OnEnemyAddedToSeparationTemporalBuffers();
MarkEnemyTargetSpatialIndexDirty();
return simulationIndex;
}
public int EnemyCount => _enemies.Count;
private int UpsertEnemy(in EnemySimData simData)
public bool TryGetEnemySnapshot(int entityId, out EnemySimData snapshot)
{
if (!EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
if (!_enemies.TryGetSimulationIndex(entityId, out int index))
{
return AddEnemy(simData);
snapshot = default;
return false;
}
_enemies[simulationIndex] = simData;
EnemyTransformSim transform = _enemies.Transform[index];
EnemyMoveInputSim moveInput = _enemies.MoveInput[index];
EnemyMoveConfigSim moveConfig = _enemies.MoveConfig[index];
EnemyAttackSim attack = _enemies.Attack[index];
EnemyBehaviorSim behavior = _enemies.Behavior[index];
snapshot = new EnemySimData
{
EntityId = entityId,
Position = ToVector3(transform.Position),
Forward = ToVector3(transform.Forward),
Rotation = ToQuaternion(transform.Rotation),
Speed = moveInput.Speed,
AttackRange = attack.AttackRange,
AvoidEnemyOverlap = moveConfig.AvoidOverlap,
EnemyBodyRadius = moveConfig.BodyRadius,
SeparationIterations = moveConfig.SeparationIterations,
State = behavior.State
};
return true;
}
private int AddEnemy(in EnemySimData simData)
{
InitializeJobDataChannels();
int index = _enemies.Add(
new EnemyIdentitySim { EntityId = simData.EntityId },
new EnemyTransformSim
{
Position = ToFloat3(simData.Position),
Forward = ToFloat3(simData.Forward),
Rotation = ToQuaternion(simData.Rotation)
},
new EnemyMoveInputSim { Speed = simData.Speed },
new EnemyMoveConfigSim
{
AvoidOverlap = simData.AvoidEnemyOverlap,
BodyRadius = simData.EnemyBodyRadius,
SeparationIterations = simData.SeparationIterations
},
new EnemyAttackSim { AttackRange = simData.AttackRange },
new EnemyBehaviorSim { State = simData.State });
MarkEnemyTargetSpatialIndexDirty();
return simulationIndex;
return index;
}
internal int UpsertEnemy(in EnemySimData simData)
{
if (!_enemies.TryGetSimulationIndex(simData.EntityId, out int index))
{
return AddEnemy(in simData);
}
_enemies.Identity[index] = new EnemyIdentitySim { EntityId = simData.EntityId };
_enemies.Transform[index] = new EnemyTransformSim
{
Position = ToFloat3(simData.Position),
Forward = ToFloat3(simData.Forward),
Rotation = ToQuaternion(simData.Rotation)
};
_enemies.MoveInput[index] = new EnemyMoveInputSim { Speed = simData.Speed };
_enemies.MoveConfig[index] = new EnemyMoveConfigSim
{
AvoidOverlap = simData.AvoidEnemyOverlap,
BodyRadius = simData.EnemyBodyRadius,
SeparationIterations = simData.SeparationIterations
};
_enemies.Attack[index] = new EnemyAttackSim { AttackRange = simData.AttackRange };
_enemies.Behavior[index] = new EnemyBehaviorSim { State = simData.State };
MarkEnemyTargetSpatialIndexDirty();
return index;
}
private bool RemoveEnemyByEntityId(int entityId)
{
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
if (!_enemies.RemoveByEntityId(entityId))
{
return false;
}
int lastIndex = _enemies.Count - 1;
if (simulationIndex != lastIndex)
{
EnemySimData movedData = _enemies[lastIndex];
_enemies[simulationIndex] = movedData;
EnemyBinding.RemapIndex(movedData.EntityId, simulationIndex);
}
_enemies.RemoveAt(lastIndex);
OnEnemyRemovedFromSeparationTemporalBuffers(simulationIndex);
EnemyBinding.UnbindByEntityId(entityId);
MarkEnemyTargetSpatialIndexDirty();
return true;
}
@@ -88,85 +136,122 @@ namespace SepCore.Simulation
RemoveEnemyByEntityId(entityId);
}
private bool TryGetEnemyData(int entityId, out EnemySimData enemyData)
{
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex) || simulationIndex < 0 ||
simulationIndex >= _enemies.Count)
{
enemyData = default;
return false;
}
enemyData = _enemies[simulationIndex];
return true;
}
public void SyncEnemyMovementInput(int entityId, bool isMoving, in UnityEngine.Vector3 direction, float speed,
public void SyncEnemyMovementInput(int entityId, bool isMoving, in Vector3 direction, float speed,
bool avoidEnemyOverlap, float enemyBodyRadius, int separationIterations)
{
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex) || simulationIndex < 0 ||
simulationIndex >= _enemies.Count)
if (!_enemies.TryGetSimulationIndex(entityId, out int index))
{
return;
}
EnemySimData enemyData = _enemies[simulationIndex];
enemyData.Speed = isMoving ? UnityEngine.Mathf.Max(0f, speed) : 0f;
enemyData.AvoidEnemyOverlap = avoidEnemyOverlap;
enemyData.EnemyBodyRadius = UnityEngine.Mathf.Max(0.01f, enemyBodyRadius);
enemyData.SeparationIterations = UnityEngine.Mathf.Max(1, separationIterations);
UnityEngine.Vector3 planarDirection = direction;
planarDirection.y = 0f;
if (planarDirection.sqrMagnitude > UnityEngine.Mathf.Epsilon)
_enemies.MoveInput[index] = new EnemyMoveInputSim
{
enemyData.Forward = planarDirection.normalized;
}
Speed = isMoving ? Mathf.Max(0f, speed) : 0f
};
_enemies.MoveConfig[index] = new EnemyMoveConfigSim
{
AvoidOverlap = avoidEnemyOverlap,
BodyRadius = Mathf.Max(0.01f, enemyBodyRadius),
SeparationIterations = Mathf.Max(1, separationIterations)
};
_enemies[simulationIndex] = enemyData;
Vector3 planarDirection = direction;
planarDirection.y = 0f;
if (planarDirection.sqrMagnitude > Mathf.Epsilon)
{
planarDirection.Normalize();
EnemyTransformSim transform = _enemies.Transform[index];
transform.Forward = ToFloat3(planarDirection);
_enemies.Transform[index] = transform;
}
}
#endregion
#region Projectile Simulation State
private int AddProjectile(in ProjectileSimData simData)
{
int simulationIndex = _projectiles.Count;
_projectiles.Add(simData);
ProjectileBinding.Bind(simData.EntityId, simulationIndex);
return simulationIndex;
}
public int ProjectileCount => _projectiles.Count;
private int UpsertProjectile(in ProjectileSimData simData)
public bool TryGetProjectileSnapshot(int entityId, out ProjectileSimData snapshot)
{
if (!ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
if (!_projectiles.TryGetSimulationIndex(entityId, out int index))
{
return AddProjectile(simData);
snapshot = default;
return false;
}
_projectiles[simulationIndex] = simData;
return simulationIndex;
ProjectileIdentitySim identity = _projectiles.Identity[index];
ProjectileTransformSim transform = _projectiles.Transform[index];
ProjectileVelocitySim velocity = _projectiles.Velocity[index];
ProjectileLifeSim life = _projectiles.Life[index];
snapshot = new ProjectileSimData
{
EntityId = identity.EntityId,
OwnerEntityId = identity.OwnerEntityId,
Position = ToVector3(transform.Position),
Forward = ToVector3(transform.Forward),
Velocity = ToVector3(velocity.Velocity),
LifeTime = life.LifeTime,
Age = life.Age,
Active = life.Active
};
return true;
}
private int AddProjectile(in ProjectileSimData simData)
{
InitializeJobDataChannels();
return _projectiles.Add(
new ProjectileIdentitySim
{
EntityId = simData.EntityId,
OwnerEntityId = simData.OwnerEntityId
},
new ProjectileTransformSim
{
Position = ToFloat3(simData.Position),
Forward = ToFloat3(simData.Forward)
},
new ProjectileVelocitySim { Velocity = ToFloat3(simData.Velocity) },
new ProjectileLifeSim
{
Age = simData.Age,
LifeTime = simData.LifeTime,
Active = simData.Active
});
}
internal int UpsertProjectile(in ProjectileSimData simData)
{
if (!_projectiles.TryGetSimulationIndex(simData.EntityId, out int index))
{
return AddProjectile(in simData);
}
_projectiles.Identity[index] = new ProjectileIdentitySim
{
EntityId = simData.EntityId,
OwnerEntityId = simData.OwnerEntityId
};
_projectiles.Transform[index] = new ProjectileTransformSim
{
Position = ToFloat3(simData.Position),
Forward = ToFloat3(simData.Forward)
};
_projectiles.Velocity[index] = new ProjectileVelocitySim { Velocity = ToFloat3(simData.Velocity) };
_projectiles.Life[index] = new ProjectileLifeSim
{
Age = simData.Age,
LifeTime = simData.LifeTime,
Active = simData.Active
};
return index;
}
private bool RemoveProjectileByEntityId(int entityId)
{
if (!ProjectileBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
{
return false;
}
int lastIndex = _projectiles.Count - 1;
if (simulationIndex != lastIndex)
{
ProjectileSimData movedData = _projectiles[lastIndex];
_projectiles[simulationIndex] = movedData;
ProjectileBinding.RemapIndex(movedData.EntityId, simulationIndex);
}
_projectiles.RemoveAt(lastIndex);
ProjectileBinding.UnbindByEntityId(entityId);
return true;
return _projectiles.RemoveByEntityId(entityId);
}
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
@@ -188,43 +273,66 @@ namespace SepCore.Simulation
#region Pickup Simulation State
private int AddPickup(in PickupSimData simData)
{
int simulationIndex = _pickups.Count;
_pickups.Add(simData);
PickupBinding.Bind(simData.EntityId, simulationIndex);
return simulationIndex;
}
public int PickupCount => _pickups.Count;
private int UpsertPickup(in PickupSimData simData)
public bool TryGetPickupSnapshot(int entityId, out PickupSimData snapshot)
{
if (!PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
if (!_pickups.TryGetSimulationIndex(entityId, out int index))
{
return AddPickup(simData);
snapshot = default;
return false;
}
_pickups[simulationIndex] = simData;
return simulationIndex;
PickupTransformSim transform = _pickups.Transform[index];
PickupRadiusSim radius = _pickups.Radius[index];
PickupStateSim state = _pickups.State[index];
snapshot = new PickupSimData
{
EntityId = entityId,
Position = ToVector3(transform.Position),
PickupRadius = radius.Radius,
State = state.State
};
return true;
}
private int AddPickup(in PickupSimData simData)
{
InitializeJobDataChannels();
return _pickups.Add(
new PickupIdentitySim { EntityId = simData.EntityId },
new PickupTransformSim { Position = ToFloat3(simData.Position) },
new PickupRadiusSim { Radius = simData.PickupRadius },
new PickupStateSim { State = simData.State });
}
internal int UpsertPickup(in PickupSimData simData)
{
if (!_pickups.TryGetSimulationIndex(simData.EntityId, out int index))
{
return AddPickup(in simData);
}
_pickups.Identity[index] = new PickupIdentitySim { EntityId = simData.EntityId };
_pickups.Transform[index] = new PickupTransformSim { Position = ToFloat3(simData.Position) };
_pickups.Radius[index] = new PickupRadiusSim { Radius = simData.PickupRadius };
_pickups.State[index] = new PickupStateSim { State = simData.State };
return index;
}
public void TriggerFullMapMagnet()
{
int pickupCount = _pickups.Count;
for (int i = 0; i < pickupCount; i++)
{
_pickups.State[i] = new PickupStateSim { State = PickupStateSim.StateAbsorbing };
}
}
private bool RemovePickupByEntityId(int entityId)
{
if (!PickupBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
{
return false;
}
int lastIndex = _pickups.Count - 1;
if (simulationIndex != lastIndex)
{
PickupSimData movedData = _pickups[lastIndex];
_pickups[simulationIndex] = movedData;
PickupBinding.RemapIndex(movedData.EntityId, simulationIndex);
}
_pickups.RemoveAt(lastIndex);
PickupBinding.UnbindByEntityId(entityId);
return true;
return _pickups.RemoveByEntityId(entityId);
}
private void RegisterPickupLifecycle(EntityBase pickupEntity)
@@ -1,5 +1,6 @@
using SepCore.Debugger;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
namespace SepCore.Simulation
@@ -17,8 +18,8 @@ namespace SepCore.Simulation
BuildEnemyTargetSpatialIndexIfNeeded();
float cellSize = GetTargetSelectionCellSize();
int centerCellX = ToCell(origin.x, cellSize);
int centerCellZ = ToCell(origin.z, cellSize);
int centerCellX = SimulationCell.ToCell(origin.x, cellSize);
int centerCellZ = SimulationCell.ToCell(origin.z, cellSize);
float range = Mathf.Sqrt(maxSqrRange);
int queryRange = Mathf.Max(1, Mathf.CeilToInt(range / cellSize));
@@ -31,7 +32,7 @@ namespace SepCore.Simulation
{
for (int dz = -queryRange; dz <= queryRange; dz++)
{
long key = CellKey(centerCellX + dx, centerCellZ + dz);
long key = SimulationCell.Key(centerCellX + dx, centerCellZ + dz);
if (!_enemyTargetBuckets.TryGetFirstValue(key, out int enemyIndex,
out NativeParallelMultiHashMapIterator<long> iterator))
{
@@ -45,17 +46,17 @@ namespace SepCore.Simulation
continue;
}
EnemySimData enemy = _enemies[enemyIndex];
Vector3 delta = enemy.Position - origin;
delta.y = 0f;
float sqrDistance = delta.sqrMagnitude;
float3 enemyPosition = _enemies.Transform[enemyIndex].Position;
float deltaX = enemyPosition.x - origin.x;
float deltaZ = enemyPosition.z - origin.z;
float sqrDistance = deltaX * deltaX + deltaZ * deltaZ;
if (sqrDistance >= minSqrDistance)
{
continue;
}
minSqrDistance = sqrDistance;
enemyEntityId = enemy.EntityId;
enemyEntityId = _enemies.Identity[enemyIndex].EntityId;
found = true;
} while (_enemyTargetBuckets.TryGetNextValue(out enemyIndex, ref iterator));
}
@@ -125,10 +126,10 @@ namespace SepCore.Simulation
for (int i = 0; i < enemyCount; i++)
{
EnemySimData enemy = _enemies[i];
int cellX = ToCell(enemy.Position.x, cellSize);
int cellZ = ToCell(enemy.Position.z, cellSize);
_enemyTargetBuckets.Add(CellKey(cellX, cellZ), i);
float3 enemyPosition = _enemies.Transform[i].Position;
int cellX = SimulationCell.ToCell(enemyPosition.x, cellSize);
int cellZ = SimulationCell.ToCell(enemyPosition.z, cellSize);
_enemyTargetBuckets.Add(SimulationCell.Key(cellX, cellZ), i);
}
}
@@ -139,15 +140,5 @@ namespace SepCore.Simulation
{
return Mathf.Max(0.1f, _targetSelectionCellSize);
}
private static int ToCell(float value, float cellSize)
{
return Mathf.FloorToInt(value / cellSize);
}
private static long CellKey(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
}
}
@@ -1,50 +1,45 @@
using System.Collections.Generic;
using SepCore.Debugger;
using SepCore.Debugger;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace SepCore.Simulation
{
public sealed partial class SimulationWorld : GameFrameworkComponent
public sealed partial class SimulationWorld : GameFrameworkComponent, ISimulationWorld
{
// Partial layout:
// - SimulationWorld.cs: 鏍稿績鐘舵€併€佸父閲忓拰 Unity 鐢熷懡鍛ㄦ湡鍏ュ彛鐐广€?
// - SimulationWorld.RuntimeModules.cs: 杩愯鏃跺煙瀵硅薄銆侀厤缃拰鐘舵€佷唬鐞嗐€?
// - SimulationWorld.SimEntityState.cs: 妯℃嫙鐘舵€佺殑澧炲垹鏀规煡鍜岀敓鍛藉懆鏈熸敞鍐屻€?
// - SimulationWorld.EntityToSimData.cs: Unity 瀹炰綋鍒?sim data 鐨勫垵濮嬪寲閫傞厤銆?
// - SimulationWorld.EntitySync.cs: GameFramework 瀹炰綋 show/hide 浜嬩欢妗ャ€?
// - SimulationWorld.TargetSelectionSpatialIndex.cs: 鏈€杩戞晫绌洪棿绱㈠紩鏌ヨ銆?
// - Presentation/SimulationWorld.TransformSync.cs: late-update transform 鍚屾妗ャ€?
// - Presentation/SimulationWorld.HitPresentation.cs: 鎶曞皠鐗╁懡涓簨浠惰〃鐜版ˉ銆?
// - DataChannel/SimulationWorld.JobDataChannel.cs: Job 閫氶亾鍏变韩瀛楁銆佸父閲忓拰杩愯鏃剁姸鎬併€?
// - DataChannel/SimulationWorld.JobDataLifecycle.cs: Native 閫氶亾鍒濆鍖栥€佹竻鐞嗗拰 clear銆?
// - DataChannel/SimulationWorld.JobDataConversion.cs: sim/job 鏁版嵁杞崲涓庤緭鍏ヨ緭鍑虹紦鍐插噯澶囥€?
// - DataChannel/SimulationWorld.CollisionTransient.cs: 纰版挒涓存椂閫氶亾鍜岃繍琛屾椂缁熻銆?
// - DataChannel/SimulationWorld.EnemySeparationTemporal.cs: 鏁屼汉鍒嗙鐨勫抚闂翠复鏃剁姸鎬併€?
// - DataChannel/SimulationWorld.JobOutputCommit.cs: Job 杈撳嚭鍥炲啓涓诲鍣ㄣ€?
// - Jobs/SimulationWorld.EnemyJobs.cs: 妯℃嫙閫氶亾 缂栨帓 + 鏁屼汉绉诲姩/鍒嗙 椤哄簭鎵ц
// - Jobs/SimulationWorld.ProjectileJobs.cs: 鎶曞皠鐗╃Щ鍔ㄤ笌鍥炴敹
// - Jobs/SimulationWorld.CollisionPipeline.cs: 纰版挒绠$嚎鍏变韩閰嶇疆鍜岀姸鎬?
// - Jobs/SimulationWorld.CollisionRequests.cs: area/sector 璇锋眰缂撳啿
// - Jobs/SimulationWorld.CollisionBroadPhase.cs: broad-phase 鍊欓€夋瀯寤哄拰 Job 璋冨害
// - Jobs/SimulationWorld.CollisionResolve.cs: 涓荤嚎绋嬪懡涓粨绠椾笌 area settle
// - Jobs/SimulationWorld.CollisionPresentation.cs: 鍛戒腑琛ㄧ幇浜嬩欢鍜屽疄浣?impact 瑙f瀽
// - JobStruct/*.cs: burst job 鍐呮牳鍜岄潰鍚?job 鐨勬暟鎹粨鏋?
private const float DefaultAttackRange = 1f;
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;
// - SimulationWorld.cs: 核心状态、常量和 Unity 生命周期入口点。
// - SimulationWorld.RuntimeModules.cs: 运行时域对象、配置和状态代理。
// - SimulationWorld.SimEntityState.cs: 模拟状态的增删改查和生命周期注册。
// - SimulationWorld.EntityToSimData.cs: Unity 实体与组件列/快照之间的边界转换。
// - SimulationWorld.EntitySync.cs: GameFramework 实体 show/hide 事件桥。
// - SimulationWorld.TargetSelectionSpatialIndex.cs: 最近敌空间索引查询。
// - SimulationWorld.PlayerMovement.cs: 玩家模拟移动输入与步进。
// - Components/*: 敌人/投射物/掉落物 SoA 组件列与存储。
// - Presentation/SimulationWorld.TransformSync.cs: late-update transform 同步桥。
// - Presentation/SimulationWorld.HitPresentation.cs: 投射物命中事件表现桥。
// - DataChannel/*: Job 通道与帧间临时状态。
// - Systems/*: 独立数据并行系统(移动/分离/碰撞查询)。
// - Jobs/*: 模拟管线编排、碰撞调度与结算。
// - JobStruct/*: 面向 job 的瞬时数据结构。
// 模拟数据以组件列(SoA)为唯一存储,XxxSimData 只作为生成/查询边界的快照结构。
private const float MinFixedStepSeconds = 0.0001f;
private const double FixedStepEpsilonScale = 1e-4;
[Header("固定步长模拟")]
[Tooltip("开启后按固定步长累积时间,一帧内可能执行多次模拟步进。")]
[SerializeField]
private bool _fixedStepEnabled = true;
[Tooltip("固定步长(秒)。")]
[SerializeField]
private float _fixedStepSeconds = 1f / 60f;
private double _fixedStepAccumulator;
private EntitySync _entitySync;
private TransformSync _transformSync;
private HitPresentation _hitPresentation;
public IReadOnlyList<EnemySimData> Enemies => _enemies;
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
public IReadOnlyList<PickupSimData> Pickups => _pickups;
#region Lifecycle
protected override void Awake()
@@ -64,9 +59,30 @@ namespace SepCore.Simulation
public void Tick(in SimulationTickContext context)
{
Vector3 playerPosition = ResolvePlayerPositionForTick(in context);
SimulationTickContext resolvedContext =
new SimulationTickContext(context.DeltaTime, context.RealDeltaTime, playerPosition);
if (!_fixedStepEnabled)
{
TickSimulationStep(context.PlayerPosition, context.DeltaTime);
return;
}
double fixedStep = Mathf.Max(MinFixedStepSeconds, _fixedStepSeconds);
double stepEpsilon = fixedStep * FixedStepEpsilonScale;
_fixedStepAccumulator += context.DeltaTime;
while (_fixedStepAccumulator + stepEpsilon >= fixedStep)
{
TickSimulationStep(context.PlayerPosition, (float)fixedStep);
_fixedStepAccumulator -= fixedStep;
if (_fixedStepAccumulator < 0d)
{
_fixedStepAccumulator = 0d;
}
}
}
private void TickSimulationStep(Vector3 fallbackPlayerPosition, float deltaTime)
{
Vector3 playerPosition = ResolvePlayerPositionForTick(fallbackPlayerPosition, deltaTime);
SimulationTickContext resolvedContext = new SimulationTickContext(deltaTime, playerPosition);
using (CustomProfilerMarker.TickEnemies.Auto())
{
TickSimulationPipeline(in resolvedContext);
@@ -91,4 +107,3 @@ namespace SepCore.Simulation
#endregion
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 57a5e62d1ac18754e98d0ebf2f2c12c8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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));
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d4cc79b0ecd0c248ac2028cd928dd7c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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 };
}
}
}

Some files were not shown because too many files have changed in this diff Show More