Basic game template addon
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 6s
Create tag and build when new code gets to main / Export (push) Successful in 1m1s

This commit is contained in:
2026-01-30 19:45:56 +01:00
parent b923f6bec2
commit 44f251ed66
406 changed files with 12602 additions and 1 deletions

View File

@@ -0,0 +1,68 @@
extends Control
## Node that captures UI focus when switching menus.
##
## This script assists with capturing UI focus when
## opening, closing, or switching between menus.
## When attached to a node, it will check if it was changed to visible
## and if it should grab focus. If both are true, it will capture focus
## on the first eligible node in its scene tree.
## Hierarchical depth to search in the scene tree for a focusable control node.
@export var search_depth : int = 1
## If true, always capture focus when made visible.
@export var enabled : bool = false
## If true, capture focus if nothing currently is in focus.
@export var null_focus_enabled : bool = true
## If true, capture focus if there is a joypad detected.
@export var joypad_enabled : bool = true
## If true, capture focus if the mouse is hidden.
@export var mouse_hidden_enabled : bool = true
## Locks focus
@export var lock : bool = false :
set(value):
var value_changed : bool = lock != value
lock = value
if value_changed and not lock:
update_focus()
func _focus_first_search(control_node : Control, levels : int = 1) -> bool:
if control_node == null or !control_node.is_visible_in_tree():
return false
if control_node.focus_mode == FOCUS_ALL:
control_node.grab_focus()
if control_node is ItemList:
control_node.select(0)
return true
if levels < 1:
return false
var children = control_node.get_children()
for child in children:
if _focus_first_search(child, levels - 1):
return true
return false
func focus_first() -> void:
_focus_first_search(self, search_depth)
func update_focus() -> void:
if lock : return
if _is_visible_and_should_capture():
focus_first()
func _should_capture_focus() -> bool:
return enabled or \
(get_viewport().gui_get_focus_owner() == null and null_focus_enabled) or \
(Input.get_connected_joypads().size() > 0 and joypad_enabled) or \
(Input.mouse_mode not in [Input.MOUSE_MODE_VISIBLE, Input.MOUSE_MODE_CONFINED] and mouse_hidden_enabled)
func _is_visible_and_should_capture() -> bool:
return is_visible_in_tree() and _should_capture_focus()
func _on_visibility_changed() -> void:
call_deferred("update_focus")
func _ready() -> void:
if is_inside_tree():
update_focus()
connect("visibility_changed", _on_visibility_changed)

View File

@@ -0,0 +1 @@
uid://1nf36h0gms3q

View File

@@ -0,0 +1,55 @@
@tool
extends Node
class_name FileLister
## Helper class for listing all the scenes in a directory.
## List of paths to scene files.
@export var _refresh_files_action : bool = false :
set(value):
if value and Engine.is_editor_hint():
_refresh_files()
# For Godot 4.4
# @export_tool_button("Refresh Files") var _refresh_files_action = _refresh_files
## Filled in the editor by selecting a directory.
@export var files : Array[String]
## Fills files with those discovered in directories, and matching constraints.
@export_dir var directories : Array[String] :
set(value):
directories = value
_refresh_files()
@export_group("Constraints")
## Include any results that match the string.
@export var search : String
## Exclude any results that match the string.
@export var filter : String
@export_subgroup("Advanced Search")
## Include any results that begin with the string.
@export var begins_with : String
## Include any results that end with the string.
@export var ends_with : String
## Exclude any results that begin with the string.
@export var not_begins_with : String
## Exclude any results that end with the string.
@export var not_ends_with : String
func _refresh_files():
if not is_inside_tree(): return
files.clear()
for directory in directories:
var dir_access = DirAccess.open(directory)
if dir_access:
for file in dir_access.get_files():
if (not search.is_empty()) and (not file.contains(search)):
continue
if (not filter.is_empty()) and (file.contains(filter)):
continue
if (not begins_with.is_empty()) and (not file.begins_with(begins_with)):
continue
if (not ends_with.is_empty()) and (not file.ends_with(ends_with)):
continue
if (not not_begins_with.is_empty()) and (file.begins_with(not_begins_with)):
continue
if (not not_ends_with.is_empty()) and (file.ends_with(not_ends_with)):
continue
files.append(directory + "/" + file)

View File

@@ -0,0 +1 @@
uid://bij7wsh8d44gv

View File

