
Godot Adapt Mobile To Desktop
- 140 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-adapt-mobile-to-desktop for development tasks
About
godot-adapt-mobile-to-desktop: A skill for development. This provides functionality for development workflows.
- godot-adapt-mobile-to-desktop
Godot Adapt Mobile To Desktop by the numbers
- 140 all-time installs (skills.sh)
- +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,605 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-adapt-mobile-to-desktopAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 140 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-adapt-mobile-to-desktop for development tasks
Files
Adapt: Mobile to Desktop
Expert guidance for scaling mobile games to desktop platforms.
NEVER Do
- NEVER keep touch-only controls — Add mouse/keyboard alternatives. Touch controls on desktop feel awkward and limit precision.
- NEVER lock to mobile resolution — Desktop can handle 1920x1080+ and higher frame rates. Upscale UI, increase render distance.
- NEVER hide graphics settings — Desktop players expect quality options (resolution, VSync, shadows, anti-aliasing).
- NEVER use mobile-sized UI — Touch targets (44pt) are too large for mouse. Reduce button/text size by 30-50%.
- NEVER forget window management — Players expect fullscreen, borderless, maximize, and multi-monitor support.
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
mouse_capture_look.gd
Expert Mouse Capture Controller that completely overrides mobile touch/swipe logic by accumulating InputEventMouseMotion.relative against pitch/yaw variables while clamped.
dynamic_window_manager.gd
Crucial lifecycle manager handling the expectation of PC gamers to toggle between Windowed, Fullscreen Exclusive, and modern Borderless Fullscreen via DisplayServer flags.
keybinding_remapper.gd
Complete runtime input remapper. Mobile relies on hardcoded touch zones, but PC requires the ability to swap WASD to custom keycodes via InputMap and saving to ConfigFile.
cursor_state_manager.gd
Hardware cursor state machine. Replaces the default OS arrow with custom Texture2D hardware cursors and handles hiding the cursor during combat while freeing it in menus.
uncapped_framerate.gd
Expert VSync and FPS unlocker. Mobile locks at 60FPS to save battery; PC gamers expect the ability to disable VSync and unlock Engine.max_fps for 144Hz+ monitors.
resolution_dropdown.gd
Query engine utilizing DisplayServer.screen_get_size to build an OptionButton of supported native 16:9, 21:9, and 4K resolutions without exceeding the user's physical monitor.
desktop_ui_scaler.gd
Recursive SceneTree crawler that scales down massive thumb-sized mobile buttons by a percentage shrink factor specifically on Desktop builds while retaining their anchor points.
scroll_wheel_zoom.gd
Replaces the mobile Pinch-to-Zoom gesture with discrete physical mouse wheel ticks, smoothly interpolating the Camera2D zoom continuously via delta.
quit_confirmation.gd
Hooks get_tree().set_auto_accept_quit(false) to intercept the OS-level 'X' window button, pausing the game and prompting the user to save instead of instantly terminating like mobile.
multi_monitor_handling.gd
Advanced PC window placement script that queries the mouse position to identify the active screen on multi-monitor setups, ensuring the game launches exactly where the user is looking.
---
Control Scheme Expansion
Touch → Mouse Conversion
# Mobile: Virtual joystick for movement
var direction: Vector2 = virtual_joystick.get_direction()
# ⬇️ Desktop: WASD + mouse aim
extends CharacterBody2D
func _physics_process(delta: float) -> void:
# Keyboard movement (WASD)
var input := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = input.normalized() * SPEED
# Mouse aiming
var mouse_pos := get_global_mouse_position()
look_at(mouse_pos)
move_and_slide()
# Configure Project Settings → Input Map:
# move_left: A, Left Arrow
# move_right: D, Right Arrow
# move_up: W, Up Arrow
# move_down: S, Down ArrowAdd Keyboard Shortcuts
# desktop_shortcuts.gd
extends Node
func _input(event: InputEvent) -> void:
if event.is_action_pressed("toggle_fullscreen"):
toggle_fullscreen()
if event.is_action_pressed("quick_save"):
save_game()
if event.is_action_pressed("toggle_inventory"):
$UI/Inventory.visible = not $UI/Inventory.visible
func toggle_fullscreen() -> void:
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
# Add to Project Settings → Input Map:
# toggle_fullscreen: F11
# quick_save: F5
# toggle_inventory: I, TabScroll Wheel Support
# Mobile: Pinch to zoom
# Desktop: Scroll wheel
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
camera.zoom *= 1.1
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
camera.zoom *= 0.9---
Graphics Enhancement
Resolution Scaling
# mobile_settings.gd (mobile)
func _ready() -> void:
get_viewport().size = Vector2i(1280, 720) # Mobile resolution
# ⬇️ desktop_settings.gd (desktop)
extends Node
@export var supported_resolutions: Array[Vector2i] = [
Vector2i(1280, 720),
Vector2i(1920, 1080),
Vector2i(2560, 1440),
Vector2i(3840, 2160)
]
func _ready() -> void:
if OS.get_name() in ["Windows", "macOS", "Linux"]:
# Start at native resolution
var screen_size := DisplayServer.screen_get_size()
get_window().size = screen_size
# Enable higher quality
enable_desktop_graphics()
func enable_desktop_graphics() -> void:
# Enable MSAA
get_viewport().msaa_2d = Viewport.MSAA_2X
get_viewport().msaa_3d = Viewport.MSAA_4X
# Enable screen space AA
get_viewport().screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
# Higher shadow resolution
RenderingServer.directional_shadow_atlas_set_size(4096, true)
# Enable post-processing
var env := get_viewport().world_3d.environment
if env:
env.glow_enabled = true
env.ssao_enabled = true
env.adjustment_enabled = trueSettings Menu
# graphics_settings.gd
extends Control
@onready var resolution_option: OptionButton = $VBoxContainer/Resolution
@onready var quality_option: OptionButton = $VBoxContainer/Quality
@onready var vsync_check: CheckBox = $VBoxContainer/VSync
@onready var fullscreen_check: CheckBox = $VBoxContainer/Fullscreen
func _ready() -> void:
populate_settings()
load_settings()
func populate_settings() -> void:
# Resolution options
resolution_option.add_item("1280x720")
resolution_option.add_item("1920x1080")
resolution_option.add_item("2560x1440")
resolution_option.add_item("3840x2160")
# Quality presets
quality_option.add_item("Low")
quality_option.add_item("Medium")
quality_option.add_item("High")
quality_option.add_item("Ultra")
func _on_resolution_selected(index: int) -> void:
var resolutions := [
Vector2i(1280, 720),
Vector2i(1920, 1080),
Vector2i(2560, 1440),
Vector2i(3840, 2160)
]
get_window().size = resolutions[index]
save_settings()
func _on_quality_selected(index: int) -> void:
match index:
0: # Low
apply_low_quality()
1: # Medium
apply_medium_quality()
2: # High
apply_high_quality()
3: # Ultra
apply_ultra_quality()
save_settings()
func apply_ultra_quality() -> void:
get_viewport().msaa_3d = Viewport.MSAA_8X
get_viewport().screen_space_aa = Viewport.SCREEN_SPACE_AA_FXAA
RenderingServer.directional_shadow_atlas_set_size(8192, true)
var env := get_viewport().world_3d.environment
if env:
env.glow_enabled = true
env.ssao_enabled = true
env.ssil_enabled = true
env.sdfgi_enabled = true
func _on_vsync_toggled(enabled: bool) -> void:
if enabled:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
else:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
save_settings()
func _on_fullscreen_toggled(enabled: bool) -> void:
if enabled:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
save_settings()
func save_settings() -> void:
var config := ConfigFile.new()
config.set_value("graphics", "resolution_index", resolution_option.selected)
config.set_value("graphics", "quality", quality_option.selected)
config.set_value("graphics", "vsync", vsync_check.button_pressed)
config.set_value("graphics", "fullscreen", fullscreen_check.button_pressed)
config.save("user://settings.cfg")
func load_settings() -> void:
var config := ConfigFile.new()
if config.load("user://settings.cfg") == OK:
resolution_option.selected = config.get_value("graphics", "resolution_index", 1)
quality_option.selected = config.get_value("graphics", "quality", 2)
vsync_check.button_pressed = config.get_value("graphics", "vsync", true)
fullscreen_check.button_pressed = config.get_value("graphics", "fullscreen", false)
# Apply settings
_on_resolution_selected(resolution_option.selected)
_on_quality_selected(quality_option.selected)
_on_vsync_toggled(vsync_check.button_pressed)
_on_fullscreen_toggled(fullscreen_check.button_pressed)---
UI Layout Expansion
Mobile UI → Desktop UI
# Mobile: Compact HUD, large touch buttons
# Scene: MobileHUD.tscn
# - Virtual joystick (bottom-left)
# - Action buttons (bottom-right, 80x80px)
# ⬇️ Desktop: Spread UI, smaller elements
# Scene: DesktopHUD.tscn
# - Health/Mana bars (top-left, 40px tall)
# - Minimap (top-right, 200x200px)
# - Hotbar (bottom-center, 50x50px slots)
# - Chat (bottom-left, resizable)
extends Control
func _ready() -> void:
if OS.has_feature("mobile"):
_setup_mobile_ui()
else:
_setup_desktop_ui()
func _setup_mobile_ui() -> void:
# Large buttons, bottom corners
$VirtualJoystick.visible = true
$ActionButtons.scale = Vector2(1.5, 1.5)
$Minimap.visible = false # Too cluttered
func _setup_desktop_ui() -> void:
# Compact, corners and edges
$VirtualJoystick.visible = false
$ActionButtons.scale = Vector2(0.8, 0.8)
$Minimap.visible = true
$ChatBox.visible = true---
Window Management
Multi-Monitor Support
# window_manager.gd
extends Node
func _ready() -> void:
# Detect monitors
var screen_count := DisplayServer.get_screen_count()
print("Detected %d monitors" % screen_count)
# Allow window dragging between monitors
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
func move_to_monitor(monitor_index: int) -> void:
var screen_pos := DisplayServer.screen_get_position(monitor_index)
var screen_size := DisplayServer.screen_get_size(monitor_index)
# Center window on target monitor
var window_size := get_window().size
var centered_pos := screen_pos + (screen_size - window_size) / 2
DisplayServer.window_set_position(centered_pos)Borderless Fullscreen
func set_borderless_fullscreen(enabled: bool) -> void:
if enabled:
# Get screen size
var screen_size := DisplayServer.screen_get_size()
# Set window to screen size
get_window().size = screen_size
get_window().position = Vector2i.ZERO
# Remove border
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
else:
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)---
Platform-Specific Features
Steam Integration (Example)
# Requires GodotSteam plugin
extends Node
var steam_initialized := false
func _ready() -> void:
if OS.get_name() in ["Windows", "Linux", "macOS"]:
initialize_steam()
func initialize_steam() -> void:
var init_result := Steam.steamInit()
if init_result.status == Steam.STEAM_OK:
steam_initialized = true
print("Steam initialized")
# Enable achievements
Steam.requestStats()
func unlock_achievement(achievement_id: String) -> void:
if steam_initialized:
Steam.setAchievement(achievement_id)
Steam.storeStats()Discord Rich Presence
# Requires Discord SDK integration
extends Node
func update_presence(state: String, details: String) -> void:
if OS.get_name() == "Windows":
# Update Discord presence
# (Requires plugin)
pass---
Performance Enhancements
Unlock Frame Rate
# Mobile: Locked to 60 FPS
Engine.max_fps = 60
# Desktop: Unlock or match monitor refresh rate
func _ready() -> void:
if not OS.has_feature("mobile"):
Engine.max_fps = 0 # Unlimited (use VSync to cap)
# Or match monitor:
var refresh_rate := DisplayServer.screen_get_refresh_rate()
Engine.max_fps = int(refresh_rate)Increased Draw Distance
# Mobile: Low draw distance
var camera: Camera3D
camera.far = 100.0
# Desktop: Higher
camera.far = 500.0
# Also increase shadow distance
var sun: DirectionalLight3D
sun.directional_shadow_max_distance = 200.0 # Up from 50---
Testing Checklist
- [ ] Mouse controls feel precise (no acceleration issues)
- [ ] All mobile touch controls have keyboard/mouse equivalents
- [ ] Graphics settings menu works correctly
- [ ] Fullscreen, windowed, borderless modes all function
- [ ] Multi-monitor setup works (dragging window, centering)
- [ ] Resolution changes don't crash or distort UI
- [ ] VSync toggle works
- [ ] Runs at 144+ FPS on high-end hardware
- [ ] Settings persist across sessions
- [ ] Game scales well to ultrawide monitors (21:9, 32:9)
Expert Techniques & Optimizations
1. Standalone Desktop Launcher
For heavy desktop builds, use a lightweight Godot "Launcher" executable. This allows users to configure settings via ConfigFile before spawning the main game process using OS.create_process().
func launch_game() -> void:
# Save settings to user://settings.cfg
config.save("user://settings.cfg")
# Spawn main game with data pack argument
var path := OS.get_executable_path()
var args := PackedStringArray(["--main-pack", "game_data.pck"])
var pid := OS.create_process(path, args)
if pid > 0:
get_tree().quit()2. Graphics Benchmark Mode
Automatically suggest optimal settings by running a 5-second performance test. Use ProjectSettings.set_setting() to toggle high-end features like Screen Space AA or SDFGI half-resolution based on average FPS.
func _evaluate_benchmark(frame_count: int, duration: float) -> void:
var avg_fps := frame_count / duration
if avg_fps < 30.0:
# Disable heavy features
ProjectSettings.set_setting("rendering/anti_aliasing/quality/screen_space_aa", 0)
ProjectSettings.set_setting("rendering/global_illumination/gi/use_half_resolution", true)
ProjectSettings.save_custom("user://override.cfg")3. Steamworks Integration (GDExtension)
While Godot doesn't natively include Steamworks, the expert approach is to use a GDExtension (like GodotSteam) to bridge the Steam SDK for cloud saves and achievements. This keeps the core engine binary small while providing deep platform integration.
# Example using GodotSteam GDExtension
func _ready() -> void:
if Steam.steamInit().status == Steam.STEAM_OK:
print("Steam Cloud and Achievements Active")
Steam.requestStats()Reference
- Master Skill: godot-master
extends Node
class_name CursorStateManager
## Expert PC Cursor Manager
## Mobile has no cursor concept. On PC, the cursor must be managed:
## Custom hardware textures, hiding during action, showing in menus.
@export var default_cursor: Texture2D
@export var crosshair_cursor: Texture2D
func _ready() -> void:
# Set standard hardware cursors to look professional instead of default OS arrows
if default_cursor:
Input.set_custom_mouse_cursor(default_cursor, Input.CURSOR_ARROW, Vector2(0, 0))
if crosshair_cursor:
Input.set_custom_mouse_cursor(crosshair_cursor, Input.CURSOR_CROSS, Vector2(16, 16))
func set_combat_mode(active: bool) -> void:
if active:
# Hide standard cursor, switch to crosshair or fully captured
Input.set_mouse_mode(Input.MOUSE_MODE_CONFINED_HIDDEN)
else:
# Released for menu usage
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
func _notification(what: int) -> void:
# Hide cursor if the game window loses focus, to not confuse the user's OS usage
if what == NOTIFICATION_WM_WINDOW_FOCUS_OUT:
pass # Optional: Reset to OS default
# skills/adapt-mobile-to-desktop/scripts/desktop_input_adapter.gd
extends Node
## Desktop Input Adapter Expert Pattern
## Maps standard Desktop inputs (WASD + Mouse) to Mobile actions.
class_name DesktopInputAdapter
# Configuration -> Action Map
# Maps "mobile_action_name": [Keycodes]
var key_map = {
"interact": [KEY_E, KEY_SPACE],
"inventory": [KEY_I, KEY_TAB],
"pause": [KEY_ESCAPE],
"map": [KEY_M]
}
func _ready() -> void:
if OS.has_feature("pc") or OS.has_feature("web_pc") or OS.is_debug_build():
_inject_input_map()
_enable_mouse_look()
func _inject_input_map() -> void:
# Ensure PC keys trigger the same actions as Mobile buttons
for action in key_map:
if not InputMap.has_action(action):
InputMap.add_action(action)
for key in key_map[action]:
var ev = InputEventKey.new()
ev.keycode = key
if not InputMap.action_has_event(action, ev):
InputMap.action_add_event(action, ev)
func _enable_mouse_look() -> void:
# If the game uses virtual joystick aiming on mobile,
# we want mouse aiming on desktop.
# This is often handled in the Player script, but we can emit signals
pass
func get_aim_vector(current_pos: Vector2) -> Vector2:
if OS.has_feature("pc") or OS.has_feature("web_pc"):
# Mouse aim
return (get_viewport().get_mouse_position() - current_pos).normalized()
else:
# Joystick aim (assumes Input Map "aim_x", "aim_y" setup)
return Input.get_vector("aim_left", "aim_right", "aim_up", "aim_down")
## EXPERT USAGE:
## AutoLoad this script. Use `DesktopInputAdapter.get_aim_vector()` in player code
## to support both Joystick (mobile) and Mouse (desktop) seamlessly.
extends Control
class_name DesktopUISHRINKER
## Expert Desktop UI Scaler
## When migrating a mobile game to PC, buttons are usually MASSIVE (to fit thick thumbs).
## Using the `DisplayServer.screen_get_dpi()` we can selectively shrink the Control tree on PC monitors.
@export var shrink_factor: float = 0.65
func _ready() -> void:
if not OS.has_feature("mobile"): # If we are on Windows/macOs/Linux
_shrink_recursive(self)
func _shrink_recursive(node: Node) -> void:
if node is Control:
# If the button had a custom minimum size of 150x150 for touch screens, scale it down
var old_min = node.custom_minimum_size
if old_min.x > 0 and old_min.y > 0:
node.custom_minimum_size = old_min * shrink_factor
# Optional: reduce font sizes in theme overrides
if node.has_theme_font_size_override("font_size"):
var current_size = node.get_theme_font_size("font_size")
node.add_theme_font_size_override("font_size", int(current_size * shrink_factor))
for child in node.get_children():
_shrink_recursive(child)
extends Node
class_name DynamicWindowManager
## Expert Desktop Window Manager
## Mobile games are always full screen. PC gamers demand Borderless Windowed,
## standard Windowed, and Full Screen Exclusive.
enum WindowMode {
WINDOWED,
FULLSCREEN,
BORDERLESS_FULLSCREEN
}
var current_mode: WindowMode = WindowMode.WINDOWED
func _input(event: InputEvent) -> void:
# Standard PC shortcut: Alt+Enter to toggle fullscreen
if event.is_action_pressed("toggle_fullscreen") or (event is InputEventKey and event.keycode == KEY_ENTER and event.alt_pressed):
if current_mode == WindowMode.WINDOWED:
set_mode(WindowMode.BORDERLESS_FULLSCREEN)
else:
set_mode(WindowMode.WINDOWED)
func set_mode(mode: WindowMode) -> void:
current_mode = mode
match current_mode:
WindowMode.WINDOWED:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
WindowMode.FULLSCREEN:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, false)
WindowMode.BORDERLESS_FULLSCREEN:
# Borderless fills the screen but allows fast alt-tabbing on modern OSs
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
print("Window mode set to: ", WindowMode.keys()[current_mode])
# skills/adapt-mobile-to-desktop/scripts/hover_bridge.gd
extends Node
## Hover Bridge Expert Pattern
## Re-enables desktop hover interactions for shared codebases.
## Mobile usually ignores mouse_entered/exited; this bridges the gap.
class_name HoverBridge
signal hover_state_changed(node: Control, is_hovering: bool)
@export var hover_scale: float = 1.1
@export var hover_color: Color = Color(1.2, 1.2, 1.2) # Brighten
@export var animate: bool = true
var _original_scales: Dictionary = {}
var _original_modulates: Dictionary = {}
func _ready() -> void:
if not (OS.has_feature("pc") or OS.has_feature("web_pc")):
set_process(false)
return
# Auto-connect to all hoverable buttons in group
for node in get_tree().get_nodes_in_group("hoverable"):
connect_node(node)
get_tree().node_added.connect(_on_node_added)
func _on_node_added(node: Node) -> void:
if node.is_in_group("hoverable"):
connect_node(node)
func connect_node(node: Control) -> void:
if node.mouse_entered.is_connected(_on_mouse_entered.bind(node)):
return
node.mouse_entered.connect(_on_mouse_entered.bind(node))
node.mouse_exited.connect(_on_mouse_exited.bind(node))
# Store originals
_original_scales[node.get_instance_id()] = node.scale
_original_modulates[node.get_instance_id()] = node.modulate
func _on_mouse_entered(node: Control) -> void:
hover_state_changed.emit(node, true)
if animate:
var tween = create_tween()
tween.set_parallel(true)
tween.tween_property(node, "scale", _original_scales[node.get_instance_id()] * hover_scale, 0.1)
tween.tween_property(node, "modulate", hover_color, 0.1)
func _on_mouse_exited(node: Control) -> void:
hover_state_changed.emit(node, false)
if animate:
var tween = create_tween()
tween.set_parallel(true)
tween.tween_property(node, "scale", _original_scales[node.get_instance_id()], 0.1)
tween.tween_property(node, "modulate", _original_modulates[node.get_instance_id()], 0.1)
## EXPERT USAGE:
## Add "hoverable" group to your UI buttons. AutoLoad this script.
## Instantly gives Desktop feel to Mobile-first UI.
extends Node
class_name KeybindingRemapper
## Expert PC Input Remapping
## Mobile uses hardcoded on-screen button positions. PC requires full WASD/Key rebinding.
const SAVE_PATH = "user://keybinds.ini"
func _ready() -> void:
_load_keybindings()
func rebind_action(action_name: String, new_event: InputEventKey) -> void:
# 1. Clear old events for this action
InputMap.action_erase_events(action_name)
# 2. Assign the new key
InputMap.action_add_event(action_name, new_event)
# 3. Save to disk so bindings persist between sessions
_save_keybindings()
print("Rebound '", action_name, "' to keycode: ", new_event.keycode)
func _save_keybindings() -> void:
var config = ConfigFile.new()
for action in InputMap.get_actions():
# Ignore built-in UI actions unless you want to rebind UI navigation
if action.begins_with("ui_"): continue
var events = InputMap.action_get_events(action)
if events.size() > 0 and events[0] is InputEventKey:
config.set_value("Keybindings", action, events[0].keycode)
config.save(SAVE_PATH)
func _load_keybindings() -> void:
var config = ConfigFile.new()
if config.load(SAVE_PATH) == OK:
for action in config.get_section_keys("Keybindings"):
var keycode = config.get_value("Keybindings", action)
var event = InputEventKey.new()
event.keycode = keycode
InputMap.action_erase_events(action)
InputMap.action_add_event(action, event)
extends Node3D
class_name MouseCaptureLook
## Expert Mouse Capture Controller
## When porting a mobile dual-stick shooter or swipe-look game to PC,
## you must strictly capture the mouse and accumulate relative motion.
@export var max_pitch: float = 85.0
@export var min_pitch: float = -85.0
@export var mouse_sensitivity: float = 0.002
@onready var pitch_pivot: Node3D = $PitchPivot
var is_captured: bool = false
func _ready() -> void:
capture_mouse()
func capture_mouse() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
is_captured = true
func release_mouse() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
is_captured = false
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
release_mouse()
elif event.is_action_pressed("shoot") and not is_captured:
capture_mouse()
if is_captured and event is InputEventMouseMotion:
# Rotate horizontally (yaw) around the root object
rotate_y(-event.relative.x * mouse_sensitivity)
# Rotate vertically (pitch) around the inner pivot
pitch_pivot.rotate_x(-event.relative.y * mouse_sensitivity)
# Clamp pitch to avoid looking backwards upside down
pitch_pivot.rotation.x = clamp(
pitch_pivot.rotation.x,
deg_to_rad(min_pitch),
deg_to_rad(max_pitch)
)
extends Node
class_name MultiMonitorSnapManager
## Expert Multiple Monitor Detection
## Unlike mobile, PC users frequently have 2 or 3 monitors.
## We must intelligently query the OS and spawn the game on the correct active screen.
func _ready() -> void:
# Small delay to ensure the OS window server is fully initialized post-launch
get_tree().create_timer(0.2).timeout.connect(_center_on_active_monitor)
func _center_on_active_monitor() -> void:
var screen_count = DisplayServer.get_screen_count()
if screen_count <= 1:
return # Standard single monitor, ignore
# We determine the "Active" monitor by checking where the mouse pointer currently is
var mouse_pos = DisplayServer.mouse_get_position()
var active_screen = DisplayServer.get_screen_from_rect(Rect2i(mouse_pos, Vector2i(1,1)))
# Get the position and size of the active screen
var screen_pos = DisplayServer.screen_get_position(active_screen)
var screen_size = DisplayServer.screen_get_size(active_screen)
var window_size = DisplayServer.window_get_size()
# Calculate perfect center
var new_window_pos = screen_pos + (screen_size / 2) - (window_size / 2)
# Apply
DisplayServer.window_set_current_screen(active_screen)
DisplayServer.window_set_position(new_window_pos)
print("Snapped game window to Monitor: ", active_screen)
extends Node
class_name QuitConfirmationHandler
## Expert PC Desktop Window State interceptor
## Unlike mobile iOS/Android where swiping away means immediate termination,
## PC users pressing the 'X' window button expect a "Are you sure you want to quit?" dialog
## to save their progress!
@export var confirmation_dialog: ConfirmationDialog
func _ready() -> void:
# We must explicitly tell the OS we want to handle the quit request ourselves
# Otherwise Godot simply closes immediately upon clicking 'X'
get_tree().set_auto_accept_quit(false)
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
print("X clicked. Showing confirmation popup...")
# Pause the game in the background
get_tree().paused = true
confirmation_dialog.popup_centered()
func _on_save_and_quit_pressed() -> void: # Connected via Editor Signal
# Wait for file saving to conclude...
# ...
# Finally explicitly quit
get_tree().quit()
func _on_cancel_quit_pressed() -> void: # Connected via Editor Signal
get_tree().paused = false
confirmation_dialog.hide()
extends OptionButton
class_name ResolutionDropdownManager
## Expert PC Resolution Options
## PC games must provide native resolution choices that respect the monitor's capabilities.
## Mobile simply forces the native OS resolution, but windowed PC needs explicit arrays.
const SUPPORTED_ASPECTS = [
Vector2i(1920, 1080), # 16:9
Vector2i(2560, 1440), # 16:9 1440p
Vector2i(3840, 2160), # 16:9 4K
Vector2i(3440, 1440), # 21:9 Ultrawide
Vector2i(1280, 720) # 16:9 720p (for absolute potatoes)
]
func _ready() -> void:
item_selected.connect(_on_resolution_selected)
_populate_resolutions()
func _populate_resolutions() -> void:
self.clear()
# Query the physical monitor size
var screen_size = DisplayServer.screen_get_size()
var index = 0
for res in SUPPORTED_ASPECTS:
# Don't offer resolutions larger than the user's actual monitor
if res.x <= screen_size.x and res.y <= screen_size.y:
self.add_item(str(res.x) + " x " + str(res.y), index)
self.set_item_metadata(index, res)
index += 1
func _on_resolution_selected(index: int) -> void:
var res = self.get_item_metadata(index)
# Apply to the window frame
DisplayServer.window_set_size(res)
# Center the window back on the screen after scaling
var screen_center = DisplayServer.screen_get_position() + (DisplayServer.screen_get_size() / 2)
var new_window_pos = screen_center - (res / 2)
DisplayServer.window_set_position(new_window_pos)
extends Camera2D
class_name ScrollWheelZoomer
## Expert Mouse Wheel Zoomer
## Mobile uses "Pinch-To-Zoom", but PC relies almost exclusively on the Scroll Wheel.
## This script handles discrete scroll "ticks" and interpolates smoothly to target zoom levels.
@export var zoom_speed: float = 0.1
@export var min_zoom: float = 0.5
@export var max_zoom: float = 3.0
@export var interpolation_speed: float = 10.0
var target_zoom: float = 1.0
func _ready() -> void:
target_zoom = zoom.x
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
_increment_zoom(zoom_speed)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_increment_zoom(-zoom_speed)
func _increment_zoom(amount: float) -> void:
# Add to the TARGET zoom, rather than immediately mutating the current zoom
target_zoom = clampf(target_zoom + amount, min_zoom, max_zoom)
func _process(delta: float) -> void:
# Smoothlerp the actual camera zoom towards the target scroll wheel tick
zoom.x = lerpf(zoom.x, target_zoom, delta * interpolation_speed)
zoom.y = zoom.x
extends Node
class_name UncappedFramerateController
## Expert Desktop Framerate Unlocker
## Mobile naturally caps at 60fps to save battery. PC gamers with 144Hz, 240Hz monitors
## demand uncapped framerates and toggleable V-Sync.
func apply_video_settings(use_vsync: bool, max_fps: int = 0) -> void:
if use_vsync:
# V-Sync ties the frame rate to monitor refresh rate (prevents tearing)
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
Engine.max_fps = 0 # 0 means "unlocked", let V-Sync handle it
else:
# Mailbox mode is "fast v-sync" (renders unlimited frames but only displays the newest to prevent tearing)
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_MAILBOX)
# If the user sets a hard cap (e.g. 144), enforce it here.
# A value of 0 entirely unlocks the engine, which can cause 1000+ FPS in menus and melt GPUs.
Engine.max_fps = max_fps if max_fps > 0 else 0
print("Framerate adjusted. VSync: ", use_vsync, " Max FPS: ", Engine.max_fps)