Checkpoint 5 & Checkpoint 6

This commit is contained in:
SepComet 2026-02-22 22:00:43 +08:00
parent 5fb7ea499f
commit f0abf78128
65 changed files with 3875 additions and 137 deletions

View File

@ -1,5 +1,6 @@
# 敌人基础属性表
# Id EntityTypeId MaxHealth HpAddPerLevel Speed CoinDrop ExpDrop DropPercent
# int int int int float int int float
# 敌人编号 策划备注 敌人实体编号 最大生命 每关卡增加生命 移动速度 金币掉落 经验掉落 掉落概率
1 近战敌人 101 50 50 3 5 1 0.3
# 敌人基础属性表
# Id EntityTypeId MaxHealth HpAddPerLevel AttackDamage AttackCooldown AttackRange Speed CoinDrop ExpDrop DropPercent Params
# int int int int int float float float int int float string
# 敌人编号 策划备注 敌人实体编号 最大生命 每关卡增加生命 基础伤害 攻击间隔 攻击范围 移动速度 金币掉落 经验掉落 掉落概率 额外参数
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 []

View File

@ -2,12 +2,13 @@
# Id AssetName
# int string
# 实体编号 策划备注 资源名称
11 跟随相机 FollowCamera
1001 测试玩家 Player
101 近战敌人 MeleeEnemy
102 远程敌人 RemoteEnemy
11 跟随相机 FollowCamera
201 武器小刀 WeaponKnife
202 武器手枪 WeaponHandgun
203 武器斧头 WeaponSlash
204 武器闪电 WeaponLightning
10001 金币实体 CoinEntity
10002 经验实体 ExpEntity

View File

@ -25,3 +25,4 @@
121 Prop 119
122 Prop 120
123 Weapon 3
124 Weapon 4

View File

@ -2,7 +2,7 @@
# Id EnemyTypes EntityCounts Interval Duration
# int int[] int[] float[] int
# 关卡号 策划备注 敌人类型 每次出怪数量 每次出怪间隔 关卡时间
1 第一关 [1] [5] [2] 60
1 第一关 [1,2] [5,2] [4,5] 60
2 第二关 [1] [10] [3] 60
3 第三关 [1] [10] [3] 60
4 第四关 [1] [10] [3] 60

View File

@ -5,3 +5,4 @@
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 [] []
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] []

View File

@ -150,13 +150,13 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5383497626468778460}
serializedVersion: 2
m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0, y: 15, z: 0}
m_LocalRotation: {x: 0.5, y: 0, z: 0, w: 0.8660254}
m_LocalPosition: {x: 0, y: 15, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9112716898534404901}
m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0}
m_LocalEulerAnglesHint: {x: 60, y: 0, z: 0}
--- !u!20 &4064848608618185461
Camera:
m_ObjectHideFlags: 0
@ -189,9 +189,9 @@ Camera:
width: 1
height: 1
near clip plane: 0.3
far clip plane: 100
far clip plane: 200
field of view: 80
orthographic: 1
orthographic: 0
orthographic size: 15
m_Depth: 0
m_CullingMask:

View File

@ -14,7 +14,7 @@ GameObject:
- component: {fileID: 1932268889601128120}
- component: {fileID: 557030043145096197}
- component: {fileID: 6353753365317756414}
m_Layer: 7
m_Layer: 8
m_Name: RemoteEnemy
m_TagString: Untagged
m_Icon: {fileID: 0}
@ -124,6 +124,9 @@ MonoBehaviour:
_isMoving: 0
_direction: {x: 0, y: 0, z: 0}
_cachedTransform: {fileID: 0}
_avoidEnemyOverlap: 0
_enemyBodyRadius: 0.45
_separationIterations: 2
_speedBase: 0
--- !u!114 &6353753365317756414
MonoBehaviour:

View File

@ -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}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9d193ac5b4294e0e9ba6e867320944b7
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -164,6 +164,22 @@ namespace CustomComponent
GUILayout.Label($"Battle Time: {enemyManager.ElapsedBattleTime:F1}s / {enemyManager.BattleDuration:F1}s");
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.Label("Rate", GUILayout.Width(52f));
string rateText = GUILayout.TextField(_spawnRateScaleInput.ToString("F2"), GUILayout.Width(60f));

View File

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using UnityGameFramework.Runtime;
namespace DataTable
@ -5,23 +7,31 @@ namespace DataTable
public class DREnemy : DataRowBase
{
private int m_id;
public override int Id => m_id;
public int EntityTypeId { get; private set; }
public int MaxHealth { 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 int DropCoin { get; private set; }
public int DropExp { 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)
{
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
@ -33,12 +43,47 @@ namespace DataTable
EntityTypeId = int.Parse(columnStrings[index++]);
MaxHealth = 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++]);
DropCoin = int.Parse(columnStrings[index++]);
DropExp = int.Parse(columnStrings[index++]);
DropPercent = float.Parse(columnStrings[index++]);
Params = DeserializeParams(columnStrings[index++]);
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;
}
}
}
}

View File

@ -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)
{
if (!rawParams.StartsWith('[') || !rawParams.EndsWith(']'))

View File

@ -9,6 +9,11 @@ namespace CustomDebugger
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_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_QueryNeighbors = new("TargetSelection.QueryNeighbors");
public static readonly ProfilerMarker Movement_Update = new ProfilerMarker("Movement_Update");

View File

@ -6,5 +6,6 @@ namespace Definition.Enum
WeaponKnife = 1,
WeaponHandgun = 2,
WeaponSlash = 3,
WeaponLightning = 4,
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using DataTable;
using Definition.Enum;
using UnityEngine;
@ -8,17 +9,7 @@ namespace Entity.EntityData
[Serializable]
public class EnemyData : TargetableObjectData
{
[SerializeField] private EnemyType _enemyType;
[SerializeField] private int _entityTypeId;
[SerializeField] private float _speedBase = 0;
[SerializeField] private int _dropCoin = 0;
[SerializeField] private int _dropExp = 0;
[SerializeField] private float _dropPercent = 0;
[SerializeField] private DREnemy _drEnemy;
public EnemyData(int entityId, EnemyType enemyType, int level) : base(
entityId, (int)enemyType, CampType.Enemy)
@ -29,30 +20,46 @@ namespace Entity.EntityData
{
throw new Exception($"Enemy data table row is missing, EnemyType='{enemyType}'.");
}
else
{
_drEnemy = enemyRow;
}
int effectiveLevel = Mathf.Max(1, level);
_enemyType = enemyType;
_entityTypeId = enemyRow.EntityTypeId;
MaxHealthBase = enemyRow.MaxHealth + enemyRow.HpAddPerLevel * (effectiveLevel - 1);
_speedBase = enemyRow.Speed;
_dropCoin = enemyRow.DropCoin;
_dropExp = enemyRow.DropExp;
_dropPercent = enemyRow.DropPercent;
}
public EnemyType EnemyType => _enemyType;
public int EntityTypeId => _entityTypeId;
public EnemyType EnemyType => (EnemyType)_drEnemy.Id;
public int EntityTypeId => _drEnemy.EntityTypeId;
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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52b38f83ab6c4029803d40c189db47c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -28,14 +28,15 @@ namespace Entity.EntityData
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>

View File

@ -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)
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25b0006918fd46959c7f6b8ec1bbc8ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -7,6 +7,7 @@ public abstract class EnemyBase : TargetableObject
protected Transform _target;
public abstract override ImpactData GetImpactData();
public virtual float AttackRange => 1f;
public virtual void SetTarget(Transform target) => _target = target;

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22624f81b9364c8681b32d993f5e618f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,7 +1,8 @@
using Components;
using CustomUtility;
using Definition.DataStruct;
using Definition.Enum;
using Entity.EntityData;
using Entity.Weapon;
using UnityEngine;
using UnityGameFramework.Runtime;
@ -9,17 +10,33 @@ namespace Entity
{
public class MeleeEnemy : EnemyBase
{
private enum AttackStateType
{
Idle,
Check_OutRange,
Check_InRange,
Attack
}
private MovementComponent _movementComponent;
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 WeaponBase _weapon;
private TargetableObject _targetableTarget;
protected override TargetableObjectData _targetableObjectData => _meleeEnemyData;
public override float AttackRange => _attackRange;
public override ImpactData GetImpactData()
{
return new ImpactData(_meleeEnemyData.Camp, 0);
return new ImpactData(_meleeEnemyData.Camp, _attackDamage);
}
#region FSM
@ -35,51 +52,42 @@ namespace Entity
protected override void OnShow(object userData)
{
base.OnShow(userData);
if (userData is EnemyData enemyData)
{
_meleeEnemyData = enemyData;
_healthComponent.OnInit(enemyData.MaxHealthBase);
_movementComponent.OnInit(_meleeEnemyData.SpeedBase, this.CachedTransform, null, 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;
}
else
{
Log.Error($"Invalid data type. Data type: {userData?.GetType()}");
}
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
UpdateAttackState(elapseSeconds);
if (IsSimulationMovementEnabled())
{
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);
}
@ -102,7 +110,7 @@ namespace Entity
};
GameEntry.Entity.ShowExp(data);
}
base.OnDead(attacker);
}
@ -110,12 +118,143 @@ namespace Entity
{
_movementComponent.OnReset();
_healthComponent.OnReset();
_targetableTarget = null;
_currAttackTimer = 0f;
_attackState = AttackStateType.Idle;
base.OnHide(isShutdown, userData);
}
#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()
{
if (_target == null)

View File

@ -1,4 +1,6 @@
using Components;
using CustomUtility;
using Definition;
using Definition.DataStruct;
using Entity.EntityData;
using UnityEngine;
@ -8,17 +10,39 @@ namespace Entity
{
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 float _attackRange = 1f;
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;
protected override TargetableObjectData _targetableObjectData => _remoteEnemyData;
public override float AttackRange => _attackRange;
public override ImpactData GetImpactData()
{
return new ImpactData(_remoteEnemyData.Camp, 0);
return new ImpactData(_remoteEnemyData.Camp, _attackDamage);
}
protected override void OnInit(object userData)
@ -39,7 +63,21 @@ namespace Entity
_healthComponent.OnInit(enemyData.MaxHealthBase);
_movementComponent.OnInit(_remoteEnemyData.SpeedBase, this.CachedTransform, null, true);
_movementComponent.SetMove(true);
_attackRange = Mathf.Max(0.1f, _remoteEnemyData.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;
}
else
@ -50,25 +88,28 @@ namespace Entity
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (IsSimulationMovementEnabled())
{
return;
}
base.OnUpdate(elapseSeconds, realElapseSeconds);
_currAttackTimer += elapseSeconds;
if (_target == null)
{
_movementComponent.SetMove(false);
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
if (!IsSimulationMovementEnabled())
{
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
}
return;
}
float distanceSquared = (this.CachedTransform.position - _target.position).sqrMagnitude;
if (distanceSquared < _attackRangeSquared)
Vector3 toTarget = _target.position - this.CachedTransform.position;
toTarget.y = 0f;
float distanceSquared = toTarget.sqrMagnitude;
if (distanceSquared <= _attackRangeSquared)
{
// 攻击
_movementComponent.SetMove(false);
TryFireProjectile();
}
else
{
@ -76,17 +117,115 @@ namespace Entity
_movementComponent.SetDirection(GetTargetDirection());
}
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
if (!IsSimulationMovementEnabled())
{
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
}
}
protected override void OnHide(bool isShutdown, object userData)
{
_movementComponent.OnReset();
_healthComponent.OnReset();
_currAttackTimer = 0f;
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()
{
if (_target == null)

View File

@ -61,8 +61,8 @@ namespace Entity
private void OnTriggerEnter(Collider other)
{
EntityBase entity = other.gameObject.GetComponent<EntityBase>();
if (entity == null)
EntityBase entity = other.GetComponentInParent<EntityBase>();
if (entity == null || entity == this)
{
return;
}
@ -70,7 +70,7 @@ namespace Entity
if (entity is TargetableObject && entity.Id < Id)
{
// 碰撞事件由 Id 大的一方处理
// 在这里规定所有的 Enemy 的 Id 均大于 0
// 在这里约定 Enemy 的 Id 为非负数(通常从 0 开始)
// 而其他的 Entity (Player, Weapon, Bullet) 的 Id 均小于 0
return;
}
@ -78,4 +78,4 @@ namespace Entity
AIUtility.PerformCollision(this, entity);
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81cbdf8961ad419c91b989d56ca782d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -220,6 +220,32 @@ namespace Entity.Weapon
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)
{
TargetSelector = CreateSelector(selectorType);
@ -286,4 +312,3 @@ namespace Entity.Weapon
}

View File

@ -27,7 +27,7 @@ namespace Entity.Weapon
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private IWeaponAttackEffect _attackEffect;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
@ -116,7 +116,15 @@ namespace Entity.Weapon
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,
QueryTriggerInteraction.Collide);
@ -149,12 +157,14 @@ namespace Entity.Weapon
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
string hitRadiusRaw = _weaponData.GetParamsString(HitRadiusParamKey);
if (!float.TryParse(hitRadiusRaw, out _hitRadius))
if (_weaponData.TryGetParam(HitRadiusParamKey, out string hitRadiusRaw))
{
_hitRadius = Mathf.Max(0.1f, float.Parse(hitRadiusRaw));
}
else
{
_hitRadius = _weaponData.AttackRange;
}
_hitRadius = Mathf.Max(0.1f, _hitRadius);
_hitRadiusSqr = _hitRadius * _hitRadius;
_attackEffect = new KnifeRangeAttackEffect();

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5fded69986744ded97edb5f2ac06304f
timeCreated: 1771578597

View File

@ -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()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25854832a7d3416dacab67bcfef2a2fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25404ad172434ba38609e797b6d96919
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3aa479abb79491d83e8de76806faf7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3464037c29774c3ea326dc93fe33551e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8315d9e60c4434ebde9b23c853f27d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -93,12 +93,8 @@ namespace Entity.Weapon
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;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
@ -107,8 +103,20 @@ namespace Entity.Weapon
}
forward.Normalize();
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++)
{
Collider collider = _hitResults[i];

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7771e0f6b92ece64395ada5cdea72858
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d02876e26d8342f7af024c697e451ea4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -91,6 +91,15 @@ namespace Procedure
{
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)
@ -101,4 +110,4 @@ namespace Procedure
#endregion
}
}
}

