Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
matjlars committed Oct 10, 2022
0 parents commit eea8fe3
Show file tree
Hide file tree
Showing 16 changed files with 546 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Godot 4+ specific ignores
.godot/
47 changes: 47 additions & 0 deletions addons/multiplayer_input/device_input.gd
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)
164 changes: 164 additions & 0 deletions addons/multiplayer_input/multiplayer_input.gd
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
7 changes: 7 additions & 0 deletions addons/multiplayer_input/plugin.cfg
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"
10 changes: 10 additions & 0 deletions addons/multiplayer_input/plugin.gd
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)
42 changes: 42 additions & 0 deletions demo/demo.gd
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)
26 changes: 26 additions & 0 deletions demo/demo.tscn
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."
29 changes: 29 additions & 0 deletions demo/demo_player.gd
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)
19 changes: 19 additions & 0 deletions demo/demo_player.tscn
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
Loading

0 comments on commit eea8fe3

Please sign in to comment.