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

Comment on this article

Mac OSX Lion – Kein StartvolumeSunday, 28 Dec 2014, 20:45

Gerade eben habe ich versucht meinem das neue OSX Lion aufzuspielen. Aber als die Abfrage nach dem Volume kommt, auf das Lion installiert werden soll, kann ich keine meiner partitionen auswählen. Als Begründung steht nur: Sie können dieses Volume nicht als Startvolume für ihren Computer verwenden.

Nach längerer Suche bin ich auf die Lösung gestoßen: Man muss einfach, z.B. mit dem Festplattendienstprogramm, die Gewünschte Partition um ca. 10GB verkleinern, damit Lion die Recovery Partition anlegen kann.

Comment on this article

Android HttpClient – Download einer Datei mit NotificationSunday, 28 Dec 2014, 20:31

Eine Sache für die ich etwas länger Suchen musste, ist das herunterladen einer Datei mit Überprüfung des Netzwerksstatus. Das sollte möglichst auch nicht auf dem Main Thread ablaufen da das Interface sonst einfriert. Um das ganze im Hintergrund ausführen zu können benötigen wir einen Thread, aber um im Interface darzustellen das der Download fertig ist (z.B. eine Notification anzeigen) muss nach dem Thread wieder Code im Main Thread ausgefürt werden. Am einfachsten erreicht man das mit der AsyncTask Klasse die in der Android Bibliothek zu Verfügung steht.

package org.graetzer;

/**
* Copyright 2011 Simon Grätzer [email protected]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Bundle;

public class TestClass extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout);

        LoadTask task = new LoadTask(this);
        task.execute("http://google.de", "http://example.org");
    }
        // Ich verzichte in dieser Klasse darauf einen kontinuierlichen Fortschritt anzuzeigen, deswegen ist der zweite Parameter Void
        static class LoadTask extends AsyncTask<String, Void, List<File>> {
        private final Context context;
        private final ConnectivityManager cmanager;
        private ProgressDialog spinner;

        public LoadTask (Context ctx) {
            this.context = ctx;
            // Netzwerke abfragen
            this.cmanager = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
        }

        @Override
        protected void onPreExecute() {
                        // Anzeigen dass die Anwendung noch aktiv ist
                        spinner = ProgressDialog.show(context, "Loading...", "Load network data", true, false);
        }

        @Override
        protected List doInBackground(String... params) {
            List result = new ArrayList();
            int i = 0;
            for (String url : params) {
                try {
                    File file = cacheFile(i + "_cached");
                    if (download(url, file))
                        result.add(file);
                    i++;
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            return result;
        }

        /**
         * Entferne die Aktivitätsanzeige und löse eine Notification aus
         */
        protected void onPostExecute(List result) {
            spinner.dismiss();
            int LOAD_ID = 1;
            if (result.size() > 0) {
                for (File file : result) {
                    String ns = Context.NOTIFICATION_SERVICE;
                    NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);
                    int icon = R.drawable.appicon;
                    CharSequence tickerText = "Finished download";
                    long when = System.currentTimeMillis();
                    Context context = getApplicationContext();
                    CharSequence contentTitle = "The Download was Finished ";
                    CharSequence contentText = "Downloaded file was "+ file.getName();
                    Intent notificationIntent = new Intent(context, TestClass.class);
                    PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);

                    Notification notification = new Notification(icon, tickerText, when);
                    notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);

                    mNotificationManager.notify(LOAD_ID, notification);
                    LOAD_ID++;
                }
            } else {
                    // Zeige einen Fehler an
                AlertDialog.Builder builder = new Builder(context).setMessage("Error while downloading");
                AlertDialog alert = builder.create();
                alert.show();
            }

        }

        private File cacheFile(String name) throws IOException {
            return new File(context.getCacheDir(), name);
        }

        private boolean download(String url, File file) {
            NetworkInfo info = null;
            if (cmanager != null)
                info = cmanager.getActiveNetworkInfo();

            if (info != null && info.isAvailable()) {
                try {
                    HttpClient client = new DefaultHttpClient();
                    HttpGet get = new HttpGet(url);
                    HttpEntity entity = client.execute(get).getEntity();
                    if (entity != null) {
                        // Create the file or overwrite
                        OutputStream out = new BufferedOutputStream(
                                new FileOutputStream(file));
                        entity.writeTo(out);
                        entity.consumeContent();
                        out.close();
                    }
                    return true;
                } catch (FileNotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
                  return false;
        }
    }
}

Comment on this article

« Back