diff --git a/packages/gamepads/pubspec.yaml b/packages/gamepads/pubspec.yaml index 09b211d..2308604 100644 --- a/packages/gamepads/pubspec.yaml +++ b/packages/gamepads/pubspec.yaml @@ -17,6 +17,8 @@ flutter: default_package: gamepads_darwin windows: default_package: gamepads_windows + web: + default_package: gamepads_web environment: sdk: '>=2.19.0 <3.0.0' @@ -30,6 +32,7 @@ dependencies: gamepads_ios: ^0.1.2+2 gamepads_linux: ^0.1.1+3 gamepads_platform_interface: ^0.1.2+1 + gamepads_web: ^0.1.0 gamepads_windows: ^0.1.1+3 dev_dependencies: diff --git a/packages/gamepads_web/.gitignore b/packages/gamepads_web/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/gamepads_web/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/gamepads_web/LICENSE b/packages/gamepads_web/LICENSE new file mode 100644 index 0000000..fd50777 --- /dev/null +++ b/packages/gamepads_web/LICENSE @@ -0,0 +1 @@ +../../LICENSE diff --git a/packages/gamepads_web/README.md b/packages/gamepads_web/README.md new file mode 100644 index 0000000..fe84005 --- /dev/null +++ b/packages/gamepads_web/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/gamepads_web/analysis_options.yaml b/packages/gamepads_web/analysis_options.yaml new file mode 100644 index 0000000..ba5631f --- /dev/null +++ b/packages/gamepads_web/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flame_lint/analysis_options.yaml \ No newline at end of file diff --git a/packages/gamepads_web/lib/gamepad_detector.dart b/packages/gamepads_web/lib/gamepad_detector.dart new file mode 100644 index 0000000..126e100 --- /dev/null +++ b/packages/gamepads_web/lib/gamepad_detector.dart @@ -0,0 +1,27 @@ +import 'dart:js_interop'; + +import 'package:gamepads_platform_interface/api/gamepad_controller.dart'; +import 'package:gamepads_platform_interface/gamepads_platform_interface.dart'; +import 'package:web/web.dart'; + +List getGamepads(GamepadsPlatformInterface plugin) { + final controllers = []; + final gamepads = getGamepadList(); + for (var i = 0; i < gamepads.length; i++) { + final gamepad = gamepads[i]; + controllers.add( + GamepadController( + id: gamepad!.index.toString(), + name: gamepad.id, + plugin: plugin, + ), + ); + } + return controllers; +} + +List getGamepadList() { + final gamepads = window.navigator.getGamepads().toDart; + gamepads.removeWhere((item) => item == null); + return gamepads; +} diff --git a/packages/gamepads_web/lib/gamepads_web.dart b/packages/gamepads_web/lib/gamepads_web.dart new file mode 100644 index 0000000..b214639 --- /dev/null +++ b/packages/gamepads_web/lib/gamepads_web.dart @@ -0,0 +1,132 @@ +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:gamepads_platform_interface/api/gamepad_controller.dart'; +import 'package:gamepads_platform_interface/api/gamepad_event.dart'; +import 'package:gamepads_platform_interface/gamepads_platform_interface.dart'; +import 'package:gamepads_web/gamepad_detector.dart'; +import 'package:web/web.dart' as web; + +class _GamePadState { + _GamePadState(int length) { + keyStates = List.filled(length, null); + axesStates = List.filled(4, 0); + } + + List? keyStates; + List? axesStates; +} + +/// A web implementation of the GamepadsWebPlatform of the GamepadsWeb plugin. +class GamepadsWeb extends GamepadsPlatformInterface { + int _gamepadCount = 0; + Timer? _gamepadPollingTimer; + + final Map _lastGamePadstates = {}; + + void updateGamepadsStatus() { + final gamepads = getGamepadList(); + for (final gamepad in gamepads) { + final buttonlist = gamepad!.buttons.toDart; + final axeslist = gamepad.axes.toDart; + final gamepadId = gamepad.index.toString(); + _GamePadState lastState; + if (_lastGamePadstates.containsKey(gamepadId) && + _lastGamePadstates[gamepadId]?.keyStates?.length == + buttonlist.length) { + lastState = _lastGamePadstates[gamepadId]!; + } else { + _lastGamePadstates[gamepadId] = _GamePadState(buttonlist.length); + lastState = _lastGamePadstates[gamepadId]!; + } + for (var i = 0; i < buttonlist.length; i++) { + if (lastState.keyStates?[i] != buttonlist[i].value) { + lastState.keyStates?[i] = buttonlist[i].value; + emitGamepadEvent( + GamepadEvent( + gamepadId: gamepadId, + timestamp: DateTime.now().millisecondsSinceEpoch, + type: KeyType.button, + key: 'button $i', + value: buttonlist[i].value, + ), + ); + } + } + for (var i = 0; i < 4; i++) { + if ((lastState.axesStates![i] - axeslist[i].toDartDouble).abs() > + 0.03) { + lastState.axesStates?[i] = axeslist[i].toDartDouble; + emitGamepadEvent( + GamepadEvent( + gamepadId: gamepadId, + timestamp: DateTime.now().millisecondsSinceEpoch, + type: KeyType.analog, + key: 'analog $i', + value: axeslist[i].toDartDouble, + ), + ); + } + } + } + } + + /// Constructs a GamepadsWeb + GamepadsWeb() { + web.window.addEventListener( + 'gamepadconnected', + (web.Event event) { + _gamepadCount++; + if (_gamepadCount == 1) { + // The game pad state for web is not event driven. We need to + // query the game pad state by ourself. + // By default we set the query interval is 1ms. + _gamepadPollingTimer = + Timer.periodic(const Duration(milliseconds: 1), (timer) { + updateGamepadsStatus(); + }); + } + }.toJS, + ); + + web.window.addEventListener( + 'gamepaddisconnected', + (web.Event event) { + _gamepadCount--; + if (_gamepadCount == 0) { + _gamepadPollingTimer?.cancel(); + } + }.toJS, + ); + } + + static void registerWith(Registrar registrar) { + GamepadsPlatformInterface.instance = GamepadsWeb(); + } + + List? controllers; + + @override + Future> listGamepads() async { + controllers = getGamepads(this); + return controllers!; + } + + void emitGamepadEvent(GamepadEvent event) { + _gamepadEventsStreamController.add(event); + } + + final StreamController _gamepadEventsStreamController = + StreamController.broadcast(); + + @override + Stream get gamepadEventsStream => + _gamepadEventsStreamController.stream; + + @mustCallSuper + Future dispose() async { + _gamepadEventsStreamController.close(); + } +} diff --git a/packages/gamepads_web/pubspec.yaml b/packages/gamepads_web/pubspec.yaml new file mode 100644 index 0000000..a83b634 --- /dev/null +++ b/packages/gamepads_web/pubspec.yaml @@ -0,0 +1,32 @@ +name: gamepads_web +description: Web implementation of gamepads, a Flutter plugin to handle gamepad input across multiple platforms. +version: 0.1.0 +homepage: https://github.com/flame-engine/gamepads +repository: https://github.com/flame-engine/gamepads/tree/main/packages/gamepads_web + +flutter: + plugin: + implements: gamepads + platforms: + web: + pluginClass: GamepadsWeb + fileName: gamepads_web.dart + +environment: + sdk: '>=2.19.0 <3.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + gamepads_platform_interface: ^0.1.2+1 + js_interop: ^0.0.1 + plugin_platform_interface: ^2.1.8 + web: ^1.1.0 + +dev_dependencies: + flame_lint: ^0.2.0 + flutter_test: + sdk: flutter