Skip to content

Game Loop1

How-To Engine Code: Der Game Loop

Das Snake-Spiel ist ein erstes interaktives Spiel. Es nutzt den Game Loop der Engine. Dieser funktioniert folgendermaßen:

Der Engine Pi Game Loop

Ein Film besteht aus 24 bis 60 Bildern pro Sekunde, die schnell hintereinander abgespielt werden, um die Illusion von Bewegung zu erzeugen. Ähnlich werden bei (den meisten) Computerspielen 30 bis 60 Bilder pro Sekunde in Echtzeit gerendert, um die selbe Illusion von Bewegung zu erzeugen. Nach jedem Bild berechnet die Engine intern die nächsten Schritte und gibt die relevanten Ereignisse (Tastendruck, Kollision, Frame-Update) an die entsprechenden Listener weiter.

Alle Spiel-Logik ist also in den Listener-Interfaces geschrieben. Guter Engine-Code ist verpackt in Interfaces nach Spiel-Logik.

Snake ohne Körper

Das folgende Program implementiert ein einfaches Snake-Spiel mit einem Steuerbaren Kreis und dem Ziel, Goodies zu sammeln.

Das Snake-Spiel: Der Kreis jagt die willkürlich auftauchenden Texte

public class SnakeMinimal extends Scene
{
    private Text scoreText = new Text("Score: 0", 1.4);

    private int score = 0;

    private Snake snake = new Snake();

    public SnakeMinimal()
    {
        add(snake);
        scoreText.position(-9, 5);
        add(scoreText);
        placeRandomGoodie();
    }

    public void setScore(int score)
    {
        this.score = score;
        scoreText.content("Score: " + score);
    }

    public void increaseScore()
    {
        setScore(score + 1);
    }

    public void placeRandomGoodie()
    {
        double x = Random.range() * 10 - 5;
        double y = Random.range() * 10 - 5;
        Goodie goodie = new Goodie();
        goodie.center(x, y);
        add(goodie);
        goodie.addCollisionListener(snake, goodie);
    }

    private class Snake extends Circle
            implements FrameUpdateListener, KeyStrokeListener
    {
        private Vector movement = new Vector(0, 0);

        public Snake()
        {
            super(1);
            color("green");
        }

        @Override
        public void onFrameUpdate(double pastTime)
        {
            moveBy(movement.multiply(pastTime));
        }

        @Override
        public void onKeyDown(KeyEvent keyEvent)
        {
            switch (keyEvent.getKeyCode())
            {
            case KeyEvent.VK_W:
                movement = new Vector(0, 5);
                break;

            case KeyEvent.VK_A:
                movement = new Vector(-5, 0);
                break;

            case KeyEvent.VK_S:
                movement = new Vector(0, -5);
                break;

            case KeyEvent.VK_D:
                movement = new Vector(5, 0);
                break;
            }
        }
    }

    private class Goodie extends Text implements CollisionListener<Snake>
    {
        public Goodie()
        {
            super("Eat Me!", 1);
            color("red");
        }

        @Override
        public void onCollision(CollisionEvent<Snake> collisionEvent)
        {
            increaseScore();
            remove();
            placeRandomGoodie();
        }
    }

    public static void main(String[] args)
    {
        Controller.start(new SnakeMinimal(), 600, 400);
    }
}
Zum Java-Code: demos/docs/main_classes/controller/game_loop/SnakeMinimal.java

Snake: Frame-Weise Bewegung

Die Snake ist der spielbare Charakter. Sie soll sich jeden Frame in eine der vier Himmelsrichtungen bewegen.

Die Bewegung der Snake soll möglichst flüssig sein. Daher wird die Bewegung in jedem einzelnen Frame ausgeführt, um maximal sauber auszusehen. Dazu implementiert die Snake das Engine-Interface FrameUpdateListener, um in jedem Frame seine Bewegungslogik auszuführen.

Hierzu kennt die Snake ihre aktuelle Geschwindigkeit als gerichteten Vektor (in Meter/Sekunde). Ein Frame ist deutlich kürzer als eine Sekunde. Mathematik zur Hilfe! v = s/t und damit s = v\*t. Jeden Frame erhält die Snake die tatsächlich vergangene Zeit t seit dem letzten Frame-Update und verrechnet diese mit ihrer aktuellen Geschwindigkeit v:

        @Override
        public void onFrameUpdate(double pastTime)
        {
            moveBy(movement.multiply(pastTime));
        }
Zum Java-Code: demos/docs/main_classes/controller/game_loop/SnakeMinimal.java

Bewegungsgeschwindigkeit festlegen

Was die tatsächliche Bewegungsgeschwindigkeit der Snake ist, hängt davon ab, welche Taste der Nutzer zuletzt runtergedrückt hat und ist in der Snake über KeyStrokeListener gelöst wie im vorigen Tutorial:

        public void onKeyDown(KeyEvent keyEvent)
        {
            switch (keyEvent.getKeyCode())
            {
            case KeyEvent.VK_W:
                movement = new Vector(0, 5);
                break;

            case KeyEvent.VK_A:
                movement = new Vector(-5, 0);
                break;

            case KeyEvent.VK_S:
                movement = new Vector(0, -5);
                break;

            case KeyEvent.VK_D:
                movement = new Vector(5, 0);
                break;
            }
        }
    }
Zum Java-Code: demos/docs/main_classes/controller/game_loop/SnakeMinimal.java

Auf Kollisionen reagieren: Goodies

Die Schlange bewegt sich. Als nächstes braucht sie ein Ziel, auf das sie sich zubewegt. Hierzu schreiben wir eine Klasse für Goodies.

Ein Goodie wartet nur darauf, gegessen zu werden. Damit nicht jeden Frame "von Hand" überprüft werden muss, ob die Schlange das Goodie berührt, lässt sich das ebenfalls über ein Listener-Interface lösen: CollisionListener. Das Interface ist mit Java Generics umgesetzt, daher die spitzen Klammern. Einige Vorteile hiervon kannst du in der Dokumentation durchstöbern.

Wenn das Goodie mit der Schlange kollidiert, so soll der Punktestand geändert, das Goodie entfernt, und ein neues Goodie platziert werden.

        public void onCollision(CollisionEvent<Snake> collisionEvent)
        {
            increaseScore();
            remove();
            placeRandomGoodie();
        }
Zum Java-Code: demos/docs/main_classes/controller/game_loop/SnakeMinimal.java

In der placeRandomGoodie()-Methode wird ein neues Goodie erstellt und mit Random an einer zufälligen Stelle auf dem Spielfeld platziert. Weil das Goodie nur auf Kollision mit der Schlange reagieren soll (und nicht z.B. auf Kollision mit dem "Score"-Text), wird es abschließend als Collision-Listener spezifisch mit der Schlange angemeldet:

    public void placeRandomGoodie()
    {
        double x = Random.range() * 10 - 5;
        double y = Random.range() * 10 - 5;
        Goodie goodie = new Goodie();
        goodie.center(x, y);
        add(goodie);
        goodie.addCollisionListener(snake, goodie);
    }
Zum Java-Code: demos/docs/main_classes/controller/game_loop/SnakeMinimal.java

Anregung zum Experimentieren

Eine Snake, die mit jedem Pickup wächst

public class SnakeAdvanced extends Scene implements FrameUpdateListener
{
    private Text scoreText = new Text("Score: 0", 1.4);

    private int score = 0;

    private Snake snakeHead = new Snake();

    private double snakeSpeed = 5;

    private boolean makeNewHead = false;

    public SnakeAdvanced()
    {
        add(snakeHead);
        scoreText.position(-9, 5);
        add(scoreText);
        placeRandomGoodie();
    }

    public void setScore(int score)
    {
        this.score = score;
        snakeSpeed = 5 + (score * 0.1);
        scoreText.content("Score: " + score);
    }