View File

@ -8,7 +8,11 @@ namespace Simulation
public int OwnerEntityId;
public Vector3 Position;
public Vector3 Forward;
public Vector3 Velocity;
public float Speed;
public float LifeTime;
public float Age;
public bool Active;
public float RemainingLifetime;
public int State;
}

View File

@ -125,25 +125,44 @@ namespace Simulation
private void TickEnemiesJobified(in SimulationTickContext context)
{
if (context.DeltaTime <= 0f)
{
PrepareCollisionCandidateChannels(0, 0, 0);
ResetCollisionRuntimeStats();
ClearAreaCollisionFrameBuffers();
return;
}
using (CustomProfilerMarker.TickEnemies_BuildInput.Auto())
{
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())
{
ExecuteEnemyMovementJob(in context);
ExecuteProjectileMovementJob(in context);
}
using (CustomProfilerMarker.TickEnemies_MoveSeparation.Auto())
{
ApplyEnemySeparationForJobOutput(in context);
BuildProjectileCollisionCandidates();
}
using (CustomProfilerMarker.TickEnemies_WriteBack.Auto())
{
SyncProjectilesToJobOutput();
ApplyJobOutputToSimulation();
ResolveProjectileCollisionCandidatesMainThread();
RecycleInactiveProjectiles();
}
MarkEnemyTargetSpatialIndexDirty();
@ -413,6 +432,9 @@ namespace Simulation
if (sqrDistance <= float.Epsilon)
{
float3 zeroDistanceAxis = GetZeroDistanceSeparationAxis(index, otherIndex);
float directionSign = index < otherIndex ? 1f : -1f;
pushAccumulation += zeroDistanceAxis * (selfRadius * 0.25f * directionSign);
continue;
}
@ -528,6 +550,18 @@ namespace Simulation
return ((long)x << 32) ^ (uint)z;
}
private static float3 GetZeroDistanceSeparationAxis(int index, int otherIndex)
{
int lowIndex = math.min(index, otherIndex);
int highIndex = math.max(index, otherIndex);
uint pairHash = (uint)(lowIndex * 73856093) ^ (uint)(highIndex * 19349663);
float axisX = (pairHash & 1023u) / 511.5f - 1f;
float axisZ = ((pairHash >> 10) & 1023u) / 511.5f - 1f;
float3 axis = new float3(axisX, 0f, axisZ);
return math.normalizesafe(axis, new float3(1f, 0f, 0f));
}
private static void ExecuteEnemyMovement(int index, NativeArray<EnemyJobInputData> inputs,
NativeArray<EnemyJobOutputData> outputs, float deltaTime, float3 playerPosition)
{
@ -587,4 +621,4 @@ namespace Simulation
};
}
}
}
}

