Merge pull request #173 from FunkinCrew/rewrite/feature/precise-inputs-gamepad

Precise inputs on gamepad
This commit is contained in:
Cameron Taylor 2023-10-16 21:38:38 -04:00 committed by GitHub
commit 160fe77bbc
8 changed files with 305 additions and 19 deletions

View file

@ -104,7 +104,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "f195121ebec688b417e38ab115185c8d93c349d3",
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
"url": "https://github.com/EliteMasterEric/lime"
},
{
@ -139,7 +139,7 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1",
"ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
"url": "https://github.com/EliteMasterEric/openfl"
},
{

View file

@ -1,5 +1,7 @@
package funkin;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags;
import flixel.FlxObject;
import flixel.input.FlxInput;
@ -832,6 +834,14 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
public function getGamepadIds():Array<Int> {
return gamepadsAdded;
}
public function getGamepads():Array<FlxGamepad> {
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
}
inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
{
gamepadsAdded.push(id);

View file

@ -77,6 +77,11 @@ class PlayerSettings
this.id = id;
this.controls = new Controls('player$id', None);
addKeyboard();
}
function addKeyboard():Void
{
var useDefault = true;
if (Save.get().hasControls(id, Keys))
{
@ -96,7 +101,6 @@ class PlayerSettings
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
@ -124,6 +128,7 @@ class PlayerSettings
trace("Loading gamepad control scheme");
controls.addDefaultGamepad(gamepad.id);
}
PreciseInputManager.instance.initializeButtons(controls, gamepad);
}
/**

View file

@ -1,18 +1,25 @@
package funkin.input;
import openfl.ui.Keyboard;
import funkin.play.notes.NoteDirection;
import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import openfl.events.KeyboardEvent;
import flixel.FlxG;
import flixel.input.FlxInput;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager;
import flixel.input.gamepad.FlxGamepad;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.play.notes.NoteDirection;
import funkin.util.FlxGamepadUtil;
import haxe.Int64;
import lime.ui.Gamepad as LimeGamepad;
import lime.ui.GamepadAxis as LimeGamepadAxis;
import lime.ui.GamepadButton as LimeGamepadButton;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
import openfl.events.KeyboardEvent;
import openfl.ui.Keyboard;
/**
* A precise input manager that:
@ -43,6 +50,20 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
*/
var _keyListDir:Map<FlxKey, NoteDirection>;
/**
* A FlxGamepadID->Array<FlxGamepadInputID>, with FlxGamepadInputID being the counterpart to FlxKey.
*/
var _buttonList:Map<Int, Array<FlxGamepadInputID>>;
var _buttonListArray:Array<FlxInput<FlxGamepadInputID>>;
var _buttonListMap:Map<Int, Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>>;
/**
* A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey.
*/
var _buttonListDir:Map<Int, Map<FlxGamepadInputID, NoteDirection>>;
/**
* The timestamp at which a given note direction was last pressed.
*/
@ -53,17 +74,32 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
*/
var _dirReleaseTimestamps:Map<NoteDirection, Int64>;
var _deviceBinds:Map<FlxGamepad,
{
onButtonDown:LimeGamepadButton->Int64->Void,
onButtonUp:LimeGamepadButton->Int64->Void
}>;
public function new()
{
super(PreciseInputList.new);
_deviceBinds = [];
_keyList = [];
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
// _keyListMap
// _keyListArray
_keyListDir = new Map<FlxKey, NoteDirection>();
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
_buttonList = [];
_buttonListMap = [];
_buttonListArray = [];
_buttonListDir = new Map<Int, Map<FlxGamepadInputID, NoteDirection>>();
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
// Keyboard
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp);
@ -84,6 +120,17 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
};
}
public static function getButtonsForDirection(controls:Controls, noteDirection:NoteDirection)
{
return switch (noteDirection)
{
case NoteDirection.LEFT: controls.getButtonsForAction(NOTE_LEFT);
case NoteDirection.DOWN: controls.getButtonsForAction(NOTE_DOWN);
case NoteDirection.UP: controls.getButtonsForAction(NOTE_UP);
case NoteDirection.RIGHT: controls.getButtonsForAction(NOTE_RIGHT);
};
}
/**
* Convert from int to Int64.
*/
@ -138,6 +185,43 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
}
}
public function initializeButtons(controls:Controls, gamepad:FlxGamepad):Void
{
clearButtons();
var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
var callbacks =
{
onButtonDown: handleButtonDown.bind(gamepad),
onButtonUp: handleButtonUp.bind(gamepad)
};
limeGamepad.onButtonDownPrecise.add(callbacks.onButtonDown);
limeGamepad.onButtonUpPrecise.add(callbacks.onButtonUp);
for (noteDirection in DIRECTIONS)
{
var buttons = getButtonsForDirection(controls, noteDirection);
for (button in buttons)
{
var input = new FlxInput<FlxGamepadInputID>(button);
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null) _buttonList.set(gamepad.id, buttonListEntry = []);
buttonListEntry.push(button);
_buttonListArray.push(input);
var buttonListMapEntry = _buttonListMap.get(gamepad.id);
if (buttonListMapEntry == null) _buttonListMap.set(gamepad.id, buttonListMapEntry = new Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>());
buttonListMapEntry.set(button, input);
var buttonListDirEntry = _buttonListDir.get(gamepad.id);
if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map<FlxGamepadInputID, NoteDirection>());
buttonListDirEntry.set(button, noteDirection);
}
}
}
/**
* Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check.
@ -165,11 +249,41 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
return _keyListMap.get(key);
}
public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
{
return _buttonListMap.get(gamepad.id).get(button);
}
public function getDirectionForKey(key:FlxKey):NoteDirection
{
return _keyListDir.get(key);
}
public function getDirectionForButton(gamepad:FlxGamepad, button:FlxGamepadInputID):NoteDirection
{
return _buttonListDir.get(gamepad.id).get(button);
}
function getButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
{
return _buttonListMap.get(gamepad.id).get(button);
}
function updateButtonStates(gamepad:FlxGamepad, button:FlxGamepadInputID, down:Bool):Void
{
var input = getButton(gamepad, button);
if (input == null) return;
if (down)
{
input.press();
}
else
{
input.release();
}
}
function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
@ -198,7 +312,7 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateKeyStates(key, false);
@ -214,6 +328,54 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
}
}
function handleButtonDown(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
{
var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateButtonStates(gamepad, buttonId, true);
if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
{
onInputPressed.dispatch(
{
noteDirection: getDirectionForButton(gamepad, buttonId),
timestamp: timestamp
});
_dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
}
}
function handleButtonUp(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
{
var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateButtonStates(gamepad, buttonId, false);
if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
{
onInputReleased.dispatch(
{
noteDirection: getDirectionForButton(gamepad, buttonId),
timestamp: timestamp
});
_dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
}
}
static function convertKeyCode(input:KeyCode):FlxKey
{
@:privateAccess
@ -228,6 +390,31 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
_keyListMap.clear();
_keyListDir.clear();
}
function clearButtons():Void
{
_buttonListArray = [];
_buttonListDir.clear();
for (gamepad in _deviceBinds.keys())
{
var callbacks = _deviceBinds.get(gamepad);
var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown);
limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp);
}
_deviceBinds.clear();
}
public override function destroy():Void
{
// Keyboard
FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown);
FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp);
clearKeys();
clearButtons();
}
}
class PreciseInputList extends FlxKeyList

View file

@ -920,7 +920,6 @@ class PlayState extends MusicBeatSubState
}
// Handle keybinds.
// if (!isInCutscene && !disableKeys) keyShit(true);
processInputQueue();
if (!isInCutscene && !disableKeys) debugKeyShit();
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);

View file

@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
function onSelect():Void
{
keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
switch (currentDevice)
{
case Keys:
{
keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
}
case Gamepad(id):
{
buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID();
}
}
controlGrid.enabled = false;
canExit = false;
@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
}
var keyUsedToEnterPrompt:Null<Int> = null;
var buttonUsedToEnterPrompt:Null<Int> = null;
override function update(elapsed:Float):Void
{
@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
case Gamepad(id):
{
var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
if (button != NONE && button != keyUsedToEnterPrompt)
if (button != NONE && button != buttonUsedToEnterPrompt)
{
if (button != BACK) onInputSelect(button);
closePrompt();
}
var key = FlxG.keys.firstJustReleased();
if (key != NONE && key != keyUsedToEnterPrompt)
{
if (key == ESCAPE)
{
closePrompt();
}
else if (key == BACKSPACE)
{
onInputSelect(NONE);
closePrompt();
}
}
}
}
}
var keyJustReleased:Int = FlxG.keys.firstJustReleased();
if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
switch (currentDevice)
{
keyUsedToEnterPrompt = null;
case Keys:
{
var keyJustReleased:Int = FlxG.keys.firstJustReleased();
if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
{
keyUsedToEnterPrompt = null;
}
buttonUsedToEnterPrompt = null;
}
case Gamepad(id):
{
var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID();
if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt)
{
buttonUsedToEnterPrompt = null;
}
keyUsedToEnterPrompt = null;
}
}
}

View file

@ -122,7 +122,7 @@ class StickerSubState extends MusicBeatSubState
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
if (ind == grpStickers.members.length - 1)
if (grpStickers == null || ind == grpStickers.members.length - 1)
{
switchingState = false;
close();

View file

@ -0,0 +1,44 @@
package funkin.util;
import flixel.input.gamepad.FlxGamepad;
import flixel.input.gamepad.FlxGamepadInputID;
import lime.ui.Gamepad as LimeGamepad;
import lime.ui.GamepadAxis as LimeGamepadAxis;
import lime.ui.GamepadButton as LimeGamepadButton;
class FlxGamepadUtil
{
public static function getInputID(gamepad:FlxGamepad, button:LimeGamepadButton):FlxGamepadInputID
{
#if FLX_GAMEINPUT_API
// FLX_GAMEINPUT_API internally assigns 6 axes to IDs 0-5, which LimeGamepadButton doesn't account for, so we need to offset the ID by 6.
final OFFSET:Int = 6;
#else
final OFFSET:Int = 0;
#end
var result:FlxGamepadInputID = gamepad.mapping.getID(button + OFFSET);
if (result == NONE) return NONE;
return result;
}
public static function getLimeGamepad(input:FlxGamepad):Null<LimeGamepad>
{
#if FLX_GAMEINPUT_API @:privateAccess
return input._device.getLimeGamepad();
#else
return null;
#end
}
@:privateAccess
public static function getFlxGamepadByLimeGamepad(gamepad:LimeGamepad):FlxGamepad
{
// Why is this so elaborate?
@:privateAccess
var gameInputDevice:openfl.ui.GameInputDevice = openfl.ui.GameInput.__getDevice(gamepad);
@:privateAccess
var gamepadIndex:Int = FlxG.gamepads.findGamepadIndex(gameInputDevice);
return FlxG.gamepads.getByID(gamepadIndex);
}
}