Skip to content

Commit

Permalink
implemented time-scale feature for clock
Browse files Browse the repository at this point in the history
  • Loading branch information
AndyGura committed Apr 14, 2024
1 parent e54a5d8 commit e5839c4
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 4 deletions.
35 changes: 31 additions & 4 deletions packages/core/src/base/clock/pausable-clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,47 @@ export class PausableClock implements IClock {
return this.startedAt === -1;
}

public get elapsedTime(): number {
if (this.isPaused) {
return this.pausedAt - this.startedAt;
public get timeScale(): number {
return this._timeScale;
}

public set timeScale(value: number) {
if (value === this._timeScale && !(this.pausedByTimescale && value !== 0)) return;
if (value === 0) {
if (!this.isPaused) {
this.pause();
this.pausedByTimescale = true;
}
return;
}
if (this.isPaused && this.pausedByTimescale) {
this.resume();
this.pausedByTimescale = false;
}
if (!this.isStopped) {
const cur = this.isPaused ? this.pausedAt : this.parentClock.elapsedTime;
this.startedAt = cur - ((cur - this.startedAt) * this.timeScale) / value;
}
this._timeScale = value;
}

public get elapsedTime(): number {
if (this.isStopped) {
return this.lastStopElapsed;
}
return this.parentClock.elapsedTime - this.startedAt;
if (this.isPaused) {
return this._timeScale * (this.pausedAt - this.startedAt);
}
return this._timeScale * (this.parentClock.elapsedTime - this.startedAt);
}

// state
private startedAt: number = -1;
private oldRelativeTime: number = 0; // "elapsed", emitted on last tick
private pausedAt: number = -1;
private lastStopElapsed: number = 0;
private _timeScale: number = 1;
private pausedByTimescale: boolean = false;

constructor(autoStart: boolean = false, protected readonly parentClock: IClock = GgGlobalClock.instance) {
if (autoStart) {
Expand Down Expand Up @@ -69,6 +95,7 @@ export class PausableClock implements IClock {
pause() {
this.stopListeningTicks();
this.pausedAt = this.parentClock.elapsedTime;
this.pausedByTimescale = false;
}

resume() {
Expand Down
169 changes: 169 additions & 0 deletions packages/core/test/base/clock/pausable-clock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,173 @@ describe('PausableClock', () => {
expect(c.elapsedTime).toBe(1500);
});
});

describe('time scale', () => {
it('should scale time', () => {
const c = new PausableClock(true);
c.timeScale = 2;
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(3000);
});

it('should never change elapsed time during time scale', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(5000);
expect(c.elapsedTime).toBe(5000);

const runAssertions = (n: number) => {
expect(c.elapsedTime).toBe(n);
c.timeScale = 2;
expect(c.elapsedTime).toBe(n);
c.timeScale = 0;
expect(c.elapsedTime).toBe(n);
c.timeScale = 5;
expect(c.elapsedTime).toBe(n);
c.timeScale = -2;
expect(c.elapsedTime).toBe(n);
c.timeScale = 0;
expect(c.elapsedTime).toBe(n);
c.timeScale = 1;
expect(c.elapsedTime).toBe(n);
};

// running clock changes
runAssertions(5000);
// paused clock changes
c.pause();
runAssertions(5000);
jest.advanceTimersByTime(1000);
runAssertions(5000);
// resumed clock changes
c.resume();
runAssertions(5000);
jest.advanceTimersByTime(1000);
runAssertions(6000);
// stopped clock changes
c.stop();
runAssertions(6000);
});

it('should scale time after clock was paused beforehand', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(500);
c.pause();
jest.advanceTimersByTime(500);
c.resume();

expect(c.elapsedTime).toBe(500);
c.timeScale = 2;
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(3500);
});

it('should handle pause/resume correctly with scaled time', () => {
const c = new PausableClock(true);
c.timeScale = 3;

jest.advanceTimersByTime(1000);
expect(c.elapsedTime).toBe(3000);

c.pause();
jest.advanceTimersByTime(1000);
expect(c.elapsedTime).toBe(3000);

c.resume();
expect(c.elapsedTime).toBe(3000);
jest.advanceTimersByTime(1000);
expect(c.elapsedTime).toBe(6000);

c.pause();
jest.advanceTimersByTime(1000);
expect(c.elapsedTime).toBe(6000);

c.resume();
expect(c.elapsedTime).toBe(6000);
jest.advanceTimersByTime(1000);
expect(c.elapsedTime).toBe(9000);
});

it('should scale time at the time timeScale was updated', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.timeScale = 2;
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(4500);
});

it('should not update past time', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.timeScale = 2;
expect(c.elapsedTime).toBe(1500);
});

it('should work with pause feature', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.pause();
c.timeScale = 2;
jest.advanceTimersByTime(500);
expect(c.elapsedTime).toBe(1500);
c.resume();
jest.advanceTimersByTime(500);
expect(c.elapsedTime).toBe(2500);
});

it('should not update last stopped time', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.stop();
c.timeScale = 2;
expect(c.elapsedTime).toBe(1500);
});

it('should be able to set negative time scale', () => {
const c = new PausableClock(true);

jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.timeScale = -0.5;
expect(c.elapsedTime).toBe(1500);

jest.advanceTimersByTime(1000);
expect(c.elapsedTime).toBe(1000);
c.timeScale = 1;
expect(c.elapsedTime).toBe(1000);

jest.advanceTimersByTime(500);
expect(c.elapsedTime).toBe(1500);
});

it('time scale 0 should pause clock', () => {
const c = new PausableClock(true);
jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.timeScale = 0;
expect(c.isPaused).toBeTruthy();
});

it('should unpause clock after changing time scale from 0', () => {
const c = new PausableClock(true);

jest.advanceTimersByTime(1500);
c.timeScale = 0;

jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(1500);
c.timeScale = 1;
expect(c.isPaused).toBeFalsy();
expect(c.elapsedTime).toBe(1500);

jest.advanceTimersByTime(1500);
expect(c.elapsedTime).toBe(3000);
});
});

// TODO add hierarchy tests, including timeScale

});

0 comments on commit e5839c4

Please sign in to comment.