Skip to content

Commit

Permalink
Add Split Screen Demo showing input handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Sauermann committed Mar 26, 2024
1 parent 71eea49 commit 93dc327
Show file tree
Hide file tree
Showing 12 changed files with 362 additions and 0 deletions.
17 changes: 17 additions & 0 deletions viewport/split_screen_input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Split Screen Input

A demo showing a Split Screen GUI and input handling for local multiplayer using viewports.

It demonstrates:
- Single World2D, that is shared among many Viewports
- Simplified Input Map, that uses the same Actions for all Split Screens
- Input event routing to different viewports based on joypad device id and dedicated keyboard keys
- Dynamic keybinding adjustment for each Split Screen

Language: GDScript

Renderer: Compatibility

## Screenshots

![Screenshot](screenshots/split_screen_input.webp)
1 change: 1 addition & 0 deletions viewport/split_screen_input/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions viewport/split_screen_input/icon.svg.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://ci5b7o7h2bmj0"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
28 changes: 28 additions & 0 deletions viewport/split_screen_input/player.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class_name Player
extends CharacterBody2D
## Player implementation.

const factor: float = 200.0 # Factor to multiply the movement.

var _movement: Vector2 = Vector2(0, 0) # Current movement rate of node.


# Update movement variable based on input that reaches this SubViewport.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ux_up") or event.is_action_released("ux_down"):
_movement.y -= 1
get_viewport().set_input_as_handled()
elif event.is_action_pressed("ux_down") or event.is_action_released("ux_up"):
_movement.y += 1
get_viewport().set_input_as_handled()
elif event.is_action_pressed("ux_left") or event.is_action_released("ux_right"):
_movement.x -= 1
get_viewport().set_input_as_handled()
elif event.is_action_pressed("ux_right") or event.is_action_released("ux_left"):
_movement.x += 1
get_viewport().set_input_as_handled()


# Move the node based on the content of the movement variable.
func _physics_process(delta: float) -> void:
move_and_collide(_movement * factor * delta)
85 changes: 85 additions & 0 deletions viewport/split_screen_input/project.godot
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters

config_version=5

[application]

config/name="Split Screen Input"
run/main_scene="res://split_screen_demo.tscn"
config/features=PackedStringArray("4.2")
config/icon="res://icon.svg"

[display]

window/size/viewport_width=900
window/size/viewport_height=900

[input]

ui_left={
"deadzone": 0.2,
"events": []
}
ui_right={
"deadzone": 0.2,
"events": []
}
ui_up={
"deadzone": 0.2,
"events": []
}
ui_down={
"deadzone": 0.2,
"events": []
}
ux_left={
"deadzone": 0.2,
"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194442,"key_label":0,"unicode":52,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
]
}
ux_right={
"deadzone": 0.2,
"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":76,"key_label":0,"unicode":108,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194444,"key_label":0,"unicode":54,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
]
}
ux_up={
"deadzone": 0.2,
"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":105,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194446,"key_label":0,"unicode":56,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
]
}
ux_down={
"deadzone": 0.2,
"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":107,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194443,"key_label":0,"unicode":53,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
]
}

[rendering]

renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
42 changes: 42 additions & 0 deletions viewport/split_screen_input/root.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
extends Node
## Set up different Split Screens
#
## Provide Input configuration
## Connect Split Screens to Play Area


const keyboard_options: Dictionary = {
"wasd": {"keys": [KEY_W, KEY_A, KEY_S, KEY_D]},
"ijkl": {"keys": [KEY_I, KEY_J, KEY_K, KEY_L]},
"arrows": {"keys": [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]},
"numpad": {"keys": [KEY_KP_4, KEY_KP_5, KEY_KP_6, KEY_KP_8]},
} # 4 keyboard sets for moving players around.

const player_colors: Array[Color] = [Color.WHITE, Color("ff8f02"), Color("05ff5a"), Color("ff05a0")] # Modulate Colors of each Player.


var config: Dictionary = {
"keyboard": keyboard_options,
"joypads": 4,
"world": null,
"position": Vector2(),
"index": -1,
"color": Color(),
} # Split Screen configuration Dictionary.

@onready var play_area: SubViewport = $PlayArea # The central Viewport, all Split Screens are sharing.


# Initialize each Split Screen and each player node.
func _ready() -> void:
config["world"] = play_area.world_2d
var c: Array[Node] = get_children()
var i = 0
for n: Node in c:
if n is SplitScreen:
config["position"] = Vector2(i % 2, floor(i / 2.0)) * 132 + Vector2(132, 0)
config["index"] = i
config["color"] = player_colors[i]
var s: SplitScreen = n as SplitScreen
s.set_config(config)
i += 1
Empty file.
Binary file not shown.
40 changes: 40 additions & 0 deletions viewport/split_screen_input/split_screen.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class_name SplitScreen
extends Node
## Interface for a SplitScreen


const keypad_string:String = "Joypad" # Prefix for joypads.

@export var init_position: Vector2

var _keyboard_options: Dictionary # Copy of all keyboard options.

@onready var opt: OptionButton = $OptionButton
@onready var v: SubViewport = $SubViewportContainer/SubViewport
@onready var svc: MySV = $SubViewportContainer
@onready var play: Player = $SubViewportContainer/SubViewport/Player


