CombatNode 相关逻辑补充
- CombatFinishForm:战斗节点的结算页面(杀死的敌人、获得的金币、获得的道具) - CombatSelectForm:战斗节点建造防御塔的选择 UI 以及对防御塔进行操作的 UI(只实现了建造逻辑,在 MapEntity 里做射线检测) - 构建了基本的防御塔三组件(ShooterMuzzleComp/BasicBearingComp/BasicBaseComp)和防御塔主控(DefenseTowerController)以及相应的实体类(DefenseTowerEntity)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class BasicBaseComp : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(0.01f)] private float _attackSpeed = 1f;
|
||||
[SerializeField] private AttackPropertyType _attackPropertyType = AttackPropertyType.Physics;
|
||||
|
||||
private float _cooldownRemaining;
|
||||
|
||||
public float AttackSpeed => _attackSpeed;
|
||||
public AttackPropertyType AttackPropertyType => _attackPropertyType;
|
||||
public bool CanAttack => _cooldownRemaining <= 0f;
|
||||
|
||||
public void OnInit(float attackSpeed, AttackPropertyType attackPropertyType)
|
||||
{
|
||||
_attackSpeed = Mathf.Max(0.01f, attackSpeed);
|
||||
_attackPropertyType = attackPropertyType;
|
||||
_cooldownRemaining = 0f;
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_attackSpeed = 1f;
|
||||
_attackPropertyType = AttackPropertyType.None;
|
||||
_cooldownRemaining = 0f;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (_cooldownRemaining <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cooldownRemaining = Mathf.Max(0f, _cooldownRemaining - Mathf.Max(0f, deltaTime));
|
||||
}
|
||||
|
||||
public bool TryAttack(BasicBearingComp bearingComp, ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
|
||||
{
|
||||
if (bearingComp == null || shooterMuzzleComp == null || target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Tick(deltaTime);
|
||||
bool isAligned = bearingComp.TrackTarget(target, deltaTime);
|
||||
if (!isAligned || !CanAttack)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool fired = shooterMuzzleComp.Attack(target, _attackPropertyType);
|
||||
if (!fired)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_cooldownRemaining = 1f / _attackSpeed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11e3dd172f9d486194b6ca5d7f4dd5e6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,111 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class BasicBearingComp : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(1f)] private float _rotateSpeed = 180f;
|
||||
[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;
|
||||
public float AimToleranceAngle => _aimToleranceAngle;
|
||||
|
||||
public void OnInit(float rotateSpeed, float attackRange = 5f)
|
||||
{
|
||||
_rotateSpeed = Mathf.Max(1f, rotateSpeed);
|
||||
_attackRange = Mathf.Max(0.1f, attackRange);
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_rotateSpeed = 1f;
|
||||
_attackRange = 5f;
|
||||
}
|
||||
|
||||
public bool IsTargetInRange(Transform target, Transform origin = null)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform originTransform = origin != null ? origin : transform;
|
||||
Vector3 delta = target.position - originTransform.position;
|
||||
return delta.sqrMagnitude <= _attackRange * _attackRange;
|
||||
}
|
||||
|
||||
public bool TrackTarget(Transform target, float deltaTime)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
|
||||
Vector3 toTarget = target.position - rotateRoot.position;
|
||||
if (_yawOnly)
|
||||
{
|
||||
toTarget.y = 0f;
|
||||
}
|
||||
|
||||
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);
|
||||
return IsTargetAligned(target);
|
||||
}
|
||||
|
||||
public bool IsTargetAligned(Transform target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
|
||||
Vector3 toTarget = target.position - rotateRoot.position;
|
||||
if (_yawOnly)
|
||||
{
|
||||
toTarget.y = 0f;
|
||||
}
|
||||
|
||||
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float angle = Vector3.Angle(rotateRoot.forward, toTarget.normalized);
|
||||
return angle <= _aimToleranceAngle;
|
||||
}
|
||||
|
||||
public bool TryAttack(ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
|
||||
{
|
||||
if (shooterMuzzleComp == null || target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsTargetInRange(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isAligned = TrackTarget(target, deltaTime);
|
||||
if (!isAligned)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return shooterMuzzleComp.Attack(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c8b3d58a8bb41f493706b40fc451558
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,168 @@
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class DefenseTowerController : MonoBehaviour
|
||||
{
|
||||
[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;
|
||||
|
||||
private Transform _currentTarget;
|
||||
private float _retargetTimer;
|
||||
|
||||
public Transform CurrentTarget => _currentTarget;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveComponents();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_autoUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnUpdate(Time.deltaTime);
|
||||
}
|
||||
|
||||
public void OnInit(DefenseTowerStatsData stats)
|
||||
{
|
||||
ResolveComponents();
|
||||
if (stats == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_muzzleComp?.OnInit(stats.AttackDamage, stats.AttackMethodType);
|
||||
_bearingComp?.OnInit(stats.RotateSpeed, stats.AttackRange);
|
||||
_baseComp?.OnInit(stats.AttackSpeed, stats.AttackPropertyType);
|
||||
_currentTarget = null;
|
||||
_retargetTimer = 0f;
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_currentTarget = null;
|
||||
_retargetTimer = 0f;
|
||||
_muzzleComp?.OnReset();
|
||||
_bearingComp?.OnReset();
|
||||
_baseComp?.OnReset();
|
||||
}
|
||||
|
||||
public void SetAutoUpdate(bool autoUpdate)
|
||||
{
|
||||
_autoUpdate = autoUpdate;
|
||||
}
|
||||
|
||||
public void SetTarget(Transform target)
|
||||
{
|
||||
_currentTarget = target;
|
||||
}
|
||||
|
||||
public void ClearTarget()
|
||||
{
|
||||
_currentTarget = null;
|
||||
}
|
||||
|
||||
public void OnUpdate(float deltaTime)
|
||||
{
|
||||
if (!HasCoreComponents())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsTargetValid(_currentTarget) || !_bearingComp.IsTargetInRange(_currentTarget, _scanOrigin))
|
||||
{
|
||||
TryRetarget(deltaTime);
|
||||
}
|
||||
|
||||
if (_currentTarget == null)
|
||||
{
|
||||
_baseComp.Tick(deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
_baseComp.TryAttack(_bearingComp, _muzzleComp, _currentTarget, deltaTime);
|
||||
}
|
||||
|
||||
private void TryRetarget(float deltaTime)
|
||||
{
|
||||
_retargetTimer -= Mathf.Max(0f, deltaTime);
|
||||
if (_retargetTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_retargetTimer = _retargetInterval;
|
||||
_currentTarget = FindNearestEnemyTarget();
|
||||
}
|
||||
|
||||
private Transform FindNearestEnemyTarget()
|
||||
{
|
||||
Vector3 origin = _scanOrigin != null ? _scanOrigin.position : transform.position;
|
||||
float maxRange = _bearingComp.AttackRange;
|
||||
float maxRangeSquared = maxRange * maxRange;
|
||||
|
||||
EnemyEntity bestEnemy = null;
|
||||
float bestDistanceSquared = float.MaxValue;
|
||||
for (int i = 0; i < EnemyEntity.ActiveEnemies.Count; i++)
|
||||
{
|
||||
EnemyEntity enemy = EnemyEntity.ActiveEnemies[i];
|
||||
if (enemy == null || !enemy.isActiveAndEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Transform enemyTransform = enemy.transform;
|
||||
Vector3 delta = enemyTransform.position - origin;
|
||||
float distanceSquared = delta.sqrMagnitude;
|
||||
if (distanceSquared > maxRangeSquared || distanceSquared >= bestDistanceSquared)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bestDistanceSquared = distanceSquared;
|
||||
bestEnemy = enemy;
|
||||
}
|
||||
|
||||
return bestEnemy != null ? bestEnemy.transform : null;
|
||||
}
|
||||
|
||||
private void ResolveComponents()
|
||||
{
|
||||
if (_muzzleComp == null)
|
||||
{
|
||||
_muzzleComp = GetComponent<ShooterMuzzleComp>();
|
||||
}
|
||||
|
||||
if (_bearingComp == null)
|
||||
{
|
||||
_bearingComp = GetComponent<BasicBearingComp>();
|
||||
}
|
||||
|
||||
if (_baseComp == null)
|
||||
{
|
||||
_baseComp = GetComponent<BasicBaseComp>();
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasCoreComponents()
|
||||
{
|
||||
return _muzzleComp != null && _bearingComp != null && _baseComp != null;
|
||||
}
|
||||
|
||||
private static bool IsTargetValid(Transform target)
|
||||
{
|
||||
return target != null && target.gameObject.activeInHierarchy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b33f7d4136745b8bbf516ec80495b80
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
public interface IDamageReceiver
|
||||
{
|
||||
void TakeDamage(int damage, AttackPropertyType attackPropertyType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b8c22c0144cd4e4c887c3cf49ba35a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,121 @@
|
||||
using UnityEngine;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class ShooterBullet : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(0.1f)] private float _defaultSpeed = 12f;
|
||||
[SerializeField] [Min(0.02f)] private float _hitDistance = 0.15f;
|
||||
[SerializeField] [Min(0.1f)] private float _maxLifetime = 3f;
|
||||
|
||||
private Transform _target;
|
||||
private float _speed;
|
||||
private int _damage;
|
||||
private AttackPropertyType _attackPropertyType;
|
||||
private float _lifetime;
|
||||
private float _runtimeMaxLifetime;
|
||||
private bool _isRunning;
|
||||
private bool _despawnRequested;
|
||||
|
||||
public void OnShow(BulletData bulletData)
|
||||
{
|
||||
_target = bulletData != null ? bulletData.Target : null;
|
||||
_damage = bulletData != null ? Mathf.Max(0, bulletData.Damage) : 0;
|
||||
_attackPropertyType = bulletData != null ? bulletData.AttackPropertyType : AttackPropertyType.None;
|
||||
_speed = bulletData != null && bulletData.Speed > 0f ? bulletData.Speed : _defaultSpeed;
|
||||
_runtimeMaxLifetime = bulletData != null && bulletData.MaxLifetime > 0f ? bulletData.MaxLifetime : _maxLifetime;
|
||||
_lifetime = 0f;
|
||||
_despawnRequested = false;
|
||||
_isRunning = _target != null;
|
||||
if (!_isRunning)
|
||||
{
|
||||
_despawnRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_target = null;
|
||||
_speed = 0f;
|
||||
_damage = 0;
|
||||
_attackPropertyType = AttackPropertyType.None;
|
||||
_lifetime = 0f;
|
||||
_runtimeMaxLifetime = _maxLifetime;
|
||||
_isRunning = false;
|
||||
_despawnRequested = false;
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lifetime += Mathf.Max(0f, deltaTime);
|
||||
if (_lifetime >= _runtimeMaxLifetime || _target == null)
|
||||
{
|
||||
_isRunning = false;
|
||||
_despawnRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 toTarget = _target.position - transform.position;
|
||||
float hitDistanceSquared = _hitDistance * _hitDistance;
|
||||
if (toTarget.sqrMagnitude <= hitDistanceSquared)
|
||||
{
|
||||
HitTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 forward = toTarget.normalized;
|
||||
float moveDistance = _speed * Mathf.Max(0f, deltaTime);
|
||||
if (moveDistance * moveDistance >= toTarget.sqrMagnitude)
|
||||
{
|
||||
transform.position = _target.position;
|
||||
HitTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
transform.position += forward * moveDistance;
|
||||
transform.forward = forward;
|
||||
}
|
||||
|
||||
public bool TryConsumeDespawnRequest()
|
||||
{
|
||||
if (!_despawnRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_despawnRequested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HitTarget()
|
||||
{
|
||||
if (_target == null)
|
||||
{
|
||||
_isRunning = false;
|
||||
_despawnRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
MonoBehaviour[] components = _target.GetComponentsInParent<MonoBehaviour>();
|
||||
for (int i = 0; i < components.Length; i++)
|
||||
{
|
||||
if (components[i] is IDamageReceiver damageReceiver)
|
||||
{
|
||||
damageReceiver.TakeDamage(_damage, _attackPropertyType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_isRunning = false;
|
||||
_despawnRequested = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81f9627601c091545a9b92fc7ef41f04
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Components
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class ShooterMuzzleComp : MonoBehaviour
|
||||
{
|
||||
[SerializeField] [Min(1f)] private int _attackDamage = 10;
|
||||
[SerializeField] private AttackMethodType _attackMethodType = AttackMethodType.NormalBullet;
|
||||
[SerializeField] [Min(1)] private int _bulletTypeId = 501;
|
||||
[SerializeField] private Transform _muzzlePoint;
|
||||
[SerializeField] [Min(0.1f)] private float _bulletSpeed = 12f;
|
||||
|
||||
public int AttackDamage => _attackDamage;
|
||||
public AttackMethodType AttackMethodType => _attackMethodType;
|
||||
|
||||
public void OnInit(int attackDamage, AttackMethodType attackMethodType = AttackMethodType.NormalBullet, int bulletTypeId = 501)
|
||||
{
|
||||
_attackDamage = Mathf.Max(1, attackDamage);
|
||||
_attackMethodType = attackMethodType;
|
||||
_bulletTypeId = Mathf.Max(1, bulletTypeId);
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
_attackDamage = 1;
|
||||
_attackMethodType = AttackMethodType.None;
|
||||
_bulletTypeId = 501;
|
||||
}
|
||||
|
||||
public bool Attack(Transform target)
|
||||
{
|
||||
return Attack(target, AttackPropertyType.None);
|
||||
}
|
||||
|
||||
public bool Attack(Transform target, AttackPropertyType attackPropertyType)
|
||||
{
|
||||
if (_attackMethodType != AttackMethodType.NormalBullet)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform spawnPoint = _muzzlePoint != null ? _muzzlePoint : transform;
|
||||
int bulletEntityId = GameEntry.Entity.GenerateSerialId();
|
||||
BulletData bulletData = new BulletData(
|
||||
bulletEntityId,
|
||||
_bulletTypeId,
|
||||
spawnPoint.position,
|
||||
spawnPoint.rotation,
|
||||
target,
|
||||
_attackDamage,
|
||||
_bulletSpeed,
|
||||
attackPropertyType);
|
||||
GameEntry.Entity.ShowBullet(bulletData);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20a2c131403122146a41148cb72fcd43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user