GamePad in Flutter and Flame - Dualsense - Xbox

- Andrés Cruz

En español
GamePad in Flutter and Flame - Dualsense - Xbox

The GamePad is the input device used to interact with a video game and one of the first elements that come to mind when we hear the word "video game" or "video game console"; using these types of controls for our games is not impossible; however, in Flame, we don't have many options; in the official documentation, we will see that at the time these words were written, there is a very short section that mentions the use of the gamepad in Flame:

https://docs.flame-engine.org/latest/flame/inputs/other_inputs.html

The problem with the package that the Flame team mentions:

https://github.com/flame-engine/flame_gamepad

It is that at the time these words were written, it has not been updated for more than 3 years and in software development, which includes video game development, it is a considerably long time, therefore, I personally would not recommend its use since there may be package version problems with the current project, which would cause problems in the implementation.

In Flutter (not directly Flame, but the framework known as Flutter) there are a few plugins that we can use; for example this:

https://pub.dev/packages/gamepads

At the time these words were written, the package is under development, therefore, its documentation is very scarce and its implementation is not entirely user friendly, as we will see in the example section; however, it is a package that has potential and is in constant development, therefore it is the one that we are going to use; It is important to mention that this package is not designed for Flame, like the Sharedpreferences package, but we can use it without problems in a project with Flutter and Flame; currently this pack is not available for web or mobile.

First steps with the gamepads plugin

Let's start by adding the dependency for the above package:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  ***
  gamepads:

Which has 2 main classes:

  1. GamepadController: Represents a single currently connected gamepad.
  2. GamepadEvent: It is an event that is built when an input from a gamepad occurs, in other words, when a key is pressed, an event of this type is built.

The GamepadEvent class contains the following properties:

class GamepadEvent {
  // The id of the gamepad controller that fired the event.
  final String gamepadId;

  // The timestamp in which the event was fired, in milliseconds since epoch.
  final int timestamp;

  // The [KeyType] of the key that was triggered.
  final KeyType type;

  // A platform-dependant identifier for the key that was triggered.
  final String key;

  // The current value of the key.
  final double value;
}

The most important would be the key, which indicates the key pressed, and the value, with which, depending on the type of button pressed, we can know the angle or if the button was pressed or released.

The input type can be of two types that is represented by an enumerated type called KeyType:

  1. analog: Analog inputs have a range of possible values depending on how far/hard they are pressed (they represent analog sticks, back triggers, some kinds of d-pads, etc.)
  2. button: Buttons have only two states, pressed (1.0) or not (0.0).

Define a listener to detect keys pressed

For this plugin, we have two main uses, knowing the number of connected controls; for it:

gamepads = await Gamepads.list()

Or create a listener that allows you to listen to the keys pressed on all connected gamepads:

Gamepads.events.listen((GamepadEvent event) { });

As you can see, here we have the GamepadEvent event that has the structure mentioned before; as you can see, the use of the package is extremely simple, since it does not require establishing a connected control or something similar, we automatically have a listener of all the controls connected to the device; from the player component, we place the listener for the gamepad:

import 'package:gamepads/gamepads.dart';
***
@override
Future<void>? onLoad() async {
  // final gamepads = await Gamepads.list();
  // print('Gamepads' + gamepads.toString());
  Gamepads.events.listen((GamepadEvent event) {
    print("gamepadId" + event.gamepadId);
    print("timestamp" + event.timestamp.toString());
    print("type   " + event.type.toString());
    print("key   " + event.key.toString());
    print("value   " + event.value.toString());
  });
}

As a recommendation, connect a device to your PC/Mac, by Bluetooth or its USB cable, start the application and start pressing some buttons on the gamepad, including its joystick and d-pad and analyze the response, you will see that some are actually "button" and others are "analog" based on the type classification mentioned above, in the case of buttons, you will see that the above listener is executed when the button is pressed and released.

Based on tests performed from a Windows 11 device with an Xbox S|X controller, we have the following implementation; to detect the "A" button:

import 'package:gamepads/gamepads.dart';
***
@override
Future<void>? onLoad() async {
  Gamepads.events.listen((GamepadEvent event) {

    if (event.key == 'button-0' && event.value == 1.0) {
      print('rotate');
    }
  });
}

And for the d-pad, detect the moving:

import 'package:gamepads/gamepads.dart';
***
@override
Future<void>? onLoad() async {
  Gamepads.events.listen((GamepadEvent event) {

    if (event.key == 'button-0' && event.value == 1.0) {
      print('jump');
    } else if (event.key == 'pov' && event.value == 0.0) {
      // up
      print('up');
    } else if (event.key == 'pov' && event.value == 4500.0) {
      // up - right
      print('up - right');
    } else if (event.key == 'pov' && event.value == 9000.0) {
      // right
      print('right');
    } else if (event.key == 'pov' && event.value == 13500.0) {
      // buttom right
      print('buttom right');
    } else if (event.key == 'pov' && event.value == 18000.0) {
      // buttom
      print('buttom');
    } else if (event.key == 'pov' && event.value == 22500.0) {
      // buttom left
      print('buttom left');
    } else if (event.key == 'pov' && event.value == 27000.0) {
      // left
      print('left');
    } else if (event.key == 'pov' && event.value == 31500.0) {
      // top left
      print('top left');
    }
  });
}

The case of the d-pad is interesting since, depending on the plugin used, it corresponds to the same button, therefore, when pressing "up arrow" we have a value of 0.0, when pressing "down arrow" we have a value of 18000, like you can see, they are values that correspond to a space of 360 degrees; therefore, we can customize the experience however we want; finally, the implementation using the d-pad looks like:

import 'package:gamepads/gamepads.dart';
***
@override
FutureOr<void> onLoad() async {
  // var gamepads = await Gamepads.list();
  // print('*******');
  // print(gamepads.toString());


  Gamepads.events.listen((GamepadEvent event) {
    
    if (event.key == 'button-0' && event.value == 1.0) {
      // print('Rotate');
      _rotate();
    } else if (event.key == 'pov' && event.value == 0) {
      // up
      // print('up');
      movementType = MovementType.up;
    } else if (event.key == 'pov' && event.value == 4500) {
      // up right
      // print('up right');
    } else if (event.key == 'pov' && event.value == 9000) {
      // right
      // print('right');
      movementType = MovementType.right;
    } else if (event.key == 'pov' && event.value == 13500) {
      // buttom right
      // print('buttom right');
    } else if (event.key == 'pov' && event.value == 18000) {
      //bottom
      // print('bottom');
      movementType = MovementType.down;
    } else if (event.key == 'pov' && event.value == 22500) {
      // buttom left
      // print('buttom left');
    } else if (event.key == 'pov' && event.value == 27000) {
      // left
      // print('left');
      movementType = MovementType.left;
    } else if (event.key == 'pov' && event.value == 31500) {
      // top left
      // print('top left');
    } else {
      movementType = MovementType.idle;
      //
    }
  });
  ***
}

Of course, it is possible that these values will change if you use another type of control or in future versions of the plugin, therefore, the reader is recommended to adapt the above script to your needs.

With this implementation, we have other inputs for the games that we have implemented in this book and that you can of course adapt to others like the dinosaur game.

Andrés Cruz

Develop with Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz In Udemy

I agree to receive announcements of interest about this Blog.