image image


Algorithmen für Spieleprogrammierer: Tile Based Movement

Manchmal möchte man (besonders in kachelbasierten Spielewelten), daß die Bewegungen der Spielfiguren exakt auf einer Kachel beginnen und auch wieder exakt auf einer Kachel enden, so wie bei einem Schachspiel oder einem Sokoban-Klon. Im Zweifelsfalle kann man sich damit behelfen, daß man die Figur von Kachel zu Kachel springen läßt, sie also keine Zwischenschritte macht. Aber in dem netten, schon mehrmals von mir gelobten Buch Mission Python von Sean McManus habe ich eine elegantere Lösung gefunden, die ich Euch hier vorstellen möchte. Sean McManus hatte sie in Pygame Zero implementiert, meine Version habe ich dann aber in Processing.py, dem Python-Mode von Processing programmiert.

image

Die Grundidee des Algorithmus’ ist eigentlich einfach: Die Bewegung wird in vier Einzelschritte aufgeteilt und bei jedem Schritt legt die Spielfigur 1/4 der Kachelgröße zurück. Das kann man im folgenden Code-Schnipsel nachvollziehen:

    def move(self):
        self.old_x = self.x
        self.old_y = self.y
        if self.frame > 0:
            self.frame += 1
            time.sleep(0.1)
            if self.frame == 5:
                self.frame = 0
                self.offset_x = 0
                self.offset_y = 0

        if self.dir == "right" and self.frame > 0:
            self.offset_x = -1 + (0.25 * self.frame)
        if self.dir == "left" and self.frame > 0:
            self.offset_x = 1 - (0.25 * self.frame)
        if self.dir == "up" and self.frame > 0:
            self.offset_y = 1 - (0.25 * self.frame)
        if self.dir == "down" and self.frame > 0:
            self.offset_y = -1 + (0.25 * self.frame)

Eine Bewegung der Spielfigur erfolgt nur, wenn self.frame > 0 ist. Dann wird bei jedem Teilschritt self.frame um Eins erhöht. Gilt self.frame == 5, dann werden self.frame und die Offset-Werte wieder auf Null zurückgesetzt, denn dann hat die Spielfigur das Ziel-Tile (die Ziel-Kachel) erreicht.

