添加武器长枪 + bug fix

- WeaponLance:Entity/Goods/Weapon 表,状态机
- Battle -> Shop/LevelUp:隐藏子弹
This commit is contained in:
2026-03-19 17:53:24 +08:00
parent f31babdb85
commit 8d21d53c4d
35 changed files with 945 additions and 79 deletions
@@ -59,7 +59,7 @@ namespace Entity.EntityData
return false;
}
return _drEnemy.Params.TryGetValue(key.ToLower(), out value);
return _drEnemy.Params.TryGetValue(key, out value);
}
}
}
@@ -37,7 +37,7 @@ namespace Entity.EntityData
return false;
}
return Params.TryGetValue(key.ToLower(), out value);
return Params.TryGetValue(key, out value);
}
protected TParams ParseParams<TParams>() where TParams : new()
@@ -0,0 +1,56 @@
using System;
using Definition.Enum;
namespace Entity.EntityData
{
[Serializable]
public sealed class WeaponLanceParamsData
{
/// <summary>
/// 枪尖命中半径。
/// </summary>
public float HitRadius { get; set; }
/// <summary>
/// 武器模型前刺的位移距离。
/// </summary>
public float ThrustDistance { get; set; }
/// <summary>
/// 实际判定的前刺长度。
/// </summary>
public float PierceLength { get; set; }
/// <summary>
/// 判定起点相对武器当前位置的前置偏移。
/// </summary>
public float ForwardOffset { get; set; }
/// <summary>
/// 追踪目标时的转向速度。
/// </summary>
public float RotateSpeed { get; set; }
/// <summary>
/// 向前突刺阶段耗时。
/// </summary>
public float AttackDuration { get; set; }
/// <summary>
/// 收枪返回阶段耗时。
/// </summary>
public float ReturnDuration { get; set; }
}
[Serializable]
public class WeaponLanceData : WeaponData
{
public WeaponLanceParamsData ParamsData { get; }
public WeaponLanceData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponLance, ownerId, ownerCamp)
{
ParamsData = ParseParams<WeaponLanceParamsData>();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4d1a821e8ee1a9a4b912b70b0a1616eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -37,7 +37,7 @@ namespace Entity.Weapon
protected ITargetSelector TargetSelector { get; set; }
protected Dictionary<WeaponStateType, WeaponStateBase> _states;
protected WeaponStateBase _currentState;
protected EntityBase _target;
@@ -50,7 +50,7 @@ namespace Entity.Weapon
private StatComponent _attackStatComponent;
private System.Action<StatModifier, bool> _attackStatCallback;
private static readonly List<EntityBase> s_EmptyCandidates = new();
#region Lifecycle
@@ -304,11 +304,4 @@ namespace Entity.Weapon
public abstract void OnLeave();
public override string ToString() => State.ToString();
}
}
}
@@ -46,22 +46,15 @@ namespace Entity.Weapon
{
FaceTargetImmediately();
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset);
Vector3 fireDirection = CachedTransform.forward;
float maxDistance = Mathf.Max(0.1f, _weaponData.AttackRange);
if (Physics.Raycast(fireOrigin, fireDirection, out RaycastHit hit, maxDistance, _hitMask,
QueryTriggerInteraction.Collide))
if (!TryResolveAttackTarget(out TargetableObject targetable, out Vector3 hitPosition))
{
TargetableObject targetable = hit.collider.GetComponentInParent<TargetableObject>();
if (targetable != null && targetable.Available && !targetable.IsDead)
{
_attackEffect?.Play(this, hit.point, targetable, 0f);
_isAttacking = true;
AIUtility.PerformCollision(targetable, this);
_isAttacking = false;
}
return;
}
_attackEffect?.Play(this, hitPosition, targetable, 0f);
_isAttacking = true;
AIUtility.PerformCollision(targetable, this);
_isAttacking = false;
}
protected override void Check()
@@ -95,6 +88,40 @@ namespace Entity.Weapon
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
private bool TryResolveAttackTarget(out TargetableObject targetable, out Vector3 hitPosition)
{
targetable = _target as TargetableObject;
hitPosition = CachedTransform.position;
if (targetable == null || !targetable.Available || targetable.IsDead)
{
return false;
}
Transform targetTransform = targetable.CachedTransform;
if (targetTransform == null)
{
return false;
}
hitPosition = targetTransform.position;
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset);
Vector3 directionToTarget = targetTransform.position - fireOrigin;
float maxDistance = Mathf.Max(0.1f, _weaponData.AttackRange);
if (directionToTarget.sqrMagnitude > Mathf.Epsilon &&
Physics.Raycast(fireOrigin, directionToTarget.normalized, out RaycastHit hit, maxDistance, _hitMask,
QueryTriggerInteraction.Collide))
{
TargetableObject raycastTarget = hit.collider.GetComponentInParent<TargetableObject>();
if (raycastTarget == targetable)
{
hitPosition = hit.point;
}
}
return true;
}
#region Lifecycle
protected override bool OnWeaponShow(object userData)
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: aee5d5037e73e894cac11712e321b930
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,31 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
private class AttackState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
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: d9991ee189b733b4e9d40395357f5ef2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
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: 01edc8bb0ff46a14d8028e03af932b52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
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: 8009ad2c332a956438e3357ed5e30eb6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
public class IdleState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
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: 4c9e98f5381e579469dcdac7bf3e1e25
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,325 @@
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 WeaponLance : WeaponBase
{
// 长枪专用数据,包含强类型 ParamsData。
[SerializeField] private WeaponLanceData _weaponData;
private Quaternion _cachedRotation;
// 朝向目标时的旋转速度,可被 ParamsData.RotateSpeed 覆盖。
[SerializeField] private float _rotateSpeed = 5f;
private Sequence _attackSequence;
private Transform _attackParent;
// 前刺动画耗时。
[SerializeField] private float _attackDuration = 0.12f;
// 收枪返回耗时。
[SerializeField] private float _returnDuration = 0.18f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private IWeaponAttackEffect _attackEffect;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
// 枪尖判定半径。
private float _hitRadius;
// 从判定起点向前延伸的有效刺击长度。
private float _pierceLength;
// 判定起点相对武器当前位置的前移量。
private float _forwardOffset;
// 武器模型本身向前突刺的位移长度。
private float _thrustDistance;
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckInRangeState());
RegisterState(new CheckOutRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
StopAttackTween(false);
FaceTargetImmediately();
_isAttacking = true;
_attackParent = CachedTransform.parent;
CachedTransform.SetParent(null);
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * _thrustDistance;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(targetPos, _attackDuration).SetEase(Ease.OutQuad));
_attackSequence.AppendCallback(() =>
{
Vector3 strikeCenter = GetStrikeCenter();
_attackEffect?.Play(this, strikeCenter, _target, _hitRadius);
ApplyPierceDamage();
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
}
});
_attackSequence.Append(CachedTransform.DOLocalMove(Vector3.zero, _returnDuration).SetEase(Ease.InQuad));
_attackSequence.AppendCallback(() =>
{
_isAttacking = false;
_attackSequence = null;
_attackParent = null;
});
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
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);
}
private Vector3 GetStrikeStart()
{
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
forward.Normalize();
return CachedTransform.position + forward * _forwardOffset;
}
private Vector3 GetStrikeEnd()
{
Vector3 start = GetStrikeStart();
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
forward.Normalize();
return start + forward * _pierceLength;
}
private Vector3 GetStrikeCenter()
{
return Vector3.Lerp(GetStrikeStart(), GetStrikeEnd(), 0.5f);
}
private void ApplyPierceDamage()
{
if (_hitRadius <= 0f || _pierceLength <= 0f) return;
Vector3 strikeStart = GetStrikeStart();
Vector3 strikeEnd = GetStrikeEnd();
Vector3 strikeDirection = strikeEnd - strikeStart;
strikeDirection.y = 0f;
if (strikeDirection.sqrMagnitude <= Mathf.Epsilon) return;
strikeDirection.Normalize();
float halfAngle = Mathf.Rad2Deg * Mathf.Atan2(_hitRadius, Mathf.Max(0.01f, _pierceLength));
if (TryQueueSectorCollisionQuery(strikeStart, _pierceLength, in strikeDirection, halfAngle,
Mathf.Max(1, _maxHitColliders)))
{
_hitEntityIds.Clear();
return;
}
int capacity = Mathf.Max(1, _maxHitColliders);
float broadPhaseRadius = _pierceLength * 0.5f + _hitRadius;
Vector3 broadPhaseCenter = Vector3.Lerp(strikeStart, strikeEnd, 0.5f);
if (_hitResults == null || _hitResults.Length != capacity)
{
_hitResults = new Collider[capacity];
}
int hitCount = Physics.OverlapSphereNonAlloc(broadPhaseCenter, broadPhaseRadius, _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;
if (!IsTargetInsidePierce(targetable, strikeStart, strikeDirection)) continue;
AIUtility.PerformCollision(targetable, this);
}
}
private bool IsTargetInsidePierce(TargetableObject targetable, Vector3 strikeStart, Vector3 strikeDirection)
{
Vector3 toTarget = targetable.CachedTransform.position - strikeStart;
toTarget.y = 0f;
float projection = Vector3.Dot(toTarget, strikeDirection);
if (projection < 0f || projection > _pierceLength) return false;
Vector3 closestPoint = strikeStart + strikeDirection * projection;
Vector3 delta = targetable.CachedTransform.position - closestPoint;
delta.y = 0f;
return delta.sqrMagnitude <= _hitRadius * _hitRadius;
}
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponLanceData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
WeaponLanceParamsData paramsData = _weaponData.ParamsData;
_hitRadius = paramsData != null && paramsData.HitRadius > 0f
? Mathf.Max(0.1f, paramsData.HitRadius)
: 0.45f;
_thrustDistance = paramsData != null && paramsData.ThrustDistance > 0f
? paramsData.ThrustDistance
: _weaponData.AttackRange;
_pierceLength = paramsData != null && paramsData.PierceLength > 0f
? paramsData.PierceLength
: Mathf.Max(_weaponData.AttackRange, _thrustDistance);
_forwardOffset = paramsData != null && paramsData.ForwardOffset > 0f
? paramsData.ForwardOffset
: 0f;
if (paramsData != null && paramsData.RotateSpeed > 0f)
{
_rotateSpeed = paramsData.RotateSpeed;
}
if (paramsData != null && paramsData.AttackDuration > 0f)
{
_attackDuration = paramsData.AttackDuration;
}
if (paramsData != null && paramsData.ReturnDuration > 0f)
{
_returnDuration = paramsData.ReturnDuration;
}
int colliderCapacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != colliderCapacity)
{
_hitResults = new Collider[colliderCapacity];
}
_attackEffect = new KnifeRangeAttackEffect();
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 void StopAttackTween(bool resetTransform)
{
if (_attackSequence != null)
{
_attackSequence.Kill();
_attackSequence = null;
}
_isAttacking = false;
if (resetTransform && _attackParent != null)
{
CachedTransform.SetParent(_attackParent);
CachedTransform.localPosition = Vector3.zero;
CachedTransform.rotation = _cachedRotation;
}
_attackParent = null;
_hitEntityIds.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f90612be7695ee549b39473c9b706122
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: