unit interpolation

This commit is contained in:
2025-05-10 12:37:37 +02:00
parent a3a48cad40
commit c1f56c69aa
3 changed files with 85 additions and 28 deletions

View File

@@ -29,10 +29,10 @@ export class GameComponent implements OnInit {
const initialState: GameState = { const initialState: GameState = {
tick: 0, tick: 0,
units: [{ id: 'u1', x: 0, y: 0 }], units: [{ id: 'u1', x: 0, y: 0 }],
commandHistory: [] commandHistory: [],
}; };
const json = await fetch('/assets/gameData.json').then(r => r.json()); const json = await fetch('/assets/gameData.json').then((r) => r.json());
this.engine = new SimulationEngine(initialState, GameRules, json); this.engine = new SimulationEngine(initialState, GameRules, json);
requestAnimationFrame(this.gameLoop.bind(this)); requestAnimationFrame(this.gameLoop.bind(this));
} }
@@ -58,7 +58,7 @@ export class GameComponent implements OnInit {
} }
async reloadGameData() { async reloadGameData() {
const json = await fetch('/assets/gameData.json').then(r => r.json()); const json = await fetch('/assets/gameData.json').then((r) => r.json());
this.engine.reinitializeWithNewData(json); this.engine.reinitializeWithNewData(json);
} }
@@ -66,17 +66,55 @@ export class GameComponent implements OnInit {
const cmd: Command = { const cmd: Command = {
step: this.engine.currentStep + 1, step: this.engine.currentStep + 1,
type: 'move', type: 'move',
payload: { id: 'u1', dx: 1, dy: 0 } payload: { id: 'u1', dx: 1, dy: 0 },
}; };
this.engine.issueCommand(cmd); this.engine.issueCommand(cmd);
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.resizeCanvas(); this.resizeCanvas();
requestAnimationFrame(this.render);
window.addEventListener('resize', this.resizeCanvas.bind(this)); window.addEventListener('resize', this.resizeCanvas.bind(this));
} }
render = (now: number) => {
if (!this.engine) return;
const [prevState, currState] = this.engine.getRenderStates();
// Compute interpolation factor (0..1)
const lastStepTime = this.engine.isRunning ? this.engine.lastStepTime : now;
const t = this.engine.isRunning
? Math.max(Math.min((now - lastStepTime) / this.engine.interval, 1), 0)
: 1; // fully render the last known state
this.renderFrame(prevState, currState, t);
requestAnimationFrame(this.render);
};
renderFrame(prev: GameState, curr: GameState, t: number) {
const ctx = this.canvasRef.nativeElement.getContext('2d');
if (!ctx) return;
const canvas = this.canvasRef.nativeElement;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < curr.units.length; i++) {
const currUnit = curr.units[i];
const prevUnit =
prev.units.find((u) => u.id === currUnit.id) ?? currUnit;
const x = prevUnit.x + (currUnit.x - prevUnit.x) * t;
const y = prevUnit.y + (currUnit.y - prevUnit.y) * t;
ctx.fillStyle = 'cyan';
ctx.beginPath();
ctx.arc(x * 10, y * 10, 5, 0, Math.PI * 2);
ctx.fill();
}
}
resizeCanvas(): void { resizeCanvas(): void {
const wrapper = this.wrapperRef.nativeElement; const wrapper = this.wrapperRef.nativeElement;
const canvas = this.canvasRef.nativeElement; const canvas = this.canvasRef.nativeElement;

View File

@@ -4,23 +4,25 @@ import { GameRules } from './gameRules';
import { GameState } from './gameState'; import { GameState } from './gameState';
export class SimulationEngine { export class SimulationEngine {
private interval = 50; // ms per step interval: number = 50; // ms per step
private lastTime = 0; lastTime = 0;
private frameRequestId: number | null = null; private frameRequestId: number | null = null;
private data: GameData; private data: GameData;
private previousState: GameState | null = null;
currentStep = 0; currentStep = 0;
initialState: GameState; initialState: GameState;
state: GameState; state: GameState;
isRunning = false; isRunning = false;
lastStepTime: number = 0;
constructor( constructor(
initialState: GameState, initialState: GameState,
private rules: typeof GameRules, private rules: typeof GameRules,
initialData: GameData initialData: GameData
) { ) {
this.initialState = JSON.parse(JSON.stringify(initialState)); this.initialState = initialState;
this.state = JSON.parse(JSON.stringify(initialState)); this.state = this.cloneState(this.initialState);
this.data = initialData; this.data = initialData;
} }
@@ -40,7 +42,9 @@ export class SimulationEngine {
} }
fastForwardToEnd() { fastForwardToEnd() {
this.fastForwardTo(this.state.commandHistory[this.state.commandHistory.length - 1].step); this.fastForwardTo(
this.state.commandHistory[this.state.commandHistory.length - 1].step
);
} }
private fastForwardTo(target: number) { private fastForwardTo(target: number) {
@@ -51,8 +55,10 @@ export class SimulationEngine {
} }
step() { step() {
this.state = this.rules.step(this.state, this.data); this.previousState = this.cloneState(this.state);
this.currentStep++; this.rules.step(this.state, this.data);
this.currentStep = this.state.tick;
this.lastStepTime = performance.now();
} }
start() { start() {
@@ -72,7 +78,8 @@ export class SimulationEngine {
rewindToZero() { rewindToZero() {
this.stop(); this.stop();
this.state = JSON.parse(JSON.stringify(this.initialState)); this.state = this.cloneState(this.initialState);
this.previousState = this.cloneState(this.state);
this.currentStep = 0; this.currentStep = 0;
} }
@@ -84,6 +91,18 @@ export class SimulationEngine {
this.state.commandHistory.push(command); this.state.commandHistory.push(command);
} }
getRenderStates(): [GameState, GameState] {
return [this.previousState ?? this.state, this.state];
}
cloneState(state: GameState): GameState {
return {
tick: state.tick,
commandHistory: [...state.commandHistory],
units: state.units.map((u) => ({ ...u })),
};
}
private loop = () => { private loop = () => {
const now = performance.now(); const now = performance.now();
while (now - this.lastTime >= this.interval) { while (now - this.lastTime >= this.interval) {

View File

@@ -437,6 +437,6 @@
{ {
"path": "manifest.json", "path": "manifest.json",
"type": "other", "type": "other",
"size": 7721 "size": 7722
} }
] ]