@@ -0,0 +1,175 @@
class_name InputEventHelper
extends Node
## Helper class for organizing constants related to [InputEvent].
const DEVICE_KEYBOARD = "Keyboard"
const DEVICE_MOUSE = "Mouse"
const DEVICE_XBOX_CONTROLLER = "Xbox"
const DEVICE_SWITCH_CONTROLLER = "Switch"
const DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "Switch Left Joycon"
const DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "Switch Right Joycon"
const DEVICE_SWITCH_JOYCON_COMBINED_CONTROLLER = "Switch Combined Joycons"
const DEVICE_PLAYSTATION_CONTROLLER = "Playstation"
const DEVICE_STEAMDECK_CONTROLLER = "Steamdeck"
const DEVICE_GENERIC = "Generic"
const JOYSTICK_LEFT_NAME = "Left Stick"
const JOYSTICK_RIGHT_NAME = "Right Stick"
const D_PAD_NAME = "Dpad"
const MOUSE_BUTTONS : Array = ["None", "Left", "Right", "Middle", "Scroll Up", "Scroll Down", "Wheel Left", "Wheel Right"]
const JOYPAD_BUTTON_NAME_MAP : Dictionary = {
DEVICE_GENERIC : ["Trigger A", "Trigger B", "Trigger C", "", "", "", "", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right"],
DEVICE_XBOX_CONTROLLER : ["A", "B", "X", "Y", "View", "Home", "Menu", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Share"],
DEVICE_SWITCH_CONTROLLER : ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Capture"],
DEVICE_PLAYSTATION_CONTROLLER : ["Cross", "Circle", "Square", "Triangle", "Select", "PS", "Options", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Microphone"],
DEVICE_STEAMDECK_CONTROLLER : ["A", "B", "X", "Y", "View", "", "Options", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right"]
} # Dictionary[String, Array]
const SDL_DEVICE_NAMES: Dictionary = {
DEVICE_XBOX_CONTROLLER: ["XInput", "XBox"],
DEVICE_PLAYSTATION_CONTROLLER: ["Sony", "PS5", "PS4", "Nacon"],
DEVICE_STEAMDECK_CONTROLLER: ["Steam"],
DEVICE_SWITCH_CONTROLLER: ["Switch"],
DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER: ["Joy-Con (L)", "Left Joy-Con"],
DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: ["Joy-Con (R)", "Right Joy-Con"],
DEVICE_SWITCH_JOYCON_COMBINED_CONTROLLER: ["Joy-Con (L/R)", "Combined Joy-Cons"],
}
const JOY_BUTTON_NAMES : Dictionary = {
JOY_BUTTON_A: "Button A",
JOY_BUTTON_B: "Button B",
JOY_BUTTON_X: "Button X",
JOY_BUTTON_Y: "Button Y",
JOY_BUTTON_LEFT_SHOULDER: "Left Shoulder",
JOY_BUTTON_RIGHT_SHOULDER: "Right Shoulder",
JOY_BUTTON_LEFT_STICK: "Left Stick",
JOY_BUTTON_RIGHT_STICK: "Right Stick",
JOY_BUTTON_START : "Button Start",
JOY_BUTTON_GUIDE : "Button Guide",
JOY_BUTTON_BACK : "Button Back",
JOY_BUTTON_DPAD_UP : D_PAD_NAME + " Up",
JOY_BUTTON_DPAD_DOWN : D_PAD_NAME + " Down",
JOY_BUTTON_DPAD_LEFT : D_PAD_NAME + " Left",
JOY_BUTTON_DPAD_RIGHT : D_PAD_NAME + " Right",
JOY_BUTTON_MISC1 : "Misc",
}
const JOYPAD_DPAD_NAMES : Dictionary = {
JOY_BUTTON_DPAD_UP : D_PAD_NAME + " Up",
JOY_BUTTON_DPAD_DOWN : D_PAD_NAME + " Down",
JOY_BUTTON_DPAD_LEFT : D_PAD_NAME + " Left",
JOY_BUTTON_DPAD_RIGHT : D_PAD_NAME + " Right",
}
const JOY_AXIS_NAMES : Dictionary = {
JOY_AXIS_TRIGGER_LEFT: "Left Trigger",
JOY_AXIS_TRIGGER_RIGHT: "Right Trigger",
}
const BUILT_IN_ACTION_NAME_MAP : Dictionary = {
"ui_accept" : "Accept",
"ui_select" : "Select",
"ui_cancel" : "Cancel",
"ui_focus_next" : "Focus Next",
"ui_focus_prev" : "Focus Prev",
"ui_left" : "Left (UI)",
"ui_right" : "Right (UI)",
"ui_up" : "Up (UI)",
"ui_down" : "Down (UI)",
"ui_page_up" : "Page Up",
"ui_page_down" : "Page Down",
"ui_home" : "Home",
"ui_end" : "End",
"ui_cut" : "Cut",
"ui_copy" : "Copy",
"ui_paste" : "Paste",
"ui_undo" : "Undo",
"ui_redo" : "Redo",
}
static func has_joypad() -> bool:
return Input.get_connected_joypads().size() > 0
static func is_joypad_event(event: InputEvent) -> bool:
return event is InputEventJoypadButton or event is InputEventJoypadMotion
static func is_mouse_event(event: InputEvent) -> bool:
return event is InputEventMouseButton or event is InputEventMouseMotion
static func get_device_name_by_id(device_id : int) -> String:
if device_id >= 0:
var device_name = Input.get_joy_name(device_id)
for device_key in SDL_DEVICE_NAMES:
for keyword in SDL_DEVICE_NAMES[device_key]:
if device_name.containsn(keyword):
return device_key
return DEVICE_GENERIC
static func get_device_name(event: InputEvent) -> String:
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
if event.device == -1:
return DEVICE_GENERIC
var device_id = event.device
return get_device_name_by_id(device_id)
return DEVICE_GENERIC
static func _display_server_supports_keycode_from_physical():
return OS.has_feature("windows") or OS.has_feature("macos") or OS.has_feature("linux")
static func get_text(event : InputEvent) -> String:
if event == null:
return ""
if event is InputEventJoypadButton:
if event.button_index in JOY_BUTTON_NAMES:
return JOY_BUTTON_NAMES[event.button_index]
elif event is InputEventJoypadMotion:
var full_string := ""
var direction_string := ""
var is_right_or_down : bool = event.axis_value > 0.0
if event.axis in JOY_AXIS_NAMES:
return JOY_AXIS_NAMES[event.axis]
match(event.axis):
JOY_AXIS_LEFT_X:
full_string = JOYSTICK_LEFT_NAME
direction_string = "Right" if is_right_or_down else "Left"
JOY_AXIS_LEFT_Y:
full_string = JOYSTICK_LEFT_NAME
direction_string = "Down" if is_right_or_down else "Up"
JOY_AXIS_RIGHT_X:
full_string = JOYSTICK_RIGHT_NAME
direction_string = "Right" if is_right_or_down else "Left"
JOY_AXIS_RIGHT_Y:
full_string = JOYSTICK_RIGHT_NAME
direction_string = "Down" if is_right_or_down else "Up"
full_string += " " + direction_string
return full_string
elif event is InputEventKey:
var keycode : Key = event.get_physical_keycode()
if keycode:
keycode = event.get_physical_keycode_with_modifiers()
else:
keycode = event.get_keycode_with_modifiers()
if _display_server_supports_keycode_from_physical():
keycode = DisplayServer.keyboard_get_keycode_from_physical(keycode)
return OS.get_keycode_string(keycode)
return event.as_text()
static func get_device_specific_text(event : InputEvent, device_name : String = "") -> String:
if device_name.is_empty():
device_name = get_device_name(event)
if event is InputEventJoypadButton:
var joypad_button : String = ""
if event.button_index in JOYPAD_DPAD_NAMES:
joypad_button = JOYPAD_DPAD_NAMES[event.button_index]
elif event.button_index < JOYPAD_BUTTON_NAME_MAP[device_name].size():
joypad_button = JOYPAD_BUTTON_NAME_MAP[device_name][event.button_index]
return "%s %s" % [device_name, joypad_button]
if event is InputEventJoypadMotion:
return "%s %s" % [device_name, get_text(event)]
if event is InputEventMouseButton:
if event.button_index < MOUSE_BUTTONS.size():
var mouse_button : String = MOUSE_BUTTONS[event.button_index]
return "%s %s" % [DEVICE_MOUSE, mouse_button]
return get_text(event).capitalize()

View File

@@ -0,0 +1 @@
uid://6xujceamar4h

View File

@@ -0,0 +1,27 @@
extends Node
## Node for opening a pause menu when detecting a 'ui_cancel' event.
@export var pause_menu_packed : PackedScene
@export var focused_viewport : Viewport
var pause_menu : Node
func _unhandled_input(event : InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
if pause_menu.visible: return
if not focused_viewport:
focused_viewport = get_viewport()
var _initial_focus_control = focused_viewport.gui_get_focus_owner()
pause_menu.show()
if pause_menu is CanvasLayer:
await pause_menu.visibility_changed
else:
await pause_menu.hidden
if is_inside_tree() and _initial_focus_control:
_initial_focus_control.grab_focus()
func _ready() -> void:
pause_menu = pause_menu_packed.instantiate()
pause_menu.hide()
get_tree().current_scene.call_deferred("add_child", pause_menu)

View File

@@ -0,0 +1 @@
uid://cyh0d64pfygbl