feat : mobile input controller

This commit is contained in:
2026-04-13 18:15:49 +08:00
parent a478e3fc2e
commit a592eb1de0
25 changed files with 771 additions and 2058 deletions
+1
View File
@@ -917,6 +917,7 @@ func reset() -> void:
available_rooms.clear()
is_host = false
_all_ready = false
is_tutorial_mode = false
match_duration = 180 # Reset to default 3 minutes
game_mode = "Freemode"
_update_available_areas(game_mode)
+22 -10
View File
@@ -7,6 +7,10 @@ var movement_manager: Node
var race_manager: Node
@onready var SettingsManager = get_node_or_null("/root/SettingsManager")
# Analog stick repeat throttle (prevents firing a new move every frame while held)
var _analog_move_timer: float = 0.0
const ANALOG_MOVE_RATE: float = 0.18 # seconds between grid steps when holding stick
func initialize(p_player: Node3D, p_movement_manager: Node, p_race_manager: Node):
player = p_player
movement_manager = p_movement_manager
@@ -21,16 +25,21 @@ func _process(delta):
return
var move_vec = Vector2i.ZERO
# 1. Controller / Joystick Movement
if SettingsManager and SettingsManager.settings.controls.get("use_controller", false):
var joystick_vec = Input.get_vector("move_west", "move_east", "move_north", "move_south")
if joystick_vec.length() > 0.3: # Deadzone
# 1. Left Analog Stick — always active (controller plugged in, any mode)
var joystick_vec = Input.get_vector("move_west", "move_east", "move_north", "move_south", 0.25)
if joystick_vec.length() > 0.25:
_analog_move_timer -= delta
if _analog_move_timer <= 0.0:
if abs(joystick_vec.x) > abs(joystick_vec.y):
move_vec.x = 1 if joystick_vec.x > 0 else -1
else:
move_vec.y = 1 if joystick_vec.y > 0 else -1
_analog_move_timer = ANALOG_MOVE_RATE
else:
_analog_move_timer = 0.0 # Reset so next touch fires immediately
# 2. Keyboard Movement (Fallback)
# 2. Keyboard / D-pad Movement (fires via action pressed, handled by event)
if move_vec == Vector2i.ZERO:
if Input.is_action_pressed("move_north"): move_vec.y -= 1
if Input.is_action_pressed("move_south"): move_vec.y += 1
@@ -75,8 +84,11 @@ func handle_unhandled_input(event):
if not player.is_multiplayer_authority() or player.is_frozen or player.is_stop_frozen or (TurnManager.turn_based_mode and (not player.is_my_turn or movement_manager.is_moving)):
return
# --- Keyboard Shortcuts (Event-based) ---
if event is InputEventKey and event.pressed and not event.echo:
# --- Action Shortcuts (Keyboard OR Controller Button) ---
var is_action_event = (event is InputEventKey and event.pressed and not event.echo) \
or (event is InputEventJoypadButton and event.pressed)
if is_action_event:
# Safety check for SettingsManager
if not SettingsManager:
return
@@ -87,21 +99,21 @@ func handle_unhandled_input(event):
get_viewport().set_input_as_handled()
return
# 3. Action Buttons (Remappable via InputMap)
# 2. Action Buttons (Remappable via InputMap)
if event.is_action_pressed("action_knock_tekton"):
if player.powerup_manager:
player.powerup_manager.use_special_effect()
if player.is_attack_mode and player.has_method("enter_attack_mode"):
player.enter_attack_mode()
get_viewport().set_input_as_handled()
elif event.is_action_pressed("action_grab_tekton"):
if not player.is_carrying_tekton:
if player.powerup_manager and player.powerup_manager.has_method("can_use_special"):
player.grab_tekton()
get_viewport().set_input_as_handled()
# Handle spawn point selection if not yet selected
if not player.spawn_point_selected and player.highlighted_spawn_points.size() > 0:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
+85 -1
View File
@@ -53,7 +53,13 @@ var settings = {
# Power Bar Controls / Special
"attack_mode": KEY_Q,
"attack_mode_alt": KEY_U
"attack_mode_alt": KEY_U,
# Controller Button Bindings (JOY_BUTTON_* indices)
"ctrl_grab": JOY_BUTTON_A,
"ctrl_tekton_grab": JOY_BUTTON_RIGHT_SHOULDER,
"ctrl_use_powerup": JOY_BUTTON_Y,
"ctrl_attack_mode": JOY_BUTTON_X
}
}
@@ -207,6 +213,21 @@ func apply_control_settings():
event.keycode = secondary_key
InputMap.action_add_event(action_name, event)
# Add Joypad defaults for actions - use saved ctrl_* bindings
var joy_button_mapped = -1
match action_name:
"action_grab": joy_button_mapped = settings.controls.get("ctrl_grab", JOY_BUTTON_A)
"use_powerup": joy_button_mapped = settings.controls.get("ctrl_use_powerup", JOY_BUTTON_Y)
"action_grab_tekton": joy_button_mapped = settings.controls.get("ctrl_tekton_grab", JOY_BUTTON_RIGHT_SHOULDER)
"action_knock_tekton": joy_button_mapped = settings.controls.get("ctrl_attack_mode", JOY_BUTTON_X)
if action_name == "action_put": joy_button_mapped = JOY_BUTTON_B
if joy_button_mapped != -1:
var joy_btn_event = InputEventJoypadButton.new()
joy_btn_event.button_index = joy_button_mapped
InputMap.action_add_event(action_name, joy_btn_event)
# Add Joypad defaults for movement
if action_name.begins_with("move_"):
var joy_axis = -1
@@ -230,6 +251,69 @@ func set_control(action_name: String, keycode: int):
emit_signal("control_remapped", action_name, keycode)
save_settings()
# --- Controller Binding ---
# Godot 4 JoyButton name map (var not const — enum keys can't be used in const dicts)
var CTRL_BUTTON_NAMES: Dictionary = {
JOY_BUTTON_A: "A / Cross",
JOY_BUTTON_B: "B / Circle",
JOY_BUTTON_X: "X / Square",
JOY_BUTTON_Y: "Y / Triangle",
JOY_BUTTON_LEFT_SHOULDER: "LB / L1",
JOY_BUTTON_RIGHT_SHOULDER: "RB / R1",
JOY_BUTTON_LEFT_STICK: "L3",
JOY_BUTTON_RIGHT_STICK: "R3",
JOY_BUTTON_BACK: "Select / Share",
JOY_BUTTON_START: "Start / Options",
JOY_BUTTON_DPAD_UP: "D-Up",
JOY_BUTTON_DPAD_DOWN: "D-Down",
JOY_BUTTON_DPAD_LEFT: "D-Left",
JOY_BUTTON_DPAD_RIGHT: "D-Right",
}
func get_joy_button_name(button_index: int) -> String:
return CTRL_BUTTON_NAMES.get(button_index, "Btn %d" % button_index)
func get_controller_binding_text(ctrl_key: String) -> String:
"""Return the button name for a ctrl_* settings key."""
var idx = settings.controls.get(ctrl_key, -1)
if idx == -1:
return "Unbound"
return get_joy_button_name(idx)
func get_action_display(action_key: String) -> String:
"""Return keyboard text or controller button name based on use_controller setting."""
if settings.controls.get("use_controller", false):
# Map action key to its ctrl_* key
var ctrl_key_map = {
"grab": "ctrl_grab",
"use_powerup": "ctrl_use_powerup",
"tekton_grab": "ctrl_tekton_grab",
"attack_mode": "ctrl_attack_mode",
}
if ctrl_key_map.has(action_key):
return get_controller_binding_text(ctrl_key_map[action_key])
# Movement axes on left stick
if action_key.begins_with("move_"):
return "L-Stick"
return get_control_text(action_key)
return get_control_text(action_key)
func set_controller_binding(ctrl_key: String, button_index: int):
"""Save a controller button binding and re-apply input maps."""
if settings.controls.has(ctrl_key):
settings.controls[ctrl_key] = button_index
apply_control_settings()
emit_signal("control_remapped", ctrl_key, button_index)
save_settings()
func is_controller_button_used(button_index: int) -> String:
"""Check if a controller button is already assigned."""
for key in settings.controls.keys():
if key.begins_with("ctrl_") and settings.controls[key] == button_index:
return key
return ""
func get_control_keycode(action_name: String) -> int:
return settings.controls.get(action_name, -1)
+35 -42
View File
@@ -23,6 +23,7 @@ var tekton_grab_button: Button
const CONFIG_PATH = "user://touch_controls_settings.cfg"
var button_size: float = 70.0
var button_opacity: float = 0.7
var joystick_size: float = 60.0
var joystick_enabled: bool = true
var touch_buttons_enabled: bool = true # Master toggle for action buttons
var joystick_position: Vector2 = Vector2(120, -120) # Relative to bottom-left
@@ -45,20 +46,30 @@ func initialize(p_main: Node):
_load_settings()
# Connect to remapping signals
if SettingsManager and not SettingsManager.control_remapped.is_connected(_on_control_remapped):
SettingsManager.control_remapped.connect(_on_control_remapped)
if SettingsManager:
if not SettingsManager.control_remapped.is_connected(_on_control_remapped):
SettingsManager.control_remapped.connect(_on_control_remapped)
# Also refresh when settings_applied fires (e.g. use_controller toggle)
if not SettingsManager.settings_applied.is_connected(_on_settings_applied_refresh):
SettingsManager.settings_applied.connect(_on_settings_applied_refresh)
func _on_control_remapped(_action: String, _key: int):
print("[TouchControls] Control remapped: %s. Refreshing labels." % _action)
# Refresh primary assigned buttons
_refresh_all_shortcut_labels()
func _on_settings_applied_refresh():
"""Called when SettingsManager.settings_applied fires (e.g. use_controller toggled)."""
_refresh_all_shortcut_labels()
func _refresh_all_shortcut_labels():
"""Re-run _ensure_shortcut_label on all known buttons to reflect current binding mode."""
if grab_button: _ensure_shortcut_label(grab_button, "Grab")
if put_button: _ensure_shortcut_label(put_button, "Put")
if attack_mode_button: _ensure_shortcut_label(attack_mode_button, "AttackMode")
if spawn_boost_button: _ensure_shortcut_label(spawn_boost_button, "SpawnBoost")
if tekton_grab_button: _ensure_shortcut_label(tekton_grab_button, "TektonGrab")
# Also check all direct children of containers just in case
# Also walk containers for any dynamically added buttons
if power_bar_container:
for child in power_bar_container.get_children():
if child is Button:
@@ -106,35 +117,13 @@ func _create_touch_ui():
# Check if container already exists (added in scene)
var container = self
# Helper to find or create control
var find_or_create_joystick = func():
var joy = container.get_node_or_null("VirtualJoystick")
if joy:
print("[TouchControls] Found existing VirtualJoystick")
return joy
var joystick_script = load("res://scripts/ui/virtual_joystick.gd")
joy = Control.new()
joy.set_script(joystick_script)
joy.name = "VirtualJoystick"
joy.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
# Use standard size from joystick script defaults (radius 60 -> size 160)
var joy_size = Vector2(160, 160)
joy.custom_minimum_size = joy_size
joy.size = joy_size
joy.offset_left = 120
joy.offset_top = -280
joy.offset_right = 280
joy.offset_bottom = -120
container.add_child(joy)
return joy
virtual_joystick = find_or_create_joystick.call()
if not virtual_joystick.direction_changed.is_connected(_on_joystick_direction):
virtual_joystick.direction_changed.connect(_on_joystick_direction)
virtual_joystick = container.get_node_or_null("VirtualJoystick")
if virtual_joystick:
print("[TouchControls] Found local VirtualJoystick node")
if not virtual_joystick.direction_changed.is_connected(_on_joystick_direction):
virtual_joystick.direction_changed.connect(_on_joystick_direction)
else:
push_error("[TouchControls] VirtualJoystick node missing!")
# --- Actions Containers ---
power_bar_container = container.get_node_or_null("PowerBarBtn")
@@ -298,10 +287,10 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
if not SettingsManager: return
match button_name:
"Grab": existing_lbl.text = SettingsManager.get_control_text("grab")
"Put": existing_lbl.text = SettingsManager.get_control_text("put")
"AttackMode": existing_lbl.text = SettingsManager.get_control_text("attack_mode")
"TektonGrab": existing_lbl.text = SettingsManager.get_control_text("tekton_grab")
"Grab": existing_lbl.text = SettingsManager.get_action_display("grab")
"Put": existing_lbl.text = SettingsManager.get_action_display("put")
"AttackMode": existing_lbl.text = SettingsManager.get_action_display("attack_mode")
"TektonGrab": existing_lbl.text = SettingsManager.get_action_display("tekton_grab")
print("[TouchControls] Updated %s shortcut label to: %s" % [button_name, existing_lbl.text])
@@ -330,10 +319,10 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
shortcut_lbl.add_theme_constant_override("outline_size", 4)
match button_name:
"Grab": shortcut_lbl.text = SettingsManager.get_control_text("grab") if SettingsManager else "Space"
"Put": shortcut_lbl.text = SettingsManager.get_control_text("put") if SettingsManager else "R"
"AttackMode": shortcut_lbl.text = SettingsManager.get_control_text("attack_mode") if SettingsManager else "Q"
"TektonGrab": shortcut_lbl.text = SettingsManager.get_control_text("tekton_grab") if SettingsManager else "G"
"Grab": shortcut_lbl.text = SettingsManager.get_action_display("grab") if SettingsManager else "Space"
"Put": shortcut_lbl.text = SettingsManager.get_action_display("put") if SettingsManager else "R"
"AttackMode": shortcut_lbl.text = SettingsManager.get_action_display("attack_mode") if SettingsManager else "Q"
"TektonGrab": shortcut_lbl.text = SettingsManager.get_action_display("tekton_grab") if SettingsManager else "G"
btn.add_child(shortcut_lbl)
@@ -434,6 +423,7 @@ func _load_settings():
button_opacity = config.get_value("touch_controls", "button_opacity", 0.7)
button_scale = config.get_value("touch_controls", "button_scale", 1.0)
joystick_enabled = config.get_value("touch_controls", "joystick_enabled", true)
joystick_size = config.get_value("touch_controls", "joystick_size", 60.0)
touch_buttons_enabled = config.get_value("touch_controls", "touch_buttons_enabled", true)
# Load button positions
@@ -463,6 +453,7 @@ func _save_settings():
config.set_value("touch_controls", "button_opacity", button_opacity)
config.set_value("touch_controls", "button_scale", button_scale)
config.set_value("touch_controls", "joystick_enabled", joystick_enabled)
config.set_value("touch_controls", "joystick_size", joystick_size)
config.set_value("touch_controls", "touch_buttons_enabled", touch_buttons_enabled)
config.set_value("touch_controls", "grab_position", button_positions.grab)
config.set_value("touch_controls", "put_position", button_positions.put)
@@ -480,6 +471,8 @@ func _apply_settings():
# Apply joystick visibility
if virtual_joystick:
virtual_joystick.visible = joystick_enabled and _is_touch_device()
if virtual_joystick.has_method("set_radius"):
virtual_joystick.set_radius(joystick_size)
# Apply touch buttons visibility - FORCED ON per request to "just show them"
# Apply touch buttons visibility
+1 -1
View File
@@ -30,7 +30,7 @@ var _previous_playerboard_state: Array = []
func initialize(player_node):
# Get PowerUp Inventory UI from scene
powerup_inventory_ui = player_node.get_node_or_null("PowerUpInventoryUI")
powerup_inventory_ui = player_node.get_node_or_null("TouchLayer/TouchControls/PowerUpInventoryUI")
# Get node references from main scene
playerboard_ui = player_node.get_node_or_null("PlayerBoardUI/PlayerboardUI")