fix(core): IntervalSystem时间累加bug修复 & 添加Core.paused文档
- 修复 onCheckProcessing() 在 update/lateUpdate 被调用两次导致时间累加翻倍的问题 - 添加 Core.paused 游戏暂停文档(中/英文) - 新增 IntervalSystem 相关测试用例
This commit is contained in:
402
docs/en/guide/time-and-timers.md
Normal file
402
docs/en/guide/time-and-timers.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# Time and Timer System
|
||||||
|
|
||||||
|
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
|
||||||
|
|
||||||
|
## Time Class
|
||||||
|
|
||||||
|
The Time class is the core of the framework's time management, providing all game time-related functionality.
|
||||||
|
|
||||||
|
### Basic Time Properties
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Time } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class GameSystem extends EntitySystem {
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Get frame time (seconds)
|
||||||
|
const deltaTime = Time.deltaTime;
|
||||||
|
|
||||||
|
// Get unscaled frame time
|
||||||
|
const unscaledDelta = Time.unscaledDeltaTime;
|
||||||
|
|
||||||
|
// Get total game time
|
||||||
|
const totalTime = Time.totalTime;
|
||||||
|
|
||||||
|
// Get current frame count
|
||||||
|
const frameCount = Time.frameCount;
|
||||||
|
|
||||||
|
console.log(`Frame ${frameCount}, delta: ${deltaTime}s, total: ${totalTime}s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Game Pause
|
||||||
|
|
||||||
|
The framework provides two pause methods for different scenarios:
|
||||||
|
|
||||||
|
#### Core.paused (Recommended)
|
||||||
|
|
||||||
|
`Core.paused` is a **true pause** - when set, the entire game loop stops:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class PauseMenuSystem extends EntitySystem {
|
||||||
|
public pauseGame(): void {
|
||||||
|
// True pause - all systems stop executing
|
||||||
|
Core.paused = true;
|
||||||
|
console.log('Game paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
public resumeGame(): void {
|
||||||
|
// Resume game
|
||||||
|
Core.paused = false;
|
||||||
|
console.log('Game resumed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public togglePause(): void {
|
||||||
|
Core.paused = !Core.paused;
|
||||||
|
console.log(Core.paused ? 'Game paused' : 'Game resumed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Time.timeScale = 0
|
||||||
|
|
||||||
|
`Time.timeScale = 0` only makes `deltaTime` become 0, **systems still execute**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class SlowMotionSystem extends EntitySystem {
|
||||||
|
public freezeTime(): void {
|
||||||
|
// Time freeze - systems still execute, just deltaTime = 0
|
||||||
|
Time.timeScale = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Comparison
|
||||||
|
|
||||||
|
| Feature | `Core.paused = true` | `Time.timeScale = 0` |
|
||||||
|
|---------|---------------------|---------------------|
|
||||||
|
| System Execution | Completely stopped | Still running |
|
||||||
|
| CPU Overhead | Zero | Normal overhead |
|
||||||
|
| Time Updates | Stopped | Continues (deltaTime=0) |
|
||||||
|
| Timers | Stopped | Continues (but time doesn't advance) |
|
||||||
|
| Use Cases | Pause menu, game pause | Slow motion, bullet time effects |
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
- Pause menu, true game pause → Use `Core.paused = true`
|
||||||
|
- Slow motion, bullet time effects → Use `Time.timeScale`
|
||||||
|
|
||||||
|
### Time Scaling
|
||||||
|
|
||||||
|
The Time class supports time scaling for slow motion, fast forward, and other effects:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class TimeControlSystem extends EntitySystem {
|
||||||
|
public enableSlowMotion(): void {
|
||||||
|
// Set to slow motion (50% speed)
|
||||||
|
Time.timeScale = 0.5;
|
||||||
|
console.log('Slow motion enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
public enableFastForward(): void {
|
||||||
|
// Set to fast forward (200% speed)
|
||||||
|
Time.timeScale = 2.0;
|
||||||
|
console.log('Fast forward enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
public enableBulletTime(): void {
|
||||||
|
// Bullet time effect (10% speed)
|
||||||
|
Time.timeScale = 0.1;
|
||||||
|
console.log('Bullet time enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
public resumeNormalSpeed(): void {
|
||||||
|
// Resume normal speed
|
||||||
|
Time.timeScale = 1.0;
|
||||||
|
console.log('Normal speed resumed');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// deltaTime is affected by timeScale
|
||||||
|
const scaledDelta = Time.deltaTime; // Affected by time scale
|
||||||
|
const realDelta = Time.unscaledDeltaTime; // Not affected by time scale
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
const movement = entity.getComponent(Movement);
|
||||||
|
if (movement) {
|
||||||
|
// Use scaled time for game logic updates
|
||||||
|
movement.update(scaledDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ui = entity.getComponent(UIComponent);
|
||||||
|
if (ui) {
|
||||||
|
// UI animations use real time, not affected by game time scale
|
||||||
|
ui.update(realDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Time Check Utilities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class CooldownSystem extends EntitySystem {
|
||||||
|
private lastAttackTime = 0;
|
||||||
|
private lastSpawnTime = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(Weapon));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Check attack cooldown
|
||||||
|
if (Time.checkEvery(1.5, this.lastAttackTime)) {
|
||||||
|
this.performAttack();
|
||||||
|
this.lastAttackTime = Time.totalTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check spawn interval
|
||||||
|
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
|
||||||
|
this.spawnEnemy();
|
||||||
|
this.lastSpawnTime = Time.totalTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performAttack(): void {
|
||||||
|
console.log('Performing attack!');
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnEnemy(): void {
|
||||||
|
console.log('Spawning enemy!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core.schedule Timer System
|
||||||
|
|
||||||
|
Core provides powerful timer scheduling functionality for creating one-time or repeating timers.
|
||||||
|
|
||||||
|
### Basic Timer Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class GameScene extends Scene {
|
||||||
|
protected initialize(): void {
|
||||||
|
// Create one-time timers
|
||||||
|
this.createOneTimeTimers();
|
||||||
|
|
||||||
|
// Create repeating timers
|
||||||
|
this.createRepeatingTimers();
|
||||||
|
|
||||||
|
// Create timers with context
|
||||||
|
this.createContextTimers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createOneTimeTimers(): void {
|
||||||
|
// Execute once after 2 seconds
|
||||||
|
Core.schedule(2.0, false, null, (timer) => {
|
||||||
|
console.log('Executed after 2 second delay');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show tip after 5 seconds
|
||||||
|
Core.schedule(5.0, false, this, (timer) => {
|
||||||
|
const scene = timer.getContext<GameScene>();
|
||||||
|
scene.showTip('Game tip: 5 seconds have passed!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRepeatingTimers(): void {
|
||||||
|
// Execute every second
|
||||||
|
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
|
||||||
|
console.log(`Game heartbeat - Total time: ${Time.totalTime.toFixed(1)}s`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save timer reference for later control
|
||||||
|
this.saveTimerReference(heartbeatTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContextTimers(): void {
|
||||||
|
const gameData = { score: 0, level: 1 };
|
||||||
|
|
||||||
|
// Add score every 2 seconds
|
||||||
|
Core.schedule(2.0, true, gameData, (timer) => {
|
||||||
|
const data = timer.getContext<typeof gameData>();
|
||||||
|
data.score += 10;
|
||||||
|
console.log(`Score increased! Current score: ${data.score}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveTimerReference(timer: any): void {
|
||||||
|
// Can stop timer later
|
||||||
|
setTimeout(() => {
|
||||||
|
timer.stop();
|
||||||
|
console.log('Timer stopped');
|
||||||
|
}, 10000); // Stop after 10 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
private showTip(message: string): void {
|
||||||
|
console.log('Tip:', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timer Control
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class TimerControlExample {
|
||||||
|
private attackTimer: any;
|
||||||
|
private spawnerTimer: any;
|
||||||
|
|
||||||
|
public startCombat(): void {
|
||||||
|
// Start attack timer
|
||||||
|
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
|
||||||
|
const self = timer.getContext<TimerControlExample>();
|
||||||
|
self.performAttack();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start enemy spawn timer
|
||||||
|
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
|
||||||
|
this.spawnEnemy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopCombat(): void {
|
||||||
|
// Stop all combat-related timers
|
||||||
|
if (this.attackTimer) {
|
||||||
|
this.attackTimer.stop();
|
||||||
|
console.log('Attack timer stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.spawnerTimer) {
|
||||||
|
this.spawnerTimer.stop();
|
||||||
|
console.log('Spawn timer stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetAttackTimer(): void {
|
||||||
|
// Reset attack timer
|
||||||
|
if (this.attackTimer) {
|
||||||
|
this.attackTimer.reset();
|
||||||
|
console.log('Attack timer reset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private performAttack(): void {
|
||||||
|
console.log('Performing attack');
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnEnemy(): void {
|
||||||
|
console.log('Spawning enemy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Appropriate Time Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MovementSystem extends EntitySystem {
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
const movement = entity.getComponent(Movement);
|
||||||
|
|
||||||
|
// Use scaled time for game logic
|
||||||
|
movement.position.x += movement.velocity.x * Time.deltaTime;
|
||||||
|
|
||||||
|
// Use real time for UI animations (not affected by game pause)
|
||||||
|
const ui = entity.getComponent(UIAnimation);
|
||||||
|
if (ui) {
|
||||||
|
ui.update(Time.unscaledDeltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Timer Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class TimerManager {
|
||||||
|
private timers: any[] = [];
|
||||||
|
|
||||||
|
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
|
||||||
|
const timer = Core.schedule(duration, repeats, null, callback);
|
||||||
|
this.timers.push(timer);
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopAllTimers(): void {
|
||||||
|
for (const timer of this.timers) {
|
||||||
|
timer.stop();
|
||||||
|
}
|
||||||
|
this.timers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanupCompletedTimers(): void {
|
||||||
|
this.timers = this.timers.filter(timer => !timer.isDone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Avoid Too Many Timers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Avoid: Creating a timer for each entity
|
||||||
|
class BadExample extends EntitySystem {
|
||||||
|
protected onAdded(entity: Entity): void {
|
||||||
|
Core.schedule(1.0, true, entity, (timer) => {
|
||||||
|
// One timer per entity - poor performance
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recommended: Manage time uniformly in the system
|
||||||
|
class GoodExample extends EntitySystem {
|
||||||
|
private lastUpdateTime = 0;
|
||||||
|
|
||||||
|
protected process(entities: readonly Entity[]): void {
|
||||||
|
// Execute logic once per second
|
||||||
|
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
|
||||||
|
this.processAllEntities(entities);
|
||||||
|
this.lastUpdateTime = Time.totalTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private processAllEntities(entities: readonly Entity[]): void {
|
||||||
|
// Batch process all entities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Timer Context Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TimerContext {
|
||||||
|
entityId: number;
|
||||||
|
duration: number;
|
||||||
|
onComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContextualTimerExample {
|
||||||
|
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
|
||||||
|
const context: TimerContext = {
|
||||||
|
entityId,
|
||||||
|
duration,
|
||||||
|
onComplete
|
||||||
|
};
|
||||||
|
|
||||||
|
Core.schedule(duration, false, context, (timer) => {
|
||||||
|
const ctx = timer.getContext<TimerContext>();
|
||||||
|
console.log(`Timer for entity ${ctx.entityId} completed`);
|
||||||
|
ctx.onComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The time and timer system is an essential tool in game development. Using these features correctly will make your game logic more precise and controllable.
|
||||||
@@ -30,6 +30,64 @@ class GameSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 游戏暂停
|
||||||
|
|
||||||
|
框架提供两种暂停方式,适用于不同场景:
|
||||||
|
|
||||||
|
#### Core.paused(推荐)
|
||||||
|
|
||||||
|
`Core.paused` 是**真正的暂停**,设置后整个游戏循环停止:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
class PauseMenuSystem extends EntitySystem {
|
||||||
|
public pauseGame(): void {
|
||||||
|
// 真正暂停 - 所有系统停止执行
|
||||||
|
Core.paused = true;
|
||||||
|
console.log('游戏已暂停');
|
||||||
|
}
|
||||||
|
|
||||||
|
public resumeGame(): void {
|
||||||
|
// 恢复游戏
|
||||||
|
Core.paused = false;
|
||||||
|
console.log('游戏已恢复');
|
||||||
|
}
|
||||||
|
|
||||||
|
public togglePause(): void {
|
||||||
|
Core.paused = !Core.paused;
|
||||||
|
console.log(Core.paused ? '游戏已暂停' : '游戏已恢复');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Time.timeScale = 0
|
||||||
|
|
||||||
|
`Time.timeScale = 0` 只是让 `deltaTime` 变为 0,**系统仍然在执行**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class SlowMotionSystem extends EntitySystem {
|
||||||
|
public freezeTime(): void {
|
||||||
|
// 时间冻结 - 系统仍在执行,只是 deltaTime = 0
|
||||||
|
Time.timeScale = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 两种方式对比
|
||||||
|
|
||||||
|
| 特性 | `Core.paused = true` | `Time.timeScale = 0` |
|
||||||
|
|------|---------------------|---------------------|
|
||||||
|
| 系统执行 | ❌ 完全停止 | ✅ 仍在执行 |
|
||||||
|
| CPU 开销 | 零 | 正常开销 |
|
||||||
|
| Time 更新 | ❌ 停止 | ✅ 继续(deltaTime=0) |
|
||||||
|
| 定时器 | ❌ 停止 | ✅ 继续(但时间不走) |
|
||||||
|
| 适用场景 | 暂停菜单、游戏暂停 | 慢动作、时间冻结特效 |
|
||||||
|
|
||||||
|
**推荐**:
|
||||||
|
- 暂停菜单、真正的游戏暂停 → 使用 `Core.paused = true`
|
||||||
|
- 慢动作、子弹时间等特效 → 使用 `Time.timeScale`
|
||||||
|
|
||||||
### 时间缩放
|
### 时间缩放
|
||||||
|
|
||||||
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||||
@@ -48,10 +106,10 @@ class TimeControlSystem extends EntitySystem {
|
|||||||
console.log('快进模式启用');
|
console.log('快进模式启用');
|
||||||
}
|
}
|
||||||
|
|
||||||
public pauseGame(): void {
|
public enableBulletTime(): void {
|
||||||
// 暂停游戏(时间静止)
|
// 子弹时间效果(10%速度)
|
||||||
Time.timeScale = 0;
|
Time.timeScale = 0.1;
|
||||||
console.log('游戏暂停');
|
console.log('子弹时间启用');
|
||||||
}
|
}
|
||||||
|
|
||||||
public resumeNormalSpeed(): void {
|
public resumeNormalSpeed(): void {
|
||||||
|
|||||||
@@ -135,6 +135,19 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
*/
|
*/
|
||||||
private _lastProcessEpoch: number = 0;
|
private _lastProcessEpoch: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前帧是否应该处理
|
||||||
|
* Whether this frame should be processed
|
||||||
|
*
|
||||||
|
* 由 update() 中的 onCheckProcessing() 决定,lateUpdate() 复用此结果。
|
||||||
|
* 避免 onCheckProcessing() 被多次调用导致副作用重复执行(如 IntervalSystem 的时间累加)。
|
||||||
|
*
|
||||||
|
* Determined by onCheckProcessing() in update(), reused by lateUpdate().
|
||||||
|
* Prevents onCheckProcessing() being called multiple times causing side effects
|
||||||
|
* to execute repeatedly (e.g., IntervalSystem's time accumulation).
|
||||||
|
*/
|
||||||
|
private _shouldProcessThisFrame: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统处理的实体列表
|
* 获取系统处理的实体列表
|
||||||
*/
|
*/
|
||||||
@@ -774,7 +787,11 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
* 更新系统
|
* 更新系统
|
||||||
*/
|
*/
|
||||||
public update(): void {
|
public update(): void {
|
||||||
if (!this._enabled || !this.onCheckProcessing()) {
|
// 检查是否应该处理,并缓存结果供 lateUpdate 使用
|
||||||
|
// Check if should process and cache result for lateUpdate
|
||||||
|
this._shouldProcessThisFrame = this._enabled && this.onCheckProcessing();
|
||||||
|
|
||||||
|
if (!this._shouldProcessThisFrame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,9 +816,17 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 后期更新系统
|
* 后期更新系统
|
||||||
|
*
|
||||||
|
* lateUpdate 复用 update 中 onCheckProcessing() 的结果,
|
||||||
|
* 避免 IntervalSystem 等子类的副作用被重复执行。
|
||||||
|
*
|
||||||
|
* lateUpdate reuses the onCheckProcessing() result from update,
|
||||||
|
* preventing side effects in subclasses like IntervalSystem from executing repeatedly.
|
||||||
*/
|
*/
|
||||||
public lateUpdate(): void {
|
public lateUpdate(): void {
|
||||||
if (!this._enabled || !this.onCheckProcessing()) {
|
// 复用 update() 中的检查结果,不再调用 onCheckProcessing()
|
||||||
|
// Reuse check result from update(), don't call onCheckProcessing() again
|
||||||
|
if (!this._shouldProcessThisFrame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,32 @@ class ConcreteIntervalSystem extends IntervalSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 用于独立测试的间隔系统(避免 Scene 的类型去重)
|
||||||
|
class IndependentIntervalSystem extends IntervalSystem {
|
||||||
|
public processCallCount = 0;
|
||||||
|
|
||||||
|
constructor(interval: number) {
|
||||||
|
super(interval, Matcher.all(TestComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: Entity[]): void {
|
||||||
|
this.processCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于长时间运行测试的间隔系统
|
||||||
|
class LongRunIntervalSystem extends IntervalSystem {
|
||||||
|
public processCallCount = 0;
|
||||||
|
|
||||||
|
constructor(interval: number) {
|
||||||
|
super(interval, Matcher.all(TestComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: Entity[]): void {
|
||||||
|
this.processCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 具体的处理系统实现
|
// 具体的处理系统实现
|
||||||
class ConcreteProcessingSystem extends ProcessingSystem {
|
class ConcreteProcessingSystem extends ProcessingSystem {
|
||||||
public processSystemCallCount = 0;
|
public processSystemCallCount = 0;
|
||||||
@@ -217,6 +243,98 @@ describe('System Types - 系统类型测试', () => {
|
|||||||
intervalSystem.update();
|
intervalSystem.update();
|
||||||
expect(intervalSystem.processCallCount).toBe(2);
|
expect(intervalSystem.processCallCount).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('update和lateUpdate同时调用时,onCheckProcessing只应执行一次副作用', () => {
|
||||||
|
// 这个测试验证修复:onCheckProcessing() 的副作用(时间累加)不应该在 lateUpdate 中重复执行
|
||||||
|
// 模拟一帧内的完整调用流程
|
||||||
|
|
||||||
|
const initialProcessCount = intervalSystem.processCallCount;
|
||||||
|
|
||||||
|
// 第一帧:时间不足,不应触发
|
||||||
|
Time.update(testInterval / 2);
|
||||||
|
intervalSystem.update();
|
||||||
|
intervalSystem.lateUpdate();
|
||||||
|
expect(intervalSystem.processCallCount).toBe(initialProcessCount);
|
||||||
|
|
||||||
|
// 第二帧:累计时间刚好达到间隔,应该触发一次
|
||||||
|
Time.update(testInterval / 2);
|
||||||
|
intervalSystem.update();
|
||||||
|
intervalSystem.lateUpdate();
|
||||||
|
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
|
||||||
|
|
||||||
|
// 第三帧:需要再等待完整间隔
|
||||||
|
Time.update(testInterval / 2);
|
||||||
|
intervalSystem.update();
|
||||||
|
intervalSystem.lateUpdate();
|
||||||
|
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
|
||||||
|
|
||||||
|
// 第四帧:再次达到间隔
|
||||||
|
Time.update(testInterval / 2);
|
||||||
|
intervalSystem.update();
|
||||||
|
intervalSystem.lateUpdate();
|
||||||
|
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lateUpdate应复用update的检查结果,不重复累加时间', () => {
|
||||||
|
// 这是用户报告的 bug 场景:5秒间隔,但实际触发不规律
|
||||||
|
// 原因是 onCheckProcessing() 在 update 和 lateUpdate 中都被调用,导致时间累加了两次
|
||||||
|
|
||||||
|
// 核心验证:在 N*interval 时间内,触发次数应该接近 N 次
|
||||||
|
// 如果 bug 存在(时间累加两次),触发次数会接近 2N 次
|
||||||
|
|
||||||
|
const initialCount = intervalSystem.processCallCount;
|
||||||
|
|
||||||
|
// 模拟 50 帧,每帧 testInterval/5 (总共 10*testInterval)
|
||||||
|
for (let frame = 0; frame < 50; frame++) {
|
||||||
|
Time.update(testInterval / 5);
|
||||||
|
intervalSystem.update();
|
||||||
|
intervalSystem.lateUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 50帧 * (testInterval/5) = 10*testInterval,应该触发约 10 次
|
||||||
|
// 如果 bug 存在(时间累加两次),会触发约 20 次
|
||||||
|
const triggers = intervalSystem.processCallCount - initialCount;
|
||||||
|
|
||||||
|
// 正常情况:触发 9-11 次
|
||||||
|
// Bug 情况:触发 18-22 次
|
||||||
|
expect(triggers).toBeGreaterThanOrEqual(9);
|
||||||
|
expect(triggers).toBeLessThanOrEqual(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('精确间隔测试 - 验证触发间隔的一致性', () => {
|
||||||
|
// 验证触发间隔是稳定的,而不是忽大忽小
|
||||||
|
|
||||||
|
const initialCount = intervalSystem.processCallCount;
|
||||||
|
const frameTimes: number[] = [];
|
||||||
|
let framesSinceLastTrigger = 0;
|
||||||
|
|
||||||
|
// 模拟 100 帧,每帧 testInterval/5
|
||||||
|
for (let frame = 0; frame < 100; frame++) {
|
||||||
|
Time.update(testInterval / 5);
|
||||||
|
framesSinceLastTrigger++;
|
||||||
|
|
||||||
|
const beforeCount = intervalSystem.processCallCount;
|
||||||
|
intervalSystem.update();
|
||||||
|
intervalSystem.lateUpdate();
|
||||||
|
|
||||||
|
if (intervalSystem.processCallCount > beforeCount) {
|
||||||
|
frameTimes.push(framesSinceLastTrigger);
|
||||||
|
framesSinceLastTrigger = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 100帧 * (testInterval/5) = 20*testInterval,应该触发约 20 次
|
||||||
|
const totalTriggers = intervalSystem.processCallCount - initialCount;
|
||||||
|
expect(totalTriggers).toBeGreaterThanOrEqual(19);
|
||||||
|
expect(totalTriggers).toBeLessThanOrEqual(21);
|
||||||
|
|
||||||
|
// 验证每次触发的帧间隔都接近 5 帧(因为 5 * testInterval/5 = testInterval)
|
||||||
|
// 如果 bug 存在,帧间隔会不稳定(有的 2-3 帧,有的 7-8 帧)
|
||||||
|
for (const frames of frameTimes) {
|
||||||
|
expect(frames).toBeGreaterThanOrEqual(4);
|
||||||
|
expect(frames).toBeLessThanOrEqual(6);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ProcessingSystem - 处理系统', () => {
|
describe('ProcessingSystem - 处理系统', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user