Checkpoint 9
This commit is contained in:
parent
2e4108aebc
commit
ccbd27e87e
|
|
@ -75,9 +75,8 @@ crashlytics-build.properties
|
||||||
# Packed Addressables
|
# Packed Addressables
|
||||||
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
|
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
|
||||||
|
|
||||||
# Temporary auto-generated Android Assets
|
/Assets/StreamingAssets
|
||||||
/[Aa]ssets/[Ss]treamingAssets/aa.meta
|
/Assets/StreamingAssets.meta
|
||||||
/[Aa]ssets/[Ss]treamingAssets/aa/*
|
|
||||||
|
|
||||||
/UI参考
|
/UI参考
|
||||||
/AGENTS.md
|
/AGENTS.md
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<UnityGameFramework>
|
<UnityGameFramework>
|
||||||
<ResourceBuilder>
|
<ResourceBuilder>
|
||||||
<Settings>
|
<Settings>
|
||||||
<InternalResourceVersion>3</InternalResourceVersion>
|
<InternalResourceVersion>5</InternalResourceVersion>
|
||||||
<Platforms>33</Platforms>
|
<Platforms>33</Platforms>
|
||||||
<AssetBundleCompression>1</AssetBundleCompression>
|
<AssetBundleCompression>1</AssetBundleCompression>
|
||||||
<CompressionHelperTypeName>UnityGameFramework.Runtime.DefaultCompressionHelper</CompressionHelperTypeName>
|
<CompressionHelperTypeName>UnityGameFramework.Runtime.DefaultCompressionHelper</CompressionHelperTypeName>
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,28 @@
|
||||||
<UnityGameFramework>
|
<UnityGameFramework>
|
||||||
<ResourceCollection>
|
<ResourceCollection>
|
||||||
<Resources>
|
<Resources>
|
||||||
<Resource Name="Configs" FileSystem="GameData" LoadType="0" Packed="True" ResourceGroups="Base" />
|
<Resource Name="Configs" FileSystem="GameData" LoadType="0" Packed="True"
|
||||||
<Resource Name="DataTables" FileSystem="GameData" LoadType="0" Packed="True" ResourceGroups="Base" />
|
ResourceGroups="Base" />
|
||||||
|
<Resource Name="DataTables" FileSystem="GameData" LoadType="0" Packed="True"
|
||||||
|
ResourceGroups="Base" />
|
||||||
<Resource Name="Entities" FileSystem="Resources" LoadType="0" Packed="True" />
|
<Resource Name="Entities" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||||
<Resource Name="Fonts" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
<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="en-us" FileSystem="UI" LoadType="0"
|
||||||
<Resource Name="Localization/Dictionaries" Variant="ko-kr" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
Packed="True" ResourceGroups="Base" />
|
||||||
<Resource Name="Localization/Dictionaries" Variant="zh-cn" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
<Resource Name="Localization/Dictionaries" Variant="ko-kr" FileSystem="UI" LoadType="0"
|
||||||
<Resource Name="Localization/Dictionaries" Variant="zh-tw" FileSystem="UI" LoadType="0" Packed="True" ResourceGroups="Base" />
|
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="Materials" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||||
<Resource Name="Meshes" 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/About" FileSystem="Resources" LoadType="0" Packed="True"
|
||||||
<Resource Name="Music/Background" FileSystem="Resources" LoadType="0" Packed="True" ResourceGroups="Music" />
|
ResourceGroups="Music" />
|
||||||
<Resource Name="Music/Menu" 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="Scenes" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||||
<Resource Name="SceneSettings" LoadType="0" Packed="False" />
|
<Resource Name="SceneSettings" LoadType="0" Packed="False" />
|
||||||
<Resource Name="Sounds" FileSystem="Resources" LoadType="0" Packed="True" />
|
<Resource Name="Sounds" FileSystem="Resources" LoadType="0" Packed="True" />
|
||||||
|
|
@ -41,9 +50,11 @@
|
||||||
<Asset Guid="0ed73dc47f4cb38489020d05e9f02c99" ResourceName="Materials" />
|
<Asset Guid="0ed73dc47f4cb38489020d05e9f02c99" ResourceName="Materials" />
|
||||||
<Asset Guid="0f995b3145e0e7247a42da6cef1dbf23" ResourceName="Materials" />
|
<Asset Guid="0f995b3145e0e7247a42da6cef1dbf23" ResourceName="Materials" />
|
||||||
<Asset Guid="1046dcb12e547564d8b54bd15419a787" ResourceName="Entities" />
|
<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="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="156d241f796508c4da4fc354a7fbf5a8" ResourceName="UI/UISprites/Common" />
|
||||||
<Asset Guid="185f97f18bd603a478461ce9c08bd039" ResourceName="Materials" />
|
<Asset Guid="185f97f18bd603a478461ce9c08bd039" ResourceName="Materials" />
|
||||||
<Asset Guid="18dc0cd2c080841dea60987a38ce93fa" ResourceName="URPAssets" />
|
<Asset Guid="18dc0cd2c080841dea60987a38ce93fa" ResourceName="URPAssets" />
|
||||||
|
|
@ -92,7 +103,8 @@
|
||||||
<Asset Guid="578af1667322bab45b652b79a40bb4fb" ResourceName="Materials" />
|
<Asset Guid="578af1667322bab45b652b79a40bb4fb" ResourceName="Materials" />
|
||||||
<Asset Guid="583ff7026dac91849b7c7b2468ba456b" ResourceName="Materials" />
|
<Asset Guid="583ff7026dac91849b7c7b2468ba456b" ResourceName="Materials" />
|
||||||
<Asset Guid="5b5a6a737c460eb4abc105d6583d405e" ResourceName="Fonts" />
|
<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="5ebb46af6f16ae94e87f64a7dc0a49cb" ResourceName="Entities" />
|
||||||
<Asset Guid="62af9e5c8f39cfa49af9e10ccf42f1da" ResourceName="UI/UISprites/Common" />
|
<Asset Guid="62af9e5c8f39cfa49af9e10ccf42f1da" ResourceName="UI/UISprites/Common" />
|
||||||
<Asset Guid="638ff8ae4a0d15047839cd265d3bc296" ResourceName="Music/Background" />
|
<Asset Guid="638ff8ae4a0d15047839cd265d3bc296" ResourceName="Music/Background" />
|
||||||
|
|
@ -132,7 +144,9 @@
|
||||||
<Asset Guid="97b1f8b25cca2bc458cb9d6127c8d186" ResourceName="Materials" />
|
<Asset Guid="97b1f8b25cca2bc458cb9d6127c8d186" ResourceName="Materials" />
|
||||||
<Asset Guid="99d811b0183246646a2ce8df996f4bca" ResourceName="Fonts" />
|
<Asset Guid="99d811b0183246646a2ce8df996f4bca" ResourceName="Fonts" />
|
||||||
<Asset Guid="9afa958d6d8235941b9badb42aae4370" ResourceName="Meshes" />
|
<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="9ddab293e2a8af3499dac05f5fd6169c" ResourceName="Meshes" />
|
||||||
<Asset Guid="9f5bba6d2f5c95049a59fcb56df2d38f" ResourceName="UI/UIItems" />
|
<Asset Guid="9f5bba6d2f5c95049a59fcb56df2d38f" ResourceName="UI/UIItems" />
|
||||||
<Asset Guid="9f847ec5e66e03e4ead1d3c5f7b510e8" ResourceName="UI/UISprites/Common" />
|
<Asset Guid="9f847ec5e66e03e4ead1d3c5f7b510e8" ResourceName="UI/UISprites/Common" />
|
||||||
|
|
@ -157,6 +171,7 @@
|
||||||
<Asset Guid="ba157ba55f72c424a9e88f3c029997c4" ResourceName="Textures" />
|
<Asset Guid="ba157ba55f72c424a9e88f3c029997c4" ResourceName="Textures" />
|
||||||
<Asset Guid="baedbbad82997f445a8cb4da210404e0" ResourceName="Meshes" />
|
<Asset Guid="baedbbad82997f445a8cb4da210404e0" ResourceName="Meshes" />
|
||||||
<Asset Guid="bbfd75fe6fe00e1448fe988173ede7f9" ResourceName="UI/UIForms" />
|
<Asset Guid="bbfd75fe6fe00e1448fe988173ede7f9" ResourceName="UI/UIForms" />
|
||||||
|
<Asset Guid="bc065bcf1474d7d4387fafd202678c37" ResourceName="Fonts" />
|
||||||
<Asset Guid="bf75b984df8a84987bcf3a8bf6e2862d" ResourceName="Sounds" />
|
<Asset Guid="bf75b984df8a84987bcf3a8bf6e2862d" ResourceName="Sounds" />
|
||||||
<Asset Guid="c40be3174f62c4acf8c1216858c64956" ResourceName="URPAssets" />
|
<Asset Guid="c40be3174f62c4acf8c1216858c64956" ResourceName="URPAssets" />
|
||||||
<Asset Guid="c49cffd4fc1dfb549b2b30448a0becda" ResourceName="UI/UISprites/Icons" />
|
<Asset Guid="c49cffd4fc1dfb549b2b30448a0becda" ResourceName="UI/UISprites/Icons" />
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ GameObject:
|
||||||
- component: {fileID: 7683855655592166216}
|
- component: {fileID: 7683855655592166216}
|
||||||
- component: {fileID: 6418687210998749921}
|
- component: {fileID: 6418687210998749921}
|
||||||
- component: {fileID: 4710806460657047075}
|
- component: {fileID: 4710806460657047075}
|
||||||
- component: {fileID: 8116679074104541426}
|
|
||||||
- component: {fileID: 1932268889601128120}
|
- component: {fileID: 1932268889601128120}
|
||||||
- component: {fileID: 557030043145096197}
|
- component: {fileID: 557030043145096197}
|
||||||
- component: {fileID: 6353753365317756414}
|
- component: {fileID: 6353753365317756414}
|
||||||
|
|
@ -87,33 +86,6 @@ MeshRenderer:
|
||||||
m_SortingLayer: 0
|
m_SortingLayer: 0
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_AdditionalVertexStreams: {fileID: 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
|
--- !u!136 &1932268889601128120
|
||||||
CapsuleCollider:
|
CapsuleCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -131,7 +103,7 @@ CapsuleCollider:
|
||||||
m_LayerOverridePriority: 0
|
m_LayerOverridePriority: 0
|
||||||
m_IsTrigger: 1
|
m_IsTrigger: 1
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Radius: 0.5
|
m_Radius: 0.5
|
||||||
m_Height: 2
|
m_Height: 2
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ CapsuleCollider:
|
||||||
m_LayerOverridePriority: 0
|
m_LayerOverridePriority: 0
|
||||||
m_IsTrigger: 0
|
m_IsTrigger: 0
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Radius: 0.5
|
m_Radius: 0.5
|
||||||
m_Height: 2
|
m_Height: 2
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GameFramework.ObjectPool;
|
using GameFramework.ObjectPool;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
|
|
@ -9,7 +10,7 @@ namespace CustomComponent
|
||||||
{
|
{
|
||||||
public class DamageTextComponent : GameFrameworkComponent
|
public class DamageTextComponent : GameFrameworkComponent
|
||||||
{
|
{
|
||||||
[SerializeField] private int _instancePoolCapacity = 32;
|
[SerializeField] private int _instancePoolCapacity = 256;
|
||||||
|
|
||||||
[SerializeField] private string _poolName = "DamageTextItem";
|
[SerializeField] private string _poolName = "DamageTextItem";
|
||||||
|
|
||||||
|
|
@ -43,6 +44,12 @@ namespace CustomComponent
|
||||||
|
|
||||||
private DamageTextItem CreateDamageTextItem()
|
private DamageTextItem CreateDamageTextItem()
|
||||||
{
|
{
|
||||||
|
if (_activeDamageTextItems.Count == _instancePoolCapacity)
|
||||||
|
{
|
||||||
|
_instancePoolCapacity = Mathf.Min(_instancePoolCapacity * 2, 1024);
|
||||||
|
_damageTextItemPool.Capacity = _instancePoolCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
DamageTextItemObject itemObject = _damageTextItemPool.Spawn();
|
DamageTextItemObject itemObject = _damageTextItemPool.Spawn();
|
||||||
if (itemObject != null)
|
if (itemObject != null)
|
||||||
{
|
{
|
||||||
|
|
@ -63,5 +70,12 @@ namespace CustomComponent
|
||||||
_activeDamageTextItems.Remove(item);
|
_activeDamageTextItems.Remove(item);
|
||||||
_damageTextItemPool.Unspawn(item);
|
_damageTextItemPool.Unspawn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
_activeDamageTextItems.Clear();
|
||||||
|
_damageTextItemPool.Release();
|
||||||
|
_damageTextItemPool = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Components;
|
||||||
|
using CustomEvent;
|
||||||
using DataTable;
|
using DataTable;
|
||||||
using Definition.DataStruct;
|
using Definition.DataStruct;
|
||||||
using Entity;
|
using Entity;
|
||||||
|
|
@ -19,6 +21,7 @@ namespace CustomComponent
|
||||||
private const float MinSpawnRate = 0.1f;
|
private const float MinSpawnRate = 0.1f;
|
||||||
private const float CornerTapWindow = 0.6f;
|
private const float CornerTapWindow = 0.6f;
|
||||||
private const int RequiredCornerTapCount = 3;
|
private const int RequiredCornerTapCount = 3;
|
||||||
|
private const int DebugHealAmount = 200;
|
||||||
|
|
||||||
private Rect _windowRect = new Rect(20f, 60f, 460f, 800f);
|
private Rect _windowRect = new Rect(20f, 60f, 460f, 800f);
|
||||||
private bool _isPanelVisible;
|
private bool _isPanelVisible;
|
||||||
|
|
@ -38,6 +41,7 @@ namespace CustomComponent
|
||||||
|
|
||||||
private int _cornerTapCount;
|
private int _cornerTapCount;
|
||||||
private float _lastCornerTapTime = -10f;
|
private float _lastCornerTapTime = -10f;
|
||||||
|
private bool _lockPlayerHealthToMax;
|
||||||
|
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
|
|
@ -54,6 +58,10 @@ namespace CustomComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleCornerTapGesture();
|
HandleCornerTapGesture();
|
||||||
|
if (_lockPlayerHealthToMax)
|
||||||
|
{
|
||||||
|
KeepPlayerHealthAtMax();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGUI()
|
private void OnGUI()
|
||||||
|
|
@ -147,6 +155,7 @@ namespace CustomComponent
|
||||||
ProcedureGame procedure = GameEntry.Procedure.CurrentProcedure as ProcedureGame;
|
ProcedureGame procedure = GameEntry.Procedure.CurrentProcedure as ProcedureGame;
|
||||||
EnemyManagerComponent enemyManager = GameEntry.EnemyManager;
|
EnemyManagerComponent enemyManager = GameEntry.EnemyManager;
|
||||||
Player player = FindPlayer();
|
Player player = FindPlayer();
|
||||||
|
HealthComponent playerHealth = player != null ? player.GetComponent<HealthComponent>() : null;
|
||||||
|
|
||||||
if (enemyManager == null)
|
if (enemyManager == null)
|
||||||
{
|
{
|
||||||
|
|
@ -168,8 +177,6 @@ namespace CustomComponent
|
||||||
if (simulationWorld != null)
|
if (simulationWorld != null)
|
||||||
{
|
{
|
||||||
GUILayout.Space(4f);
|
GUILayout.Space(4f);
|
||||||
GUILayout.Label(
|
|
||||||
$"Sim Switch: Move={(simulationWorld.UseSimulationMovement ? "On" : "Off")} Job={(simulationWorld.UseJobSimulation ? "On" : "Off")} Burst={(simulationWorld.UseBurstJobs ? "On" : "Off")}");
|
|
||||||
GUILayout.Label(
|
GUILayout.Label(
|
||||||
$"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})");
|
$"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})");
|
||||||
GUILayout.Label(
|
GUILayout.Label(
|
||||||
|
|
@ -265,6 +272,28 @@ namespace CustomComponent
|
||||||
|
|
||||||
GUI.enabled = true;
|
GUI.enabled = true;
|
||||||
GUILayout.EndHorizontal();
|
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)
|
private void EnsurePropList(bool force = false)
|
||||||
|
|
@ -324,6 +353,52 @@ namespace CustomComponent
|
||||||
return UnityEngine.Object.FindObjectOfType<Player>();
|
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)
|
private static void AddSelectedBuffToPlayer(DRProp prop, int count)
|
||||||
{
|
{
|
||||||
Player player = FindPlayer();
|
Player player = FindPlayer();
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,25 @@ namespace CustomDebugger
|
||||||
{
|
{
|
||||||
public static class CustomProfilerMarker
|
public static class CustomProfilerMarker
|
||||||
{
|
{
|
||||||
public static readonly ProfilerMarker TickEnemies = new ProfilerMarker("TickEnemies");
|
public static readonly ProfilerMarker TickEnemies = new("TickEnemies");
|
||||||
public static readonly ProfilerMarker TickEnemies_BuildInput = new ProfilerMarker("TickEnemies.BuildInput");
|
public static readonly ProfilerMarker TickEnemies_BuildInput = new("TickEnemies.BuildInput");
|
||||||
public static readonly ProfilerMarker TickEnemies_MoveSeparation = new ProfilerMarker("TickEnemies.MoveSeparation");
|
public static readonly ProfilerMarker TickEnemies_StateUpdate = new("TickEnemies.StateUpdate");
|
||||||
public static readonly ProfilerMarker TickEnemies_StateUpdate = new ProfilerMarker("TickEnemies.StateUpdate");
|
public static readonly ProfilerMarker TickEnemies_Schedule = new("TickEnemies.Schedule");
|
||||||
public static readonly ProfilerMarker TickEnemies_Schedule = new ProfilerMarker("TickEnemies.Schedule");
|
public static readonly ProfilerMarker TickEnemies_Complete = new("TickEnemies.Complete");
|
||||||
public static readonly ProfilerMarker TickEnemies_Complete = new ProfilerMarker("TickEnemies.Complete");
|
public static readonly ProfilerMarker TickEnemies_MainThreadCommit = new("TickEnemies.MainThreadCommit");
|
||||||
public static readonly ProfilerMarker TickEnemies_MainThreadCommit = new ProfilerMarker("TickEnemies.MainThreadCommit");
|
public static readonly ProfilerMarker TickEnemies_WriteBack = new("TickEnemies.WriteBack");
|
||||||
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
|
||||||
|
public static readonly ProfilerMarker Collision = new("Collision");
|
||||||
public static readonly ProfilerMarker Collision_BuildQueries = new("Collision.BuildQueries");
|
public static readonly ProfilerMarker Collision_BuildQueries = new("Collision.BuildQueries");
|
||||||
public static readonly ProfilerMarker Collision_BuildBuckets = new("Collision.BuildBuckets");
|
public static readonly ProfilerMarker Collision_BuildBuckets = new("Collision.BuildBuckets");
|
||||||
public static readonly ProfilerMarker Collision_QueryCandidates = new("Collision.QueryCandidates");
|
public static readonly ProfilerMarker Collision_QueryCandidates = new("Collision.QueryCandidates");
|
||||||
public static readonly ProfilerMarker Collision_ResolveProjectile = new("Collision.ResolveProjectile");
|
public static readonly ProfilerMarker Collision_ResolveProjectile = new("Collision.ResolveProjectile");
|
||||||
public static readonly ProfilerMarker Collision_ResolveArea = new("Collision.ResolveArea");
|
public static readonly ProfilerMarker Collision_ResolveArea = new("Collision.ResolveArea");
|
||||||
|
|
||||||
public static readonly ProfilerMarker TargetSelection_BuildBuckets = new("TargetSelection.BuildBuckets");
|
public static readonly ProfilerMarker TargetSelection_BuildBuckets = new("TargetSelection.BuildBuckets");
|
||||||
public static readonly ProfilerMarker TargetSelection_QueryNeighbors = new("TargetSelection.QueryNeighbors");
|
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 ShopUI_Update = new("UGF.ShopUI.Update");
|
||||||
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
public static readonly ProfilerMarker Inventory_Refresh = new("UGF.Inventory.Refresh");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ namespace Entity
|
||||||
private static bool IsDrivenBySimulationWorld()
|
private static bool IsDrivenBySimulationWorld()
|
||||||
{
|
{
|
||||||
var simulationWorld = GameEntry.SimulationWorld;
|
var simulationWorld = GameEntry.SimulationWorld;
|
||||||
return simulationWorld != null && simulationWorld.UseSimulationMovement && simulationWorld.UseJobSimulation;
|
return simulationWorld != null && simulationWorld.UseSimulationMovement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetColliderEnabled(bool enabled)
|
private void SetColliderEnabled(bool enabled)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
|
using GameFramework.ObjectPool;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Entity.Weapon
|
namespace Entity.Weapon
|
||||||
{
|
{
|
||||||
public sealed class HandgunHitMarkerAttackEffect : IWeaponAttackEffect
|
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 _size;
|
||||||
private readonly float _yOffset;
|
private readonly float _yOffset;
|
||||||
private readonly float _duration;
|
private readonly float _duration;
|
||||||
private readonly Color _color;
|
private readonly Color _color;
|
||||||
|
private Material _sharedMaterial;
|
||||||
|
|
||||||
public HandgunHitMarkerAttackEffect(float size, float yOffset, float duration, Color color)
|
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)
|
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
|
||||||
{
|
{
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
|
if (!TrySpawnMarker(out HandgunHitMarkerPooledInstance markerInstance))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
Transform targetTransform = target.CachedTransform;
|
||||||
marker.name = "HandgunHitMarker";
|
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)
|
if (collider != null)
|
||||||
{
|
{
|
||||||
Object.Destroy(collider);
|
Object.Destroy(collider);
|
||||||
}
|
}
|
||||||
|
|
||||||
marker.transform.SetParent(target.CachedTransform, false);
|
markerInstance = markerGameObject.AddComponent<HandgunHitMarkerPooledInstance>();
|
||||||
marker.transform.localPosition = new Vector3(0f, _yOffset, 0f);
|
markerGameObject.SetActive(false);
|
||||||
marker.transform.localScale = Vector3.one * Mathf.Max(0.01f, _size);
|
pool.Register(HandgunHitMarkerPoolObject.Create(markerInstance), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Renderer renderer = marker.GetComponent<Renderer>();
|
private static IObjectPool<HandgunHitMarkerPoolObject> EnsurePool()
|
||||||
if (renderer != null)
|
{
|
||||||
|
var poolComponent = GameEntry.ObjectPool;
|
||||||
|
if (poolComponent == null)
|
||||||
{
|
{
|
||||||
Shader shader = Shader.Find("Sprites/Default");
|
return null;
|
||||||
if (shader == null)
|
|
||||||
{
|
|
||||||
shader = Shader.Find("Unlit/Color");
|
|
||||||
}
|
|
||||||
|
|
||||||
Material material = new Material(shader);
|
|
||||||
material.color = _color;
|
|
||||||
renderer.material = material;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 966d34a18f5d00c49bddea3c7c5ec13d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7ff8a67466d8f0643845c41fd5952f50
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -44,7 +44,7 @@ namespace Entity.Weapon
|
||||||
}
|
}
|
||||||
|
|
||||||
var simulationWorld = GameEntry.SimulationWorld;
|
var simulationWorld = GameEntry.SimulationWorld;
|
||||||
if (simulationWorld == null || !simulationWorld.UseSimulationMovement || !simulationWorld.UseJobSimulation)
|
if (simulationWorld == null || !simulationWorld.UseSimulationMovement)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ namespace Entity.Weapon
|
||||||
}
|
}
|
||||||
|
|
||||||
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
|
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,
|
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;
|
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);
|
halfAngleDeg, maxTargets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ namespace Procedure
|
||||||
base.OnEnter(procedureOwner);
|
base.OnEnter(procedureOwner);
|
||||||
|
|
||||||
_procedureOwner = procedureOwner;
|
_procedureOwner = procedureOwner;
|
||||||
GameEntry.SimulationWorld?.Clear();
|
GameEntry.SimulationWorld?.ClearSimulationState();
|
||||||
|
|
||||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
||||||
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
||||||
|
|
@ -136,7 +136,7 @@ namespace Procedure
|
||||||
Player = null;
|
Player = null;
|
||||||
|
|
||||||
_procedureOwner = null;
|
_procedureOwner = null;
|
||||||
GameEntry.SimulationWorld?.Clear();
|
GameEntry.SimulationWorld?.ClearSimulationState();
|
||||||
|
|
||||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OpenUIFormSuccess);
|
||||||
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, ShowEntitySuccess);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f28ff313e954720a04246191b7f9ddd
|
||||||
|
timeCreated: 1771901064
|
||||||
|
|
@ -8,115 +8,7 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
public sealed partial class SimulationWorld
|
public sealed partial class SimulationWorld
|
||||||
{
|
{
|
||||||
private struct EnemyJobInputData
|
// Native buffers used by burst jobs and main-thread collision post-processing.
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int CollisionSourceTypeProjectile = 1;
|
private const int CollisionSourceTypeProjectile = 1;
|
||||||
private const int CollisionSourceTypeArea = 2;
|
private const int CollisionSourceTypeArea = 2;
|
||||||
private const int CollisionShapeCircle = 0;
|
private const int CollisionShapeCircle = 0;
|
||||||
|
|
@ -352,7 +244,7 @@ namespace Simulation
|
||||||
_lastCollisionHasEnemyTargets = false;
|
_lastCollisionHasEnemyTargets = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncSimulationToJobInput()
|
private void SyncSimulationStateToJobInputs()
|
||||||
{
|
{
|
||||||
InitializeJobDataChannels();
|
InitializeJobDataChannels();
|
||||||
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
|
EnsureCapacity(ref _enemyJobInputs, _enemies.Count);
|
||||||
|
|
@ -361,34 +253,14 @@ namespace Simulation
|
||||||
_enemyJobInputs.Clear();
|
_enemyJobInputs.Clear();
|
||||||
_projectileJobInputs.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]));
|
_projectileJobInputs.Add(ConvertToProjectileJobInput(data));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,19 +288,7 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncProjectilesToJobOutput()
|
private void CopyProjectileInputsToOutputs()
|
||||||
{
|
|
||||||
InitializeJobDataChannels();
|
|
||||||
EnsureCapacity(ref _projectileJobOutputs, _projectiles.Count);
|
|
||||||
_projectileJobOutputs.Clear();
|
|
||||||
|
|
||||||
for (int i = 0; i < _projectiles.Count; i++)
|
|
||||||
{
|
|
||||||
_projectileJobOutputs.Add(ConvertToProjectileJobOutput(_projectiles[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyProjectileInputToOutput()
|
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _projectileJobInputs.Length; i++)
|
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();
|
InitializeJobDataChannels();
|
||||||
EnsureCapacity(ref _collisionQueryInputs, queryCount);
|
EnsureCapacity(ref _collisionQueryInputs, queryCount);
|
||||||
|
|
@ -621,7 +482,7 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyJobOutputToSimulation()
|
private void ApplyJobOutputsToSimulationState()
|
||||||
{
|
{
|
||||||
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
int enemyCount = Mathf.Min(_enemies.Count, _enemyJobOutputs.Length);
|
||||||
bool hasEnemyPositionChanged = false;
|
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)
|
private static EnemySimData ConvertToEnemySimData(in EnemyJobOutputData enemy)
|
||||||
{
|
{
|
||||||
return new EnemySimData
|
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)
|
private static ProjectileSimData ConvertToProjectileSimData(in ProjectileJobOutputData projectile)
|
||||||
{
|
{
|
||||||
return new ProjectileSimData
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.Burst;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
using Unity.Jobs;
|
using Unity.Jobs;
|
||||||
using Unity.Mathematics;
|
using Unity.Mathematics;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using CustomDebugger;
|
|
||||||
using CustomEvent;
|
|
||||||
using Definition.DataStruct;
|
|
||||||
using Definition.Enum;
|
|
||||||
using Entity;
|
|
||||||
using Entity.Weapon;
|
|
||||||
using CustomUtility;
|
|
||||||
using UnityGameFramework.Runtime;
|
|
||||||
|
|
||||||
namespace Simulation
|
namespace Simulation
|
||||||
{
|
{
|
||||||
public sealed partial class SimulationWorld
|
public sealed partial class SimulationWorld
|
||||||
{
|
{
|
||||||
private const int PlayerEntityId = -1;
|
private const int PlayerEntityId = -1;
|
||||||
|
private JobHandle _collisionCandidateQueryHandle;
|
||||||
|
private bool _collisionCandidateQueryScheduled;
|
||||||
|
|
||||||
[Header("投射物模拟参数")] [Tooltip("投射物距离玩家超过该水平半径时回收。小于等于 0 表示不启用该回收条件。")] [SerializeField]
|
[Header("Projectile Collision Query")]
|
||||||
private float _projectileMaxDistanceFromPlayer = 120f;
|
[Tooltip("Projectile broad-phase collision query radius.")]
|
||||||
|
[SerializeField]
|
||||||
[Tooltip("投射物与玩家的垂直高度差超过该值时回收。小于等于 0 表示不启用该回收条件。")] [SerializeField]
|
|
||||||
private float _projectileMaxVerticalOffsetFromPlayer = 30f;
|
|
||||||
|
|
||||||
[Tooltip("投射物 Broad Phase 命中查询半径。")] [SerializeField]
|
|
||||||
private float _projectileCollisionQueryRadius = 0.35f;
|
private float _projectileCollisionQueryRadius = 0.35f;
|
||||||
|
|
||||||
[Tooltip("每个投射物最多保留的候选目标数量。")] [SerializeField]
|
[Tooltip("Maximum retained candidates per projectile query.")]
|
||||||
|
[SerializeField]
|
||||||
private int _projectileMaxCandidatesPerQuery = 1;
|
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;
|
private float _projectileCollisionCellSize = 0f;
|
||||||
|
|
||||||
[Header("投射物命中事件派发")] [Tooltip("是否派发投射物命中表现事件。")] [SerializeField]
|
[Header("Projectile Hit Event Dispatch")]
|
||||||
|
[Tooltip("Dispatch projectile hit presentation event.")]
|
||||||
|
[SerializeField]
|
||||||
private bool _dispatchProjectileHitPresentationEvent = true;
|
private bool _dispatchProjectileHitPresentationEvent = true;
|
||||||
|
|
||||||
[Tooltip("命中时是否请求命中标记表现。")] [SerializeField]
|
[Tooltip("Request hit marker when projectile hits.")]
|
||||||
|
[SerializeField]
|
||||||
private bool _dispatchProjectileHitMarkerEvent = true;
|
private bool _dispatchProjectileHitMarkerEvent = true;
|
||||||
|
|
||||||
[Tooltip("命中时是否请求特效表现。")] [SerializeField]
|
[Tooltip("Request hit effect when projectile hits.")]
|
||||||
|
[SerializeField]
|
||||||
private bool _dispatchProjectileHitEffectEvent = true;
|
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;
|
private int _projectileHitPresentationEffectTypeId = 0;
|
||||||
|
|
||||||
[BurstCompile]
|
#region Area Query Request
|
||||||
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)
|
public bool TryRequestAreaCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||||
{
|
|
||||||
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,
|
|
||||||
float radius, int maxTargets = 16)
|
float radius, int maxTargets = 16)
|
||||||
{
|
{
|
||||||
return TryEnqueueAreaCollisionQueryInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
|
||||||
maxTargets, CollisionShapeCircle, Vector3.forward, 180f);
|
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)
|
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);
|
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)
|
float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg)
|
||||||
{
|
{
|
||||||
if (!_useSimulationMovement || !_useJobSimulation)
|
if (!_useSimulationMovement)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +80,7 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
|
|
||||||
int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId;
|
int resolvedOwnerEntityId = sourceOwnerEntityId != 0 ? sourceOwnerEntityId : sourceEntityId;
|
||||||
bool sourceWasActiveAtQueryTime = IsCollisionSourceActiveAtQueryTime(sourceEntityId);
|
bool sourceWasActiveAtQueryTime = WasCollisionSourceActiveAtQueryTime(sourceEntityId);
|
||||||
Vector3 normalizedDirection = direction;
|
Vector3 normalizedDirection = direction;
|
||||||
normalizedDirection.y = 0f;
|
normalizedDirection.y = 0f;
|
||||||
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
|
@ -134,12 +108,12 @@ namespace Simulation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetPendingAreaCollisionQueryCount()
|
private int GetPendingAreaCollisionRequestCount()
|
||||||
{
|
{
|
||||||
return _areaCollisionRequests.Count;
|
return _areaCollisionRequests.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int EstimatePendingAreaCollisionCandidateCount()
|
private int EstimatePendingAreaCollisionCandidateCountFromRequests()
|
||||||
{
|
{
|
||||||
int expectedCount = 0;
|
int expectedCount = 0;
|
||||||
for (int i = 0; i < _areaCollisionRequests.Count; i++)
|
for (int i = 0; i < _areaCollisionRequests.Count; i++)
|
||||||
|
|
@ -150,61 +124,19 @@ namespace Simulation
|
||||||
return expectedCount;
|
return expectedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JobHandle ExecuteProjectileMovementJob(in SimulationTickContext context)
|
#endregion
|
||||||
|
|
||||||
|
#region Collision Pipeline
|
||||||
|
|
||||||
|
private void PrepareCollisionCandidatesForFrame()
|
||||||
{
|
{
|
||||||
int projectileCount = _projectileJobInputs.Length;
|
_collisionCandidateQueryScheduled = false;
|
||||||
if (projectileCount == 0)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.DeltaTime <= 0f)
|
|
||||||
{
|
|
||||||
CopyProjectileInputToOutput();
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
float maxDistance = Mathf.Max(0f, _projectileMaxDistanceFromPlayer);
|
|
||||||
float maxSqrDistanceFromPlayer = maxDistance > 0f ? maxDistance * maxDistance : -1f;
|
|
||||||
float maxVerticalOffsetFromPlayer = Mathf.Max(0f, _projectileMaxVerticalOffsetFromPlayer);
|
|
||||||
float3 playerPosition =
|
|
||||||
new float3(context.PlayerPosition.x, context.PlayerPosition.y, context.PlayerPosition.z);
|
|
||||||
NativeArray<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 ||
|
if (!_collisionQueryInputs.IsCreated || !_collisionCandidates.IsCreated ||
|
||||||
!_enemyCollisionBuckets.IsCreated)
|
!_enemyCollisionBuckets.IsCreated)
|
||||||
{
|
{
|
||||||
ResetCollisionRuntimeStats();
|
ResetCollisionRuntimeStats();
|
||||||
ClearAreaCollisionFrameBuffers();
|
ClearAreaCollisionTransientBuffers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,29 +210,37 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
if (hasEnemyTargets)
|
if (hasEnemyTargets)
|
||||||
{
|
{
|
||||||
BuildEnemyCollisionBucketsForProjectiles(cellSize);
|
BuildEnemyCollisionBuckets(cellSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int projectileCandidateCount;
|
|
||||||
int areaCandidateCount;
|
|
||||||
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
|
using (CustomProfilerMarker.Collision_QueryCandidates.Auto())
|
||||||
{
|
{
|
||||||
QueryProjectileCollisionCandidates(cellSize, hasEnemyTargets, out projectileCandidateCount,
|
_collisionCandidateQueryScheduled = ScheduleCollisionCandidateQueryJob(cellSize, hasEnemyTargets,
|
||||||
out areaCandidateCount);
|
out _collisionCandidateQueryHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteCollisionCandidatesForFrame()
|
||||||
|
{
|
||||||
|
if (_collisionCandidateQueryScheduled)
|
||||||
|
{
|
||||||
|
_collisionCandidateQueryHandle.Complete();
|
||||||
|
_collisionCandidateQueryScheduled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CountCollisionCandidatesBySourceType(out int projectileCandidateCount, out int areaCandidateCount);
|
||||||
_lastProjectileCollisionCandidateCount = projectileCandidateCount;
|
_lastProjectileCollisionCandidateCount = projectileCandidateCount;
|
||||||
_lastAreaCollisionCandidateCount = areaCandidateCount;
|
_lastAreaCollisionCandidateCount = areaCandidateCount;
|
||||||
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
|
_lastCollisionCandidateCount = projectileCandidateCount + areaCandidateCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResolveProjectileCollisionCandidatesMainThread()
|
private void ResolveCollisionCandidatesOnMainThread()
|
||||||
{
|
{
|
||||||
if (!_collisionCandidates.IsCreated)
|
if (!_collisionCandidates.IsCreated)
|
||||||
{
|
{
|
||||||
_lastResolvedAreaHitCount = 0;
|
_lastResolvedAreaHitCount = 0;
|
||||||
ClearAreaCollisionFrameBuffers();
|
ClearAreaCollisionTransientBuffers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,7 +251,7 @@ namespace Simulation
|
||||||
if (_collisionCandidates.Length == 0)
|
if (_collisionCandidates.Length == 0)
|
||||||
{
|
{
|
||||||
_lastResolvedAreaHitCount = 0;
|
_lastResolvedAreaHitCount = 0;
|
||||||
ClearAreaCollisionFrameBuffers();
|
ClearAreaCollisionTransientBuffers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,7 +268,7 @@ namespace Simulation
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryGetActiveProjectileData(projectileEntityId, out _, out ProjectileSimData projectile))
|
if (!TryGetActiveProjectileSimData(projectileEntityId, out _, out ProjectileSimData projectile))
|
||||||
{
|
{
|
||||||
_projectileResolvedEntityIds.Add(projectileEntityId);
|
_projectileResolvedEntityIds.Add(projectileEntityId);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -338,11 +278,13 @@ namespace Simulation
|
||||||
bool shouldDispatchPresentation = false;
|
bool shouldDispatchPresentation = false;
|
||||||
int damage = 0;
|
int damage = 0;
|
||||||
Vector3 hitPosition = projectile.Position;
|
Vector3 hitPosition = projectile.Position;
|
||||||
if (TryGetTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
|
if (TryGetAliveTargetableEntity(candidate.TargetEntityId, out TargetableObject target))
|
||||||
{
|
{
|
||||||
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
|
EntityBase sourceEntity = TryGetEntityById(candidate.SourceEntityId);
|
||||||
EntityBase ownerEntity = TryGetEntityById(candidate.SourceOwnerEntityId);
|
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);
|
out damage, out hitPosition, out shouldDispatchPresentation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,9 +296,10 @@ namespace Simulation
|
||||||
|
|
||||||
if (shouldExpireProjectile)
|
if (shouldExpireProjectile)
|
||||||
{
|
{
|
||||||
MarkProjectileExpired(projectileEntityId);
|
MarkProjectileAsExpired(projectileEntityId);
|
||||||
_projectileResolvedEntityIds.Add(projectileEntityId);
|
_projectileResolvedEntityIds.Add(projectileEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,49 +326,20 @@ namespace Simulation
|
||||||
int resolvedAreaHitCount;
|
int resolvedAreaHitCount;
|
||||||
using (CustomProfilerMarker.Collision_ResolveArea.Auto())
|
using (CustomProfilerMarker.Collision_ResolveArea.Auto())
|
||||||
{
|
{
|
||||||
resolvedAreaHitCount = ResolveAreaCollisionHitsMainThread();
|
resolvedAreaHitCount = ResolveAreaCollisionHitsOnMainThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastResolvedAreaHitCount = resolvedAreaHitCount;
|
_lastResolvedAreaHitCount = resolvedAreaHitCount;
|
||||||
_projectileResolvedEntityIds.Clear();
|
_projectileResolvedEntityIds.Clear();
|
||||||
ClearAreaCollisionFrameBuffers();
|
ClearAreaCollisionTransientBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecycleInactiveProjectiles()
|
#endregion
|
||||||
{
|
|
||||||
_projectileRecycleEntityIds.Clear();
|
|
||||||
for (int i = 0; i < _projectiles.Count; i++)
|
|
||||||
{
|
|
||||||
ProjectileSimData projectile = _projectiles[i];
|
|
||||||
if (!ShouldRecycleProjectile(projectile))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_projectileRecycleEntityIds.Add(projectile.EntityId);
|
#region Collision Resolve Helpers
|
||||||
}
|
|
||||||
|
|
||||||
if (_projectileRecycleEntityIds.Count == 0)
|
private bool ResolveProjectileHitAgainstTarget(TargetableObject target, EntityBase sourceEntity,
|
||||||
{
|
EntityBase ownerEntity,
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var entityComponent = GameEntry.Entity;
|
|
||||||
for (int i = 0; i < _projectileRecycleEntityIds.Count; i++)
|
|
||||||
{
|
|
||||||
int entityId = _projectileRecycleEntityIds[i];
|
|
||||||
if (entityComponent != null)
|
|
||||||
{
|
|
||||||
entityComponent.HideEntity(entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveProjectileByEntityId(entityId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_projectileRecycleEntityIds.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ResolveProjectileHit(TargetableObject target, EntityBase sourceEntity, EntityBase ownerEntity,
|
|
||||||
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
|
in ProjectileSimData projectile, out int damage, out Vector3 hitPosition,
|
||||||
out bool shouldDispatchPresentation)
|
out bool shouldDispatchPresentation)
|
||||||
{
|
{
|
||||||
|
|
@ -540,7 +454,7 @@ namespace Simulation
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetTargetableEntity(int entityId, out TargetableObject target)
|
private static bool TryGetAliveTargetableEntity(int entityId, out TargetableObject target)
|
||||||
{
|
{
|
||||||
target = null;
|
target = null;
|
||||||
|
|
||||||
|
|
@ -570,7 +484,7 @@ namespace Simulation
|
||||||
return entityComponent != null ? entityComponent.GetGameEntity(entityId) : null;
|
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)
|
out ProjectileSimData projectile)
|
||||||
{
|
{
|
||||||
simulationIndex = -1;
|
simulationIndex = -1;
|
||||||
|
|
@ -593,7 +507,7 @@ namespace Simulation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MarkProjectileExpired(int projectileEntityId)
|
private bool MarkProjectileAsExpired(int projectileEntityId)
|
||||||
{
|
{
|
||||||
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
|
if (!ProjectileBinding.TryGetSimulationIndex(projectileEntityId, out int simulationIndex) ||
|
||||||
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
|
simulationIndex < 0 || simulationIndex >= _projectiles.Count)
|
||||||
|
|
@ -614,7 +528,7 @@ namespace Simulation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int ResolveAreaCollisionHitsMainThread()
|
private int ResolveAreaCollisionHitsOnMainThread()
|
||||||
{
|
{
|
||||||
if (_areaCollisionHitEvents.Count == 0)
|
if (_areaCollisionHitEvents.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -625,7 +539,7 @@ namespace Simulation
|
||||||
for (int i = 0; i < _areaCollisionHitEvents.Count; i++)
|
for (int i = 0; i < _areaCollisionHitEvents.Count; i++)
|
||||||
{
|
{
|
||||||
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
|
AreaCollisionHitEventData hitEvent = _areaCollisionHitEvents[i];
|
||||||
if (!TryGetCollisionQueryById(hitEvent.QueryId, out CollisionQueryData query) ||
|
if (!TryGetCollisionQueryByQueryId(hitEvent.QueryId, out CollisionQueryData query) ||
|
||||||
query.SourceType != CollisionSourceTypeArea)
|
query.SourceType != CollisionSourceTypeArea)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -642,7 +556,7 @@ namespace Simulation
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryGetTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
|
if (!TryGetAliveTargetableEntity(hitEvent.TargetEntityId, out TargetableObject target))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -659,7 +573,7 @@ namespace Simulation
|
||||||
return resolvedHitCount;
|
return resolvedHitCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetCollisionQueryById(int queryId, out CollisionQueryData query)
|
private bool TryGetCollisionQueryByQueryId(int queryId, out CollisionQueryData query)
|
||||||
{
|
{
|
||||||
query = default;
|
query = default;
|
||||||
if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length)
|
if (!_collisionQueryInputs.IsCreated || queryId < 0 || queryId >= _collisionQueryInputs.Length)
|
||||||
|
|
@ -734,139 +648,107 @@ namespace Simulation
|
||||||
return angle <= halfAngle;
|
return angle <= halfAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearAreaCollisionFrameBuffers()
|
private void ClearAreaCollisionTransientBuffers()
|
||||||
{
|
{
|
||||||
_areaCollisionRequests.Clear();
|
_areaCollisionRequests.Clear();
|
||||||
_areaCollisionHitEvents.Clear();
|
_areaCollisionHitEvents.Clear();
|
||||||
_areaCollisionHitDedupKeys.Clear();
|
_areaCollisionHitDedupKeys.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildEnemyCollisionBucketsForProjectiles(float cellSize)
|
private void BuildEnemyCollisionBuckets(float cellSize)
|
||||||
{
|
{
|
||||||
_enemyCollisionBuckets.Clear();
|
_enemyCollisionBuckets.Clear();
|
||||||
for (int i = 0; i < _enemyJobOutputs.Length; i++)
|
int enemyCount = _enemyJobOutputs.Length;
|
||||||
|
if (enemyCount <= 0)
|
||||||
{
|
{
|
||||||
EnemyJobOutputData enemy = _enemyJobOutputs[i];
|
return;
|
||||||
int cellX = (int)math.floor(enemy.Position.x / cellSize);
|
}
|
||||||
int cellZ = (int)math.floor(enemy.Position.z / cellSize);
|
|
||||||
_enemyCollisionBuckets.Add(SeparationCellKey(cellX, cellZ), i);
|
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,
|
[BurstCompile]
|
||||||
out int projectileCandidateCount, out int areaCandidateCount)
|
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;
|
projectileCandidateCount = 0;
|
||||||
areaCandidateCount = 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];
|
return;
|
||||||
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 &&
|
for (int i = 0; i < _collisionCandidates.Length; i++)
|
||||||
query.SourceOwnerEntityId != playerTargetEntityId)
|
{
|
||||||
|
CollisionCandidateData candidate = _collisionCandidates[i];
|
||||||
|
if (candidate.SourceType == CollisionSourceTypeProjectile)
|
||||||
{
|
{
|
||||||
playerPosition.y = query.Position.y;
|
projectileCandidateCount++;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else if (candidate.SourceType == CollisionSourceTypeArea)
|
||||||
if (!hasEnemyTargets || reachedLimit)
|
|
||||||
{
|
{
|
||||||
continue;
|
areaCandidateCount++;
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -876,7 +758,7 @@ namespace Simulation
|
||||||
playerEntityId = PlayerEntityId;
|
playerEntityId = PlayerEntityId;
|
||||||
playerPosition = default;
|
playerPosition = default;
|
||||||
|
|
||||||
if (!TryGetTargetableEntity(playerEntityId, out TargetableObject playerTarget))
|
if (!TryGetAliveTargetableEntity(playerEntityId, out TargetableObject playerTarget))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -892,22 +774,7 @@ namespace Simulation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldRecycleProjectile(in ProjectileSimData projectile)
|
private static bool WasCollisionSourceActiveAtQueryTime(int sourceEntityId)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
EntityBase sourceEntity = TryGetEntityById(sourceEntityId);
|
EntityBase sourceEntity = TryGetEntityById(sourceEntityId);
|
||||||
if (sourceEntity == null || !sourceEntity.Available)
|
if (sourceEntity == null || !sourceEntity.Available)
|
||||||
|
|
@ -928,84 +795,6 @@ namespace Simulation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ExecuteProjectileMovement(int index, NativeArray<ProjectileJobInputData> inputs,
|
#endregion
|
||||||
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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
namespace Simulation
|
||||||
{
|
{
|
||||||
public partial class SimulationWorld
|
public sealed partial class SimulationWorld
|
||||||
{
|
{
|
||||||
|
// Bridges entity show/hide events into simulation state registration.
|
||||||
public sealed class EntitySync
|
public sealed class EntitySync
|
||||||
{
|
{
|
||||||
private const string EnemyGroupName = "Enemy";
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_useSimulationMovement || !_useJobSimulation)
|
if (!_useSimulationMovement)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Components;
|
|
||||||
using CustomDebugger;
|
using CustomDebugger;
|
||||||
using CustomUtility;
|
|
||||||
using Entity;
|
|
||||||
using Entity.EntityData;
|
|
||||||
using Procedure;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
|
|
@ -12,6 +7,18 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
public sealed partial class SimulationWorld : GameFrameworkComponent
|
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 float DefaultAttackRange = 1f;
|
||||||
private const int EnemyStateIdle = 0;
|
private const int EnemyStateIdle = 0;
|
||||||
private const int EnemyStateChasing = 1;
|
private const int EnemyStateChasing = 1;
|
||||||
|
|
@ -19,43 +26,18 @@ namespace Simulation
|
||||||
private const int ProjectileStateActive = 0;
|
private const int ProjectileStateActive = 0;
|
||||||
private const int ProjectileStateExpired = 1;
|
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]
|
[Header("模拟世界全局设置")] [Tooltip("是否启用世界模拟")] [SerializeField]
|
||||||
private bool _useSimulationMovement;
|
private bool _useSimulationMovement = true;
|
||||||
|
|
||||||
[Tooltip("是否启用 Job 运算路径")] [SerializeField]
|
|
||||||
private bool _useJobSimulation;
|
|
||||||
|
|
||||||
[Tooltip("是否使用 Burst 来完成计算")] [SerializeField]
|
|
||||||
private bool _useBurstJobs = true;
|
|
||||||
|
|
||||||
private EntitySync _entitySync;
|
private EntitySync _entitySync;
|
||||||
private Presentation _presentation;
|
private TransformSync _transformSync;
|
||||||
|
private HitPresentation _hitPresentation;
|
||||||
|
|
||||||
private readonly List<EnemySimData> _enemies = new List<EnemySimData>();
|
private readonly List<EnemySimData> _enemies = new List<EnemySimData>();
|
||||||
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
|
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
|
||||||
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
|
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
|
||||||
private readonly List<int> _projectileRecycleEntityIds = new List<int>();
|
private readonly List<int> _projectileRecycleEntityIds = new List<int>();
|
||||||
private readonly HashSet<int> _projectileResolvedEntityIds = new HashSet<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 EnemyBinding { get; } = new EntityBinding();
|
||||||
private EntityBinding ProjectileBinding { get; } = new EntityBinding();
|
private EntityBinding ProjectileBinding { get; } = new EntityBinding();
|
||||||
|
|
@ -65,255 +47,22 @@ namespace Simulation
|
||||||
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
|
public IReadOnlyList<ProjectileSimData> Projectiles => _projectiles;
|
||||||
public IReadOnlyList<PickupSimData> Pickups => _pickups;
|
public IReadOnlyList<PickupSimData> Pickups => _pickups;
|
||||||
public bool UseSimulationMovement => _useSimulationMovement;
|
public bool UseSimulationMovement => _useSimulationMovement;
|
||||||
public bool UseJobSimulation => _useJobSimulation;
|
|
||||||
public bool UseBurstJobs => _useBurstJobs;
|
|
||||||
|
|
||||||
public void SetUseSimulationMovement(bool enabled)
|
#region Lifecycle
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
base.Awake();
|
base.Awake();
|
||||||
_entitySync = new EntitySync(this);
|
_entitySync = new EntitySync(this);
|
||||||
_presentation = new Presentation(this);
|
_transformSync = new TransformSync(this);
|
||||||
|
_hitPresentation = new HitPresentation(this);
|
||||||
InitializeJobDataChannels();
|
InitializeJobDataChannels();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
_entitySync?.OnStart();
|
_entitySync?.OnStart();
|
||||||
_presentation?.OnStart();
|
_hitPresentation?.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(in SimulationTickContext context)
|
public void Tick(in SimulationTickContext context)
|
||||||
|
|
@ -323,307 +72,27 @@ namespace Simulation
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_useJobSimulation)
|
|
||||||
{
|
|
||||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
|
||||||
{
|
|
||||||
TickEnemiesJobified(in context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies.Auto())
|
using (CustomProfilerMarker.TickEnemies.Auto())
|
||||||
{
|
{
|
||||||
TickEnemies(in context);
|
TickSimulationPipeline(in context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
_enemies.Clear();
|
_hitPresentation?.OnDestroy();
|
||||||
_projectiles.Clear();
|
_entitySync?.OnDestroy();
|
||||||
_pickups.Clear();
|
_entitySync = null;
|
||||||
_projectileRecycleEntityIds.Clear();
|
_transformSync = null;
|
||||||
_projectileResolvedEntityIds.Clear();
|
_hitPresentation = null;
|
||||||
_areaCollisionRequests.Clear();
|
DisposeJobDataChannels();
|
||||||
_areaCollisionHitEvents.Clear();
|
|
||||||
_areaCollisionHitDedupKeys.Clear();
|
|
||||||
_enemySeparationAgents.Clear();
|
|
||||||
_enemyTickWorkItems.Clear();
|
|
||||||
ClearJobDataChannels();
|
|
||||||
|
|
||||||
EnemyBinding.Clear();
|
|
||||||
ProjectileBinding.Clear();
|
|
||||||
PickupBinding.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TickEnemies(in SimulationTickContext context)
|
private void LateUpdate()
|
||||||
{
|
{
|
||||||
if (_enemies.Count == 0 || context.DeltaTime <= 0f)
|
_transformSync?.OnLateUpdate();
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildEnemyTickInput(in Vector3 playerPosition)
|
#endregion
|
||||||
{
|
|
||||||
_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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ MonoBehaviour:
|
||||||
showUnityEditorReport: 0
|
showUnityEditorReport: 0
|
||||||
logBehaviour: 0
|
logBehaviour: 0
|
||||||
drawGizmos: 1
|
drawGizmos: 1
|
||||||
defaultRecyclable: 0
|
defaultRecyclable: 1
|
||||||
defaultAutoPlay: 3
|
defaultAutoPlay: 3
|
||||||
defaultUpdateType: 0
|
defaultUpdateType: 0
|
||||||
defaultTimeScaleIndependent: 0
|
defaultTimeScaleIndependent: 0
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,111 @@
|
||||||
|
# P2 Job System + Burst 落地(结项与验收)
|
||||||
|
|
||||||
|
## 1. 文档目的
|
||||||
|
本文件用于对齐 `docs/TodoList.md` 的 P2 Checkpoint 9,作为 P2 结项与 P3 输入基线。
|
||||||
|
|
||||||
|
目标:
|
||||||
|
- 固化压测口径(1k/2k/3k)
|
||||||
|
- 给出回归验证结论
|
||||||
|
- 给出开关/回滚策略
|
||||||
|
- 给出最终验收判定(通过/不通过)
|
||||||
|
|
||||||
|
## 2. 验收标准(对齐 TodoList)
|
||||||
|
来源:`docs/TodoList.md` 第 171~179 行。
|
||||||
|
|
||||||
|
- 在 `3k` 敌人规模下,CPU Main Thread 明显下降(目标 `>= 30%`)。
|
||||||
|
- Profiler 中战斗帧 `GC Alloc` 接近 `0`(持续帧)。
|
||||||
|
|
||||||
|
## 3. 测试设备与环境
|
||||||
|
- 设备:iQOO Neo8
|
||||||
|
- CPU:第一代骁龙 8+
|
||||||
|
- 内存:12 GB
|
||||||
|
- 系统:OriginOS 6(Android 16)
|
||||||
|
- Profiler 口径:以 CPU `ms` 为主,`fps` 仅作辅助(Android 端存在 60fps 上限)
|
||||||
|
- Profiler 配置:`Call Stacks = Off`
|
||||||
|
|
||||||
|
## 4. P2 开关与回滚策略
|
||||||
|
|
||||||
|
### 4.1 运行开关
|
||||||
|
- `UseSimulationMovement`
|
||||||
|
- `UseJobSimulation`
|
||||||
|
- `UseBurstJobs`
|
||||||
|
|
||||||
|
### 4.2 生效时机约束
|
||||||
|
- `UseSimulationMovement` / `UseJobSimulation`:战斗内不支持热切换,需在 Battle 外修改后生效。
|
||||||
|
- `UseBurstJobs`:可切换,但建议仅用于战斗外 A/B。
|
||||||
|
|
||||||
|
### 4.3 回滚策略(建议)
|
||||||
|
1. 切回非 Job 路径:`UseJobSimulation = false`
|
||||||
|
2. 若仍异常,切回旧移动:`UseSimulationMovement = false`
|
||||||
|
3. 保留 `UseBurstJobs` 仅在 Job 路径 A/B 对照
|
||||||
|
|
||||||
|
## 5. 回归验证(Checkpoint 9)
|
||||||
|
|
||||||
|
| 用例 | 目标 | 状态 | 证据 |
|
||||||
|
|------------------------------------------|--------------|----|----|
|
||||||
|
| 10 分钟连续战斗 | 无异常日志、流程稳定 | 待补 | 待补 |
|
||||||
|
| `Battle -> LevelUp -> Shop -> Battle` 循环 | 状态切换稳定、无卡死 | 待补 | 待补 |
|
||||||
|
| 掉落拾取链路 | 掉落生成/吸附/回收正常 | 待补 | 待补 |
|
||||||
|
|
||||||
|
建议附证据:
|
||||||
|
- `Logs/playmode-tests.log`
|
||||||
|
- 关键流程录屏/截图
|
||||||
|
- 回归脚本或人工步骤说明
|
||||||
|
|
||||||
|
## 6. 压测口径与数据
|
||||||
|
|
||||||
|
### 6.1 标准口径(必须覆盖)
|
||||||
|
- 敌人规模:`1k / 2k / 3k`
|
||||||
|
- 指标:
|
||||||
|
- Main Thread (`ms`)
|
||||||
|
- Job Workers (`ms`)
|
||||||
|
- GC Alloc (`B/frame`)
|
||||||
|
- 关键 Marker(`BuildInput / MoveSeparation / Complete / WriteBack`)
|
||||||
|
|
||||||
|
### 6.2 当前已测数据(你提供)
|
||||||
|
|
||||||
|
#### CPU 分阶段数据(P2)
|
||||||
|
| 指标 | `500 enemies` | `1000 enemies` | `1500 enemies` | `2000 enemies` |
|
||||||
|
|----------------|--------------------:|--------------------:|--------------------:|--------------------:|
|
||||||
|
| 帧率 | 62.6 fps (15.96 ms) | 52.6 fps (19.00 ms) | 35.0 fps (28.56 ms) | 24.9 fps (40.05 ms) |
|
||||||
|
| BuildInput | 0.28 ms | 0.58 ms | 0.88 ms | 1.13 ms |
|
||||||
|
| MoveSeparation | 0.38 ms | 0.94 ms | 1.59 ms | 2.48 ms |
|
||||||
|
| StateUpdate | 0.01 ms | 0.01 ms | 0.01 ms | 0.01 ms |
|
||||||
|
| Schedule | 0.00 ms | 0.00 ms | 0.00 ms | 0.00 ms |
|
||||||
|
| Complete | 0.45 ms | 1.20 ms | 1.86 ms | 3.79 ms |
|
||||||
|
| WriteBack | 0.15 ms | 0.31 ms | 1.20 ms | 2.00 ms |
|
||||||
|
|
||||||
|
#### CPU 热路径对比(P1.5 -> P2)
|
||||||
|
说明:P2 以六阶段总和近似对齐 P1.5 四阶段 `TickEnemies ms`。
|
||||||
|
|
||||||
|
| 敌人数量 | P1.5 TickEnemies | P2 TickEnemies | 降幅 |
|
||||||
|
|--------|-----------------:|---------------:|-------:|
|
||||||
|
| `500` | 4.77 ms | 1.30 ms | -72.7% |
|
||||||
|
| `1000` | 9.86 ms | 3.06 ms | -68.9% |
|
||||||
|
| `1500` | 15.42 ms | 5.57 ms | -63.8% |
|
||||||
|
| `2000` | 21.68 ms | 9.44 ms | -56.4% |
|
||||||
|
|
||||||
|
## 7. 验收判定
|
||||||
|
|
||||||
|
| 验收项 | 标准 | 当前状态 | 判定 |
|
||||||
|
|--------------------|----------|----------|-----|
|
||||||
|
| Main Thread 降幅(2k) | `>= 30%` | 缺失 3k 数据 | 不通过 |
|
||||||
|
| 持续帧 GC Alloc | 接近 0 | 缺失 GC 数据 | 不通过 |
|
||||||
|
|
||||||
|
**当前结论:P2 Checkpoint 9 暂不通过。**
|
||||||
|
|
||||||
|
可确认部分:
|
||||||
|
- P2 在 `500~2000` 规模的热路径 CPU 优化已显著成立。
|
||||||
|
- 但未满足 TodoList 的完整验收口径(3k + GC + 回归证据)。
|
||||||
|
|
||||||
|
## 8. 下一步补齐动作(建议)
|
||||||
|
1. 按同一场景补采 `3k` 数据(P1.5 与 P2 各一次,至少 60s 稳态窗口)。
|
||||||
|
2. 记录 `Main Thread / Job Workers / GC Alloc` 三项,写入 6.3 对应表。
|
||||||
|
3. 完成 5.0 的三个回归用例并填入证据。
|
||||||
|
4. 补齐后将第 7 节判定更新为“通过”,再在 `TodoList.md` 把 P2 Checkpoint 9 勾选。
|
||||||
|
|
||||||
|
## 9. 测试命令(复用)
|
||||||
|
- PlayMode:
|
||||||
|
- `Unity -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log`
|
||||||
|
- EditMode:
|
||||||
|
- `Unity -batchmode -nographics -projectPath . -runTests -testPlatform EditMode -testResults Logs/editmode-test-results.xml -logFile Logs/editmode-tests.log`
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
## 1. P0 基线修正与性能基准
|
## 1. P0 基线修正与性能基准
|
||||||
- [x] 建立性能基准场景(建议复用 `Game.unity` + 压测参数):
|
- [x] 建立性能基准场景(建议复用 `Game.unity` + 压测参数):
|
||||||
- 指标:`1k / 2k / 3k` 敌人时的 FPS、CPU Main Thread、GC Alloc、Draw Calls。
|
- 指标:`0.5k / 1k / 1.5k / 2k` 敌人时的 FPS、CPU Main Thread、GC Alloc、Draw Calls。
|
||||||
- 输出:一份基线表格(开发机配置 + Unity Profiler 截图)。
|
- 输出:一份基线表格(开发机配置 + Unity Profiler 截图)。
|
||||||
- [x] 修正当前高风险逻辑问题(避免后续优化建立在不稳定行为上):
|
- [x] 修正当前高风险逻辑问题(避免后续优化建立在不稳定行为上):
|
||||||
- `ProcedureGame.OnEnter()` 与 `_hudInitialized` 逻辑中有重复初始化状态机风险(`InitGameState()` 被调用两次)。
|
- `ProcedureGame.OnEnter()` 与 `_hudInitialized` 逻辑中有重复初始化状态机风险(`InitGameState()` 被调用两次)。
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
- [x] Checkpoint 7:P1 阶段回归与性能记录
|
- [x] Checkpoint 7:P1 阶段回归与性能记录
|
||||||
- 回归用例:战斗 10 分钟、`Battle -> LevelUp -> Shop -> Battle` 循环、掉落吸附与拾取。
|
- 回归用例:战斗 10 分钟、`Battle -> LevelUp -> Shop -> Battle` 循环、掉落吸附与拾取。
|
||||||
- Profiling 对比:记录 1k/2k/3k 敌人下 Main Thread、GC Alloc、敌人更新耗时。
|
- Profiling 对比:记录 `0.5k / 1k / 1.5k / 2k` 敌人下 Main Thread、GC Alloc、敌人更新耗时。
|
||||||
- 输出文档:`P1 Simulation 分层设计 + 回滚开关说明 + 对比数据`。
|
- 输出文档:`P1 Simulation 分层设计 + 回滚开关说明 + 对比数据`。
|
||||||
- 完成标准:核心流程稳定,无新增 Error/Exception;可一键回滚到旧更新路径。
|
- 完成标准:核心流程稳定,无新增 Error/Exception;可一键回滚到旧更新路径。
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
- 目标:将 `TickEnemies GC` 从当前 `27~108 KB` 降到 `< 5 KB / frame`。
|
- 目标:将 `TickEnemies GC` 从当前 `27~108 KB` 降到 `< 5 KB / frame`。
|
||||||
- 重点文件:`Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs`。
|
- 重点文件:`Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs`。
|
||||||
- 处理方式:桶容器与临时列表复用(包含 bucket list 复用池),避免每帧重建集合。
|
- 处理方式:桶容器与临时列表复用(包含 bucket list 复用池),避免每帧重建集合。
|
||||||
- 完成标准:`2000` 敌人压测下 `TickEnemies GC` 稳定 `< 5 KB / frame`。
|
- 完成标准:`2k` 敌人压测下 `TickEnemies GC` 稳定 `< 5 KB / frame`。
|
||||||
|
|
||||||
- [x] Checkpoint 2:解耦 Simulation 核心与 `Transform` 运行时依赖
|
- [x] Checkpoint 2:解耦 Simulation 核心与 `Transform` 运行时依赖
|
||||||
- 目标:`SimulationWorld.TickEnemies` 不直接读取或写入 `Transform`。
|
- 目标:`SimulationWorld.TickEnemies` 不直接读取或写入 `Transform`。
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
- 构建分桶(Build Buckets)
|
- 构建分桶(Build Buckets)
|
||||||
- 邻域候选查询(Query Neighbors)
|
- 邻域候选查询(Query Neighbors)
|
||||||
- 避免全量最近邻搜索,控制复杂度随敌人数增长的斜率。
|
- 避免全量最近邻搜索,控制复杂度随敌人数增长的斜率。
|
||||||
- 完成标准:`3k` 敌人下目标选择阶段耗时稳定,且无索引越界/漏目标回归。
|
- 完成标准:`2k` 敌人下目标选择阶段耗时稳定,且无索引越界/漏目标回归。
|
||||||
|
|
||||||
- [x] Checkpoint 5:投射物批量移动与寿命回收 Job 化
|
- [x] Checkpoint 5:投射物批量移动与寿命回收 Job 化
|
||||||
- 投射物数据结构最少包含:`position/velocity/lifeTime/age/active`。
|
- 投射物数据结构最少包含:`position/velocity/lifeTime/age/active`。
|
||||||
|
|
@ -170,12 +170,12 @@
|
||||||
|
|
||||||
- [ ] Checkpoint 9:P2 回归、压测与结项文档
|
- [ ] Checkpoint 9:P2 回归、压测与结项文档
|
||||||
- 回归用例:10 分钟战斗、`Battle -> LevelUp -> Shop -> Battle` 循环、掉落拾取链路。
|
- 回归用例:10 分钟战斗、`Battle -> LevelUp -> Shop -> Battle` 循环、掉落拾取链路。
|
||||||
- 压测口径:`1k/2k/3k` 敌人,记录 Main Thread、Job Workers、GC Alloc、关键 Marker。
|
- 压测口径:`0.5k / 1k / 1.5k / 2k` 敌人,记录 Main Thread、Job Workers、GC Alloc、关键 Marker。
|
||||||
- 输出文档:`P2 Job/Burst 改造说明 + 开关/回滚策略 + 前后对比数据`。
|
- 输出文档:`P2 Job/Burst 改造说明 + 开关/回滚策略 + 前后对比数据`。
|
||||||
- 完成标准:结论可复现,可作为 P3 GPU Instancing 的输入基线。
|
- 完成标准:结论可复现,可作为 P3 GPU Instancing 的输入基线。
|
||||||
|
|
||||||
**验收标准**
|
**验收标准**
|
||||||
- 在 3k 敌人规模下,CPU Main Thread 明显下降(目标 >= 30%)。
|
- 在 2k 敌人规模下,CPU Main Thread 明显下降(目标 >= 30%)。
|
||||||
- Profiler 中战斗帧 GC Alloc 接近 0(持续帧)。
|
- Profiler 中战斗帧 GC Alloc 接近 0(持续帧)。
|
||||||
|
|
||||||
## 4. P3 GPU Instancing 渲染管线(与 Job 并行推进)
|
## 4. P3 GPU Instancing 渲染管线(与 Job 并行推进)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
# UI 五层架构设计规范(RawData / Controller / View / Context / UseCase)
|
||||||
|
|
||||||
|
## 1. 适用范围
|
||||||
|
|
||||||
|
- 适用目录:`Assets/GameMain/Scripts/UI/*`
|
||||||
|
- 重点对象:采用五层拆分的 UI 模块(`MenuScene`、`GameScene`、`General` 下的分层 UI)
|
||||||
|
- 本文不展开 Unity GameFramework 底层实现细节,仅约束项目内 UI 代码组织与协作方式
|
||||||
|
|
||||||
|
## 2. 架构总览
|
||||||
|
|
||||||
|
UI 模块采用“输入数据 -> 业务编排 -> 展示数据 -> 渲染表现”的分层方式,核心链路如下:
|
||||||
|
|
||||||
|
1. 外部流程(Procedure/GameState)创建并绑定 UseCase
|
||||||
|
2. 通过 `GameEntry.UIRouter` 打开指定 UI
|
||||||
|
3. Controller 从 UseCase 取 RawData,并转换为 Context
|
||||||
|
4. View 使用 Context 渲染
|
||||||
|
5. View 通过事件回传交互,Controller 处理后驱动 UseCase 更新,再刷新 View
|
||||||
|
|
||||||
|
简化关系图:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Procedure/GameState
|
||||||
|
-> UIRouter
|
||||||
|
-> Controller <-> UseCase
|
||||||
|
-> Context -> View
|
||||||
|
View --(CustomEvent)--> Controller
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 五层职责定义
|
||||||
|
|
||||||
|
### 3.1 RawData 层
|
||||||
|
|
||||||
|
职责:承载“业务原始数据”,作为 UseCase 到 Controller 的传输模型。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 命名:`XXXFormRawData`
|
||||||
|
- 只描述数据,不包含 UI 渲染行为
|
||||||
|
- 可保留领域对象或数据表对象(例如 `DRLevelUpReward`、`WeaponBase`)
|
||||||
|
- 不依赖具体 View 组件
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/RawData/ShopFormRawData.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/RawData/LevelUpFormRawData.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/MenuScene/RawData/SelectRoleFormRawData.cs`
|
||||||
|
|
||||||
|
### 3.2 UseCase 层
|
||||||
|
|
||||||
|
职责:封装 UI 对应业务用例,负责业务规则、状态推进、数据生成。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 实现 `IUIUseCase`
|
||||||
|
- 命名:`XXXFormUseCase`
|
||||||
|
- 对外提供 `CreateInitialModel / TryRefresh / Select / Confirm` 等语义化方法
|
||||||
|
- 返回 RawData(或结果对象),不直接操作具体 View
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/UseCase/ShopFormUseCase.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/UseCase/LevelUpFormUseCase.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/MenuScene/UseCase/SelectRoleFormUseCase.cs`
|
||||||
|
|
||||||
|
### 3.3 Controller 层
|
||||||
|
|
||||||
|
职责:UI 编排层,连接 UseCase 与 View,管理 UI 生命周期、事件订阅、数据转换。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 继承 `UIFormControllerCommonBase<TContext, TForm>`
|
||||||
|
- 命名:`XXXFormController`
|
||||||
|
- 通过 `BindUseCase(IUIUseCase)` 注入用例并做类型校验
|
||||||
|
- `OpenUI(object userData = null)` 支持:`Context`、`RawData`、`null`
|
||||||
|
- 负责 RawData -> Context 的转换(常见 `BuildContext`)
|
||||||
|
- 在 `SubscribeCustomEvents / UnsubscribeCustomEvents` 成对管理事件
|
||||||
|
- 可做局部刷新(避免整窗重建)
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/Controller/ShopFormController.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/Controller/LevelUpFormController.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/MenuScene/Controller/SelectRoleFormController.cs`
|
||||||
|
|
||||||
|
### 3.4 Context 层
|
||||||
|
|
||||||
|
职责:承载“可直接驱动 UI 展示”的上下文数据。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 继承 `UIContext`
|
||||||
|
- 命名:`XXXFormContext` 或 `XXXItemContext`
|
||||||
|
- 字段以展示友好为目标(标题、描述、图标、稀有度、列表等)
|
||||||
|
- 允许组合子 Context(例如列表区 + 条目)
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/Context/ShopFormContext.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/Context/DisplayListAreaContext.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/MenuScene/Context/SelectRoleFormContext.cs`
|
||||||
|
|
||||||
|
### 3.5 View 层
|
||||||
|
|
||||||
|
职责:纯表现层,负责控件绑定、显示刷新、交互事件抛出。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- Form 类继承 `UGuiForm`,子组件通常继承 `MonoBehaviour`
|
||||||
|
- 命名:`XXXForm` / `XXXItem` / `XXXArea`
|
||||||
|
- 提供 `RefreshUI(Context)`、`OnInit(Context)`、`OnReset()` 等渲染入口
|
||||||
|
- 用户交互通过 `GameEntry.Event.Fire(...)` 通知 Controller
|
||||||
|
- 不承载业务规则(计算、流程推进、数据筛选应在 UseCase)
|
||||||
|
|
||||||
|
参考:
|
||||||
|
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/View/ShopForm.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/GameScene/View/DisplayListArea.cs`
|
||||||
|
- `Assets/GameMain/Scripts/UI/MenuScene/View/SelectRoleForm.cs`
|
||||||
|
|
||||||
|
## 4. 标准交互流程
|
||||||
|
|
||||||
|
### 4.1 初始化与绑定
|
||||||
|
|
||||||
|
1. Procedure/GameState 创建 UseCase
|
||||||
|
2. 调用 `GameEntry.UIRouter.BindUIUseCase(UIFormType.X, useCase)`
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
- `Assets/GameMain/Scripts/Procedure/Game/GameStateShop.cs`
|
||||||
|
- `Assets/GameMain/Scripts/Procedure/Game/GameStateLevelUp.cs`
|
||||||
|
- `Assets/GameMain/Scripts/Procedure/ProcedureStartMenu.cs`
|
||||||
|
|
||||||
|
### 4.2 打开 UI
|
||||||
|
|
||||||
|
1. 调用 `GameEntry.UIRouter.OpenUI(UIFormType.X)`
|
||||||
|
2. Controller 从 UseCase 取 RawData(或接收外部 RawData/Context)
|
||||||
|
3. Controller 构建 Context 后打开/刷新 Form
|
||||||
|
4. View 在 `OnOpen` 中校验 Context 类型并执行 `RefreshUI`
|
||||||
|
|
||||||
|
### 4.3 用户交互到刷新
|
||||||
|
|
||||||
|
1. View 触发事件(如购买、刷新、选择)
|
||||||
|
2. Controller 监听事件并调用 UseCase
|
||||||
|
3. UseCase 返回新数据或操作结果
|
||||||
|
4. Controller 更新 Context 并刷新全部或局部 UI
|
||||||
|
|
||||||
|
### 4.4 关闭 UI
|
||||||
|
|
||||||
|
1. 调用 `GameEntry.UIRouter.CloseUI(UIFormType.X)`
|
||||||
|
2. Controller 解除事件订阅并关闭窗体
|
||||||
|
3. View `OnClose` 清理本地状态
|
||||||
|
|
||||||
|
## 5. 目录与命名规范
|
||||||
|
|
||||||
|
- 目录:`UI/<SceneDomain>/RawData|UseCase|Controller|Context|View`
|
||||||
|
- 五层同名前缀保持一致:`ShopForm*`、`LevelUpForm*`、`SelectRoleForm*`
|
||||||
|
- 子组件上下文命名:`RoleItemContext`、`DisplayItemContext`、`LevelUpRewardItemContext`
|
||||||
|
- 新增 UI Form 时优先建立完整五层;仅纯静态展示可降级为 View-only
|
||||||
|
|
||||||
|
## 6. 依赖方向约束
|
||||||
|
|
||||||
|
允许依赖:
|
||||||
|
|
||||||
|
- `UseCase -> RawData / 领域对象`
|
||||||
|
- `Controller -> UseCase + RawData + Context + View + Event`
|
||||||
|
- `View -> Context + Event`
|
||||||
|
|
||||||
|
禁止依赖:
|
||||||
|
|
||||||
|
- `View -> UseCase`
|
||||||
|
- `View -> 领域状态修改`
|
||||||
|
- `Context/RawData -> View`
|
||||||
|
|
||||||
|
## 7. 新增一个五层 UI 的落地步骤
|
||||||
|
|
||||||
|
1. 在目标场景目录创建 `RawData / UseCase / Context / Controller / View` 对应类型
|
||||||
|
2. 在 UseCase 中实现模型创建与交互方法
|
||||||
|
3. 在 Controller 中实现 `BindUseCase`、`OpenUI`、`BuildContext`、事件订阅
|
||||||
|
4. 在 View 中实现 `RefreshUI` 和交互事件抛出
|
||||||
|
5. 在对应 Procedure/GameState 里完成 UseCase 绑定与 Open/Close 调用
|
||||||
|
6. 自测三条主链路:首次打开、交互刷新、关闭重开
|
||||||
|
|
||||||
|
## 8. 项目当前实践说明
|
||||||
|
|
||||||
|
- `ShopForm`、`LevelUpForm`、`SelectRoleForm` 是当前五层模式的主要样板
|
||||||
|
- `DialogForm` 也有 Controller/Context/RawData,但 UseCase 为可选
|
||||||
|
- `HudForm`、`StartMenuForm` 当前为轻用例场景,可不强制 UseCase
|
||||||
|
- `SettingForm`、`AboutForm` 属于历史直连型 UI,不属于五层完整样板
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如后续需要统一重构,建议优先把历史直连型 UI(如 `SettingForm`)迁移到五层模板,以降低 UI 逻辑耦合度。
|
||||||
|
|
@ -1,40 +1,46 @@
|
||||||
---
|
---
|
||||||
name: simulation-development
|
name: simulation-development
|
||||||
description: Maintain and extend the VampireLike Simulation layer. Use when modifying `Assets/GameMain/Scripts/Simulation` or related runtime paths (`GameStateBattle`, enemy movement gate, entity lifecycle sync, separation solver), including P1.5 cleanup and P2 Job/Burst preparation.
|
description: Maintain and extend VampireLike SimulationWorld (P2 baseline). Use for Simulation data contracts, lifecycle sync, Job/Burst pipeline, collision settlement, and rollback-safe runtime switches.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Simulation Development
|
# Simulation Development
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
1. Read baseline design doc: `./references/SimulationDevelopmentSkill.md`.
|
1. Read the design spec first: `./references/SimulationDevelopmentSkill.md`.
|
||||||
2. Read latest measured baseline: `../../docs/P1.5 Simulation-Supplement.md`.
|
2. If performance conclusions change, sync evidence to `../../docs/P2 Job System + Burst 落地.md`.
|
||||||
3. Confirm your change scope is one or more of:
|
3. Classify change scope before coding:
|
||||||
- `SimData` contracts (`EnemySimData`, `ProjectileSimData`, `PickupSimData`)
|
- `SimData/JobData` contracts
|
||||||
- lifecycle sync (`SimulationWorld.EntitySync`)
|
- lifecycle sync (`SimulationWorld.EntitySync`)
|
||||||
- per-frame simulation (`SimulationWorld.Tick`, `TickEnemies`)
|
- Job/Burst execution pipeline (`SimulationWorld.EnemyJobs`, `SimulationWorld.ProjectileJobs`)
|
||||||
|
- collision query/settlement semantics
|
||||||
- presentation write-back (`SimulationWorld.Presentation`)
|
- presentation write-back (`SimulationWorld.Presentation`)
|
||||||
- enemy separation solver integration (`EnemySeparationSolverProvider`)
|
4. Decide rollback behavior up front:
|
||||||
4. Keep rollback path available through `UseSimulationMovement`.
|
- `UseSimulationMovement` off path
|
||||||
|
- `UseJobSimulation` off path
|
||||||
|
5. Add/adjust both EditMode and PlayMode regression tests.
|
||||||
|
|
||||||
## Source Map
|
## Source Map
|
||||||
|
|
||||||
- Simulation core: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`
|
- Simulation core: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`
|
||||||
|
- Job data channel: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.JobDataChannel.cs`
|
||||||
|
- Enemy jobs: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.EnemyJobs.cs`
|
||||||
|
- Projectile jobs: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.ProjectileJobs.cs`
|
||||||
- Lifecycle sync: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs`
|
- Lifecycle sync: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs`
|
||||||
- Presentation sync: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs`
|
- Presentation sync: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs`
|
||||||
|
- Target selection index: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.TargetSelectionSpatialIndex.cs`
|
||||||
- Tick context: `../../Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs`
|
- Tick context: `../../Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs`
|
||||||
- Index binding: `../../Assets/GameMain/Scripts/Simulation/EntityBinding.cs`
|
- Index binding: `../../Assets/GameMain/Scripts/Simulation/EntityBinding.cs`
|
||||||
- Battle entry: `../../Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs`
|
- Battle entry: `../../Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs`
|
||||||
|
- Battle state gate: `../../Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs`
|
||||||
|
- Damage/collision utility: `../../Assets/GameMain/Scripts/Utility/AIUtility.cs`
|
||||||
- Global component init: `../../Assets/GameMain/Scripts/Base/GameEntry.Custom.cs`
|
- Global component init: `../../Assets/GameMain/Scripts/Base/GameEntry.Custom.cs`
|
||||||
- Enemy old path gate:
|
- Enemy old path gate:
|
||||||
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs`
|
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs`
|
||||||
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs`
|
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs`
|
||||||
- Separation solver:
|
- Regression tests:
|
||||||
- `../../Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs`
|
- `../../Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs`
|
||||||
- `../../Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs`
|
- `../../Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs`
|
||||||
- `../../Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs`
|
|
||||||
- P1.5 baseline doc:
|
|
||||||
- `../../docs/P1.5 Simulation-Supplement.md`
|
|
||||||
|
|
||||||
## Non-Negotiable Invariants
|
## Non-Negotiable Invariants
|
||||||
|
|
||||||
|
|
@ -44,46 +50,53 @@ description: Maintain and extend the VampireLike Simulation layer. Use when modi
|
||||||
- Keep logic/presentation boundary:
|
- Keep logic/presentation boundary:
|
||||||
- Simulation computes logical outputs.
|
- Simulation computes logical outputs.
|
||||||
- Presentation writes back `Transform`.
|
- Presentation writes back `Transform`.
|
||||||
- Keep A/B rollback path:
|
- Keep A/B rollback path (`UseSimulationMovement`/`UseJobSimulation`).
|
||||||
- `UseSimulationMovement == false` must preserve old behavior path.
|
- `SetUseSimulationMovement` and `SetUseJobSimulation` must not hot-switch during `Battle`.
|
||||||
|
- Keep area query snapshot semantics (`SourceWasActiveAtQueryTime`) intact.
|
||||||
|
- Keep dodge semantics using `Value` (additive), not `Percent`.
|
||||||
- Avoid new managed allocations in Tick hot paths.
|
- Avoid new managed allocations in Tick hot paths.
|
||||||
|
|
||||||
## Change Recipes
|
## Change Recipes
|
||||||
|
|
||||||
### Add or Change SimData Fields
|
### Add or Change SimData Fields
|
||||||
|
|
||||||
1. Update target struct in `Simulation/SimData/`.
|
1. Update target structs in `Simulation/SimData/` and Job channel structs.
|
||||||
2. Populate default/initial values in `EntitySync` create methods.
|
2. Populate defaults in `Create*InitialSimData` / lifecycle registration path.
|
||||||
3. Apply runtime updates in `Tick` phase.
|
3. Apply runtime updates in simulation stages.
|
||||||
4. Consume outputs in `Presentation` only if visual write-back is needed.
|
4. Consume visual fields in `Presentation` only.
|
||||||
5. Ensure backward compatibility when `UseSimulationMovement` is off.
|
5. Ensure backward compatibility when `UseSimulationMovement` is off.
|
||||||
|
|
||||||
### Extend Enemy Tick Behavior
|
### Extend Job/Burst Pipeline
|
||||||
|
|
||||||
1. Keep deterministic stage order (`BuildInput -> Move/Separation -> StateUpdate -> WriteBack`).
|
1. Keep deterministic stage ownership (Build/Schedule/Complete/Commit).
|
||||||
2. Preserve state semantics and avoid direct UI/event side effects in Tick.
|
2. Preserve state semantics; avoid UI/audio/effect side effects in simulation loops.
|
||||||
3. Keep `ProfilerMarker` coverage for each stage.
|
3. Keep `ProfilerMarker` coverage for new or changed stages.
|
||||||
4. Keep Tick hot path data-driven (no direct `Transform` read/write).
|
4. Keep hot paths data-driven (no direct `Transform` reads/writes).
|
||||||
|
|
||||||
### Implement Projectile/Pickup Tick (from placeholder to real behavior)
|
### Modify Collision / Area Query Behavior
|
||||||
|
|
||||||
1. Keep lifecycle path unchanged (`EntitySync` handles add/remove).
|
1. Treat broad phase candidate generation and main-thread settlement as separate steps.
|
||||||
2. Add dedicated tick methods in `SimulationWorld` for each data type.
|
2. Preserve `MaxTargets` semantics across player + enemy candidates.
|
||||||
3. Keep outputs in data containers; write visuals in presentation phase.
|
3. If adding query metadata, flow it through:
|
||||||
4. Ensure removal path and binding remap rules are identical to enemy path.
|
- request buffer -> collision query input -> candidate -> settlement.
|
||||||
|
4. Keep area-source snapshot behavior and avoid runtime-state race regressions.
|
||||||
|
|
||||||
### Refactor Toward Job/Burst
|
### Add or Adjust Runtime Switches
|
||||||
|
|
||||||
1. Prioritize `Move/Separation` stage parallelization.
|
1. Define exact effective timing (`Battle` or out-of-`Battle`) before implementation.
|
||||||
2. Keep `ProfilerMarker` and stage boundaries stable for P1.5/P2 comparison.
|
2. For high-risk switches, enforce out-of-battle-only changes.
|
||||||
3. Leave transform write-back to presentation-only stage.
|
3. Provide clear warning logs for ignored runtime switch attempts.
|
||||||
4. Keep managed allocations and virtual dispatch out of core loops.
|
|
||||||
|
|
||||||
## Validation Checklist
|
## Validation Checklist
|
||||||
|
|
||||||
- `UseSimulationMovement = false` and `true` both run correctly.
|
- `UseSimulationMovement = false` and `true` both run correctly.
|
||||||
|
- `UseJobSimulation = false` and `true` both run correctly under simulation mode.
|
||||||
- No duplicate registration or stale index after entity hide/destroy.
|
- No duplicate registration or stale index after entity hide/destroy.
|
||||||
- Battle loop remains stable (`Battle -> LevelUp -> Shop -> Battle`).
|
- Battle loop remains stable (`Battle -> LevelUp -> Shop -> Battle`).
|
||||||
- No new per-frame GC spikes in `TickEnemies`.
|
- No new per-frame GC spikes in hot paths.
|
||||||
- Main flow has no new Error/Exception logs.
|
- Main flow has no new Error/Exception logs.
|
||||||
- Update `./references/SimulationDevelopmentSkill.md` and `../../docs/P1.5 Simulation-Supplement.md` if contracts, boundaries, or baseline data changed.
|
- Keep these regression tests green in both EditMode and PlayMode:
|
||||||
|
- `TickProjectiles_LimitsCandidatesToMaxTargets_IncludingPlayerCandidate`
|
||||||
|
- `SetUseSimulationAndJob_AreIgnored_WhenBattleStateIsActive`
|
||||||
|
- `EnqueueAreaQuery_CapturesInactiveSourceSnapshot_WhenSourceEntityUnavailable`
|
||||||
|
- Update `./references/SimulationDevelopmentSkill.md` when contracts, boundaries, or rules change.
|
||||||
|
|
|
||||||
|
|
@ -1,159 +1,200 @@
|
||||||
# Simulation Development Skill(VampireLike)
|
# Simulation Development Skill (VampireLike)
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
本文件是 `Simulation` 分层的开发规范与速查手册。
|
本文件是 SimulationWorld 的正式设计说明和扩展开发规范。
|
||||||
后续调整敌人移动、补齐投射物/掉落物逻辑、推进 Job/Burst 改造时,优先按本文档执行,避免反复通读全部代码。
|
后续在 Simulation 相关模块做功能扩展、性能优化、回归修复时,统一按本规范执行。
|
||||||
|
|
||||||
## 当前架构总览(P1.5 已落地)
|
## 适用范围
|
||||||
- Simulation 主目录:`Assets/GameMain/Scripts/Simulation/`
|
- Assets/GameMain/Scripts/Simulation/*
|
||||||
- 核心组件:`SimulationWorld`(`GameFrameworkComponent`)
|
- Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs
|
||||||
- 数据容器:
|
- Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs
|
||||||
- `List<EnemySimData> _enemies`
|
- Assets/GameMain/Scripts/Utility/AIUtility.cs
|
||||||
- `List<ProjectileSimData> _projectiles`
|
- Assets/Tests/Simulation/EditMode/*
|
||||||
- `List<PickupSimData> _pickups`
|
- Assets/Tests/Simulation/PlayMode/*
|
||||||
- Tick 临时缓冲:
|
|
||||||
- `List<EnemyTickWorkItem> _enemyTickWorkItems`
|
|
||||||
- `List<EnemySeparationAgent> _enemySeparationAgents`
|
|
||||||
- 索引绑定:`EntityBinding`(`EntityId <-> SimulationIndex` 双向映射)
|
|
||||||
- 生命周期同步:`SimulationWorld.EntitySync`(监听实体 Show/Hide 事件)
|
|
||||||
- 表现层回写:`SimulationWorld.Presentation`(`LateUpdate` 写回 `Transform`)
|
|
||||||
- Tick 上下文:`SimulationTickContext`(`DeltaTime`、`RealDeltaTime`、`PlayerPosition`)
|
|
||||||
|
|
||||||
## 运行时主链路(按帧)
|
当前状态:P2 Job/Burst 主体已完成,SimulationWorld 已是战斗核心调度层。
|
||||||
1. `GameEntry.InitCustomComponents()` 获取或自动挂载 `SimulationWorld`
|
|
||||||
文件:`Assets/GameMain/Scripts/Base/GameEntry.Custom.cs`
|
|
||||||
2. `ProcedureGame.OnEnter()` 清理旧 Simulation 数据
|
|
||||||
文件:`Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs`
|
|
||||||
3. `GameStateBattle.OnUpdate()` 中先执行刷怪,再执行 `SimulationWorld.Tick(...)`
|
|
||||||
文件:`Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs`
|
|
||||||
4. `SimulationWorld.Tick()` 仅在 `UseSimulationMovement == true` 时执行敌人 Tick
|
|
||||||
5. `SimulationWorld.LateUpdate()` 执行 `Presentation.OnLateUpdate()`,将仿真结果写回敌人 `Transform`
|
|
||||||
|
|
||||||
## 生命周期与数据同步设计
|
## 模块分层
|
||||||
`EntitySync` 通过事件驱动保持 Simulation 容器与实体生命周期一致:
|
SimulationWorld 使用 partial 拆分,职责如下:
|
||||||
|
|
||||||
| 事件 | 组名 | 行为 |
|
- SimulationWorld.cs
|
||||||
|-------------------------------|-------------------------|----------------------------------------------------------------|
|
- 开关、主容器、绑定、主 Tick 入口。
|
||||||
| `ShowEntitySuccessEventArgs` | `Enemy` | `RegisterEnemyLifecycle` + `UpsertEnemy` |
|
- SimulationWorld.EntitySync.cs
|
||||||
| `HideEntityCompleteEventArgs` | `Enemy` | `UnregisterEnemyLifecycle` + `RemoveEnemyByEntityId` |
|
- 实体 Show/Hide 到仿真容器的生命周期同步。
|
||||||
| `ShowEntitySuccessEventArgs` | `Drop` | `RegisterPickupLifecycle` + `UpsertPickup` |
|
- SimulationWorld.EnemyJobs.cs
|
||||||
| `HideEntityCompleteEventArgs` | `Drop` | `UnregisterPickupLifecycle` + `RemovePickupByEntityId` |
|
- 敌人移动和互斥分离的 Job/Burst 链路。
|
||||||
| `ShowEntitySuccessEventArgs` | `Bullet` / `Projectile` | `RegisterProjectileLifecycle` + `UpsertProjectile` |
|
- SimulationWorld.ProjectileJobs.cs
|
||||||
| `HideEntityCompleteEventArgs` | `Bullet` / `Projectile` | `UnregisterProjectileLifecycle` + `RemoveProjectileByEntityId` |
|
- 投射物移动、寿命、碰撞候选、主线程结算。
|
||||||
|
- SimulationWorld.JobDataChannel.cs
|
||||||
|
- Native 容器、拷贝转换、容量管理、运行时统计。
|
||||||
|
- SimulationWorld.TargetSelectionSpatialIndex.cs
|
||||||
|
- 目标选择空间索引(最近敌人查询)。
|
||||||
|
- SimulationWorld.Presentation.cs
|
||||||
|
- 表现层写回(Transform)和命中表现事件消费。
|
||||||
|
|
||||||
关键规则:
|
## 运行时执行链路
|
||||||
- 删除容器元素统一使用“末尾覆盖 + `RemoveAt(lastIndex)` + `EntityBinding.RemapIndex`”。
|
1. GameStateBattle.OnUpdate
|
||||||
- `Upsert` 语义:`EntityId` 已存在则覆盖,不存在则追加。
|
- 先执行 EnemyManager.OnUpdate
|
||||||
|
- 再执行 SimulationWorld.Tick
|
||||||
|
|
||||||
## EnemySimData 合约(当前实现)
|
2. SimulationWorld.Tick
|
||||||
文件:`Assets/GameMain/Scripts/Simulation/SimData/EnemySimData.cs`
|
- UseSimulationMovement = false:直接返回(完全回退旧链路)
|
||||||
|
- UseSimulationMovement = true 且 UseJobSimulation = false:走主线程敌人仿真
|
||||||
|
- UseSimulationMovement = true 且 UseJobSimulation = true:走 Job/Burst 总链路
|
||||||
|
|
||||||
- `EntityId`:实体唯一标识
|
3. SimulationWorld.LateUpdate
|
||||||
- `Position / Forward / Rotation`:逻辑输出与表现层写回字段
|
- 调用 Presentation.OnLateUpdate
|
||||||
- `Speed`:来自 `EnemyData.SpeedBase`
|
- 统一写回 Enemy/Projectile 表现
|
||||||
- `AttackRange`:当前固定初始化为 `1f`
|
|
||||||
- `AvoidEnemyOverlap / EnemyBodyRadius / SeparationIterations`:从 `MovementComponent` 读取
|
|
||||||
- `TargetType / State`:状态扩展预留
|
|
||||||
|
|
||||||
当前状态值(`SimulationWorld` 常量):
|
## 核心数据契约
|
||||||
- `0`:Idle
|
### 主容器
|
||||||
- `1`:Chasing
|
- List<EnemySimData> _enemies
|
||||||
- `2`:InAttackRange
|
- List<ProjectileSimData> _projectiles
|
||||||
|
- List<PickupSimData> _pickups
|
||||||
|
|
||||||
## TickEnemies 当前算法(P1.5 分阶段)
|
### 绑定关系
|
||||||
文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`
|
- EntityBinding 维护 EntityId <-> SimulationIndex 双向映射。
|
||||||
|
- 删除必须使用 swap-back:
|
||||||
|
- 尾元素覆盖删除位
|
||||||
|
- RemapIndex
|
||||||
|
- RemoveAt(last)
|
||||||
|
|
||||||
`TickEnemies` 入口保持纯数据热路径,不直接读写 `Transform`,按四阶段执行:
|
### Job 通道
|
||||||
1. `BuildInput`
|
- EnemyJobInput/Output
|
||||||
- 计算到玩家平面距离
|
- ProjectileJobInput/Output
|
||||||
- 产出 `EnemyTickWorkItem`
|
- CollisionQuery/CollisionCandidate
|
||||||
- 生成分离输入 `EnemySeparationAgent`
|
- NativeParallelMultiHashMap(互斥桶、碰撞桶、目标桶)
|
||||||
2. `Move/Separation`
|
|
||||||
- 计算追踪位移与朝向
|
|
||||||
- 通过 `EnemySeparationSolverProvider.ResolveSimulation(...)` 做互斥求解
|
|
||||||
3. `StateUpdate`
|
|
||||||
- 按距离与可追逐状态更新 `Idle/Chasing/InAttackRange`
|
|
||||||
4. `WriteBack`
|
|
||||||
- 回写 `EnemySimData`(`Position/Forward/Rotation/State`)
|
|
||||||
|
|
||||||
## 互斥求解器双通道(Legacy + Simulation)
|
统一规则:
|
||||||
文件:
|
- Allocator.Persistent 分配
|
||||||
- `Assets/GameMain/Scripts/Utility/EnemySeperator/IEnemySeparationSolver.cs`
|
- Initialize/Dispose 集中管理
|
||||||
- `Assets/GameMain/Scripts/Utility/EnemySeperator/EnemySeparationSolverProvider.cs`
|
- Clear 只清容器,不破坏生命周期
|
||||||
- `Assets/GameMain/Scripts/Utility/EnemySeperator/GridBucketEnemySeparationSolver.cs`
|
|
||||||
|
|
||||||
说明:
|
## 不可破坏的设计约束
|
||||||
- `SetSimulationAgents/ResolveSimulation`:供 Simulation 纯数据路径调用。
|
### 生命周期单入口
|
||||||
- `Register/Unregister/Resolve(Transform, ...)`:保留旧路径兼容与回滚能力。
|
仿真容器增删只能由 EntitySync 驱动。
|
||||||
- `GridBucketEnemySeparationSolver` 已加入桶列表复用池(`_bucketListPool`)以降低 GC。
|
禁止在 Enemy/Weapon/Projectile 业务代码中直接改仿真容器。
|
||||||
|
|
||||||
## 表现层回写(Presentation)规则
|
### 逻辑与表现边界
|
||||||
文件:`Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs`
|
- Simulation 只产出逻辑数据,不直接写 Transform。
|
||||||
|
- Transform 写回只能在 Presentation。
|
||||||
|
- 命中表现通过事件缓冲在主线程提交。
|
||||||
|
|
||||||
- 仅在 `UseSimulationMovement == true` 时执行
|
### 开关和回滚
|
||||||
- 遍历 `EnemyManager.Enemies`,按 `EntityId` 查找 `EnemySimData`
|
- UseSimulationMovement:总开关,支持一键回滚。
|
||||||
- 写回顺序:
|
- UseJobSimulation:Job 开关,支持 P1.5/ P2 对照。
|
||||||
- 始终写回 `position`
|
- UseBurstJobs:Burst 开关。
|
||||||
- 优先使用 `rotation`
|
|
||||||
- 若 `rotation` 无效,则回退到 `forward`
|
|
||||||
|
|
||||||
## 与旧移动系统的关系(重要)
|
### 生效时机约束(重要)
|
||||||
- `MeleeEnemy` / `RemoteEnemy` 在 `OnUpdate` 开头门控:
|
- SetUseSimulationMovement / SetUseJobSimulation 在 Battle 中会被忽略。
|
||||||
- 开启 Simulation:直接 `return`
|
- 这两个开关不支持战斗内热切换,只允许战斗外修改生效。
|
||||||
- 关闭 Simulation:走旧 `MovementComponent`
|
|
||||||
- 回滚能力来自同一构建内的 `UseSimulationMovement` A/B 开关。
|
|
||||||
|
|
||||||
## Projectile / Pickup 现状
|
## 敌人和投射物执行模型
|
||||||
- `ProjectileSimData`、`PickupSimData` 已具备容器、绑定与生命周期同步通道
|
### 敌人(Job)
|
||||||
- 当前仍未接入独立 Tick 行为,仅完成“创建/回收/索引同步”占位目标
|
固定阶段:
|
||||||
|
- BuildInput
|
||||||
|
- Move
|
||||||
|
- Separation
|
||||||
|
- Commit
|
||||||
|
|
||||||
## P1.5 实测基线(P2 输入)
|
约束:
|
||||||
基线文档:`docs/P1.5 Simulation-Supplement.md`
|
- 热路径禁止 LINQ 和托管分配
|
||||||
|
- 不读写 Transform
|
||||||
|
- 阶段必须可独立 Profile
|
||||||
|
|
||||||
关键结论:
|
### 投射物(Job)
|
||||||
- `TickEnemies GC` 在 `500/1000/1500/2000` 敌人数下均为 `0 KB`
|
包含:
|
||||||
- `GC Allocated In Frame` 从 P1 的 `29.5~109.7 KB` 降至 `2.1 KB`
|
- 移动更新
|
||||||
- `TickEnemies` 热路径耗时(四阶段合计)对比 P1 降幅约 `22.8%~26.8%`
|
- 寿命和越界回收
|
||||||
- Android 端评估以 CPU `ms` 为主,`fps` 受 60 上限影响
|
- Broad Phase 候选构建
|
||||||
|
- 主线程命中结算和回收
|
||||||
|
|
||||||
## 自动化回归(P1.5 已补)
|
## 碰撞和伤害结算规范
|
||||||
目录:
|
### Broad Phase
|
||||||
- `Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs`
|
候选由 _collisionQueryInputs + _enemyCollisionBuckets 计算。
|
||||||
- `Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs`
|
MaxTargets 必须覆盖“玩家候选 + 敌人候选”的总量。
|
||||||
|
|
||||||
覆盖点:
|
### Area Query 快照语义
|
||||||
- 敌人追踪玩家
|
- 入队时记录 SourceWasActiveAtQueryTime。
|
||||||
- 进入攻击距离后停止移动
|
- 结算按快照判定来源有效性,避免查询后状态变化导致误判。
|
||||||
- 实体移除后的索引 remap 稳定性
|
|
||||||
|
|
||||||
## 后续扩展规范(必须遵守)
|
### 主线程结算
|
||||||
1. 先扩数据,再扩行为
|
- Projectile 命中:按 ImpactData + AIUtility.CalcDamageHP。
|
||||||
先在 `SimData` 增字段,再改 `EntitySync` 初始化与 `Tick` 逻辑,最后改表现层消费。
|
- Area 命中:调用 AIUtility.PerformCollision(target, source, true)。
|
||||||
|
|
||||||
2. 保留 A/B 路径
|
### 伤害公式约束
|
||||||
任何迁移都必须可在同一构建内通过开关回退到旧路径。
|
AIUtility.CalcDamageHP:
|
||||||
|
- 闪避使用 dodgeStat.Value(加算语义),不使用 Percent。
|
||||||
|
- 攻击: (attack + AttackStat.Value) * AttackStat.Percent
|
||||||
|
- 防御: (damage - DefenseStat.Value) / DefenseStat.Percent
|
||||||
|
- 最终伤害最小值为 1。
|
||||||
|
|
||||||
3. 生命周期只走 EntitySync
|
## 已修复问题(纳入长期约束)
|
||||||
禁止在敌人业务代码中手动改写 Simulation 容器,避免双写导致索引错乱。
|
1. 闪避语义修正:使用 Value 而非 Percent。
|
||||||
|
2. UseSimulationMovement / UseJobSimulation 战斗内禁止热切换。
|
||||||
|
3. MaxTargets 统计覆盖玩家候选,避免超额候选。
|
||||||
|
4. Area 查询引入来源活跃快照并按快照结算。
|
||||||
|
|
||||||
4. 维持“逻辑输出 / 表现消费”边界
|
后续改动若触碰这些路径,必须保持行为不回退。
|
||||||
Simulation 只产出逻辑结果,不直接触发 UI、特效、音频事件。
|
|
||||||
|
|
||||||
5. 删除策略统一用 swap-back
|
## 扩展开发 SOP
|
||||||
所有 Simulation 容器删除都必须 remap 索引,严禁 `RemoveAt(i)` 直接删中间项。
|
### Step 0:定义模式和回滚
|
||||||
|
- 明确功能在哪条路径生效(Simulation / Job / Burst)。
|
||||||
|
- 明确开关关闭后的回退行为。
|
||||||
|
|
||||||
6. 热路径禁用托管分配
|
### Step 1:扩数据
|
||||||
`TickEnemies`、互斥求解、阶段化循环里禁止 LINQ/临时集合扩张。
|
- 先改 SimData 和 JobData。
|
||||||
|
- 再改 CreateInitialSimData 与转换函数。
|
||||||
|
|
||||||
## P2 前的已知技术债
|
### Step 2:接生命周期
|
||||||
- `AttackRange` 目前固定值 `1f`,尚未由配置化数值驱动
|
- 只在 EntitySync 增加注册/反注册。
|
||||||
- `EnemySimData.TargetType/State` 语义仍偏轻量,未形成完整状态机合约
|
- 保持 group 到容器映射清晰。
|
||||||
- Projectile/Pickup 尚未迁移真实 Tick 行为
|
|
||||||
|
|
||||||
## 提交前检查清单
|
### Step 3:接执行阶段
|
||||||
- 是否保持了 `UseSimulationMovement` 关闭时行为不变
|
- 优先放入现有阶段(Build/Schedule/Commit)。
|
||||||
- 是否保持了 `EntityId <-> SimulationIndex` 一致性(含移除 remap)
|
- 新阶段必须补 ProfilerMarker。
|
||||||
- 是否避免在 Tick 热路径引入新 GC
|
|
||||||
- 是否将新字段接入了 `EntitySync -> Tick -> Presentation` 全链路
|
### Step 4:接结算和表现
|
||||||
- 是否补充了最小回归验证(至少 Battle 循环、敌人移除、索引稳定性)
|
- 逻辑结算收口到主线程。
|
||||||
- 是否同步更新本 Skill 文档与 `docs/P1.5 Simulation-Supplement.md`
|
- 表现写回放在 Presentation。
|
||||||
|
|
||||||
|
### Step 5:补测试
|
||||||
|
EditMode 和 PlayMode 同步补回归,至少覆盖:
|
||||||
|
- 行为正确性
|
||||||
|
- 开关路径
|
||||||
|
- 索引稳定性
|
||||||
|
- 新增边界条件
|
||||||
|
|
||||||
|
### Step 6:更新文档
|
||||||
|
- 更新本文件。
|
||||||
|
- 需要性能结论时,同步更新 docs/P2 Job System + Burst 落地.md。
|
||||||
|
|
||||||
|
## 测试命令
|
||||||
|
- PlayMode
|
||||||
|
- Unity -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log
|
||||||
|
- EditMode
|
||||||
|
- Unity -batchmode -nographics -projectPath . -runTests -testPlatform EditMode -testResults Logs/editmode-test-results.xml -logFile Logs/editmode-tests.log
|
||||||
|
|
||||||
|
## 关键回归用例(必须保留)
|
||||||
|
- TickEnemies_MatchesOutput_WhenBurstJobsToggled
|
||||||
|
- TryGetNearestEnemyEntityId_SelectsNearestBucketCandidate_WhenJobSimulationEnabled
|
||||||
|
- TickProjectiles_LimitsCandidatesToMaxTargets_IncludingPlayerCandidate
|
||||||
|
- SetUseSimulationAndJob_AreIgnored_WhenBattleStateIsActive
|
||||||
|
- EnqueueAreaQuery_CapturesInactiveSourceSnapshot_WhenSourceEntityUnavailable
|
||||||
|
|
||||||
|
对应文件:
|
||||||
|
- Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs
|
||||||
|
- Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs
|
||||||
|
|
||||||
|
## P2 验收口径
|
||||||
|
- 3k 敌人下 Main Thread 明显下降(目标 >= 30%)。
|
||||||
|
- 战斗持续帧 GC Alloc 接近 0。
|
||||||
|
- Battle -> LevelUp -> Shop -> Battle 循环稳定。
|
||||||
|
|
||||||
|
## 提交前门禁清单
|
||||||
|
- 关闭 UseSimulationMovement 是否完全回退旧链路。
|
||||||
|
- EntityBinding 是否保持双向一致,删除后是否正确 remap。
|
||||||
|
- Job 容器是否无泄漏(Persistent 都可 Dispose)。
|
||||||
|
- 是否引入新热路径 GC(LINQ、临时集合、装箱)。
|
||||||
|
- 是否破坏“战斗内不热切换 UseSimulationMovement/UseJobSimulation”。
|
||||||
|
- 是否同步更新测试和本设计文档。
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue