26 Jul

Bewegungspfade in Android – Game Development

In den letzten Tagen und Wochen habe ich extrem viele Prototypen für diverse Spielideen geschrieben um daran Machbarkeitsstudien und Performancetests durchzuführen. Bei einem Spielprototypen war es nötig einen Bewegungspfad zu implementieren. In diesem Tutorial möchte ich zeigen wie man eine Linie auf dem Bildschirm zeichnet, die dann danach von einem Strich abgefahren wird.

[ad#contentadbig]

Ein Motion Path ist wie jede Linie nur eine Aneinanderreihung von Punkten. Wenn man alle Punkte für eine Bewegung somit in einem Array von Punkten verpackt kann man an diesem Array entlang etwas bewegen. Bei Spielen mit einer AI kann man dieses Array selbstverständlich nicht von Beginn an füllen – der Weg muss regelmäßig neu berechnet werden und die Spielfigur muss sich bestimmten Situationen anpassen. Aber bei Tower Defense Spielen kann man zum Beispiel für jeden einzelne Angriffswelle von Beginn an einen Pfad bestimmen.
Dieses Tutorial wird zeigen wie man „Löcher“ ausgleicht und einen Strich entlang des gezeichneten Bewegungspfad bewegt.
Am Ende des Tutorials kann das gesamte Projekt heruntergeladen und beliebig modifiziert werden.

Bewegungspfad zeichnen

Das Projekt besteht aus 3 Dateien. Der Activity, einem SurfaceView (es wird wieder ein Canvas verwendet) und dem Thread, der für das Zeichnen verantwortlich ist.

Die Activity besteht nur aus onCreate

package de.milestoneblog.example.motionpath;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;

public class MotionPathTest extends Activity
{
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);

    requestWindowFeature(Window.FEATURE_NO_TITLE);
    MotionPathView view = new MotionPathView(this);
    setContentView(view);
  }
}
 

Ich denke zu der Activity muss ich nicht mehr viel sagen.

Die DrawThread ist für das Zeichnen verantwortlich. Durch ein Delay von 40 ms erzwinge ich konstante 25 FPS (Frames per second).

package de.milestoneblog.example.motionpath;

import android.graphics.Canvas;
import android.view.SurfaceHolder;

class DrawThread extends Thread
{
  private SurfaceHolder surfaceHolder;
  private MotionPathView view;
  private boolean isRunning;
  private long nextTimeStamp;
  private int delay = 40;

  public DrawThread(SurfaceHolder surfaceHolder, MotionPathView view)
  {
    this.nextTimeStamp = 0;
    this.surfaceHolder = surfaceHolder;
    this.view = view;
    this.isRunning = true;
  }

  public void setRunning(boolean isRunning)
  {
    this.isRunning = isRunning;
  }

  @Override
  public void run()
  {
    Canvas c;
    while (isRunning)
    {
      c = null;
      try
      {
        if (nextTimeStamp > System.currentTimeMillis())
        {
          try
          {
            sleep(nextTimeStamp – System.currentTimeMillis());
          } catch (InterruptedException ignore)
          {
           
          }
        }
       
        nextTimeStamp = System.currentTimeMillis() + delay;
       
        c = surfaceHolder.lockCanvas(null);
        synchronized (surfaceHolder)
        {
          view.onDraw(c);
        }
      } finally
      {
        if (c != null)
        {
          surfaceHolder.unlockCanvasAndPost(c);
        }
      }
    }
  }
}

 

Der DrawThread ist auch relativ einfach aufgebaut. Der Thread prüft vor dem Neuzeichnen, ob es schon an der Zeit ist den nächsten Frame zu erstellen. Wenn dies nicht der Fall ist, wird die entsprechende Differenz gewartet. Auch diese Klasse stellt noch keine hohen Ansprüche.

Etwas komplizierter wird es da schon mit dem SurfaceView. Dieser muss die berührten Punkte miteinander verbinden, (leider und auch sinnvollerweise bekommt im ACTION_MOVE nicht alle berührten Punkte) dies könnte man natürlich über ein drawLine hinbekommen, dann würden jedoch alle Punkte dazwischen nciht als Bewegungspfad angenommen werden. Zudem möchte ich die Linie, die dem Bewegungspfad folgt, Senkrecht zur aktuellen Position zeichnen.

Für den Surfaceview benötigen wir die folgenden Variablen

  private Bitmap line;
  private Canvas lineCanvas;
  private DrawThread thread;
  private Paint paint;
  private Paint linepaint;
  private Paint verticalPaint;
  private ArrayList<PointF> points;
  private PointF last;
  private int idx;
  private float incline;
  private float inclineVertical;
 

Das Bitmap und das dazugehörige Canvas sind für das Buffering der gezeichneten Linie verantwortlich, dadurch ersparen wir uns das Rechenintensive Neuzeichnen alle Punkte in jedem Frame.
Der Thread erklärt sich von selbst. Die Paints werden zum Zeichnen vom Hintergrund, des Bewegungspfades und der Senkrechten benötigt.
Die Arrayliste beinhaltet die Punkte für die Bewegung und last den letzten Punkt, der durch das Touchevent getriggert wurde. Index ist eine Hilfsvariable zum Zählen der aktuellen Position in der Arrayliste und incline und inclineVertical gegen die Steigung des Bewegungspfades in einem Punkt bzw die Steigung der Senkrechten in diesem Punkt an.

Im Konstruktor werden alle benötigten Variablen initialisiert und der Thread erstellt.

public MotionPathView(Context context)
  {
    super(context);

    setFocusable(true);
    setClickable(true);
    setOnTouchListener(this);

    getHolder().addCallback(this);
    thread = new DrawThread(getHolder(), this);

    points = new ArrayList<PointF>();

    paint = new Paint();
    paint.setStrokeWidth(2);
    paint.setAntiAlias(false);
    paint.setColor(Color.GREEN);
   
    linepaint = new Paint();
    linepaint.setStrokeWidth(4);
    linepaint.setAntiAlias(true);
    linepaint.setColor(Color.BLUE);
   

    verticalPaint = new Paint();
    verticalPaint.setStrokeWidth(3);
    verticalPaint.setAntiAlias(true);
    verticalPaint.setColor(Color.RED);

    idx = -1;
  }
 

Sobald das Surface erstellt wurde kann der Thread gestartet und das gepufferte Bitmap inklusive zugehöriges Canvas erstellt werden

@Override
  public void surfaceCreated(SurfaceHolder holder)
  {
    thread.setRunning(true);
    thread.start();

    line = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565);
    lineCanvas = new Canvas(line);
    lineCanvas.drawColor(Color.BLACK);
  }
 

Der nächste interessante Teil ist des onTouch Event

@Override
  public boolean onTouch(View v, MotionEvent event)
  {
    switch (event.getAction())
    {
    case MotionEvent.ACTION_DOWN:
      idx = -1;
      points.clear();
      lineCanvas.drawColor(Color.BLACK);
      last = null;
     
     
    case MotionEvent.ACTION_MOVE:
      if (last == null)
      {
        last = new PointF();
        lineCanvas.drawPoint(event.getX(), event.getY(), paint);
      } else
      {
        float y = 0;
        float innerIncline = 0;
        if (last.x < event.getX())
        {
          innerIncline = (event.getY() – last.y) / (event.getX() – last.x);
          for (int i = (int)last.x ; i < event.getX() ; i++)
          {
            y = innerIncline * (i – last.x) + last.y;
            lineCanvas.drawPoint(i, y, paint);
            points.add(new PointF( i, y));
          }
        }
        else
        {
          innerIncline = (-event.getY() + last.y) / (-event.getX() + last.x);
          for (int i = (int)last.x ; i > event.getX() ; i–)
          {
            y = innerIncline * (i – last.x) + last.y;
            lineCanvas.drawPoint(i, y, paint);
            points.add(new PointF( i, y));
          }
        }
       
        lineCanvas.drawPoint(event.getX(), event.getY(), paint);
      }
     
      points.add(new PointF( event.getX(), event.getY()));
      last.set(event.getX(), event.getY());
      break;

    case MotionEvent.ACTION_UP:
      idx = 0;
      break;

    default:
      break;
    }

    return true;
  }
 

In dieser Methode wird gegebenefalls bei einer ersten Berührung der alte Bewegungspfad zurückgesetzt und bei einem Zeichnen auf dem DIsplay entsprechend die Punkte auf dem Canvas gemalt und in der ArrayListe gespeichert. Leider ist es technisch nicht möglich für jeden Pixel das onTouch Event aufzurufen, dadurch hat man nur einen Bruchteil der Punkte die man eigentlich bräuchte. Damit dies zu keinen Problem wird, wird zwischen dem letzten Punkt und dem aktuellen Punkt die Steigung berechnet um mittels der Punkt-Steigungs-Form die Punkte dazwischen zu berechnen. Wenn man eine vertikale Linie zeichnet funktioniert dies leider nur bedingt, weil Mathematisch keinem X Wert zwei Y Werte zugeordnet sein dürften. Diesen Fall betrachte ich aus dem Grund nicht. Man könnte aber natürlich das Problem umgehen indem man in diesem Fall das Koordinatensystem dreht und den X und Y Wert vertauscht. Dies wollte ich jedoch nicht in einem Tutorial unterbringen.
Wenn man den Finger dann absetzt, wird der index für die ArrayListe von -1 auf 0 gesetzt, das ist der Beginn für die Anmiation entlang dem Bewegungspfad.

