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

- 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

View File

@ -98,10 +98,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 20a2c131403122146a41148cb72fcd43, type: 3}
m_Name:
m_EditorClassIdentifier:
_attackDamage: 10
_attackDamage: 100
_attackMethodType: 1
_bulletTypeId: 501
_muzzlePoint: {fileID: 0}
_muzzlePoint: {fileID: 7637292285124107611}
_bulletSpeed: 12
--- !u!1 &1221576993898367501
GameObject:
@ -114,6 +114,7 @@ GameObject:
- component: {fileID: 6791423131335728073}
- component: {fileID: 8183383920109690380}
- component: {fileID: 3255949411223456789}
- component: {fileID: 4014432302095443276}
m_Layer: 0
m_Name: TowerEntity
m_TagString: Untagged
@ -155,6 +156,11 @@ MonoBehaviour:
_scanOrigin: {fileID: 6791423131335728073}
_retargetInterval: 0.1
_autoUpdate: 0
_attackRangeRenderer: {fileID: 4014432302095443276}
_attackRangeSegments: 64
_attackRangeLineWidth: 0.08
_attackRangeColor: {r: 0.1, g: 1, b: 0.4, a: 0.8}
_attackRangeZOffset: -0.01
--- !u!114 &3255949411223456789
MonoBehaviour:
m_ObjectHideFlags: 0
@ -167,6 +173,110 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d87f56efd9024709a3baf8ef8a6f4fd3, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!120 &4014432302095443276
LineRenderer:
serializedVersion: 2
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1221576993898367501}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 0
m_LightProbeUsage: 0
m_ReflectionProbeUsage: 0
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: 0, y: 0, z: 0}
- {x: 0, y: 0, z: 1}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
widthCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
colorGradient:
serializedVersion: 2
key0: {r: 1, g: 1, b: 1, a: 1}
key1: {r: 1, g: 1, b: 1, a: 1}
key2: {r: 0, g: 0, b: 0, a: 0}
key3: {r: 0, g: 0, b: 0, a: 0}
key4: {r: 0, g: 0, b: 0, a: 0}
key5: {r: 0, g: 0, b: 0, a: 0}
key6: {r: 0, g: 0, b: 0, a: 0}
key7: {r: 0, g: 0, b: 0, a: 0}
ctime0: 0
ctime1: 65535
ctime2: 0
ctime3: 0
ctime4: 0
ctime5: 0
ctime6: 0
ctime7: 0
atime0: 0
atime1: 65535
atime2: 0
atime3: 0
atime4: 0
atime5: 0
atime6: 0
atime7: 0
m_Mode: 0
m_ColorSpace: -1
m_NumColorKeys: 2
m_NumAlphaKeys: 2
numCornerVertices: 0
numCapVertices: 0
alignment: 0
textureMode: 0
textureScale: {x: 1, y: 1}
shadowBias: 0.5
generateLightingData: 0
m_MaskInteraction: 0
m_UseWorldSpace: 1
m_Loop: 0
m_ApplyActiveColorSpace: 1
--- !u!1 &2017874305906296486
GameObject:
m_ObjectHideFlags: 0
@ -269,8 +379,7 @@ MonoBehaviour:
_rotateSpeed: 180
_attackRange: 5
_aimToleranceAngle: 2
_rotateRoot: {fileID: 0}
_yawOnly: 1
_rotateRoot: {fileID: 5517541809701307552}
--- !u!1 &4867507345079921359
GameObject:
m_ObjectHideFlags: 0

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@ namespace GeometryTD.Entity.EntityData
int entityId,
int typeId,
Vector3 position,
Quaternion rotation,
Transform target,
int damage,
float speed,
@ -25,7 +24,7 @@ namespace GeometryTD.Entity.EntityData
float maxLifetime = 3f) : base(entityId, typeId)
{
Position = position;
Rotation = rotation;
_target = target;
_damage = damage;
_speed = speed;

View File

@ -1,5 +1,6 @@
using Components;
using GeometryTD.Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.Entity
@ -36,6 +37,7 @@ namespace GeometryTD.Entity
return;
}
ConstrainToZRotation();
_shooterBullet.OnShow(bulletData);
if (_shooterBullet.TryConsumeDespawnRequest())
{
@ -64,5 +66,11 @@ namespace GeometryTD.Entity
_shooterBullet?.OnReset();
base.OnHide(isShutdown, userData);
}
private void ConstrainToZRotation()
{
Vector3 localEulerAngles = CachedTransform.localEulerAngles;
CachedTransform.localRotation = Quaternion.Euler(0f, 0f, localEulerAngles.z);
}
}
}

View File

