Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for Web #48

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/gamepads/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ environment:
dependencies:
flutter:
sdk: flutter
gamepads: ^0.1.4
gamepads: ^0.1.5
zhuhaichao518 marked this conversation as resolved.
Show resolved Hide resolved

dev_dependencies:
flame_lint: ^0.2.0
Expand Down
5 changes: 4 additions & 1 deletion packages/gamepads/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: gamepads
description: A Flutter plugin to handle gamepad input across multiple platforms.
version: 0.1.4
version: 0.1.5
homepage: https://github.com/flame-engine/gamepads
repository: https://github.com/flame-engine/gamepads/tree/main/packages/gamepads

Expand All @@ -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'
Expand All @@ -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:
Expand Down
29 changes: 29 additions & 0 deletions packages/gamepads_web/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
3 changes: 3 additions & 0 deletions packages/gamepads_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Bump "gamepads_web" to `0.1.0`.
zhuhaichao518 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/gamepads_web/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../LICENSE
1 change: 1 addition & 0 deletions packages/gamepads_web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../README.md
1 change: 1 addition & 0 deletions packages/gamepads_web/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:flame_lint/analysis_options.yaml
65 changes: 65 additions & 0 deletions packages/gamepads_web/lib/gamepad_detector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'dart:js_interop';

import 'package:gamepads_platform_interface/api/gamepad_controller.dart';
import 'package:gamepads_platform_interface/gamepads_platform_interface.dart';

@JS('navigator')
external Navigator get navigator;

@JS()
@staticInterop
class Navigator {}

@JS()
@staticInterop
class JSArray {}

@JS()
@staticInterop
class Gamepad {}

extension NavigatorGamepads on Navigator {
external JSArray getGamepads();
}

// Extension for JSArray to handle array access
extension JSArrayInterop on JSArray {
external int get length;
external JSAny? operator [](int index);
}

// Extension for Gamepad to access its properties
extension GamepadInterop on Gamepad {
external int get index;
external String get id;
}

List<GamepadController> getGamepads(GamepadsPlatformInterface plugin) {
final controllers = <GamepadController>[];
final gamepads = navigator.getGamepads();
zhuhaichao518 marked this conversation as resolved.
Show resolved Hide resolved

for (var i = 0; i < gamepads.length; i++) {
final gamepad = gamepads[i] as Gamepad?;
if (gamepad != null) {
controllers.add(
GamepadController(
id: gamepad.index.toString(),
name: gamepad.id,
plugin: plugin,
),
);
}
}
return controllers;
}

List<dynamic> getGamepadList() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the list be typed here?

Copy link
Author

@zhuhaichao518 zhuhaichao518 Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did't get it well. Does the latest implementation look fine?

final gamepads = navigator.getGamepads();
return List.generate(gamepads.length, (i) {
final gamepad = gamepads[i];
if (gamepad != null) {
return gamepad;
}
return null;
}).where((gamepad) => gamepad != null).toList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final gamepads = navigator.getGamepads();
return List.generate(gamepads.length, (i) {
final gamepad = gamepads[i];
if (gamepad != null) {
return gamepad;
}
return null;
}).where((gamepad) => gamepad != null).toList();
return navigator.getGamepads().toDart.whereNotNull();

Shouldn't this be enough?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did't notice that I can use gamepad in package:web. Updated the implementation.

}
137 changes: 137 additions & 0 deletions packages/gamepads_web/lib/gamepads_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// ignore_for_file: avoid_dynamic_calls, omit_local_variable_types

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 {
zhuhaichao518 marked this conversation as resolved.
Show resolved Hide resolved
GamePadState(int length) {
keyStates = List<dynamic>.filled(length, null);
axesStates = List<dynamic>.filled(4, null);
}

List<dynamic>? keyStates;
List<dynamic>? axesStates;
}

/// A web implementation of the GamepadsWebPlatform of the GamepadsWeb plugin.
class GamepadsWeb extends GamepadsPlatformInterface {
int _gamepadCount = 0;
Timer? _gamepadPollingTimer;

Map<String, GamePadState> lastGamePadstates = {};

void updateGamepadsStatus() {
final gamepads = getGamepadList();
for (int i = 0; i < gamepads.length; i++) {
final gamepad = gamepads[i];
if (gamepad != null) {
zhuhaichao518 marked this conversation as resolved.
Show resolved Hide resolved
final int buttoncount = gamepad.buttons.length;
final String gamepadId = gamepad.index.toString();
GamePadState lastState;
if (lastGamePadstates.containsKey(gamepadId) &&
lastGamePadstates[gamepadId]?.keyStates?.length == buttoncount) {
lastState = lastGamePadstates[gamepadId]!;
} else {
lastGamePadstates[gamepadId] = GamePadState(buttoncount);
lastState = lastGamePadstates[gamepadId]!;
}
for (int i = 0; i < buttoncount; i++) {
if (lastState.keyStates?[i] != gamepad.buttons[i].value) {
lastState.keyStates?[i] = gamepad.buttons[i].value;
emitGamepadEvent(
GamepadEvent(
gamepadId: gamepadId,
timestamp: DateTime.now().millisecondsSinceEpoch,
type: KeyType.button,
key: 'button $i',
value: gamepad.buttons[i].value,
),
);
}
}
for (int i = 0; i < 4; i++) {
if (lastState.keyStates?[i] != gamepad.axes[i]) {
if (gamepad.axes[i] > 0.1 || gamepad.axes[i] < -0.1) {
lastState.axesStates?[i] = gamepad.axes[i];
emitGamepadEvent(
GamepadEvent(
gamepadId: gamepadId,
timestamp: DateTime.now().millisecondsSinceEpoch,
type: KeyType.analog,
key: 'analog $i',
value: gamepad.axes[i],
),
);
}
}
}
}
}
}

/// 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<GamepadController>? controllers;

@override
Future<List<GamepadController>> listGamepads() async {

Check notice on line 117 in packages/gamepads_web/lib/gamepads_web.dart

View workflow job for this annotation

GitHub Actions / analyze

The method doesn't override an inherited method.

Try updating this class to match the superclass, or removing the override annotation. See https://dart.dev/diagnostics/override_on_non_overriding_member to learn more about this problem.
controllers = getGamepads(this);
return controllers!;
}

void emitGamepadEvent(GamepadEvent event) {
_gamepadEventsStreamController.add(event);
}

final StreamController<GamepadEvent> _gamepadEventsStreamController =
StreamController<GamepadEvent>.broadcast();

@override
Stream<GamepadEvent> get gamepadEventsStream =>

Check notice on line 130 in packages/gamepads_web/lib/gamepads_web.dart

View workflow job for this annotation

GitHub Actions / analyze

The getter doesn't override an inherited getter.

Try updating this class to match the superclass, or removing the override annotation. See https://dart.dev/diagnostics/override_on_non_overriding_member to learn more about this problem.
_gamepadEventsStreamController.stream;

@mustCallSuper
Future<void> dispose() async {
_gamepadEventsStreamController.close();
}
}
32 changes: 32 additions & 0 deletions packages/gamepads_web/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading