LibGDX Controller

Category: 

In diesem Tutorial geht es um die Verwendung von Gamepads mit LibGDX. Insbesondere betrachte ich hier den Xbox 360 Controller. Nicht nur da ich ihn selbst benutze, sondern auch da er scheinbar allgemein ein sehr beliebter Controller ist.

Icon made by Freepik from www.flaticon.com is licensed under CC BY 3.0

Controller Support hinzufügen

Am einfachsten erhält man Controller Support, wenn man ihn direkt bei der Projekterstellung hinzufügt. Dazu startet ihr das LibGDX Setup und setzt einen Haken bei der Controller Extension.

Falls ihr bereits ein Projekt habt und ihr dieses nachträglich um Controller Support erweitern wollt, müsst ihr in eurem Wurzelprojekt die build.gradle Datei öffnen und die Abhängigkeiten der jeweiligen Projekte (Core, Android, ...) anpassen.

...

project(":desktop") {
  dependencies {
    ...
    compile "com.badlogicgames.gdx:gdx-controllers-desktop:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-controllers-platform:$gdxVersion:natives-desktop"
  }
}

project(":android") {
  dependencies {
    ...
    compile "com.badlogicgames.gdx:gdx-controllers:$gdxVersion"
    compile "com.badlogicgames.gdx:gdx-controllers-android:$gdxVersion"
  }
}

project(":core") {
  dependencies {
    ...
    compile "com.badlogicgames.gdx:gdx-controllers:$gdxVersion"
  }
}

...

Gamepad abfragen

Nun wollen wir zu Testzwecken erst einmal wissen welche Controller alles angeschlossen sind. Wenn gar keiner angeschlossen ist oder dieser ggf. nicht eingeschaltet ist, kann es sein das wir keine Ausgabe erhalten.

for (Controller c : Controllers.getControllers()) {
  System.out.println(c.getName());
}

Unter Umständen erhält man nun eine unerwartete Ausgabe. Ein XBox 360 Wireless Controller wird z.B. nicht direkt angezeigt, sondern der Receiver von jenem Controller. Dies liegt daran, dass der Controller eben nur durch den Receiver mit dem PC kommuniziert. Dies soll uns aber nicht weiter stören. In meinem Fall wird außerdem noch meine 3D Maus angezeigt, welche ich fürs 3D modelling mit Blender verwende.

SpaceMouse Pro Wireless Receiver
SpaceMouse Pro Wireless
Controller (Xbox 360 Wireless Receiver for Windows)
3Dconnexion KMJ Emulator

In anderen Tutorials habe ich schon oft gesehen, wie die Leute den Controller via Controllers.getControllers().get(0) holen. Dies ist natürlich keine gute Idee, da eben mehrere Controller angeschlossen sein könnten, von denen unter Umständen nicht alle als Gamepad geeignet sind.

Wenn man nun Controller nutzen möchte, sollte man vorher sichergehen, dass auch welche angeschlossen sind. Dazu geht man am besten die Liste einmal durch und prüft ob ein Controller angeschlossen ist, welchen wir unterstützen. Falls dies nicht der Fall ist, kann man entweder den erstbesten Controller nehmen oder eben zurück zur Tastatur greifen.

Array<Controller> controllers = Controllers.getControllers()
if(controllers.size()==0){
  //Keine Controller vorhanden...
} else {
  Controller pad = null;
  for(Controller c : controllers) {
    if(c.getName().contains("Xbox") && c.getName().contains("360")) {
      pad = c;
    }
  }

  if(xbox==null){
    //Kein Xbox Controller vorhanden
    //ggf. auf einen anderen zurückgreifen
    //z.B. pad = controllers.get(0)
  }
}

Nicht jeder XBox 360 Controller hat den gleichen Namen! Je nachdem welche Version es ist, kann sich der Name unterscheiden. Daher ist eine Abfrage wie die obige sinnvoll.

Um die eigentlichen Tasten bzw. Achsen abzufragen, muss man wissen welcher Code von welcher Taste genutzt wird. Die meisten Gamepads mappen die Analogsticks auf die Achsen 0 bis 3 und die "Hauptbuttons" auf die Tasten 0 bis 3. Leider ist dies jedoch nicht bei allen Gamepads der Fall. Außerdem kann sich dieses Mapping je nach Betriebssystem unterscheiden. Der Ipega Bluetooth Controller nutzt für die vier Hauptbuttons unter Windows die IDs 0 bis 3, während er unter Android die IDs 96, 97, 99 und 100 nutzt.
Wenn man nun nur den XBox Controller nutzen will, so kann man das folgende Mapping nutzen:

public class XBox {
  public static final int BUTTON_A = 0;
  public static final int BUTTON_B = 1;
  public static final int BUTTON_X = 2;
  public static final int BUTTON_Y = 3;
  public static final int BUTTON_LB = 4;
  public static final int BUTTON_RB = 5;
  public static final int BUTTON_BACK = 6;
  public static final int BUTTON_START = 7;
  public static final int BUTTON_LS = 8; //Left Stick pressed down
  public static final int BUTTON_RS = 9; //Right Stick pressed down
 
  public static final int POV = 0;
 
  public static final int AXIS_LY = 0; //-1 is up | +1 is down
  public static final int AXIS_LX = 1; //-1 is left | +1 is right
  public static final int AXIS_RY = 2; //-1 is up | +1 is down
  public static final int AXIS_RX = 3; //-1 is left | +1 is right
  public static final int AXIS_TRIGGER = 4; //LT and RT are on the same Axis! LT > 0 | RT < 0
}

Die beiden Trigger werden leider nur durch die XInput Schnittstelle richtig erkannt, LibGDX scheint jedoch nur DirectInput zu nutzen, welches beide Trigger als eine Achse betrachtet.

Um die eigentlichen Achsen und Tastendrücke abzufragen, gibt es grundsätzlich zwei Methoden. Polling und Eventbasiert.

Polling Controller State

Sobald man eine Controller Instanz hat, kann man diese nach dem aktuellen Zustand abfragen. Dabei kann ein Controller mehrere Komponenten haben:

  • Buttons: Typischerweise die "einfachen" Tasten (A, B, X, Y, ...), aber bei manchen Gamepads zählt auch das Steuerkreuz als Button. Sie sind entweder gedrückt oder nicht.
  • Achsen: Meistens durch Analogsticks repräsentiert. Der Wert liegt zwischen -1 und 1.
  • POVs (Point-Of-View): Eine Gruppe von Tasten, die zusammen einen diskreten Richtungszustand repräsentieren (north, south, ...). Die Werte stammen aus der Enumeration PovDirection
  • Slider: Die API sagt nicht genau was es ist und wurde wohl auch noch nicht getestet. Es ist nur klar, dass ein Slider entweder true oder false ist. Vermutlich handelt es sich um sowas wie On-Off switches.
  • Accelerometer: Ein Neigungssensor, welcher einen Vector3 zurückliefert. Die meisten Controller haben jedoch keinen Neigungssensor.

Um nun den Wert einer bestimmten Komponente zu erhalten, fragen wir dies einfach mit der passenden Methode ab.

public void render() {
  if(pad!=null) {
    if(pad.getButton(XBox.BUTTON_A)) {
      //jump
    }
    speedy = pad.getAxis(XBox.AXIS_LY);
    speedx = pad.getAxis(XBox.AXIS_LX);
    //...
  }
}

Das Polling bietet einen schnellen Einstieg, kann aber unter Umständen unübersichtlich werden. Wenn man eine Taste nur sehr kurz antippt, kann es außerdem sein, dass dies nicht bemerkt wird.

Eventbasiert

Bei diesem Ansatz registrieren wir einen Listener, dessen Methoden aufgerufen werden, wenn sich etwas ändert. Dabei ist auch sichergestellt, das wir nichts verpassen. So einen Listener kann man direkt an den Controller heften, sodass man alle Events von diesem einen Controller erhält, aber wenn man möchte kann man den Listener auch per Controllers.addListener registrieren, wodurch wir alle Events von jedem Controller erhalten.

pad.addListener(new ControllerAdapter() {
  @Override
  public boolean buttonDown(Controller controller, int buttonIndex) {
    return false;
  }
  @Override
  public boolean buttonUp(Controller controller, int buttonIndex) {
    if(buttonIndex == XBox.BUTTON_A) {
      //jump
    }
    return false;
  }
  @Override
  public boolean axisMoved(Controller controller, int axisIndex, float value) {
    if(axisIndex == XBox.AXIS_LX) {
      speedx = value;
    } else if(axisIndex == XBox.AXIS_LY) {
      speedy = value;
    }
    return false;
  }
  @Override
  public boolean povMoved(Controller controller, int povIndex, PovDirection value) {
    return false;
  }
});

Euch ist sicherlich aufgefallen, dass diese Methoden einen booleschen Wert zurückgeben. Dieser ist wichtig, wenn es mehrere Listener gibt, die auf Events des gleichen Controllers reagieren. Die Listener werden nacheinander aufgerufen, aber nur solange false zurückgegeben wird. Sobald ein Listener true zurückgibt, wird das Event nicht mehr an die nächsten weitergereicht.

Controller (Dis-)connects

Die Listener enthalten auch Callbacks für das Disconnecten, bzw. Connecten. Allerdings funktioniert dies aktuell noch nicht auf dem Desktop, sondern nur auf Android. Falls ein Controller während des Spiels die Verbindung verliert, könnt z.B. das Spiel pausieren und dem Spieler mitteilen, dass er den Controller wieder anschließen an. Wenn der Controller wieder angeschlossen wird, wird jedoch eine neue Controller Instanz erzeugt!

Und damit sind wir auch schon am Ende des Tutorials. Falls ihr Fragen oder Anregungen habt, schreibt einfach einen Kommentar.