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.

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