# Set the configuration of this split screen and perform OptionButton initialization.
func set_config(c: Dictionary):
_keyboard_options = c["keyboard"]
play.position = c["position"]
var local_index = c["index"]
play.modulate = c["color"]
opt.clear()
for k in _keyboard_options:
opt.add_item(k)
for i in c["joypads"]:
opt.add_item("%s %s" % [keypad_string, i+1])
opt.select(local_index)
_on_option_button_item_selected(local_index)
v.world_2d = c["world"] # Connect all Split Screens to the same World2D.


# Update Keyboard Settings after selecting them in the OptionButton.
func _on_option_button_item_selected(index: int) -> void:
var txt: String = opt.get_item_text(index)
if txt.begins_with(keypad_string):
svc.set_input_config({"joypad": txt.substr(txt.length()-1, -1).to_int(), "keyboard": []})
else:
svc.set_input_config({"keyboard": _keyboard_options[txt]["keys"], "joypad": -1})
42 changes: 42 additions & 0 deletions viewport/split_screen_input/split_screen.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[gd_scene load_steps=6 format=3 uid="uid://dqailbm8vcpf5"]

[ext_resource type="Script" path="res://split_screen.gd" id="1_4fp0b"]
[ext_resource type="Script" path="res://sub_viewport_container.gd" id="2_v8t84"]
[ext_resource type="Texture2D" uid="uid://ci5b7o7h2bmj0" path="res://icon.svg" id="4_787wn"]
[ext_resource type="Script" path="res://player.gd" id="5_1qhfw"]

[sub_resource type="RectangleShape2D" id="RectangleShape2D_m48mh"]
size = Vector2(128, 128)

[node name="Split" type="VBoxContainer"]
auto_translate_mode = 1
offset_right = 350.0
offset_bottom = 374.0
script = ExtResource("1_4fp0b")

[node name="OptionButton" type="OptionButton" parent="."]
auto_translate_mode = 1
layout_mode = 2

[node name="SubViewportContainer" type="SubViewportContainer" parent="."]
auto_translate_mode = 1
layout_mode = 2
script = ExtResource("2_v8t84")

[node name="SubViewport" type="SubViewport" parent="SubViewportContainer"]
handle_input_locally = false
size = Vector2i(350, 350)
render_target_update_mode = 4

[node name="Player" type="CharacterBody2D" parent="SubViewportContainer/SubViewport"]
script = ExtResource("5_1qhfw")

[node name="CollisionShape2D" type="CollisionShape2D" parent="SubViewportContainer/SubViewport/Player"]
shape = SubResource("RectangleShape2D_m48mh")

[node name="Sprite2D" type="Sprite2D" parent="SubViewportContainer/SubViewport/Player"]
texture = ExtResource("4_787wn")

[node name="Camera2D" type="Camera2D" parent="SubViewportContainer/SubViewport/Player"]

[connection signal="item_selected" from="OptionButton" to="." method="_on_option_button_item_selected"]
42 changes: 42 additions & 0 deletions viewport/split_screen_input/split_screen_demo.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[gd_scene load_steps=3 format=3 uid="uid://ccutmhshaoqih"]

[ext_resource type="Script" path="res://root.gd" id="1_2itit"]
[ext_resource type="PackedScene" uid="uid://dqailbm8vcpf5" path="res://split_screen.tscn" id="1_mcbdt"]

[node name="Node" type="Node"]
script = ExtResource("1_2itit")

[node name="Panel" type="Panel" parent="."]
offset_right = 900.0
offset_bottom = 900.0
mouse_filter = 2

[node name="SplitScreen1" parent="." instance=ExtResource("1_mcbdt")]
offset_left = 25.0
offset_top = 25.0
offset_right = 375.0
offset_bottom = 399.0

[node name="SplitScreen2" parent="." instance=ExtResource("1_mcbdt")]
offset_left = 425.0
offset_top = 25.0
offset_right = 775.0
offset_bottom = 399.0
init_position = Vector2(132, 0)

[node name="SplitScreen3" parent="." instance=ExtResource("1_mcbdt")]
offset_left = 25.0
offset_top = 425.0
offset_right = 375.0
offset_bottom = 799.0
init_position = Vector2(0, 132)

[node name="SplitScreen4" parent="." instance=ExtResource("1_mcbdt")]
offset_left = 425.0
offset_top = 425.0
offset_right = 775.0
offset_bottom = 799.0
init_position = Vector2(132, 132)

[node name="PlayArea" type="SubViewport" parent="."]
render_target_update_mode = 4
28 changes: 28 additions & 0 deletions viewport/split_screen_input/sub_viewport_container.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class_name MySV
extends SubViewportContainer
## Input Routing for different SubViewports.
#
## Based on the provided input configuration, ensures only the correct
## events reaching the SubViewport-


var _current_keyboard_set: Array = [] # Currently used keyboard set.
var _current_joypad_device: int = -1 # Currently used joypad device id.


# Make sure, that only the events are sent to the SubViewport,
# that are allowed via the OptionButton selection.
func _propagate_input_event(event: InputEvent) -> bool:
if event is InputEventKey:
if _current_keyboard_set.has(event.keycode):
return true
elif event is InputEventJoypadButton:
if _current_joypad_device > -1 and event.device == _current_joypad_device:
return true
return false


# Set new config for input handling.
func set_input_config(config: Dictionary):
_current_keyboard_set = config["keyboard"]
_current_joypad_device = config["joypad"]

0 comments on commit 93dc327

Please sign in to comment.