@ -8,6 +8,11 @@ namespace GeometryTD.Entity
{
private DefenseTowerController _towerController;
public void SetAttackRangeVisible(bool visible)
{
_towerController?.SetAttackRangeVisible(visible);
}
protected override void OnInit(object userData)
{
base.OnInit(userData);
@ -48,6 +53,7 @@ namespace GeometryTD.Entity
protected override void OnHide(bool isShutdown, object userData)
{
_towerController?.SetAttackRangeVisible(false);
_towerController?.OnReset();
base.OnHide(isShutdown, userData);
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.Entity
{
@ -116,11 +117,14 @@ namespace GeometryTD.Entity
_currentHealth = Mathf.Max(0, _currentHealth - damage);
if (_maxHealth > 0)
{
GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth, (float)_currentHealth / _maxHealth);
GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth,
(float)_currentHealth / _maxHealth);
Log.Info($"ShowBar: {_currentHealth}/{_maxHealth}");
}
if (_currentHealth <= 0)
{
Log.Info("Enemy Dead");
_killedEnemyEntityIds.Add(Id);
RequestDespawn();
}
@ -192,4 +196,4 @@ namespace GeometryTD.Entity
GameEntry.Entity.HideEntity(Entity);
}
}
}
}

View File

@ -55,7 +55,6 @@ namespace GeometryTD.Entity
Name = Utility.Text.Format("[Entity {0}]", Id);
CachedTransform.localPosition = _entityData.Position;
CachedTransform.localRotation = _entityData.Rotation;
CachedTransform.localScale = Vector3.one;
}
#if UNITY_2017_3_OR_NEWER

View File

@ -46,6 +46,7 @@ namespace GeometryTD.Entity
private bool _hasSelectedFoundationCell;
private Vector3Int _selectedFoundationCell;
private int _selectedTowerEntityId;
private int _attackRangeVisibleTowerEntityId;
public IReadOnlyList<Vector3Int> PathCells => _pathCells;
public IReadOnlyList<Vector3Int> FoundationCells => _foundationCells;
@ -486,6 +487,8 @@ namespace GeometryTD.Entity
ClearSelectedObject();
break;
}
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
}
private bool TryBuildTower(int buildIndex)
@ -517,6 +520,7 @@ namespace GeometryTD.Entity
_foundationCellByTowerEntityId[towerEntityId] = _selectedFoundationCell;
_towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats);
_selectedTowerEntityId = towerEntityId;
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
return true;
}
@ -552,6 +556,11 @@ namespace GeometryTD.Entity
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
_selectedTowerEntityId = fallbackTowerEntityId;
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
}
else
{
UpdateTowerAttackRangeDisplay(0);
}
GameEntry.CombatNode?.AddCoin(upgradeCost);
@ -564,6 +573,7 @@ namespace GeometryTD.Entity
_hasSelectedFoundationCell = true;
_selectedFoundationCell = foundationCell;
_selectedTowerEntityId = newTowerEntityId;
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
return true;
}
@ -664,11 +674,54 @@ namespace GeometryTD.Entity
private void ClearSelectedObject()
{
UpdateTowerAttackRangeDisplay(0);
_hasSelectedFoundationCell = false;
_selectedFoundationCell = default;
_selectedTowerEntityId = 0;
}
private void UpdateTowerAttackRangeDisplay(int towerEntityId)
{
if (_attackRangeVisibleTowerEntityId != 0 && _attackRangeVisibleTowerEntityId != towerEntityId)
{
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
_attackRangeVisibleTowerEntityId = 0;
}
if (towerEntityId == 0)
{
if (_attackRangeVisibleTowerEntityId != 0)
{
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
_attackRangeVisibleTowerEntityId = 0;
}
return;
}
if (SetTowerAttackRangeVisible(towerEntityId, true))
{
_attackRangeVisibleTowerEntityId = towerEntityId;
}
}
private static bool SetTowerAttackRangeVisible(int towerEntityId, bool visible)
{
if (towerEntityId == 0 || GameEntry.Entity == null)
{
return false;
}
EntityBase gameEntity = GameEntry.Entity.GetGameEntity(towerEntityId);
if (gameEntity is not DefenseTowerEntity towerEntity)
{
return false;
}
towerEntity.SetAttackRangeVisible(visible);
return true;
}
private int GetBuildTowerCost(int buildIndex)
{
if (_buildTowerCosts == null || buildIndex < 0 || buildIndex >= _buildTowerCosts.Length)
@ -707,7 +760,7 @@ namespace GeometryTD.Entity
case 0:
return new DefenseTowerStatsData
{
AttackDamage = 10,
AttackDamage = 100,
DamageRandomRate = 0f,
RotateSpeed = 200f,
AttackRange = 4.5f,
@ -755,7 +808,7 @@ namespace GeometryTD.Entity
default:
return new DefenseTowerStatsData
{
AttackDamage = 10,
AttackDamage = 100,
DamageRandomRate = 0f,
RotateSpeed = 180f,
AttackRange = 5f,