Checkpoint 9
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<UnityGameFramework>
|
||||
<ResourceBuilder>
|
||||
<Settings>
|
||||
<InternalResourceVersion>3</InternalResourceVersion>
|
||||
<InternalResourceVersion>5</InternalResourceVersion>
|
||||
<Platforms>33</Platforms>
|
||||
<AssetBundleCompression>1</AssetBundleCompression>
|
||||
<CompressionHelperTypeName>UnityGameFramework.Runtime.DefaultCompressionHelper</CompressionHelperTypeName>
|
||||
|
||||
@@ -2,19 +2,28 @@
|
||||
<UnityGameFramework>
|
||||
<ResourceCollection>
|
||||
<Resources>
|
||||
<Resource Name="Configs" FileSystem="GameData" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="DataTables" FileSystem="GameData" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Configs" FileSystem="GameData" LoadType="0" Packed="True"
|
||||
ResourceGroups="Base" />
|
||||
<Resource Name="DataTables" FileSystem="GameData" LoadType="0" Packed="True"
|
||||
ResourceGroups="Base" />
|
||||
<Resource Name="Entities" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||
<Resource Name="Fonts" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="en-us" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="ko-kr" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="zh-cn" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="zh-tw" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="en-us" FileSystem="UI" LoadType="0"
|
||||
Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="ko-kr" FileSystem="UI" LoadType="0"
|
||||
Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="zh-cn" FileSystem="UI" LoadType="0"
|
||||
Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Localization/Dictionaries" Variant="zh-tw" FileSystem="UI" LoadType="0"
|
||||
Packed="True" ResourceGroups="Base" />
|
||||
<Resource Name="Materials" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||
<Resource Name="Meshes" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||
<Resource Name="Music/About" FileSystem="Resources" LoadType="0" Packed="True" ResourceGroups="Music" />
|
||||
<Resource Name="Music/Background" FileSystem="Resources" LoadType="0" Packed="True" ResourceGroups="Music" />
|
||||
<Resource Name="Music/Menu" FileSystem="Resources" LoadType="0" Packed="True" ResourceGroups="Music" />
|
||||
<Resource Name="Music/About" FileSystem="Resources" LoadType="0" Packed="True"
|
||||
ResourceGroups="Music" />
|
||||
<Resource Name="Music/Background" FileSystem="Resources" LoadType="0" Packed="True"
|
||||
ResourceGroups="Music" />
|
||||
<Resource Name="Music/Menu" FileSystem="Resources" LoadType="0" Packed="True"
|
||||
ResourceGroups="Music" />
|
||||
<Resource Name="Scenes" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||
<Resource Name="SceneSettings" LoadType="0" Packed="False" />
|
||||
<Resource Name="Sounds" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||
@@ -41,9 +50,11 @@
|
||||
<Asset Guid="0ed73dc47f4cb38489020d05e9f02c99" ResourceName="Materials" />
|
||||
<Asset Guid="0f995b3145e0e7247a42da6cef1dbf23" ResourceName="Materials" />
|
||||
<Asset Guid="1046dcb12e547564d8b54bd15419a787" ResourceName="Entities" />
|
||||
<Asset Guid="1053b0070685be347ab58587156842dc" ResourceName="Localization/Dictionaries" ResourceVariant="zh-tw" />
|
||||
<Asset Guid="1053b0070685be347ab58587156842dc" ResourceName="Localization/Dictionaries"
|
||||
ResourceVariant="zh-tw" />
|
||||
<Asset Guid="1478894bc9a1ed241b05b0862a7b8bce" ResourceName="Textures" />
|
||||
<Asset Guid="14869ac0d4433f04db1704e39d03412e" ResourceName="Localization/Dictionaries" ResourceVariant="en-us" />
|
||||
<Asset Guid="14869ac0d4433f04db1704e39d03412e" ResourceName="Localization/Dictionaries"
|
||||
ResourceVariant="en-us" />
|
||||
<Asset Guid="156d241f796508c4da4fc354a7fbf5a8" ResourceName="UI/UISprites/Common" />
|
||||
<Asset Guid="185f97f18bd603a478461ce9c08bd039" ResourceName="Materials" />
|
||||
<Asset Guid="18dc0cd2c080841dea60987a38ce93fa" ResourceName="URPAssets" />
|
||||
@@ -92,7 +103,8 @@
|
||||
<Asset Guid="578af1667322bab45b652b79a40bb4fb" ResourceName="Materials" />
|
||||
<Asset Guid="583ff7026dac91849b7c7b2468ba456b" ResourceName="Materials" />
|
||||
<Asset Guid="5b5a6a737c460eb4abc105d6583d405e" ResourceName="Fonts" />
|
||||
<Asset Guid="5dcd89912e222bf4c87f76db4044bc5e" ResourceName="Localization/Dictionaries" ResourceVariant="ko-kr" />
|
||||
<Asset Guid="5dcd89912e222bf4c87f76db4044bc5e" ResourceName="Localization/Dictionaries"
|
||||
ResourceVariant="ko-kr" />
|
||||
<Asset Guid="5ebb46af6f16ae94e87f64a7dc0a49cb" ResourceName="Entities" />
|
||||
<Asset Guid="62af9e5c8f39cfa49af9e10ccf42f1da" ResourceName="UI/UISprites/Common" />
|
||||
<Asset Guid="638ff8ae4a0d15047839cd265d3bc296" ResourceName="Music/Background" />
|
||||
@@ -132,7 +144,9 @@
|
||||
<Asset Guid="97b1f8b25cca2bc458cb9d6127c8d186" ResourceName="Materials" />
|
||||
<Asset Guid="99d811b0183246646a2ce8df996f4bca" ResourceName="Fonts" />
|
||||
<Asset Guid="9afa958d6d8235941b9badb42aae4370" ResourceName="Meshes" />
|
||||
<Asset Guid="9be2e1e45f4edd74c8764538ad306b78" ResourceName="Localization/Dictionaries" ResourceVariant="zh-cn" />
|
||||
<Asset Guid="9be2e1e45f4edd74c8764538ad306b78" ResourceName="Localization/Dictionaries"
|
||||
ResourceVariant="zh-cn" />
|
||||
<Asset Guid="9d193ac5b4294e0e9ba6e867320944b7" ResourceName="Entities" />
|
||||
<Asset Guid="9ddab293e2a8af3499dac05f5fd6169c" ResourceName="Meshes" />
|
||||
<Asset Guid="9f5bba6d2f5c95049a59fcb56df2d38f" ResourceName="UI/UIItems" />
|
||||
<Asset Guid="9f847ec5e66e03e4ead1d3c5f7b510e8" ResourceName="UI/UISprites/Common" />
|
||||
@@ -157,6 +171,7 @@
|
||||
<Asset Guid="ba157ba55f72c424a9e88f3c029997c4" ResourceName="Textures" />
|
||||
<Asset Guid="baedbbad82997f445a8cb4da210404e0" ResourceName="Meshes" />
|
||||
<Asset Guid="bbfd75fe6fe00e1448fe988173ede7f9" ResourceName="UI/UIForms" />
|
||||
<Asset Guid="bc065bcf1474d7d4387fafd202678c37" ResourceName="Fonts" />
|
||||
<Asset Guid="bf75b984df8a84987bcf3a8bf6e2862d" ResourceName="Sounds" />
|
||||
<Asset Guid="c40be3174f62c4acf8c1216858c64956" ResourceName="URPAssets" />
|
||||
<Asset Guid="c49cffd4fc1dfb549b2b30448a0becda" ResourceName="UI/UISprites/Icons" />
|
||||
|
||||
@@ -11,7 +11,6 @@ GameObject:
|
||||
- component: {fileID: 7683855655592166216}
|
||||
- component: {fileID: 6418687210998749921}
|
||||
- component: {fileID: 4710806460657047075}
|
||||
- component: {fileID: 8116679074104541426}
|
||||
- component: {fileID: 1932268889601128120}
|
||||
- component: {fileID: 557030043145096197}
|
||||
- component: {fileID: 6353753365317756414}
|
||||
@@ -87,33 +86,6 @@ MeshRenderer:
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!54 &8116679074104541426
|
||||
Rigidbody:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9166462022471897675}
|
||||
serializedVersion: 4
|
||||
m_Mass: 1
|
||||
m_Drag: 0
|
||||
m_AngularDrag: 0.05
|
||||
m_CenterOfMass: {x: 0, y: 0, z: 0}
|
||||
m_InertiaTensor: {x: 1, y: 1, z: 1}
|
||||
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 5376
|
||||
m_ImplicitCom: 1
|
||||
m_ImplicitTensor: 1
|
||||
m_UseGravity: 0
|
||||
m_IsKinematic: 1
|
||||
m_Interpolate: 0
|
||||
m_Constraints: 0
|
||||
m_CollisionDetection: 0
|
||||
--- !u!136 &1932268889601128120
|
||||
CapsuleCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -131,7 +103,7 @@ CapsuleCollider:
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 1
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
serializedVersion: 2
|
||||
m_Radius: 0.5
|
||||
m_Height: 2
|
||||
|
||||
@@ -103,7 +103,7 @@ CapsuleCollider:
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 0
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
serializedVersion: 2
|
||||
m_Radius: 0.5
|
||||
m_Height: 2
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.ObjectPool;
|
||||
using TMPro;
|
||||
@@ -9,7 +10,7 @@ namespace CustomComponent
|
||||
{
|
||||
public class DamageTextComponent : GameFrameworkComponent
|
||||
{
|
||||
[SerializeField] private int _instancePoolCapacity = 32;
|
||||
[SerializeField] private int _instancePoolCapacity = 256;
|
||||
|
||||
[SerializeField] private string _poolName = "DamageTextItem";
|
||||
|
||||
@@ -43,14 +44,20 @@ namespace CustomComponent
|
||||
|
||||
private DamageTextItem CreateDamageTextItem()
|
||||
{
|
||||
if (_activeDamageTextItems.Count == _instancePoolCapacity)
|
||||
{
|
||||
_instancePoolCapacity = Mathf.Min(_instancePoolCapacity * 2, 1024);
|
||||
_damageTextItemPool.Capacity = _instancePoolCapacity;
|
||||
}
|
||||
|
||||
DamageTextItemObject itemObject = _damageTextItemPool.Spawn();
|
||||
if (itemObject != null)
|
||||
{
|
||||
return (DamageTextItem)itemObject.Target;
|
||||
}
|
||||
|
||||
|
||||
GameObject itemGo = Instantiate(_damageTextItemPrefab, _instanceRoot, false);
|
||||
|
||||
|
||||
DamageTextItem item = itemGo.GetComponent<DamageTextItem>();
|
||||
_damageTextItemPool.Register(DamageTextItemObject.Create(item), true);
|
||||
return item;
|
||||
@@ -63,5 +70,12 @@ namespace CustomComponent
|
||||
_activeDamageTextItems.Remove(item);
|
||||
_damageTextItemPool.Unspawn(item);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_activeDamageTextItems.Clear();
|
||||
_damageTextItemPool.Release();
|
||||
_damageTextItemPool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Components;
|
||||
using CustomEvent;
|
||||
using DataTable;
|
||||
using Definition.DataStruct;
|
||||
using Entity;
|
||||
@@ -19,6 +21,7 @@ namespace CustomComponent
|
||||
private const float MinSpawnRate = 0.1f;
|
||||
private const float CornerTapWindow = 0.6f;
|
||||
private const int RequiredCornerTapCount = 3;
|
||||
private const int DebugHealAmount = 200;
|
||||
|
||||
private Rect _windowRect = new Rect(20f, 60f, 460f, 800f);
|
||||
private bool _isPanelVisible;
|
||||
@@ -38,6 +41,7 @@ namespace CustomComponent
|
||||
|
||||
private int _cornerTapCount;
|
||||
private float _lastCornerTapTime = -10f;
|
||||
private bool _lockPlayerHealthToMax;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
@@ -54,6 +58,10 @@ namespace CustomComponent
|
||||
}
|
||||
|
||||
HandleCornerTapGesture();
|
||||
if (_lockPlayerHealthToMax)
|
||||
{
|
||||
KeepPlayerHealthAtMax();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
@@ -147,6 +155,7 @@ namespace CustomComponent
|
||||
ProcedureGame procedure = GameEntry.Procedure.CurrentProcedure as ProcedureGame;
|
||||
EnemyManagerComponent enemyManager = GameEntry.EnemyManager;
|
||||
Player player = FindPlayer();
|
||||
HealthComponent playerHealth = player != null ? player.GetComponent<HealthComponent>() : null;
|
||||
|
||||
if (enemyManager == null)
|
||||
{
|
||||
@@ -168,8 +177,6 @@ namespace CustomComponent
|
||||
if (simulationWorld != null)
|
||||
{
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label(
|
||||
$"Sim Switch: Move={(simulationWorld.UseSimulationMovement ? "On" : "Off")} Job={(simulationWorld.UseJobSimulation ? "On" : "Off")} Burst={(simulationWorld.UseBurstJobs ? "On" : "Off")}");
|
||||
GUILayout.Label(
|
||||
$"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})");
|
||||
GUILayout.Label(
|
||||
@@ -265,6 +272,28 @@ namespace CustomComponent
|
||||
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.Space(4f);
|
||||
GUILayout.Label(
|
||||
$"Player HP: {(playerHealth == null ? "Unavailable" : $"{playerHealth.CurrentHealth}/{playerHealth.MaxHealth}")}");
|
||||
GUILayout.BeginHorizontal();
|
||||
GUI.enabled = playerHealth != null;
|
||||
if (GUILayout.Button($"+{DebugHealAmount} HP", GUILayout.Height(24f)))
|
||||
{
|
||||
AddPlayerHealth(playerHealth, DebugHealAmount);
|
||||
}
|
||||
|
||||
if (GUILayout.Button(_lockPlayerHealthToMax ? "GodMode: ON" : "GodMode: OFF", GUILayout.Height(24f)))
|
||||
{
|
||||
_lockPlayerHealthToMax = !_lockPlayerHealthToMax;
|
||||
if (_lockPlayerHealthToMax)
|
||||
{
|
||||
RestorePlayerHealthToMax(playerHealth);
|
||||
}
|
||||
}
|
||||
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void EnsurePropList(bool force = false)
|
||||
@@ -324,6 +353,52 @@ namespace CustomComponent
|
||||
return UnityEngine.Object.FindObjectOfType<Player>();
|
||||
}
|
||||
|
||||
private void KeepPlayerHealthAtMax()
|
||||
{
|
||||
Player player = FindPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
HealthComponent playerHealth = player.GetComponent<HealthComponent>();
|
||||
if (playerHealth == null) return;
|
||||
|
||||
RestorePlayerHealthToMax(playerHealth);
|
||||
}
|
||||
|
||||
private static void AddPlayerHealth(HealthComponent playerHealth, int amount)
|
||||
{
|
||||
if (playerHealth == null || amount <= 0) return;
|
||||
if (playerHealth.CurrentHealth <= 0) return;
|
||||
|
||||
int maxHealth = playerHealth.MaxHealth;
|
||||
if (maxHealth <= 0) return;
|
||||
|
||||
int nextHealth = Mathf.Clamp(playerHealth.CurrentHealth + amount, 0, maxHealth);
|
||||
if (nextHealth == playerHealth.CurrentHealth) return;
|
||||
|
||||
playerHealth.CurrentHealth = nextHealth;
|
||||
PublishPlayerHealthChanged(playerHealth);
|
||||
}
|
||||
|
||||
private static void RestorePlayerHealthToMax(HealthComponent playerHealth)
|
||||
{
|
||||
if (playerHealth == null) return;
|
||||
if (playerHealth.CurrentHealth <= 0) return;
|
||||
|
||||
int maxHealth = playerHealth.MaxHealth;
|
||||
if (maxHealth <= 0 || playerHealth.CurrentHealth >= maxHealth) return;
|
||||
|
||||
playerHealth.CurrentHealth = maxHealth;
|
||||
PublishPlayerHealthChanged(playerHealth);
|
||||
}
|
||||
|
||||
private static void PublishPlayerHealthChanged(HealthComponent playerHealth)
|
||||
{
|
||||
if (playerHealth == null || GameEntry.Event == null) return;
|
||||
|
||||
GameEntry.Event.Fire(null,
|
||||
PlayerHealthChangeEventArgs.Create(0, playerHealth.CurrentHealth, playerHealth.MaxHealth));
|
||||
}
|
||||
|
||||
private static void AddSelectedBuffToPlayer(DRProp prop, int count)
|
||||
{
|
||||
Player player = FindPlayer();
|
||||
@@ -407,4 +482,4 @@ namespace CustomComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -4,23 +4,26 @@ namespace CustomDebugger
|
||||
{
|
||||
public static class CustomProfilerMarker
|
||||
{
|
||||
public static readonly ProfilerMarker TickEnemies = new ProfilerMarker("TickEnemies");
|
||||
public static readonly ProfilerMarker TickEnemies_BuildInput = new ProfilerMarker("TickEnemies.BuildInput");
|
||||
public static readonly ProfilerMarker TickEnemies_MoveSeparation = new ProfilerMarker("TickEnemies.MoveSeparation");
|
||||
public static readonly ProfilerMarker TickEnemies_StateUpdate = new ProfilerMarker("TickEnemies.StateUpdate");
|
||||
public static readonly ProfilerMarker TickEnemies_Schedule = new ProfilerMarker("TickEnemies.Schedule");
|
||||
public static readonly ProfilerMarker TickEnemies_Complete = new ProfilerMarker("TickEnemies.Complete");
|
||||
public static readonly ProfilerMarker TickEnemies_MainThreadCommit = new ProfilerMarker("TickEnemies.MainThreadCommit");
|
||||
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
||||
public static readonly ProfilerMarker TickEnemies = new("TickEnemies");
|
||||
public static readonly ProfilerMarker TickEnemies_BuildInput = new("TickEnemies.BuildInput");
|
||||
public static readonly ProfilerMarker TickEnemies_StateUpdate = new("TickEnemies.StateUpdate");
|
||||
public static readonly ProfilerMarker TickEnemies_Schedule = new("TickEnemies.Schedule");
|
||||
public static readonly ProfilerMarker TickEnemies_Complete = new("TickEnemies.Complete");
|
||||
public static readonly ProfilerMarker TickEnemies_MainThreadCommit = new("TickEnemies.MainThreadCommit");
|
||||
public static readonly ProfilerMarker TickEnemies_WriteBack = new("TickEnemies.WriteBack");
|
||||
|
||||
public static readonly ProfilerMarker Collision = new("Collision");
|
||||
public static readonly ProfilerMarker Collision_BuildQueries = new("Collision.BuildQueries");
|
||||
public static readonly ProfilerMarker Collision_BuildBuckets = new("Collision.BuildBuckets");
|
||||
public static readonly ProfilerMarker Collision_QueryCandidates = new("Collision.QueryCandidates");
|
||||
public static readonly ProfilerMarker Collision_ResolveProjectile = new("Collision.ResolveProjectile");
|
||||
public static readonly ProfilerMarker Collision_ResolveArea = new("Collision.ResolveArea");
|
||||
|
||||
public static readonly ProfilerMarker TargetSelection_BuildBuckets = new("TargetSelection.BuildBuckets");
|
||||
public static readonly ProfilerMarker TargetSelection_QueryNeighbors = new("TargetSelection.QueryNeighbors");
|
||||
public static readonly ProfilerMarker Movement_Update = new ProfilerMarker("Movement_Update");
|
||||
|
||||
public static readonly ProfilerMarker Movement_Update = new("Movement_Update");
|
||||
public static readonly ProfilerMarker ShopUI_Update = new("UGF.ShopUI.Update");
|
||||
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ namespace Entity
|
||||
private static bool IsDrivenBySimulationWorld()
|
||||
{
|
||||
var simulationWorld = GameEntry.SimulationWorld;
|
||||
return simulationWorld != null && simulationWorld.UseSimulationMovement && simulationWorld.UseJobSimulation;
|
||||
return simulationWorld != null && simulationWorld.UseSimulationMovement;
|
||||
}
|
||||
|
||||
private void SetColliderEnabled(bool enabled)
|
||||
@@ -137,4 +137,4 @@ namespace Entity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
-19
@@ -1,13 +1,23 @@
|
||||
using GameFramework.ObjectPool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Entity.Weapon
|
||||
{
|
||||
public sealed class HandgunHitMarkerAttackEffect : IWeaponAttackEffect
|
||||
{
|
||||
private const string PoolName = "Weapon.HandgunHitMarker";
|
||||
private const float PoolAutoReleaseInterval = 60f;
|
||||
private const int PoolCapacity = 128;
|
||||
private const float PoolExpireTime = 120f;
|
||||
private const int PoolPriority = 0;
|
||||
|
||||
private static IObjectPool<HandgunHitMarkerPoolObject> s_Pool;
|
||||
|
||||
private readonly float _size;
|
||||
private readonly float _yOffset;
|
||||
private readonly float _duration;
|
||||
private readonly Color _color;
|
||||
private Material _sharedMaterial;
|
||||
|
||||
public HandgunHitMarkerAttackEffect(float size, float yOffset, float duration, Color color)
|
||||
{
|
||||
@@ -20,35 +30,95 @@ namespace Entity.Weapon
|
||||
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
|
||||
{
|
||||
if (target == null) return;
|
||||
if (!TrySpawnMarker(out HandgunHitMarkerPooledInstance markerInstance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||||
marker.name = "HandgunHitMarker";
|
||||
Transform targetTransform = target.CachedTransform;
|
||||
Vector3 worldPosition = targetTransform != null ? targetTransform.position : position;
|
||||
markerInstance.transform.SetParent(null, false);
|
||||
markerInstance.transform.position = worldPosition + Vector3.up * _yOffset;
|
||||
markerInstance.transform.localScale = Vector3.one * Mathf.Max(0.01f, _size);
|
||||
markerInstance.ApplyMaterial(GetSharedMaterial());
|
||||
markerInstance.Activate(Mathf.Max(0.01f, _duration), s_Pool);
|
||||
}
|
||||
|
||||
Collider collider = marker.GetComponent<Collider>();
|
||||
private bool TrySpawnMarker(out HandgunHitMarkerPooledInstance markerInstance)
|
||||
{
|
||||
markerInstance = null;
|
||||
IObjectPool<HandgunHitMarkerPoolObject> pool = EnsurePool();
|
||||
if (pool == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HandgunHitMarkerPoolObject pooledObject = pool.Spawn();
|
||||
if (pooledObject != null)
|
||||
{
|
||||
markerInstance = pooledObject.Target as HandgunHitMarkerPooledInstance;
|
||||
if (markerInstance != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
GameObject markerGameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||||
markerGameObject.name = "HandgunHitMarker";
|
||||
Collider collider = markerGameObject.GetComponent<Collider>();
|
||||
if (collider != null)
|
||||
{
|
||||
Object.Destroy(collider);
|
||||
}
|
||||
|
||||
marker.transform.SetParent(target.CachedTransform, false);
|
||||
marker.transform.localPosition = new Vector3(0f, _yOffset, 0f);
|
||||
marker.transform.localScale = Vector3.one * Mathf.Max(0.01f, _size);
|
||||
markerInstance = markerGameObject.AddComponent<HandgunHitMarkerPooledInstance>();
|
||||
markerGameObject.SetActive(false);
|
||||
pool.Register(HandgunHitMarkerPoolObject.Create(markerInstance), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
Renderer renderer = marker.GetComponent<Renderer>();
|
||||
if (renderer != null)
|
||||
private static IObjectPool<HandgunHitMarkerPoolObject> EnsurePool()
|
||||
{
|
||||
var poolComponent = GameEntry.ObjectPool;
|
||||
if (poolComponent == null)
|
||||
{
|
||||
Shader shader = Shader.Find("Sprites/Default");
|
||||
if (shader == null)
|
||||
{
|
||||
shader = Shader.Find("Unlit/Color");
|
||||
}
|
||||
|
||||
Material material = new Material(shader);
|
||||
material.color = _color;
|
||||
renderer.material = material;
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.Destroy(marker, Mathf.Max(0.01f, _duration));
|
||||
if (s_Pool != null && poolComponent.HasObjectPool<HandgunHitMarkerPoolObject>(PoolName))
|
||||
{
|
||||
return s_Pool;
|
||||
}
|
||||
|
||||
s_Pool = poolComponent.HasObjectPool<HandgunHitMarkerPoolObject>(PoolName)
|
||||
? poolComponent.GetObjectPool<HandgunHitMarkerPoolObject>(PoolName)
|
||||
: poolComponent.CreateSingleSpawnObjectPool<HandgunHitMarkerPoolObject>(
|
||||
PoolName,
|
||||
PoolAutoReleaseInterval,
|
||||
PoolCapacity,
|
||||
PoolExpireTime,
|
||||
PoolPriority);
|
||||
return s_Pool;
|
||||
}
|
||||
|
||||
private Material GetSharedMaterial()
|
||||
{
|
||||
if (_sharedMaterial != null)
|
||||
{
|
||||
return _sharedMaterial;
|
||||
}
|
||||
|
||||
Shader shader = Shader.Find("Sprites/Default");
|
||||
if (shader == null)
|
||||
{
|
||||
shader = Shader.Find("Unlit/Color");
|
||||
}
|
||||
|
||||
_sharedMaterial = new Material(shader)
|
||||
{
|
||||
color = _color
|
||||
};
|
||||
return _sharedMaterial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using GameFramework;
|
||||
using GameFramework.ObjectPool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Entity.Weapon
|
||||
{
|
||||
public sealed class HandgunHitMarkerPoolObject : ObjectBase
|
||||
{
|
||||
public static HandgunHitMarkerPoolObject Create(object target)
|
||||
{
|
||||
HandgunHitMarkerPoolObject pooledObject = ReferencePool.Acquire<HandgunHitMarkerPoolObject>();
|
||||
pooledObject.Initialize(target);
|
||||
return pooledObject;
|
||||
}
|
||||
|
||||
protected override void Release(bool isShutdown)
|
||||
{
|
||||
HandgunHitMarkerPooledInstance markerInstance = Target as HandgunHitMarkerPooledInstance;
|
||||
if (markerInstance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Object.Destroy(markerInstance.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 966d34a18f5d00c49bddea3c7c5ec13d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
using GameFramework.ObjectPool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Entity.Weapon
|
||||
{
|
||||
public sealed class HandgunHitMarkerPooledInstance : MonoBehaviour
|
||||
{
|
||||
private Renderer _renderer;
|
||||
private IObjectPool<HandgunHitMarkerPoolObject> _ownerPool;
|
||||
private float _expireTime;
|
||||
private bool _isActive;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_renderer = GetComponent<Renderer>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_isActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Time.time < _expireTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnToPool();
|
||||
}
|
||||
|
||||
public void ApplyMaterial(Material material)
|
||||
{
|
||||
if (_renderer == null)
|
||||
{
|
||||
_renderer = GetComponent<Renderer>();
|
||||
}
|
||||
|
||||
if (_renderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_renderer.sharedMaterial = material;
|
||||
}
|
||||
|
||||
public void Activate(float duration, IObjectPool<HandgunHitMarkerPoolObject> pool)
|
||||
{
|
||||
_ownerPool = pool;
|
||||
_expireTime = Time.time + Mathf.Max(0.01f, duration);
|
||||
_isActive = true;
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_isActive = false;
|
||||
_ownerPool = null;
|
||||
}
|
||||
|
||||
private void ReturnToPool()
|
||||
{
|
||||
_isActive = false;
|
||||
IObjectPool<HandgunHitMarkerPoolObject> pool = _ownerPool;
|
||||
_ownerPool = null;
|
||||
transform.SetParent(null, false);
|
||||
|
||||
if (pool == null)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
pool.Unspawn(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ff8a67466d8f0643845c41fd5952f50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+2
-2
@@ -44,7 +44,7 @@ namespace Entity.Weapon
|
||||
}
|
||||
|
||||
var simulationWorld = GameEntry.SimulationWorld;
|
||||
if (simulationWorld == null || !simulationWorld.UseSimulationMovement || !simulationWorld.UseJobSimulation)
|
||||
if (simulationWorld == null || !simulationWorld.UseSimulationMovement)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -65,4 +65,4 @@ namespace Entity.Weapon
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ namespace Entity.Weapon
|
||||
}
|
||||
|
||||
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
|
||||
return simulationWorld.TryEnqueueAreaCollisionQuery(Id, ownerEntityId, in center, radius, maxTargets);
|
||||
return simulationWorld.TryRequestAreaCollision(Id, ownerEntityId, in center, radius, maxTargets);
|
||||
}
|
||||
|
||||
protected bool TryQueueSectorCollisionQuery(in Vector3 center, float radius, in Vector3 direction,
|
||||
@@ -242,7 +242,7 @@ namespace Entity.Weapon
|
||||
}
|
||||
|
||||
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
|
||||
return simulationWorld.TryEnqueueSectorCollisionQuery(Id, ownerEntityId, in center, radius, in direction,
|
||||
return simulationWorld.TryRequestSectorCollision(Id, ownerEntityId, in center, radius, in direction,
|
||||
halfAngleDeg, maxTargets);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Procedure
|
||||
base.OnEnter(procedureOwner);
|
||||
|
||||
_procedureOwner = procedureOwner;
|
||||
GameEntry.SimulationWorld?.Clear();
|
||||
GameEntry.SimulationWorld?.ClearSimulationState();
|
||||
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
||||
@@ -136,7 +136,7 @@ namespace Procedure
|
||||
Player = null;
|
||||
|
||||
_procedureOwner = null;
|
||||
GameEntry.SimulationWorld?.Clear();
|
||||
GameEntry.SimulationWorld?.ClearSimulationState();
|
||||
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f28ff313e954720a04246191b7f9ddd
|
||||
timeCreated: 1771901064
|
||||
+10
-185
@@ -8,115 +8,7 @@ namespace 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Shared broad-phase query payload for CP5 projectile collision and CP6 AOE collision candidates.
|
||||
private struct CollisionQueryData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public bool SourceWasActiveAtQueryTime;
|
||||
public float3 Position;
|
||||
public float Radius;
|
||||
public int MaxTargets;
|
||||
public int ShapeType;
|
||||
public float3 Direction;
|
||||
public float HalfAngleDeg;
|
||||
}
|
||||
|
||||
// Shared candidate buffer consumed by the main thread settlement stage.
|
||||
private struct CollisionCandidateData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
|
||||
private struct AreaCollisionRequestData
|
||||
{
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public bool SourceWasActiveAtQueryTime;
|
||||
public Vector3 Center;
|
||||
public float Radius;
|
||||
public int MaxTargets;
|
||||
public int ShapeType;
|
||||
public Vector3 Direction;
|
||||
public float HalfAngleDeg;
|
||||
}
|
||||
|
||||
private struct AreaCollisionHitEventData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
|
||||
// Native buffers used by burst jobs and main-thread collision post-processing.
|
||||
private const int CollisionSourceTypeProjectile = 1;
|
||||
private const int CollisionSourceTypeArea = 2;
|
||||
private const int CollisionShapeCircle = 0;
|
||||
@@ -352,7 +244,7 @@ namespace Simulation
|
||||
_lastCollisionHasEnemyTargets = false;
|
||||
}
|
||||
|
||||
private void SyncSimulationToJobInput()
|
||||
private void SyncSimulationStateToJobInputs()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
|
||||
@@ -361,34 +253,14 @@ namespace Simulation
|
||||
_enemyJobInputs.Clear();
|
||||
_projectileJobInputs.Clear();
|
||||
|
||||
for (int i = 0; i < _enemies.Count; i++)
|
||||
foreach (var data in _enemies)
|
||||
{
|
||||
_enemyJobInputs.Add(ConvertToEnemyJobInput(_enemies[i]));
|
||||
_enemyJobInputs.Add(ConvertToEnemyJobInput(data));
|
||||
}
|
||||
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
foreach (var data in _projectiles)
|
||||
{
|
||||
_projectileJobInputs.Add(ConvertToProjectileJobInput(_projectiles[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncSimulationToJobOutput()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _enemyJobOutputs, _enemies.Count);
|
||||
EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count);
|
||||
|
||||
_enemyJobOutputs.Clear();
|
||||
_projectileJobOutputs.Clear();
|
||||
|
||||
for (int i = 0; i < _enemies.Count; i++)
|
||||
{
|
||||
_enemyJobOutputs.Add(ConvertToEnemyJobOutput(_enemies[i]));
|
||||
}
|
||||
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
_projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i]));
|
||||
_projectileJobInputs.Add(ConvertToProjectileJobInput(data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,19 +288,7 @@ namespace Simulation
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncProjectilesToJobOutput()
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count);
|
||||
_projectileJobOutputs.Clear();
|
||||
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
_projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyProjectileInputToOutput()
|
||||
private void CopyProjectileInputsToOutputs()
|
||||
{
|
||||
for (int i = 0; i < _projectileJobInputs.Length; i++)
|
||||
{
|
||||
@@ -450,7 +310,8 @@ namespace Simulation
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareCollisionCandidateChannels(int queryCount, int expectedCandidateCount, int bucketCapacity)
|
||||
private void PrepareCollisionQueryAndCandidateChannels(int queryCount, int expectedCandidateCount,
|
||||
int bucketCapacity)
|
||||
{
|
||||
InitializeJobDataChannels();
|
||||
EnsureCapacity(ref _collisionQueryInputs, queryCount);
|
||||
@@ -621,7 +482,7 @@ namespace Simulation
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyJobOutputToSimulation()
|
||||
private void ApplyJobOutputsToSimulationState()
|
||||
{
|
||||
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
||||
bool hasEnemyPositionChanged = false;
|
||||
@@ -732,24 +593,6 @@ namespace Simulation
|
||||
};
|
||||
}
|
||||
|
||||
private static EnemyJobOutputData ConvertToEnemyJobOutput(in EnemySimData enemy)
|
||||
{
|
||||
return new EnemyJobOutputData
|
||||
{
|
||||
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
|
||||
@@ -787,24 +630,6 @@ namespace Simulation
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileJobOutputData ConvertToProjectileJobOutput(in ProjectileSimData projectile)
|
||||
{
|
||||
return new ProjectileJobOutputData
|
||||
{
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f549ca9decb4f38933329abacbf78ac
|
||||
timeCreated: 1771901120
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct AreaCollisionHitEventData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5140d989cba042e6bcb217893491273d
|
||||
timeCreated: 1771901785
|
||||
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e32f4e0724314c70882f5ed043a4dca4
|
||||
timeCreated: 1771901749
|
||||
@@ -0,0 +1,48 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e833e91677bb49a4a458c9c8b90099e9
|
||||
timeCreated: 1771901500
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private struct CollisionCandidateData
|
||||
{
|
||||
public int QueryId;
|
||||
public int SourceType;
|
||||
public int SourceEntityId;
|
||||
public int SourceOwnerEntityId;
|
||||
public int TargetEntityId;
|
||||
public float SqrDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 453c14964a4b4aed89c0abd7b25cd26c
|
||||
timeCreated: 1771901722
|
||||
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58abe0d4fd7a49acb716fbd26e0fb19f
|
||||
timeCreated: 1771901691
|
||||
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 171936b8a6b84a8fbacc7651ce473b98
|
||||
timeCreated: 1771901146
|
||||
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34ef6fbf123f45ca88d8e7ab8311d543
|
||||
timeCreated: 1771901204
|
||||
@@ -0,0 +1,93 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[BurstCompile]
|
||||
private struct EnemyMovementBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemyMovement(
|
||||
index,
|
||||
Inputs,
|
||||
Outputs,
|
||||
DeltaTime,
|
||||
PlayerPosition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abd5519779ed41d4947280eec326ffb1
|
||||
timeCreated: 1771901472
|
||||
@@ -0,0 +1,245 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a77477c3a8410cb58b5028dd5d2c63
|
||||
timeCreated: 1771901543
|
||||
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 334ca3869d7940058256ba5419573637
|
||||
timeCreated: 1771901235
|
||||
@@ -0,0 +1,22 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5c4a144914548379d9991d8ef061da7
|
||||
timeCreated: 1771901266
|
||||
@@ -0,0 +1,118 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49001e849c1b4afdaa37d851b9b76866
|
||||
timeCreated: 1771901884
|
||||
@@ -0,0 +1,122 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace 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];
|
||||
float radiusSqr = query.Radius * query.Radius;
|
||||
int centerCellX = (int)math.floor(query.Position.x / CellSize);
|
||||
int centerCellZ = (int)math.floor(query.Position.z / CellSize);
|
||||
int queryRange = math.max(1, (int)math.ceil(query.Radius / CellSize));
|
||||
int selectedCount = 0;
|
||||
bool reachedLimit = false;
|
||||
|
||||
if (HasPlayerTarget && query.SourceEntityId != PlayerTargetEntityId &&
|
||||
query.SourceOwnerEntityId != PlayerTargetEntityId)
|
||||
{
|
||||
float3 playerPosition = PlayerPosition;
|
||||
playerPosition.y = query.Position.y;
|
||||
float3 playerDelta = playerPosition - query.Position;
|
||||
float playerSqrDistance = math.lengthsq(playerDelta);
|
||||
if (playerSqrDistance <= radiusSqr)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
float3 delta = new float3(
|
||||
enemy.Position.x - query.Position.x,
|
||||
enemy.Position.y - query.Position.y,
|
||||
enemy.Position.z - query.Position.z);
|
||||
float sqrDistance = math.lengthsq(delta);
|
||||
if (sqrDistance > radiusSqr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81aa43bb35f547c48b867eafa17a3857
|
||||
timeCreated: 1771914779
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c66a8c10fffb404c9f4819adffea45fd
|
||||
timeCreated: 1771900893
|
||||
+162
-373
@@ -1,101 +1,75 @@
|
||||
using CustomDebugger;
|
||||
using CustomEvent;
|
||||
using CustomUtility;
|
||||
using Definition.DataStruct;
|
||||
using Definition.Enum;
|
||||
using Entity;
|
||||
using Entity.Weapon;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using CustomDebugger;
|
||||
using CustomEvent;
|
||||
using Definition.DataStruct;
|
||||
using Definition.Enum;
|
||||
using Entity;
|
||||
using Entity.Weapon;
|
||||
using CustomUtility;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private const int PlayerEntityId = -1;
|
||||
private JobHandle _collisionCandidateQueryHandle;
|
||||
private bool _collisionCandidateQueryScheduled;
|
||||
|
||||
[Header("投射物模拟参数")] [Tooltip("投射物距离玩家超过该水平半径时回收。小于等于 0 表示不启用该回收条件。")] [SerializeField]
|
||||
private float _projectileMaxDistanceFromPlayer = 120f;
|
||||
|
||||
[Tooltip("投射物与玩家的垂直高度差超过该值时回收。小于等于 0 表示不启用该回收条件。")] [SerializeField]
|
||||
private float _projectileMaxVerticalOffsetFromPlayer = 30f;
|
||||
|
||||
[Tooltip("投射物 Broad Phase 命中查询半径。")] [SerializeField]
|
||||
[Header("Projectile Collision Query")]
|
||||
[Tooltip("Projectile broad-phase collision query radius.")]
|
||||
[SerializeField]
|
||||
private float _projectileCollisionQueryRadius = 0.35f;
|
||||
|
||||
[Tooltip("每个投射物最多保留的候选目标数量。")] [SerializeField]
|
||||
[Tooltip("Maximum retained candidates per projectile query.")]
|
||||
[SerializeField]
|
||||
private int _projectileMaxCandidatesPerQuery = 1;
|
||||
|
||||
[Tooltip("投射物 Broad Phase 分桶网格尺寸。小于等于 0 时将按查询半径自动推导。")] [SerializeField]
|
||||
[Tooltip("Broad-phase bucket cell size. <=0 derives from query radius.")]
|
||||
[SerializeField]
|
||||
private float _projectileCollisionCellSize = 0f;
|
||||
|
||||
[Header("投射物命中事件派发")] [Tooltip("是否派发投射物命中表现事件。")] [SerializeField]
|
||||
[Header("Projectile Hit Event Dispatch")]
|
||||
[Tooltip("Dispatch projectile hit presentation event.")]
|
||||
[SerializeField]
|
||||
private bool _dispatchProjectileHitPresentationEvent = true;
|
||||
|
||||
[Tooltip("命中时是否请求命中标记表现。")] [SerializeField]
|
||||
[Tooltip("Request hit marker when projectile hits.")]
|
||||
[SerializeField]
|
||||
private bool _dispatchProjectileHitMarkerEvent = true;
|
||||
|
||||
[Tooltip("命中时是否请求特效表现。")] [SerializeField]
|
||||
[Tooltip("Request hit effect when projectile hits.")]
|
||||
[SerializeField]
|
||||
private bool _dispatchProjectileHitEffectEvent = true;
|
||||
|
||||
[Tooltip("命中事件建议使用的特效实体类型 Id(<=0 表示不指定,由表现层决定)。")] [SerializeField]
|
||||
[Tooltip("Default hit effect entity type id in presentation event. 0 means not specified.")]
|
||||
[SerializeField]
|
||||
private int _projectileHitPresentationEffectTypeId = 0;
|
||||
|
||||
[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;
|
||||
#region Area Query Request
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteProjectileMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition,
|
||||
MaxSqrDistanceFromPlayer, MaxVerticalOffsetFromPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProjectileMovementJob : 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);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryEnqueueAreaCollisionQuery(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
public bool TryRequestAreaCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
float radius, int maxTargets = 16)
|
||||
{
|
||||
return TryEnqueueAreaCollisionQueryInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||
maxTargets, CollisionShapeCircle, Vector3.forward, 180f);
|
||||
}
|
||||
|
||||
public bool TryEnqueueSectorCollisionQuery(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16)
|
||||
{
|
||||
return TryEnqueueAreaCollisionQueryInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||
maxTargets, CollisionShapeSector, direction, halfAngleDeg);
|
||||
}
|
||||
|
||||
private bool TryEnqueueAreaCollisionQueryInternal(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||
private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId,
|
||||
in Vector3 center,
|
||||
float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg)
|
||||
{
|
||||
if (!_useSimulationMovement || !_useJobSimulation)
|
||||
if (!_useSimulationMovement)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -106,7 +80,7 @@ namespace Simulation
|
||||
}
|
||||
|
||||
int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId;
|
||||
bool sourceWasActiveAtQueryTime = IsCollisionSourceActiveAtQueryTime(sourceEntityId);
|
||||
bool sourceWasActiveAtQueryTime = WasCollisionSourceActiveAtQueryTime(sourceEntityId);
|
||||
Vector3 normalizedDirection = direction;
|
||||
normalizedDirection.y = 0f;
|
||||
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||
@@ -134,12 +108,12 @@ namespace Simulation
|
||||
return true;
|
||||
}
|
||||
|
||||
private int GetPendingAreaCollisionQueryCount()
|
||||
private int GetPendingAreaCollisionRequestCount()
|
||||
{
|
||||
return _areaCollisionRequests.Count;
|
||||
}
|
||||
|
||||
private int EstimatePendingAreaCollisionCandidateCount()
|
||||
private int EstimatePendingAreaCollisionCandidateCountFromRequests()
|
||||
{
|
||||
int expectedCount = 0;
|
||||
for (int i = 0; i < _areaCollisionRequests.Count; i++)
|
||||
@@ -150,61 +124,19 @@ namespace Simulation
|
||||
return expectedCount;
|
||||
}
|
||||
|
||||
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
|
||||
#endregion
|
||||
|
||||
#region Collision Pipeline
|
||||
|
||||
private void PrepareCollisionCandidatesForFrame()
|
||||
{
|
||||
int projectileCount = _projectileJobInputs.Length;
|
||||
if (projectileCount == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
_collisionCandidateQueryScheduled = false;
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyProjectileInputToOutput();
|
||||
return default;
|
||||
}
|
||||
|
||||
float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer);
|
||||
float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f;
|
||||
float maxVerticalOffsetFromPlayer = Mathf.Max(0f, _projectileMaxVerticalOffsetFromPlayer);
|
||||
float3 playerPosition =
|
||||
new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z);
|
||||
NativeArray<ProjectileJobInputData> inputArray = _projectileJobInputs.AsArray();
|
||||
NativeArray<ProjectileJobOutputData> outputArray = _projectileJobOutputs.AsArray();
|
||||
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
ProjectileMovementBurstJob burstJob = new ProjectileMovementBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition,
|
||||
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
|
||||
};
|
||||
return burstJob.Schedule(projectileCount, 64);
|
||||
}
|
||||
|
||||
ProjectileMovementJob job = new ProjectileMovementJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition,
|
||||
MaxSqrDistanceFromPlayer = maxSqrDistanceFromPlayer,
|
||||
MaxVerticalOffsetFromPlayer = maxVerticalOffsetFromPlayer
|
||||
};
|
||||
return job.Schedule(projectileCount, 64);
|
||||
}
|
||||
|
||||
private void BuildProjectileCollisionCandidates()
|
||||
{
|
||||
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated ||
|
||||
!_enemyCollisionBuckets.IsCreated)
|
||||
{
|
||||
ResetCollisionRuntimeStats();
|
||||
ClearAreaCollisionFrameBuffers();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,29 +210,37 @@ namespace Simulation
|
||||
{
|
||||
if (hasEnemyTargets)
|
||||
{
|
||||
BuildEnemyCollisionBucketsForProjectiles(cellSize);
|
||||
BuildEnemyCollisionBuckets(cellSize);
|
||||
}
|
||||
}
|
||||
|
||||
int projectileCandidateCount;
|
||||
int areaCandidateCount;
|
||||
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
|
||||
{
|
||||
QueryProjectileCollisionCandidates(cellSize, hasEnemyTargets, out projectileCandidateCount,
|
||||
out areaCandidateCount);
|
||||
_collisionCandidateQueryScheduled = ScheduleCollisionCandidateQueryJob(cellSize, hasEnemyTargets,
|
||||
out _collisionCandidateQueryHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteCollisionCandidatesForFrame()
|
||||
{
|
||||
if (_collisionCandidateQueryScheduled)
|
||||
{
|
||||
_collisionCandidateQueryHandle.Complete();
|
||||
_collisionCandidateQueryScheduled = false;
|
||||
}
|
||||
|
||||
CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount);
|
||||
_lastProjectileCollisionCandidateCount = projectileCandidateCount;
|
||||
_lastAreaCollisionCandidateCount = areaCandidateCount;
|
||||
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
|
||||
}
|
||||
|
||||
private void ResolveProjectileCollisionCandidatesMainThread()
|
||||
private void ResolveCollisionCandidatesOnMainThread()
|
||||
{
|
||||
if (!_collisionCandidates.IsCreated)
|
||||
{
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
ClearAreaCollisionFrameBuffers();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,7 +251,7 @@ namespace Simulation
|
||||
if (_collisionCandidates.Length == 0)
|
||||
{
|
||||
_lastResolvedAreaHitCount = 0;
|
||||
ClearAreaCollisionFrameBuffers();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,7 +268,7 @@ namespace Simulation
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetActiveProjectileData(projectileEntityId, out _, out ProjectileSimData projectile))
|
||||
if (!TryGetActiveProjectileSimData(projectileEntityId, out _, out ProjectileSimData projectile))
|
||||
{
|
||||
_projectileResolvedEntityIds.Add(projectileEntityId);
|
||||
continue;
|
||||
@@ -338,11 +278,13 @@ namespace Simulation
|
||||
bool shouldDispatchPresentation = false;
|
||||
int damage = 0;
|
||||
Vector3 hitPosition = projectile.Position;
|
||||
if (TryGetTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
|
||||
if (TryGetAliveTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
|
||||
{
|
||||
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
|
||||
EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId);
|
||||
shouldExpireProjectile = ResolveProjectileHit(target, sourceEntity, ownerEntity, in projectile,
|
||||
shouldExpireProjectile = ResolveProjectileHitAgainstTarget(target, sourceEntity,
|
||||
ownerEntity,
|
||||
in projectile,
|
||||
out damage, out hitPosition, out shouldDispatchPresentation);
|
||||
}
|
||||
|
||||
@@ -354,9 +296,10 @@ namespace Simulation
|
||||
|
||||
if (shouldExpireProjectile)
|
||||
{
|
||||
MarkProjectileExpired(projectileEntityId);
|
||||
MarkProjectileAsExpired(projectileEntityId);
|
||||
_projectileResolvedEntityIds.Add(projectileEntityId);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -383,49 +326,20 @@ namespace Simulation
|
||||
int resolvedAreaHitCount;
|
||||
using (CustomProfilerMarker.Collision_ResolveArea.Auto())
|
||||
{
|
||||
resolvedAreaHitCount = ResolveAreaCollisionHitsMainThread();
|
||||
resolvedAreaHitCount = ResolveAreaCollisionHitsOnMainThread();
|
||||
}
|
||||
|
||||
_lastResolvedAreaHitCount = resolvedAreaHitCount;
|
||||
_projectileResolvedEntityIds.Clear();
|
||||
ClearAreaCollisionFrameBuffers();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
}
|
||||
|
||||
private void RecycleInactiveProjectiles()
|
||||
{
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectile = _projectiles[i];
|
||||
if (!ShouldRecycleProjectile(projectile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
#endregion
|
||||
|
||||
_projectileRecycleEntityIds.Add(projectile.EntityId);
|
||||
}
|
||||
#region Collision Resolve Helpers
|
||||
|
||||
if (_projectileRecycleEntityIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
for (int i = 0; i < _projectileRecycleEntityIds.Count; i++)
|
||||
{
|
||||
int entityId = _projectileRecycleEntityIds[i];
|
||||
if (entityComponent != null)
|
||||
{
|
||||
entityComponent.HideEntity(entityId);
|
||||
}
|
||||
|
||||
RemoveProjectileByEntityId(entityId);
|
||||
}
|
||||
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
}
|
||||
|
||||
private bool ResolveProjectileHit(TargetableObject target, EntityBase sourceEntity, EntityBase ownerEntity,
|
||||
private bool ResolveProjectileHitAgainstTarget(TargetableObject target, EntityBase sourceEntity,
|
||||
EntityBase ownerEntity,
|
||||
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
|
||||
out bool shouldDispatchPresentation)
|
||||
{
|
||||
@@ -540,7 +454,7 @@ namespace Simulation
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetTargetableEntity(int entityId, out TargetableObject target)
|
||||
private static bool TryGetAliveTargetableEntity(int entityId, out TargetableObject target)
|
||||
{
|
||||
target = null;
|
||||
|
||||
@@ -570,7 +484,7 @@ namespace Simulation
|
||||
return entityComponent != null ? entityComponent.GetGameEntity(entityId) : null;
|
||||
}
|
||||
|
||||
private bool TryGetActiveProjectileData(int projectileEntityId, out int simulationIndex,
|
||||
private bool TryGetActiveProjectileSimData(int projectileEntityId, out int simulationIndex,
|
||||
out ProjectileSimData projectile)
|
||||
{
|
||||
simulationIndex = -1;
|
||||
@@ -593,7 +507,7 @@ namespace Simulation
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MarkProjectileExpired(int projectileEntityId)
|
||||
private bool MarkProjectileAsExpired(int projectileEntityId)
|
||||
{
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
|
||||
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
|
||||
@@ -614,7 +528,7 @@ namespace Simulation
|
||||
return true;
|
||||
}
|
||||
|
||||
private int ResolveAreaCollisionHitsMainThread()
|
||||
private int ResolveAreaCollisionHitsOnMainThread()
|
||||
{
|
||||
if (_areaCollisionHitEvents.Count == 0)
|
||||
{
|
||||
@@ -625,7 +539,7 @@ namespace Simulation
|
||||
for (int i = 0; i < _areaCollisionHitEvents.Count; i++)
|
||||
{
|
||||
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
|
||||
if (!TryGetCollisionQueryById(hitEvent.QueryId, out CollisionQueryData query) ||
|
||||
if (!TryGetCollisionQueryByQueryId(hitEvent.QueryId, out CollisionQueryData query) ||
|
||||
query.SourceType != CollisionSourceTypeArea)
|
||||
{
|
||||
continue;
|
||||
@@ -642,7 +556,7 @@ namespace Simulation
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
|
||||
if (!TryGetAliveTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -659,7 +573,7 @@ namespace Simulation
|
||||
return resolvedHitCount;
|
||||
}
|
||||
|
||||
private bool TryGetCollisionQueryById(int queryId, out CollisionQueryData query)
|
||||
private bool TryGetCollisionQueryByQueryId(int queryId, out CollisionQueryData query)
|
||||
{
|
||||
query = default;
|
||||
if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length)
|
||||
@@ -734,139 +648,107 @@ namespace Simulation
|
||||
return angle <= halfAngle;
|
||||
}
|
||||
|
||||
private void ClearAreaCollisionFrameBuffers()
|
||||
private void ClearAreaCollisionTransientBuffers()
|
||||
{
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
}
|
||||
|
||||
private void BuildEnemyCollisionBucketsForProjectiles(float cellSize)
|
||||
private void BuildEnemyCollisionBuckets(float cellSize)
|
||||
{
|
||||
_enemyCollisionBuckets.Clear();
|
||||
for (int i = 0; i < _enemyJobOutputs.Length; i++)
|
||||
int enemyCount = _enemyJobOutputs.Length;
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
EnemyJobOutputData enemy = _enemyJobOutputs[i];
|
||||
int cellX = (int)math.floor(enemy.Position.x / cellSize);
|
||||
int cellZ = (int)math.floor(enemy.Position.z / cellSize);
|
||||
_enemyCollisionBuckets.Add(SeparationCellKey(cellX, cellZ), i);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private void QueryProjectileCollisionCandidates(float cellSize, bool hasEnemyTargets,
|
||||
out int projectileCandidateCount, out int areaCandidateCount)
|
||||
[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;
|
||||
areaCandidateCount = 0;
|
||||
bool hasPlayerTarget = TryGetPlayerCollisionTarget(out int playerTargetEntityId, out float3 playerPosition);
|
||||
|
||||
for (int i = 0; i < _collisionQueryInputs.Length; i++)
|
||||
if (!_collisionCandidates.IsCreated)
|
||||
{
|
||||
CollisionQueryData query = _collisionQueryInputs[i];
|
||||
float radiusSqr = query.Radius * query.Radius;
|
||||
int centerCellX = (int)math.floor(query.Position.x / cellSize);
|
||||
int centerCellZ = (int)math.floor(query.Position.z / cellSize);
|
||||
int queryRange = math.max(1, (int)math.ceil(query.Radius / cellSize));
|
||||
int selectedCount = 0;
|
||||
bool reachedLimit = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPlayerTarget && query.SourceEntityId != playerTargetEntityId &&
|
||||
query.SourceOwnerEntityId != playerTargetEntityId)
|
||||
for (int i = 0; i < _collisionCandidates.Length; i++)
|
||||
{
|
||||
CollisionCandidateData candidate = _collisionCandidates[i];
|
||||
if (candidate.SourceType == CollisionSourceTypeProjectile)
|
||||
{
|
||||
playerPosition.y = query.Position.y;
|
||||
float3 playerDelta = playerPosition - query.Position;
|
||||
float playerSqrDistance = math.lengthsq(playerDelta);
|
||||
// Log.Info(
|
||||
// $"playerPos:{playerPosition} - queryPos:{query.Position} = playerSqrDistance:{playerSqrDistance}");
|
||||
if (playerSqrDistance <= radiusSqr)
|
||||
{
|
||||
AddCollisionCandidate(
|
||||
query.QueryId,
|
||||
query.SourceType,
|
||||
query.SourceEntityId,
|
||||
query.SourceOwnerEntityId,
|
||||
playerTargetEntityId,
|
||||
playerSqrDistance);
|
||||
if (query.SourceType == CollisionSourceTypeProjectile)
|
||||
{
|
||||
projectileCandidateCount++;
|
||||
}
|
||||
else if (query.SourceType == CollisionSourceTypeArea)
|
||||
{
|
||||
areaCandidateCount++;
|
||||
}
|
||||
|
||||
selectedCount++;
|
||||
if (selectedCount >= query.MaxTargets)
|
||||
{
|
||||
reachedLimit = true;
|
||||
}
|
||||
}
|
||||
projectileCandidateCount++;
|
||||
}
|
||||
|
||||
if (!hasEnemyTargets || reachedLimit)
|
||||
else if (candidate.SourceType == CollisionSourceTypeArea)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int dx = -queryRange; dx <= queryRange && !reachedLimit; dx++)
|
||||
{
|
||||
for (int dz = -queryRange; dz <= queryRange && !reachedLimit; dz++)
|
||||
{
|
||||
long key = SeparationCellKey(centerCellX + dx, centerCellZ + dz);
|
||||
if (!_enemyCollisionBuckets.TryGetFirstValue(key, out int enemyIndex,
|
||||
out NativeParallelMultiHashMapIterator<long> iterator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (enemyIndex < 0 || enemyIndex >= _enemyJobOutputs.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyJobOutputData enemy = _enemyJobOutputs[enemyIndex];
|
||||
if (enemy.EntityId == query.SourceOwnerEntityId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float3 delta = new float3(
|
||||
enemy.Position.x - query.Position.x,
|
||||
enemy.Position.y - query.Position.y,
|
||||
enemy.Position.z - query.Position.z);
|
||||
float sqrDistance = math.lengthsq(delta);
|
||||
if (sqrDistance > radiusSqr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddCollisionCandidate(
|
||||
query.QueryId,
|
||||
query.SourceType,
|
||||
query.SourceEntityId,
|
||||
query.SourceOwnerEntityId,
|
||||
enemy.EntityId,
|
||||
sqrDistance);
|
||||
if (query.SourceType == CollisionSourceTypeProjectile)
|
||||
{
|
||||
projectileCandidateCount++;
|
||||
}
|
||||
else if (query.SourceType == CollisionSourceTypeArea)
|
||||
{
|
||||
areaCandidateCount++;
|
||||
}
|
||||
selectedCount++;
|
||||
|
||||
if (selectedCount >= query.MaxTargets)
|
||||
{
|
||||
reachedLimit = true;
|
||||
break;
|
||||
}
|
||||
} while (_enemyCollisionBuckets.TryGetNextValue(out enemyIndex, ref iterator));
|
||||
}
|
||||
areaCandidateCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,7 +758,7 @@ namespace Simulation
|
||||
playerEntityId = PlayerEntityId;
|
||||
playerPosition = default;
|
||||
|
||||
if (!TryGetTargetableEntity(playerEntityId, out TargetableObject playerTarget))
|
||||
if (!TryGetAliveTargetableEntity(playerEntityId, out TargetableObject playerTarget))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -892,22 +774,7 @@ namespace Simulation
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ShouldRecycleProjectile(in ProjectileSimData projectile)
|
||||
{
|
||||
if (!projectile.Active)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (projectile.State == ProjectileStateExpired)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return projectile.LifeTime > 0f && projectile.Age >= projectile.LifeTime;
|
||||
}
|
||||
|
||||
private static bool IsCollisionSourceActiveAtQueryTime(int sourceEntityId)
|
||||
private static bool WasCollisionSourceActiveAtQueryTime(int sourceEntityId)
|
||||
{
|
||||
EntityBase sourceEntity = TryGetEntityById(sourceEntityId);
|
||||
if (sourceEntity == null || !sourceEntity.Available)
|
||||
@@ -928,84 +795,6 @@ namespace Simulation
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87bc5c5ec75920d46a65881e949bd2a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,280 @@
|
||||
using CustomDebugger;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
// Orchestrates per-tick simulation pipeline and enemy movement/separation jobs.
|
||||
[Header("敌人互斥参数")]
|
||||
[Tooltip("敌人互斥分桶使用的网格尺寸。小于等于 0 时,将根据敌人体积半径自动计算。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationCellSize = 0f;
|
||||
|
||||
[Tooltip("每次迭代对互斥推力累积值的阻尼系数。数值越大,分离速度越快。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationPushDamping = 0.75f;
|
||||
|
||||
[Tooltip("每次迭代允许的最大互斥位移步长(按敌人体积半径倍率计算)。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationMaxStepScale = 1f;
|
||||
|
||||
[Tooltip("敌人进入攻击范围后,互斥位移是否保持为相对玩家方向的切向分量(避免被径向推离玩家)。")]
|
||||
[SerializeField]
|
||||
private bool _enemySeparationUseTangentialInAttackRange = true;
|
||||
|
||||
[Tooltip("互斥推力方向突变时的时间平滑系数。越大越稳定,但响应越慢。")]
|
||||
[SerializeField]
|
||||
private float _enemySeparationPushSmoothing = 0.55f;
|
||||
|
||||
private void TickSimulationPipeline(in SimulationTickContext context)
|
||||
{
|
||||
// 1. 早退分支:deltaTime <= 0 时只清理碰撞通道和统计,然后返回。
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
PrepareCollisionQueryAndCandidateChannels(0, 0, 0);
|
||||
ResetCollisionRuntimeStats();
|
||||
ClearAreaCollisionTransientBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
JobHandle enemyMovementHandle = default;
|
||||
JobHandle projectileMovementHandle = default;
|
||||
JobHandle enemySeparationHandle = default;
|
||||
bool hasEnemySeparationJob = false;
|
||||
bool hasEnemySeparationCandidates = false;
|
||||
int enemySeparationCount = 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);
|
||||
|
||||
enemySeparationCount = enemyCount;
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
EnemyJobInputData input = _enemyJobInputs[i];
|
||||
if (!input.AvoidEnemyOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasEnemySeparationCandidates = true;
|
||||
float radius = input.EnemyBodyRadius > 0f ? input.EnemyBodyRadius : 0.45f;
|
||||
if (radius > enemySeparationMaxRadius)
|
||||
{
|
||||
enemySeparationMaxRadius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEnemySeparationCandidates)
|
||||
{
|
||||
int separationBucketCapacity = Mathf.Max(128, enemyCount * 2);
|
||||
PrepareEnemySeparationJobBuffers(enemyCount, separationBucketCapacity);
|
||||
}
|
||||
|
||||
int projectileQueryCount = _projectiles.Count;
|
||||
int areaQueryCount = GetPendingAreaCollisionRequestCount();
|
||||
int queryCount = projectileQueryCount + areaQueryCount;
|
||||
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);
|
||||
PrepareCollisionQueryAndCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
|
||||
}
|
||||
|
||||
// 3. StateUpdate 阶段(调度两个移动 Job)
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
|
||||
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
|
||||
}
|
||||
|
||||
// 4. Schedule 阶段(可选分离 Job)
|
||||
// 如果有需要分离的敌人,调度:
|
||||
// BuildEnemySeparationBucketsBurstJob(依赖 EnemyMovement)
|
||||
// EnemySeparationBurstJob(依赖分桶 Job)
|
||||
// 然后把“敌人链路 handle”和“投射物移动 handle”合并。
|
||||
JobHandle simulationHandle;
|
||||
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
|
||||
{
|
||||
hasEnemySeparationJob = TryScheduleEnemySeparationFromJobOutput(
|
||||
in context,
|
||||
enemyMovementHandle,
|
||||
hasEnemySeparationCandidates,
|
||||
enemySeparationCount,
|
||||
enemySeparationMaxRadius,
|
||||
out enemySeparationHandle);
|
||||
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
|
||||
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
|
||||
}
|
||||
|
||||
// 5. Complete 阶段:等待上述 Job 全部完成。
|
||||
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
|
||||
{
|
||||
simulationHandle.Complete();
|
||||
}
|
||||
|
||||
// 6. 主线程后处理阶段:
|
||||
// - 把分离结果覆盖回敌人输出(如果有分离 Job)
|
||||
// - 构建碰撞候选(投射物查询 + area 请求查询 + 网格筛选)
|
||||
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
|
||||
{
|
||||
if (hasEnemySeparationJob)
|
||||
{
|
||||
CommitEnemySeparationFromJobOutput(enemySeparationCount);
|
||||
}
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.Collision.Auto())
|
||||
{
|
||||
PrepareCollisionCandidatesForFrame();
|
||||
CompleteCollisionCandidatesForFrame();
|
||||
}
|
||||
|
||||
// 7. MainThreadCommit 阶段
|
||||
// - ApplyJobOutputsToSimulationState(写回 _enemies/_projectiles)
|
||||
// - ResolveCollisionCandidatesOnMainThread(命中结算、事件、范围碰撞)
|
||||
// - RecycleInactiveAndExpiredProjectiles(隐藏并移除失效投射物)
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
|
||||
{
|
||||
ApplyJobOutputsToSimulationState();
|
||||
ResolveCollisionCandidatesOnMainThread();
|
||||
RecycleInactiveAndExpiredProjectiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
if (enemyCount == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
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
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Rotation = input.Rotation,
|
||||
Speed = input.Speed,
|
||||
AttackRange = input.AttackRange,
|
||||
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = input.EnemyBodyRadius,
|
||||
SeparationIterations = input.SeparationIterations,
|
||||
TargetType = input.TargetType,
|
||||
State = input.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryScheduleEnemySeparationFromJobOutput(in SimulationTickContext context, JobHandle dependency,
|
||||
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
|
||||
{
|
||||
separationHandle = dependency;
|
||||
if (enemyCount <= 0 || !hasSeparationCandidates)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float autoCellSize = maxRadius * 2f;
|
||||
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
|
||||
float cellSize = Mathf.Max(0.1f, configuredCellSize);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[Header("Projectile Simulation")]
|
||||
[Tooltip("Recycle projectile when horizontal distance to player exceeds this range. <=0 disables this rule.")]
|
||||
[SerializeField]
|
||||
private float _projectileMaxDistanceFromPlayer = 120f;
|
||||
|
||||
[Tooltip("Recycle projectile when vertical offset to player exceeds this range. <=0 disables this rule.")]
|
||||
[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()
|
||||
{
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
for (int i = 0; i < _projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectile = _projectiles[i];
|
||||
if (!ShouldRecycleProjectileSimData(projectile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_projectileRecycleEntityIds.Add(projectile.EntityId);
|
||||
}
|
||||
|
||||
if (_projectileRecycleEntityIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
for (int i = 0; i < _projectileRecycleEntityIds.Count; i++)
|
||||
{
|
||||
int entityId = _projectileRecycleEntityIds[i];
|
||||
if (entityComponent != null)
|
||||
{
|
||||
entityComponent.HideEntity(entityId);
|
||||
}
|
||||
|
||||
RemoveProjectileByEntityId(entityId);
|
||||
}
|
||||
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
}
|
||||
|
||||
private 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bf448696a8641268abb660b16def6f5
|
||||
timeCreated: 1771911417
|
||||
@@ -0,0 +1,145 @@
|
||||
using CustomEvent;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using Entity.Weapon;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[Header("投射物命中表现")]
|
||||
[Tooltip("是否监听投射物命中表现事件。")]
|
||||
[SerializeField]
|
||||
private bool _projectileHitPresentationEnabled = true;
|
||||
|
||||
[Tooltip("是否播放投射物命中标记。")]
|
||||
[SerializeField]
|
||||
private bool _projectileHitMarkerEnabled = true;
|
||||
|
||||
[Tooltip("命中标记尺寸。")][SerializeField] private float _projectileHitMarkerSize = 0.2f;
|
||||
|
||||
[Tooltip("命中标记在目标上的高度偏移。")]
|
||||
[SerializeField]
|
||||
private float _projectileHitMarkerYOffset = 1.2f;
|
||||
|
||||
[Tooltip("命中标记持续时间。")]
|
||||
[SerializeField]
|
||||
private float _projectileHitMarkerDuration = 0.15f;
|
||||
|
||||
[Tooltip("命中标记颜色。")][SerializeField] private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
|
||||
|
||||
[Tooltip("是否播放投射物命中特效实体。")]
|
||||
[SerializeField]
|
||||
private bool _projectileHitEffectEnabled;
|
||||
|
||||
[Tooltip("投射物命中特效实体类型 Id(<=0 表示不启用)。")]
|
||||
[SerializeField]
|
||||
private int _projectileHitEffectTypeId;
|
||||
|
||||
private sealed class HitPresentation
|
||||
{
|
||||
private readonly SimulationWorld _world;
|
||||
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
|
||||
private bool _isProjectileHitEventSubscribed;
|
||||
|
||||
public HitPresentation(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnStart()
|
||||
{
|
||||
if (_world == null || !_world._projectileHitPresentationEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventComponent.Subscribe(ProjectileHitPresentationEventArgs.EventId, OnProjectileHitPresentationEvent);
|
||||
_isProjectileHitEventSubscribed = true;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent != null)
|
||||
{
|
||||
eventComponent.Unsubscribe(ProjectileHitPresentationEventArgs.EventId,
|
||||
OnProjectileHitPresentationEvent);
|
||||
}
|
||||
|
||||
_isProjectileHitEventSubscribed = false;
|
||||
}
|
||||
|
||||
private void OnProjectileHitPresentationEvent(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed || _world == null ||
|
||||
e is not ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ShowHitMarker && _world._projectileHitMarkerEnabled)
|
||||
{
|
||||
PlayHitMarker(args);
|
||||
}
|
||||
|
||||
if (args.ShowHitEffect && _world._projectileHitEffectEnabled)
|
||||
{
|
||||
PlayHitEffect(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayHitMarker(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
EntityBase targetEntity = TryGetEntityById(args.TargetEntityId);
|
||||
if (targetEntity == null || !targetEntity.Available)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_projectileHitMarkerEffect ??= new HandgunHitMarkerAttackEffect(
|
||||
Mathf.Max(0.01f, _world._projectileHitMarkerSize),
|
||||
_world._projectileHitMarkerYOffset,
|
||||
Mathf.Max(0.01f, _world._projectileHitMarkerDuration),
|
||||
_world._projectileHitMarkerColor);
|
||||
_projectileHitMarkerEffect.Play(null, args.HitPosition, targetEntity, 0f);
|
||||
}
|
||||
|
||||
private void PlayHitEffect(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
int effectTypeId = args.EffectEntityTypeId > 0
|
||||
? args.EffectEntityTypeId
|
||||
: _world._projectileHitEffectTypeId;
|
||||
if (effectTypeId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
if (entityComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EffectData effectData = new EffectData(entityComponent.GenerateSerialId(), effectTypeId)
|
||||
{
|
||||
Position = args.HitPosition
|
||||
};
|
||||
entityComponent.ShowEffect(effectData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99cec505913140a2b4bdf1498b28c044
|
||||
timeCreated: 1771911517
|
||||
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Entity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
private sealed class TransformSync
|
||||
{
|
||||
private readonly SimulationWorld _world;
|
||||
|
||||
public TransformSync(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnLateUpdate()
|
||||
{
|
||||
if (_world == null || !_world.UseSimulationMovement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enemyManager = GameEntry.EnemyManager;
|
||||
if (enemyManager == null || enemyManager.Enemies == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enemies = enemyManager.Enemies;
|
||||
foreach (var enemy in enemies)
|
||||
{
|
||||
if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyEnemyPresentation(enemyEntity, enemyData);
|
||||
}
|
||||
|
||||
var projectiles = _world._projectiles;
|
||||
for (int i = 0; i < projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectileData = projectiles[i];
|
||||
if (!projectileData.Active || projectileData.State != ProjectileStateActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityBase projectileEntity = TryGetEntityById(projectileData.EntityId);
|
||||
if (projectileEntity == null || !projectileEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyProjectilePresentation(projectileEntity, projectileData);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
|
||||
{
|
||||
Transform enemyTransform = enemyEntity.CachedTransform;
|
||||
enemyTransform.position = enemyData.Position;
|
||||
|
||||
Quaternion rotation = enemyData.Rotation;
|
||||
float rotationMagnitude = Mathf.Abs(rotation.x) + Mathf.Abs(rotation.y) + Mathf.Abs(rotation.z) +
|
||||
Mathf.Abs(rotation.w);
|
||||
if (rotationMagnitude > float.Epsilon)
|
||||
{
|
||||
enemyTransform.rotation = rotation;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 forward = enemyData.Forward;
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude > float.Epsilon)
|
||||
{
|
||||
enemyTransform.forward = forward.normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
|
||||
in ProjectileSimData projectileData)
|
||||
{
|
||||
Transform projectileTransform = projectileEntity.CachedTransform;
|
||||
if (projectileTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.position = projectileData.Position;
|
||||
Vector3 forward = projectileData.Forward;
|
||||
if (forward.sqrMagnitude <= float.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.rotation = Quaternion.LookRotation(forward.normalized, Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21da1bcb02d247358738e9b562af5512
|
||||
timeCreated: 1771911431
|
||||
@@ -1,651 +0,0 @@
|
||||
using CustomDebugger;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
[Header("敌人互斥参数")] [Tooltip("敌人互斥分桶使用的网格尺寸。小于等于 0 时,将根据敌人体积半径自动计算。")] [SerializeField]
|
||||
private float _enemySeparationCellSize = 0f;
|
||||
|
||||
[Tooltip("每次迭代对互斥推力累积值的阻尼系数。数值越大,分离速度越快。")] [SerializeField]
|
||||
private float _enemySeparationPushDamping = 0.75f;
|
||||
|
||||
[Tooltip("每次迭代允许的最大互斥位移步长(按敌人体积半径倍率计算)。")] [SerializeField]
|
||||
private float _enemySeparationMaxStepScale = 1f;
|
||||
|
||||
[Tooltip("敌人进入攻击范围后,互斥位移是否保持为相对玩家方向的切向分量(避免被径向推离玩家)。")] [SerializeField]
|
||||
private bool _enemySeparationUseTangentialInAttackRange = true;
|
||||
|
||||
[Tooltip("互斥推力方向突变时的时间平滑系数。越大越稳定,但响应越慢。")] [SerializeField]
|
||||
private float _enemySeparationPushSmoothing = 0.55f;
|
||||
|
||||
[BurstCompile]
|
||||
private struct EnemyMovementBurstJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemyMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private struct EnemyMovementJob : IJobParallelFor
|
||||
{
|
||||
[ReadOnly] public NativeArray<EnemyJobInputData> Inputs;
|
||||
public NativeArray<EnemyJobOutputData> Outputs;
|
||||
public float DeltaTime;
|
||||
public float3 PlayerPosition;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
ExecuteEnemyMovement(index, Inputs, Outputs, DeltaTime, PlayerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
[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 struct BuildEnemySeparationBucketsJob : 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);
|
||||
}
|
||||
}
|
||||
|
||||
[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 struct EnemySeparationJob : 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 void TickEnemiesJobified(in SimulationTickContext context)
|
||||
{
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
PrepareCollisionCandidateChannels(0, 0, 0);
|
||||
ResetCollisionRuntimeStats();
|
||||
ClearAreaCollisionFrameBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
JobHandle enemyMovementHandle = default;
|
||||
JobHandle projectileMovementHandle = default;
|
||||
JobHandle enemySeparationHandle = default;
|
||||
bool hasEnemySeparationJob = false;
|
||||
bool hasEnemySeparationCandidates = false;
|
||||
int enemySeparationCount = 0;
|
||||
float enemySeparationMaxRadius = 0.45f;
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||
{
|
||||
SyncSimulationToJobInput();
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
int projectileCount = _projectileJobInputs.Length;
|
||||
PrepareEnemyJobOutputBuffer(enemyCount);
|
||||
PrepareProjectileJobOutputBuffer(projectileCount);
|
||||
|
||||
enemySeparationCount = enemyCount;
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
EnemyJobInputData input = _enemyJobInputs[i];
|
||||
if (!input.AvoidEnemyOverlap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasEnemySeparationCandidates = true;
|
||||
float radius = input.EnemyBodyRadius > 0f ? input.EnemyBodyRadius : 0.45f;
|
||||
if (radius > enemySeparationMaxRadius)
|
||||
{
|
||||
enemySeparationMaxRadius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEnemySeparationCandidates)
|
||||
{
|
||||
int separationBucketCapacity = Mathf.Max(128, enemyCount * 2);
|
||||
PrepareEnemySeparationJobBuffers(enemyCount, separationBucketCapacity);
|
||||
}
|
||||
|
||||
int projectileQueryCount = _projectiles.Count;
|
||||
int areaQueryCount = GetPendingAreaCollisionQueryCount();
|
||||
int queryCount = projectileQueryCount + areaQueryCount;
|
||||
int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery);
|
||||
int areaExpectedCount = EstimatePendingAreaCollisionCandidateCount();
|
||||
int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount);
|
||||
int bucketCapacity = Mathf.Max(256, _enemies.Count * 2 + queryCount);
|
||||
PrepareCollisionCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
enemyMovementHandle = ExecuteEnemyMovementJob(in context);
|
||||
projectileMovementHandle = ExecuteProjectileMovementJob(in context);
|
||||
}
|
||||
|
||||
JobHandle simulationHandle;
|
||||
using (CustomProfilerMarker.TickEnemies_Schedule.Auto())
|
||||
{
|
||||
hasEnemySeparationJob = TryScheduleEnemySeparationForJobOutput(
|
||||
in context,
|
||||
enemyMovementHandle,
|
||||
hasEnemySeparationCandidates,
|
||||
enemySeparationCount,
|
||||
enemySeparationMaxRadius,
|
||||
out enemySeparationHandle);
|
||||
JobHandle enemyHandle = hasEnemySeparationJob ? enemySeparationHandle : enemyMovementHandle;
|
||||
simulationHandle = JobHandle.CombineDependencies(enemyHandle, projectileMovementHandle);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_Complete.Auto())
|
||||
{
|
||||
simulationHandle.Complete();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||
{
|
||||
if (hasEnemySeparationJob)
|
||||
{
|
||||
CommitEnemySeparationForJobOutput(enemySeparationCount);
|
||||
}
|
||||
|
||||
BuildProjectileCollisionCandidates();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
using (CustomProfilerMarker.TickEnemies_MainThreadCommit.Auto())
|
||||
{
|
||||
ApplyJobOutputToSimulation();
|
||||
ResolveProjectileCollisionCandidatesMainThread();
|
||||
RecycleInactiveProjectiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JobHandle ExecuteEnemyMovementJob(in SimulationTickContext context)
|
||||
{
|
||||
int enemyCount = _enemyJobInputs.Length;
|
||||
if (enemyCount == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (context.DeltaTime <= 0f)
|
||||
{
|
||||
CopyEnemyInputToOutput();
|
||||
return default;
|
||||
}
|
||||
|
||||
float3 playerPosition = new float3(context.PlayerPosition.x, 0f, context.PlayerPosition.z);
|
||||
NativeArray<EnemyJobInputData> inputArray = _enemyJobInputs.AsArray();
|
||||
NativeArray<EnemyJobOutputData> outputArray = _enemyJobOutputs.AsArray();
|
||||
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
EnemyMovementBurstJob burstJob = new EnemyMovementBurstJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
return burstJob.Schedule(enemyCount, 64);
|
||||
}
|
||||
|
||||
EnemyMovementJob job = new EnemyMovementJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Outputs = outputArray,
|
||||
DeltaTime = context.DeltaTime,
|
||||
PlayerPosition = playerPosition
|
||||
};
|
||||
return job.Schedule(enemyCount, 64);
|
||||
}
|
||||
|
||||
private void CopyEnemyInputToOutput()
|
||||
{
|
||||
for (int i = 0; i < _enemyJobInputs.Length; i++)
|
||||
{
|
||||
EnemyJobInputData input = _enemyJobInputs[i];
|
||||
_enemyJobOutputs[i] = new EnemyJobOutputData
|
||||
{
|
||||
EntityId = input.EntityId,
|
||||
Position = input.Position,
|
||||
Forward = input.Forward,
|
||||
Rotation = input.Rotation,
|
||||
Speed = input.Speed,
|
||||
AttackRange = input.AttackRange,
|
||||
AvoidEnemyOverlap = input.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = input.EnemyBodyRadius,
|
||||
SeparationIterations = input.SeparationIterations,
|
||||
TargetType = input.TargetType,
|
||||
State = input.State
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryScheduleEnemySeparationForJobOutput(in SimulationTickContext context, JobHandle dependency,
|
||||
bool hasSeparationCandidates, int enemyCount, float maxRadius, out JobHandle separationHandle)
|
||||
{
|
||||
separationHandle = dependency;
|
||||
if (enemyCount <= 0 || !hasSeparationCandidates)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float autoCellSize = maxRadius * 2f;
|
||||
float configuredCellSize = _enemySeparationCellSize > 0f ? _enemySeparationCellSize : autoCellSize;
|
||||
float cellSize = Mathf.Max(0.1f, configuredCellSize);
|
||||
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();
|
||||
if (_useBurstJobs)
|
||||
{
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
BuildEnemySeparationBucketsJob nonBurstBuildJob = new BuildEnemySeparationBucketsJob
|
||||
{
|
||||
Inputs = inputArray,
|
||||
Buckets = _enemySeparationBuckets.AsParallelWriter(),
|
||||
CellSize = cellSize
|
||||
};
|
||||
JobHandle nonBurstBuildHandle = nonBurstBuildJob.Schedule(enemyCount, 64, dependency);
|
||||
EnemySeparationJob nonBurstSeparationJob = new EnemySeparationJob
|
||||
{
|
||||
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 = nonBurstSeparationJob.Schedule(enemyCount, 64, nonBurstBuildHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CommitEnemySeparationForJobOutput(int enemyCount)
|
||||
{
|
||||
if (enemyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CommitEnemySeparationTemporalBuffers(enemyCount);
|
||||
for (int i = 0; i < enemyCount; i++)
|
||||
{
|
||||
_enemyJobOutputs[i] = _enemyJobSeparationOutputs[i];
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 long SeparationCellKey(int x, int z)
|
||||
{
|
||||
return ((long)x << 32) ^ (uint)z;
|
||||
}
|
||||
|
||||
private static float3 GetZeroDistanceSeparationAxis(int index, int otherIndex)
|
||||
{
|
||||
int lowIndex = math.min(index, otherIndex);
|
||||
int highIndex = math.max(index, otherIndex);
|
||||
uint pairHash = (uint)(lowIndex * 73856093) ^ (uint)(highIndex * 19349663);
|
||||
|
||||
float axisX = (pairHash & 1023u) / 511.5f - 1f;
|
||||
float axisZ = ((pairHash >> 10) & 1023u) / 511.5f - 1f;
|
||||
float3 axis = new float3(axisX, 0f, axisZ);
|
||||
return math.normalizesafe(axis, new float3(1f, 0f, 0f));
|
||||
}
|
||||
|
||||
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
|
||||
NativeArray<EnemyJobOutputData> outputs, float deltaTime, float3 playerPosition)
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ using UnityGameFramework.Runtime;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public partial class SimulationWorld
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
// Bridges entity show/hide events into simulation state registration.
|
||||
public sealed class EntitySync
|
||||
{
|
||||
private const string EnemyGroupName = "Enemy";
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
using CustomEvent;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using Entity.Weapon;
|
||||
using GameFramework.Event;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public partial class SimulationWorld
|
||||
{
|
||||
[Header("投射物命中表现")] [Tooltip("是否监听投射物命中表现事件。")] [SerializeField]
|
||||
private bool _projectileHitPresentationEnabled = true;
|
||||
|
||||
[Tooltip("是否播放投射物命中标记。")] [SerializeField]
|
||||
private bool _projectileHitMarkerEnabled = true;
|
||||
|
||||
[Tooltip("命中标记尺寸。")] [SerializeField]
|
||||
private float _projectileHitMarkerSize = 0.2f;
|
||||
|
||||
[Tooltip("命中标记在目标上的高度偏移。")] [SerializeField]
|
||||
private float _projectileHitMarkerYOffset = 1.2f;
|
||||
|
||||
[Tooltip("命中标记持续时间。")] [SerializeField]
|
||||
private float _projectileHitMarkerDuration = 0.15f;
|
||||
|
||||
[Tooltip("命中标记颜色。")] [SerializeField]
|
||||
private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
|
||||
|
||||
[Tooltip("是否播放投射物命中特效实体。")] [SerializeField]
|
||||
private bool _projectileHitEffectEnabled;
|
||||
|
||||
[Tooltip("投射物命中特效实体类型 Id(<=0 表示不启用)。")] [SerializeField]
|
||||
private int _projectileHitEffectTypeId;
|
||||
|
||||
private sealed class Presentation
|
||||
{
|
||||
private readonly SimulationWorld _world;
|
||||
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
|
||||
private bool _isProjectileHitEventSubscribed;
|
||||
|
||||
public Presentation(SimulationWorld world)
|
||||
{
|
||||
_world = world;
|
||||
}
|
||||
|
||||
public void OnStart()
|
||||
{
|
||||
if (_world == null || !_world._projectileHitPresentationEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventComponent.Subscribe(ProjectileHitPresentationEventArgs.EventId, OnProjectileHitPresentationEvent);
|
||||
_isProjectileHitEventSubscribed = true;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventComponent = GameEntry.Event;
|
||||
if (eventComponent != null)
|
||||
{
|
||||
eventComponent.Unsubscribe(ProjectileHitPresentationEventArgs.EventId,
|
||||
OnProjectileHitPresentationEvent);
|
||||
}
|
||||
|
||||
_isProjectileHitEventSubscribed = false;
|
||||
}
|
||||
|
||||
public void OnLateUpdate()
|
||||
{
|
||||
if (_world == null || !_world.UseSimulationMovement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enemyManager = GameEntry.EnemyManager;
|
||||
if (enemyManager == null || enemyManager.Enemies == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enemies = enemyManager.Enemies;
|
||||
foreach (var enemy in enemies)
|
||||
{
|
||||
if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_world.TryGetEnemyData(enemyEntity.Id, out EnemySimData enemyData))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyEnemyPresentation(enemyEntity, enemyData);
|
||||
}
|
||||
|
||||
if (!_world.UseJobSimulation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var projectiles = _world._projectiles;
|
||||
for (int i = 0; i < projectiles.Count; i++)
|
||||
{
|
||||
ProjectileSimData projectileData = projectiles[i];
|
||||
if (!projectileData.Active || projectileData.State != ProjectileStateActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityBase projectileEntity = TryGetEntityById(projectileData.EntityId);
|
||||
if (projectileEntity == null || !projectileEntity.Available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ApplyProjectilePresentation(projectileEntity, projectileData);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProjectileHitPresentationEvent(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!_isProjectileHitEventSubscribed || _world == null ||
|
||||
e is not ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ShowHitMarker && _world._projectileHitMarkerEnabled)
|
||||
{
|
||||
PlayHitMarker(args);
|
||||
}
|
||||
|
||||
if (args.ShowHitEffect && _world._projectileHitEffectEnabled)
|
||||
{
|
||||
PlayHitEffect(args);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayHitMarker(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
EntityBase targetEntity = TryGetEntityById(args.TargetEntityId);
|
||||
if (targetEntity == null || !targetEntity.Available)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_projectileHitMarkerEffect ??= new HandgunHitMarkerAttackEffect(
|
||||
Mathf.Max(0.01f, _world._projectileHitMarkerSize),
|
||||
_world._projectileHitMarkerYOffset,
|
||||
Mathf.Max(0.01f, _world._projectileHitMarkerDuration),
|
||||
_world._projectileHitMarkerColor);
|
||||
_projectileHitMarkerEffect.Play(null, args.HitPosition, targetEntity, 0f);
|
||||
}
|
||||
|
||||
private void PlayHitEffect(ProjectileHitPresentationEventArgs args)
|
||||
{
|
||||
int effectTypeId = args.EffectEntityTypeId > 0 ? args.EffectEntityTypeId : _world._projectileHitEffectTypeId;
|
||||
if (effectTypeId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entityComponent = GameEntry.Entity;
|
||||
if (entityComponent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EffectData effectData = new EffectData(entityComponent.GenerateSerialId(), effectTypeId)
|
||||
{
|
||||
Position = args.HitPosition
|
||||
};
|
||||
entityComponent.ShowEffect(effectData);
|
||||
}
|
||||
|
||||
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
|
||||
{
|
||||
Transform enemyTransform = enemyEntity.CachedTransform;
|
||||
enemyTransform.position = enemyData.Position;
|
||||
|
||||
Quaternion rotation = enemyData.Rotation;
|
||||
float rotationMagnitude = Mathf.Abs(rotation.x) + Mathf.Abs(rotation.y) + Mathf.Abs(rotation.z) +
|
||||
Mathf.Abs(rotation.w);
|
||||
if (rotationMagnitude > float.Epsilon)
|
||||
{
|
||||
enemyTransform.rotation = rotation;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 forward = enemyData.Forward;
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude > float.Epsilon)
|
||||
{
|
||||
enemyTransform.forward = forward.normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
|
||||
in ProjectileSimData projectileData)
|
||||
{
|
||||
Transform projectileTransform = projectileEntity.CachedTransform;
|
||||
if (projectileTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.position = projectileData.Position;
|
||||
Vector3 forward = projectileData.Forward;
|
||||
if (forward.sqrMagnitude <= float.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
projectileTransform.rotation = Quaternion.LookRotation(forward.normalized, Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
using Components;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld
|
||||
{
|
||||
#region Simulation State Lifecycle
|
||||
|
||||
public void ClearSimulationState()
|
||||
{
|
||||
_enemies.Clear();
|
||||
_projectiles.Clear();
|
||||
_pickups.Clear();
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
_projectileResolvedEntityIds.Clear();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
ClearJobDataChannels();
|
||||
|
||||
EnemyBinding.Clear();
|
||||
ProjectileBinding.Clear();
|
||||
PickupBinding.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
private int UpsertEnemy(in EnemySimData simData)
|
||||
{
|
||||
if (!EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddEnemy(simData);
|
||||
}
|
||||
|
||||
_enemies[simulationIndex] = simData;
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private bool RemoveEnemyByEntityId(int entityId)
|
||||
{
|
||||
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private void RegisterEnemyLifecycle(EnemyBase enemy, object userData)
|
||||
{
|
||||
if (enemy == null || enemy.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyData enemyData = userData as EnemyData;
|
||||
UpsertEnemy(CreateEnemyInitialSimData(enemy, enemyData));
|
||||
}
|
||||
|
||||
private void UnregisterEnemyLifecycle(int entityId)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
|
||||
{
|
||||
Transform enemyTransform = enemy.CachedTransform;
|
||||
MovementComponent movementComponent = enemy.GetComponent<MovementComponent>();
|
||||
|
||||
float speed = 0f;
|
||||
if (enemyData != null)
|
||||
{
|
||||
speed = enemyData.SpeedBase;
|
||||
}
|
||||
else if (movementComponent != null)
|
||||
{
|
||||
speed = movementComponent.Speed;
|
||||
}
|
||||
|
||||
float attackRange = enemy != null && enemy.AttackRange > 0f
|
||||
? enemy.AttackRange
|
||||
: DefaultAttackRange;
|
||||
|
||||
return new EnemySimData
|
||||
{
|
||||
EntityId = enemy.Id,
|
||||
Position = enemyTransform.position,
|
||||
Forward = enemyTransform.forward,
|
||||
Rotation = enemyTransform.rotation,
|
||||
Speed = speed,
|
||||
AttackRange = attackRange,
|
||||
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
|
||||
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
|
||||
TargetType = 0,
|
||||
State = EnemyStateIdle
|
||||
};
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
private int UpsertProjectile(in ProjectileSimData simData)
|
||||
{
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddProjectile(simData);
|
||||
}
|
||||
|
||||
_projectiles[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
if (projectileEntity == null || projectileEntity.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity, userData));
|
||||
}
|
||||
|
||||
private void UnregisterProjectileLifecycle(int entityId)
|
||||
{
|
||||
RemoveProjectileByEntityId(entityId);
|
||||
}
|
||||
|
||||
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
Vector3 forward = projectileEntity.CachedTransform.forward;
|
||||
int ownerEntityId = 0;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
float speed = 0f;
|
||||
float lifeTime = 0f;
|
||||
|
||||
if (userData is EnemyProjectileData enemyProjectileData)
|
||||
{
|
||||
ownerEntityId = enemyProjectileData.OwnerEntityId;
|
||||
|
||||
Vector3 direction = enemyProjectileData.Direction;
|
||||
direction.y = 0f;
|
||||
if (direction.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
direction.Normalize();
|
||||
forward = direction;
|
||||
}
|
||||
else if (forward.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
forward = forward.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
forward = Vector3.forward;
|
||||
}
|
||||
|
||||
speed = Mathf.Max(0f, enemyProjectileData.Speed);
|
||||
velocity = forward * speed;
|
||||
lifeTime = Mathf.Max(0f, enemyProjectileData.LifeTime);
|
||||
}
|
||||
|
||||
return new ProjectileSimData
|
||||
{
|
||||
EntityId = projectileEntity.Id,
|
||||
OwnerEntityId = ownerEntityId,
|
||||
Position = projectileEntity.CachedTransform.position,
|
||||
Forward = forward,
|
||||
Velocity = velocity,
|
||||
Speed = speed,
|
||||
LifeTime = lifeTime,
|
||||
Age = 0f,
|
||||
Active = true,
|
||||
RemainingLifetime = lifeTime,
|
||||
State = ProjectileStateActive
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pickup Simulation State
|
||||
|
||||
private int AddPickup(in PickupSimData simData)
|
||||
{
|
||||
int simulationIndex = _pickups.Count;
|
||||
_pickups.Add(simData);
|
||||
PickupBinding.Bind(simData.EntityId, simulationIndex);
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertPickup(in PickupSimData simData)
|
||||
{
|
||||
if (!PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddPickup(simData);
|
||||
}
|
||||
|
||||
_pickups[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void RegisterPickupLifecycle(EntityBase pickupEntity)
|
||||
{
|
||||
if (pickupEntity == null || pickupEntity.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpsertPickup(CreatePickupInitialSimData(pickupEntity));
|
||||
}
|
||||
|
||||
private void UnregisterPickupLifecycle(int entityId)
|
||||
{
|
||||
RemovePickupByEntityId(entityId);
|
||||
}
|
||||
|
||||
private static PickupSimData CreatePickupInitialSimData(EntityBase pickupEntity)
|
||||
{
|
||||
return new PickupSimData
|
||||
{
|
||||
EntityId = pickupEntity.Id,
|
||||
Position = pickupEntity.CachedTransform.position,
|
||||
PickupRadius = 0.35f,
|
||||
State = 0
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a3f6ab748524f8f8ab6655e294889f1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -19,7 +19,7 @@ namespace Simulation
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_useSimulationMovement || !_useJobSimulation)
|
||||
if (!_useSimulationMovement)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Components;
|
||||
using CustomDebugger;
|
||||
using CustomUtility;
|
||||
using Entity;
|
||||
using Entity.EntityData;
|
||||
using Procedure;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
@@ -12,6 +7,18 @@ namespace Simulation
|
||||
{
|
||||
public sealed partial class SimulationWorld : GameFrameworkComponent
|
||||
{
|
||||
// Partial layout:
|
||||
// - SimulationWorld.cs: 核心状态、常量和 Unity 生命周期入口点。
|
||||
// - SimulationWorld.SimEntityState.cs: 模拟状态的增删改查和生命周期注册。
|
||||
// - SimulationWorld.EntitySync.cs: GameFramework 实体 show/hide 事件桥。
|
||||
// - SimulationWorld.TargetSelectionSpatialIndex.cs: 最近敌空间索引查询。
|
||||
// - Presentation/SimulationWorld.TransformSync.cs: late-update transform 同步桥。
|
||||
// - Presentation/SimulationWorld.HitPresentation.cs: 投射物命中事件表现桥。
|
||||
// - DataChannel/SimulationWorld.JobDataChannel.cs: 本地 通道/缓冲区 持有者和数据的相互转换。
|
||||
// - Jobs/SimulationWorld.EnemyJobs.cs: 模拟通道 编排 + 敌人移动/分离 顺序执行
|
||||
// - Jobs/SimulationWorld.ProjectileJobs.cs: 投射物移动与回收
|
||||
// - Jobs/SimulationWorld.CollisionPipeline.cs: 碰撞请求的构造、过滤、求解流水线
|
||||
// - JobStruct/*.cs: burst job 内核和面向 job 的数据结构
|
||||
private const float DefaultAttackRange = 1f;
|
||||
private const int EnemyStateIdle = 0;
|
||||
private const int EnemyStateChasing = 1;
|
||||
@@ -19,43 +26,18 @@ namespace Simulation
|
||||
private const int ProjectileStateActive = 0;
|
||||
private const int ProjectileStateExpired = 1;
|
||||
|
||||
private struct EnemyTickWorkItem
|
||||
{
|
||||
public int EntityId;
|
||||
public Vector3 CurrentPosition;
|
||||
public Vector3 DesiredPosition;
|
||||
public Vector3 ToPlayer;
|
||||
public Vector3 Forward;
|
||||
public Quaternion Rotation;
|
||||
public float SqrDistanceToPlayer;
|
||||
public float AttackRangeSqr;
|
||||
public float Speed;
|
||||
public int SeparationIterations;
|
||||
public bool AvoidEnemyOverlap;
|
||||
public bool CanChase;
|
||||
public bool HasRotationUpdate;
|
||||
public int NextState;
|
||||
}
|
||||
|
||||
[Header("模拟世界全局设置")] [Tooltip("是否启用世界模拟")] [SerializeField]
|
||||
private bool _useSimulationMovement;
|
||||
|
||||
[Tooltip("是否启用 Job 运算路径")] [SerializeField]
|
||||
private bool _useJobSimulation;
|
||||
|
||||
[Tooltip("是否使用 Burst 来完成计算")] [SerializeField]
|
||||
private bool _useBurstJobs = true;
|
||||
private bool _useSimulationMovement = true;
|
||||
|
||||
private EntitySync _entitySync;
|
||||
private Presentation _presentation;
|
||||
private TransformSync _transformSync;
|
||||
private HitPresentation _hitPresentation;
|
||||
|
||||
private readonly List<EnemySimData> _enemies = new List<EnemySimData>();
|
||||
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
|
||||
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
|
||||
private readonly List<int> _projectileRecycleEntityIds = new List<int>();
|
||||
private readonly HashSet<int> _projectileResolvedEntityIds = new HashSet<int>();
|
||||
private readonly List<EnemySeparationAgent> _enemySeparationAgents = new List<EnemySeparationAgent>();
|
||||
private readonly List<EnemyTickWorkItem> _enemyTickWorkItems = new List<EnemyTickWorkItem>();
|
||||
|
||||
private EntityBinding EnemyBinding { get; } = new EntityBinding();
|
||||
private EntityBinding ProjectileBinding { get; } = new EntityBinding();
|
||||
@@ -65,255 +47,22 @@ namespace Simulation
|
||||
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
|
||||
public IReadOnlyList<PickupSimData> Pickups => _pickups;
|
||||
public bool UseSimulationMovement => _useSimulationMovement;
|
||||
public bool UseJobSimulation => _useJobSimulation;
|
||||
public bool UseBurstJobs => _useBurstJobs;
|
||||
|
||||
public void SetUseSimulationMovement(bool enabled)
|
||||
{
|
||||
if (IsBattleStateActive())
|
||||
{
|
||||
Log.Warning("SetUseSimulationMovement is ignored during Battle. Change this switch outside Battle.");
|
||||
return;
|
||||
}
|
||||
|
||||
_useSimulationMovement = enabled;
|
||||
}
|
||||
|
||||
public void SetUseJobSimulation(bool enabled)
|
||||
{
|
||||
if (IsBattleStateActive())
|
||||
{
|
||||
Log.Warning("SetUseJobSimulation is ignored during Battle. Change this switch outside Battle.");
|
||||
return;
|
||||
}
|
||||
|
||||
_useJobSimulation = enabled;
|
||||
}
|
||||
|
||||
public void SetUseBurstJobs(bool enabled)
|
||||
{
|
||||
_useBurstJobs = enabled;
|
||||
}
|
||||
|
||||
private static bool IsBattleStateActive()
|
||||
{
|
||||
var procedureComponent = GameEntry.Procedure;
|
||||
if (procedureComponent == null ||
|
||||
procedureComponent.CurrentProcedure is not ProcedureGame procedureGame)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return procedureGame.CurrentGameStateType == GameStateType.Battle;
|
||||
}
|
||||
#region Lifecycle
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
_entitySync = new EntitySync(this);
|
||||
_presentation = new Presentation(this);
|
||||
_transformSync = new TransformSync(this);
|
||||
_hitPresentation = new HitPresentation(this);
|
||||
InitializeJobDataChannels();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_entitySync?.OnStart();
|
||||
_presentation?.OnStart();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_presentation?.OnDestroy();
|
||||
_entitySync?.OnDestroy();
|
||||
_entitySync = null;
|
||||
_presentation = null;
|
||||
DisposeJobDataChannels();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
_presentation?.OnLateUpdate();
|
||||
}
|
||||
|
||||
private int AddEnemy(in EnemySimData simData)
|
||||
{
|
||||
int simulationIndex = _enemies.Count;
|
||||
_enemies.Add(simData);
|
||||
EnemyBinding.Bind(simData.EntityId, simulationIndex);
|
||||
OnEnemyAddedToSeparationTemporalBuffers();
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertEnemy(in EnemySimData simData)
|
||||
{
|
||||
if (!EnemyBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddEnemy(simData);
|
||||
}
|
||||
|
||||
_enemies[simulationIndex] = simData;
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private bool RemoveEnemyByEntityId(int entityId)
|
||||
{
|
||||
if (!EnemyBinding.TryGetSimulationIndex(entityId, out int simulationIndex))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private void RegisterEnemyLifecycle(EnemyBase enemy, object userData)
|
||||
{
|
||||
if (enemy == null || enemy.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyData enemyData = userData as EnemyData;
|
||||
UpsertEnemy(CreateEnemyInitialSimData(enemy, enemyData));
|
||||
}
|
||||
|
||||
private void UnregisterEnemyLifecycle(int entityId)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private int AddProjectile(in ProjectileSimData simData)
|
||||
{
|
||||
int simulationIndex = _projectiles.Count;
|
||||
_projectiles.Add(simData);
|
||||
ProjectileBinding.Bind(simData.EntityId, simulationIndex);
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertProjectile(in ProjectileSimData simData)
|
||||
{
|
||||
if (!ProjectileBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddProjectile(simData);
|
||||
}
|
||||
|
||||
_projectiles[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
if (projectileEntity == null || projectileEntity.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity, userData));
|
||||
}
|
||||
|
||||
private void UnregisterProjectileLifecycle(int entityId)
|
||||
{
|
||||
RemoveProjectileByEntityId(entityId);
|
||||
}
|
||||
|
||||
private int AddPickup(in PickupSimData simData)
|
||||
{
|
||||
int simulationIndex = _pickups.Count;
|
||||
_pickups.Add(simData);
|
||||
PickupBinding.Bind(simData.EntityId, simulationIndex);
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
private int UpsertPickup(in PickupSimData simData)
|
||||
{
|
||||
if (!PickupBinding.TryGetSimulationIndex(simData.EntityId, out int simulationIndex))
|
||||
{
|
||||
return AddPickup(simData);
|
||||
}
|
||||
|
||||
_pickups[simulationIndex] = simData;
|
||||
return simulationIndex;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void RegisterPickupLifecycle(EntityBase pickupEntity)
|
||||
{
|
||||
if (pickupEntity == null || pickupEntity.CachedTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpsertPickup(CreatePickupInitialSimData(pickupEntity));
|
||||
}
|
||||
|
||||
private void UnregisterPickupLifecycle(int entityId)
|
||||
{
|
||||
RemovePickupByEntityId(entityId);
|
||||
_hitPresentation?.OnStart();
|
||||
}
|
||||
|
||||
public void Tick(in SimulationTickContext context)
|
||||
@@ -323,307 +72,27 @@ namespace Simulation
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useJobSimulation)
|
||||
{
|
||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
||||
{
|
||||
TickEnemiesJobified(in context);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
||||
{
|
||||
TickEnemies(in context);
|
||||
TickSimulationPipeline(in context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
private void OnDestroy()
|
||||
{
|
||||
_enemies.Clear();
|
||||
_projectiles.Clear();
|
||||
_pickups.Clear();
|
||||
_projectileRecycleEntityIds.Clear();
|
||||
_projectileResolvedEntityIds.Clear();
|
||||
_areaCollisionRequests.Clear();
|
||||
_areaCollisionHitEvents.Clear();
|
||||
_areaCollisionHitDedupKeys.Clear();
|
||||
_enemySeparationAgents.Clear();
|
||||
_enemyTickWorkItems.Clear();
|
||||
ClearJobDataChannels();
|
||||
|
||||
EnemyBinding.Clear();
|
||||
ProjectileBinding.Clear();
|
||||
PickupBinding.Clear();
|
||||
_hitPresentation?.OnDestroy();
|
||||
_entitySync?.OnDestroy();
|
||||
_entitySync = null;
|
||||
_transformSync = null;
|
||||
_hitPresentation = null;
|
||||
DisposeJobDataChannels();
|
||||
}
|
||||
|
||||
private void TickEnemies(in SimulationTickContext context)
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_enemies.Count == 0 || context.DeltaTime <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 playerPosition = context.PlayerPosition;
|
||||
playerPosition.y = 0f;
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||
{
|
||||
BuildEnemyTickInput(in playerPosition);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||
{
|
||||
MoveAndSeparateEnemies(context.DeltaTime);
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||
{
|
||||
UpdateEnemyStates();
|
||||
}
|
||||
|
||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||
{
|
||||
WriteBackEnemyTickResults();
|
||||
}
|
||||
_transformSync?.OnLateUpdate();
|
||||
}
|
||||
|
||||
private void BuildEnemyTickInput(in Vector3 playerPosition)
|
||||
{
|
||||
_enemyTickWorkItems.Clear();
|
||||
_enemySeparationAgents.Clear();
|
||||
|
||||
for (int i = 0; i < _enemies.Count; i++)
|
||||
{
|
||||
EnemySimData enemy = _enemies[i];
|
||||
|
||||
Vector3 currentPosition = enemy.Position;
|
||||
Vector3 horizontalPosition = currentPosition;
|
||||
horizontalPosition.y = 0f;
|
||||
Vector3 toPlayer = playerPosition - horizontalPosition;
|
||||
float sqrDistance = toPlayer.sqrMagnitude;
|
||||
|
||||
float attackRange = enemy.AttackRange > 0f ? enemy.AttackRange : DefaultAttackRange;
|
||||
float attackRangeSqr = attackRange * attackRange;
|
||||
bool isInAttackRange = sqrDistance <= attackRangeSqr;
|
||||
bool canChase = !isInAttackRange && enemy.Speed > 0f && sqrDistance > float.Epsilon;
|
||||
|
||||
EnemyTickWorkItem workItem = new EnemyTickWorkItem
|
||||
{
|
||||
EntityId = enemy.EntityId,
|
||||
CurrentPosition = currentPosition,
|
||||
DesiredPosition = currentPosition,
|
||||
ToPlayer = toPlayer,
|
||||
Forward = enemy.Forward,
|
||||
Rotation = enemy.Rotation,
|
||||
SqrDistanceToPlayer = sqrDistance,
|
||||
AttackRangeSqr = attackRangeSqr,
|
||||
Speed = enemy.Speed,
|
||||
SeparationIterations = enemy.SeparationIterations > 0 ? enemy.SeparationIterations : 1,
|
||||
AvoidEnemyOverlap = enemy.AvoidEnemyOverlap,
|
||||
CanChase = canChase,
|
||||
HasRotationUpdate = false,
|
||||
NextState = EnemyStateIdle
|
||||
};
|
||||
_enemyTickWorkItems.Add(workItem);
|
||||
|
||||
if (!enemy.AvoidEnemyOverlap) continue;
|
||||
|
||||
_enemySeparationAgents.Add(new EnemySeparationAgent
|
||||
{
|
||||
AgentId = enemy.EntityId,
|
||||
Position = horizontalPosition,
|
||||
Radius = enemy.EnemyBodyRadius > 0f ? enemy.EnemyBodyRadius : 0.45f
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveAndSeparateEnemies(float deltaTime)
|
||||
{
|
||||
EnemySeparationSolverProvider.SetSimulationAgents(_enemySeparationAgents);
|
||||
|
||||
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
|
||||
{
|
||||
EnemyTickWorkItem workItem = _enemyTickWorkItems[i];
|
||||
if (!workItem.CanChase)
|
||||
{
|
||||
_enemyTickWorkItems[i] = workItem;
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector3 forward = workItem.ToPlayer.normalized;
|
||||
Vector3 desiredPosition = workItem.CurrentPosition + forward * workItem.Speed * deltaTime;
|
||||
|
||||
if (workItem.AvoidEnemyOverlap)
|
||||
{
|
||||
desiredPosition = EnemySeparationSolverProvider.ResolveSimulation(
|
||||
workItem.EntityId,
|
||||
desiredPosition,
|
||||
forward,
|
||||
workItem.SeparationIterations);
|
||||
}
|
||||
|
||||
workItem.Forward = forward;
|
||||
workItem.DesiredPosition = desiredPosition;
|
||||
|
||||
if (forward.sqrMagnitude > float.Epsilon)
|
||||
{
|
||||
workItem.Rotation = Quaternion.LookRotation(forward, Vector3.up);
|
||||
workItem.HasRotationUpdate = true;
|
||||
}
|
||||
|
||||
_enemyTickWorkItems[i] = workItem;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateEnemyStates()
|
||||
{
|
||||
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
|
||||
{
|
||||
EnemyTickWorkItem workItem = _enemyTickWorkItems[i];
|
||||
|
||||
if (workItem.SqrDistanceToPlayer <= workItem.AttackRangeSqr)
|
||||
{
|
||||
workItem.NextState = EnemyStateInAttackRange;
|
||||
}
|
||||
else if (workItem.CanChase)
|
||||
{
|
||||
workItem.NextState = EnemyStateChasing;
|
||||
}
|
||||
else
|
||||
{
|
||||
workItem.NextState = EnemyStateIdle;
|
||||
}
|
||||
|
||||
_enemyTickWorkItems[i] = workItem;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteBackEnemyTickResults()
|
||||
{
|
||||
bool hasPositionChanged = false;
|
||||
|
||||
for (int i = 0; i < _enemyTickWorkItems.Count; i++)
|
||||
{
|
||||
EnemySimData enemy = _enemies[i];
|
||||
EnemyTickWorkItem workItem = _enemyTickWorkItems[i];
|
||||
|
||||
if (workItem.CanChase)
|
||||
{
|
||||
enemy.Forward = workItem.Forward;
|
||||
enemy.Position = workItem.DesiredPosition;
|
||||
if (workItem.HasRotationUpdate)
|
||||
{
|
||||
enemy.Rotation = workItem.Rotation;
|
||||
}
|
||||
|
||||
hasPositionChanged = true;
|
||||
}
|
||||
|
||||
enemy.State = workItem.NextState;
|
||||
_enemies[i] = enemy;
|
||||
}
|
||||
|
||||
if (hasPositionChanged)
|
||||
{
|
||||
MarkEnemyTargetSpatialIndexDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private static EnemySimData CreateEnemyInitialSimData(EnemyBase enemy, EnemyData enemyData)
|
||||
{
|
||||
Transform enemyTransform = enemy.CachedTransform;
|
||||
MovementComponent movementComponent = enemy.GetComponent<MovementComponent>();
|
||||
|
||||
float speed = 0f;
|
||||
if (enemyData != null)
|
||||
{
|
||||
speed = enemyData.SpeedBase;
|
||||
}
|
||||
else if (movementComponent != null)
|
||||
{
|
||||
speed = movementComponent.Speed;
|
||||
}
|
||||
|
||||
float attackRange = enemy != null && enemy.AttackRange > 0f
|
||||
? enemy.AttackRange
|
||||
: DefaultAttackRange;
|
||||
|
||||
return new EnemySimData
|
||||
{
|
||||
EntityId = enemy.Id,
|
||||
Position = enemyTransform.position,
|
||||
Forward = enemyTransform.forward,
|
||||
Rotation = enemyTransform.rotation,
|
||||
Speed = speed,
|
||||
AttackRange = attackRange,
|
||||
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
|
||||
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
|
||||
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
|
||||
TargetType = 0,
|
||||
State = EnemyStateIdle
|
||||
};
|
||||
}
|
||||
|
||||
private static PickupSimData CreatePickupInitialSimData(EntityBase pickupEntity)
|
||||
{
|
||||
return new PickupSimData
|
||||
{
|
||||
EntityId = pickupEntity.Id,
|
||||
Position = pickupEntity.CachedTransform.position,
|
||||
PickupRadius = 0.35f,
|
||||
State = 0
|
||||
};
|
||||
}
|
||||
|
||||
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity, object userData)
|
||||
{
|
||||
Vector3 forward = projectileEntity.CachedTransform.forward;
|
||||
int ownerEntityId = 0;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
float speed = 0f;
|
||||
float lifeTime = 0f;
|
||||
|
||||
if (userData is EnemyProjectileData enemyProjectileData)
|
||||
{
|
||||
ownerEntityId = enemyProjectileData.OwnerEntityId;
|
||||
|
||||
Vector3 direction = enemyProjectileData.Direction;
|
||||
direction.y = 0f;
|
||||
if (direction.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
direction.Normalize();
|
||||
forward = direction;
|
||||
}
|
||||
else if (forward.sqrMagnitude > Mathf.Epsilon)
|
||||
{
|
||||
forward = forward.normalized;
|
||||
}
|
||||
else
|
||||
{
|
||||
forward = Vector3.forward;
|
||||
}
|
||||
|
||||
speed = Mathf.Max(0f, enemyProjectileData.Speed);
|
||||
velocity = forward * speed;
|
||||
lifeTime = Mathf.Max(0f, enemyProjectileData.LifeTime);
|
||||
}
|
||||
|
||||
return new ProjectileSimData
|
||||
{
|
||||
EntityId = projectileEntity.Id,
|
||||
OwnerEntityId = ownerEntityId,
|
||||
Position = projectileEntity.CachedTransform.position,
|
||||
Forward = forward,
|
||||
Velocity = velocity,
|
||||
Speed = speed,
|
||||
LifeTime = lifeTime,
|
||||
Age = 0f,
|
||||
Active = true,
|
||||
RemainingLifetime = lifeTime,
|
||||
State = ProjectileStateActive
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ MonoBehaviour:
|
||||
showUnityEditorReport: 0
|
||||
logBehaviour: 0
|
||||
drawGizmos: 1
|
||||
defaultRecyclable: 0
|
||||
defaultRecyclable: 1
|
||||
defaultAutoPlay: 3
|
||||
defaultUpdateType: 0
|
||||
defaultTimeScaleIndependent: 0
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user