View File

@ -12,6 +12,7 @@ namespace Simulation
private const string DropGroupName = "Drop";
private const string BulletGroupName = "Bullet";
private const string ProjectileGroupName = "Projectile";
private const string EnemyProjectileGroupName = "EnemyProjectile";
private readonly SimulationWorld _world;
@ -53,10 +54,11 @@ namespace Simulation
return;
}
if ((groupName == BulletGroupName || groupName == ProjectileGroupName) &&
if ((groupName == BulletGroupName || groupName == ProjectileGroupName ||
groupName == EnemyProjectileGroupName) &&
args.Entity.Logic is EntityBase projectileEntity)
{
_world.RegisterProjectileLifecycle(projectileEntity);
_world.RegisterProjectileLifecycle(projectileEntity, args.UserData);
}
}
@ -79,7 +81,8 @@ namespace Simulation
return;
}
if (groupName == BulletGroupName || groupName == ProjectileGroupName)
if (groupName == BulletGroupName || groupName == ProjectileGroupName ||
groupName == EnemyProjectileGroupName)
{
_world.UnregisterProjectileLifecycle(args.EntityId);
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
@ -43,7 +44,11 @@ namespace Simulation
public int OwnerEntityId;
public Vector3 Position;
public Vector3 Forward;
public Vector3 Velocity;
public float Speed;
public float LifeTime;
public float Age;
public bool Active;
public float RemainingLifetime;
public int State;
}
@ -54,11 +59,67 @@ namespace Simulation
public int OwnerEntityId;
public Vector3 Position;
public Vector3 Forward;
public Vector3 Velocity;
public float Speed;
public float LifeTime;
public float Age;
public bool Active;
public float RemainingLifetime;
public int State;
}
// 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<EnemyJobOutputData> _enemyJobOutputs;
private NativeList<EnemyJobOutputData> _enemyJobSeparationOutputs;
@ -66,7 +127,34 @@ namespace Simulation
private NativeList<float2> _enemySeparationCurrentPushes;
private NativeList<ProjectileJobInputData> _projectileJobInputs;
private NativeList<ProjectileJobOutputData> _projectileJobOutputs;
private NativeList<CollisionQueryData> _collisionQueryInputs;
private NativeList<CollisionCandidateData> _collisionCandidates;
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()
{
@ -83,7 +171,10 @@ namespace Simulation
_enemySeparationCurrentPushes = new NativeList<float2>(64, Allocator.Persistent);
_projectileJobInputs = new NativeList<ProjectileJobInputData>(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);
_enemyCollisionBuckets = new NativeParallelMultiHashMap<long, int>(256, Allocator.Persistent);
InitializeEnemyTargetSpatialIndex();
}
@ -93,57 +184,94 @@ namespace Simulation
{
_enemyJobInputs.Dispose();
}
_enemyJobInputs = default;
if (_enemyJobOutputs.IsCreated)
{
_enemyJobOutputs.Dispose();
}
_enemyJobOutputs = default;
if (_enemyJobSeparationOutputs.IsCreated)
{
_enemyJobSeparationOutputs.Dispose();
}
_enemyJobSeparationOutputs = default;
if (_enemySeparationPreviousPushes.IsCreated)
{
_enemySeparationPreviousPushes.Dispose();
}
_enemySeparationPreviousPushes = default;
if (_enemySeparationCurrentPushes.IsCreated)
{
_enemySeparationCurrentPushes.Dispose();
}
_enemySeparationCurrentPushes = default;
if (_projectileJobInputs.IsCreated)
{
_projectileJobInputs.Dispose();
}
_projectileJobInputs = default;
if (_projectileJobOutputs.IsCreated)
{
_projectileJobOutputs.Dispose();
}
_projectileJobOutputs = default;
if (_collisionQueryInputs.IsCreated)
{
_collisionQueryInputs.Dispose();
}
_collisionQueryInputs = default;
if (_collisionCandidates.IsCreated)
{
_collisionCandidates.Dispose();
}
_collisionCandidates = default;
if (_enemySeparationBuckets.IsCreated)
{
_enemySeparationBuckets.Dispose();
}
_enemySeparationBuckets = default;
if (_enemyCollisionBuckets.IsCreated)
{
_enemyCollisionBuckets.Dispose();
}
_enemyCollisionBuckets = default;
DisposeEnemyTargetSpatialIndex();
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
ResetCollisionRuntimeStats();
}
private void ClearJobDataChannels()
{
if (!AreJobDataChannelsUsable())
{
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
ResetCollisionRuntimeStats();
return;
}
@ -167,6 +295,16 @@ namespace Simulation
_projectileJobOutputs.Clear();
}
if (_collisionQueryInputs.IsCreated)
{
_collisionQueryInputs.Clear();
}
if (_collisionCandidates.IsCreated)
{
_collisionCandidates.Clear();
}
if (_enemyJobSeparationOutputs.IsCreated)
{
_enemyJobSeparationOutputs.Clear();
@ -187,7 +325,29 @@ namespace Simulation
_enemySeparationBuckets.Clear();
}
if (_enemyCollisionBuckets.IsCreated)
{
_enemyCollisionBuckets.Clear();
}
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()
@ -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()
{
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)
{
InitializeJobDataChannels();
@ -358,7 +640,10 @@ namespace Simulation
IsNativeListUsable(_enemySeparationCurrentPushes) &&
IsNativeListUsable(_projectileJobInputs) &&
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
@ -472,7 +757,11 @@ namespace Simulation
OwnerEntityId = projectile.OwnerEntityId,
Position = projectile.Position,
Forward = projectile.Forward,
Velocity = projectile.Velocity,
Speed = projectile.Speed,
LifeTime = projectile.LifeTime,
Age = projectile.Age,
Active = projectile.Active,
RemainingLifetime = projectile.RemainingLifetime,
State = projectile.State
};
@ -486,7 +775,11 @@ namespace Simulation
OwnerEntityId = projectile.OwnerEntityId,
Position = projectile.Position,
Forward = projectile.Forward,
Velocity = projectile.Velocity,
Speed = projectile.Speed,
LifeTime = projectile.LifeTime,
Age = projectile.Age,
Active = projectile.Active,
RemainingLifetime = projectile.RemainingLifetime,
State = projectile.State
};
@ -500,7 +793,11 @@ namespace Simulation
OwnerEntityId = projectile.OwnerEntityId,
Position = projectile.Position,
Forward = projectile.Forward,
Velocity = projectile.Velocity,
Speed = projectile.Speed,
LifeTime = projectile.LifeTime,
Age = projectile.Age,
Active = projectile.Active,
RemainingLifetime = projectile.RemainingLifetime,
State = projectile.State
};