Der Clou ist, daß man für jede Teilbewegung ein Dictionary definiert, das ein entsprechendes Bild für diese Teilbewegung zur Verügung stellt (in der Reihenfolge: Steht, Schritt 1, Schritt 2, Schritt 3, Schritt 4):

        self.images = {
            "left": [self.orclf2, self.orclf1, self.orclf2,
                     self.orclf1, self.orclf2],
            "right": [self.orcrt2, self.orcrt1, self.orcrt2,
                      self.orcrt1, self.orcrt2],
            "up": [self.orcbk2, self.orcbk1, self.orcbk2,
                   self.orcbk1, self.orcbk2],
            "down": [self.orcfr2, self.orcfr1, self.orcfr2,
                     self.orcfr1, self.orcfr2]

Ich habe für dieses Beispiel wieder die kleinen Orks aus der freien (CC-BY-3.0) Sprite-Sammlung von Philipp Lenssen (über 700 animierte Avatare in der klassischen Größe von 32x32 Pixeln) herangezogen. Die Lizenz verlangt eine Erwähnung des Urhebers, was ich hiermit mit Vergnügen erledigt habe.

image image image image image image image image

Es gibt nur zwei Bilder für jede Bewegung in jeder Richtung, also acht insgesamt und so tauchen die Einzelbilder in dem Dictionary mehrfach auf, aber da es ja nur Variablen auf das Bild sind, ist dies für die Funktionalität unerheblich. Und wenn Ihr ein Spritesheet mit vier Bewegungsbildern habt und vielleicht sogar noch ein separates Bild für »Stehen« in jede Richtung, braucht Ihr nur die Namen zu ändern.

Durch die Organisation der Einzelbewegungen in einem Dictionary ist die Darstellung der Spielfiguren recht einfach zu programmieren:

    def display(self):
        image(self.images[self.dir][self.frame], self.x * self.sz + self.offset_x * self.sz,
              self.y * self.sz + self.offset_y * self.sz)

self.sz ist die Kachelgröße, in diesem Falle 32 Pixel, aber natürlich kommt der Algorithmus mit jeder anderen Kachelgröße ebenfalls zurecht. Und es müssen nicht einmal unterschiedliche Bewegungen je Schritt sein, hat man nur ein Bild von einer Spielfigur, dann gleitet dieses Bild eben von der Herkunftskachel auf die Zielkachel. Das macht zum Beispiel bei einem Schachprogramm richtig etwas her.

Da ein Processing.py-Sketch keine richtige Game-Loop kennt, habe ich die Logik der Bewegungen in diesem Beispielprogramm in die Funktion keyPressed() gelegt:

def keyPressed():
    if keyPressed and key == CODED:
        if orc.frame == 0:
            if keyCode == RIGHT:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.x >= ((width - TILESIZE)/TILESIZE):
                    orc.x = orc.from_x
                    orc.frame = 0
                else:
                    orc.x += 1
                    orc.frame = 1
                orc.dir = "right"
            elif keyCode == LEFT:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.x <= 0:
                    orc.x = orc.from_x
                    orc.frame = 0
                else:
                    orc.x -= 1
                    orc.frame = 1
                orc.dir = "left"
            elif keyCode == UP:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.y <= 0:
                    orc.y = orc.from_y
                    orc.frame = 0
                else:
                    orc.y -= 1
                    orc.frame = 1
                orc.dir = "up"
            elif keyCode == DOWN:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.y >= ((height - TILESIZE)/TILESIZE):
                    orc.y = orc.from_y
                    orc.frame = 0
                else:
                    orc.y += 1
                    orc.frame = 1 
                orc.dir = "down"

Bei einem rundenbasierten RPG oder bei einem Schachspiel müßte man vielleicht zusätzlich noch einen Status (zum Beispiel player_ready) einführen, den man nach jeder Runde auf False setzt, damit der oder die Gegner ihren Zug/ihre Züge machen können. Erst wenn die Gegenzüge durch sind, wird dann player_ready wieder auf True gesetzt.

Zum Schluß wie gewohnt den kompletten Quellcode, damit Ihr das Beispielprogramm nachvollziehen, erweitern oder sonst etwas damit anstellen könnt. Ich habe das Programm wieder aufgeteilt, zuerst also der Code für den Reiter orc.py:

# coding=utf-8
import time

TILESIZE = 32

class Orc():

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.from_x = 0
        self.from_y = 0
        self.offset_x = 0
        self.offset_y = 0
        self.dir = "right"
        self.frame = 0
        self.sz = TILESIZE
        self.images = {}

    def loadPics(self):
        self.orcrt1 = loadImage("orcrt1.gif")
        self.orcrt2 = loadImage("orcrt2.gif")
        self.orcfr1 = loadImage("orcfr1.gif")
        self.orcfr2 = loadImage("orcfr2.gif")
        self.orclf1 = loadImage("orclf1.gif")
        self.orclf2 = loadImage("orclf2.gif")
        self.orcbk1 = loadImage("orcbk1.gif")
        self.orcbk2 = loadImage("orcbk2.gif")
        self.images = {
            "left": [self.orclf2, self.orclf1, self.orclf2,
                     self.orclf1, self.orclf2],
            "right": [self.orcrt2, self.orcrt1, self.orcrt2,
                      self.orcrt1, self.orcrt2],
            "up": [self.orcbk2, self.orcbk1, self.orcbk2,
                   self.orcbk1, self.orcbk2],
            "down": [self.orcfr2, self.orcfr1, self.orcfr2,
                     self.orcfr1, self.orcfr2]
        }

    def move(self):
        self.old_x = self.x
        self.old_y = self.y
        if self.frame > 0:
            self.frame += 1
            time.sleep(0.1)
            if self.frame == 5:
                self.frame = 0
                self.offset_x = 0
                self.offset_y = 0

        if self.dir == "right" and self.frame > 0:
            self.offset_x = -1 + (0.25 * self.frame)
        if self.dir == "left" and self.frame > 0:
            self.offset_x = 1 - (0.25 * self.frame)
        if self.dir == "up" and self.frame > 0:
            self.offset_y = 1 - (0.25 * self.frame)
        if self.dir == "down" and self.frame > 0:
            self.offset_y = -1 + (0.25 * self.frame)
            
    def display(self):
        image(self.images[self.dir][self.frame], self.x * self.sz + self.offset_x * self.sz,
              self.y * self.sz + self.offset_y * self.sz)

Und dann das Hauptprogramm. Ich habe dort Hilfslinien eingezeichnet, damit Ihr nachprüfen könnt, daß die Bewegungen des Orks tatsächlich immer exakt auf einer Kachel beginnen und enden.

TILESIZE = 32
from orc import Orc

orc = Orc(3, 3)

def setup():
    size(320, 320)
    this.surface.setTitle("Tile Based Movement")
    orc.loadPics()

def draw():
    background(100, 150, 0)
    drawGrid()
    orc.move()
    orc.display()

def drawGrid():
    stroke(255)
    for i in range(0, width, TILESIZE):
        line(i, 0, i, height)
    for i in range(0, height, TILESIZE):
        line(0, i, width, i)
        
def keyPressed():
    if keyPressed and key == CODED:
        if orc.frame == 0:
            if keyCode == RIGHT:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.x >= ((width - TILESIZE)/TILESIZE):
                    orc.x = orc.from_x
                    orc.frame = 0
                else:
                    orc.x += 1
                    orc.frame = 1
                orc.dir = "right"
            elif keyCode == LEFT:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.x <= 0:
                    orc.x = orc.from_x
                    orc.frame = 0
                else:
                    orc.x -= 1
                    orc.frame = 1
                orc.dir = "left"
            elif keyCode == UP:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.y <= 0:
                    orc.y = orc.from_y
                    orc.frame = 0
                else:
                    orc.y -= 1
                    orc.frame = 1
                orc.dir = "up"
            elif keyCode == DOWN:
                orc.from_x = orc.x
                orc.from_y = orc.y
                if orc.y >= ((height - TILESIZE)/TILESIZE):
                    orc.y = orc.from_y
                    orc.frame = 0
                else:
                    orc.y += 1
                    orc.frame = 1 
                orc.dir = "down"

Es ist ja immer noch mein heimlicher Traum, mal so etwas wie ein Rogue-like zu programmieren. Das hier könnte ja schon fast der Anfang sein, aber vielleicht implementiere ich das erst einmal zur Übung in Pygame oder noch rudimentärer in Tkinter oder dem darauf aufbauenden Graphics.py. Rogue-likes verlangen nicht nach einer großartigen Graphikleistung und in der Beschränkung liegt die Kraft. Still digging!

image


(Kommentieren) 

image image



Über …

Der Schockwellenreiter ist seit dem 24. April 2000 das Weblog digitale Kritzelheft von Jörg Kantel (Neuköllner, EDV-Leiter, Autor, Netzaktivist und Hundesportler — Reihenfolge rein zufällig). Hier steht, was mir gefällt. Wem es nicht gefällt, der braucht ja nicht mitzulesen. Wer aber mitliest, ist herzlich willkommen und eingeladen, mitzudiskutieren!

Alle eigenen Inhalte des Schockwellenreiters stehen unter einer Creative-Commons-Lizenz, jedoch können fremde Inhalte (speziell Videos, Photos und sonstige Bilder) unter einer anderen Lizenz stehen.

Der Besuch dieser Webseite wird aktuell von der Piwik Webanalyse erfaßt. Hier können Sie der Erfassung widersprechen.

Diese Seite verwendet keine Cookies. Warum auch? Was allerdings die iframes von Amazon, YouTube und Co. machen, entzieht sich meiner Kenntnis.


Werbung


Werbung


image  image  image
image  image  image


image