480 lines
11 KiB
Markdown
480 lines
11 KiB
Markdown
|
|
# 定时器系统 (Timer)
|
|||
|
|
|
|||
|
|
`@esengine/timer` 提供了一个灵活的定时器和冷却系统,用于游戏中的延迟执行、重复任务、技能冷却等场景。
|
|||
|
|
|
|||
|
|
## 安装
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm install @esengine/timer
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 快速开始
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createTimerService } from '@esengine/timer';
|
|||
|
|
|
|||
|
|
// 创建定时器服务
|
|||
|
|
const timerService = createTimerService();
|
|||
|
|
|
|||
|
|
// 一次性定时器(1秒后执行)
|
|||
|
|
const handle = timerService.schedule('myTimer', 1000, () => {
|
|||
|
|
console.log('Timer fired!');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 重复定时器(每100毫秒执行)
|
|||
|
|
timerService.scheduleRepeating('heartbeat', 100, () => {
|
|||
|
|
console.log('Tick');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 冷却系统(5秒冷却)
|
|||
|
|
timerService.startCooldown('skill_fireball', 5000);
|
|||
|
|
|
|||
|
|
if (timerService.isCooldownReady('skill_fireball')) {
|
|||
|
|
// 可以使用技能
|
|||
|
|
useFireball();
|
|||
|
|
timerService.startCooldown('skill_fireball', 5000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 游戏循环中更新
|
|||
|
|
function gameLoop(deltaTime: number) {
|
|||
|
|
timerService.update(deltaTime);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 核心概念
|
|||
|
|
|
|||
|
|
### 定时器 vs 冷却
|
|||
|
|
|
|||
|
|
| 特性 | 定时器 (Timer) | 冷却 (Cooldown) |
|
|||
|
|
|------|---------------|-----------------|
|
|||
|
|
| 用途 | 延迟执行代码 | 限制操作频率 |
|
|||
|
|
| 回调 | 有回调函数 | 无回调函数 |
|
|||
|
|
| 重复 | 支持重复执行 | 一次性 |
|
|||
|
|
| 查询 | 查询剩余时间 | 查询进度/是否就绪 |
|
|||
|
|
|
|||
|
|
### TimerHandle
|
|||
|
|
|
|||
|
|
调度定时器后返回的句柄对象,用于控制定时器:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface TimerHandle {
|
|||
|
|
readonly id: string; // 定时器 ID
|
|||
|
|
readonly isValid: boolean; // 是否有效(未被取消)
|
|||
|
|
cancel(): void; // 取消定时器
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### TimerInfo
|
|||
|
|
|
|||
|
|
定时器信息对象:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface TimerInfo {
|
|||
|
|
readonly id: string; // 定时器 ID
|
|||
|
|
readonly remaining: number; // 剩余时间(毫秒)
|
|||
|
|
readonly repeating: boolean; // 是否重复执行
|
|||
|
|
readonly interval?: number; // 间隔时间(仅重复定时器)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### CooldownInfo
|
|||
|
|
|
|||
|
|
冷却信息对象:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface CooldownInfo {
|
|||
|
|
readonly id: string; // 冷却 ID
|
|||
|
|
readonly duration: number; // 总持续时间(毫秒)
|
|||
|
|
readonly remaining: number; // 剩余时间(毫秒)
|
|||
|
|
readonly progress: number; // 进度(0-1,0=刚开始,1=结束)
|
|||
|
|
readonly isReady: boolean; // 是否已就绪
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## API 参考
|
|||
|
|
|
|||
|
|
### createTimerService
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function createTimerService(config?: TimerServiceConfig): ITimerService
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**配置选项:**
|
|||
|
|
|
|||
|
|
| 属性 | 类型 | 默认值 | 描述 |
|
|||
|
|
|------|------|--------|------|
|
|||
|
|
| `maxTimers` | `number` | `0` | 最大定时器数量(0 表示无限制) |
|
|||
|
|
| `maxCooldowns` | `number` | `0` | 最大冷却数量(0 表示无限制) |
|
|||
|
|
|
|||
|
|
### 定时器 API
|
|||
|
|
|
|||
|
|
#### schedule
|
|||
|
|
|
|||
|
|
调度一次性定时器:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const handle = timerService.schedule('explosion', 2000, () => {
|
|||
|
|
createExplosion();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 提前取消
|
|||
|
|
handle.cancel();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### scheduleRepeating
|
|||
|
|
|
|||
|
|
调度重复定时器:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 每秒执行
|
|||
|
|
timerService.scheduleRepeating('regen', 1000, () => {
|
|||
|
|
player.hp += 5;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 立即执行一次,然后每秒重复
|
|||
|
|
timerService.scheduleRepeating('tick', 1000, () => {
|
|||
|
|
console.log('Tick');
|
|||
|
|
}, true); // immediate = true
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### cancel / cancelById
|
|||
|
|
|
|||
|
|
取消定时器:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 通过句柄取消
|
|||
|
|
handle.cancel();
|
|||
|
|
// 或
|
|||
|
|
timerService.cancel(handle);
|
|||
|
|
|
|||
|
|
// 通过 ID 取消
|
|||
|
|
timerService.cancelById('regen');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### hasTimer
|
|||
|
|
|
|||
|
|
检查定时器是否存在:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
if (timerService.hasTimer('explosion')) {
|
|||
|
|
console.log('Explosion is pending');
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### getTimerInfo
|
|||
|
|
|
|||
|
|
获取定时器信息:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const info = timerService.getTimerInfo('explosion');
|
|||
|
|
if (info) {
|
|||
|
|
console.log(`剩余时间: ${info.remaining}ms`);
|
|||
|
|
console.log(`是否重复: ${info.repeating}`);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 冷却 API
|
|||
|
|
|
|||
|
|
#### startCooldown
|
|||
|
|
|
|||
|
|
开始冷却:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 5秒冷却
|
|||
|
|
timerService.startCooldown('skill_fireball', 5000);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### isCooldownReady / isOnCooldown
|
|||
|
|
|
|||
|
|
检查冷却状态:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
if (timerService.isCooldownReady('skill_fireball')) {
|
|||
|
|
// 可以使用技能
|
|||
|
|
castFireball();
|
|||
|
|
timerService.startCooldown('skill_fireball', 5000);
|
|||
|
|
} else {
|
|||
|
|
console.log('技能还在冷却中');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 或使用 isOnCooldown
|
|||
|
|
if (timerService.isOnCooldown('skill_fireball')) {
|
|||
|
|
console.log('冷却中...');
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### getCooldownProgress / getCooldownRemaining
|
|||
|
|
|
|||
|
|
获取冷却进度:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 进度 0-1(0=刚开始,1=完成)
|
|||
|
|
const progress = timerService.getCooldownProgress('skill_fireball');
|
|||
|
|
console.log(`冷却进度: ${(progress * 100).toFixed(0)}%`);
|
|||
|
|
|
|||
|
|
// 剩余时间(毫秒)
|
|||
|
|
const remaining = timerService.getCooldownRemaining('skill_fireball');
|
|||
|
|
console.log(`剩余时间: ${(remaining / 1000).toFixed(1)}s`);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### getCooldownInfo
|
|||
|
|
|
|||
|
|
获取完整冷却信息:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const info = timerService.getCooldownInfo('skill_fireball');
|
|||
|
|
if (info) {
|
|||
|
|
console.log(`总时长: ${info.duration}ms`);
|
|||
|
|
console.log(`剩余: ${info.remaining}ms`);
|
|||
|
|
console.log(`进度: ${info.progress}`);
|
|||
|
|
console.log(`就绪: ${info.isReady}`);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### resetCooldown / clearAllCooldowns
|
|||
|
|
|
|||
|
|
重置冷却:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 重置单个冷却
|
|||
|
|
timerService.resetCooldown('skill_fireball');
|
|||
|
|
|
|||
|
|
// 清除所有冷却(例如角色复活时)
|
|||
|
|
timerService.clearAllCooldowns();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 生命周期
|
|||
|
|
|
|||
|
|
#### update
|
|||
|
|
|
|||
|
|
更新定时器服务(需要每帧调用):
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function gameLoop(deltaTime: number) {
|
|||
|
|
// deltaTime 单位是毫秒
|
|||
|
|
timerService.update(deltaTime);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### clear
|
|||
|
|
|
|||
|
|
清除所有定时器和冷却:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
timerService.clear();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 调试属性
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 获取活跃定时器数量
|
|||
|
|
console.log(timerService.activeTimerCount);
|
|||
|
|
|
|||
|
|
// 获取活跃冷却数量
|
|||
|
|
console.log(timerService.activeCooldownCount);
|
|||
|
|
|
|||
|
|
// 获取所有活跃定时器 ID
|
|||
|
|
const timerIds = timerService.getActiveTimerIds();
|
|||
|
|
|
|||
|
|
// 获取所有活跃冷却 ID
|
|||
|
|
const cooldownIds = timerService.getActiveCooldownIds();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 实际示例
|
|||
|
|
|
|||
|
|
### 技能冷却系统
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { createTimerService, type ITimerService } from '@esengine/timer';
|
|||
|
|
|
|||
|
|
class SkillSystem {
|
|||
|
|
private timerService: ITimerService;
|
|||
|
|
private skills: Map<string, SkillData> = new Map();
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
this.timerService = createTimerService();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
registerSkill(id: string, data: SkillData): void {
|
|||
|
|
this.skills.set(id, data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
useSkill(skillId: string): boolean {
|
|||
|
|
const skill = this.skills.get(skillId);
|
|||
|
|
if (!skill) return false;
|
|||
|
|
|
|||
|
|
// 检查冷却
|
|||
|
|
if (!this.timerService.isCooldownReady(skillId)) {
|
|||
|
|
const remaining = this.timerService.getCooldownRemaining(skillId);
|
|||
|
|
console.log(`技能 ${skillId} 冷却中,剩余 ${remaining}ms`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用技能
|
|||
|
|
this.executeSkill(skill);
|
|||
|
|
|
|||
|
|
// 开始冷却
|
|||
|
|
this.timerService.startCooldown(skillId, skill.cooldown);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getSkillCooldownProgress(skillId: string): number {
|
|||
|
|
return this.timerService.getCooldownProgress(skillId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update(dt: number): void {
|
|||
|
|
this.timerService.update(dt);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface SkillData {
|
|||
|
|
cooldown: number;
|
|||
|
|
// ... other properties
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 延迟和定时效果
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
class EffectSystem {
|
|||
|
|
private timerService: ITimerService;
|
|||
|
|
|
|||
|
|
constructor(timerService: ITimerService) {
|
|||
|
|
this.timerService = timerService;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 延迟爆炸
|
|||
|
|
scheduleExplosion(position: { x: number; y: number }, delay: number): void {
|
|||
|
|
this.timerService.schedule(`explosion_${Date.now()}`, delay, () => {
|
|||
|
|
this.createExplosion(position);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DOT 伤害(每秒造成伤害)
|
|||
|
|
applyDOT(target: Entity, damage: number, duration: number): void {
|
|||
|
|
const dotId = `dot_${target.id}_${Date.now()}`;
|
|||
|
|
let elapsed = 0;
|
|||
|
|
|
|||
|
|
this.timerService.scheduleRepeating(dotId, 1000, () => {
|
|||
|
|
elapsed += 1000;
|
|||
|
|
target.takeDamage(damage);
|
|||
|
|
|
|||
|
|
if (elapsed >= duration) {
|
|||
|
|
this.timerService.cancelById(dotId);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BUFF 效果(持续一段时间)
|
|||
|
|
applyBuff(target: Entity, buffId: string, duration: number): void {
|
|||
|
|
target.addBuff(buffId);
|
|||
|
|
|
|||
|
|
this.timerService.schedule(`buff_expire_${buffId}`, duration, () => {
|
|||
|
|
target.removeBuff(buffId);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 与 ECS 集成
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { Component, EntitySystem, Matcher } from '@esengine/ecs-framework';
|
|||
|
|
import { createTimerService, type ITimerService } from '@esengine/timer';
|
|||
|
|
|
|||
|
|
// 定时器组件
|
|||
|
|
class TimerComponent extends Component {
|
|||
|
|
timerService: ITimerService;
|
|||
|
|
|
|||
|
|
constructor() {
|
|||
|
|
super();
|
|||
|
|
this.timerService = createTimerService();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定时器系统
|
|||
|
|
class TimerSystem extends EntitySystem {
|
|||
|
|
constructor() {
|
|||
|
|
super(Matcher.all(TimerComponent));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected processEntity(entity: Entity, dt: number): void {
|
|||
|
|
const timer = entity.getComponent(TimerComponent);
|
|||
|
|
timer.timerService.update(dt);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 冷却组件(用于共享冷却)
|
|||
|
|
class CooldownComponent extends Component {
|
|||
|
|
constructor(public timerService: ITimerService) {
|
|||
|
|
super();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 蓝图节点
|
|||
|
|
|
|||
|
|
Timer 模块提供了可视化脚本支持的蓝图节点:
|
|||
|
|
|
|||
|
|
### 冷却节点
|
|||
|
|
|
|||
|
|
- `StartCooldown` - 开始冷却
|
|||
|
|
- `IsCooldownReady` - 检查冷却是否就绪
|
|||
|
|
- `GetCooldownProgress` - 获取冷却进度
|
|||
|
|
- `GetCooldownInfo` - 获取详细冷却信息
|
|||
|
|
- `ResetCooldown` - 重置冷却
|
|||
|
|
|
|||
|
|
### 定时器节点
|
|||
|
|
|
|||
|
|
- `HasTimer` - 检查定时器是否存在
|
|||
|
|
- `CancelTimer` - 取消定时器
|
|||
|
|
- `GetTimerRemaining` - 获取定时器剩余时间
|
|||
|
|
|
|||
|
|
## 服务令牌
|
|||
|
|
|
|||
|
|
在依赖注入场景中使用:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { TimerServiceToken, createTimerService } from '@esengine/timer';
|
|||
|
|
|
|||
|
|
// 注册服务
|
|||
|
|
services.register(TimerServiceToken, createTimerService());
|
|||
|
|
|
|||
|
|
// 获取服务
|
|||
|
|
const timerService = services.get(TimerServiceToken);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 最佳实践
|
|||
|
|
|
|||
|
|
1. **使用有意义的 ID**:使用描述性的 ID 便于调试和管理
|
|||
|
|
```typescript
|
|||
|
|
// 好
|
|||
|
|
timerService.startCooldown('skill_fireball', 5000);
|
|||
|
|
|
|||
|
|
// 不好
|
|||
|
|
timerService.startCooldown('cd1', 5000);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **避免重复 ID**:相同 ID 的定时器会覆盖之前的
|
|||
|
|
```typescript
|
|||
|
|
// 使用唯一 ID
|
|||
|
|
const uniqueId = `explosion_${entity.id}_${Date.now()}`;
|
|||
|
|
timerService.schedule(uniqueId, 1000, callback);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **及时清理**:在适当时机清理不需要的定时器和冷却
|
|||
|
|
```typescript
|
|||
|
|
// 实体销毁时
|
|||
|
|
onDestroy() {
|
|||
|
|
this.timerService.cancelById(this.timerId);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **配置限制**:在生产环境考虑设置最大数量限制
|
|||
|
|
```typescript
|
|||
|
|
const timerService = createTimerService({
|
|||
|
|
maxTimers: 1000,
|
|||
|
|
maxCooldowns: 500
|
|||
|
|
});
|
|||
|
|
```
|