Echtzeit Rendern mit Javascript und Canvas ElementSunday, 28 Dec 2014, 20:45
Mir ist während der letzten Informatik-Vorlesung an der RWTH die Idee gekommen mich mal etwas genauer mit den Möglichkeiten von HTML 5 auseinanderzusetzen. Dabei habe ich mir vorgenommen, dass recht bekannte Spiel “Achtung die Kurve” mittels Canvas Element zu implementieren und nach ein paar Stunden ist eine kleine Techdemo dabei heraus gekommen. Wie ich dabei vorgegangen bin will ich hier kurz erläutern.
Wenn man so ein einfaches Multiplayer Spiel bauen möchte, dann muss man sich um die Spiellogik und das Rendering kümmern. Dazu benötigt man eine game-loop die in regelmäßigen Abständen die Logik und das Rendering ausführt.
Ein naiver Ansatz dafür wäre etwas in der Art:
function run(){
setInterval(animate}, 1000 / 30);
}
function animate(){
calculateLogic();
render();
}
//...
Das Problem dabei ist das wir uns hier auf eine Framerate von 30fps verlassen, dass kann sich aber schnell ändern wenn der Computer langsam ist, das Intervall also z.B. erst wieder nach 500ms ausgeführt wird. Außerdem könnten andere Javascript Timer die Ausführung verzögern. Man muss also noch die Zeit die seit der letzten Ausführung vergangen ist beachten. So etwa in der Art:
function animate(){
var timeSinceLastFrame = timeAtLastFrame - Date.now();
var distance = speed*timeSinceLastFrame;
timeAtLastFrame = Date.now();
}
Das ist aber auch nur suboptimal, weil man nun in der Gesamten Spiellogik die Zeit beachten muss, außerdem kann es bei manchen Algorithmen (z.B. in der Kollisionsberechnung) von Vorteil sein, wenn man weiß das diese nur alle X ms ausgeführt werden. Außerdem ist die setInterval Funktion in den meisten Browsern inzwischen überflüssig, es existiert die Funktion requestAnimationFrame.
Letztlich habe ich also das hier benutzt:
// Diese Funktion ruft nach einem gewissen Zeitabstand einen callback auf,
// der dann einen Schritt der Animation ausführt
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame
|| window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || function(fn) {setTimeout(fn, 1000/30);};
const frameTime = 1000/30;//30 fps
var lastFrame = new Date().getTime();
var leftover = 0.0;
function animate(timestamp) {
if(this.status != runStatus.running)
return;
// Wir errechnen hier wie oft man die Spiellogik ausführen muss.
var timePassed = timestamp - this.lastFrame + this.leftover;
var catchUpCount = Math.floor(timePassed/frameTime);
// Wir fangen die Ungenauigkeiten durch das Runden ab
this.leftover = timePassed - catchUpCount*frameTime;
this.lastFrame = timestamp;
// Die Spiellogik ausführen
for(var x = 0; x < catchUpCount; x++) {
calculateLogic();
}
render();
requestAnimationFrame(animate);
};
Die Spiellogik wird Zeitlich unabhängig von der genauen Framerate und dem Zeichnen des Spiels, indem wir berechnen wie viel Zeit seit dem letzten Ausführen vergangen ist und sie dann entsprechend oft ausführen. Jetzt muss man nur noch die Spiellogik und das Zeichnen implementieren.
Die etwas gekürzte Logik für den Player:
const w = 30/desiredFramerate;
const q = w*(Math.PI / 20);
Player.prototype.calculateNextFrame = function() {
if (this.movement == move.left)
this.setDirection(this.angle + q);
else if (this.movement == move.right)
this.setDirection(this.angle - q);
this.x += this.deltaX*this.speed*3*w;
this.y += this.deltaY*this.speed*3*w;
this.path.push([this.x, this.y]);
}
Und der Zeichencode für einen Player:
Player.prototype.draw = function(ctx) {
//Draw the path
ctx.beginPath();
ctx.lineWidth = this.radius * 2 + 0.5;
ctx.strokeStyle = this.color;
for(var i = 0; i < this.path.length - 1; i++) {
if(this.path[i] != null) {
var x = this.path[i][0];
var y = this.path[i][1];
ctx.lineTo(x, y);
}
}
ctx.stroke();
ctx.closePath();
// Draw player head
ctx.beginPath();
ctx.fillStyle = "yellow";
ctx.arc(this.x, this.y, this.radius + 0.5, 0, Math.PI * 2, true);
ctx.fill();
ctx.closePath();
};
Wer sich für den genauen Code interessiert, um z.B. zu sehen wie die Keyboard Events funktionieren: achtung.js