    public void increaseScore()
    {
        setScore(score + 1);
    }

    public void placeRandomGoodie()
    {
        double x = Random.range() * 10 - 5;
        double y = Random.range() * 10 - 5;
        Goodie goodie = new Goodie();
        goodie.center(x, y);
        add(goodie);
        goodie.addCollisionListener(snakeHead, goodie);
    }

    @Override
    public void onFrameUpdate(double pastTime)
    {
        double dX = 0, dY = 0;
        if (Controller.isKeyPressed(KeyEvent.VK_W))
        {
            dY = snakeSpeed * pastTime;
        }
        if (Controller.isKeyPressed(KeyEvent.VK_A))
        {
            dX = -snakeSpeed * pastTime;
        }
        if (Controller.isKeyPressed(KeyEvent.VK_S))
        {
            dY = -snakeSpeed * pastTime;
        }
        if (Controller.isKeyPressed(KeyEvent.VK_D))
        {
            dX = snakeSpeed * pastTime;
        }
        if (makeNewHead)
        {
            Snake newHead = new Snake();
            newHead.center(snakeHead.center());
            newHead.next = snakeHead;
            add(newHead);
            snakeHead = newHead;
            makeNewHead = false;
        }
        else if (dX != 0 || dY != 0)
        {
            snakeHead.snakeHeadMove(dX, dY);
        }
    }

    private class Snake extends Circle
    {
        private Snake next = null;

        public Snake()
        {
            super(1);
            color("green");
        }

        private void snakeHeadMove(double dX, double dY)
        {
            Vector mycenter = center();
            moveBy(dX, dY);
            if (next != null)
            {
                next.snakeChildrenMove(mycenter);
            }
        }

        private void snakeChildrenMove(Vector newCenter)
        {
            Vector mycenter = center();
            center(newCenter);
            if (next != null)
            {
                next.snakeChildrenMove(mycenter);
            }
        }
    }

    private class Goodie extends Text implements CollisionListener<Snake>
    {
        public Goodie()
        {
            super("Eat Me!", 1);
            color("red");
        }

        @Override
        public void onCollision(CollisionEvent<Snake> collisionEvent)
        {
            increaseScore();
            makeNewHead = true;
            remove();
            placeRandomGoodie();
        }
    }

    public static void main(String[] args)
    {
        Controller.start(new SnakeAdvanced(), 600, 400);
    }
}
Zum Java-Code: demos/docs/main_classes/controller/game_loop/SnakeAdvanced.java

  • Deadly Pickups: Es gibt noch keine Gefahr für die Schlange. Ein giftiges Pick-Up tötet die Schlange und beendet das Spiel (oder zieht der Schlange einen von mehreren Hit Points ab).
  • Smoother Movement: Die aktuelle Implementierung für die Bewegung der Schlange ist sehr steif und die Schlange kann nicht stehen bleiben. Vielleicht möchtest du dem Spieler mehr Kontrolle über die Schlange geben: Statt des KeyStrokeListener-Interfaces, kann die Schlange in ihrer onFrameUpdate(float)-Methode abfragen, ob gerade der W/A/S/D-Key heruntergedrückt ist und sich entsprechend dessen weiter bewegen. Tipp: Die Methode ea.Game.isKeyPressed(int keycode) ist hierfür hilfreich.
  • Escalating Difficulty: Je mehr Pick-Ups gesammelt wurden (und damit desto höher der Score), desto schneller bewegt sich die Schlange. Actual Snake: Wenn du Lust auf eine Herausforderung hast, kannst du aus dem Spiel ein echtes Snake machen: Beim aufnehmen eines Pick-Ups wird die Schlange um ein Glied länger. Die Glieder bewegen sich versetzt mit der Schlange weiter. Wenn die Schlange sich selbst berührt, ist das Spiel beendet.

  1. Der Abschnitt stammt aus dem Engine-Alpha-Wiki: https://engine-alpha.org/wiki/v4.x/Game_Loop