Beginner's Guide: Using the Time Rewind System
This example demonstrates the following effect:
Server-side Code
App.ts
typescript
import { EntityNode } from "@dao3fun/component";
import { TimeRewindSystem } from "./TimeRewindSystem";
import { TimeRewindComponent } from "./TimeRewindComponent";
/**
* Default system configuration
* @property {number} maxRecordTime - Maximum rewind time (in milliseconds).
* @property {number} recordInterval - Time interval for recording state (in milliseconds).
* @property {number} speedFactor - Playback speed factor, used to control playback speed.
*/
const DEFAULT_CONFIG: ISystemConfig = {
maxRecordTime: 7500,
recordInterval: 10,
speedFactor: 1,
};
console.clear();
world.useOBB = true;
// Initialize the system
const sys = initTimeRewindSystem();
// Get and configure entities
const entities = world.querySelectorAll(".time");
for (const entity of entities) {
const node = createTimeRewindNode(entity);
sys.addEntityNode(node);
}
// Start the initial rewind
setTimeout(() => sys.startRewind(), DEFAULT_CONFIG.maxRecordTime);
/**
* System configuration object
*/
interface ISystemConfig {
/** Maximum rewind time (in milliseconds) */
maxRecordTime: number;
/** Time interval for recording state (in milliseconds) */
recordInterval: number;
/** Speed factor */
speedFactor: number;
}
/**
* Initializes the time rewind system
* @param {Partial<ISystemConfig>} config - Optional system configuration parameters
* @returns {TimeRewindSystem} A configured instance of the time rewind system
*/
function initTimeRewindSystem(
config: Partial<ISystemConfig> = {}
): TimeRewindSystem {
const sys = new TimeRewindSystem();
const finalConfig = { ...DEFAULT_CONFIG, ...config };
sys.config = {
maxRecordTime: finalConfig.maxRecordTime,
recordInterval: finalConfig.recordInterval,
speedFactor: finalConfig.speedFactor,
};
sys.onProgress = (progress: number) => {
remoteChannel.broadcastClientEvent({
type: "timeRewindProgress",
progress,
});
};
sys.onEnd = () => {
remoteChannel.broadcastClientEvent({ type: "timeRewindEnd" });
setTimeout(() => sys.startRewind(), finalConfig.maxRecordTime);
};
return sys;
}
/**
* Creates a state handler configuration
* @param {EntityNode} node - The entity node
* @returns A state handler configuration object
*/
function createStateHandlers(node: EntityNode) {
return {
position: {
get: () => node.entity.position,
set: (value: GameVector3) =>
node.entity.position.set(value.x, value.y, value.z),
},
meshOrientation: {
get: () => node.entity.meshOrientation,
set: (value: GameQuaternion) =>
node.entity.meshOrientation.set(value.w, value.x, value.y, value.z),
},
};
}
/**
* Creates a callback function configuration
* @param {EntityNode} node - The entity node
* @returns A callback function configuration object
*/
function createCallbacks(node: EntityNode) {
return {
onStart: () => {
node.entity.fixed = true;
node.entity.gravity = false;
node.entity.collides = false;
},
onEnd: () => {
node.entity.fixed = false;
node.entity.gravity = true;
node.entity.collides = true;
},
};
}
/**
* Creates and configures a time rewind node for an entity
* @param {GameEntity} entity - The game entity
* @returns {EntityNode} The configured entity node
*/
function createTimeRewindNode(entity: GameEntity): EntityNode {
const node = new EntityNode(entity);
node.addComponent(TimeRewindComponent);
const trc = node.getComponent(TimeRewindComponent);
if (trc) {
trc.config = {
stateHandlers: createStateHandlers(node),
callbacks: createCallbacks(node),
};
}
return node;
}
TimeRewindComponent.ts
typescript
import { _decorator, Component } from "@dao3fun/component";
const { apclass } = _decorator;
/**
* Type definition for state getters and setters
*/
export interface IStateHandler<T = any> {
/** Get the state value */
get: () => T;
/** Set the state value */
set: (value: T) => void;
}
/**
* Configuration interface for the time rewind component
* @interface IRewindConfig
*/
export interface IRewindConfig {
/** A map of state handlers, including methods for getting and setting state */
stateHandlers: Record<string, IStateHandler>;
/** Rewind event callbacks */
callbacks?: {
/** Callback function for when rewinding starts */
onStart?: () => void;
/** Callback function for when rewinding ends */
onEnd?: () => void;
/** Callback function for rewind progress updates */
onProgress?: (progress: number) => void;
};
}
/**
* Time Rewind Component
* Used to identify if an entity has rewind functionality and to store rewind-related configuration.
*
* This component follows ECS architecture design principles:
* 1. The component only stores data, not business logic.
* 2. State data is managed centrally by the TimeRewindSystem.
* 3. The component notifies the system of state changes through events.
*
* @class TimeRewindComponent
* @extends {Component<GameEntity>}
*/
@apclass()
export class TimeRewindComponent extends Component<GameEntity> {
/**
* Configuration options for the time rewind component
*/
config: IRewindConfig = {
stateHandlers: {},
callbacks: {},
};
}
TimeRewindSystem.ts
typescript
import { _decorator, NodeSystem } from "@dao3fun/component";
import { TimeRewindComponent } from "./TimeRewindComponent";
/**
* Interface definition for a state snapshot
* Used to store an entity's state data at a specific point in time.
*
* Snapshot design principles:
* 1. Time identification: Each snapshot must have a unique timestamp.
* 2. State integrity: Contains all states of the entity that need to be rewound at that point in time.
* 3. Entity association: Establishes a correspondence with a specific entity through entityId.
* 4. Data independence: Snapshot data is independent of the entity's current state.
*
* @interface IStateSnapshot
* @property {number} timestamp - The timestamp of the snapshot, recording the specific time the state was saved.
* @property {Record<string, any>} states - The state data contained in the snapshot, where the key is the state name and the value is the state value.
* @property {string} entityId - The entity ID, used to identify which entity the state belongs to.
*/
interface IStateSnapshot {
/** The timestamp of the snapshot */
timestamp: number;
/** The state data contained in the snapshot */
states: Record<string, any>;
/** The entity ID */
entityId: string;
}
/**
* Time Rewind System
* Used to centrally manage the rewind functionality of multiple entities, achieving a time-rewind effect in the game.
*
* System design principles:
* 1. Single responsibility: Focuses on managing the recording and playback of entity states.
* 2. Data-driven: Stores and restores entity states through snapshots.
* 3. Component decoupling: Works with TimeRewindComponent to achieve separation of concerns.
* 4. Performance optimization: Uses a Map to store snapshots and supports periodic cleanup of expired data.
* 5. State consistency: Ensures the continuity and accuracy of entity states during the rewind process.
*
* Core functionalities:
* 1. State recording: Periodically saves entity state snapshots.
* 2. State playback: Supports rewinding entity states along a timeline.
* 3. Interpolation calculation: Achieves smooth transitions between states.
* 4. Memory management: Automatically cleans up expired snapshot data.
*
* @extends {NodeSystem}
*/
export class TimeRewindSystem extends NodeSystem {
/** Whether rewinding is in progress */
private isRewinding: boolean = false;
/** Stores state snapshots for all entities, using a Map to improve query efficiency */
private snapshotMap: Map<string, IStateSnapshot[]> = new Map();
/** Timestamp of the last state recording, used to control recording frequency */
private lastRecordTime: number = 0;
/** Rewind start time */
private rewindStartTime: number = 0;
/** Rewind end time */
private rewindEndTime: number = 0;
/**
* Default configuration for the system
* @property {number} maxRecordTime - Maximum rewind time (in milliseconds).
* @property {number} recordInterval - Time interval for recording state (in milliseconds).
* @property {number} speedFactor - Playback speed factor, used to control playback speed.
*/
config = {
maxRecordTime: 6000,
recordInterval: 50,
speedFactor: 1,
};
/** System update callback */
onProgress: (progress: number) => void = () => {};
/** Rewind end callback */
onEnd: () => void = () => {};
/**
* System update function
* Executes recording or playback operations based on the current state.
*
* @param {number} deltaTime - The frame interval time (in milliseconds).
*/
protected update(deltaTime: number): void {
if (this.isRewinding) {
this.rewindEntities();
} else {
this.recordEntities();
}
}
/**
* Records the state of all entities managed by the system
*/
private recordEntities(): void {
const now = Date.now();
if (now - this.lastRecordTime < this.config.recordInterval) {
return;
}
this.lastRecordTime = now;
for (const node of this.entities) {
const component = node.getComponent(TimeRewindComponent);
if (!component) continue;
const states: Record<string, any> = {};
for (const key in component.config.stateHandlers) {
states[key] = component.config.stateHandlers[key].get();
}
const snapshots = this.snapshotMap.get(node.uuid) || [];
snapshots.push({
timestamp: now,
states,
entityId: node.uuid,
});
this.snapshotMap.set(node.uuid, snapshots);
}
this.cleanupOldSnapshots();
}
/**
* Cleans up snapshots that are older than the maximum record time
*/
private cleanupOldSnapshots(): void {
const now = Date.now();
for (const [entityId, snapshots] of this.snapshotMap.entries()) {
const filteredSnapshots = snapshots.filter(
(s) => now - s.timestamp < this.config.maxRecordTime
);
this.snapshotMap.set(entityId, filteredSnapshots);
}
}
/**
* Rewinds the state of all entities managed by the system
*/
private rewindEntities(): void {
const now = Date.now();
const elapsedTime = (now - this.rewindStartTime) * this.config.speedFactor;
const rewindTime = this.rewindEndTime - elapsedTime;
if (rewindTime <= this.rewindEndTime - this.config.maxRecordTime) {
this.stopRewind();
return;
}
for (const node of this.entities) {
this.rewindEntity(node, rewindTime);
}
const progress = (elapsedTime / this.config.maxRecordTime) * 100;
this.onProgress(Math.min(progress, 100));
}
/**
* Rewinds the state of a single entity
* @param {EntityNode} node - The entity node to rewind
* @param {number} rewindTime - The target time to rewind to
*/
private rewindEntity(node: EntityNode, rewindTime: number): void {
const snapshots = this.snapshotMap.get(node.uuid);
if (!snapshots || snapshots.length < 2) return;
const futureIndex = snapshots.findIndex((s) => s.timestamp >= rewindTime);
if (futureIndex <= 0) return;
const past = snapshots[futureIndex - 1];
const future = snapshots[futureIndex];
const t =
(rewindTime - past.timestamp) / (future.timestamp - past.timestamp);
const component = node.getComponent(TimeRewindComponent);
if (!component) return;
for (const key in component.config.stateHandlers) {
const pastState = past.states[key];
const futureState = future.states[key];
const interpolatedState = this.interpolate(pastState, futureState, t);
component.config.stateHandlers[key].set(interpolatedState);
}
}
/**
* Starts the time rewind process
*/
public startRewind(): void {
if (this.isRewinding) return;
this.isRewinding = true;
this.rewindStartTime = Date.now();
this.rewindEndTime = this.rewindStartTime;
for (const node of this.entities) {
const component = node.getComponent(TimeRewindComponent);
component?.config.callbacks?.onStart?.();
}
}
/**
* Stops the time rewind process
*/
public stopRewind(): void {
if (!this.isRewinding) return;
this.isRewinding = false;
for (const node of this.entities) {
const component = node.getComponent(TimeRewindComponent);
component?.config.callbacks?.onEnd?.();
}
this.onEnd();
}
/**
* Interpolates between two states
* @param {any} start - The start state
* @param {any} end - The end state
* @param {number} t - The interpolation factor
* @returns {any} The interpolated state
*/
private interpolate(start: any, end: any, t: number): any {
if (typeof start === "number") {
return start + (end - start) * t;
}
if (typeof start === "object" && start !== null) {
if (typeof start.lerp === "function") {
return start.lerp(end, t);
}
const result: Record<string, any> = {};
for (const key in start) {
result[key] = this.interpolate(start[key], end[key], t);
}
return result;
}
return start;
}
}
Client-side Code
app.js
javascript
const progressBar = new ui.UiBox({
id: "progress-bar",
scale: [0, 0.05],
position: [0.5, 0.1],
pivot: [0.5, 0.5],
color: [0, 1, 0],
});
screen.add(progressBar);
remoteChannel.onClientEvent(({ type, progress }) => {
switch (type) {
case "timeRewindProgress":
progressBar.scale.x = progress / 100;
break;
case "timeRewindEnd":
progressBar.scale.x = 0;
break;
}
});