@Override
  protected void onDraw(Canvas canvas)
  {
    canvas.drawColor(Color.BLACK);

    if (line != null)
      canvas.drawBitmap(line, 0, 0, null);

    if (idx > -1 && idx < points.size())
    {
      if (idx < points.size()1)
      {
        if (points.get(idx + 1).x – points.get(idx).x != 0)
          incline =  (points.get(idx + 1).y – points.get(idx).y) / (points.get(idx + 1).x – points.get(idx).x);
        else
          incline = 0;
     
        if (points.get(idx + 1).y – points.get(idx).y == 0)
        {
          canvas.drawLine(points.get(idx).x, points.get(idx).y, points.get(idx).x, points.get(idx).y + 10, verticalPaint);
          canvas.drawLine(points.get(idx).x, points.get(idx).y, points.get(idx).x, points.get(idx).y10, verticalPaint);
          idx++;
          return;
        }
      }
     
      inclineVertical = incline == 0 ? incline : -1 / incline;
     
     

      float x = points.get(idx).x + 10;
      float y = inclineVertical * (x – points.get(idx).x) + points.get(idx).y;
      canvas.drawLine(points.get(idx).x, points.get(idx).y, x, y, verticalPaint);
     
      x = points.get(idx).x10;
      y = inclineVertical * (x – points.get(idx).x) + points.get(idx).y;
      canvas.drawLine(points.get(idx).x, points.get(idx).y, x, y, verticalPaint);
      idx++;
    }
  }
 

Sobald der Index auf 0 gesetzt wurde, wird zusätzlich zum Canvas auch noch eine Linie gezeichnet, die Senkrecht zum aktuellen Punkt steht. Die Implementierung ist wieder relativ einfach gehalten. Es wird einfach die Steigung von dem vorhergehenden und dem aktuellen Punkt berechnet. Dies könnte noch um einiges optimiert werden, in dem man mahrere Punkte zur Berechnung der Steigung einsetzt. Im Beispielprogramm kommt es aus diesem Grund manchmal zu kurzen ruckartiken Bewegungen der Linie. Aus der Schule wissen wir, dass sich die Steigung der Senkrechten mit -1 * Steigung berechnen lässt. Mit Steigung der Senkrechten, dem aktuellen Punkt und einem beliebig gewählten x (im Beispiel +/- 10) können wir das dazugehörige Y berechnen. Die Logik ist nicht optimiert, weil dadurch die Linie mal länger und mal kürzer gezeichnet wird, aber auch das aufgrund der Übersicht.

Die Punkte werden nun alle durchlaufen und die Linie bewegt sich dem Bewegungspfad entlang.

Zeichnen des Bewegungspfads im Emulator

Zeichnen des Bewegungspfads im Emulator




Bewegung entlang des Motion Path

Bewegung entlang des Motion Path

Hier das versprochene Beispielprojekt MotionPathTest
Ich hoffe ich konnte mit dem Tutorial einigen helfen und würde mich über Anmerkungen freuen.




2 Kommentare zu “Bewegungspfade in Android – Game Development”

  1. 1
    Carlos sagt:

    Hallo

    danke sehr für dieses Tutorial. Echt super!
    Aber wie könnte man anstatt des roten Striches ein Bild hinzufügen?

    Z.B ein Auto oder so? Ich hoffe Sie können mir weiterhelfen. Denke man musste nicht viel ändern am Code, oder?

    Vielen Dank

    Freundliche Grüsse

  2. 2
    Jens sagt:

    Es sind wirklich nur ein paar wenige Handgriffe.

    Als erstes muss man sich das Bild laden (am einfachsten aus den Ressourcen)
    z.b. mit folgendem Code
    Bitmap meinbild = ((BitmapDrawable) getResources().getDrawable(R.drawable.meinbild )).getBitmap();
    Und dann muss man lediglich anstelle der Strichlogik das Bild auf dem Canvas zeichnen.
    z.b. mit canvas.drawBitmap(meinbild);

    Ich hoffe, dass dies etwas geholfen hat bei der Beantwortung der Frage

Hinterlasse ein Kommentar