Wiki

Clone wiki

User Apps / HTML-UI / Canvas-Performance

<canvas> Performance

Mit HTML5 gibt es die Möglichkeit auch Bereiche zum zeichnen zu definieren. In der Regel wird hierfür ein <canvas> verwendet was dann mittels JavaScript angesprochen wird.

Um Animationen für aufwändigere Spiele zu erstellen, muss ein sogenannter draw-cycle entstehen. Übersetzt heißt dies, dass eine Methode kontinuierlich aufgerufen wird die dann Zeichenoperationen durchführt.

Entwickler die vielleicht von Java kommen sollten bereits mit paint(Graphics g) oder gar paintComponent(Graphics g) vertraut sein; Nichts anderes passiert nämlich hier - Wird eine Komponente als "bitte neuzeichnen" vorgemerkt werden, durchläuft der draw-cycle diese Methoden. In Javascript müssen wir diesen Cycle selbst steuern.

Ein typisches Beispiel wäre folgender simpler Code:

function draw() {
    // Hier wird nun etwas in das Canvas gezeichnet
}

setInterval(draw, 100);

die Methode draw() wird hier jede 100 Millisekunden ausgeführt. Natürlich ist dies nun eine asyncrone-loop, egal wie lange die Zeichenoperatoren innerhalb dieser Methode benötigt, sie wird immer exakt jede 100 Millisekunden ausgeführt. Dies kann durchaus zu einem "Schluckauf" kommen, da die rendering-engine des Browsers (oder in dem Falle von JavaFX deren WebView) eventuell bei Leistungsschwächeren Computern nicht mehr mit kommt. Man empfindet dies dann meist als "laggen".

Eine andere Methode wäre hier setTimeout zu verwenden. Hier läge der Vorteil dass der jeweilige draw-call erst ausgeführt wird, bevor ein neuer beginnt. Das heißt dass bei setTimeout der Bildschirm nur aktualisiert wird, wenn es wirklich benötigt wird und nicht wenn der Computer dazu in der Lage ist.

function draw() {
    // Hier wird nun etwas in das Canvas gezeichnet
    setTimeout(draw, 100);
}

draw();

Framerate nutzen!

Eine Framerate (oder auch Wiederholungsrate) gibt an - oftmals kennt ihr dies unter den Begriff "FPS" - wie oft eine Zeichenoperation durchlaufen wird. Übersetzt heißt FPS "Frames per second". Je höher die Framerate liegt, desto weicher läuft eine Animation ab, hat aber auch den Nachteil dass leistungsschwächere Computer diese nicht verarbeiten können.

Die meisten Monitore haben eine Aktualisierungsrate von 60Hz, die schnellste Wiederholungsrate wäre hier dann 60 FPS. Im Vergleich dazu haben Videos eine Framerate zwischen 24-30 FPS.

Egal wie hoch nun die Framerate liegt, es ist sinnvoll einen Mittelpunkt zu finden deren Einklang mit der Animation sowie der Computerleistung liegt; Denn es ist irrelevant ob man nun Animationen mit 60 FPS oder gar mit 120 FPS durchläuft, das menschliche Auge (oder auch Gehirn) kann ohnehin nur eine begrenzte Anzahl an Bilder verarbeiten. Ab 24 FPS beginnt der Mensch ein flüssiges Bild zu sehen.

Was heisst das ganze nun für mein Script? Die einfachste Methode wäre hier mathematisch den Interval des draw-calls zu beeinflussen. Wir wissen das die Einheit von "FPS" Sekunden sind. Wollen wir also eine FPS von 60 haben, brauchen wir nur wenig am Script ändern:

setInterval(draw, 1000 / 60);

All in one

Egal wie komplex dein Spiel sein kann, es ist sehr sinnvoll nur einen Timer für den Animationsablauf zu verwenden. Je mehr Timeouts/Intervalle im Hintergrund laufen desto mehr Rechenleistung wird logischerweise benötigt.

Baue dir also eine Hauptklasse die für den draw-cycle zuständig ist und weise nur den Objekten an dass diese zeichnen sollen. Würdest du nun jedem Objekt einen eigenen Timer zuweisen würde dies nur unnötige Last für den Computer sein.

Hier mal ein simples Beispiel, wie es ablaufen könnte:

<canvas id="MyGame" width="100" height="100"></canvas>

Die Hauptklasse ist für die draw-cycle zuständig und weist den Objekten nur an "du darfst zeichnen":

function MyPainter() {
    var _fps        = 60;
    var _canvas = document.getElementById("MyGame");
    var _context    = _canvas.getContext("2d");
    var _objects    = [
        new MyObject("A"),
        new MyObject("B"),
        new MyObject("C")
    ];

    this.draw = function() {
        var length = _objects.length;

        // Gehe jedes Objekt durch und weise diesen an zu zeichnen....
        for(var index = 0; index < length; ++index) {
            _objects[index].draw(_context);
        }

        // Nächster draw-cycle ausführen
        setTimeout(this.draw, 1000 / _fps);
    };

    this.draw();
};

new MyPainter();

Die Objekte besitzen dann nur einfach die draw Methode, die durch die Hauptklasse stetig aufgerufen wird:

function MyObject(string) {
    var _string = string;

    this.draw = function(context) {
        // Was soll das Objekt denn zeichnen?
        context.drawString(_string, 10, 10);
    };
};

Frame Dropping/Skipping

Hier kommen wir zu einem Thema was insbesondere für leistungsschwächere Computer bestimmt ist. Kommt der Computer beim zeichnen der draw-cycles nicht mit, so zeichnet sich dies oftmals durch "laggen" aus. Hier gibt es aber einen simplen Trick der dies verhindern soll: Wir lassen einfach draw-cycles aus die der Computer ohnehin nicht verarbeiten kann. So verhindern wir dass dieser dich bei einer Wiederholung verschluckt und dann ins stottern gerät.

Die einfachste Variante hierfür ist requestAnimationFrame(function) zu verwenden. Anstelle dass du also setTimeout mit einer Zeitangabe durchlaufen lässt und dich gar hier um das Frame-Skipping selbst kümmern müsstest, kannst du auf einfacher Art und weise die Leistung steigern. Die rendering-engine des Browsers kümmert sich dann von selbst darum, ob er den nächsten draw-cycle zeichnen kann und überspringt diesen möglicherweise.

function draw() {
    requestAnimationFrame(draw);
    // Hier wird nun etwas in das Canvas gezeichnet
}

draw();

Nun fällt dir sicherlich auch auf, dass hier keine Zeitangabe mehr benötigt wird, die Framerate wird nun vollautomatisch vom Browser gesteuert. Ein Hauptaugenmerk ist aber auch dass der Browser die notwendigen draw-cycles bündelt und native nur einmal zeichnet; So garantiert es dir eine sehr gute performance die auch für schwache Computer geeignet ist.

Nachteil an der Geschichte von requestAnimationFrame ist dass es derzeit noch nicht für jeden Webbrowser funktioniert oder gar unter einem anderen Namen existiert (z.B. mozRequestAnimationFrame für Webbrowser mit der Mozilla-Engine). Für dieses Problem gibt es aber PolyFills die (möglw. alte) Browser damit aufrüsten und die Methoden vereinheitlichen.

Updated