修复旋转问题 + 防御塔范围可视化

- BasicBearingComp 和 ShooterBullet 的旋转由之前的 3D 方式调整为只旋转 Z 轴
- DefenseTowerController 通过 LineRenderer 添加了攻击范围可视化
This commit is contained in:
SepComet
2026-03-02 10:15:13 +08:00
parent 564817d752
commit 344191a91c
11 changed files with 324 additions and 35 deletions
@@ -9,7 +9,6 @@ namespace Components
[SerializeField] [Min(0.1f)] private float _attackRange = 5f;
[SerializeField] [Min(0.1f)] private float _aimToleranceAngle = 2f;
[SerializeField] private Transform _rotateRoot;
[SerializeField] private bool _yawOnly = true;
public float RotateSpeed => _rotateSpeed;
public float AttackRange => _attackRange;
@@ -47,20 +46,19 @@ namespace Components
}
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
Vector3 toTarget = target.position - rotateRoot.position;
if (_yawOnly)
{
toTarget.y = 0f;
}
Vector2 toTarget = (Vector2)(target.position - rotateRoot.position);
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
{
return true;
}
Quaternion targetRotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up);
float rotateAngle = _rotateSpeed * Mathf.Max(0f, deltaTime);
rotateRoot.rotation = Quaternion.RotateTowards(rotateRoot.rotation, targetRotation, rotateAngle);
float targetZAngle = Vector2.SignedAngle(Vector2.up, toTarget);
float currentZAngle = rotateRoot.eulerAngles.z;
float maxStep = _rotateSpeed * Mathf.Max(0f, deltaTime);
float angleDelta = Mathf.DeltaAngle(currentZAngle, targetZAngle);
float nextZAngle = currentZAngle + Mathf.Clamp(angleDelta, -maxStep, maxStep);
rotateRoot.rotation = Quaternion.Euler(0f, 0f, nextZAngle);
return IsTargetAligned(target);
}
@@ -72,18 +70,16 @@ namespace Components
}
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
Vector3 toTarget = target.position - rotateRoot.position;
if (_yawOnly)
{
toTarget.y = 0f;
}
Vector2 toTarget = (Vector2)(target.position - rotateRoot.position);
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
{
return true;
}
float angle = Vector3.Angle(rotateRoot.forward, toTarget.normalized);
float targetZAngle = Vector2.SignedAngle(Vector2.up, toTarget);
float currentZAngle = rotateRoot.eulerAngles.z;
float angle = Mathf.Abs(Mathf.DeltaAngle(currentZAngle, targetZAngle));
return angle <= _aimToleranceAngle;
}
@@ -7,15 +7,24 @@ namespace Components
[DisallowMultipleComponent]
public class DefenseTowerController : MonoBehaviour
{
private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator";
private static Material s_AttackRangeSharedMaterial;
[SerializeField] private ShooterMuzzleComp _muzzleComp;
[SerializeField] private BasicBearingComp _bearingComp;
[SerializeField] private BasicBaseComp _baseComp;
[SerializeField] private Transform _scanOrigin;
[SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f;
[SerializeField] private bool _autoUpdate = true;
[SerializeField] private LineRenderer _attackRangeRenderer;
[SerializeField] [Min(12)] private int _attackRangeSegments = 64;
[SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f;
[SerializeField] private Color _attackRangeColor = new Color(0.1f, 1f, 0.4f, 0.8f);
[SerializeField] private float _attackRangeZOffset = -0.01f;
private Transform _currentTarget;
private float _retargetTimer;
private float _attackRange;
public Transform CurrentTarget => _currentTarget;
@@ -45,12 +54,15 @@ namespace Components
_muzzleComp?.OnInit(stats.AttackDamage, stats.AttackMethodType);
_bearingComp?.OnInit(stats.RotateSpeed, stats.AttackRange);
_baseComp?.OnInit(stats.AttackSpeed, stats.AttackPropertyType);
SetAttackRange(stats.AttackRange);
SetAttackRangeVisible(false);
_currentTarget = null;
_retargetTimer = 0f;
}
public void OnReset()
{
SetAttackRangeVisible(false);
_currentTarget = null;
_retargetTimer = 0f;
_muzzleComp?.OnReset();
@@ -58,6 +70,22 @@ namespace Components
_baseComp?.OnReset();
}
public void SetAttackRangeVisible(bool visible)
{
EnsureAttackRangeRenderer();
if (_attackRangeRenderer != null)
{
_attackRangeRenderer.enabled = visible;
}
}
public void SetAttackRange(float range)
{
_attackRange = Mathf.Max(0.05f, range);
EnsureAttackRangeRenderer();
RebuildAttackRangeGeometry();
}
public void SetAutoUpdate(bool autoUpdate)
{
_autoUpdate = autoUpdate;
@@ -153,6 +181,8 @@ namespace Components
{
_baseComp = GetComponent<BasicBaseComp>();
}
EnsureAttackRangeRenderer();
}
private bool HasCoreComponents()
@@ -164,5 +194,72 @@ namespace Components
{
return target != null && target.gameObject.activeInHierarchy;
}
private void EnsureAttackRangeRenderer()
{
if (_attackRangeRenderer == null)
{
Transform indicatorTransform = transform.Find(AttackRangeIndicatorObjectName);
if (indicatorTransform == null)
{
GameObject indicatorObject = new GameObject(AttackRangeIndicatorObjectName);
indicatorTransform = indicatorObject.transform;
indicatorTransform.SetParent(transform, false);
}
_attackRangeRenderer = indicatorTransform.GetComponent<LineRenderer>();
if (_attackRangeRenderer == null)
{
_attackRangeRenderer = indicatorTransform.gameObject.AddComponent<LineRenderer>();
}
}
_attackRangeRenderer.useWorldSpace = false;
_attackRangeRenderer.loop = true;
_attackRangeRenderer.positionCount = Mathf.Max(12, _attackRangeSegments);
_attackRangeRenderer.widthMultiplier = _attackRangeLineWidth;
_attackRangeRenderer.startColor = _attackRangeColor;
_attackRangeRenderer.endColor = _attackRangeColor;
_attackRangeRenderer.numCapVertices = 4;
_attackRangeRenderer.numCornerVertices = 4;
_attackRangeRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
_attackRangeRenderer.receiveShadows = false;
_attackRangeRenderer.allowOcclusionWhenDynamic = false;
_attackRangeRenderer.enabled = false;
if (s_AttackRangeSharedMaterial == null)
{
Shader lineShader = Shader.Find("Sprites/Default");
if (lineShader != null)
{
s_AttackRangeSharedMaterial = new Material(lineShader);
}
}
if (s_AttackRangeSharedMaterial != null)
{
_attackRangeRenderer.sharedMaterial = s_AttackRangeSharedMaterial;
}
}
private void RebuildAttackRangeGeometry()
{
if (_attackRangeRenderer == null)
{
return;
}
int segmentCount = Mathf.Max(12, _attackRangeSegments);
_attackRangeRenderer.positionCount = segmentCount;
float stepAngle = Mathf.PI * 2f / segmentCount;
for (int i = 0; i < segmentCount; i++)
{
float angle = stepAngle * i;
float x = Mathf.Cos(angle) * _attackRange;
float y = Mathf.Sin(angle) * _attackRange;
_attackRangeRenderer.SetPosition(i, new Vector3(x, y, _attackRangeZOffset));
}
}
}
}
@@ -30,6 +30,12 @@ namespace Components
_lifetime = 0f;
_despawnRequested = false;
_isRunning = _target != null;
if (_isRunning)
{
Vector2 initialDirection = (Vector2)(_target.position - transform.position);
RotateToDirection(initialDirection);
}
if (!_isRunning)
{
_despawnRequested = true;
@@ -63,7 +69,8 @@ namespace Components
return;
}
Vector3 toTarget = _target.position - transform.position;
Vector3 currentPosition = transform.position;
Vector2 toTarget = (Vector2)(_target.position - currentPosition);
float hitDistanceSquared = _hitDistance * _hitDistance;
if (toTarget.sqrMagnitude <= hitDistanceSquared)
{
@@ -71,17 +78,18 @@ namespace Components
return;
}
Vector3 forward = toTarget.normalized;
Vector2 direction = toTarget.normalized;
RotateToDirection(direction);
float moveDistance = _speed * Mathf.Max(0f, deltaTime);
if (moveDistance * moveDistance >= toTarget.sqrMagnitude)
{
transform.position = _target.position;
Vector3 targetPosition = _target.position;
transform.position = new Vector3(targetPosition.x, targetPosition.y, currentPosition.z);
HitTarget();
return;
}
transform.position += forward * moveDistance;
transform.forward = forward;
transform.position += (Vector3)(direction * moveDistance);
}
public bool TryConsumeDespawnRequest()
@@ -105,9 +113,9 @@ namespace Components
}
MonoBehaviour[] components = _target.GetComponentsInParent<MonoBehaviour>();
for (int i = 0; i < components.Length; i++)
foreach (var mono in components)
{
if (components[i] is IDamageReceiver damageReceiver)
if (mono is IDamageReceiver damageReceiver)
{
damageReceiver.TakeDamage(_damage, _attackPropertyType);
break;
@@ -117,5 +125,16 @@ namespace Components
_isRunning = false;
_despawnRequested = true;
}
private void RotateToDirection(Vector2 direction)
{
if (direction.sqrMagnitude <= Mathf.Epsilon)
{
return;
}
float targetZAngle = Vector2.SignedAngle(Vector2.up, direction);
transform.rotation = Quaternion.Euler(0f, 0f, targetZAngle);
}
}
}
@@ -8,7 +8,7 @@ namespace Components
[DisallowMultipleComponent]
public class ShooterMuzzleComp : MonoBehaviour
{
[SerializeField] [Min(1f)] private int _attackDamage = 10;
[SerializeField] [Min(1f)] private int _attackDamage = 100;
[SerializeField] private AttackMethodType _attackMethodType = AttackMethodType.NormalBullet;
[SerializeField] [Min(1)] private int _bulletTypeId = 501;
[SerializeField] private Transform _muzzlePoint;
@@ -54,7 +54,6 @@ namespace Components
bulletEntityId,
_bulletTypeId,
spawnPoint.position,
spawnPoint.rotation,
target,
_attackDamage,
_bulletSpeed,