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 类支持时间缩放功能,可以实现慢动作、快进等效果:
|
||||
@@ -48,10 +106,10 @@ class TimeControlSystem extends EntitySystem {
|
||||
console.log('快进模式启用');
|
||||
}
|
||||
|
||||
public pauseGame(): void {
|
||||
// 暂停游戏(时间静止)
|
||||
Time.timeScale = 0;
|
||||
console.log('游戏暂停');
|
||||
public enableBulletTime(): void {
|
||||
// 子弹时间效果(10%速度)
|
||||
Time.timeScale = 0.1;
|
||||
console.log('子弹时间启用');
|
||||
}
|
||||
|
||||
public resumeNormalSpeed(): void {
|
||||
|
||||
@@ -135,6 +135,19 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
*/
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
if (!this._enabled || !this.onCheckProcessing()) {
|
||||
// 复用 update() 中的检查结果,不再调用 onCheckProcessing()
|
||||
// Reuse check result from update(), don't call onCheckProcessing() again
|
||||
if (!this._shouldProcessThisFrame) {
|
||||
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 {
|
||||
public processSystemCallCount = 0;
|
||||
@@ -207,16 +233,108 @@ describe('System Types - 系统类型测试', () => {
|
||||
Time.update(testInterval);
|
||||
intervalSystem.update();
|
||||
expect(intervalSystem.processCallCount).toBe(1);
|
||||
|
||||
|
||||
// 再次触发需要等待完整间隔
|
||||
Time.update(testInterval / 2);
|
||||
intervalSystem.update();
|
||||
expect(intervalSystem.processCallCount).toBe(1);
|
||||
|
||||
|
||||
Time.update(testInterval / 2);
|
||||
intervalSystem.update();
|
||||
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 - 处理系统', () => {
|
||||
|
||||
Reference in New Issue
Block a user