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
@@ -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));
+54 -9
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;
}
}
}
}
@@ -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(']'))
@@ -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");
@@ -6,5 +6,6 @@ namespace Definition.Enum
WeaponKnife = 1,
WeaponHandgun = 2,
WeaponSlash = 3,
WeaponLightning = 4,
}
}
@@ -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);
}
}
}
}
@@ -0,0 +1,29 @@
using System;
using Definition.Enum;
using UnityEngine;
namespace Entity.EntityData
{
[Serializable]
public class EnemyProjectileData : EntityDataBase
{
public EnemyProjectileData(int entityId, int ownerEntityId, CampType ownerCamp, int attackDamage,
float speed, float lifeTime, Vector3 direction)
: base(entityId, 0)
{
OwnerEntityId = ownerEntityId;
OwnerCamp = ownerCamp;
AttackDamage = attackDamage;
Speed = speed;
LifeTime = lifeTime;
Direction = direction;
}
public int OwnerEntityId { get; }
public CampType OwnerCamp { get; }
public int AttackDamage { get; }
public float Speed { get; }
public float LifeTime { get; }
public Vector3 Direction { get; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52b38f83ab6c4029803d40c189db47c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -28,14 +28,15 @@ namespace Entity.EntityData
public WeaponType WeaponType => (WeaponType)_drWeapon.Id;
public 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>
@@ -0,0 +1,12 @@
using Definition.Enum;
namespace Entity.EntityData
{
public class WeaponLightningData : WeaponData
{
public WeaponLightningData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponLightning, ownerId, ownerCamp)
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25b0006918fd46959c7f6b8ec1bbc8ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -7,6 +7,7 @@ public abstract class EnemyBase : TargetableObject
protected Transform _target;
public abstract override ImpactData GetImpactData();
public virtual float AttackRange => 1f;
public virtual void SetTarget(Transform target) => _target = target;
@@ -0,0 +1,140 @@
using Definition.DataStruct;
using Definition.Enum;
using Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity
{
public class EnemyProjectile : EntityBase
{
private EnemyProjectileData _projectileData;
private Vector3 _direction = Vector3.forward;
private float _elapsedTime;
private bool _isActive;
private bool _isSimulationDriven;
private ImpactData _impactData;
private Collider[] _cachedColliders;
public bool IsActive => _isActive;
public ImpactData GetImpactData() => _impactData;
protected override void OnShow(object userData)
{
base.OnShow(userData);
_projectileData = userData as EnemyProjectileData;
if (_projectileData == null)
{
Log.Error("Enemy projectile data is invalid.");
_isActive = false;
GameEntry.Entity.HideEntity(this);
return;
}
_isActive = true;
_elapsedTime = 0f;
_impactData = new ImpactData(_projectileData.OwnerCamp, _projectileData.AttackDamage);
_direction = _projectileData.Direction;
_direction.y = 0f;
if (_direction.sqrMagnitude <= Mathf.Epsilon)
{
_direction = CachedTransform.forward;
_direction.y = 0f;
}
if (_direction.sqrMagnitude <= Mathf.Epsilon)
{
_direction = Vector3.forward;
}
else
{
_direction.Normalize();
}
CachedTransform.rotation = Quaternion.LookRotation(_direction, Vector3.up);
if (_projectileData.OwnerCamp == CampType.Player)
{
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
}
else if (_projectileData.OwnerCamp == CampType.Enemy)
{
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
}
_isSimulationDriven = IsDrivenBySimulationWorld();
SetColliderEnabled(!_isSimulationDriven);
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_isActive || _projectileData == null) return;
bool isSimulationDriven = IsDrivenBySimulationWorld();
if (isSimulationDriven != _isSimulationDriven)
{
_isSimulationDriven = isSimulationDriven;
SetColliderEnabled(!_isSimulationDriven);
}
if (_isSimulationDriven) return;
if (_projectileData.Speed > 0f)
{
CachedTransform.position += _direction * (_projectileData.Speed * elapseSeconds);
}
_elapsedTime += elapseSeconds;
if (_projectileData.LifeTime > 0f && _elapsedTime >= _projectileData.LifeTime)
{
Expire();
}
}
protected override void OnHide(bool isShutdown, object userData)
{
_isActive = false;
_projectileData = null;
_elapsedTime = 0f;
_isSimulationDriven = false;
_impactData = default;
_direction = Vector3.forward;
base.OnHide(isShutdown, userData);
}
public void Expire()
{
if (!_isActive) return;
_isActive = false;
GameEntry.Entity.HideEntity(this);
}
private static bool IsDrivenBySimulationWorld()
{
var simulationWorld = GameEntry.SimulationWorld;
return simulationWorld != null && simulationWorld.UseSimulationMovement && simulationWorld.UseJobSimulation;
}
private void SetColliderEnabled(bool enabled)
{
_cachedColliders ??= GetComponentsInChildren<Collider>(true);
if (_cachedColliders == null) return;
for (int i = 0; i < _cachedColliders.Length; i++)
{
Collider collider = _cachedColliders[i];
if (collider == null)
{
continue;
}
collider.enabled = enabled;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22624f81b9364c8681b32d993f5e618f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,7 +1,8 @@
using Components;
using 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)
@@ -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)
@@ -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);
}
}
}
}
@@ -0,0 +1,81 @@
using UnityEngine;
namespace Entity.Weapon
{
public sealed class LightningStrikeAttackEffect : IWeaponAttackEffect
{
private readonly float _duration = 0.22f;
private readonly float _yOffset = 0.2f;
private readonly float _ringLineWidth = 0.045f;
private readonly float _boltLineWidth = 0.08f;
private readonly int _ringSegments = 32;
private readonly float _boltHeight = 3f;
private readonly Color _ringColor = new(0.35f, 0.82f, 1f, 0.92f);
private readonly Color _boltColor = new(0.85f, 0.96f, 1f, 0.97f);
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
{
float safeRadius = Mathf.Max(0.1f, radius);
Vector3 center = new Vector3(position.x, position.y + _yOffset, position.z);
GameObject root = new GameObject("LightningStrikeEffect");
root.transform.position = center;
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
Material ringMaterial = new Material(shader);
ringMaterial.color = _ringColor;
LineRenderer ring = root.AddComponent<LineRenderer>();
ring.loop = true;
ring.useWorldSpace = true;
ring.positionCount = _ringSegments;
ring.startWidth = _ringLineWidth;
ring.endWidth = _ringLineWidth;
ring.startColor = _ringColor;
ring.endColor = _ringColor;
ring.material = ringMaterial;
float step = Mathf.PI * 2f / _ringSegments;
for (int i = 0; i < _ringSegments; i++)
{
float angle = i * step;
Vector3 offset = new Vector3(Mathf.Cos(angle) * safeRadius, 0f, Mathf.Sin(angle) * safeRadius);
ring.SetPosition(i, center + offset);
}
GameObject bolt = new GameObject("Bolt");
bolt.transform.SetParent(root.transform, false);
Material boltMaterial = new Material(shader);
boltMaterial.color = _boltColor;
LineRenderer boltLine = bolt.AddComponent<LineRenderer>();
boltLine.loop = false;
boltLine.useWorldSpace = true;
boltLine.positionCount = 4;
boltLine.startWidth = _boltLineWidth;
boltLine.endWidth = _boltLineWidth * 0.55f;
boltLine.startColor = _boltColor;
boltLine.endColor = _boltColor;
boltLine.material = boltMaterial;
Vector3 top = center + Vector3.up * _boltHeight;
Vector3 middleA = center + Vector3.up * (_boltHeight * 0.66f) +
new Vector3(Random.Range(-0.18f, 0.18f), 0f, Random.Range(-0.18f, 0.18f));
Vector3 middleB = center + Vector3.up * (_boltHeight * 0.3f) +
new Vector3(Random.Range(-0.12f, 0.12f), 0f, Random.Range(-0.12f, 0.12f));
boltLine.SetPosition(0, top);
boltLine.SetPosition(1, middleA);
boltLine.SetPosition(2, middleB);
boltLine.SetPosition(3, center);
Object.Destroy(root, Mathf.Max(0.01f, _duration));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81cbdf8961ad419c91b989d56ca782d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -220,6 +220,32 @@ namespace Entity.Weapon
return AIUtility.GetSqrMagnitudeXZ(this, target) < sqrRange;
}
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
}
@@ -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();
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5fded69986744ded97edb5f2ac06304f
timeCreated: 1771578597
@@ -0,0 +1,31 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class AttackState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
public override void OnEnter()
{
_weapon._currAttackTimer = 0f;
_weapon.Attack();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_weapon._isAttacking)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25854832a7d3416dacab67bcfef2a2fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
public override void OnEnter()
{
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (!_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
return;
}
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25404ad172434ba38609e797b6d96919
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3aa479abb79491d83e8de76806faf7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class IdleState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToOrigin(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target != null && _weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3464037c29774c3ea326dc93fe33551e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,272 @@
using System.Collections.Generic;
using CustomUtility;
using Definition.DataStruct;
using Definition.Enum;
using DG.Tweening;
using Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity.Weapon
{
public partial class WeaponLightning : WeaponBase
{
private const string HitRadiusParamKey = "HitRadius";
private WeaponLightningData _weaponData;
private Quaternion _cachedRotation;
[SerializeField] private float _rotateSpeed = 5f;
[SerializeField] private float _takeOffDuration = 0.3f;
[SerializeField] private float _flyToTargetDuration = 0.2f;
[SerializeField] private float _strikeDuration = 0.1f;
[SerializeField] private float _returnDuration = 0.22f;
[SerializeField] private float _hoverHeight = 15f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private Sequence _attackSequence;
private Transform _attackParent;
private Vector3 _lockedStrikePoint;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
private float _hitRadius;
private float _hitRadiusSqr;
private IWeaponAttackEffect _attackEffect;
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckOutRangeState());
RegisterState(new CheckInRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
StopAttackTween(false);
FaceTargetImmediately();
_isAttacking = true;
_lockedStrikePoint = ResolveStrikePoint();
_attackParent = CachedTransform.parent;
CachedTransform.SetParent(null);
Vector3 takeOffPosition = CachedTransform.position;
Vector3 hoverPosition = _lockedStrikePoint + Vector3.up * Mathf.Max(0.1f, _hoverHeight);
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(takeOffPosition, _takeOffDuration).SetEase(Ease.OutQuad));
_attackSequence.Append(CachedTransform.DOMove(hoverPosition, _flyToTargetDuration).SetEase(Ease.OutSine));
_attackSequence.AppendCallback(() => CachedTransform.LookAt(_lockedStrikePoint));
_attackSequence.Append(CachedTransform.DOMove(_lockedStrikePoint, _strikeDuration).SetEase(Ease.InQuad));
_attackSequence.AppendCallback(() =>
{
_attackEffect?.Play(this, _lockedStrikePoint, _target, _hitRadius);
ApplyGroundAreaDamage();
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
}
});
_attackSequence.Append(CachedTransform.DOLocalMove(Vector3.zero, _returnDuration).SetEase(Ease.OutSine));
_attackSequence.AppendCallback(() =>
{
_isAttacking = false;
_attackSequence = null;
_attackParent = null;
});
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
private Vector3 ResolveStrikePoint()
{
if (_target != null && _target.Available)
{
return _target.CachedTransform.position;
}
return CachedTransform.position;
}
private void ApplyGroundAreaDamage()
{
if (_hitRadius <= 0f) return;
if (TryQueueAreaCollisionQuery(_lockedStrikePoint, _hitRadius, Mathf.Max(1, _maxHitColliders)))
{
_hitEntityIds.Clear();
return;
}
if (_hitResults == null || _hitResults.Length == 0) return;
int hitCount = Physics.OverlapSphereNonAlloc(_lockedStrikePoint, _hitRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
for (int i = 0; i < hitCount; i++)
{
Collider collider = _hitResults[i];
if (collider == null) continue;
TargetableObject targetable = collider.GetComponentInParent<TargetableObject>();
if (targetable == null || !targetable.Available || targetable.IsDead) continue;
if (!_hitEntityIds.Add(targetable.Id)) continue;
Vector3 delta = targetable.CachedTransform.position - _lockedStrikePoint;
delta.y = 0f;
if (delta.sqrMagnitude > _hitRadiusSqr) continue;
AIUtility.PerformCollision(targetable, this);
}
}
private void RotateToTarget(float elapseSeconds)
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, targetRotation, _rotateSpeed * elapseSeconds);
}
private void RotateToOrigin(float elapseSeconds)
{
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, _cachedRotation, _rotateSpeed * elapseSeconds);
}
private void FaceTargetImmediately()
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponLightningData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
_hitRadius = ReadPositiveParam(HitRadiusParamKey, _weaponData.AttackRange);
_hitRadiusSqr = _hitRadius * _hitRadius;
int colliderCapacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != colliderCapacity)
{
_hitResults = new Collider[colliderCapacity];
}
_attackEffect = new LightningStrikeAttackEffect();
if (_weaponData.OwnerCamp == CampType.Player)
{
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
_hitMask = LayerMask.GetMask("Enemy");
}
else if (_weaponData.OwnerCamp == CampType.Enemy)
{
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
_hitMask = LayerMask.GetMask("Player");
}
return true;
}
protected override void OnWeaponHide(object userData)
{
StopAttackTween(true);
_attackEffect = null;
}
protected override void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
{
BindAttackStatFromOwner(parentEntity);
}
protected override void OnWeaponDetach(EntityLogic parentEntity, object userData)
{
StopAttackTween(true);
ReleaseAttackStatSubscription();
}
protected override void OnEnabledChanged(bool enabled)
{
if (!enabled)
{
StopAttackTween(true);
}
}
private float ReadPositiveParam(string paramName, float defaultValue)
{
if (_weaponData.Params != null &&
_weaponData.Params.TryGetValue(paramName.ToLower(), out string rawValue) &&
float.TryParse(rawValue, out float parsedValue))
{
return Mathf.Max(0.1f, parsedValue);
}
return Mathf.Max(0.1f, defaultValue);
}
private void StopAttackTween(bool resetTransform)
{
if (_attackSequence != null)
{
_attackSequence.Kill();
_attackSequence = null;
}
_isAttacking = false;
if (resetTransform)
{
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
CachedTransform.localPosition = Vector3.zero;
}
else if (CachedTransform.parent != null)
{
CachedTransform.localPosition = Vector3.zero;
}
CachedTransform.rotation = _cachedRotation;
}
_attackParent = null;
_hitEntityIds.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8315d9e60c4434ebde9b23c853f27d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -93,12 +93,8 @@ namespace Entity.Weapon
private void ApplySectorDamage()
{
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];
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7771e0f6b92ece64395ada5cdea72858
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
using GameFramework;
using GameFramework.Event;
using UnityEngine;
namespace CustomEvent
{
public sealed class ProjectileHitPresentationEventArgs : GameEventArgs
{
public static readonly int EventId = typeof(ProjectileHitPresentationEventArgs).GetHashCode();
public override int Id => EventId;
public int ProjectileEntityId { get; private set; }
public int SourceEntityId { get; private set; }
public int SourceOwnerEntityId { get; private set; }
public int TargetEntityId { get; private set; }
public int Damage { get; private set; }
public Vector3 HitPosition { get; private set; }
public bool ShowHitMarker { get; private set; }
public bool ShowHitEffect { get; private set; }
public int EffectEntityTypeId { get; private set; }
public static ProjectileHitPresentationEventArgs Create(int projectileEntityId, int sourceEntityId,
int sourceOwnerEntityId, int targetEntityId, int damage, in Vector3 hitPosition, bool showHitMarker,
bool showHitEffect, int effectEntityTypeId)
{
ProjectileHitPresentationEventArgs args = ReferencePool.Acquire<ProjectileHitPresentationEventArgs>();
args.ProjectileEntityId = projectileEntityId;
args.SourceEntityId = sourceEntityId;
args.SourceOwnerEntityId = sourceOwnerEntityId;
args.TargetEntityId = targetEntityId;
args.Damage = damage;
args.HitPosition = hitPosition;
args.ShowHitMarker = showHitMarker;
args.ShowHitEffect = showHitEffect;
args.EffectEntityTypeId = effectEntityTypeId;
return args;
}
public override void Clear()
{
ProjectileEntityId = 0;
SourceEntityId = 0;
SourceOwnerEntityId = 0;
TargetEntityId = 0;
Damage = 0;
HitPosition = Vector3.zero;
ShowHitMarker = false;
ShowHitEffect = false;
EffectEntityTypeId = 0;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d02876e26d8342f7af024c697e451ea4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -91,6 +91,15 @@ namespace Procedure
{
GameEntry.Entity.HideEntity(entity.Id);
}
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
}
}
}
@@ -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;
}
@@ -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
};
}
}
}
}
@@ -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);
}
@@ -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
};
@@ -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
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb6513f18404795a654500d3d2f8ef9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -15,6 +15,8 @@ namespace Simulation
private const int EnemyStateIdle = 0;
private const int 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
};
}
}
@@ -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;
}
+22 -2
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. 处理攻击加成 最终伤害 = (基础伤害 + 伤害提升固定值) * 伤害提升率