View File

@ -1,18 +1,83 @@
using CustomEvent;
using Entity;
using Entity.EntityData;
using Entity.Weapon;
using GameFramework.Event;
using UnityEngine;
namespace Simulation
{
public partial class SimulationWorld
{
[Header("投射物命中表现")] [Tooltip("是否监听投射物命中表现事件。")] [SerializeField]
private bool _projectileHitPresentationEnabled = true;
[Tooltip("是否播放投射物命中标记。")] [SerializeField]
private bool _projectileHitMarkerEnabled = true;
[Tooltip("命中标记尺寸。")] [SerializeField]
private float _projectileHitMarkerSize = 0.2f;
[Tooltip("命中标记在目标上的高度偏移。")] [SerializeField]
private float _projectileHitMarkerYOffset = 1.2f;
[Tooltip("命中标记持续时间。")] [SerializeField]
private float _projectileHitMarkerDuration = 0.15f;
[Tooltip("命中标记颜色。")] [SerializeField]
private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
[Tooltip("是否播放投射物命中特效实体。")] [SerializeField]
private bool _projectileHitEffectEnabled;
[Tooltip("投射物命中特效实体类型 Id<=0 表示不启用)。")] [SerializeField]
private int _projectileHitEffectTypeId;
private sealed class Presentation
{
private readonly SimulationWorld _world;
private HandgunHitMarkerAttackEffect _projectileHitMarkerEffect;
private bool _isProjectileHitEventSubscribed;
public Presentation(SimulationWorld world)
{
_world = world;
}
public void OnStart()
{
if (_world == null || !_world._projectileHitPresentationEnabled)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent == null)
{
return;
}
eventComponent.Subscribe(ProjectileHitPresentationEventArgs.EventId, OnProjectileHitPresentationEvent);
_isProjectileHitEventSubscribed = true;
}
public void OnDestroy()
{
if (!_isProjectileHitEventSubscribed)
{
return;
}
var eventComponent = GameEntry.Event;
if (eventComponent != null)
{
eventComponent.Unsubscribe(ProjectileHitPresentationEventArgs.EventId,
OnProjectileHitPresentationEvent);
}
_isProjectileHitEventSubscribed = false;
}
public void OnLateUpdate()
{
if (_world == null || !_world.UseSimulationMovement)
@ -27,9 +92,9 @@ namespace Simulation
}
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;
}
@ -41,6 +106,85 @@ namespace Simulation
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)
@ -64,6 +208,25 @@ namespace Simulation
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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb6513f18404795a654500d3d2f8ef9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -15,6 +15,8 @@ namespace Simulation
private const int EnemyStateIdle = 0;
private const int EnemyStateChasing = 1;
private const int EnemyStateInAttackRange = 2;
private const int ProjectileStateActive = 0;
private const int ProjectileStateExpired = 1;
private struct EnemyTickWorkItem
{
@ -49,6 +51,8 @@ namespace Simulation
private readonly List<EnemySimData> _enemies = new List<EnemySimData>();
private readonly List<ProjectileSimData> _projectiles = new List<ProjectileSimData>();
private readonly List<PickupSimData> _pickups = new List<PickupSimData>();
private readonly List<int> _projectileRecycleEntityIds = new List<int>();
private readonly HashSet<int> _projectileResolvedEntityIds = new HashSet<int>();
private readonly List<EnemySeparationAgent> _enemySeparationAgents = new List<EnemySeparationAgent>();
private readonly List<EnemyTickWorkItem> _enemyTickWorkItems = new List<EnemyTickWorkItem>();
@ -89,10 +93,12 @@ namespace Simulation
private void Start()
{
_entitySync?.OnStart();
_presentation?.OnStart();
}
private void OnDestroy()
{
_presentation?.OnDestroy();
_entitySync?.OnDestroy();
_entitySync = null;
_presentation = null;
@ -216,14 +222,14 @@ namespace Simulation
return true;
}
private void RegisterProjectileLifecycle(EntityBase projectileEntity)
private void RegisterProjectileLifecycle(EntityBase projectileEntity, object userData)
{
if (projectileEntity == null || projectileEntity.CachedTransform == null)
{
return;
}
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity));
UpsertProjectile(CreateProjectileInitialSimData(projectileEntity, userData));
}
private void UnregisterProjectileLifecycle(int entityId)
@ -313,6 +319,11 @@ namespace Simulation
_enemies.Clear();
_projectiles.Clear();
_pickups.Clear();
_projectileRecycleEntityIds.Clear();
_projectileResolvedEntityIds.Clear();
_areaCollisionRequests.Clear();
_areaCollisionHitEvents.Clear();
_areaCollisionHitDedupKeys.Clear();
_enemySeparationAgents.Clear();
_enemyTickWorkItems.Clear();
ClearJobDataChannels();
@ -510,6 +521,10 @@ namespace Simulation
speed = movementComponent.Speed;
}
float attackRange = enemy != null && enemy.AttackRange > 0f
? enemy.AttackRange
: DefaultAttackRange;
return new EnemySimData
{
EntityId = enemy.Id,
@ -517,7 +532,7 @@ namespace Simulation
Forward = enemyTransform.forward,
Rotation = enemyTransform.rotation,
Speed = speed,
AttackRange = 1f,
AttackRange = attackRange,
AvoidEnemyOverlap = movementComponent != null && movementComponent.AvoidEnemyOverlap,
EnemyBodyRadius = movementComponent != null ? movementComponent.EnemyBodyRadius : 0.45f,
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
{
EntityId = projectileEntity.Id,
OwnerEntityId = 0,
OwnerEntityId = ownerEntityId,
Position = projectileEntity.CachedTransform.position,
Forward = projectileEntity.CachedTransform.forward,
Speed = 0f,
RemainingLifetime = 0f,
State = 0
Forward = forward,
Velocity = velocity,
Speed = speed,
LifeTime = lifeTime,
Age = 0f,
Active = true,
RemainingLifetime = lifeTime,
State = ProjectileStateActive
};
}
}

