Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Split Screen Demo showing input handling #1023

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]