diff --git a/viewport/split_screen_input/README.md b/viewport/split_screen_input/README.md new file mode 100644 index 00000000..7c6bd52d --- /dev/null +++ b/viewport/split_screen_input/README.md @@ -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) diff --git a/viewport/split_screen_input/icon.svg b/viewport/split_screen_input/icon.svg new file mode 100644 index 00000000..b370ceb7 --- /dev/null +++ b/viewport/split_screen_input/icon.svg @@ -0,0 +1 @@ + diff --git a/viewport/split_screen_input/icon.svg.import b/viewport/split_screen_input/icon.svg.import new file mode 100644 index 00000000..fee5a484 --- /dev/null +++ b/viewport/split_screen_input/icon.svg.import @@ -0,0 +1,43 @@ +[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/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/viewport/split_screen_input/player.gd b/viewport/split_screen_input/player.gd new file mode 100644 index 00000000..985be5d0 --- /dev/null +++ b/viewport/split_screen_input/player.gd @@ -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) diff --git a/viewport/split_screen_input/player.gd.uid b/viewport/split_screen_input/player.gd.uid new file mode 100644 index 00000000..bcad9705 --- /dev/null +++ b/viewport/split_screen_input/player.gd.uid @@ -0,0 +1 @@ +uid://b7p2tpqjka6jq diff --git a/viewport/split_screen_input/project.godot b/viewport/split_screen_input/project.godot new file mode 100644 index 00000000..134bcca3 --- /dev/null +++ b/viewport/split_screen_input/project.godot @@ -0,0 +1,88 @@ +; 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" +config/tags=PackedStringArray("demo", "input", "official", "rendering") +run/main_scene="res://split_screen_demo.tscn" +config/features=PackedStringArray("4.5") +config/icon="res://icon.svg" + +[display] + +window/size/viewport_width=800 +window/size/viewport_height=800 +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[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,"location":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":74,"key_label":0,"unicode":106,"location":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":4194319,"key_label":0,"unicode":0,"location":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,"location":0,"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,"location":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":76,"key_label":0,"unicode":108,"location":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":4194321,"key_label":0,"unicode":0,"location":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,"location":0,"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,"location":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":73,"key_label":0,"unicode":105,"location":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":4194320,"key_label":0,"unicode":0,"location":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,"location":0,"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,"location":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":75,"key_label":0,"unicode":107,"location":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":4194322,"key_label":0,"unicode":0,"location":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,"location":0,"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" diff --git a/viewport/split_screen_input/root.gd b/viewport/split_screen_input/root.gd new file mode 100644 index 00000000..cb2bffe9 --- /dev/null +++ b/viewport/split_screen_input/root.gd @@ -0,0 +1,46 @@ +## Set up different Split Screens +## Provide Input configuration +## Connect Split Screens to Play Area +extends Node + + +const KEYBOARD_OPTIONS: Dictionary[String, 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 children: Array[Node] = get_children() + var index: int = 0 + for child: Node in children: + if child is SplitScreen: + config["position"] = Vector2(index % 2, floor(index / 2.0)) * 132 + Vector2(132, 0) + config["index"] = index + config["color"] = PLAYER_COLORS[index] + var split_child: SplitScreen = child as SplitScreen + split_child.set_config(config) + index += 1 diff --git a/viewport/split_screen_input/root.gd.uid b/viewport/split_screen_input/root.gd.uid new file mode 100644 index 00000000..c42d47f5 --- /dev/null +++ b/viewport/split_screen_input/root.gd.uid @@ -0,0 +1 @@ +uid://bdikev0kwlu1g diff --git a/viewport/split_screen_input/screenshots/.gdignore b/viewport/split_screen_input/screenshots/.gdignore new file mode 100644 index 00000000..e69de29b diff --git a/viewport/split_screen_input/screenshots/split_screen_input.webp b/viewport/split_screen_input/screenshots/split_screen_input.webp new file mode 100644 index 00000000..4e9abe71 Binary files /dev/null and b/viewport/split_screen_input/screenshots/split_screen_input.webp differ diff --git a/viewport/split_screen_input/split_screen.gd b/viewport/split_screen_input/split_screen.gd new file mode 100644 index 00000000..0bb61dca --- /dev/null +++ b/viewport/split_screen_input/split_screen.gd @@ -0,0 +1,40 @@ +## Interface for a SplitScreen +class_name SplitScreen +extends Node + + +const JOYPAD_PREFIX: String = "Joypad" + +@export var init_position := Vector2.ZERO + +var _keyboard_options: Dictionary # Copy of all keyboard options. + +@onready var opt: OptionButton = $OptionButton +@onready var viewport: SubViewport = $InputRoutingViewportContainer/SubViewport +@onready var input_router: InputRoutingViewportContainer = $InputRoutingViewportContainer +@onready var play: Player = $InputRoutingViewportContainer/SubViewport/Player + + +# Set the configuration of this split screen and perform OptionButton initialization. +func set_config(config_dict: Dictionary): + _keyboard_options = config_dict["keyboard"] + play.position = config_dict["position"] + var local_index: int = config_dict["index"] + play.modulate = config_dict["color"] + opt.clear() + for keyboard_opt in _keyboard_options: + opt.add_item(keyboard_opt) + for index in config_dict["joypads"]: + opt.add_item("%s %s" % [JOYPAD_PREFIX, index + 1]) + opt.select(local_index) + _on_option_button_item_selected(local_index) + viewport.world_2d = config_dict["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 text: String = opt.get_item_text(index) + if text.begins_with(JOYPAD_PREFIX): + input_router.set_input_config({"joypad": text.substr(text.length() - 1, -1).to_int(), "keyboard": []}) + else: + input_router.set_input_config({"keyboard": _keyboard_options[text]["keys"], "joypad": -1}) diff --git a/viewport/split_screen_input/split_screen.gd.uid b/viewport/split_screen_input/split_screen.gd.uid new file mode 100644 index 00000000..36be3bef --- /dev/null +++ b/viewport/split_screen_input/split_screen.gd.uid @@ -0,0 +1 @@ +uid://crrvxnm6s4ssm diff --git a/viewport/split_screen_input/split_screen.tscn b/viewport/split_screen_input/split_screen.tscn new file mode 100644 index 00000000..f4203add --- /dev/null +++ b/viewport/split_screen_input/split_screen.tscn @@ -0,0 +1,42 @@ +[gd_scene load_steps=6 format=3 uid="uid://dqailbm8vcpf5"] + +[ext_resource type="Script" uid="uid://crrvxnm6s4ssm" path="res://split_screen.gd" id="1_4fp0b"] +[ext_resource type="Script" uid="uid://mplfqw0th285" 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" uid="uid://b7p2tpqjka6jq" 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="InputRoutingViewportContainer" type="SubViewportContainer" parent="."] +auto_translate_mode = 1 +layout_mode = 2 +script = ExtResource("2_v8t84") + +[node name="SubViewport" type="SubViewport" parent="InputRoutingViewportContainer"] +handle_input_locally = false +size = Vector2i(350, 350) +render_target_update_mode = 4 + +[node name="Player" type="CharacterBody2D" parent="InputRoutingViewportContainer/SubViewport"] +script = ExtResource("5_1qhfw") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="InputRoutingViewportContainer/SubViewport/Player"] +shape = SubResource("RectangleShape2D_m48mh") + +[node name="Sprite2D" type="Sprite2D" parent="InputRoutingViewportContainer/SubViewport/Player"] +texture = ExtResource("4_787wn") + +[node name="Camera2D" type="Camera2D" parent="InputRoutingViewportContainer/SubViewport/Player"] + +[connection signal="item_selected" from="OptionButton" to="." method="_on_option_button_item_selected"] diff --git a/viewport/split_screen_input/split_screen_demo.tscn b/viewport/split_screen_input/split_screen_demo.tscn new file mode 100644 index 00000000..ecd9fca0 --- /dev/null +++ b/viewport/split_screen_input/split_screen_demo.tscn @@ -0,0 +1,73 @@ +[gd_scene load_steps=3 format=3 uid="uid://ccutmhshaoqih"] + +[ext_resource type="Script" uid="uid://bdikev0kwlu1g" 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="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="SplitScreen1" parent="." instance=ExtResource("1_mcbdt")] +anchors_preset = -1 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -375.0 +offset_top = -387.0 +offset_right = -25.0 +offset_bottom = -13.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="SplitScreen2" parent="." instance=ExtResource("1_mcbdt")] +anchors_preset = -1 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = 25.0 +offset_top = -387.0 +offset_right = 375.0 +offset_bottom = -13.0 +grow_horizontal = 2 +grow_vertical = 2 +init_position = Vector2(132, 0) + +[node name="SplitScreen3" parent="." instance=ExtResource("1_mcbdt")] +anchors_preset = -1 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -375.0 +offset_top = 13.0 +offset_right = -25.0 +offset_bottom = 387.0 +grow_horizontal = 2 +grow_vertical = 2 +init_position = Vector2(0, 132) + +[node name="SplitScreen4" parent="." instance=ExtResource("1_mcbdt")] +anchors_preset = -1 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = 25.0 +offset_top = 13.0 +offset_right = 375.0 +offset_bottom = 387.0 +grow_horizontal = 2 +grow_vertical = 2 +init_position = Vector2(132, 132) + +[node name="PlayArea" type="SubViewport" parent="."] +render_target_update_mode = 4 diff --git a/viewport/split_screen_input/sub_viewport_container.gd b/viewport/split_screen_input/sub_viewport_container.gd new file mode 100644 index 00000000..cf98dcf5 --- /dev/null +++ b/viewport/split_screen_input/sub_viewport_container.gd @@ -0,0 +1,27 @@ +## Input Routing for different SubViewports. +## Based on the provided input configuration, ensures only the correct +## events reaching the SubViewport. +class_name InputRoutingViewportContainer +extends SubViewportContainer + + +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(input_event: InputEvent) -> bool: + if input_event is InputEventKey: + if _current_keyboard_set.has(input_event.keycode): + return true + elif input_event is InputEventJoypadButton: + if _current_joypad_device > -1 and input_event.device == _current_joypad_device: + return true + return false + + +# Set new config for input handling. +func set_input_config(config_dict: Dictionary): + _current_keyboard_set = config_dict["keyboard"] + _current_joypad_device = config_dict["joypad"] diff --git a/viewport/split_screen_input/sub_viewport_container.gd.uid b/viewport/split_screen_input/sub_viewport_container.gd.uid new file mode 100644 index 00000000..337418c9 --- /dev/null +++ b/viewport/split_screen_input/sub_viewport_container.gd.uid @@ -0,0 +1 @@ +uid://mplfqw0th285