Checkpoint 5 & Checkpoint 6
This commit is contained in:
parent
5fb7ea499f
commit
f0abf78128
|
|
@ -1,5 +1,6 @@
|
||||||
# 敌人基础属性表
|
# 敌人基础属性表
|
||||||
# Id EntityTypeId MaxHealth HpAddPerLevel Speed CoinDrop ExpDrop DropPercent
|
# Id EntityTypeId MaxHealth HpAddPerLevel AttackDamage AttackCooldown AttackRange Speed CoinDrop ExpDrop DropPercent Params
|
||||||
# int int int int float int int float
|
# int int int int int float float float int int float string
|
||||||
# 敌人编号 策划备注 敌人实体编号 最大生命 每关卡增加生命 移动速度 金币掉落 经验掉落 掉落概率
|
# 敌人编号 策划备注 敌人实体编号 最大生命 每关卡增加生命 基础伤害 攻击间隔 攻击范围 移动速度 金币掉落 经验掉落 掉落概率 额外参数
|
||||||
1 近战敌人 101 50 50 3 5 1 0.3
|
1 近战敌人 101 50 50 1 1 1.5 3 5 1 0.3 []
|
||||||
|
2 远程敌人 102 40 40 1 2 8 2.5 4 2 0.2 []
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
# Id AssetName
|
# Id AssetName
|
||||||
# int string
|
# int string
|
||||||
# 实体编号 策划备注 资源名称
|
# 实体编号 策划备注 资源名称
|
||||||
|
11 跟随相机 FollowCamera
|
||||||
1001 测试玩家 Player
|
1001 测试玩家 Player
|
||||||
101 近战敌人 MeleeEnemy
|
101 近战敌人 MeleeEnemy
|
||||||
102 远程敌人 RemoteEnemy
|
102 远程敌人 RemoteEnemy
|
||||||
11 跟随相机 FollowCamera
|
|
||||||
201 武器小刀 WeaponKnife
|
201 武器小刀 WeaponKnife
|
||||||
202 武器手枪 WeaponHandgun
|
202 武器手枪 WeaponHandgun
|
||||||
203 武器斧头 WeaponSlash
|
203 武器斧头 WeaponSlash
|
||||||
|
204 武器闪电 WeaponLightning
|
||||||
10001 金币实体 CoinEntity
|
10001 金币实体 CoinEntity
|
||||||
10002 经验实体 ExpEntity
|
10002 经验实体 ExpEntity
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,4 @@
|
||||||
121 Prop 119
|
121 Prop 119
|
||||||
122 Prop 120
|
122 Prop 120
|
||||||
123 Weapon 3
|
123 Weapon 3
|
||||||
|
124 Weapon 4
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# Id EnemyTypes EntityCounts Interval Duration
|
# Id EnemyTypes EntityCounts Interval Duration
|
||||||
# int int[] int[] float[] int
|
# int int[] int[] float[] int
|
||||||
# 关卡号 策划备注 敌人类型 每次出怪数量 每次出怪间隔 关卡时间
|
# 关卡号 策划备注 敌人类型 每次出怪数量 每次出怪间隔 关卡时间
|
||||||
1 第一关 [1] [5] [2] 60
|
1 第一关 [1,2] [5,2] [4,5] 60
|
||||||
2 第二关 [1] [10] [3] 60
|
2 第二关 [1] [10] [3] 60
|
||||||
3 第三关 [1] [10] [3] 60
|
3 第三关 [1] [10] [3] 60
|
||||||
4 第四关 [1] [10] [3] 60
|
4 第四关 [1] [10] [3] 60
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,4 @@
|
||||||
1 玩家武器 201 小刀 Almighty_Icon White 120 0.05 100 1.5 5 10000 [hitRadius:2] []
|
1 玩家武器 201 小刀 Almighty_Icon White 120 0.05 100 1.5 5 10000 [hitRadius:2] []
|
||||||
2 202 手枪 Almighty_Icon White 130 0.05 120 1 15 10000 [] []
|
2 202 手枪 Almighty_Icon White 130 0.05 120 1 15 10000 [] []
|
||||||
3 203 斧头 Almighty_Icon White 100 0.1 150 2 5 10000 [SectorAngle:120] []
|
3 203 斧头 Almighty_Icon White 100 0.1 150 2 5 10000 [SectorAngle:120] []
|
||||||
|
4 204 闪电 Almighty_Icon White 150 0.08 80 3 12 10000 [hitRadius:3;HoverHeight:10] []
|
||||||
|
|
|
||||||
|
|
@ -150,13 +150,13 @@ Transform:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 5383497626468778460}
|
m_GameObject: {fileID: 5383497626468778460}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
|
m_LocalRotation: {x: 0.5, y: 0, z: 0, w: 0.8660254}
|
||||||
m_LocalPosition: {x: 0, y: 15, z: 0}
|
m_LocalPosition: {x: 0, y: 15, z: -10}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 9112716898534404901}
|
m_Father: {fileID: 9112716898534404901}
|
||||||
m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 60, y: 0, z: 0}
|
||||||
--- !u!20 &4064848608618185461
|
--- !u!20 &4064848608618185461
|
||||||
Camera:
|
Camera:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -189,9 +189,9 @@ Camera:
|
||||||
width: 1
|
width: 1
|
||||||
height: 1
|
height: 1
|
||||||
near clip plane: 0.3
|
near clip plane: 0.3
|
||||||
far clip plane: 100
|
far clip plane: 200
|
||||||
field of view: 80
|
field of view: 80
|
||||||
orthographic: 1
|
orthographic: 0
|
||||||
orthographic size: 15
|
orthographic size: 15
|
||||||
m_Depth: 0
|
m_Depth: 0
|
||||||
m_CullingMask:
|
m_CullingMask:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ GameObject:
|
||||||
- component: {fileID: 1932268889601128120}
|
- component: {fileID: 1932268889601128120}
|
||||||
- component: {fileID: 557030043145096197}
|
- component: {fileID: 557030043145096197}
|
||||||
- component: {fileID: 6353753365317756414}
|
- component: {fileID: 6353753365317756414}
|
||||||
m_Layer: 7
|
m_Layer: 8
|
||||||
m_Name: RemoteEnemy
|
m_Name: RemoteEnemy
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
|
|
@ -124,6 +124,9 @@ MonoBehaviour:
|
||||||
_isMoving: 0
|
_isMoving: 0
|
||||||
_direction: {x: 0, y: 0, z: 0}
|
_direction: {x: 0, y: 0, z: 0}
|
||||||
_cachedTransform: {fileID: 0}
|
_cachedTransform: {fileID: 0}
|
||||||
|
_avoidEnemyOverlap: 0
|
||||||
|
_enemyBodyRadius: 0.45
|
||||||
|
_separationIterations: 2
|
||||||
_speedBase: 0
|
_speedBase: 0
|
||||||
--- !u!114 &6353753365317756414
|
--- !u!114 &6353753365317756414
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &3331174537915643484
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 5009597865007941286}
|
||||||
|
- component: {fileID: 2357377495634329419}
|
||||||
|
- component: {fileID: 5508018305229695465}
|
||||||
|
- component: {fileID: 6560911649159431343}
|
||||||
|
m_Layer: 11
|
||||||
|
m_Name: Capsule
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &5009597865007941286
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3331174537915643484}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 0.4, y: 0.5, z: 0.4}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 1074967493716089666}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!33 &2357377495634329419
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3331174537915643484}
|
||||||
|
m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!23 &5508018305229695465
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3331174537915643484}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_CastShadows: 1
|
||||||
|
m_ReceiveShadows: 1
|
||||||
|
m_DynamicOccludee: 1
|
||||||
|
m_StaticShadowCaster: 0
|
||||||
|
m_MotionVectors: 1
|
||||||
|
m_LightProbeUsage: 1
|
||||||
|
m_ReflectionProbeUsage: 1
|
||||||
|
m_RayTracingMode: 2
|
||||||
|
m_RayTraceProcedural: 0
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_RendererPriority: 0
|
||||||
|
m_Materials:
|
||||||
|
- {fileID: 2100000, guid: 429ed03405bf8854eab46552b7470ac0, type: 2}
|
||||||
|
m_StaticBatchInfo:
|
||||||
|
firstSubMesh: 0
|
||||||
|
subMeshCount: 0
|
||||||
|
m_StaticBatchRoot: {fileID: 0}
|
||||||
|
m_ProbeAnchor: {fileID: 0}
|
||||||
|
m_LightProbeVolumeOverride: {fileID: 0}
|
||||||
|
m_ScaleInLightmap: 1
|
||||||
|
m_ReceiveGI: 1
|
||||||
|
m_PreserveUVs: 0
|
||||||
|
m_IgnoreNormalsForChartDetection: 0
|
||||||
|
m_ImportantGI: 0
|
||||||
|
m_StitchLightmapSeams: 1
|
||||||
|
m_SelectedEditorRenderState: 3
|
||||||
|
m_MinimumChartSize: 4
|
||||||
|
m_AutoUVMaxDistance: 0.5
|
||||||
|
m_AutoUVMaxAngle: 89
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_SortingLayerID: 0
|
||||||
|
m_SortingLayer: 0
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!136 &6560911649159431343
|
||||||
|
CapsuleCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3331174537915643484}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 0
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Radius: 0.5
|
||||||
|
m_Height: 2
|
||||||
|
m_Direction: 1
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &4668848878531932975
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1074967493716089666}
|
||||||
|
m_Layer: 11
|
||||||
|
m_Name: WeaponLightning
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1074967493716089666
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 4668848878531932975}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 5009597865007941286}
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9d193ac5b4294e0e9ba6e867320944b7
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -164,6 +164,22 @@ namespace CustomComponent
|
||||||
GUILayout.Label($"Battle Time: {enemyManager.ElapsedBattleTime:F1}s / {enemyManager.BattleDuration:F1}s");
|
GUILayout.Label($"Battle Time: {enemyManager.ElapsedBattleTime:F1}s / {enemyManager.BattleDuration:F1}s");
|
||||||
GUILayout.Label($"Enemy Count: {enemyManager.CurrentEnemyCount}");
|
GUILayout.Label($"Enemy Count: {enemyManager.CurrentEnemyCount}");
|
||||||
|
|
||||||
|
Simulation.SimulationWorld simulationWorld = GameEntry.SimulationWorld;
|
||||||
|
if (simulationWorld != null)
|
||||||
|
{
|
||||||
|
GUILayout.Space(4f);
|
||||||
|
GUILayout.Label(
|
||||||
|
$"Sim Switch: Move={(simulationWorld.UseSimulationMovement ? "On" : "Off")} Job={(simulationWorld.UseJobSimulation ? "On" : "Off")} Burst={(simulationWorld.UseBurstJobs ? "On" : "Off")}");
|
||||||
|
GUILayout.Label(
|
||||||
|
$"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})");
|
||||||
|
GUILayout.Label(
|
||||||
|
$"Collision Candidates: total {simulationWorld.LastCollisionCandidateCount} (Projectile {simulationWorld.LastProjectileCollisionCandidateCount} / Area {simulationWorld.LastAreaCollisionCandidateCount})");
|
||||||
|
GUILayout.Label(
|
||||||
|
$"Area Resolve: hits {simulationWorld.LastResolvedAreaHitCount}");
|
||||||
|
GUILayout.Label(
|
||||||
|
$"Broad Phase: cell {simulationWorld.LastCollisionCellSize:F2}, hasEnemyTargets {(simulationWorld.LastCollisionHasEnemyTargets ? "Yes" : "No")}");
|
||||||
|
}
|
||||||
|
|
||||||
GUILayout.BeginHorizontal();
|
GUILayout.BeginHorizontal();
|
||||||
GUILayout.Label("Rate", GUILayout.Width(52f));
|
GUILayout.Label("Rate", GUILayout.Width(52f));
|
||||||
string rateText = GUILayout.TextField(_spawnRateScaleInput.ToString("F2"), GUILayout.Width(60f));
|
string rateText = GUILayout.TextField(_spawnRateScaleInput.ToString("F2"), GUILayout.Width(60f));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
namespace DataTable
|
namespace DataTable
|
||||||
|
|
@ -14,6 +16,12 @@ namespace DataTable
|
||||||
|
|
||||||
public int HpAddPerLevel { get; private set; }
|
public int HpAddPerLevel { get; private set; }
|
||||||
|
|
||||||
|
public int AttackDamage { get; private set; }
|
||||||
|
|
||||||
|
public float AttackCooldown { get; private set; }
|
||||||
|
|
||||||
|
public float AttackRange { get; private set; }
|
||||||
|
|
||||||
public float Speed { get; private set; }
|
public float Speed { get; private set; }
|
||||||
|
|
||||||
public int DropCoin { get; private set; }
|
public int DropCoin { get; private set; }
|
||||||
|
|
@ -22,6 +30,8 @@ namespace DataTable
|
||||||
|
|
||||||
public float DropPercent { get; private set; }
|
public float DropPercent { get; private set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string> Params { get; private set; }
|
||||||
|
|
||||||
public override bool ParseDataRow(string dataRowString, object userData)
|
public override bool ParseDataRow(string dataRowString, object userData)
|
||||||
{
|
{
|
||||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||||
|
|
@ -33,12 +43,47 @@ namespace DataTable
|
||||||
EntityTypeId = int.Parse(columnStrings[index++]);
|
EntityTypeId = int.Parse(columnStrings[index++]);
|
||||||
MaxHealth = int.Parse(columnStrings[index++]);
|
MaxHealth = int.Parse(columnStrings[index++]);
|
||||||
HpAddPerLevel = int.Parse(columnStrings[index++]);
|
HpAddPerLevel = int.Parse(columnStrings[index++]);
|
||||||
|
AttackDamage = int.Parse(columnStrings[index++]);
|
||||||
|
AttackCooldown = float.Parse(columnStrings[index++]);
|
||||||
|
AttackRange = float.Parse(columnStrings[index++]);
|
||||||
Speed = float.Parse(columnStrings[index++]);
|
Speed = float.Parse(columnStrings[index++]);
|
||||||
DropCoin = int.Parse(columnStrings[index++]);
|
DropCoin = int.Parse(columnStrings[index++]);
|
||||||
DropExp = int.Parse(columnStrings[index++]);
|
DropExp = int.Parse(columnStrings[index++]);
|
||||||
DropPercent = float.Parse(columnStrings[index++]);
|
DropPercent = float.Parse(columnStrings[index++]);
|
||||||
|
Params = DeserializeParams(columnStrings[index++]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解参数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawParams"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
private Dictionary<string, string> DeserializeParams(string rawParams)
|
||||||
|
{
|
||||||
|
if (!rawParams.StartsWith('[') || !rawParams.EndsWith(']'))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Input must be enclosed in square brackets.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var dict = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(rawParams)) return dict;
|
||||||
|
|
||||||
|
string[] items = rawParams.Substring(1, rawParams.Length - 2).Split(";");
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
string entry = item.Trim();
|
||||||
|
if (string.IsNullOrEmpty(entry)) continue;
|
||||||
|
|
||||||
|
string[] pair = entry.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (pair.Length != 2) continue;
|
||||||
|
dict.Add(pair[0].ToLower(), pair[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +109,12 @@ namespace DataTable
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解参数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawParams"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
private Dictionary<string, string> DeserializeParams(string rawParams)
|
private Dictionary<string, string> DeserializeParams(string rawParams)
|
||||||
{
|
{
|
||||||
if (!rawParams.StartsWith('[') || !rawParams.EndsWith(']'))
|
if (!rawParams.StartsWith('[') || !rawParams.EndsWith(']'))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ namespace CustomDebugger
|
||||||
public static readonly ProfilerMarker TickEnemies_MoveSeparation = new ProfilerMarker("TickEnemies.MoveSeparation");
|
public static readonly ProfilerMarker TickEnemies_MoveSeparation = new ProfilerMarker("TickEnemies.MoveSeparation");
|
||||||
public static readonly ProfilerMarker TickEnemies_StateUpdate = new ProfilerMarker("TickEnemies.StateUpdate");
|
public static readonly ProfilerMarker TickEnemies_StateUpdate = new ProfilerMarker("TickEnemies.StateUpdate");
|
||||||
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
public static readonly ProfilerMarker TickEnemies_WriteBack = new ProfilerMarker("TickEnemies.WriteBack");
|
||||||
|
public static readonly ProfilerMarker Collision_BuildQueries = new("Collision.BuildQueries");
|
||||||
|
public static readonly ProfilerMarker Collision_BuildBuckets = new("Collision.BuildBuckets");
|
||||||
|
public static readonly ProfilerMarker Collision_QueryCandidates = new("Collision.QueryCandidates");
|
||||||
|
public static readonly ProfilerMarker Collision_ResolveProjectile = new("Collision.ResolveProjectile");
|
||||||
|
public static readonly ProfilerMarker Collision_ResolveArea = new("Collision.ResolveArea");
|
||||||
public static readonly ProfilerMarker TargetSelection_BuildBuckets = new("TargetSelection.BuildBuckets");
|
public static readonly ProfilerMarker TargetSelection_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 ProfilerMarker("Movement_Update");
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@ namespace Definition.Enum
|
||||||
WeaponKnife = 1,
|
WeaponKnife = 1,
|
||||||
WeaponHandgun = 2,
|
WeaponHandgun = 2,
|
||||||
WeaponSlash = 3,
|
WeaponSlash = 3,
|
||||||
|
WeaponLightning = 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using DataTable;
|
using DataTable;
|
||||||
using Definition.Enum;
|
using Definition.Enum;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -8,17 +9,7 @@ namespace Entity.EntityData
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class EnemyData : TargetableObjectData
|
public class EnemyData : TargetableObjectData
|
||||||
{
|
{
|
||||||
[SerializeField] private EnemyType _enemyType;
|
[SerializeField] private DREnemy _drEnemy;
|
||||||
|
|
||||||
[SerializeField] private int _entityTypeId;
|
|
||||||
|
|
||||||
[SerializeField] private float _speedBase = 0;
|
|
||||||
|
|
||||||
[SerializeField] private int _dropCoin = 0;
|
|
||||||
|
|
||||||
[SerializeField] private int _dropExp = 0;
|
|
||||||
|
|
||||||
[SerializeField] private float _dropPercent = 0;
|
|
||||||
|
|
||||||
public EnemyData(int entityId, EnemyType enemyType, int level) : base(
|
public EnemyData(int entityId, EnemyType enemyType, int level) : base(
|
||||||
entityId, (int)enemyType, CampType.Enemy)
|
entityId, (int)enemyType, CampType.Enemy)
|
||||||
|
|
@ -29,30 +20,46 @@ namespace Entity.EntityData
|
||||||
{
|
{
|
||||||
throw new Exception($"Enemy data table row is missing, EnemyType='{enemyType}'.");
|
throw new Exception($"Enemy data table row is missing, EnemyType='{enemyType}'.");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_drEnemy = enemyRow;
|
||||||
|
}
|
||||||
|
|
||||||
int effectiveLevel = Mathf.Max(1, level);
|
int effectiveLevel = Mathf.Max(1, level);
|
||||||
|
|
||||||
_enemyType = enemyType;
|
|
||||||
_entityTypeId = enemyRow.EntityTypeId;
|
|
||||||
MaxHealthBase = enemyRow.MaxHealth + enemyRow.HpAddPerLevel * (effectiveLevel - 1);
|
MaxHealthBase = enemyRow.MaxHealth + enemyRow.HpAddPerLevel * (effectiveLevel - 1);
|
||||||
_speedBase = enemyRow.Speed;
|
|
||||||
_dropCoin = enemyRow.DropCoin;
|
|
||||||
_dropExp = enemyRow.DropExp;
|
|
||||||
_dropPercent = enemyRow.DropPercent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EnemyType EnemyType => _enemyType;
|
public EnemyType EnemyType => (EnemyType)_drEnemy.Id;
|
||||||
|
|
||||||
public int EntityTypeId => _entityTypeId;
|
public int EntityTypeId => _drEnemy.EntityTypeId;
|
||||||
|
|
||||||
public override int MaxHealthBase { get; }
|
public override int MaxHealthBase { get; }
|
||||||
|
|
||||||
public float SpeedBase => _speedBase;
|
public int AttackDamage => _drEnemy.AttackDamage;
|
||||||
|
|
||||||
public int DropCoin => _dropCoin;
|
public float AttackCooldown => _drEnemy.AttackCooldown;
|
||||||
|
|
||||||
public int DropExp => _dropExp;
|
public float AttackRange => _drEnemy.AttackRange;
|
||||||
|
|
||||||
public float DropPercent => _dropPercent;
|
public float SpeedBase => _drEnemy.Speed;
|
||||||
|
|
||||||
|
public int DropCoin => _drEnemy.DropCoin;
|
||||||
|
|
||||||
|
public int DropExp => _drEnemy.DropExp;
|
||||||
|
|
||||||
|
public float DropPercent => _drEnemy.DropPercent;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> Params => _drEnemy.Params;
|
||||||
|
|
||||||
|
public bool TryGetParam(string key, out string value)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
if (string.IsNullOrEmpty(key) || _drEnemy?.Params == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _drEnemy.Params.TryGetValue(key.ToLower(), out value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using Definition.Enum;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Entity.EntityData
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class EnemyProjectileData : EntityDataBase
|
||||||
|
{
|
||||||
|
public EnemyProjectileData(int entityId, int ownerEntityId, CampType ownerCamp, int attackDamage,
|
||||||
|
float speed, float lifeTime, Vector3 direction)
|
||||||
|
: base(entityId, 0)
|
||||||
|
{
|
||||||
|
OwnerEntityId = ownerEntityId;
|
||||||
|
OwnerCamp = ownerCamp;
|
||||||
|
AttackDamage = attackDamage;
|
||||||
|
Speed = speed;
|
||||||
|
LifeTime = lifeTime;
|
||||||
|
Direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int OwnerEntityId { get; }
|
||||||
|
public CampType OwnerCamp { get; }
|
||||||
|
public int AttackDamage { get; }
|
||||||
|
public float Speed { get; }
|
||||||
|
public float LifeTime { get; }
|
||||||
|
public Vector3 Direction { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 52b38f83ab6c4029803d40c189db47c7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -28,14 +28,15 @@ namespace Entity.EntityData
|
||||||
|
|
||||||
public WeaponType WeaponType => (WeaponType)_drWeapon.Id;
|
public WeaponType WeaponType => (WeaponType)_drWeapon.Id;
|
||||||
|
|
||||||
public string GetParamsString(string paramsName)
|
public bool TryGetParam(string key, out string value)
|
||||||
{
|
{
|
||||||
if (!Params.TryGetValue(paramsName.ToLower(), out var value))
|
value = null;
|
||||||
|
if (string.IsNullOrEmpty(key) || Params == null)
|
||||||
{
|
{
|
||||||
throw new Exception($"Parameter '{paramsName}' not found.");
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return Params.TryGetValue(key.ToLower(), out value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
using Definition.Enum;
|
||||||
|
|
||||||
|
namespace Entity.EntityData
|
||||||
|
{
|
||||||
|
public class WeaponLightningData : WeaponData
|
||||||
|
{
|
||||||
|
public WeaponLightningData(int entityId, int ownerId, CampType ownerCamp)
|
||||||
|
: base(entityId, WeaponType.WeaponLightning, ownerId, ownerCamp)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 25b0006918fd46959c7f6b8ec1bbc8ab
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -7,6 +7,7 @@ public abstract class EnemyBase : TargetableObject
|
||||||
protected Transform _target;
|
protected Transform _target;
|
||||||
|
|
||||||
public abstract override ImpactData GetImpactData();
|
public abstract override ImpactData GetImpactData();
|
||||||
|
public virtual float AttackRange => 1f;
|
||||||
|
|
||||||
public virtual void SetTarget(Transform target) => _target = target;
|
public virtual void SetTarget(Transform target) => _target = target;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
using Definition.DataStruct;
|
||||||
|
using Definition.Enum;
|
||||||
|
using Entity.EntityData;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
|
namespace Entity
|
||||||
|
{
|
||||||
|
public class EnemyProjectile : EntityBase
|
||||||
|
{
|
||||||
|
private EnemyProjectileData _projectileData;
|
||||||
|
private Vector3 _direction = Vector3.forward;
|
||||||
|
private float _elapsedTime;
|
||||||
|
private bool _isActive;
|
||||||
|
private bool _isSimulationDriven;
|
||||||
|
private ImpactData _impactData;
|
||||||
|
private Collider[] _cachedColliders;
|
||||||
|
|
||||||
|
public bool IsActive => _isActive;
|
||||||
|
public ImpactData GetImpactData() => _impactData;
|
||||||
|
|
||||||
|
protected override void OnShow(object userData)
|
||||||
|
{
|
||||||
|
base.OnShow(userData);
|
||||||
|
|
||||||
|
_projectileData = userData as EnemyProjectileData;
|
||||||
|
if (_projectileData == null)
|
||||||
|
{
|
||||||
|
Log.Error("Enemy projectile data is invalid.");
|
||||||
|
_isActive = false;
|
||||||
|
GameEntry.Entity.HideEntity(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isActive = true;
|
||||||
|
_elapsedTime = 0f;
|
||||||
|
_impactData = new ImpactData(_projectileData.OwnerCamp, _projectileData.AttackDamage);
|
||||||
|
|
||||||
|
_direction = _projectileData.Direction;
|
||||||
|
_direction.y = 0f;
|
||||||
|
if (_direction.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
_direction = CachedTransform.forward;
|
||||||
|
_direction.y = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_direction.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
_direction = Vector3.forward;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_direction.Normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedTransform.rotation = Quaternion.LookRotation(_direction, Vector3.up);
|
||||||
|
|
||||||
|
if (_projectileData.OwnerCamp == CampType.Player)
|
||||||
|
{
|
||||||
|
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
|
||||||
|
}
|
||||||
|
else if (_projectileData.OwnerCamp == CampType.Enemy)
|
||||||
|
{
|
||||||
|
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
|
||||||
|
}
|
||||||
|
|
||||||
|
_isSimulationDriven = IsDrivenBySimulationWorld();
|
||||||
|
SetColliderEnabled(!_isSimulationDriven);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
|
{
|
||||||
|
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||||
|
|
||||||
|
if (!_isActive || _projectileData == null) return;
|
||||||
|
|
||||||
|
bool isSimulationDriven = IsDrivenBySimulationWorld();
|
||||||
|
if (isSimulationDriven != _isSimulationDriven)
|
||||||
|
{
|
||||||
|
_isSimulationDriven = isSimulationDriven;
|
||||||
|
SetColliderEnabled(!_isSimulationDriven);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isSimulationDriven) return;
|
||||||
|
|
||||||
|
if (_projectileData.Speed > 0f)
|
||||||
|
{
|
||||||
|
CachedTransform.position += _direction * (_projectileData.Speed * elapseSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
_elapsedTime += elapseSeconds;
|
||||||
|
if (_projectileData.LifeTime > 0f && _elapsedTime >= _projectileData.LifeTime)
|
||||||
|
{
|
||||||
|
Expire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHide(bool isShutdown, object userData)
|
||||||
|
{
|
||||||
|
_isActive = false;
|
||||||
|
_projectileData = null;
|
||||||
|
_elapsedTime = 0f;
|
||||||
|
_isSimulationDriven = false;
|
||||||
|
_impactData = default;
|
||||||
|
_direction = Vector3.forward;
|
||||||
|
|
||||||
|
base.OnHide(isShutdown, userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Expire()
|
||||||
|
{
|
||||||
|
if (!_isActive) return;
|
||||||
|
_isActive = false;
|
||||||
|
GameEntry.Entity.HideEntity(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDrivenBySimulationWorld()
|
||||||
|
{
|
||||||
|
var simulationWorld = GameEntry.SimulationWorld;
|
||||||
|
return simulationWorld != null && simulationWorld.UseSimulationMovement && simulationWorld.UseJobSimulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetColliderEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
_cachedColliders ??= GetComponentsInChildren<Collider>(true);
|
||||||
|
if (_cachedColliders == null) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < _cachedColliders.Length; i++)
|
||||||
|
{
|
||||||
|
Collider collider = _cachedColliders[i];
|
||||||
|
if (collider == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
collider.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 22624f81b9364c8681b32d993f5e618f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
using Components;
|
using Components;
|
||||||
|
using CustomUtility;
|
||||||
using Definition.DataStruct;
|
using Definition.DataStruct;
|
||||||
|
using Definition.Enum;
|
||||||
using Entity.EntityData;
|
using Entity.EntityData;
|
||||||
using Entity.Weapon;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
|
|
@ -9,17 +10,33 @@ namespace Entity
|
||||||
{
|
{
|
||||||
public class MeleeEnemy : EnemyBase
|
public class MeleeEnemy : EnemyBase
|
||||||
{
|
{
|
||||||
|
private enum AttackStateType
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Check_OutRange,
|
||||||
|
Check_InRange,
|
||||||
|
Attack
|
||||||
|
}
|
||||||
|
|
||||||
private MovementComponent _movementComponent;
|
private MovementComponent _movementComponent;
|
||||||
|
|
||||||
private float _attackRange = 1f;
|
private float _attackRange = 1f;
|
||||||
private float _attackRangeSquared;
|
private float _attackCooldown = 1f;
|
||||||
|
private int _attackDamage = 1;
|
||||||
|
|
||||||
|
private float _sqrAttackRange;
|
||||||
|
private float _currAttackTimer;
|
||||||
|
|
||||||
|
private AttackStateType _attackState = AttackStateType.Idle;
|
||||||
private EnemyData _meleeEnemyData;
|
private EnemyData _meleeEnemyData;
|
||||||
private WeaponBase _weapon;
|
private TargetableObject _targetableTarget;
|
||||||
|
|
||||||
protected override TargetableObjectData _targetableObjectData => _meleeEnemyData;
|
protected override TargetableObjectData _targetableObjectData => _meleeEnemyData;
|
||||||
|
public override float AttackRange => _attackRange;
|
||||||
|
|
||||||
public override ImpactData GetImpactData()
|
public override ImpactData GetImpactData()
|
||||||
{
|
{
|
||||||
return new ImpactData(_meleeEnemyData.Camp, 0);
|
return new ImpactData(_meleeEnemyData.Camp, _attackDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region FSM
|
#region FSM
|
||||||
|
|
@ -42,44 +59,35 @@ namespace Entity
|
||||||
_healthComponent.OnInit(enemyData.MaxHealthBase);
|
_healthComponent.OnInit(enemyData.MaxHealthBase);
|
||||||
_movementComponent.OnInit(_meleeEnemyData.SpeedBase, this.CachedTransform, null, true);
|
_movementComponent.OnInit(_meleeEnemyData.SpeedBase, this.CachedTransform, null, true);
|
||||||
_movementComponent.SetMove(true);
|
_movementComponent.SetMove(true);
|
||||||
_attackRangeSquared = _attackRange * _attackRange;
|
|
||||||
|
_attackRange = Mathf.Max(0.1f, _meleeEnemyData.AttackRange);
|
||||||
|
_attackCooldown = Mathf.Max(0.01f, _meleeEnemyData.AttackCooldown);
|
||||||
|
_attackDamage = Mathf.Max(1, _meleeEnemyData.AttackDamage);
|
||||||
|
_sqrAttackRange = _attackRange * _attackRange;
|
||||||
|
|
||||||
|
_currAttackTimer = 0f;
|
||||||
|
_attackState = AttackStateType.Idle;
|
||||||
|
_targetableTarget = null;
|
||||||
|
|
||||||
this.CachedTransform.position = enemyData.Position;
|
this.CachedTransform.position = enemyData.Position;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Error($"Invalid data type. Data type: {userData?.GetType()}");
|
Log.Error($"Invalid data type. Data type: {userData?.GetType()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
{
|
{
|
||||||
|
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||||
|
|
||||||
|
UpdateAttackState(elapseSeconds);
|
||||||
|
|
||||||
if (IsSimulationMovementEnabled())
|
if (IsSimulationMovementEnabled())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
|
||||||
|
|
||||||
if (_target == null)
|
|
||||||
{
|
|
||||||
_movementComponent.SetMove(false);
|
|
||||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float distanceSquared = (this.CachedTransform.position - _target.position).sqrMagnitude;
|
|
||||||
if (distanceSquared < _attackRangeSquared)
|
|
||||||
{
|
|
||||||
// 攻击
|
|
||||||
_movementComponent.SetMove(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_movementComponent.SetMove(true);
|
|
||||||
_movementComponent.SetDirection(GetTargetDirection());
|
|
||||||
}
|
|
||||||
|
|
||||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,12 +118,143 @@ namespace Entity
|
||||||
{
|
{
|
||||||
_movementComponent.OnReset();
|
_movementComponent.OnReset();
|
||||||
_healthComponent.OnReset();
|
_healthComponent.OnReset();
|
||||||
|
_targetableTarget = null;
|
||||||
|
_currAttackTimer = 0f;
|
||||||
|
_attackState = AttackStateType.Idle;
|
||||||
|
|
||||||
base.OnHide(isShutdown, userData);
|
base.OnHide(isShutdown, userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public override void SetTarget(Transform target)
|
||||||
|
{
|
||||||
|
base.SetTarget(target);
|
||||||
|
_targetableTarget = target != null ? target.GetComponent<TargetableObject>() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAttackState(float elapseSeconds)
|
||||||
|
{
|
||||||
|
_currAttackTimer += elapseSeconds;
|
||||||
|
|
||||||
|
switch (_attackState)
|
||||||
|
{
|
||||||
|
case AttackStateType.Idle:
|
||||||
|
SetMove(false);
|
||||||
|
if (HasValidTarget())
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Check_OutRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case AttackStateType.Check_OutRange:
|
||||||
|
if (!HasValidTarget())
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsTargetInRange())
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Check_InRange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetMove(true);
|
||||||
|
_movementComponent.SetDirection(GetTargetDirection());
|
||||||
|
break;
|
||||||
|
case AttackStateType.Check_InRange:
|
||||||
|
if (!HasValidTarget())
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetMove(false);
|
||||||
|
if (!IsTargetInRange())
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Check_OutRange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currAttackTimer >= _attackCooldown)
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Attack);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TransitionTo(AttackStateType newState)
|
||||||
|
{
|
||||||
|
_attackState = newState;
|
||||||
|
|
||||||
|
if (_attackState == AttackStateType.Check_InRange)
|
||||||
|
{
|
||||||
|
SetMove(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_attackState == AttackStateType.Check_InRange && _currAttackTimer >= _attackCooldown)
|
||||||
|
{
|
||||||
|
TransitionTo(AttackStateType.Attack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_attackState == AttackStateType.Attack)
|
||||||
|
{
|
||||||
|
SetMove(false);
|
||||||
|
ExecuteAttack();
|
||||||
|
_currAttackTimer = 0f;
|
||||||
|
TransitionTo(AttackStateType.Check_InRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasValidTarget()
|
||||||
|
{
|
||||||
|
if (_target == null) return false;
|
||||||
|
|
||||||
|
if (_targetableTarget == null || _targetableTarget.CachedTransform != _target)
|
||||||
|
{
|
||||||
|
_targetableTarget = _target.GetComponent<TargetableObject>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _targetableTarget != null && _targetableTarget.Available && !_targetableTarget.IsDead;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTargetInRange()
|
||||||
|
{
|
||||||
|
if (_target == null) return false;
|
||||||
|
|
||||||
|
Vector3 delta = _target.position - this.CachedTransform.position;
|
||||||
|
delta.y = 0f;
|
||||||
|
return delta.sqrMagnitude <= _sqrAttackRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteAttack()
|
||||||
|
{
|
||||||
|
if (!HasValidTarget() || !IsTargetInRange()) return;
|
||||||
|
|
||||||
|
ImpactData targetImpactData = _targetableTarget.GetImpactData();
|
||||||
|
ImpactData selfImpactData = GetImpactData();
|
||||||
|
if (AIUtility.GetRelation(selfImpactData.Camp, targetImpactData.Camp) == RelationType.Friendly)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int damage = AIUtility.CalcDamageHP(_attackDamage, null, targetImpactData.DefenseStat,
|
||||||
|
targetImpactData.DodgeStat);
|
||||||
|
_targetableTarget.ApplyDamage(this, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMove(bool value)
|
||||||
|
{
|
||||||
|
if (_movementComponent != null)
|
||||||
|
{
|
||||||
|
_movementComponent.SetMove(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Vector3 GetTargetDirection()
|
private Vector3 GetTargetDirection()
|
||||||
{
|
{
|
||||||
if (_target == null)
|
if (_target == null)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using Components;
|
using Components;
|
||||||
|
using CustomUtility;
|
||||||
|
using Definition;
|
||||||
using Definition.DataStruct;
|
using Definition.DataStruct;
|
||||||
using Entity.EntityData;
|
using Entity.EntityData;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -8,17 +10,39 @@ namespace Entity
|
||||||
{
|
{
|
||||||
public class RemoteEnemy : EnemyBase
|
public class RemoteEnemy : EnemyBase
|
||||||
{
|
{
|
||||||
private float _speed;
|
private const string EnemyProjectileGroupName = "Bullet";
|
||||||
|
private const float EnemyProjectileGroupAutoReleaseInterval = 60f;
|
||||||
|
private const int EnemyProjectileGroupCapacity = 64;
|
||||||
|
private const float EnemyProjectileGroupExpireTime = 60f;
|
||||||
|
private const int EnemyProjectileGroupPriority = 0;
|
||||||
|
|
||||||
|
private const string ProjectileSpeedParamKey = "ProjectileSpeed";
|
||||||
|
private const string ProjectileLifeTimeParamKey = "ProjectileLifeTime";
|
||||||
|
private const string ProjectileSpawnForwardOffsetParamKey = "ProjectileSpawnForwardOffset";
|
||||||
|
private const string ProjectileSpawnHeightOffsetParamKey = "ProjectileSpawnHeightOffset";
|
||||||
|
private const string ProjectileAssetNameParamKey = "ProjectileAssetName";
|
||||||
|
|
||||||
private MovementComponent _movementComponent;
|
private MovementComponent _movementComponent;
|
||||||
private float _attackRange = 1f;
|
private float _attackRange = 1f;
|
||||||
private float _attackRangeSquared;
|
private float _attackRangeSquared;
|
||||||
|
private float _attackCooldown = 1f;
|
||||||
|
private int _attackDamage = 1;
|
||||||
|
private float _currAttackTimer;
|
||||||
|
|
||||||
|
[SerializeField] private float _projectileSpeed = 12f;
|
||||||
|
[SerializeField] private float _projectileLifeTime = 3f;
|
||||||
|
[SerializeField] private float _projectileSpawnForwardOffset = 0.7f;
|
||||||
|
[SerializeField] private float _projectileSpawnHeightOffset = 0.6f;
|
||||||
|
[SerializeField] private string _projectileAssetName = "BulletHandgun";
|
||||||
|
|
||||||
private EnemyData _remoteEnemyData;
|
private EnemyData _remoteEnemyData;
|
||||||
|
|
||||||
protected override TargetableObjectData _targetableObjectData => _remoteEnemyData;
|
protected override TargetableObjectData _targetableObjectData => _remoteEnemyData;
|
||||||
|
public override float AttackRange => _attackRange;
|
||||||
|
|
||||||
public override ImpactData GetImpactData()
|
public override ImpactData GetImpactData()
|
||||||
{
|
{
|
||||||
return new ImpactData(_remoteEnemyData.Camp, 0);
|
return new ImpactData(_remoteEnemyData.Camp, _attackDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInit(object userData)
|
protected override void OnInit(object userData)
|
||||||
|
|
@ -39,7 +63,21 @@ namespace Entity
|
||||||
_healthComponent.OnInit(enemyData.MaxHealthBase);
|
_healthComponent.OnInit(enemyData.MaxHealthBase);
|
||||||
_movementComponent.OnInit(_remoteEnemyData.SpeedBase, this.CachedTransform, null, true);
|
_movementComponent.OnInit(_remoteEnemyData.SpeedBase, this.CachedTransform, null, true);
|
||||||
_movementComponent.SetMove(true);
|
_movementComponent.SetMove(true);
|
||||||
|
|
||||||
|
_attackRange = Mathf.Max(0.1f, _remoteEnemyData.AttackRange);
|
||||||
_attackRangeSquared = _attackRange * _attackRange;
|
_attackRangeSquared = _attackRange * _attackRange;
|
||||||
|
_attackCooldown = Mathf.Max(0.01f, _remoteEnemyData.AttackCooldown);
|
||||||
|
_attackDamage = Mathf.Max(1, _remoteEnemyData.AttackDamage);
|
||||||
|
|
||||||
|
_projectileSpeed = ReadPositiveParam(ProjectileSpeedParamKey, _projectileSpeed);
|
||||||
|
_projectileLifeTime = ReadPositiveParam(ProjectileLifeTimeParamKey, _projectileLifeTime);
|
||||||
|
_projectileSpawnForwardOffset = ReadPositiveParam(ProjectileSpawnForwardOffsetParamKey,
|
||||||
|
_projectileSpawnForwardOffset);
|
||||||
|
_projectileSpawnHeightOffset = ReadPositiveParam(ProjectileSpawnHeightOffsetParamKey,
|
||||||
|
_projectileSpawnHeightOffset);
|
||||||
|
_projectileAssetName = ReadStringParam(ProjectileAssetNameParamKey, _projectileAssetName);
|
||||||
|
|
||||||
|
_currAttackTimer = 0f;
|
||||||
this.CachedTransform.position = enemyData.Position;
|
this.CachedTransform.position = enemyData.Position;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -50,25 +88,28 @@ namespace Entity
|
||||||
|
|
||||||
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
{
|
{
|
||||||
if (IsSimulationMovementEnabled())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||||
|
|
||||||
|
_currAttackTimer += elapseSeconds;
|
||||||
|
|
||||||
if (_target == null)
|
if (_target == null)
|
||||||
{
|
{
|
||||||
_movementComponent.SetMove(false);
|
_movementComponent.SetMove(false);
|
||||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
if (!IsSimulationMovementEnabled())
|
||||||
|
{
|
||||||
|
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float distanceSquared = (this.CachedTransform.position - _target.position).sqrMagnitude;
|
Vector3 toTarget = _target.position - this.CachedTransform.position;
|
||||||
if (distanceSquared < _attackRangeSquared)
|
toTarget.y = 0f;
|
||||||
|
float distanceSquared = toTarget.sqrMagnitude;
|
||||||
|
if (distanceSquared <= _attackRangeSquared)
|
||||||
{
|
{
|
||||||
// 攻击
|
|
||||||
_movementComponent.SetMove(false);
|
_movementComponent.SetMove(false);
|
||||||
|
TryFireProjectile();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -76,17 +117,115 @@ namespace Entity
|
||||||
_movementComponent.SetDirection(GetTargetDirection());
|
_movementComponent.SetDirection(GetTargetDirection());
|
||||||
}
|
}
|
||||||
|
|
||||||
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
if (!IsSimulationMovementEnabled())
|
||||||
|
{
|
||||||
|
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnHide(bool isShutdown, object userData)
|
protected override void OnHide(bool isShutdown, object userData)
|
||||||
{
|
{
|
||||||
_movementComponent.OnReset();
|
_movementComponent.OnReset();
|
||||||
_healthComponent.OnReset();
|
_healthComponent.OnReset();
|
||||||
|
_currAttackTimer = 0f;
|
||||||
|
|
||||||
base.OnHide(isShutdown, userData);
|
base.OnHide(isShutdown, userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryFireProjectile()
|
||||||
|
{
|
||||||
|
if (_currAttackTimer < _attackCooldown || _target == null) return;
|
||||||
|
if (!EnsureEnemyProjectileGroup()) return;
|
||||||
|
|
||||||
|
Vector3 spawnPosition = this.CachedTransform.position +
|
||||||
|
this.CachedTransform.forward * _projectileSpawnForwardOffset +
|
||||||
|
Vector3.up * _projectileSpawnHeightOffset;
|
||||||
|
Vector3 direction = _target.position - spawnPosition;
|
||||||
|
direction.y = 0f;
|
||||||
|
if (direction.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
direction = this.CachedTransform.forward;
|
||||||
|
direction.y = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
direction = Vector3.forward;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
direction.Normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
int projectileEntityId = GameEntry.Entity.GenerateSerialId();
|
||||||
|
var projectileData = new EnemyProjectileData(projectileEntityId, Id, _remoteEnemyData.Camp,
|
||||||
|
_attackDamage, _projectileSpeed, _projectileLifeTime, direction)
|
||||||
|
{
|
||||||
|
Position = spawnPosition,
|
||||||
|
Rotation = Quaternion.LookRotation(direction, Vector3.up)
|
||||||
|
};
|
||||||
|
|
||||||
|
GameEntry.Entity.ShowEntity(
|
||||||
|
entityId: projectileEntityId,
|
||||||
|
entityLogicType: typeof(EnemyProjectile),
|
||||||
|
entityAssetName: AssetUtility.GetEntityAsset(_projectileAssetName),
|
||||||
|
entityGroupName: EnemyProjectileGroupName,
|
||||||
|
priority: Constant.AssetPriority.BulletAsset,
|
||||||
|
userData: projectileData);
|
||||||
|
|
||||||
|
_currAttackTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EnsureEnemyProjectileGroup()
|
||||||
|
{
|
||||||
|
var entityComponent = GameEntry.Entity;
|
||||||
|
if (entityComponent == null) return false;
|
||||||
|
|
||||||
|
if (entityComponent.HasEntityGroup(EnemyProjectileGroupName))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addResult = entityComponent.AddEntityGroup(
|
||||||
|
EnemyProjectileGroupName,
|
||||||
|
EnemyProjectileGroupAutoReleaseInterval,
|
||||||
|
EnemyProjectileGroupCapacity,
|
||||||
|
EnemyProjectileGroupExpireTime,
|
||||||
|
EnemyProjectileGroupPriority);
|
||||||
|
|
||||||
|
if (!addResult)
|
||||||
|
{
|
||||||
|
Log.Warning("Can not create entity group '{0}'.", EnemyProjectileGroupName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float ReadPositiveParam(string paramName, float defaultValue)
|
||||||
|
{
|
||||||
|
if (_remoteEnemyData != null &&
|
||||||
|
_remoteEnemyData.TryGetParam(paramName, out string rawValue) &&
|
||||||
|
float.TryParse(rawValue, out float parsedValue))
|
||||||
|
{
|
||||||
|
return Mathf.Max(0.01f, parsedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mathf.Max(0.01f, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ReadStringParam(string paramName, string defaultValue)
|
||||||
|
{
|
||||||
|
if (_remoteEnemyData != null &&
|
||||||
|
_remoteEnemyData.TryGetParam(paramName, out string rawValue) &&
|
||||||
|
!string.IsNullOrWhiteSpace(rawValue))
|
||||||
|
{
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
private Vector3 GetTargetDirection()
|
private Vector3 GetTargetDirection()
|
||||||
{
|
{
|
||||||
if (_target == null)
|
if (_target == null)
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,8 @@ namespace Entity
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
private void OnTriggerEnter(Collider other)
|
||||||
{
|
{
|
||||||
EntityBase entity = other.gameObject.GetComponent<EntityBase>();
|
EntityBase entity = other.GetComponentInParent<EntityBase>();
|
||||||
if (entity == null)
|
if (entity == null || entity == this)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ namespace Entity
|
||||||
if (entity is TargetableObject && entity.Id < Id)
|
if (entity is TargetableObject && entity.Id < Id)
|
||||||
{
|
{
|
||||||
// 碰撞事件由 Id 大的一方处理
|
// 碰撞事件由 Id 大的一方处理
|
||||||
// 在这里规定所有的 Enemy 的 Id 均大于 0
|
// 在这里约定 Enemy 的 Id 为非负数(通常从 0 开始)
|
||||||
// 而其他的 Entity (Player, Weapon, Bullet) 的 Id 均小于 0
|
// 而其他的 Entity (Player, Weapon, Bullet) 的 Id 均小于 0
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Entity.Weapon
|
||||||
|
{
|
||||||
|
public sealed class LightningStrikeAttackEffect : IWeaponAttackEffect
|
||||||
|
{
|
||||||
|
private readonly float _duration = 0.22f;
|
||||||
|
private readonly float _yOffset = 0.2f;
|
||||||
|
private readonly float _ringLineWidth = 0.045f;
|
||||||
|
private readonly float _boltLineWidth = 0.08f;
|
||||||
|
private readonly int _ringSegments = 32;
|
||||||
|
private readonly float _boltHeight = 3f;
|
||||||
|
private readonly Color _ringColor = new(0.35f, 0.82f, 1f, 0.92f);
|
||||||
|
private readonly Color _boltColor = new(0.85f, 0.96f, 1f, 0.97f);
|
||||||
|
|
||||||
|
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
|
||||||
|
{
|
||||||
|
float safeRadius = Mathf.Max(0.1f, radius);
|
||||||
|
Vector3 center = new Vector3(position.x, position.y + _yOffset, position.z);
|
||||||
|
|
||||||
|
GameObject root = new GameObject("LightningStrikeEffect");
|
||||||
|
root.transform.position = center;
|
||||||
|
|
||||||
|
Shader shader = Shader.Find("Sprites/Default");
|
||||||
|
if (shader == null)
|
||||||
|
{
|
||||||
|
shader = Shader.Find("Unlit/Color");
|
||||||
|
}
|
||||||
|
|
||||||
|
Material ringMaterial = new Material(shader);
|
||||||
|
ringMaterial.color = _ringColor;
|
||||||
|
|
||||||
|
LineRenderer ring = root.AddComponent<LineRenderer>();
|
||||||
|
ring.loop = true;
|
||||||
|
ring.useWorldSpace = true;
|
||||||
|
ring.positionCount = _ringSegments;
|
||||||
|
ring.startWidth = _ringLineWidth;
|
||||||
|
ring.endWidth = _ringLineWidth;
|
||||||
|
ring.startColor = _ringColor;
|
||||||
|
ring.endColor = _ringColor;
|
||||||
|
ring.material = ringMaterial;
|
||||||
|
|
||||||
|
float step = Mathf.PI * 2f / _ringSegments;
|
||||||
|
for (int i = 0; i < _ringSegments; i++)
|
||||||
|
{
|
||||||
|
float angle = i * step;
|
||||||
|
Vector3 offset = new Vector3(Mathf.Cos(angle) * safeRadius, 0f, Mathf.Sin(angle) * safeRadius);
|
||||||
|
ring.SetPosition(i, center + offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject bolt = new GameObject("Bolt");
|
||||||
|
bolt.transform.SetParent(root.transform, false);
|
||||||
|
|
||||||
|
Material boltMaterial = new Material(shader);
|
||||||
|
boltMaterial.color = _boltColor;
|
||||||
|
|
||||||
|
LineRenderer boltLine = bolt.AddComponent<LineRenderer>();
|
||||||
|
boltLine.loop = false;
|
||||||
|
boltLine.useWorldSpace = true;
|
||||||
|
boltLine.positionCount = 4;
|
||||||
|
boltLine.startWidth = _boltLineWidth;
|
||||||
|
boltLine.endWidth = _boltLineWidth * 0.55f;
|
||||||
|
boltLine.startColor = _boltColor;
|
||||||
|
boltLine.endColor = _boltColor;
|
||||||
|
boltLine.material = boltMaterial;
|
||||||
|
|
||||||
|
Vector3 top = center + Vector3.up * _boltHeight;
|
||||||
|
Vector3 middleA = center + Vector3.up * (_boltHeight * 0.66f) +
|
||||||
|
new Vector3(Random.Range(-0.18f, 0.18f), 0f, Random.Range(-0.18f, 0.18f));
|
||||||
|
Vector3 middleB = center + Vector3.up * (_boltHeight * 0.3f) +
|
||||||
|
new Vector3(Random.Range(-0.12f, 0.12f), 0f, Random.Range(-0.12f, 0.12f));
|
||||||
|
|
||||||
|
boltLine.SetPosition(0, top);
|
||||||
|
boltLine.SetPosition(1, middleA);
|
||||||
|
boltLine.SetPosition(2, middleB);
|
||||||
|
boltLine.SetPosition(3, center);
|
||||||
|
|
||||||
|
Object.Destroy(root, Mathf.Max(0.01f, _duration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 81cbdf8961ad419c91b989d56ca782d0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -220,6 +220,32 @@ namespace Entity.Weapon
|
||||||
return AIUtility.GetSqrMagnitudeXZ(this, target) < sqrRange;
|
return AIUtility.GetSqrMagnitudeXZ(this, target) < sqrRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected bool TryQueueAreaCollisionQuery(in Vector3 center, float radius, int maxTargets = 16)
|
||||||
|
{
|
||||||
|
var simulationWorld = GameEntry.SimulationWorld;
|
||||||
|
if (simulationWorld == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
|
||||||
|
return simulationWorld.TryEnqueueAreaCollisionQuery(Id, ownerEntityId, in center, radius, maxTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool TryQueueSectorCollisionQuery(in Vector3 center, float radius, in Vector3 direction,
|
||||||
|
float halfAngleDeg, int maxTargets = 16)
|
||||||
|
{
|
||||||
|
var simulationWorld = GameEntry.SimulationWorld;
|
||||||
|
if (simulationWorld == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
|
||||||
|
return simulationWorld.TryEnqueueSectorCollisionQuery(Id, ownerEntityId, in center, radius, in direction,
|
||||||
|
halfAngleDeg, maxTargets);
|
||||||
|
}
|
||||||
|
|
||||||
protected void SetTargetSelector(TargetSelectorType selectorType)
|
protected void SetTargetSelector(TargetSelectorType selectorType)
|
||||||
{
|
{
|
||||||
TargetSelector = CreateSelector(selectorType);
|
TargetSelector = CreateSelector(selectorType);
|
||||||
|
|
@ -286,4 +312,3 @@ namespace Entity.Weapon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,15 @@ namespace Entity.Weapon
|
||||||
|
|
||||||
private void ApplyGroundAreaDamage()
|
private void ApplyGroundAreaDamage()
|
||||||
{
|
{
|
||||||
if (_hitRadius <= 0f || _hitResults == null || _hitResults.Length == 0) return;
|
if (_hitRadius <= 0f) return;
|
||||||
|
|
||||||
|
if (TryQueueAreaCollisionQuery(_attackCenter, _hitRadius, Mathf.Max(1, _maxHitColliders)))
|
||||||
|
{
|
||||||
|
_hitEntityIds.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hitResults == null || _hitResults.Length == 0) return;
|
||||||
|
|
||||||
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _hitRadius, _hitResults, _hitMask,
|
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _hitRadius, _hitResults, _hitMask,
|
||||||
QueryTriggerInteraction.Collide);
|
QueryTriggerInteraction.Collide);
|
||||||
|
|
@ -149,12 +157,14 @@ namespace Entity.Weapon
|
||||||
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||||
_cachedRotation = CachedTransform.rotation;
|
_cachedRotation = CachedTransform.rotation;
|
||||||
|
|
||||||
string hitRadiusRaw = _weaponData.GetParamsString(HitRadiusParamKey);
|
if (_weaponData.TryGetParam(HitRadiusParamKey, out string hitRadiusRaw))
|
||||||
if (!float.TryParse(hitRadiusRaw, out _hitRadius))
|
{
|
||||||
|
_hitRadius = Mathf.Max(0.1f, float.Parse(hitRadiusRaw));
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_hitRadius = _weaponData.AttackRange;
|
_hitRadius = _weaponData.AttackRange;
|
||||||
}
|
}
|
||||||
_hitRadius = Mathf.Max(0.1f, _hitRadius);
|
|
||||||
|
|
||||||
_hitRadiusSqr = _hitRadius * _hitRadius;
|
_hitRadiusSqr = _hitRadius * _hitRadius;
|
||||||
_attackEffect = new KnifeRangeAttackEffect();
|
_attackEffect = new KnifeRangeAttackEffect();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5fded69986744ded97edb5f2ac06304f
|
||||||
|
timeCreated: 1771578597
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
namespace Entity.Weapon
|
||||||
|
{
|
||||||
|
public partial class WeaponLightning
|
||||||
|
{
|
||||||
|
private class AttackState : WeaponStateBase
|
||||||
|
{
|
||||||
|
private WeaponLightning _weapon;
|
||||||
|
|
||||||
|
public override WeaponStateType State => WeaponStateType.Attack;
|
||||||
|
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
|
||||||
|
|
||||||
|
public override void OnEnter()
|
||||||
|
{
|
||||||
|
_weapon._currAttackTimer = 0f;
|
||||||
|
_weapon.Attack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
|
{
|
||||||
|
if (!_weapon._isAttacking)
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Check_InRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnLeave()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 25854832a7d3416dacab67bcfef2a2fa
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
namespace Entity.Weapon
|
||||||
|
{
|
||||||
|
public partial class WeaponLightning
|
||||||
|
{
|
||||||
|
private class CheckInRangeState : WeaponStateBase
|
||||||
|
{
|
||||||
|
private WeaponLightning _weapon;
|
||||||
|
|
||||||
|
public override WeaponStateType State => WeaponStateType.Check_InRange;
|
||||||
|
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
|
||||||
|
|
||||||
|
public override void OnEnter()
|
||||||
|
{
|
||||||
|
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
|
{
|
||||||
|
_weapon.Check();
|
||||||
|
_weapon.RotateToTarget(elapseSeconds);
|
||||||
|
_weapon._currAttackTimer += elapseSeconds;
|
||||||
|
|
||||||
|
if (_weapon._target == null || !_weapon._target.Available)
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnLeave()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 25404ad172434ba38609e797b6d96919
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
namespace Entity.Weapon
|
||||||
|
{
|
||||||
|
public partial class WeaponLightning
|
||||||
|
{
|
||||||
|
private class CheckOutRangeState : WeaponStateBase
|
||||||
|
{
|
||||||
|
private WeaponLightning _weapon;
|
||||||
|
|
||||||
|
public override WeaponStateType State => WeaponStateType.Check_OutRange;
|
||||||
|
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
|
||||||
|
|
||||||
|
public override void OnEnter()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
|
{
|
||||||
|
_weapon.Check();
|
||||||
|
_weapon.RotateToTarget(elapseSeconds);
|
||||||
|
_weapon._currAttackTimer += elapseSeconds;
|
||||||
|
|
||||||
|
if (_weapon._target == null || !_weapon._target.Available)
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Check_InRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnLeave()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f3aa479abb79491d83e8de76806faf7a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
namespace Entity.Weapon
|
||||||
|
{
|
||||||
|
public partial class WeaponLightning
|
||||||
|
{
|
||||||
|
private class IdleState : WeaponStateBase
|
||||||
|
{
|
||||||
|
private WeaponLightning _weapon;
|
||||||
|
|
||||||
|
public override WeaponStateType State => WeaponStateType.Idle;
|
||||||
|
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
|
||||||
|
|
||||||
|
public override void OnEnter()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
|
||||||
|
{
|
||||||
|
_weapon.Check();
|
||||||
|
_weapon.RotateToOrigin(elapseSeconds);
|
||||||
|
_weapon._currAttackTimer += elapseSeconds;
|
||||||
|
|
||||||
|
if (_weapon._target != null && _weapon._target.Available)
|
||||||
|
{
|
||||||
|
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnLeave()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3464037c29774c3ea326dc93fe33551e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using CustomUtility;
|
||||||
|
using Definition.DataStruct;
|
||||||
|
using Definition.Enum;
|
||||||
|
using DG.Tweening;
|
||||||
|
using Entity.EntityData;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
|
namespace Entity.Weapon
|
||||||
|
{
|
||||||
|
public partial class WeaponLightning : WeaponBase
|
||||||
|
{
|
||||||
|
private const string HitRadiusParamKey = "HitRadius";
|
||||||
|
|
||||||
|
private WeaponLightningData _weaponData;
|
||||||
|
|
||||||
|
private Quaternion _cachedRotation;
|
||||||
|
[SerializeField] private float _rotateSpeed = 5f;
|
||||||
|
|
||||||
|
[SerializeField] private float _takeOffDuration = 0.3f;
|
||||||
|
[SerializeField] private float _flyToTargetDuration = 0.2f;
|
||||||
|
[SerializeField] private float _strikeDuration = 0.1f;
|
||||||
|
[SerializeField] private float _returnDuration = 0.22f;
|
||||||
|
|
||||||
|
[SerializeField] private float _hoverHeight = 15f;
|
||||||
|
|
||||||
|
[SerializeField] private LayerMask _hitMask = ~0;
|
||||||
|
[SerializeField] private int _maxHitColliders = 32;
|
||||||
|
|
||||||
|
private Sequence _attackSequence;
|
||||||
|
private Transform _attackParent;
|
||||||
|
private Vector3 _lockedStrikePoint;
|
||||||
|
|
||||||
|
private Collider[] _hitResults;
|
||||||
|
private readonly HashSet<int> _hitEntityIds = new();
|
||||||
|
|
||||||
|
private float _hitRadius;
|
||||||
|
private float _hitRadiusSqr;
|
||||||
|
|
||||||
|
private IWeaponAttackEffect _attackEffect;
|
||||||
|
|
||||||
|
public override ImpactData GetImpactData()
|
||||||
|
{
|
||||||
|
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void BuildStates()
|
||||||
|
{
|
||||||
|
RegisterState(new IdleState());
|
||||||
|
RegisterState(new CheckOutRangeState());
|
||||||
|
RegisterState(new CheckInRangeState());
|
||||||
|
RegisterState(new AttackState());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Attack()
|
||||||
|
{
|
||||||
|
StopAttackTween(false);
|
||||||
|
FaceTargetImmediately();
|
||||||
|
|
||||||
|
_isAttacking = true;
|
||||||
|
_lockedStrikePoint = ResolveStrikePoint();
|
||||||
|
_attackParent = CachedTransform.parent;
|
||||||
|
CachedTransform.SetParent(null);
|
||||||
|
|
||||||
|
Vector3 takeOffPosition = CachedTransform.position;
|
||||||
|
Vector3 hoverPosition = _lockedStrikePoint + Vector3.up * Mathf.Max(0.1f, _hoverHeight);
|
||||||
|
|
||||||
|
_attackSequence = DOTween.Sequence();
|
||||||
|
_attackSequence.Append(CachedTransform.DOMove(takeOffPosition, _takeOffDuration).SetEase(Ease.OutQuad));
|
||||||
|
_attackSequence.Append(CachedTransform.DOMove(hoverPosition, _flyToTargetDuration).SetEase(Ease.OutSine));
|
||||||
|
_attackSequence.AppendCallback(() => CachedTransform.LookAt(_lockedStrikePoint));
|
||||||
|
_attackSequence.Append(CachedTransform.DOMove(_lockedStrikePoint, _strikeDuration).SetEase(Ease.InQuad));
|
||||||
|
_attackSequence.AppendCallback(() =>
|
||||||
|
{
|
||||||
|
_attackEffect?.Play(this, _lockedStrikePoint, _target, _hitRadius);
|
||||||
|
ApplyGroundAreaDamage();
|
||||||
|
|
||||||
|
if (_attackParent != null)
|
||||||
|
{
|
||||||
|
CachedTransform.SetParent(_attackParent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_attackSequence.Append(CachedTransform.DOLocalMove(Vector3.zero, _returnDuration).SetEase(Ease.OutSine));
|
||||||
|
_attackSequence.AppendCallback(() =>
|
||||||
|
{
|
||||||
|
_isAttacking = false;
|
||||||
|
_attackSequence = null;
|
||||||
|
_attackParent = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Check()
|
||||||
|
{
|
||||||
|
_target = SelectTarget(_sqrRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 ResolveStrikePoint()
|
||||||
|
{
|
||||||
|
if (_target != null && _target.Available)
|
||||||
|
{
|
||||||
|
return _target.CachedTransform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CachedTransform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyGroundAreaDamage()
|
||||||
|
{
|
||||||
|
if (_hitRadius <= 0f) return;
|
||||||
|
|
||||||
|
if (TryQueueAreaCollisionQuery(_lockedStrikePoint, _hitRadius, Mathf.Max(1, _maxHitColliders)))
|
||||||
|
{
|
||||||
|
_hitEntityIds.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hitResults == null || _hitResults.Length == 0) return;
|
||||||
|
|
||||||
|
int hitCount = Physics.OverlapSphereNonAlloc(_lockedStrikePoint, _hitRadius, _hitResults, _hitMask,
|
||||||
|
QueryTriggerInteraction.Collide);
|
||||||
|
|
||||||
|
_hitEntityIds.Clear();
|
||||||
|
for (int i = 0; i < hitCount; i++)
|
||||||
|
{
|
||||||
|
Collider collider = _hitResults[i];
|
||||||
|
if (collider == null) continue;
|
||||||
|
|
||||||
|
TargetableObject targetable = collider.GetComponentInParent<TargetableObject>();
|
||||||
|
if (targetable == null || !targetable.Available || targetable.IsDead) continue;
|
||||||
|
if (!_hitEntityIds.Add(targetable.Id)) continue;
|
||||||
|
|
||||||
|
Vector3 delta = targetable.CachedTransform.position - _lockedStrikePoint;
|
||||||
|
delta.y = 0f;
|
||||||
|
if (delta.sqrMagnitude > _hitRadiusSqr) continue;
|
||||||
|
|
||||||
|
AIUtility.PerformCollision(targetable, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RotateToTarget(float elapseSeconds)
|
||||||
|
{
|
||||||
|
if (_target == null || !_target.Available) return;
|
||||||
|
|
||||||
|
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
|
||||||
|
directionToTarget.y = 0f;
|
||||||
|
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
|
||||||
|
|
||||||
|
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
|
||||||
|
CachedTransform.rotation =
|
||||||
|
Quaternion.Slerp(CachedTransform.rotation, targetRotation, _rotateSpeed * elapseSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RotateToOrigin(float elapseSeconds)
|
||||||
|
{
|
||||||
|
CachedTransform.rotation =
|
||||||
|
Quaternion.Slerp(CachedTransform.rotation, _cachedRotation, _rotateSpeed * elapseSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FaceTargetImmediately()
|
||||||
|
{
|
||||||
|
if (_target == null || !_target.Available) return;
|
||||||
|
|
||||||
|
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
|
||||||
|
directionToTarget.y = 0f;
|
||||||
|
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
|
||||||
|
|
||||||
|
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnWeaponShow(object userData)
|
||||||
|
{
|
||||||
|
_weaponData = RequireWeaponData<WeaponLightningData>(userData);
|
||||||
|
if (_weaponData == null) return false;
|
||||||
|
WeaponData = _weaponData;
|
||||||
|
|
||||||
|
_currAttackTimer = 0f;
|
||||||
|
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||||
|
_cachedRotation = CachedTransform.rotation;
|
||||||
|
|
||||||
|
_hitRadius = ReadPositiveParam(HitRadiusParamKey, _weaponData.AttackRange);
|
||||||
|
_hitRadiusSqr = _hitRadius * _hitRadius;
|
||||||
|
|
||||||
|
int colliderCapacity = Mathf.Max(1, _maxHitColliders);
|
||||||
|
if (_hitResults == null || _hitResults.Length != colliderCapacity)
|
||||||
|
{
|
||||||
|
_hitResults = new Collider[colliderCapacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
_attackEffect = new LightningStrikeAttackEffect();
|
||||||
|
|
||||||
|
if (_weaponData.OwnerCamp == CampType.Player)
|
||||||
|
{
|
||||||
|
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
|
||||||
|
_hitMask = LayerMask.GetMask("Enemy");
|
||||||
|
}
|
||||||
|
else if (_weaponData.OwnerCamp == CampType.Enemy)
|
||||||
|
{
|
||||||
|
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
|
||||||
|
_hitMask = LayerMask.GetMask("Player");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnWeaponHide(object userData)
|
||||||
|
{
|
||||||
|
StopAttackTween(true);
|
||||||
|
_attackEffect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
|
||||||
|
{
|
||||||
|
BindAttackStatFromOwner(parentEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnWeaponDetach(EntityLogic parentEntity, object userData)
|
||||||
|
{
|
||||||
|
StopAttackTween(true);
|
||||||
|
ReleaseAttackStatSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnEnabledChanged(bool enabled)
|
||||||
|
{
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
StopAttackTween(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float ReadPositiveParam(string paramName, float defaultValue)
|
||||||
|
{
|
||||||
|
if (_weaponData.Params != null &&
|
||||||
|
_weaponData.Params.TryGetValue(paramName.ToLower(), out string rawValue) &&
|
||||||
|
float.TryParse(rawValue, out float parsedValue))
|
||||||
|
{
|
||||||
|
return Mathf.Max(0.1f, parsedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mathf.Max(0.1f, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopAttackTween(bool resetTransform)
|
||||||
|
{
|
||||||
|
if (_attackSequence != null)
|
||||||
|
{
|
||||||
|
_attackSequence.Kill();
|
||||||
|
_attackSequence = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isAttacking = false;
|
||||||
|
|
||||||
|
if (resetTransform)
|
||||||
|
{
|
||||||
|
if (_attackParent != null)
|
||||||
|
{
|
||||||
|
CachedTransform.SetParent(_attackParent);
|
||||||
|
CachedTransform.localPosition = Vector3.zero;
|
||||||
|
}
|
||||||
|
else if (CachedTransform.parent != null)
|
||||||
|
{
|
||||||
|
CachedTransform.localPosition = Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedTransform.rotation = _cachedRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attackParent = null;
|
||||||
|
_hitEntityIds.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a8315d9e60c4434ebde9b23c853f27d0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -93,12 +93,8 @@ namespace Entity.Weapon
|
||||||
|
|
||||||
private void ApplySectorDamage()
|
private void ApplySectorDamage()
|
||||||
{
|
{
|
||||||
if (_attackRadius <= 0f || _hitResults == null || _hitResults.Length == 0) return;
|
if (_attackRadius <= 0f) return;
|
||||||
|
|
||||||
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _attackRadius, _hitResults, _hitMask,
|
|
||||||
QueryTriggerInteraction.Collide);
|
|
||||||
|
|
||||||
_hitEntityIds.Clear();
|
|
||||||
Vector3 forward = CachedTransform.forward;
|
Vector3 forward = CachedTransform.forward;
|
||||||
forward.y = 0f;
|
forward.y = 0f;
|
||||||
if (forward.sqrMagnitude <= Mathf.Epsilon)
|
if (forward.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
|
@ -107,8 +103,20 @@ namespace Entity.Weapon
|
||||||
}
|
}
|
||||||
|
|
||||||
forward.Normalize();
|
forward.Normalize();
|
||||||
|
|
||||||
float halfAngle = _sectorAngle * 0.5f;
|
float halfAngle = _sectorAngle * 0.5f;
|
||||||
|
if (TryQueueSectorCollisionQuery(_attackCenter, _attackRadius, in forward, halfAngle,
|
||||||
|
Mathf.Max(1, _maxHitColliders)))
|
||||||
|
{
|
||||||
|
_hitEntityIds.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hitResults == null || _hitResults.Length == 0) return;
|
||||||
|
|
||||||
|
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _attackRadius, _hitResults, _hitMask,
|
||||||
|
QueryTriggerInteraction.Collide);
|
||||||
|
|
||||||
|
_hitEntityIds.Clear();
|
||||||
for (int i = 0; i < hitCount; i++)
|
for (int i = 0; i < hitCount; i++)
|
||||||
{
|
{
|
||||||
Collider collider = _hitResults[i];
|
Collider collider = _hitResults[i];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7771e0f6b92ece64395ada5cdea72858
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
using GameFramework;
|
||||||
|
using GameFramework.Event;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace CustomEvent
|
||||||
|
{
|
||||||
|
public sealed class ProjectileHitPresentationEventArgs : GameEventArgs
|
||||||
|
{
|
||||||
|
public static readonly int EventId = typeof(ProjectileHitPresentationEventArgs).GetHashCode();
|
||||||
|
|
||||||
|
public override int Id => EventId;
|
||||||
|
|
||||||
|
public int ProjectileEntityId { get; private set; }
|
||||||
|
public int SourceEntityId { get; private set; }
|
||||||
|
public int SourceOwnerEntityId { get; private set; }
|
||||||
|
public int TargetEntityId { get; private set; }
|
||||||
|
public int Damage { get; private set; }
|
||||||
|
public Vector3 HitPosition { get; private set; }
|
||||||
|
public bool ShowHitMarker { get; private set; }
|
||||||
|
public bool ShowHitEffect { get; private set; }
|
||||||
|
public int EffectEntityTypeId { get; private set; }
|
||||||
|
|
||||||
|
public static ProjectileHitPresentationEventArgs Create(int projectileEntityId, int sourceEntityId,
|
||||||
|
int sourceOwnerEntityId, int targetEntityId, int damage, in Vector3 hitPosition, bool showHitMarker,
|
||||||
|
bool showHitEffect, int effectEntityTypeId)
|
||||||
|
{
|
||||||
|
ProjectileHitPresentationEventArgs args = ReferencePool.Acquire<ProjectileHitPresentationEventArgs>();
|
||||||
|
args.ProjectileEntityId = projectileEntityId;
|
||||||
|
args.SourceEntityId = sourceEntityId;
|
||||||
|
args.SourceOwnerEntityId = sourceOwnerEntityId;
|
||||||
|
args.TargetEntityId = targetEntityId;
|
||||||
|
args.Damage = damage;
|
||||||
|
args.HitPosition = hitPosition;
|
||||||
|
args.ShowHitMarker = showHitMarker;
|
||||||
|
args.ShowHitEffect = showHitEffect;
|
||||||
|
args.EffectEntityTypeId = effectEntityTypeId;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Clear()
|
||||||
|
{
|
||||||
|
ProjectileEntityId = 0;
|
||||||
|
SourceEntityId = 0;
|
||||||
|
SourceOwnerEntityId = 0;
|
||||||
|
TargetEntityId = 0;
|
||||||
|
Damage = 0;
|
||||||
|
HitPosition = Vector3.zero;
|
||||||
|
ShowHitMarker = false;
|
||||||
|
ShowHitEffect = false;
|
||||||
|
EffectEntityTypeId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d02876e26d8342f7af024c697e451ea4
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -91,6 +91,15 @@ namespace Procedure
|
||||||
{
|
{
|
||||||
GameEntry.Entity.HideEntity(entity.Id);
|
GameEntry.Entity.HideEntity(entity.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var enemyProjectiles = GameEntry.Entity.GetEntityGroup("EnemyProjectile")?.GetAllEntities();
|
||||||
|
if (enemyProjectiles != null)
|
||||||
|
{
|
||||||
|
foreach (var projectile in enemyProjectiles)
|
||||||
|
{
|
||||||
|
GameEntry.Entity.HideEntity(projectile.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnDestroy(IFsm<IProcedureManager> procedureOwner)
|
public override void OnDestroy(IFsm<IProcedureManager> procedureOwner)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@ namespace Simulation
|
||||||
public int OwnerEntityId;
|
public int OwnerEntityId;
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public Vector3 Forward;
|
public Vector3 Forward;
|
||||||
|
public Vector3 Velocity;
|
||||||
public float Speed;
|
public float Speed;
|
||||||
|
public float LifeTime;
|
||||||
|
public float Age;
|
||||||
|
public bool Active;
|
||||||
public float RemainingLifetime;
|
public float RemainingLifetime;
|
||||||
public int State;
|
public int State;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,25 +125,44 @@ namespace Simulation
|
||||||
|
|
||||||
private void TickEnemiesJobified(in SimulationTickContext context)
|
private void TickEnemiesJobified(in SimulationTickContext context)
|
||||||
{
|
{
|
||||||
|
if (context.DeltaTime <= 0f)
|
||||||
|
{
|
||||||
|
PrepareCollisionCandidateChannels(0, 0, 0);
|
||||||
|
ResetCollisionRuntimeStats();
|
||||||
|
ClearAreaCollisionFrameBuffers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
|
||||||
{
|
{
|
||||||
SyncSimulationToJobInput();
|
SyncSimulationToJobInput();
|
||||||
|
int projectileQueryCount = _projectiles.Count;
|
||||||
|
int areaQueryCount = GetPendingAreaCollisionQueryCount();
|
||||||
|
int queryCount = projectileQueryCount + areaQueryCount;
|
||||||
|
int projectileExpectedCount = projectileQueryCount * Mathf.Max(1, _projectileMaxCandidatesPerQuery);
|
||||||
|
int areaExpectedCount = EstimatePendingAreaCollisionCandidateCount();
|
||||||
|
int expectedCandidateCount = Mathf.Max(16, projectileExpectedCount + areaExpectedCount);
|
||||||
|
int bucketCapacity = Mathf.Max(256, _enemies.Count * 2 + queryCount);
|
||||||
|
PrepareCollisionCandidateChannels(queryCount, expectedCandidateCount, bucketCapacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
using (CustomProfilerMarker.TickEnemies_StateUpdate.Auto())
|
||||||
{
|
{
|
||||||
ExecuteEnemyMovementJob(in context);
|
ExecuteEnemyMovementJob(in context);
|
||||||
|
ExecuteProjectileMovementJob(in context);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
|
||||||
{
|
{
|
||||||
ApplyEnemySeparationForJobOutput(in context);
|
ApplyEnemySeparationForJobOutput(in context);
|
||||||
|
BuildProjectileCollisionCandidates();
|
||||||
}
|
}
|
||||||
|
|
||||||
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
|
||||||
{
|
{
|
||||||
SyncProjectilesToJobOutput();
|
|
||||||
ApplyJobOutputToSimulation();
|
ApplyJobOutputToSimulation();
|
||||||
|
ResolveProjectileCollisionCandidatesMainThread();
|
||||||
|
RecycleInactiveProjectiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkEnemyTargetSpatialIndexDirty();
|
MarkEnemyTargetSpatialIndexDirty();
|
||||||
|
|
@ -413,6 +432,9 @@ namespace Simulation
|
||||||
|
|
||||||
if (sqrDistance <= float.Epsilon)
|
if (sqrDistance <= float.Epsilon)
|
||||||
{
|
{
|
||||||
|
float3 zeroDistanceAxis = GetZeroDistanceSeparationAxis(index, otherIndex);
|
||||||
|
float directionSign = index < otherIndex ? 1f : -1f;
|
||||||
|
pushAccumulation += zeroDistanceAxis * (selfRadius * 0.25f * directionSign);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -528,6 +550,18 @@ namespace Simulation
|
||||||
return ((long)x << 32) ^ (uint)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,
|
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
|
||||||
NativeArray<EnemyJobOutputData> outputs, float deltaTime, float3 playerPosition)
|
NativeArray<EnemyJobOutputData> outputs, float deltaTime, float3 playerPosition)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ namespace Simulation
|
||||||
private const string DropGroupName = "Drop";
|
private const string DropGroupName = "Drop";
|
||||||
private const string BulletGroupName = "Bullet";
|
private const string BulletGroupName = "Bullet";
|
||||||
private const string ProjectileGroupName = "Projectile";
|
private const string ProjectileGroupName = "Projectile";
|
||||||
|
private const string EnemyProjectileGroupName = "EnemyProjectile";
|
||||||
|
|
||||||
private readonly SimulationWorld _world;
|
private readonly SimulationWorld _world;
|
||||||
|
|
||||||
|
|
@ -53,10 +54,11 @@ namespace Simulation
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((groupName == BulletGroupName || groupName == ProjectileGroupName) &&
|
if ((groupName == BulletGroupName || groupName == ProjectileGroupName ||
|
||||||
|
groupName == EnemyProjectileGroupName) &&
|
||||||
args.Entity.Logic is EntityBase projectileEntity)
|
args.Entity.Logic is EntityBase projectileEntity)
|
||||||
{
|
{
|
||||||
_world.RegisterProjectileLifecycle(projectileEntity);
|
_world.RegisterProjectileLifecycle(projectileEntity, args.UserData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +81,8 @@ namespace Simulation
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupName == BulletGroupName || groupName == ProjectileGroupName)
|
if (groupName == BulletGroupName || groupName == ProjectileGroupName ||
|
||||||
|
groupName == EnemyProjectileGroupName)
|
||||||
{
|
{
|
||||||
_world.UnregisterProjectileLifecycle(args.EntityId);
|
_world.UnregisterProjectileLifecycle(args.EntityId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
using Unity.Mathematics;
|
using Unity.Mathematics;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
@ -43,7 +44,11 @@ namespace Simulation
|
||||||
public int OwnerEntityId;
|
public int OwnerEntityId;
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public Vector3 Forward;
|
public Vector3 Forward;
|
||||||
|
public Vector3 Velocity;
|
||||||
public float Speed;
|
public float Speed;
|
||||||
|
public float LifeTime;
|
||||||
|
public float Age;
|
||||||
|
public bool Active;
|
||||||
public float RemainingLifetime;
|
public float RemainingLifetime;
|
||||||
public int State;
|
public int State;
|
||||||
}
|
}
|
||||||
|
|
@ -54,11 +59,67 @@ namespace Simulation
|
||||||
public int OwnerEntityId;
|
public int OwnerEntityId;
|
||||||
public Vector3 Position;
|
public Vector3 Position;
|
||||||
public Vector3 Forward;
|
public Vector3 Forward;
|
||||||
|
public Vector3 Velocity;
|
||||||
public float Speed;
|
public float Speed;
|
||||||
|
public float LifeTime;
|
||||||
|
public float Age;
|
||||||
|
public bool Active;
|
||||||
public float RemainingLifetime;
|
public float RemainingLifetime;
|
||||||
public int State;
|
public int State;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared broad-phase query payload for CP5 projectile collision and CP6 AOE collision candidates.
|
||||||
|
private struct CollisionQueryData
|
||||||
|
{
|
||||||
|
public int QueryId;
|
||||||
|
public int SourceType;
|
||||||
|
public int SourceEntityId;
|
||||||
|
public int SourceOwnerEntityId;
|
||||||
|
public float3 Position;
|
||||||
|
public float Radius;
|
||||||
|
public int MaxTargets;
|
||||||
|
public int ShapeType;
|
||||||
|
public float3 Direction;
|
||||||
|
public float HalfAngleDeg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared candidate buffer consumed by the main thread settlement stage.
|
||||||
|
private struct CollisionCandidateData
|
||||||
|
{
|
||||||
|
public int QueryId;
|
||||||
|
public int SourceType;
|
||||||
|
public int SourceEntityId;
|
||||||
|
public int SourceOwnerEntityId;
|
||||||
|
public int TargetEntityId;
|
||||||
|
public float SqrDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AreaCollisionRequestData
|
||||||
|
{
|
||||||
|
public int SourceEntityId;
|
||||||
|
public int SourceOwnerEntityId;
|
||||||
|
public Vector3 Center;
|
||||||
|
public float Radius;
|
||||||
|
public int MaxTargets;
|
||||||
|
public int ShapeType;
|
||||||
|
public Vector3 Direction;
|
||||||
|
public float HalfAngleDeg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AreaCollisionHitEventData
|
||||||
|
{
|
||||||
|
public int QueryId;
|
||||||
|
public int SourceEntityId;
|
||||||
|
public int SourceOwnerEntityId;
|
||||||
|
public int TargetEntityId;
|
||||||
|
public float SqrDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int CollisionSourceTypeProjectile = 1;
|
||||||
|
private const int CollisionSourceTypeArea = 2;
|
||||||
|
private const int CollisionShapeCircle = 0;
|
||||||
|
private const int CollisionShapeSector = 1;
|
||||||
|
|
||||||
private NativeList<EnemyJobInputData> _enemyJobInputs;
|
private NativeList<EnemyJobInputData> _enemyJobInputs;
|
||||||
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
|
private NativeList<EnemyJobOutputData> _enemyJobOutputs;
|
||||||
private NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs;
|
private NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs;
|
||||||
|
|
@ -66,7 +127,34 @@ namespace Simulation
|
||||||
private NativeList<float2> _enemySeparationCurrentPushes;
|
private NativeList<float2> _enemySeparationCurrentPushes;
|
||||||
private NativeList<ProjectileJobInputData> _projectileJobInputs;
|
private NativeList<ProjectileJobInputData> _projectileJobInputs;
|
||||||
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
|
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
|
||||||
|
private NativeList<CollisionQueryData> _collisionQueryInputs;
|
||||||
|
private NativeList<CollisionCandidateData> _collisionCandidates;
|
||||||
private NativeParallelMultiHashMap<long, int> _enemySeparationBuckets;
|
private NativeParallelMultiHashMap<long, int> _enemySeparationBuckets;
|
||||||
|
private NativeParallelMultiHashMap<long, int> _enemyCollisionBuckets;
|
||||||
|
private readonly List<AreaCollisionRequestData> _areaCollisionRequests = new(16);
|
||||||
|
private readonly List<AreaCollisionHitEventData> _areaCollisionHitEvents = new(32);
|
||||||
|
private readonly HashSet<long> _areaCollisionHitDedupKeys = new();
|
||||||
|
private int _lastCollisionQueryCount;
|
||||||
|
private int _lastProjectileCollisionQueryCount;
|
||||||
|
private int _lastAreaCollisionQueryCount;
|
||||||
|
private int _lastCollisionCandidateCount;
|
||||||
|
private int _lastProjectileCollisionCandidateCount;
|
||||||
|
private int _lastAreaCollisionCandidateCount;
|
||||||
|
private int _lastResolvedAreaHitCount;
|
||||||
|
private float _lastCollisionCellSize;
|
||||||
|
private bool _lastCollisionHasEnemyTargets;
|
||||||
|
|
||||||
|
public int CollisionCandidateCount => _collisionCandidates.IsCreated ? _collisionCandidates.Length : 0;
|
||||||
|
public int PendingAreaCollisionRequestCount => _areaCollisionRequests.Count;
|
||||||
|
public int LastCollisionQueryCount => _lastCollisionQueryCount;
|
||||||
|
public int LastProjectileCollisionQueryCount => _lastProjectileCollisionQueryCount;
|
||||||
|
public int LastAreaCollisionQueryCount => _lastAreaCollisionQueryCount;
|
||||||
|
public int LastCollisionCandidateCount => _lastCollisionCandidateCount;
|
||||||
|
public int LastProjectileCollisionCandidateCount => _lastProjectileCollisionCandidateCount;
|
||||||
|
public int LastAreaCollisionCandidateCount => _lastAreaCollisionCandidateCount;
|
||||||
|
public int LastResolvedAreaHitCount => _lastResolvedAreaHitCount;
|
||||||
|
public float LastCollisionCellSize => _lastCollisionCellSize;
|
||||||
|
public bool LastCollisionHasEnemyTargets => _lastCollisionHasEnemyTargets;
|
||||||
|
|
||||||
private void InitializeJobDataChannels()
|
private void InitializeJobDataChannels()
|
||||||
{
|
{
|
||||||
|
|
@ -83,7 +171,10 @@ namespace Simulation
|
||||||
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
|
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
|
||||||
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
|
_projectileJobInputs = new NativeList<ProjectileJobInputData>(64, Allocator.Persistent);
|
||||||
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
_projectileJobOutputs = new NativeList<ProjectileJobOutputData>(64, Allocator.Persistent);
|
||||||
|
_collisionQueryInputs = new NativeList<CollisionQueryData>(64, Allocator.Persistent);
|
||||||
|
_collisionCandidates = new NativeList<CollisionCandidateData>(128, Allocator.Persistent);
|
||||||
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
_enemySeparationBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||||
|
_enemyCollisionBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
|
||||||
InitializeEnemyTargetSpatialIndex();
|
InitializeEnemyTargetSpatialIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,57 +184,94 @@ namespace Simulation
|
||||||
{
|
{
|
||||||
_enemyJobInputs.Dispose();
|
_enemyJobInputs.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemyJobInputs = default;
|
_enemyJobInputs = default;
|
||||||
|
|
||||||
if (_enemyJobOutputs.IsCreated)
|
if (_enemyJobOutputs.IsCreated)
|
||||||
{
|
{
|
||||||
_enemyJobOutputs.Dispose();
|
_enemyJobOutputs.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemyJobOutputs = default;
|
_enemyJobOutputs = default;
|
||||||
|
|
||||||
if (_enemyJobSeparationOutputs.IsCreated)
|
if (_enemyJobSeparationOutputs.IsCreated)
|
||||||
{
|
{
|
||||||
_enemyJobSeparationOutputs.Dispose();
|
_enemyJobSeparationOutputs.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemyJobSeparationOutputs = default;
|
_enemyJobSeparationOutputs = default;
|
||||||
|
|
||||||
if (_enemySeparationPreviousPushes.IsCreated)
|
if (_enemySeparationPreviousPushes.IsCreated)
|
||||||
{
|
{
|
||||||
_enemySeparationPreviousPushes.Dispose();
|
_enemySeparationPreviousPushes.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemySeparationPreviousPushes = default;
|
_enemySeparationPreviousPushes = default;
|
||||||
|
|
||||||
if (_enemySeparationCurrentPushes.IsCreated)
|
if (_enemySeparationCurrentPushes.IsCreated)
|
||||||
{
|
{
|
||||||
_enemySeparationCurrentPushes.Dispose();
|
_enemySeparationCurrentPushes.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemySeparationCurrentPushes = default;
|
_enemySeparationCurrentPushes = default;
|
||||||
|
|
||||||
if (_projectileJobInputs.IsCreated)
|
if (_projectileJobInputs.IsCreated)
|
||||||
{
|
{
|
||||||
_projectileJobInputs.Dispose();
|
_projectileJobInputs.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_projectileJobInputs = default;
|
_projectileJobInputs = default;
|
||||||
|
|
||||||
if (_projectileJobOutputs.IsCreated)
|
if (_projectileJobOutputs.IsCreated)
|
||||||
{
|
{
|
||||||
_projectileJobOutputs.Dispose();
|
_projectileJobOutputs.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_projectileJobOutputs = default;
|
_projectileJobOutputs = default;
|
||||||
|
|
||||||
|
if (_collisionQueryInputs.IsCreated)
|
||||||
|
{
|
||||||
|
_collisionQueryInputs.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_collisionQueryInputs = default;
|
||||||
|
|
||||||
|
if (_collisionCandidates.IsCreated)
|
||||||
|
{
|
||||||
|
_collisionCandidates.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_collisionCandidates = default;
|
||||||
|
|
||||||
if (_enemySeparationBuckets.IsCreated)
|
if (_enemySeparationBuckets.IsCreated)
|
||||||
{
|
{
|
||||||
_enemySeparationBuckets.Dispose();
|
_enemySeparationBuckets.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_enemySeparationBuckets = default;
|
_enemySeparationBuckets = default;
|
||||||
|
|
||||||
|
if (_enemyCollisionBuckets.IsCreated)
|
||||||
|
{
|
||||||
|
_enemyCollisionBuckets.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_enemyCollisionBuckets = default;
|
||||||
|
|
||||||
DisposeEnemyTargetSpatialIndex();
|
DisposeEnemyTargetSpatialIndex();
|
||||||
|
_areaCollisionRequests.Clear();
|
||||||
|
_areaCollisionHitEvents.Clear();
|
||||||
|
_areaCollisionHitDedupKeys.Clear();
|
||||||
|
ResetCollisionRuntimeStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearJobDataChannels()
|
private void ClearJobDataChannels()
|
||||||
{
|
{
|
||||||
if (!AreJobDataChannelsUsable())
|
if (!AreJobDataChannelsUsable())
|
||||||
{
|
{
|
||||||
|
_areaCollisionRequests.Clear();
|
||||||
|
_areaCollisionHitEvents.Clear();
|
||||||
|
_areaCollisionHitDedupKeys.Clear();
|
||||||
|
ResetCollisionRuntimeStats();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,6 +295,16 @@ namespace Simulation
|
||||||
_projectileJobOutputs.Clear();
|
_projectileJobOutputs.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_collisionQueryInputs.IsCreated)
|
||||||
|
{
|
||||||
|
_collisionQueryInputs.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_collisionCandidates.IsCreated)
|
||||||
|
{
|
||||||
|
_collisionCandidates.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (_enemyJobSeparationOutputs.IsCreated)
|
if (_enemyJobSeparationOutputs.IsCreated)
|
||||||
{
|
{
|
||||||
_enemyJobSeparationOutputs.Clear();
|
_enemyJobSeparationOutputs.Clear();
|
||||||
|
|
@ -187,7 +325,29 @@ namespace Simulation
|
||||||
_enemySeparationBuckets.Clear();
|
_enemySeparationBuckets.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_enemyCollisionBuckets.IsCreated)
|
||||||
|
{
|
||||||
|
_enemyCollisionBuckets.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
ClearEnemyTargetSpatialIndex();
|
ClearEnemyTargetSpatialIndex();
|
||||||
|
_areaCollisionRequests.Clear();
|
||||||
|
_areaCollisionHitEvents.Clear();
|
||||||
|
_areaCollisionHitDedupKeys.Clear();
|
||||||
|
ResetCollisionRuntimeStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetCollisionRuntimeStats()
|
||||||
|
{
|
||||||
|
_lastCollisionQueryCount = 0;
|
||||||
|
_lastProjectileCollisionQueryCount = 0;
|
||||||
|
_lastAreaCollisionQueryCount = 0;
|
||||||
|
_lastCollisionCandidateCount = 0;
|
||||||
|
_lastProjectileCollisionCandidateCount = 0;
|
||||||
|
_lastAreaCollisionCandidateCount = 0;
|
||||||
|
_lastResolvedAreaHitCount = 0;
|
||||||
|
_lastCollisionCellSize = 0f;
|
||||||
|
_lastCollisionHasEnemyTargets = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncSimulationToJobInput()
|
private void SyncSimulationToJobInput()
|
||||||
|
|
@ -242,6 +402,18 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PrepareProjectileJobOutputBuffer(int projectileCount)
|
||||||
|
{
|
||||||
|
InitializeJobDataChannels();
|
||||||
|
EnsureCapacity(ref _projectileJobOutputs, projectileCount);
|
||||||
|
_projectileJobOutputs.Clear();
|
||||||
|
|
||||||
|
if (projectileCount > 0)
|
||||||
|
{
|
||||||
|
_projectileJobOutputs.ResizeUninitialized(projectileCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SyncProjectilesToJobOutput()
|
private void SyncProjectilesToJobOutput()
|
||||||
{
|
{
|
||||||
InitializeJobDataChannels();
|
InitializeJobDataChannels();
|
||||||
|
|
@ -254,6 +426,116 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CopyProjectileInputToOutput()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _projectileJobInputs.Length; i++)
|
||||||
|
{
|
||||||
|
ProjectileJobInputData input = _projectileJobInputs[i];
|
||||||
|
_projectileJobOutputs[i] = new ProjectileJobOutputData
|
||||||
|
{
|
||||||
|
EntityId = input.EntityId,
|
||||||
|
OwnerEntityId = input.OwnerEntityId,
|
||||||
|
Position = input.Position,
|
||||||
|
Forward = input.Forward,
|
||||||
|
Velocity = input.Velocity,
|
||||||
|
Speed = input.Speed,
|
||||||
|
LifeTime = input.LifeTime,
|
||||||
|
Age = input.Age,
|
||||||
|
Active = input.Active,
|
||||||
|
RemainingLifetime = input.RemainingLifetime,
|
||||||
|
State = input.State
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrepareCollisionCandidateChannels(int queryCount, int expectedCandidateCount, int bucketCapacity)
|
||||||
|
{
|
||||||
|
InitializeJobDataChannels();
|
||||||
|
EnsureCapacity(ref _collisionQueryInputs, queryCount);
|
||||||
|
EnsureCapacity(ref _collisionCandidates, expectedCandidateCount);
|
||||||
|
EnsureCapacity(ref _enemyCollisionBuckets, bucketCapacity);
|
||||||
|
|
||||||
|
_collisionQueryInputs.Clear();
|
||||||
|
_collisionCandidates.Clear();
|
||||||
|
_enemyCollisionBuckets.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddProjectileCollisionQuery(int queryId, in ProjectileJobOutputData projectile, float radius,
|
||||||
|
int maxTargets = 1)
|
||||||
|
{
|
||||||
|
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_collisionQueryInputs.Add(new CollisionQueryData
|
||||||
|
{
|
||||||
|
QueryId = queryId,
|
||||||
|
SourceType = CollisionSourceTypeProjectile,
|
||||||
|
SourceEntityId = projectile.EntityId,
|
||||||
|
SourceOwnerEntityId = projectile.OwnerEntityId,
|
||||||
|
Position = new float3(projectile.Position.x, projectile.Position.y, projectile.Position.z),
|
||||||
|
Radius = radius,
|
||||||
|
MaxTargets = math.max(1, maxTargets),
|
||||||
|
ShapeType = CollisionShapeCircle,
|
||||||
|
Direction = new float3(0f, 0f, 1f),
|
||||||
|
HalfAngleDeg = 180f
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
|
||||||
|
float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg)
|
||||||
|
{
|
||||||
|
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 normalizedDirection = direction;
|
||||||
|
normalizedDirection.y = 0f;
|
||||||
|
if (normalizedDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
normalizedDirection = Vector3.forward;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
normalizedDirection.Normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
_collisionQueryInputs.Add(new CollisionQueryData
|
||||||
|
{
|
||||||
|
QueryId = queryId,
|
||||||
|
SourceType = CollisionSourceTypeArea,
|
||||||
|
SourceEntityId = sourceEntityId,
|
||||||
|
SourceOwnerEntityId = sourceOwnerEntityId,
|
||||||
|
Position = new float3(center.x, center.y, center.z),
|
||||||
|
Radius = radius,
|
||||||
|
MaxTargets = math.max(1, maxTargets),
|
||||||
|
ShapeType = shapeType,
|
||||||
|
Direction = new float3(normalizedDirection.x, normalizedDirection.y, normalizedDirection.z),
|
||||||
|
HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCollisionCandidate(int queryId, int sourceType, int sourceEntityId, int sourceOwnerEntityId,
|
||||||
|
int targetEntityId, float sqrDistance)
|
||||||
|
{
|
||||||
|
if (!_collisionCandidates.IsCreated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_collisionCandidates.Add(new CollisionCandidateData
|
||||||
|
{
|
||||||
|
QueryId = queryId,
|
||||||
|
SourceType = sourceType,
|
||||||
|
SourceEntityId = sourceEntityId,
|
||||||
|
SourceOwnerEntityId = sourceOwnerEntityId,
|
||||||
|
TargetEntityId = targetEntityId,
|
||||||
|
SqrDistance = sqrDistance
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
|
private void PrepareEnemySeparationJobBuffers(int enemyCount, int bucketCapacity)
|
||||||
{
|
{
|
||||||
InitializeJobDataChannels();
|
InitializeJobDataChannels();
|
||||||
|
|
@ -358,7 +640,10 @@ namespace Simulation
|
||||||
IsNativeListUsable(_enemySeparationCurrentPushes) &&
|
IsNativeListUsable(_enemySeparationCurrentPushes) &&
|
||||||
IsNativeListUsable(_projectileJobInputs) &&
|
IsNativeListUsable(_projectileJobInputs) &&
|
||||||
IsNativeListUsable(_projectileJobOutputs) &&
|
IsNativeListUsable(_projectileJobOutputs) &&
|
||||||
IsNativeMultiHashMapUsable(_enemySeparationBuckets);
|
IsNativeListUsable(_collisionQueryInputs) &&
|
||||||
|
IsNativeListUsable(_collisionCandidates) &&
|
||||||
|
IsNativeMultiHashMapUsable(_enemySeparationBuckets) &&
|
||||||
|
IsNativeMultiHashMapUsable(_enemyCollisionBuckets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
private static void EnsureCapacity<T>(ref NativeList<T> nativeList, int targetCount) where T : unmanaged
|
||||||
|
|
@ -472,7 +757,11 @@ namespace Simulation
|
||||||
OwnerEntityId = projectile.OwnerEntityId,
|
OwnerEntityId = projectile.OwnerEntityId,
|
||||||
Position = projectile.Position,
|
Position = projectile.Position,
|
||||||
Forward = projectile.Forward,
|
Forward = projectile.Forward,
|
||||||
|
Velocity = projectile.Velocity,
|
||||||
Speed = projectile.Speed,
|
Speed = projectile.Speed,
|
||||||
|
LifeTime = projectile.LifeTime,
|
||||||
|
Age = projectile.Age,
|
||||||
|
Active = projectile.Active,
|
||||||
RemainingLifetime = projectile.RemainingLifetime,
|
RemainingLifetime = projectile.RemainingLifetime,
|
||||||
State = projectile.State
|
State = projectile.State
|
||||||
};
|
};
|
||||||
|
|
@ -486,7 +775,11 @@ namespace Simulation
|
||||||
OwnerEntityId = projectile.OwnerEntityId,
|
OwnerEntityId = projectile.OwnerEntityId,
|
||||||
Position = projectile.Position,
|
Position = projectile.Position,
|
||||||
Forward = projectile.Forward,
|
Forward = projectile.Forward,
|
||||||
|
Velocity = projectile.Velocity,
|
||||||
Speed = projectile.Speed,
|
Speed = projectile.Speed,
|
||||||
|
LifeTime = projectile.LifeTime,
|
||||||
|
Age = projectile.Age,
|
||||||
|
Active = projectile.Active,
|
||||||
RemainingLifetime = projectile.RemainingLifetime,
|
RemainingLifetime = projectile.RemainingLifetime,
|
||||||
State = projectile.State
|
State = projectile.State
|
||||||
};
|
};
|
||||||
|
|
@ -500,7 +793,11 @@ namespace Simulation
|
||||||
OwnerEntityId = projectile.OwnerEntityId,
|
OwnerEntityId = projectile.OwnerEntityId,
|
||||||
Position = projectile.Position,
|
Position = projectile.Position,
|
||||||
Forward = projectile.Forward,
|
Forward = projectile.Forward,
|
||||||
|
Velocity = projectile.Velocity,
|
||||||
Speed = projectile.Speed,
|
Speed = projectile.Speed,
|
||||||
|
LifeTime = projectile.LifeTime,
|
||||||
|
Age = projectile.Age,
|
||||||
|
Active = projectile.Active,
|
||||||
RemainingLifetime = projectile.RemainingLifetime,
|
RemainingLifetime = projectile.RemainingLifetime,
|
||||||
State = projectile.State
|
State = projectile.State
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,83 @@
|
||||||
|
using CustomEvent;
|
||||||
|
using Entity;
|
||||||
|
using Entity.EntityData;
|
||||||
|
using Entity.Weapon;
|
||||||
|
using GameFramework.Event;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Simulation
|
namespace Simulation
|
||||||
{
|
{
|
||||||
public partial class SimulationWorld
|
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 sealed class Presentation
|
||||||
{
|
{
|
||||||
private readonly SimulationWorld _world;
|
private readonly SimulationWorld _world;
|
||||||
|
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
|
||||||
|
private bool _isProjectileHitEventSubscribed;
|
||||||
|
|
||||||
public Presentation(SimulationWorld world)
|
public Presentation(SimulationWorld world)
|
||||||
{
|
{
|
||||||
_world = 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()
|
public void OnLateUpdate()
|
||||||
{
|
{
|
||||||
if (_world == null || !_world.UseSimulationMovement)
|
if (_world == null || !_world.UseSimulationMovement)
|
||||||
|
|
@ -27,9 +92,9 @@ namespace Simulation
|
||||||
}
|
}
|
||||||
|
|
||||||
var enemies = enemyManager.Enemies;
|
var enemies = enemyManager.Enemies;
|
||||||
for (int i = 0; i < enemies.Count; i++)
|
foreach (var enemy in enemies)
|
||||||
{
|
{
|
||||||
if (enemies[i] is not EnemyBase enemyEntity || !enemyEntity.Available)
|
if (enemy is not EnemyBase enemyEntity || !enemyEntity.Available)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +106,85 @@ namespace Simulation
|
||||||
|
|
||||||
ApplyEnemyPresentation(enemyEntity, enemyData);
|
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)
|
private static void ApplyEnemyPresentation(EnemyBase enemyEntity, in EnemySimData enemyData)
|
||||||
|
|
@ -64,6 +208,25 @@ namespace Simulation
|
||||||
enemyTransform.forward = forward.normalized;
|
enemyTransform.forward = forward.normalized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyProjectilePresentation(EntityBase projectileEntity,
|
||||||
|
in ProjectileSimData projectileData)
|
||||||
|
{
|
||||||
|
Transform projectileTransform = projectileEntity.CachedTransform;
|
||||||
|
if (projectileTransform == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projectileTransform.position = projectileData.Position;
|
||||||
|
Vector3 forward = projectileData.Forward;
|
||||||
|
if (forward.sqrMagnitude <= float.Epsilon)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projectileTransform.rotation = Quaternion.LookRotation(forward.normalized, Vector3.up);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bcb6513f18404795a654500d3d2f8ef9
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -15,6 +15,8 @@ namespace Simulation
|
||||||
private const int EnemyStateIdle = 0;
|
private const int EnemyStateIdle = 0;
|
||||||
private const int EnemyStateChasing = 1;
|
private const int EnemyStateChasing = 1;
|
||||||
private const int EnemyStateInAttackRange = 2;
|
private const int EnemyStateInAttackRange = 2;
|
||||||
|
private const int ProjectileStateActive = 0;
|
||||||
|
private const int ProjectileStateExpired = 1;
|
||||||
|
|
||||||
private struct EnemyTickWorkItem
|
private struct EnemyTickWorkItem
|
||||||
{
|
{
|
||||||
|
|
@ -49,6 +51,8 @@ namespace Simulation
|
||||||
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 HashSet<int> _projectileResolvedEntityIds = new HashSet<int>();
|
||||||
private readonly List<EnemySeparationAgent> _enemySeparationAgents = new List<EnemySeparationAgent>();
|
private readonly List<EnemySeparationAgent> _enemySeparationAgents = new List<EnemySeparationAgent>();
|
||||||
private readonly List<EnemyTickWorkItem> _enemyTickWorkItems = new List<EnemyTickWorkItem>();
|
private readonly List<EnemyTickWorkItem> _enemyTickWorkItems = new List<EnemyTickWorkItem>();
|
||||||
|
|
||||||
|
|
@ -89,10 +93,12 @@ namespace Simulation
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
_entitySync?.OnStart();
|
_entitySync?.OnStart();
|
||||||
|
_presentation?.OnStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
|
_presentation?.OnDestroy();
|
||||||
_entitySync?.OnDestroy();
|
_entitySync?.OnDestroy();
|
||||||
_entitySync = null;
|
_entitySync = null;
|
||||||
_presentation = null;
|
_presentation = null;
|
||||||
|
|
@ -216,14 +222,14 @@ namespace Simulation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterProjectileLifecycle(EntityBase projectileEntity)
|
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
|
||||||
{
|
{
|
||||||
if (projectileEntity == null || projectileEntity.CachedTransform == null)
|
if (projectileEntity == null || projectileEntity.CachedTransform == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity));
|
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity, userData));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnregisterProjectileLifecycle(int entityId)
|
private void UnregisterProjectileLifecycle(int entityId)
|
||||||
|
|
@ -313,6 +319,11 @@ namespace Simulation
|
||||||
_enemies.Clear();
|
_enemies.Clear();
|
||||||
_projectiles.Clear();
|
_projectiles.Clear();
|
||||||
_pickups.Clear();
|
_pickups.Clear();
|
||||||
|
_projectileRecycleEntityIds.Clear();
|
||||||
|
_projectileResolvedEntityIds.Clear();
|
||||||
|
_areaCollisionRequests.Clear();
|
||||||
|
_areaCollisionHitEvents.Clear();
|
||||||
|
_areaCollisionHitDedupKeys.Clear();
|
||||||
_enemySeparationAgents.Clear();
|
_enemySeparationAgents.Clear();
|
||||||
_enemyTickWorkItems.Clear();
|
_enemyTickWorkItems.Clear();
|
||||||
ClearJobDataChannels();
|
ClearJobDataChannels();
|
||||||
|
|
@ -510,6 +521,10 @@ namespace Simulation
|
||||||
speed = movementComponent.Speed;
|
speed = movementComponent.Speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float attackRange = enemy != null && enemy.AttackRange > 0f
|
||||||
|
? enemy.AttackRange
|
||||||
|
: DefaultAttackRange;
|
||||||
|
|
||||||
return new EnemySimData
|
return new EnemySimData
|
||||||
{
|
{
|
||||||
EntityId = enemy.Id,
|
EntityId = enemy.Id,
|
||||||
|
|
@ -517,7 +532,7 @@ namespace Simulation
|
||||||
Forward = enemyTransform.forward,
|
Forward = enemyTransform.forward,
|
||||||
Rotation = enemyTransform.rotation,
|
Rotation = enemyTransform.rotation,
|
||||||
Speed = speed,
|
Speed = speed,
|
||||||
AttackRange = 1f,
|
AttackRange = attackRange,
|
||||||
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
|
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
|
||||||
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
|
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
|
||||||
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
|
SeparationIterations = movementComponent != null ? movementComponent.SeparationIterations : 2,
|
||||||
|
|
@ -537,17 +552,52 @@ namespace Simulation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity)
|
private static ProjectileSimData CreateProjectileInitialSimData(EntityBase projectileEntity, object userData)
|
||||||
{
|
{
|
||||||
|
Vector3 forward = projectileEntity.CachedTransform.forward;
|
||||||
|
int ownerEntityId = 0;
|
||||||
|
Vector3 velocity = Vector3.zero;
|
||||||
|
float speed = 0f;
|
||||||
|
float lifeTime = 0f;
|
||||||
|
|
||||||
|
if (userData is EnemyProjectileData enemyProjectileData)
|
||||||
|
{
|
||||||
|
ownerEntityId = enemyProjectileData.OwnerEntityId;
|
||||||
|
|
||||||
|
Vector3 direction = enemyProjectileData.Direction;
|
||||||
|
direction.y = 0f;
|
||||||
|
if (direction.sqrMagnitude > Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
direction.Normalize();
|
||||||
|
forward = direction;
|
||||||
|
}
|
||||||
|
else if (forward.sqrMagnitude > Mathf.Epsilon)
|
||||||
|
{
|
||||||
|
forward = forward.normalized;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
forward = Vector3.forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
speed = Mathf.Max(0f, enemyProjectileData.Speed);
|
||||||
|
velocity = forward * speed;
|
||||||
|
lifeTime = Mathf.Max(0f, enemyProjectileData.LifeTime);
|
||||||
|
}
|
||||||
|
|
||||||
return new ProjectileSimData
|
return new ProjectileSimData
|
||||||
{
|
{
|
||||||
EntityId = projectileEntity.Id,
|
EntityId = projectileEntity.Id,
|
||||||
OwnerEntityId = 0,
|
OwnerEntityId = ownerEntityId,
|
||||||
Position = projectileEntity.CachedTransform.position,
|
Position = projectileEntity.CachedTransform.position,
|
||||||
Forward = projectileEntity.CachedTransform.forward,
|
Forward = forward,
|
||||||
Speed = 0f,
|
Velocity = velocity,
|
||||||
RemainingLifetime = 0f,
|
Speed = speed,
|
||||||
State = 0
|
LifeTime = lifeTime,
|
||||||
|
Age = 0f,
|
||||||
|
Active = true,
|
||||||
|
RemainingLifetime = lifeTime,
|
||||||
|
State = ProjectileStateActive
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,8 @@ namespace UI
|
||||||
return new WeaponHandgunData(entityId, ownerId, ownerCamp);
|
return new WeaponHandgunData(entityId, ownerId, ownerCamp);
|
||||||
case WeaponType.WeaponSlash:
|
case WeaponType.WeaponSlash:
|
||||||
return new WeaponSlashData(entityId, ownerId, ownerCamp);
|
return new WeaponSlashData(entityId, ownerId, ownerCamp);
|
||||||
|
case WeaponType.WeaponLightning:
|
||||||
|
return new WeaponLightningData(entityId, ownerId, ownerCamp);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,26 @@ namespace CustomUtility
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
EnemyProjectile enemyProjectile = other as EnemyProjectile;
|
||||||
|
if (enemyProjectile != null)
|
||||||
|
{
|
||||||
|
if (!enemyProjectile.IsActive) return;
|
||||||
|
|
||||||
|
ImpactData entityImpactData = entity.GetImpactData();
|
||||||
|
ImpactData projectileImpactData = enemyProjectile.GetImpactData();
|
||||||
|
if (GetRelation(entityImpactData.Camp, projectileImpactData.Camp) == RelationType.Friendly)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int entityDamageHP = CalcDamageHP(projectileImpactData.AttackBase, projectileImpactData.AttackStat,
|
||||||
|
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
|
||||||
|
|
||||||
|
entity.ApplyDamage(enemyProjectile, entityDamageHP);
|
||||||
|
enemyProjectile.Expire();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
WeaponBase weapon = other as WeaponBase;
|
WeaponBase weapon = other as WeaponBase;
|
||||||
if (weapon != null)
|
if (weapon != null)
|
||||||
{
|
{
|
||||||
|
|
@ -220,13 +240,13 @@ namespace CustomUtility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CalcDamageHP(int attack, StatProperty attackStat, StatProperty defenseStat,
|
public static int CalcDamageHP(int attack, StatProperty attackStat, StatProperty defenseStat,
|
||||||
StatProperty dodgeStat)
|
StatProperty dodgeStat)
|
||||||
{
|
{
|
||||||
// 1. 处理闪避(闪避率取值 (0, 0.9),不允许拉满闪避)
|
// 1. 处理闪避(闪避率取值 (0, 0.9),不允许拉满闪避)
|
||||||
if (dodgeStat != null)
|
if (dodgeStat != null)
|
||||||
{
|
{
|
||||||
if (Random.value < Mathf.Clamp(dodgeStat.Percent, 0, 0.9f)) return 0;
|
if (Random.value < Mathf.Clamp(dodgeStat.Value, 0, 0.9f)) return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 处理攻击加成 最终伤害 = (基础伤害 + 伤害提升固定值) * 伤害提升率
|
// 2. 处理攻击加成 最终伤害 = (基础伤害 + 伤害提升固定值) * 伤害提升率
|
||||||
|
|
|
||||||
|
|
@ -607,7 +607,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[0].m_InstanceCapacity
|
propertyPath: m_EntityGroups.Array.data[0].m_InstanceCapacity
|
||||||
value: 16
|
value: 10
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[1].m_InstanceCapacity
|
propertyPath: m_EntityGroups.Array.data[1].m_InstanceCapacity
|
||||||
|
|
@ -623,7 +623,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[4].m_InstanceCapacity
|
propertyPath: m_EntityGroups.Array.data[4].m_InstanceCapacity
|
||||||
value: 2
|
value: 1
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_EntityGroups.Array.data[5].m_InstanceCapacity
|
propertyPath: m_EntityGroups.Array.data[5].m_InstanceCapacity
|
||||||
|
|
@ -1432,11 +1432,25 @@ MonoBehaviour:
|
||||||
_enemySeparationCellSize: 0
|
_enemySeparationCellSize: 0
|
||||||
_enemySeparationPushDamping: 0.5
|
_enemySeparationPushDamping: 0.5
|
||||||
_enemySeparationMaxStepScale: 0.5
|
_enemySeparationMaxStepScale: 0.5
|
||||||
_enemySeparationZeroDistancePushScale: 1
|
|
||||||
_enemySeparationUseTangentialInAttackRange: 1
|
_enemySeparationUseTangentialInAttackRange: 1
|
||||||
_enemySeparationPushSmoothing: 0.55
|
_enemySeparationPushSmoothing: 0.55
|
||||||
_enemySeparationPushCarry: 1
|
_projectileHitPresentationEnabled: 1
|
||||||
_enemySeparationMinPushRetain: 1
|
_projectileHitMarkerEnabled: 1
|
||||||
|
_projectileHitMarkerSize: 0.2
|
||||||
|
_projectileHitMarkerYOffset: 1.2
|
||||||
|
_projectileHitMarkerDuration: 0.15
|
||||||
|
_projectileHitMarkerColor: {r: 1, g: 0, b: 0, a: 0.95}
|
||||||
|
_projectileHitEffectEnabled: 0
|
||||||
|
_projectileHitEffectTypeId: 0
|
||||||
|
_projectileMaxDistanceFromPlayer: 120
|
||||||
|
_projectileMaxVerticalOffsetFromPlayer: 30
|
||||||
|
_projectileCollisionQueryRadius: 0.35
|
||||||
|
_projectileMaxCandidatesPerQuery: 1
|
||||||
|
_projectileCollisionCellSize: 0
|
||||||
|
_dispatchProjectileHitPresentationEvent: 1
|
||||||
|
_dispatchProjectileHitMarkerEvent: 1
|
||||||
|
_dispatchProjectileHitEffectEvent: 1
|
||||||
|
_projectileHitPresentationEffectTypeId: 0
|
||||||
_targetSelectionCellSize: 2
|
_targetSelectionCellSize: 2
|
||||||
--- !u!1 &1852670052
|
--- !u!1 &1852670052
|
||||||
GameObject:
|
GameObject:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,21 @@ namespace Simulation.Tests.Editor
|
||||||
private static readonly System.Type EnemySimDataType =
|
private static readonly System.Type EnemySimDataType =
|
||||||
System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}");
|
System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type ProjectileSimDataType =
|
||||||
|
System.Type.GetType($"Simulation.ProjectileSimData, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type EnemyProjectileType =
|
||||||
|
System.Type.GetType($"Entity.EnemyProjectile, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type EnemyProjectileDataType =
|
||||||
|
System.Type.GetType($"Entity.EntityData.EnemyProjectileData, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type CampTypeType =
|
||||||
|
System.Type.GetType($"Definition.Enum.CampType, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type GameEntryType =
|
||||||
|
System.Type.GetType($"GameEntry, {GameAssemblyName}");
|
||||||
|
|
||||||
private static readonly System.Type EnemySeparationSolverProviderType =
|
private static readonly System.Type EnemySeparationSolverProviderType =
|
||||||
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
|
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
|
||||||
|
|
||||||
|
|
@ -29,6 +44,12 @@ namespace Simulation.Tests.Editor
|
||||||
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
|
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
|
||||||
SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance);
|
SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo UpsertProjectileMethod =
|
||||||
|
SimulationWorldType?.GetMethod("UpsertProjectile", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo RemoveProjectileByEntityIdMethod =
|
||||||
|
SimulationWorldType?.GetMethod("RemoveProjectileByEntityId", NonPublicInstance);
|
||||||
|
|
||||||
private static readonly MethodInfo TryGetEnemyDataMethod =
|
private static readonly MethodInfo TryGetEnemyDataMethod =
|
||||||
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
|
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
|
||||||
|
|
||||||
|
|
@ -56,6 +77,39 @@ namespace Simulation.Tests.Editor
|
||||||
private static readonly PropertyInfo EnemiesProperty =
|
private static readonly PropertyInfo EnemiesProperty =
|
||||||
SimulationWorldType?.GetProperty("Enemies", PublicInstance);
|
SimulationWorldType?.GetProperty("Enemies", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly PropertyInfo ProjectilesProperty =
|
||||||
|
SimulationWorldType?.GetProperty("Projectiles", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly PropertyInfo CollisionCandidateCountProperty =
|
||||||
|
SimulationWorldType?.GetProperty("CollisionCandidateCount", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo EnemyProjectileOnUpdateMethod =
|
||||||
|
EnemyProjectileType?.GetMethod("OnUpdate", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo EnemyProjectileDataField =
|
||||||
|
EnemyProjectileType?.GetField("_projectileData", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo EnemyProjectileIsActiveField =
|
||||||
|
EnemyProjectileType?.GetField("_isActive", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo EnemyProjectileIsSimulationDrivenField =
|
||||||
|
EnemyProjectileType?.GetField("_isSimulationDriven", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly PropertyInfo GameEntrySimulationWorldProperty =
|
||||||
|
GameEntryType?.GetProperty("SimulationWorld", PublicStatic);
|
||||||
|
|
||||||
|
private static readonly MethodInfo GameEntryGetSimulationWorldMethod =
|
||||||
|
GameEntrySimulationWorldProperty?.GetGetMethod(true);
|
||||||
|
|
||||||
|
private static readonly MethodInfo GameEntrySetSimulationWorldMethod =
|
||||||
|
GameEntrySimulationWorldProperty?.GetSetMethod(true);
|
||||||
|
|
||||||
|
private static readonly FieldInfo ProjectileMaxDistanceFromPlayerField =
|
||||||
|
SimulationWorldType?.GetField("_projectileMaxDistanceFromPlayer", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo ProjectileMaxVerticalOffsetFromPlayerField =
|
||||||
|
SimulationWorldType?.GetField("_projectileMaxVerticalOffsetFromPlayer", NonPublicInstance);
|
||||||
|
|
||||||
private GameObject _worldGameObject;
|
private GameObject _worldGameObject;
|
||||||
private Component _worldComponent;
|
private Component _worldComponent;
|
||||||
|
|
||||||
|
|
@ -65,9 +119,16 @@ namespace Simulation.Tests.Editor
|
||||||
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
|
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
|
||||||
Assert.NotNull(SimulationTickContextType, "SimulationTickContext type lookup failed.");
|
Assert.NotNull(SimulationTickContextType, "SimulationTickContext type lookup failed.");
|
||||||
Assert.NotNull(EnemySimDataType, "EnemySimData type lookup failed.");
|
Assert.NotNull(EnemySimDataType, "EnemySimData type lookup failed.");
|
||||||
|
Assert.NotNull(ProjectileSimDataType, "ProjectileSimData type lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileType, "EnemyProjectile type lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileDataType, "EnemyProjectileData type lookup failed.");
|
||||||
|
Assert.NotNull(CampTypeType, "CampType type lookup failed.");
|
||||||
|
Assert.NotNull(GameEntryType, "GameEntry type lookup failed.");
|
||||||
Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed.");
|
Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed.");
|
||||||
Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed.");
|
Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed.");
|
||||||
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
||||||
|
Assert.NotNull(UpsertProjectileMethod, "UpsertProjectile reflection lookup failed.");
|
||||||
|
Assert.NotNull(RemoveProjectileByEntityIdMethod, "RemoveProjectileByEntityId reflection lookup failed.");
|
||||||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||||
Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed.");
|
Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed.");
|
||||||
|
|
@ -75,6 +136,22 @@ namespace Simulation.Tests.Editor
|
||||||
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
||||||
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||||
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
||||||
|
Assert.NotNull(ProjectilesProperty, "Projectiles property reflection lookup failed.");
|
||||||
|
Assert.NotNull(CollisionCandidateCountProperty, "CollisionCandidateCount property reflection lookup failed.");
|
||||||
|
Assert.NotNull(ProjectileMaxDistanceFromPlayerField,
|
||||||
|
"Projectile max distance field reflection lookup failed.");
|
||||||
|
Assert.NotNull(ProjectileMaxVerticalOffsetFromPlayerField,
|
||||||
|
"Projectile max vertical offset field reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileOnUpdateMethod, "EnemyProjectile.OnUpdate reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileDataField, "EnemyProjectile _projectileData reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileIsActiveField, "EnemyProjectile _isActive reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileIsSimulationDrivenField,
|
||||||
|
"EnemyProjectile _isSimulationDriven reflection lookup failed.");
|
||||||
|
Assert.NotNull(GameEntrySimulationWorldProperty, "GameEntry.SimulationWorld property lookup failed.");
|
||||||
|
Assert.NotNull(GameEntryGetSimulationWorldMethod,
|
||||||
|
"GameEntry.SimulationWorld getter reflection lookup failed.");
|
||||||
|
Assert.NotNull(GameEntrySetSimulationWorldMethod,
|
||||||
|
"GameEntry.SimulationWorld setter reflection lookup failed.");
|
||||||
|
|
||||||
_worldGameObject = new GameObject("SimulationWorldTickTests");
|
_worldGameObject = new GameObject("SimulationWorldTickTests");
|
||||||
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
|
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
|
||||||
|
|
@ -230,6 +307,209 @@ namespace Simulation.Tests.Editor
|
||||||
Assert.That(distance, Is.GreaterThanOrEqualTo(0.5f));
|
Assert.That(distance, Is.GreaterThanOrEqualTo(0.5f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_MovesAndUpdatesLifetime_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5101, position: Vector3.zero, forward: Vector3.right,
|
||||||
|
velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 2f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 2f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(1));
|
||||||
|
object projectile = GetProjectileAt(0);
|
||||||
|
Vector3 position = (Vector3)GetField(projectile, "Position");
|
||||||
|
float age = (float)GetField(projectile, "Age");
|
||||||
|
float remainingLifetime = (float)GetField(projectile, "RemainingLifetime");
|
||||||
|
bool active = (bool)GetField(projectile, "Active");
|
||||||
|
|
||||||
|
Assert.That(position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(age, Is.EqualTo(0.5f).Within(0.0001f));
|
||||||
|
Assert.That(remainingLifetime, Is.EqualTo(1.5f).Within(0.0001f));
|
||||||
|
Assert.IsTrue(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_ResumesFromLatestState_AfterTogglingJobSimulation()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5110, position: Vector3.zero, forward: Vector3.right,
|
||||||
|
velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 5f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 5f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
object afterJobEnabled = GetProjectileAt(0);
|
||||||
|
Vector3 positionAfterJobEnabled = (Vector3)GetField(afterJobEnabled, "Position");
|
||||||
|
float ageAfterJobEnabled = (float)GetField(afterJobEnabled, "Age");
|
||||||
|
Assert.That(positionAfterJobEnabled.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(ageAfterJobEnabled, Is.EqualTo(0.5f).Within(0.0001f));
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { false });
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
object afterJobDisabled = GetProjectileAt(0);
|
||||||
|
Vector3 positionAfterJobDisabled = (Vector3)GetField(afterJobDisabled, "Position");
|
||||||
|
float ageAfterJobDisabled = (float)GetField(afterJobDisabled, "Age");
|
||||||
|
Assert.That(positionAfterJobDisabled.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(ageAfterJobDisabled, Is.EqualTo(0.5f).Within(0.0001f));
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
object afterJobReEnabled = GetProjectileAt(0);
|
||||||
|
Vector3 positionAfterJobReEnabled = (Vector3)GetField(afterJobReEnabled, "Position");
|
||||||
|
float ageAfterJobReEnabled = (float)GetField(afterJobReEnabled, "Age");
|
||||||
|
float remainingLifetimeAfterJobReEnabled = (float)GetField(afterJobReEnabled, "RemainingLifetime");
|
||||||
|
bool activeAfterJobReEnabled = (bool)GetField(afterJobReEnabled, "Active");
|
||||||
|
|
||||||
|
Assert.That(positionAfterJobReEnabled.x, Is.EqualTo(2f).Within(0.0001f));
|
||||||
|
Assert.That(ageAfterJobReEnabled, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(remainingLifetimeAfterJobReEnabled, Is.EqualTo(4f).Within(0.0001f));
|
||||||
|
Assert.IsTrue(activeAfterJobReEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EnemyProjectile_TogglesCollider_WhenJobSimulationSwitchesAtRuntime()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { false });
|
||||||
|
|
||||||
|
GameObject projectileObject = new GameObject("EnemyProjectileColliderToggleEditMode");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Component projectileComponent = projectileObject.AddComponent(EnemyProjectileType);
|
||||||
|
Collider projectileCollider = projectileObject.AddComponent<BoxCollider>();
|
||||||
|
projectileCollider.enabled = true;
|
||||||
|
|
||||||
|
object previousSimulationWorld = GetGameEntrySimulationWorld();
|
||||||
|
SetGameEntrySimulationWorld(_worldComponent);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object neutralCamp = System.Enum.Parse(CampTypeType, "Neutral");
|
||||||
|
object projectileData = System.Activator.CreateInstance(
|
||||||
|
EnemyProjectileDataType,
|
||||||
|
BindingFlags.Public | BindingFlags.Instance,
|
||||||
|
null,
|
||||||
|
new object[] { 7001, 1, neutralCamp, 1, 0f, 10f, Vector3.forward },
|
||||||
|
null);
|
||||||
|
|
||||||
|
EnemyProjectileDataField.SetValue(projectileComponent, projectileData);
|
||||||
|
EnemyProjectileIsActiveField.SetValue(projectileComponent, true);
|
||||||
|
EnemyProjectileIsSimulationDrivenField.SetValue(projectileComponent, false);
|
||||||
|
|
||||||
|
InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f);
|
||||||
|
Assert.IsTrue(projectileCollider.enabled);
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f);
|
||||||
|
Assert.IsFalse(projectileCollider.enabled);
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { false });
|
||||||
|
InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f);
|
||||||
|
Assert.IsTrue(projectileCollider.enabled);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetGameEntrySimulationWorld(previousSimulationWorld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(projectileObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveProjectileByEntityId_RemapIndex_ForMovedProjectile()
|
||||||
|
{
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5105, position: new Vector3(0f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5106, position: new Vector3(1f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5107, position: new Vector3(2f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
|
||||||
|
bool removed = RemoveProjectileByEntityId(5106);
|
||||||
|
bool removedMoved = RemoveProjectileByEntityId(5107);
|
||||||
|
|
||||||
|
Assert.IsTrue(removed);
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(1));
|
||||||
|
Assert.IsTrue(removedMoved);
|
||||||
|
Assert.That((int)GetField(GetProjectileAt(0), "EntityId"), Is.EqualTo(5105));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_RecyclesWhenExceedingPlayerDistance_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 5f);
|
||||||
|
ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1000f);
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5108, position: new Vector3(6f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_RecyclesWhenExceedingVerticalOffset_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 0f);
|
||||||
|
ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1f);
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5109, position: new Vector3(0f, 2f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_RecyclesExpiredProjectile_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5102, position: Vector3.zero, forward: Vector3.forward,
|
||||||
|
velocity: Vector3.zero, speed: 0f, lifeTime: 1f, age: 0.95f, active: true, remainingLifetime: 0.05f,
|
||||||
|
state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_BuildsCollisionCandidatesAgainstEnemies_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 5201, position: Vector3.zero, speed: 0f, attackRange: 1f));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5202, position: Vector3.zero, forward: Vector3.forward,
|
||||||
|
velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true, remainingLifetime: 2f,
|
||||||
|
state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TickProjectiles_ExpiresAfterCollisionCandidateConsumed_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 5203, position: Vector3.zero, speed: 0f, attackRange: 1f));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5204, position: Vector3.zero, forward: Vector3.forward,
|
||||||
|
velocity: Vector3.zero, speed: 0f, lifeTime: 10f, age: 0f, active: true, remainingLifetime: 10f,
|
||||||
|
state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange,
|
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange,
|
||||||
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
|
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
|
||||||
{
|
{
|
||||||
|
|
@ -248,6 +528,24 @@ namespace Simulation.Tests.Editor
|
||||||
return enemy;
|
return enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object CreateProjectile(int entityId, Vector3 position, Vector3 forward, Vector3 velocity, float speed,
|
||||||
|
float lifeTime, float age, bool active, float remainingLifetime, int state)
|
||||||
|
{
|
||||||
|
object projectile = System.Activator.CreateInstance(ProjectileSimDataType);
|
||||||
|
SetField(ref projectile, "EntityId", entityId);
|
||||||
|
SetField(ref projectile, "OwnerEntityId", 0);
|
||||||
|
SetField(ref projectile, "Position", position);
|
||||||
|
SetField(ref projectile, "Forward", forward);
|
||||||
|
SetField(ref projectile, "Velocity", velocity);
|
||||||
|
SetField(ref projectile, "Speed", speed);
|
||||||
|
SetField(ref projectile, "LifeTime", lifeTime);
|
||||||
|
SetField(ref projectile, "Age", age);
|
||||||
|
SetField(ref projectile, "Active", active);
|
||||||
|
SetField(ref projectile, "RemainingLifetime", remainingLifetime);
|
||||||
|
SetField(ref projectile, "State", state);
|
||||||
|
return projectile;
|
||||||
|
}
|
||||||
|
|
||||||
private void InvokeTick(float deltaTime, float realDeltaTime, Vector3 playerPosition)
|
private void InvokeTick(float deltaTime, float realDeltaTime, Vector3 playerPosition)
|
||||||
{
|
{
|
||||||
object tickContext = System.Activator.CreateInstance(
|
object tickContext = System.Activator.CreateInstance(
|
||||||
|
|
@ -265,11 +563,37 @@ namespace Simulation.Tests.Editor
|
||||||
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
|
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpsertProjectile(object projectile)
|
||||||
|
{
|
||||||
|
UpsertProjectileMethod.Invoke(_worldComponent, new[] { projectile });
|
||||||
|
}
|
||||||
|
|
||||||
private bool RemoveEnemyByEntityId(int entityId)
|
private bool RemoveEnemyByEntityId(int entityId)
|
||||||
{
|
{
|
||||||
return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
|
return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool RemoveProjectileByEntityId(int entityId)
|
||||||
|
{
|
||||||
|
return (bool)RemoveProjectileByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetGameEntrySimulationWorld()
|
||||||
|
{
|
||||||
|
return GameEntryGetSimulationWorldMethod.Invoke(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetGameEntrySimulationWorld(object simulationWorld)
|
||||||
|
{
|
||||||
|
GameEntrySetSimulationWorldMethod.Invoke(null, new[] { simulationWorld });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InvokeEnemyProjectileUpdate(Component projectileComponent, float elapseSeconds,
|
||||||
|
float realElapseSeconds)
|
||||||
|
{
|
||||||
|
EnemyProjectileOnUpdateMethod.Invoke(projectileComponent, new object[] { elapseSeconds, realElapseSeconds });
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryGetEnemyData(int entityId, out object enemyData)
|
private bool TryGetEnemyData(int entityId, out object enemyData)
|
||||||
{
|
{
|
||||||
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
|
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
|
||||||
|
|
@ -293,6 +617,25 @@ namespace Simulation.Tests.Editor
|
||||||
return (int)countProperty.GetValue(enemies);
|
return (int)countProperty.GetValue(enemies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object GetProjectileAt(int index)
|
||||||
|
{
|
||||||
|
object projectiles = ProjectilesProperty.GetValue(_worldComponent);
|
||||||
|
PropertyInfo itemProperty = projectiles.GetType().GetProperty("Item", PublicInstance);
|
||||||
|
return itemProperty.GetValue(projectiles, new object[] { index });
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetProjectilesCount()
|
||||||
|
{
|
||||||
|
object projectiles = ProjectilesProperty.GetValue(_worldComponent);
|
||||||
|
PropertyInfo countProperty = projectiles.GetType().GetProperty("Count", PublicInstance);
|
||||||
|
return (int)countProperty.GetValue(projectiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetCollisionCandidateCount()
|
||||||
|
{
|
||||||
|
return (int)CollisionCandidateCountProperty.GetValue(_worldComponent);
|
||||||
|
}
|
||||||
|
|
||||||
private static object GetField(object target, string fieldName)
|
private static object GetField(object target, string fieldName)
|
||||||
{
|
{
|
||||||
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
|
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,21 @@ namespace Simulation.Tests.PlayMode
|
||||||
private static readonly System.Type EnemySimDataType =
|
private static readonly System.Type EnemySimDataType =
|
||||||
System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}");
|
System.Type.GetType($"Simulation.EnemySimData, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type ProjectileSimDataType =
|
||||||
|
System.Type.GetType($"Simulation.ProjectileSimData, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type EnemyProjectileType =
|
||||||
|
System.Type.GetType($"Entity.EnemyProjectile, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type EnemyProjectileDataType =
|
||||||
|
System.Type.GetType($"Entity.EntityData.EnemyProjectileData, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type CampTypeType =
|
||||||
|
System.Type.GetType($"Definition.Enum.CampType, {GameAssemblyName}");
|
||||||
|
|
||||||
|
private static readonly System.Type GameEntryType =
|
||||||
|
System.Type.GetType($"GameEntry, {GameAssemblyName}");
|
||||||
|
|
||||||
private static readonly System.Type EnemySeparationSolverProviderType =
|
private static readonly System.Type EnemySeparationSolverProviderType =
|
||||||
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
|
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
|
||||||
|
|
||||||
|
|
@ -31,6 +46,12 @@ namespace Simulation.Tests.PlayMode
|
||||||
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
|
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
|
||||||
SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance);
|
SimulationWorldType?.GetMethod("RemoveEnemyByEntityId", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo UpsertProjectileMethod =
|
||||||
|
SimulationWorldType?.GetMethod("UpsertProjectile", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo RemoveProjectileByEntityIdMethod =
|
||||||
|
SimulationWorldType?.GetMethod("RemoveProjectileByEntityId", NonPublicInstance);
|
||||||
|
|
||||||
private static readonly MethodInfo TryGetEnemyDataMethod =
|
private static readonly MethodInfo TryGetEnemyDataMethod =
|
||||||
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
|
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
|
||||||
|
|
||||||
|
|
@ -58,6 +79,39 @@ namespace Simulation.Tests.PlayMode
|
||||||
private static readonly PropertyInfo EnemiesProperty =
|
private static readonly PropertyInfo EnemiesProperty =
|
||||||
SimulationWorldType?.GetProperty("Enemies", PublicInstance);
|
SimulationWorldType?.GetProperty("Enemies", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly PropertyInfo ProjectilesProperty =
|
||||||
|
SimulationWorldType?.GetProperty("Projectiles", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly PropertyInfo CollisionCandidateCountProperty =
|
||||||
|
SimulationWorldType?.GetProperty("CollisionCandidateCount", PublicInstance);
|
||||||
|
|
||||||
|
private static readonly MethodInfo EnemyProjectileOnUpdateMethod =
|
||||||
|
EnemyProjectileType?.GetMethod("OnUpdate", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo EnemyProjectileDataField =
|
||||||
|
EnemyProjectileType?.GetField("_projectileData", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo EnemyProjectileIsActiveField =
|
||||||
|
EnemyProjectileType?.GetField("_isActive", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo EnemyProjectileIsSimulationDrivenField =
|
||||||
|
EnemyProjectileType?.GetField("_isSimulationDriven", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly PropertyInfo GameEntrySimulationWorldProperty =
|
||||||
|
GameEntryType?.GetProperty("SimulationWorld", PublicStatic);
|
||||||
|
|
||||||
|
private static readonly MethodInfo GameEntryGetSimulationWorldMethod =
|
||||||
|
GameEntrySimulationWorldProperty?.GetGetMethod(true);
|
||||||
|
|
||||||
|
private static readonly MethodInfo GameEntrySetSimulationWorldMethod =
|
||||||
|
GameEntrySimulationWorldProperty?.GetSetMethod(true);
|
||||||
|
|
||||||
|
private static readonly FieldInfo ProjectileMaxDistanceFromPlayerField =
|
||||||
|
SimulationWorldType?.GetField("_projectileMaxDistanceFromPlayer", NonPublicInstance);
|
||||||
|
|
||||||
|
private static readonly FieldInfo ProjectileMaxVerticalOffsetFromPlayerField =
|
||||||
|
SimulationWorldType?.GetField("_projectileMaxVerticalOffsetFromPlayer", NonPublicInstance);
|
||||||
|
|
||||||
private GameObject _worldGameObject;
|
private GameObject _worldGameObject;
|
||||||
private Component _worldComponent;
|
private Component _worldComponent;
|
||||||
|
|
||||||
|
|
@ -67,9 +121,16 @@ namespace Simulation.Tests.PlayMode
|
||||||
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
|
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
|
||||||
Assert.NotNull(SimulationTickContextType, "SimulationTickContext type lookup failed.");
|
Assert.NotNull(SimulationTickContextType, "SimulationTickContext type lookup failed.");
|
||||||
Assert.NotNull(EnemySimDataType, "EnemySimData type lookup failed.");
|
Assert.NotNull(EnemySimDataType, "EnemySimData type lookup failed.");
|
||||||
|
Assert.NotNull(ProjectileSimDataType, "ProjectileSimData type lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileType, "EnemyProjectile type lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileDataType, "EnemyProjectileData type lookup failed.");
|
||||||
|
Assert.NotNull(CampTypeType, "CampType type lookup failed.");
|
||||||
|
Assert.NotNull(GameEntryType, "GameEntry type lookup failed.");
|
||||||
Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed.");
|
Assert.NotNull(EnemySeparationSolverProviderType, "EnemySeparationSolverProvider type lookup failed.");
|
||||||
Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed.");
|
Assert.NotNull(UpsertEnemyMethod, "UpsertEnemy reflection lookup failed.");
|
||||||
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
Assert.NotNull(RemoveEnemyByEntityIdMethod, "RemoveEnemyByEntityId reflection lookup failed.");
|
||||||
|
Assert.NotNull(UpsertProjectileMethod, "UpsertProjectile reflection lookup failed.");
|
||||||
|
Assert.NotNull(RemoveProjectileByEntityIdMethod, "RemoveProjectileByEntityId reflection lookup failed.");
|
||||||
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
Assert.NotNull(TryGetEnemyDataMethod, "TryGetEnemyData reflection lookup failed.");
|
||||||
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
Assert.NotNull(TickMethod, "Tick reflection lookup failed.");
|
||||||
Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed.");
|
Assert.NotNull(TryGetNearestEnemyEntityIdMethod, "TryGetNearestEnemyEntityId reflection lookup failed.");
|
||||||
|
|
@ -77,6 +138,22 @@ namespace Simulation.Tests.PlayMode
|
||||||
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
Assert.NotNull(SetUseJobSimulationMethod, "SetUseJobSimulation reflection lookup failed.");
|
||||||
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
Assert.NotNull(UseGridBucketSolverMethod, "UseGridBucketSolver reflection lookup failed.");
|
||||||
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
Assert.NotNull(EnemiesProperty, "Enemies property reflection lookup failed.");
|
||||||
|
Assert.NotNull(ProjectilesProperty, "Projectiles property reflection lookup failed.");
|
||||||
|
Assert.NotNull(CollisionCandidateCountProperty, "CollisionCandidateCount property reflection lookup failed.");
|
||||||
|
Assert.NotNull(ProjectileMaxDistanceFromPlayerField,
|
||||||
|
"Projectile max distance field reflection lookup failed.");
|
||||||
|
Assert.NotNull(ProjectileMaxVerticalOffsetFromPlayerField,
|
||||||
|
"Projectile max vertical offset field reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileOnUpdateMethod, "EnemyProjectile.OnUpdate reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileDataField, "EnemyProjectile _projectileData reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileIsActiveField, "EnemyProjectile _isActive reflection lookup failed.");
|
||||||
|
Assert.NotNull(EnemyProjectileIsSimulationDrivenField,
|
||||||
|
"EnemyProjectile _isSimulationDriven reflection lookup failed.");
|
||||||
|
Assert.NotNull(GameEntrySimulationWorldProperty, "GameEntry.SimulationWorld property lookup failed.");
|
||||||
|
Assert.NotNull(GameEntryGetSimulationWorldMethod,
|
||||||
|
"GameEntry.SimulationWorld getter reflection lookup failed.");
|
||||||
|
Assert.NotNull(GameEntrySetSimulationWorldMethod,
|
||||||
|
"GameEntry.SimulationWorld setter reflection lookup failed.");
|
||||||
|
|
||||||
_worldGameObject = new GameObject("SimulationWorldPlayModeTests");
|
_worldGameObject = new GameObject("SimulationWorldPlayModeTests");
|
||||||
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
|
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
|
||||||
|
|
@ -246,6 +323,223 @@ namespace Simulation.Tests.PlayMode
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_MovesAndUpdatesLifetime_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5401, position: Vector3.zero, forward: Vector3.right,
|
||||||
|
velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 2f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 2f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(1));
|
||||||
|
object projectile = GetProjectileAt(0);
|
||||||
|
Vector3 position = (Vector3)GetField(projectile, "Position");
|
||||||
|
float age = (float)GetField(projectile, "Age");
|
||||||
|
float remainingLifetime = (float)GetField(projectile, "RemainingLifetime");
|
||||||
|
bool active = (bool)GetField(projectile, "Active");
|
||||||
|
|
||||||
|
Assert.That(position.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(age, Is.EqualTo(0.5f).Within(0.0001f));
|
||||||
|
Assert.That(remainingLifetime, Is.EqualTo(1.5f).Within(0.0001f));
|
||||||
|
Assert.IsTrue(active);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_ResumesFromLatestState_AfterTogglingJobSimulation()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5410, position: Vector3.zero, forward: Vector3.right,
|
||||||
|
velocity: new Vector3(2f, 0f, 0f), speed: 0f, lifeTime: 5f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 5f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
object afterJobEnabled = GetProjectileAt(0);
|
||||||
|
Vector3 positionAfterJobEnabled = (Vector3)GetField(afterJobEnabled, "Position");
|
||||||
|
float ageAfterJobEnabled = (float)GetField(afterJobEnabled, "Age");
|
||||||
|
Assert.That(positionAfterJobEnabled.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(ageAfterJobEnabled, Is.EqualTo(0.5f).Within(0.0001f));
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { false });
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
object afterJobDisabled = GetProjectileAt(0);
|
||||||
|
Vector3 positionAfterJobDisabled = (Vector3)GetField(afterJobDisabled, "Position");
|
||||||
|
float ageAfterJobDisabled = (float)GetField(afterJobDisabled, "Age");
|
||||||
|
Assert.That(positionAfterJobDisabled.x, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(ageAfterJobDisabled, Is.EqualTo(0.5f).Within(0.0001f));
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
InvokeTick(deltaTime: 0.5f, realDeltaTime: 0.5f, playerPosition: Vector3.zero);
|
||||||
|
object afterJobReEnabled = GetProjectileAt(0);
|
||||||
|
Vector3 positionAfterJobReEnabled = (Vector3)GetField(afterJobReEnabled, "Position");
|
||||||
|
float ageAfterJobReEnabled = (float)GetField(afterJobReEnabled, "Age");
|
||||||
|
float remainingLifetimeAfterJobReEnabled = (float)GetField(afterJobReEnabled, "RemainingLifetime");
|
||||||
|
bool activeAfterJobReEnabled = (bool)GetField(afterJobReEnabled, "Active");
|
||||||
|
|
||||||
|
Assert.That(positionAfterJobReEnabled.x, Is.EqualTo(2f).Within(0.0001f));
|
||||||
|
Assert.That(ageAfterJobReEnabled, Is.EqualTo(1f).Within(0.0001f));
|
||||||
|
Assert.That(remainingLifetimeAfterJobReEnabled, Is.EqualTo(4f).Within(0.0001f));
|
||||||
|
Assert.IsTrue(activeAfterJobReEnabled);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator EnemyProjectile_TogglesCollider_WhenJobSimulationSwitchesAtRuntime()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { false });
|
||||||
|
|
||||||
|
GameObject projectileObject = new GameObject("EnemyProjectileColliderTogglePlayMode");
|
||||||
|
Component projectileComponent = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
projectileComponent = projectileObject.AddComponent(EnemyProjectileType);
|
||||||
|
Collider projectileCollider = projectileObject.AddComponent<BoxCollider>();
|
||||||
|
projectileCollider.enabled = true;
|
||||||
|
|
||||||
|
object previousSimulationWorld = GetGameEntrySimulationWorld();
|
||||||
|
SetGameEntrySimulationWorld(_worldComponent);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object neutralCamp = System.Enum.Parse(CampTypeType, "Neutral");
|
||||||
|
object projectileData = System.Activator.CreateInstance(
|
||||||
|
EnemyProjectileDataType,
|
||||||
|
BindingFlags.Public | BindingFlags.Instance,
|
||||||
|
null,
|
||||||
|
new object[] { 8001, 1, neutralCamp, 1, 0f, 10f, Vector3.forward },
|
||||||
|
null);
|
||||||
|
|
||||||
|
EnemyProjectileDataField.SetValue(projectileComponent, projectileData);
|
||||||
|
EnemyProjectileIsActiveField.SetValue(projectileComponent, true);
|
||||||
|
EnemyProjectileIsSimulationDrivenField.SetValue(projectileComponent, false);
|
||||||
|
|
||||||
|
InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f);
|
||||||
|
Assert.IsTrue(projectileCollider.enabled);
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f);
|
||||||
|
Assert.IsFalse(projectileCollider.enabled);
|
||||||
|
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { false });
|
||||||
|
InvokeEnemyProjectileUpdate(projectileComponent, 0.016f, 0.016f);
|
||||||
|
Assert.IsTrue(projectileCollider.enabled);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetGameEntrySimulationWorld(previousSimulationWorld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (projectileObject != null)
|
||||||
|
{
|
||||||
|
Object.Destroy(projectileObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator RemoveProjectileByEntityId_RemapIndex_ForMovedProjectile()
|
||||||
|
{
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5405, position: new Vector3(0f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5406, position: new Vector3(1f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5407, position: new Vector3(2f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
|
||||||
|
bool removed = RemoveProjectileByEntityId(5406);
|
||||||
|
bool removedMoved = RemoveProjectileByEntityId(5407);
|
||||||
|
|
||||||
|
Assert.IsTrue(removed);
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(1));
|
||||||
|
Assert.IsTrue(removedMoved);
|
||||||
|
Assert.That((int)GetField(GetProjectileAt(0), "EntityId"), Is.EqualTo(5405));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_RecyclesWhenExceedingPlayerDistance_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 5f);
|
||||||
|
ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1000f);
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5408, position: new Vector3(6f, 0f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_RecyclesWhenExceedingVerticalOffset_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
ProjectileMaxDistanceFromPlayerField.SetValue(_worldComponent, 0f);
|
||||||
|
ProjectileMaxVerticalOffsetFromPlayerField.SetValue(_worldComponent, 1f);
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5409, position: new Vector3(0f, 2f, 0f),
|
||||||
|
forward: Vector3.forward, velocity: Vector3.zero, speed: 0f, lifeTime: 3f, age: 0f, active: true,
|
||||||
|
remainingLifetime: 3f, state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_RecyclesExpiredProjectile_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5402, position: Vector3.zero, forward: Vector3.forward,
|
||||||
|
velocity: Vector3.zero, speed: 0f, lifeTime: 1f, age: 0.95f, active: true, remainingLifetime: 0.05f,
|
||||||
|
state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.1f, realDeltaTime: 0.1f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_BuildsCollisionCandidatesAgainstEnemies_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 5501, position: Vector3.zero, speed: 0f, attackRange: 1f));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5502, position: Vector3.zero, forward: Vector3.forward,
|
||||||
|
velocity: Vector3.zero, speed: 0f, lifeTime: 2f, age: 0f, active: true, remainingLifetime: 2f,
|
||||||
|
state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetCollisionCandidateCount(), Is.GreaterThan(0));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnityTest]
|
||||||
|
public IEnumerator TickProjectiles_ExpiresAfterCollisionCandidateConsumed_WhenJobSimulationEnabled()
|
||||||
|
{
|
||||||
|
SetUseJobSimulationMethod.Invoke(_worldComponent, new object[] { true });
|
||||||
|
UpsertEnemy(CreateEnemy(entityId: 5503, position: Vector3.zero, speed: 0f, attackRange: 1f));
|
||||||
|
UpsertProjectile(CreateProjectile(entityId: 5504, position: Vector3.zero, forward: Vector3.forward,
|
||||||
|
velocity: Vector3.zero, speed: 0f, lifeTime: 10f, age: 0f, active: true, remainingLifetime: 10f,
|
||||||
|
state: 0));
|
||||||
|
|
||||||
|
InvokeTick(deltaTime: 0.016f, realDeltaTime: 0.016f, playerPosition: Vector3.zero);
|
||||||
|
|
||||||
|
Assert.That(GetProjectilesCount(), Is.EqualTo(0));
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange,
|
private object CreateEnemy(int entityId, Vector3 position, float speed, float attackRange,
|
||||||
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
|
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
|
||||||
{
|
{
|
||||||
|
|
@ -264,6 +558,24 @@ namespace Simulation.Tests.PlayMode
|
||||||
return enemy;
|
return enemy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object CreateProjectile(int entityId, Vector3 position, Vector3 forward, Vector3 velocity, float speed,
|
||||||
|
float lifeTime, float age, bool active, float remainingLifetime, int state)
|
||||||
|
{
|
||||||
|
object projectile = System.Activator.CreateInstance(ProjectileSimDataType);
|
||||||
|
SetField(ref projectile, "EntityId", entityId);
|
||||||
|
SetField(ref projectile, "OwnerEntityId", 0);
|
||||||
|
SetField(ref projectile, "Position", position);
|
||||||
|
SetField(ref projectile, "Forward", forward);
|
||||||
|
SetField(ref projectile, "Velocity", velocity);
|
||||||
|
SetField(ref projectile, "Speed", speed);
|
||||||
|
SetField(ref projectile, "LifeTime", lifeTime);
|
||||||
|
SetField(ref projectile, "Age", age);
|
||||||
|
SetField(ref projectile, "Active", active);
|
||||||
|
SetField(ref projectile, "RemainingLifetime", remainingLifetime);
|
||||||
|
SetField(ref projectile, "State", state);
|
||||||
|
return projectile;
|
||||||
|
}
|
||||||
|
|
||||||
private void InvokeTick(float deltaTime, float realDeltaTime, Vector3 playerPosition)
|
private void InvokeTick(float deltaTime, float realDeltaTime, Vector3 playerPosition)
|
||||||
{
|
{
|
||||||
object tickContext = System.Activator.CreateInstance(
|
object tickContext = System.Activator.CreateInstance(
|
||||||
|
|
@ -281,11 +593,37 @@ namespace Simulation.Tests.PlayMode
|
||||||
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
|
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpsertProjectile(object projectile)
|
||||||
|
{
|
||||||
|
UpsertProjectileMethod.Invoke(_worldComponent, new[] { projectile });
|
||||||
|
}
|
||||||
|
|
||||||
private bool RemoveEnemyByEntityId(int entityId)
|
private bool RemoveEnemyByEntityId(int entityId)
|
||||||
{
|
{
|
||||||
return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
|
return (bool)RemoveEnemyByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool RemoveProjectileByEntityId(int entityId)
|
||||||
|
{
|
||||||
|
return (bool)RemoveProjectileByEntityIdMethod.Invoke(_worldComponent, new object[] { entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object GetGameEntrySimulationWorld()
|
||||||
|
{
|
||||||
|
return GameEntryGetSimulationWorldMethod.Invoke(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetGameEntrySimulationWorld(object simulationWorld)
|
||||||
|
{
|
||||||
|
GameEntrySetSimulationWorldMethod.Invoke(null, new[] { simulationWorld });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InvokeEnemyProjectileUpdate(Component projectileComponent, float elapseSeconds,
|
||||||
|
float realElapseSeconds)
|
||||||
|
{
|
||||||
|
EnemyProjectileOnUpdateMethod.Invoke(projectileComponent, new object[] { elapseSeconds, realElapseSeconds });
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryGetEnemyData(int entityId, out object enemyData)
|
private bool TryGetEnemyData(int entityId, out object enemyData)
|
||||||
{
|
{
|
||||||
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
|
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
|
||||||
|
|
@ -309,6 +647,25 @@ namespace Simulation.Tests.PlayMode
|
||||||
return (int)countProperty.GetValue(enemies);
|
return (int)countProperty.GetValue(enemies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object GetProjectileAt(int index)
|
||||||
|
{
|
||||||
|
object projectiles = ProjectilesProperty.GetValue(_worldComponent);
|
||||||
|
PropertyInfo itemProperty = projectiles.GetType().GetProperty("Item", PublicInstance);
|
||||||
|
return itemProperty.GetValue(projectiles, new object[] { index });
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetProjectilesCount()
|
||||||
|
{
|
||||||
|
object projectiles = ProjectilesProperty.GetValue(_worldComponent);
|
||||||
|
PropertyInfo countProperty = projectiles.GetType().GetProperty("Count", PublicInstance);
|
||||||
|
return (int)countProperty.GetValue(projectiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetCollisionCandidateCount()
|
||||||
|
{
|
||||||
|
return (int)CollisionCandidateCountProperty.GetValue(_worldComponent);
|
||||||
|
}
|
||||||
|
|
||||||
private static object GetField(object target, string fieldName)
|
private static object GetField(object target, string fieldName)
|
||||||
{
|
{
|
||||||
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
|
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);
|
||||||
|
|
|
||||||
|
|
@ -144,13 +144,13 @@
|
||||||
- 避免全量最近邻搜索,控制复杂度随敌人数增长的斜率。
|
- 避免全量最近邻搜索,控制复杂度随敌人数增长的斜率。
|
||||||
- 完成标准:`3k` 敌人下目标选择阶段耗时稳定,且无索引越界/漏目标回归。
|
- 完成标准:`3k` 敌人下目标选择阶段耗时稳定,且无索引越界/漏目标回归。
|
||||||
|
|
||||||
- [ ] Checkpoint 5:投射物批量移动与寿命回收 Job 化
|
- [x] Checkpoint 5:投射物批量移动与寿命回收 Job 化
|
||||||
- 投射物数据结构最少包含:`position/velocity/lifeTime/age/active`。
|
- 投射物数据结构最少包含:`position/velocity/lifeTime/age/active`。
|
||||||
- 迁移投射物移动、越界判定、寿命回收到 Job。
|
- 迁移投射物移动、越界判定、寿命回收到 Job。
|
||||||
- 回收后保持实体池与索引同步,防止悬空引用。
|
- 回收后保持实体池与索引同步,防止悬空引用。
|
||||||
- 完成标准:连续战斗下投射物数量曲线稳定,无异常积压或提前回收。
|
- 完成标准:连续战斗下投射物数量曲线稳定,无异常积压或提前回收。
|
||||||
|
|
||||||
- [ ] Checkpoint 6:AOE/碰撞候选筛选 Job 化(Broad Phase 优先)
|
- [x] Checkpoint 6:AOE/碰撞候选筛选 Job 化(Broad Phase 优先)
|
||||||
- 先 Job 化候选生成(Broad Phase),减少精算对数。
|
- 先 Job 化候选生成(Broad Phase),减少精算对数。
|
||||||
- 精算与伤害结算可先保留主线程,但输入改为候选列表驱动。
|
- 精算与伤害结算可先保留主线程,但输入改为候选列表驱动。
|
||||||
- 建立命中事件缓冲区,统一在主线程提交表现层事件。
|
- 建立命中事件缓冲区,统一在主线程提交表现层事件。
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
数据表/Goods.xlsx
BIN
数据表/Goods.xlsx
Binary file not shown.
BIN
数据表/Level.xlsx
BIN
数据表/Level.xlsx
Binary file not shown.
Loading…
Reference in New Issue