Compare commits
3 Commits
fbc7e66491
...
846e4d6f44
| Author | SHA1 | Date |
|---|---|---|
|
|
846e4d6f44 | |
|
|
25f36d38c9 | |
|
|
0dc5889417 |
|
|
@ -7,6 +7,7 @@ using SepCore.CameraModule;
|
|||
using SepCore.SpriteCache;
|
||||
using SepCore.UIRouter;
|
||||
using SepCore.Simulation;
|
||||
using SepCore.Timer;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏入口。
|
||||
|
|
@ -31,6 +32,8 @@ public partial class GameEntry
|
|||
|
||||
public static CameraModuleComponent CameraModule { get; private set; }
|
||||
|
||||
public static TimerComponent Timer { get; private set; }
|
||||
|
||||
private static void InitCustomComponents()
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
|
|
@ -42,5 +45,6 @@ public partial class GameEntry
|
|||
UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>();
|
||||
InputModule = UnityGameFramework.Runtime.GameEntry.GetComponent<InputModuleComponent>();
|
||||
CameraModule = UnityGameFramework.Runtime.GameEntry.GetComponent<CameraModuleComponent>();
|
||||
Timer = UnityGameFramework.Runtime.GameEntry.GetComponent<TimerComponent>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using SepCore.Components;
|
||||
using SepCore.CustomUtility;
|
||||
using SepCore.Definition;
|
||||
using SepCore.Timer;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
|
@ -23,7 +24,8 @@ namespace SepCore.Entity
|
|||
private int _attackDamage = 1;
|
||||
|
||||
private float _sqrAttackRange;
|
||||
private float _currAttackTimer;
|
||||
private TimerHandle _attackTimerHandle;
|
||||
private bool _canAttack;
|
||||
|
||||
private AttackStateType _attackState = AttackStateType.Idle;
|
||||
|
||||
|
|
@ -69,7 +71,8 @@ namespace SepCore.Entity
|
|||
_attackDamage = Mathf.Max(1, _meleeEnemyData.AttackDamage);
|
||||
_sqrAttackRange = _attackRange * _attackRange;
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
_canAttack = false;
|
||||
_attackTimerHandle = GameEntry.Timer.ScheduleOnce(_attackCooldown, () => _canAttack = true, this);
|
||||
_attackState = AttackStateType.Idle;
|
||||
_targetableTarget = null;
|
||||
|
||||
|
|
@ -93,7 +96,8 @@ namespace SepCore.Entity
|
|||
_movementComponent.OnReset();
|
||||
_healthComponent.OnReset();
|
||||
_targetableTarget = null;
|
||||
_currAttackTimer = 0f;
|
||||
GameEntry.Timer.Cancel(_attackTimerHandle);
|
||||
_attackTimerHandle = TimerHandle.Invalid;
|
||||
_attackState = AttackStateType.Idle;
|
||||
|
||||
base.OnHide(isShutdown, userData);
|
||||
|
|
@ -109,8 +113,6 @@ namespace SepCore.Entity
|
|||
|
||||
private void UpdateAttackState(float elapseSeconds)
|
||||
{
|
||||
_currAttackTimer += elapseSeconds;
|
||||
|
||||
switch (_attackState)
|
||||
{
|
||||
case AttackStateType.Idle:
|
||||
|
|
@ -151,8 +153,10 @@ namespace SepCore.Entity
|
|||
return;
|
||||
}
|
||||
|
||||
if (_currAttackTimer >= _attackCooldown)
|
||||
if (_canAttack)
|
||||
{
|
||||
_canAttack = false;
|
||||
_attackTimerHandle = GameEntry.Timer.ScheduleOnce(_attackCooldown, () => _canAttack = true, this);
|
||||
TransitionTo(AttackStateType.Attack);
|
||||
}
|
||||
|
||||
|
|
@ -169,21 +173,15 @@ namespace SepCore.Entity
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using SepCore.AsyncTask;
|
|||
using SepCore.CustomUtility;
|
||||
using SepCore.Definition;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using SepCore.Timer;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
|
@ -27,7 +28,8 @@ namespace SepCore.Entity
|
|||
private float _attackRangeSquared;
|
||||
private float _attackCooldown = 1f;
|
||||
private int _attackDamage = 1;
|
||||
private float _currAttackTimer;
|
||||
private TimerHandle _attackTimerHandle;
|
||||
private bool _canAttack;
|
||||
|
||||
[SerializeField] private float _projectileSpeed = 12f;
|
||||
[SerializeField] private float _projectileLifeTime = 3f;
|
||||
|
|
@ -81,7 +83,8 @@ namespace SepCore.Entity
|
|||
_projectileSpawnHeightOffset);
|
||||
_projectileAssetName = ReadStringParam(ProjectileAssetNameParamKey, _projectileAssetName);
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
_canAttack = false;
|
||||
_attackTimerHandle = GameEntry.Timer.ScheduleOnce(_attackCooldown, () => _canAttack = true, this);
|
||||
this.CachedTransform.position = enemyData.Position;
|
||||
}
|
||||
else
|
||||
|
|
@ -94,8 +97,6 @@ namespace SepCore.Entity
|
|||
{
|
||||
base.OnUpdate(elapseSeconds, realElapseSeconds);
|
||||
|
||||
_currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_target == null)
|
||||
{
|
||||
_movementComponent.SetMove(false);
|
||||
|
|
@ -108,7 +109,12 @@ namespace SepCore.Entity
|
|||
if (distanceSquared <= _attackRangeSquared)
|
||||
{
|
||||
_movementComponent.SetMove(false);
|
||||
if (_canAttack)
|
||||
{
|
||||
TryFireProjectile();
|
||||
_canAttack = false;
|
||||
_attackTimerHandle = GameEntry.Timer.ScheduleOnce(_attackCooldown, () => _canAttack = true, this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -121,14 +127,14 @@ namespace SepCore.Entity
|
|||
{
|
||||
_movementComponent.OnReset();
|
||||
_healthComponent.OnReset();
|
||||
_currAttackTimer = 0f;
|
||||
GameEntry.Timer.Cancel(_attackTimerHandle);
|
||||
_attackTimerHandle = TimerHandle.Invalid;
|
||||
|
||||
base.OnHide(isShutdown, userData);
|
||||
}
|
||||
|
||||
private void TryFireProjectile()
|
||||
{
|
||||
if (_currAttackTimer < _attackCooldown || _target == null) return;
|
||||
if (!EnsureEnemyProjectileGroup()) return;
|
||||
|
||||
Vector3 spawnPosition = this.CachedTransform.position +
|
||||
|
|
@ -165,8 +171,6 @@ namespace SepCore.Entity
|
|||
EnemyProjectileGroupName,
|
||||
Constant.AssetPriority.BulletAsset,
|
||||
projectileData).Forget();
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
}
|
||||
|
||||
private static bool EnsureEnemyProjectileGroup()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using SepCore.Definition;
|
|||
using SepCore.Entity;
|
||||
using GameFramework;
|
||||
using SepCore.CustomUtility;
|
||||
using SepCore.Timer;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
|
|
@ -41,7 +42,8 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
protected EntityBase _target;
|
||||
|
||||
protected float _currAttackTimer;
|
||||
protected bool _canAttack;
|
||||
protected TimerHandle _attackTimerHandle;
|
||||
|
||||
protected float _sqrRange;
|
||||
|
||||
|
|
@ -306,6 +308,26 @@ namespace SepCore.Entity.Weapon
|
|||
_attackStatCallback = null;
|
||||
AttackStat = new StatProperty();
|
||||
}
|
||||
|
||||
protected void StartAttackCooldown(float cooldown)
|
||||
{
|
||||
_canAttack = false;
|
||||
_attackTimerHandle = GameEntry.Timer.ScheduleOnce(cooldown, () => _canAttack = true, this);
|
||||
}
|
||||
|
||||
protected void ResetAttackCooldown(float cooldown)
|
||||
{
|
||||
GameEntry.Timer.Cancel(_attackTimerHandle);
|
||||
_canAttack = false;
|
||||
_attackTimerHandle = GameEntry.Timer.ScheduleOnce(cooldown, () => _canAttack = true, this);
|
||||
}
|
||||
|
||||
protected void CancelAttackCooldown()
|
||||
{
|
||||
GameEntry.Timer.Cancel(_attackTimerHandle);
|
||||
_attackTimerHandle = TimerHandle.Invalid;
|
||||
_canAttack = false;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class WeaponStateBase
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon._currAttackTimer = 0f;
|
||||
_weapon.ResetAttackCooldown(_weapon._weaponData.Cooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
@ -34,7 +33,7 @@ namespace SepCore.Entity.Weapon
|
|||
return;
|
||||
}
|
||||
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToOrigin(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target != null && _weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ namespace SepCore.Entity.Weapon
|
|||
if (_weaponData == null) return false;
|
||||
|
||||
WeaponData = _weaponData;
|
||||
_currAttackTimer = 0f;
|
||||
StartAttackCooldown(_weaponData.Cooldown);
|
||||
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||
_cachedRotation = CachedTransform.rotation;
|
||||
_attackEffect = new HandgunHitMarkerAttackEffect(_hitMarkerSize, _hitMarkerYOffset, _hitMarkerDuration,
|
||||
|
|
@ -151,6 +151,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
protected override void OnWeaponHide(object userData)
|
||||
{
|
||||
CancelAttackCooldown();
|
||||
_attackEffect = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon._currAttackTimer = 0f;
|
||||
_weapon.ResetAttackCooldown(_weapon._weaponData.Cooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
@ -35,7 +34,7 @@ namespace SepCore.Entity.Weapon
|
|||
return;
|
||||
}
|
||||
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToOrigin(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target != null && _weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ namespace SepCore.Entity.Weapon
|
|||
if (_weaponData == null) return false;
|
||||
WeaponData = _weaponData;
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
StartAttackCooldown(_weaponData.Cooldown);
|
||||
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||
_cachedRotation = CachedTransform.rotation;
|
||||
|
||||
|
|
@ -183,6 +183,7 @@ namespace SepCore.Entity.Weapon
|
|||
protected override void OnWeaponHide(object userData)
|
||||
{
|
||||
StopAttackTween(true);
|
||||
CancelAttackCooldown();
|
||||
_attackEffect = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon._currAttackTimer = 0f;
|
||||
_weapon.ResetAttackCooldown(_weapon._weaponData.Cooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
@ -35,7 +34,7 @@ namespace SepCore.Entity.Weapon
|
|||
return;
|
||||
}
|
||||
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToOrigin(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target != null && _weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ namespace SepCore.Entity.Weapon
|
|||
if (_weaponData == null) return false;
|
||||
WeaponData = _weaponData;
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
StartAttackCooldown(_weaponData.Cooldown);
|
||||
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||
_cachedRotation = CachedTransform.rotation;
|
||||
|
||||
|
|
@ -308,6 +308,7 @@ namespace SepCore.Entity.Weapon
|
|||
protected override void OnWeaponHide(object userData)
|
||||
{
|
||||
StopAttackTween(true);
|
||||
CancelAttackCooldown();
|
||||
_attackEffect = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon._currAttackTimer = 0f;
|
||||
_weapon.ResetAttackCooldown(_weapon._weaponData.Cooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
@ -35,7 +34,7 @@ namespace SepCore.Entity.Weapon
|
|||
return;
|
||||
}
|
||||
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToOrigin(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target != null && _weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ namespace SepCore.Entity.Weapon
|
|||
if (_weaponData == null) return false;
|
||||
WeaponData = _weaponData;
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
StartAttackCooldown(_weaponData.Cooldown);
|
||||
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||
_cachedRotation = CachedTransform.rotation;
|
||||
|
||||
|
|
@ -209,6 +209,7 @@ namespace SepCore.Entity.Weapon
|
|||
protected override void OnWeaponHide(object userData)
|
||||
{
|
||||
StopAttackTween(true);
|
||||
CancelAttackCooldown();
|
||||
_attackEffect = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon._currAttackTimer = 0f;
|
||||
_weapon.ResetAttackCooldown(_weapon._weaponData.Cooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace SepCore.Entity.Weapon
|
|||
|
||||
public override void OnEnter()
|
||||
{
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
@ -34,7 +33,7 @@ namespace SepCore.Entity.Weapon
|
|||
return;
|
||||
}
|
||||
|
||||
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
|
||||
if (_weapon._canAttack)
|
||||
{
|
||||
_weapon.TransitionTo(WeaponStateType.Attack);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToTarget(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target == null || !_weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ namespace SepCore.Entity.Weapon
|
|||
{
|
||||
_weapon.Check();
|
||||
_weapon.RotateToOrigin(elapseSeconds);
|
||||
_weapon._currAttackTimer += elapseSeconds;
|
||||
|
||||
if (_weapon._target != null && _weapon._target.Available)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ namespace SepCore.Entity.Weapon
|
|||
if (_weaponData == null) return false;
|
||||
WeaponData = _weaponData;
|
||||
|
||||
_currAttackTimer = 0f;
|
||||
StartAttackCooldown(_weaponData.Cooldown);
|
||||
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
|
||||
_cachedRotation = CachedTransform.rotation;
|
||||
|
||||
|
|
@ -209,6 +209,7 @@ namespace SepCore.Entity.Weapon
|
|||
protected override void OnWeaponHide(object userData)
|
||||
{
|
||||
StopAttackTween(true);
|
||||
CancelAttackCooldown();
|
||||
_attackEffect = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -245,6 +245,7 @@ Transform:
|
|||
- {fileID: 1652245191}
|
||||
- {fileID: 1245211031}
|
||||
- {fileID: 1170305582}
|
||||
- {fileID: 1852433568}
|
||||
m_Father: {fileID: 1852670053}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &120093239
|
||||
|
|
@ -1423,6 +1424,50 @@ MonoBehaviour:
|
|||
ProjectileHitPresentationEffectTypeId: 0
|
||||
_targetSelectionSettings:
|
||||
CellSize: 2
|
||||
--- !u!1 &1852433567
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1852433568}
|
||||
- component: {fileID: 1852433569}
|
||||
m_Layer: 0
|
||||
m_Name: Timer
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1852433568
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1852433567}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1852433569
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1852433567}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 86583a9107c4c3b45a888f4cc47c2856, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &1852670052
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f47e8e71c7cdc5743a416147e47c5d0a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b96faa6532eb3314597c8e167cfd20ea
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "SepCore.Timer.Editor",
|
||||
"rootNamespace": "SepCore.Timer",
|
||||
"references": [
|
||||
"GUID:363c5eb08ff8e6a439b85e37b8c20d96",
|
||||
"GUID:436e23dbdc31e7d4fb5c3f804548b2df"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2703d0b6ecf886044905648043ceb65d
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SepCore.Timer.Editor
|
||||
{
|
||||
[CustomEditor(typeof(TimerComponent))]
|
||||
public sealed class TimerComponentEditor : UnityEditor.Editor
|
||||
{
|
||||
private TimerComponent _timer;
|
||||
private Vector2 _scrollPosition;
|
||||
private bool _showDebugControls = true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_timer = (TimerComponent)target;
|
||||
EditorApplication.update += Repaint;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= Repaint;
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.HelpBox("运行时显示任务列表和调试信息", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawStatistics();
|
||||
DrawDebugControls();
|
||||
DrawTaskList();
|
||||
}
|
||||
|
||||
private void DrawStatistics()
|
||||
{
|
||||
TimerTaskSnapshot[] tasks = _timer.GetTaskSnapshots();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("统计信息", EditorStyles.boldLabel);
|
||||
|
||||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||
{
|
||||
int scaledCount = 0;
|
||||
int unscaledCount = 0;
|
||||
int errorCount = 0;
|
||||
int infiniteCount = 0;
|
||||
|
||||
foreach (TimerTaskSnapshot task in tasks)
|
||||
{
|
||||
if (task.TimeMode == TimerTimeMode.Scaled)
|
||||
scaledCount++;
|
||||
else
|
||||
unscaledCount++;
|
||||
|
||||
if (task.HasError)
|
||||
errorCount++;
|
||||
|
||||
if (task.RemainingRepeatCount < 0)
|
||||
infiniteCount++;
|
||||
}
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField("活跃任务", $"{tasks.Length}", GUILayout.Width(120));
|
||||
EditorGUILayout.LabelField("Scaled", $"{scaledCount}", GUILayout.Width(80));
|
||||
EditorGUILayout.LabelField("Unscaled", $"{unscaledCount}", GUILayout.Width(80));
|
||||
}
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField("无限循环", $"{infiniteCount}", GUILayout.Width(120));
|
||||
EditorGUILayout.LabelField("异常任务", $"{errorCount}", errorCount > 0 ? GetErrorLabelStyle() : GUIStyle.none, GUILayout.Width(80));
|
||||
EditorGUILayout.LabelField("全局状态", _timer.IsPaused ? "<color=orange>已暂停</color>" : "<color=green>运行中</color>", GetRichLabelStyle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDebugControls()
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
_showDebugControls = EditorGUILayout.Foldout(_showDebugControls, "调试控制", true);
|
||||
|
||||
if (!_showDebugControls)
|
||||
return;
|
||||
|
||||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button(_timer.IsPaused ? "恢复全部" : "暂停全部", GUILayout.Height(25)))
|
||||
{
|
||||
if (_timer.IsPaused)
|
||||
_timer.Resume();
|
||||
else
|
||||
_timer.Pause();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("清理全部", GUILayout.Height(25)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("确认", "确定要清除所有定时任务吗?", "确定", "取消"))
|
||||
{
|
||||
_timer.ClearAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTaskList()
|
||||
{
|
||||
TimerTaskSnapshot[] tasks = _timer.GetTaskSnapshots();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField($"活跃任务列表 ({tasks.Length})", EditorStyles.boldLabel);
|
||||
|
||||
if (tasks.Length == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("当前没有活跃的定时任务", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||
|
||||
foreach (TimerTaskSnapshot task in tasks)
|
||||
{
|
||||
DrawTaskItem(task);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawTaskItem(TimerTaskSnapshot task)
|
||||
{
|
||||
Color originalColor = GUI.backgroundColor;
|
||||
|
||||
if (task.HasError)
|
||||
GUI.backgroundColor = new Color(1f, 0.3f, 0.3f, 0.3f);
|
||||
|
||||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||
{
|
||||
if (task.HasError)
|
||||
{
|
||||
EditorGUILayout.LabelField("⚠ 该任务回调曾抛出异常", GetErrorLabelStyle());
|
||||
}
|
||||
|
||||
// 头部:ID + 时间模式 + 取消按钮
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField($"ID: #{task.Id}", GUILayout.Width(60));
|
||||
|
||||
string modeLabel = task.TimeMode == TimerTimeMode.Scaled ? "Scaled" : "Unscaled";
|
||||
Color modeColor = task.TimeMode == TimerTimeMode.Scaled ? Color.cyan : Color.yellow;
|
||||
EditorGUILayout.LabelField($"<color=#{ColorUtility.ToHtmlStringRGB(modeColor)}>{modeLabel}</color>", GetRichLabelStyle(), GUILayout.Width(60));
|
||||
|
||||
string repeatLabel = task.RemainingRepeatCount < 0 ? "∞" : $"{task.RemainingRepeatCount}";
|
||||
EditorGUILayout.LabelField($"剩余: {repeatLabel} 次", GUILayout.Width(80));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("取消", GUILayout.Width(50)))
|
||||
{
|
||||
_timer.Cancel(new TimerHandle(task.Id));
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条
|
||||
DrawProgressBar(task);
|
||||
|
||||
// 回调信息
|
||||
EditorGUILayout.LabelField("回调:", task.CallbackMethod, EditorStyles.miniLabel);
|
||||
|
||||
// 归属对象(可点击 Ping)
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.LabelField("归属:", GUILayout.Width(40));
|
||||
if (task.Owner is UnityEngine.Object unityObject && unityObject != null)
|
||||
{
|
||||
if (GUILayout.Button(unityObject.name, EditorStyles.linkLabel))
|
||||
{
|
||||
EditorGUIUtility.PingObject(unityObject);
|
||||
Selection.activeObject = unityObject;
|
||||
}
|
||||
}
|
||||
else if (task.Owner != null)
|
||||
{
|
||||
EditorGUILayout.LabelField(task.Owner.ToString(), EditorStyles.miniLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField("(无归属对象)", EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUI.backgroundColor = originalColor;
|
||||
EditorGUILayout.Space(2);
|
||||
}
|
||||
|
||||
private void DrawProgressBar(TimerTaskSnapshot task)
|
||||
{
|
||||
float progress;
|
||||
string label;
|
||||
|
||||
if (task.Interval <= 0f || task.RemainingRepeatCount == 1)
|
||||
{
|
||||
// 一次性任务:显示延迟进度
|
||||
float totalTime = Mathf.Max(task.Interval, task.RemainingTime);
|
||||
progress = totalTime > 0f ? 1f - (task.RemainingTime / totalTime) : 1f;
|
||||
label = $"剩余 {task.RemainingTime:F2}s (一次性)";
|
||||
}
|
||||
else if (task.RemainingRepeatCount < 0)
|
||||
{
|
||||
// 无限循环:显示当前周期进度
|
||||
progress = 1f - (task.RemainingTime / task.Interval);
|
||||
label = $"剩余 {task.RemainingTime:F2}s (无限循环)";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 有限循环:显示当前周期进度
|
||||
progress = 1f - (task.RemainingTime / task.Interval);
|
||||
label = $"剩余 {task.RemainingTime:F2}s (周期进度)";
|
||||
}
|
||||
|
||||
Rect rect = EditorGUILayout.GetControlRect(false, 18);
|
||||
EditorGUI.ProgressBar(rect, Mathf.Clamp01(progress), label);
|
||||
}
|
||||
|
||||
private static GUIStyle GetRichLabelStyle()
|
||||
{
|
||||
GUIStyle style = new GUIStyle(EditorStyles.label);
|
||||
style.richText = true;
|
||||
return style;
|
||||
}
|
||||
|
||||
private static GUIStyle GetErrorLabelStyle()
|
||||
{
|
||||
GUIStyle style = new GUIStyle(EditorStyles.label);
|
||||
style.normal.textColor = Color.red;
|
||||
style.fontStyle = FontStyle.Bold;
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e536163046bf5ff40bd04430d3a45af5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dfd4cfbe4e915a3479007a9ed6f8539e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"reference": "GUID:436e23dbdc31e7d4fb5c3f804548b2df"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4fad44cf84287fc45a187478fb6c9891
|
||||
AssemblyDefinitionReferenceImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,622 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace SepCore.Timer
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用定时器组件。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Game Main/Timer")]
|
||||
public sealed class TimerComponent : GameFrameworkComponent
|
||||
{
|
||||
private readonly List<TimerTaskInfo> _timerTasks = new List<TimerTaskInfo>();
|
||||
private bool _isPaused = false;
|
||||
private bool _isUpdating = false;
|
||||
private int _serialId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前定时任务数量。
|
||||
/// </summary>
|
||||
public int Count => _timerTasks.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 获取定时器是否暂停。
|
||||
/// </summary>
|
||||
public bool IsPaused => _isPaused;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateTasks(Time.deltaTime, Time.unscaledDeltaTime);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ClearAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建定时任务。
|
||||
/// </summary>
|
||||
/// <param name="timerTask">定时任务描述。</param>
|
||||
/// <returns>定时任务句柄。</returns>
|
||||
public TimerHandle Schedule(TimerTask timerTask)
|
||||
{
|
||||
if (timerTask.Callback == null)
|
||||
{
|
||||
Log.Error("Timer task callback is invalid.");
|
||||
return TimerHandle.Invalid;
|
||||
}
|
||||
|
||||
if (timerTask.RepeatCount == 0)
|
||||
{
|
||||
return TimerHandle.Invalid;
|
||||
}
|
||||
|
||||
float delay = Mathf.Max(0f, timerTask.Delay);
|
||||
float interval = Mathf.Max(0f, timerTask.Interval);
|
||||
if (timerTask.RepeatCount != 1 && interval <= 0f)
|
||||
{
|
||||
Log.Error("Timer task interval must be greater than zero for repeated tasks.");
|
||||
return TimerHandle.Invalid;
|
||||
}
|
||||
|
||||
int id = GenerateSerialId();
|
||||
TimerTaskInfo taskInfo = new TimerTaskInfo(id, delay, interval, timerTask.RepeatCount,
|
||||
timerTask.TimeMode, timerTask.Owner, timerTask.Callback);
|
||||
_timerTasks.Add(taskInfo);
|
||||
return new TimerHandle(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一次性定时任务。
|
||||
/// </summary>
|
||||
public TimerHandle ScheduleOnce(float delay, Action callback, object owner = null,
|
||||
TimerTimeMode timeMode = TimerTimeMode.Scaled)
|
||||
{
|
||||
return Schedule(new TimerTask(delay, delay, 1, callback, owner, timeMode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建循环定时任务,首次触发延迟等于循环间隔。
|
||||
/// </summary>
|
||||
public TimerHandle ScheduleRepeat(float interval, Action callback, int repeatCount = -1, object owner = null,
|
||||
TimerTimeMode timeMode = TimerTimeMode.Scaled)
|
||||
{
|
||||
return ScheduleRepeat(interval, interval, callback, repeatCount, owner, timeMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建循环定时任务。
|
||||
/// </summary>
|
||||
public TimerHandle ScheduleRepeat(float delay, float interval, Action callback, int repeatCount = -1,
|
||||
object owner = null, TimerTimeMode timeMode = TimerTimeMode.Scaled)
|
||||
{
|
||||
return Schedule(new TimerTask(delay, interval, repeatCount, callback, owner, timeMode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消定时任务。
|
||||
/// </summary>
|
||||
public bool Cancel(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = _timerTasks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id != handle.Id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
taskInfo.IsCancelled = true;
|
||||
if (!_isUpdating)
|
||||
{
|
||||
_timerTasks.RemoveAt(i);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消归属于指定对象的全部定时任务。
|
||||
/// </summary>
|
||||
public int CancelByOwner(object owner)
|
||||
{
|
||||
if (owner == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (int i = _timerTasks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (!ReferenceEquals(taskInfo.Owner, owner))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
taskInfo.IsCancelled = true;
|
||||
count++;
|
||||
if (!_isUpdating)
|
||||
{
|
||||
_timerTasks.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停所有定时任务。
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
_isPaused = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复所有定时任务。
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
_isPaused = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有定时任务。
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
if (!_isUpdating)
|
||||
{
|
||||
_timerTasks.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
_timerTasks[i].IsCancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有活跃任务的快照(调试用)。
|
||||
/// </summary>
|
||||
public TimerTaskSnapshot[] GetTaskSnapshots()
|
||||
{
|
||||
List<TimerTaskSnapshot> snapshots = new List<TimerTaskSnapshot>(_timerTasks.Count);
|
||||
foreach (TimerTaskInfo taskInfo in _timerTasks)
|
||||
{
|
||||
if (taskInfo.IsCancelled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshots.Add(new TimerTaskSnapshot
|
||||
{
|
||||
Id = taskInfo.Id,
|
||||
RemainingTime = taskInfo.RemainingTime,
|
||||
Interval = taskInfo.Interval,
|
||||
RemainingRepeatCount = taskInfo.RemainingRepeatCount,
|
||||
TimeMode = taskInfo.TimeMode,
|
||||
Owner = taskInfo.Owner,
|
||||
CallbackMethod = $"{taskInfo.Callback.Method.DeclaringType?.Name}.{taskInfo.Callback.Method.Name}",
|
||||
HasError = taskInfo.HasError,
|
||||
IsPaused = taskInfo.IsPaused
|
||||
});
|
||||
}
|
||||
|
||||
return snapshots.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置任务的循环间隔。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <param name="newInterval">新的间隔时间(秒)。</param>
|
||||
/// <param name="adjustRemainingTime">是否根据新旧间隔的比例调整当前剩余时间。</param>
|
||||
/// <returns>是否设置成功。</returns>
|
||||
public bool SetInterval(TimerHandle handle, float newInterval, bool adjustRemainingTime = false)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id != handle.Id || taskInfo.IsCancelled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float safeInterval = Mathf.Max(0f, newInterval);
|
||||
if (adjustRemainingTime && taskInfo.Interval > 0f && safeInterval > 0f)
|
||||
{
|
||||
float ratio = safeInterval / taskInfo.Interval;
|
||||
taskInfo.RemainingTime *= ratio;
|
||||
}
|
||||
|
||||
taskInfo.Interval = safeInterval;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停单个任务。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <returns>是否暂停成功。</returns>
|
||||
public bool PauseTask(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id != handle.Id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
taskInfo.IsPaused = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复单个任务。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <returns>是否恢复成功。</returns>
|
||||
public bool ResumeTask(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id != handle.Id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
taskInfo.IsPaused = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取任务的剩余时间。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <returns>剩余时间(秒),任务不存在返回 -1f。</returns>
|
||||
public float GetRemainingTime(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return -1f;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id == handle.Id && !taskInfo.IsCancelled)
|
||||
{
|
||||
return taskInfo.RemainingTime;
|
||||
}
|
||||
}
|
||||
|
||||
return -1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取任务的循环间隔。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <returns>循环间隔(秒),任务不存在返回 -1f。</returns>
|
||||
public float GetInterval(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return -1f;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id == handle.Id && !taskInfo.IsCancelled)
|
||||
{
|
||||
return taskInfo.Interval;
|
||||
}
|
||||
}
|
||||
|
||||
return -1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取任务的剩余重复次数。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <returns>剩余重复次数,任务不存在返回 -1。</returns>
|
||||
public int GetRemainingRepeatCount(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id == handle.Id && !taskInfo.IsCancelled)
|
||||
{
|
||||
return taskInfo.RemainingRepeatCount;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置任务的剩余时间。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <param name="newRemainingTime">新的剩余时间(秒)。</param>
|
||||
/// <returns>是否重置成功。</returns>
|
||||
public bool ResetRemainingTime(TimerHandle handle, float newRemainingTime)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id == handle.Id && !taskInfo.IsCancelled)
|
||||
{
|
||||
taskInfo.RemainingTime = Mathf.Max(0f, newRemainingTime);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务是否已暂停。
|
||||
/// </summary>
|
||||
/// <param name="handle">任务句柄。</param>
|
||||
/// <returns>任务是否已暂停,任务不存在返回 false。</returns>
|
||||
public bool IsTaskPaused(TimerHandle handle)
|
||||
{
|
||||
if (!handle.IsValid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _timerTasks.Count; i++)
|
||||
{
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.Id == handle.Id && !taskInfo.IsCancelled)
|
||||
{
|
||||
return taskInfo.IsPaused;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateTasks(float scaledDeltaTime, float unscaledDeltaTime)
|
||||
{
|
||||
int taskCount = _timerTasks.Count;
|
||||
_isUpdating = true;
|
||||
for (int i = taskCount - 1; i >= 0; i--)
|
||||
{
|
||||
if (i >= _timerTasks.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TimerTaskInfo taskInfo = _timerTasks[i];
|
||||
if (taskInfo.IsCancelled || taskInfo.IsPaused)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float deltaTime = taskInfo.TimeMode == TimerTimeMode.Unscaled ? unscaledDeltaTime : scaledDeltaTime;
|
||||
UpdateTask(taskInfo, deltaTime);
|
||||
}
|
||||
|
||||
_isUpdating = false;
|
||||
RemoveCancelledTasks();
|
||||
}
|
||||
|
||||
private void UpdateTask(TimerTaskInfo taskInfo, float deltaTime)
|
||||
{
|
||||
if (deltaTime <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
taskInfo.RemainingTime -= deltaTime;
|
||||
if (taskInfo.RemainingTime > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InvokeCallback(taskInfo);
|
||||
if (taskInfo.IsCancelled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskInfo.RemainingRepeatCount > 0)
|
||||
{
|
||||
taskInfo.RemainingRepeatCount--;
|
||||
}
|
||||
|
||||
if (taskInfo.RemainingRepeatCount == 0)
|
||||
{
|
||||
taskInfo.IsCancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
taskInfo.RemainingTime = taskInfo.Interval;
|
||||
}
|
||||
|
||||
private void InvokeCallback(TimerTaskInfo taskInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
taskInfo.Callback.Invoke();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
taskInfo.HasError = true;
|
||||
Log.Error("Timer task callback exception: {0}", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveCancelledTasks()
|
||||
{
|
||||
for (int i = _timerTasks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_timerTasks[i].IsCancelled)
|
||||
{
|
||||
_timerTasks.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int GenerateSerialId()
|
||||
{
|
||||
_serialId++;
|
||||
if (_serialId <= 0)
|
||||
{
|
||||
_serialId = 1;
|
||||
}
|
||||
|
||||
return _serialId;
|
||||
}
|
||||
|
||||
private sealed class TimerTaskInfo
|
||||
{
|
||||
public TimerTaskInfo(int id, float delay, float interval, int repeatCount, TimerTimeMode timeMode,
|
||||
object owner, Action callback)
|
||||
{
|
||||
Id = id;
|
||||
RemainingTime = delay;
|
||||
Interval = interval;
|
||||
RemainingRepeatCount = repeatCount;
|
||||
TimeMode = timeMode;
|
||||
Owner = owner;
|
||||
Callback = callback;
|
||||
IsCancelled = false;
|
||||
IsPaused = false;
|
||||
}
|
||||
|
||||
public int Id
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public float RemainingTime
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public float Interval
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int RemainingRepeatCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public TimerTimeMode TimeMode
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public object Owner
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public Action Callback
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public bool IsCancelled
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IsPaused
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool HasError
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时任务快照(调试用)。
|
||||
/// </summary>
|
||||
public struct TimerTaskSnapshot
|
||||
{
|
||||
public int Id;
|
||||
public float RemainingTime;
|
||||
public float Interval;
|
||||
public int RemainingRepeatCount;
|
||||
public TimerTimeMode TimeMode;
|
||||
public object Owner;
|
||||
public string CallbackMethod;
|
||||
public bool HasError;
|
||||
public bool IsPaused;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 86583a9107c4c3b45a888f4cc47c2856
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
|
||||
namespace SepCore.Timer
|
||||
{
|
||||
/// <summary>
|
||||
/// 定时任务句柄。
|
||||
/// </summary>
|
||||
public readonly struct TimerHandle : IEquatable<TimerHandle>
|
||||
{
|
||||
public static readonly TimerHandle Invalid = new TimerHandle(0);
|
||||
|
||||
public TimerHandle(int id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取定时任务编号。
|
||||
/// </summary>
|
||||
public int Id
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取句柄是否有效。
|
||||
/// </summary>
|
||||
public bool IsValid => Id > 0;
|
||||
|
||||
public bool Equals(TimerHandle other)
|
||||
{
|
||||
return Id == other.Id;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is TimerHandle other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id;
|
||||
}
|
||||
|
||||
public static bool operator ==(TimerHandle left, TimerHandle right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(TimerHandle left, TimerHandle right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d24d22014412ca54eaee690f92779349
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
|
||||
namespace SepCore.Timer
|
||||
{
|
||||
/// <summary>
|
||||
/// 定时任务描述。
|
||||
/// </summary>
|
||||
public struct TimerTask
|
||||
{
|
||||
public TimerTask(float delay, float interval, int repeatCount, Action callback, object owner = null,
|
||||
TimerTimeMode timeMode = TimerTimeMode.Scaled)
|
||||
{
|
||||
Delay = delay;
|
||||
Interval = interval;
|
||||
RepeatCount = repeatCount;
|
||||
Callback = callback;
|
||||
Owner = owner;
|
||||
TimeMode = timeMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置首次触发前的延迟时间(秒)。
|
||||
/// </summary>
|
||||
public float Delay
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置循环触发间隔(秒)。一次性任务会忽略该值。
|
||||
/// </summary>
|
||||
public float Interval
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置触发次数。1 表示一次性,负数表示无限循环,0 表示不创建任务。
|
||||
/// </summary>
|
||||
public int RepeatCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置任务回调。
|
||||
/// </summary>
|
||||
public Action Callback
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置任务归属对象,用于批量取消。
|
||||
/// </summary>
|
||||
public object Owner
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置任务时间源。
|
||||
/// </summary>
|
||||
public TimerTimeMode TimeMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cd3e0d5a67e7bac488d42f26de4f977e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
namespace SepCore.Timer
|
||||
{
|
||||
/// <summary>
|
||||
/// 定时任务使用的时间源。
|
||||
/// </summary>
|
||||
public enum TimerTimeMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 受 Time.timeScale 影响的游戏时间。
|
||||
/// </summary>
|
||||
Scaled = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 不受 Time.timeScale 影响的真实时间。
|
||||
/// </summary>
|
||||
Unscaled = 1,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3bd4063a62490e74d938f365a2366e1f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# TimerModule 接入说明
|
||||
|
||||
基于 UnityGameFramework 的通用定时器组件插件,支持延迟执行、循环执行、按归属对象批量取消等功能。
|
||||
|
||||
## 导入步骤
|
||||
|
||||
1. 将 `Assets/Plugins/TimerModule/` 整个目录导入目标项目
|
||||
2. 导入后通过 asmref 自动并入 `SepCore.Runtime` 程序集,基座项目仍可正常编译
|
||||
3. 移除 TimerModule 时只需删除整个目录,不会留下残留引用
|
||||
|
||||
## 程序集结构
|
||||
|
||||
```
|
||||
SepCore.Runtime.Timer.asmref → 引用 SepCore.Runtime,使 TimerComponent 与其他基座 Component 平级
|
||||
```
|
||||
|
||||
- 依赖:`UnityGameFramework.Runtime`(框架自带)
|
||||
- 无第三方依赖,纯 C# 实现
|
||||
- 命名空间:`SepCore.Timer`
|
||||
|
||||
## 场景挂载要求
|
||||
|
||||
在 GameFramework 框架的组件挂载对象上(通常是场景中的 `GameEntry` 对象):
|
||||
|
||||
1. 添加 `TimerComponent` 组件
|
||||
2. 组件会通过 `Update()` 自动驱动,无需额外初始化
|
||||
3. AddComponentMenu 路径:`Game Main/Timer`
|
||||
|
||||
## API 使用方式
|
||||
|
||||
```csharp
|
||||
using SepCore.Timer;
|
||||
|
||||
// 获取组件引用
|
||||
var timer = GameEntry.GetComponent<TimerComponent>();
|
||||
|
||||
// 1. 一次性延迟执行
|
||||
timer.ScheduleOnce(2f, () =>
|
||||
{
|
||||
Debug.Log("2秒后执行");
|
||||
});
|
||||
|
||||
// 2. 无限循环(默认)
|
||||
timer.ScheduleRepeat(1f, () =>
|
||||
{
|
||||
Debug.Log("每秒执行一次");
|
||||
});
|
||||
|
||||
// 3. 指定次数循环(首次延迟 = 循环间隔)
|
||||
timer.ScheduleRepeat(0.5f, () =>
|
||||
{
|
||||
Debug.Log("每0.5秒执行,共执行10次");
|
||||
}, repeatCount: 10);
|
||||
|
||||
// 4. 自定义首次延迟 + 循环间隔
|
||||
timer.ScheduleRepeat(delay: 3f, interval: 1f, () =>
|
||||
{
|
||||
Debug.Log("3秒后开始,然后每秒执行一次");
|
||||
});
|
||||
|
||||
// 5. 使用 unscaledTime(不受 Time.timeScale 影响)
|
||||
timer.ScheduleRepeat(1f, () =>
|
||||
{
|
||||
Debug.Log("暂停时也会执行");
|
||||
}, timeMode: TimerTimeMode.Unscaled);
|
||||
|
||||
// 6. 取消单个任务
|
||||
TimerHandle handle = timer.ScheduleOnce(5f, Callback);
|
||||
timer.Cancel(handle); // 提前取消
|
||||
|
||||
// 7. 按归属对象批量取消(适合 MonoBehaviour 销毁时清理)
|
||||
private void OnDestroy()
|
||||
{
|
||||
timer.CancelByOwner(this); // 取消所有归属于当前对象的定时器
|
||||
}
|
||||
|
||||
// 8. 全局暂停/恢复
|
||||
timer.Pause(); // 暂停所有定时器
|
||||
timer.Resume(); // 恢复所有定时器
|
||||
|
||||
// 9. 清理所有
|
||||
timer.ClearAll();
|
||||
```
|
||||
|
||||
## TimerHandle 说明
|
||||
|
||||
- 值类型,可安全作为成员变量存储
|
||||
- `handle.IsValid` - 检查句柄是否有效
|
||||
- `TimerHandle.Invalid` - 无效句柄常量
|
||||
- 实现 `IEquatable<TimerHandle>`,可用于字典键或比较
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `delay` | float | - | 首次触发前的延迟时间(秒) |
|
||||
| `interval` | float | - | 循环触发间隔(秒),一次性任务忽略 |
|
||||
| `repeatCount` | int | -1 | 触发次数:1=一次性,负数=无限循环,0=不创建 |
|
||||
| `owner` | object | null | 任务归属对象,用于 `CancelByOwner` 批量取消 |
|
||||
| `timeMode` | TimerTimeMode | Scaled | 时间源:Scaled 受 timeScale 影响,Unscaled 不受影响 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **MonoBehaviour 中使用时务必设置 owner**,并在 `OnDestroy` 中调用 `CancelByOwner(this)` 避免回调执行在已销毁对象上
|
||||
|
||||
2. **在回调中取消自身是安全的**:回调执行时标记取消,当前帧更新结束后统一清理
|
||||
|
||||
3. **精度说明**:定时器基于帧更新,精度受帧率影响,适合游戏逻辑计时,不适合高精度物理模拟
|
||||
|
||||
4. **推荐使用便捷方法**:`ScheduleOnce` 和 `ScheduleRepeat` 比直接 `Schedule(TimerTask)` 更清晰
|
||||
|
||||
## 编辑器调试功能
|
||||
|
||||
TimerModule 内置了完整的 Editor 调试扩展,选中挂载 `TimerComponent` 的对象即可看到:
|
||||
|
||||
### 统计信息面板
|
||||
- **活跃任务总数** - 当前正在运行的任务数量
|
||||
- **Scaled/Unscaled 分布** - 受时间缩放影响的任务 vs 不受影响的任务
|
||||
- **无限循环数** - `repeatCount < 0` 的永久任务数量
|
||||
- **异常任务数** - 回调曾抛出异常的任务(红色高亮)
|
||||
- **全局运行状态** - 绿色=运行中,橙色=已暂停
|
||||
|
||||
### 任务列表可视化
|
||||
- **ID + 时间模式标签** - 快速识别任务,青色=Scaled,黄色=Unscaled
|
||||
- **进度条** - 显示剩余时间和当前周期完成进度
|
||||
- 一次性任务:显示延迟完成度
|
||||
- 循环任务:显示当前周期进度
|
||||
- **回调方法名** - `Class.Method` 格式,定位来源
|
||||
- **归属对象** - 点击可直接 Ping 到场景中的 Owner 对象
|
||||
- **异常标记** - 曾抛异常的任务会有红色背景警告
|
||||
|
||||
### 调试控制
|
||||
- **暂停/恢复全部** - 一键控制全局定时器
|
||||
- **单任务取消** - 每个任务右侧的取消按钮,可随时终止
|
||||
- **清理全部** - 一键清除所有任务(含确认对话框)
|
||||
|
||||
## 性能特性
|
||||
|
||||
- 回调异常自动捕获并 LogError,不会中断其他任务或游戏
|
||||
- 帧更新时遍历取消采用反向遍历,避免索引错乱
|
||||
- 更新期间的删除操作延迟到帧末执行,避免并发问题
|
||||
- O(n) 线性遍历,适合千级以内任务规模
|
||||
- Editor 调试面板每帧自动刷新,不影响 Build 性能
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 46ef54076114f954aa6d941be3291bf7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue