matjlars / godot-multiplayer-input

This Godot addon provides two simple APIs for using normal Input Actions, but spread out across a Keyboard player and up to 8 Joypad players.
MIT License
96 stars 4 forks source link

Assertion error when get_action_name() called before device_actions is populated #8

Open pfarnach opened 3 months ago

pfarnach commented 3 months ago

I have a simple game with two gamepads connected. After following the setup instructions, I was getting an error Assertion failed: Device 0 has no actions. Maybe the joypad is disconnected.. After sticking in some print lines, it appears as though get_action_name is getting called before the device_actions dict has a chance to populate in _create_actions_for_device(). Commenting out the assert(...) line and adding a has() check, like if device_actions.has(device):, is a workaround. It looks like there's a bit of a race condition happening.

This seems related to https://github.com/matjlars/godot-multiplayer-input/issues/6 except that this happens when the game first starts up and the controllers are already connected.

Love the library, thanks for your work on it!

matjlars commented 3 months ago

Thank you for your kind words :)

It seems like Godot is emitting the joy_connection_changed signal after your call into the MultiplayerInput system for some reason.

Do you have a player join/leave system? I'm trying to think through why I don't have this problem in my games and I think it's maybe because I only query for input after the player has already joined, in which case we already know the gamepad is connected. See the the demo player manager's handle_join_input function for what I mean exactly.

If that is not it, here is another thing you could try. It seems like maybe in your case for some reason you need to only get input if the device exists. I can only guess as to why without seeing your code, but perhaps you could try wrapping your input logic inside a check against the is_connected bool on DeviceInput. For example like this:

var input: DeviceInput
func set_device(device: int):
    input = DeviceInput.new(device)

func _process(_delta: float):
    if input.is_connected:
        pass # your input logic here
matjlars commented 3 months ago

Thinking this through more, I think my code example will not work in your case because DeviceInput assumes it's connected when it's created. But in your case this would not be a correct assumption. I realized that I should initialize that variable by checking Input.get_connected_joypads() so I will do that later tonight when I have some time.

pfarnach commented 3 months ago

Yeah I think the difference is in my case I'm reading Input.get_vector() from the instant the game starts up (I'm prototyping right now -- in practice this likely won't happen, but it does seem like a valid use case). I'm guessing the joy_connection_changed signal fires a frame or more after the game starts. Would it be possible to call _create_actions_for_device() both in _init() and after the joy_connection_changed signal fires?

matjlars commented 3 months ago

Great idea! I think that idea will work as long as Input.get_connected_joypads returns the connected devices. I will try that tonight too. My concern is that maybe it will return an empty array before the connection changed signals, which would mean I would have no way of knowing which devices to initialize those actions for. I will give it a try.

tuimz commented 2 weeks ago

In the mean time I have added this locally in the reset() function that seems to solve this issue.

    # Initialize already connected devices
    for id in Input.get_connected_joypads():
        _on_joy_connection_changed(id, true)