-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit eea8fe3
Showing
16 changed files
with
546 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Normalize EOL for all files that Git considers text files. | ||
* text=auto eol=lf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Godot 4+ specific ignores | ||
.godot/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
extends RefCounted | ||
class_name DeviceInput | ||
|
||
# this is an optional helper class that just wraps the calls to MultiplayerInput | ||
# so you can use this to keep track of the device and then you don't have to pass that around | ||
# the following is a simple example of how to use this class | ||
# | ||
# var input | ||
# func _ready(): | ||
# input = DeviceInput.new(device) | ||
# func _process(delta): | ||
# if input.is_action_just_pressed("jump"): | ||
# jump() | ||
|
||
# if this is -1, then this is the keyboard player | ||
# otherwise, it's the "device" used in the Input class functions. | ||
var device: int | ||
|
||
func _init(device_num: int): | ||
device = device_num | ||
|
||
func is_keyboard() -> bool: | ||
return device < 0 | ||
|
||
func is_joypad() -> bool: | ||
return device >= 0 | ||
|
||
func get_action_raw_strength(action: StringName, exact_match: bool = false) -> float: | ||
return MultiplayerInput.get_action_raw_strength(device, action, exact_match) | ||
|
||
func get_action_strength(action: StringName, exact_match: bool = false) -> float: | ||
return MultiplayerInput.get_action_strength(device, action, exact_match) | ||
|
||
func get_axis(negative_action: StringName, positive_action: StringName) -> float: | ||
return MultiplayerInput.get_axis(device, negative_action, positive_action) | ||
|
||
func get_vector(negative_x: StringName, positive_x: StringName, negative_y: StringName, positive_y: StringName, deadzone: float = -1.0) -> Vector2: | ||
return MultiplayerInput.get_vector(device, negative_x, positive_x, negative_y, positive_y, deadzone) | ||
|
||
func is_action_just_pressed(action: StringName, exact_match: bool = false) -> bool: | ||
return MultiplayerInput.is_action_just_pressed(device, action, exact_match) | ||
|
||
func is_action_just_released(action: StringName, exact_match: bool = false) -> bool: | ||
return MultiplayerInput.is_action_just_released(device, action, exact_match) | ||
|
||
func is_action_pressed(action: StringName, exact_match: bool = false) -> bool: | ||
return MultiplayerInput.is_action_pressed(device, action, exact_match) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
extends Node | ||
|
||
# This is an autoloaded class that can be accessed at MultiplayerInput | ||
# device of (-1) means the keyboard player | ||
# when a device connects, actions are created for that device | ||
# these dynamically created action names start with the device number | ||
# | ||
# this is an example of how to allow players to join in your PlayerManager | ||
# | ||
# signal player_joined(device) | ||
# func is_device_joined(device: int) -> bool: | ||
# pass # implement this for your game. return true if there are any players with this device. | ||
# func unjoined_devices(): | ||
# var valid_devices = Input.get_connected_joypads() | ||
# valid_devices.append(-1) # also consider the keyboard player (device -1 for MultiplayerInput functions) | ||
# return valid_devices.filter(func(device): return is_device_joined(device)) | ||
# func _process(_delta): | ||
# for device in unjoined_devices(): | ||
# if MultiplayerInput.is_action_just_pressed(device, "join"): | ||
# player_joined.emit(device) | ||
|
||
# an array of all the non-duplicated action names | ||
var core_actions = [] | ||
|
||
# a dictionary of all action names | ||
# the keys are the device numbers | ||
# the values are a dictionary that maps action name to device action name | ||
# for example device_actions[device][action_name] is the device-specific action name | ||
# the purpose of this is to cache all the StringNames of all the actions | ||
# ... so it doesn't need to generate them every time | ||
var device_actions = {} | ||
|
||
func _init(): | ||
reset() | ||
|
||
# call this if you change any of the core actions or need to reset everything | ||
func reset(): | ||
InputMap.load_from_project_settings() | ||
core_actions = InputMap.get_actions() | ||
|
||
# disable joypad events on keyboard actions | ||
# by setting device id to 8 (out of range, so they'll never trigger) | ||
# I can't just delete them because they're used as blueprints | ||
# ... when a joypad connects | ||
for action in core_actions: | ||
for e in InputMap.action_get_events(action): | ||
if _is_joypad_event(e): | ||
e.device = 8 | ||
|
||
# create actions for already connected gamepads | ||
for device in Input.get_connected_joypads(): | ||
_create_actions_for_device(device) | ||
|
||
# create actions for gamepads that connect in the future | ||
# also clean up when gamepads disconnect | ||
if !Input.joy_connection_changed.is_connected(_on_joy_connection_changed): | ||
Input.joy_connection_changed.connect(_on_joy_connection_changed) | ||
|
||
func _on_joy_connection_changed(device: int, connected: bool): | ||
if connected: | ||
_create_actions_for_device(device) | ||
else: | ||
_delete_actions_for_device(device) | ||
|
||
func _create_actions_for_device(device: int): | ||
device_actions[device] = {} | ||
for core_action in core_actions: | ||
var new_action = "%s%s" % [device, core_action] | ||
var deadzone = InputMap.action_get_deadzone(core_action) | ||
|
||
# get all joypad events for this action | ||
var events = InputMap.action_get_events(core_action).filter(_is_joypad_event) | ||
|
||
# only copy this event if it is relevant to joypads | ||
if events.size() > 0: | ||
# first add the action with the new name | ||
InputMap.add_action(new_action, deadzone) | ||
device_actions[device][core_action] = new_action | ||
|
||
# then copy all the events associated with that action | ||
# this only includes events that are relevant to joypads | ||
for event in events: | ||
# without duplicating, all of them have a reference to the same event object | ||
# which doesn't work because this has to be unique to this device | ||
var new_event = event.duplicate() | ||
new_event.device = device | ||
|
||
# switch the device to be just this joypad | ||
InputMap.action_add_event(new_action, new_event) | ||
|
||
func _delete_actions_for_device(device: int): | ||
device_actions.erase(device) | ||
var actions_to_erase = [] | ||
var device_num_str = str(device) | ||
|
||
# figure out which actions should be erased | ||
for action in InputMap.get_actions(): | ||
var action_str = String(action) | ||
var maybe_device = action_str.substr(0, device_num_str.length()) | ||
if maybe_device == device_num_str: | ||
actions_to_erase = action | ||
|
||
# now actually erase them | ||
# this is done separately so I'm not erasing from the collection I'm looping on | ||
# not sure if this is necessary but whatever, this is safe | ||
for action in actions_to_erase: | ||
InputMap.erase_action(action) | ||
|
||
|
||
|
||
# use these functions to query the action states just like normal Input functions | ||
|
||
func get_action_raw_strength(device: int, action: StringName, exact_match: bool = false) -> float: | ||
if device >= 0: | ||
action = get_action_name(device, action) | ||
return Input.get_action_raw_strength(action, exact_match) | ||
|
||
func get_action_strength(device: int, action: StringName, exact_match: bool = false) -> float: | ||
if device >= 0: | ||
action = get_action_name(device, action) | ||
return Input.get_action_strength(action, exact_match) | ||
|
||
func get_axis(device: int, negative_action: StringName, positive_action: StringName) -> float: | ||
if device >= 0: | ||
negative_action = get_action_name(device, negative_action) | ||
positive_action = get_action_name(device, positive_action) | ||
return Input.get_axis(negative_action, positive_action) | ||
|
||
func get_vector(device: int, negative_x: StringName, positive_x: StringName, negative_y: StringName, positive_y: StringName, deadzone: float = -1.0) -> Vector2: | ||
if device >= 0: | ||
negative_x = get_action_name(device, negative_x) | ||
positive_x = get_action_name(device, positive_x) | ||
negative_y = get_action_name(device, negative_y) | ||
positive_y = get_action_name(device, positive_y) | ||
return Input.get_vector(negative_x, positive_x, negative_y, positive_y, deadzone) | ||
|
||
func is_action_just_pressed(device: int, action: StringName, exact_match: bool = false) -> bool: | ||
if device >= 0: | ||
action = get_action_name(device, action) | ||
return Input.is_action_just_pressed(action, exact_match) | ||
|
||
func is_action_just_released(device: int, action: StringName, exact_match: bool = false) -> bool: | ||
if device >= 0: | ||
action = get_action_name(device, action) | ||
return Input.is_action_just_released(action, exact_match) | ||
|
||
func is_action_pressed(device: int, action: StringName, exact_match: bool = false) -> bool: | ||
if device >= 0: | ||
action = get_action_name(device, action) | ||
return Input.is_action_pressed(action, exact_match) | ||
|
||
# returns the name of a gamepad-specific action | ||
func get_action_name(device: int, action: StringName) -> StringName: | ||
if device >= 0: | ||
# if it says this dictionary doesn't have the key, | ||
# that could mean it's an invalid action name. | ||
# or it could mean that action doesn't have a joypad event assigned | ||
return device_actions[device][action] | ||
|
||
# return the normal action name for the keyboard player | ||
return action | ||
|
||
func _is_joypad_event(event: InputEvent) -> bool: | ||
return event is InputEventJoypadButton or event is InputEventJoypadMotion |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[plugin] | ||
|
||
name="MultiplayerInput" | ||
description="Provides a clean interface for using your existing Action Map in a multiplayer setting with 1 keyboard player and/or multiple controller players." | ||
author="matjlars" | ||
version="0.1" | ||
script="plugin.gd" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
@tool | ||
extends EditorPlugin | ||
|
||
const AUTOLOAD_NAME = "MultiplayerInput" | ||
|
||
func _enter_tree(): | ||
add_autoload_singleton(AUTOLOAD_NAME, "res://addons/multiplayer_input/multiplayer_input.gd") | ||
|
||
func _exit_tree(): | ||
remove_autoload_singleton(AUTOLOAD_NAME) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
extends Node | ||
|
||
# this is a singleton autoload in my project but for the purposes of this demo, | ||
# this is simpler | ||
@onready var player_manager = $PlayerManager | ||
|
||
# map from player integer to the player node | ||
var player_nodes = {} | ||
|
||
func _ready(): | ||
player_manager.player_joined.connect(spawn_player) | ||
player_manager.player_left.connect(delete_player) | ||
|
||
func _process(_delta): | ||
player_manager.handle_join_input() | ||
|
||
func spawn_player(player: int): | ||
# create the player node | ||
var player_scene = load("res://demo/demo_player.tscn") | ||
var player_node = player_scene.instantiate() | ||
player_node.leave.connect(on_player_leave) | ||
player_nodes[player] = player_node | ||
|
||
# let the player know which device controls it | ||
var device = player_manager.get_player_device(player) | ||
player_node.init(player, device) | ||
|
||
# add the player to the tree | ||
add_child(player_node) | ||
|
||
# random spawn position | ||
player_node.position = Vector2(randf_range(50, 400), randf_range(50, 400)) | ||
|
||
func delete_player(player: int): | ||
player_nodes[player].queue_free() | ||
player_nodes.erase(player) | ||
|
||
func on_player_leave(player: int): | ||
# just let the player manager know this player is leaving | ||
# this will, through the player manager's "player_left" signal, | ||
# indirectly call delete_player because it's connected in this file's _ready() | ||
player_manager.leave(player) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
[gd_scene load_steps=3 format=3 uid="uid://c4qrt5dwy6da8"] | ||
|
||
[ext_resource type="Script" path="res://demo/demo.gd" id="1_bese3"] | ||
[ext_resource type="Script" path="res://demo/player_manager.gd" id="1_u0i6m"] | ||
|
||
[node name="Demo" type="Node"] | ||
script = ExtResource("1_bese3") | ||
|
||
[node name="PlayerManager" type="Node" parent="."] | ||
script = ExtResource("1_u0i6m") | ||
|
||
[node name="HUD" type="Control" parent="."] | ||
layout_mode = 3 | ||
anchors_preset = 0 | ||
offset_right = 40.0 | ||
offset_bottom = 40.0 | ||
|
||
[node name="Instructions" type="Label" parent="HUD"] | ||
layout_mode = 0 | ||
offset_left = 20.0 | ||
offset_top = 22.0 | ||
offset_right = 508.0 | ||
offset_bottom = 100.0 | ||
text = "MultiplayerInput demo | ||
Press the spacebar or the bottom face button to join and leave. | ||
Once joined, use WASD or the left joystick to move around." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
extends Node2D | ||
|
||
signal leave | ||
|
||
var player: int | ||
var input | ||
|
||
# call this function when spawning this player to set up the input object based on the device | ||
func init(player_num: int, device: int): | ||
player = player_num | ||
|
||
# in my project, I got the device integer by accessing the singleton autoload PlayerManager | ||
# but for simplicity, it's not an autoload in this demo. | ||
# but I recommend making it a singleton so you can access the player data from anywhere. | ||
# that would look like the following line, instead of the device function parameter above. | ||
# var device = PlayerManager.get_player_device(player) | ||
input = DeviceInput.new(device) | ||
|
||
$Player.text = "%s" % player_num | ||
|
||
func _process(_delta): | ||
var move = input.get_vector("move_left", "move_right", "move_up", "move_down") | ||
position += move | ||
|
||
# let the player leave by pressing the "join" button | ||
if input.is_action_just_pressed("join"): | ||
# an alternative to this is just call PlayerManager.leave(player) | ||
# but that only works if you set up the PlayerManager singleton | ||
leave.emit(player) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[gd_scene load_steps=3 format=3 uid="uid://bg6vhbq5vbwr7"] | ||
|
||
[ext_resource type="Texture2D" uid="uid://dto4t8eurbbid" path="res://icon.png" id="1_87t6b"] | ||
[ext_resource type="Script" path="res://demo/demo_player.gd" id="1_nm6ov"] | ||
|
||
[node name="DemoPlayer" type="Node2D"] | ||
script = ExtResource("1_nm6ov") | ||
|
||
[node name="Icon" type="Sprite2D" parent="."] | ||
position = Vector2(1, 1) | ||
texture = ExtResource("1_87t6b") | ||
|
||
[node name="Player" type="Label" parent="."] | ||
offset_left = -20.0 | ||
offset_top = -54.0 | ||
offset_right = 20.0 | ||
offset_bottom = -28.0 | ||
text = "9" | ||
horizontal_alignment = 1 |
Oops, something went wrong.