View File

@ -395,6 +395,8 @@ namespace UI
return new WeaponHandgunData(entityId, ownerId, ownerCamp);
case WeaponType.WeaponSlash:
return new WeaponSlashData(entityId, ownerId, ownerCamp);
case WeaponType.WeaponLightning:
return new WeaponLightningData(entityId, ownerId, ownerCamp);
default:
return null;
}

View File

@ -201,6 +201,26 @@ namespace CustomUtility
// 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;
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)
{
// 1. 处理闪避(闪避率取值 (0, 0.9),不允许拉满闪避)
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. 处理攻击加成 最终伤害 = (基础伤害 + 伤害提升固定值) * 伤害提升率

View File

@ -607,7 +607,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EntityGroups.Array.data[0].m_InstanceCapacity
value: 16
value: 10
objectReference: {fileID: 0}
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EntityGroups.Array.data[1].m_InstanceCapacity
@ -623,7 +623,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EntityGroups.Array.data[4].m_InstanceCapacity
value: 2
value: 1
objectReference: {fileID: 0}
- target: {fileID: 11494652, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EntityGroups.Array.data[5].m_InstanceCapacity
@ -1432,11 +1432,25 @@ MonoBehaviour:
_enemySeparationCellSize: 0
_enemySeparationPushDamping: 0.5
_enemySeparationMaxStepScale: 0.5
_enemySeparationZeroDistancePushScale: 1
_enemySeparationUseTangentialInAttackRange: 1
_enemySeparationPushSmoothing: 0.55
_enemySeparationPushCarry: 1
_enemySeparationMinPushRetain: 1
_projectileHitPresentationEnabled: 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
--- !u!1 &1852670052
GameObject:

View File

@ -20,6 +20,21 @@ namespace Simulation.Tests.Editor
private static readonly System.Type EnemySimDataType =
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 =
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
@ -29,6 +44,12 @@ namespace Simulation.Tests.Editor
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
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 =
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
@ -56,6 +77,39 @@ namespace Simulation.Tests.Editor
private static readonly PropertyInfo EnemiesProperty =
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 Component _worldComponent;
@ -65,9 +119,16 @@ namespace Simulation.Tests.Editor
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
Assert.NotNull(SimulationTickContextType, "SimulationTickContext 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(UpsertEnemyMethod, "UpsertEnemy 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(TickMethod, "Tick 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(UseGridBucketSolverMethod, "UseGridBucketSolver 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");
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
@ -230,6 +307,209 @@ namespace Simulation.Tests.Editor
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,
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
{
@ -248,6 +528,24 @@ namespace Simulation.Tests.Editor
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)
{
object tickContext = System.Activator.CreateInstance(
@ -265,11 +563,37 @@ namespace Simulation.Tests.Editor
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
}
private void UpsertProjectile(object projectile)
{
UpsertProjectileMethod.Invoke(_worldComponent, new[] { projectile });
}
private bool RemoveEnemyByEntityId(int 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)
{
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
@ -293,6 +617,25 @@ namespace Simulation.Tests.Editor
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)
{
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);

View File

@ -22,6 +22,21 @@ namespace Simulation.Tests.PlayMode
private static readonly System.Type EnemySimDataType =
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 =
System.Type.GetType($"CustomUtility.EnemySeparationSolverProvider, {GameAssemblyName}");
@ -31,6 +46,12 @@ namespace Simulation.Tests.PlayMode
private static readonly MethodInfo RemoveEnemyByEntityIdMethod =
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 =
SimulationWorldType?.GetMethod("TryGetEnemyData", NonPublicInstance);
@ -58,6 +79,39 @@ namespace Simulation.Tests.PlayMode
private static readonly PropertyInfo EnemiesProperty =
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 Component _worldComponent;
@ -67,9 +121,16 @@ namespace Simulation.Tests.PlayMode
Assert.NotNull(SimulationWorldType, "SimulationWorld type lookup failed.");
Assert.NotNull(SimulationTickContextType, "SimulationTickContext 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(UpsertEnemyMethod, "UpsertEnemy 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(TickMethod, "Tick 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(UseGridBucketSolverMethod, "UseGridBucketSolver 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");
_worldComponent = _worldGameObject.AddComponent(SimulationWorldType);
@ -246,6 +323,223 @@ namespace Simulation.Tests.PlayMode
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,
bool avoidEnemyOverlap = false, float enemyBodyRadius = 0.45f, int separationIterations = 1)
{
@ -264,6 +558,24 @@ namespace Simulation.Tests.PlayMode
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)
{
object tickContext = System.Activator.CreateInstance(
@ -281,11 +593,37 @@ namespace Simulation.Tests.PlayMode
UpsertEnemyMethod.Invoke(_worldComponent, new[] { enemy });
}
private void UpsertProjectile(object projectile)
{
UpsertProjectileMethod.Invoke(_worldComponent, new[] { projectile });
}
private bool RemoveEnemyByEntityId(int 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)
{
object boxedDefault = System.Activator.CreateInstance(EnemySimDataType);
@ -309,6 +647,25 @@ namespace Simulation.Tests.PlayMode
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)
{
FieldInfo field = target.GetType().GetField(fieldName, PublicInstance);

View File

@ -144,13 +144,13 @@
- 避免全量最近邻搜索,控制复杂度随敌人数增长的斜率。
- 完成标准:`3k` 敌人下目标选择阶段耗时稳定,且无索引越界/漏目标回归。
- [ ] Checkpoint 5投射物批量移动与寿命回收 Job 化
- [x] Checkpoint 5投射物批量移动与寿命回收 Job 化
- 投射物数据结构最少包含:`position/velocity/lifeTime/age/active`。
- 迁移投射物移动、越界判定、寿命回收到 Job。
- 回收后保持实体池与索引同步,防止悬空引用。
- 完成标准:连续战斗下投射物数量曲线稳定,无异常积压或提前回收。
- [ ] Checkpoint 6AOE/碰撞候选筛选 Job 化Broad Phase 优先)
- [x] Checkpoint 6AOE/碰撞候选筛选 Job 化Broad Phase 优先)
- 先 Job 化候选生成Broad Phase减少精算对数。
- 精算与伤害结算可先保留主线程,但输入改为候选列表驱动。
- 建立命中事件缓冲区,统一在主线程提交表现层事件。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.