skills/godot-adapt-mobile-to-desktop/SKILL.md
Expert patterns for scaling mobile games to desktop including mouse/keyboard controls, increased resolution and graphical fidelity, expanded UI layouts, settings menus, window management, and platform-specific features. Use when creating desktop ports or cross-platform releases. Trigger keywords: mouse_controls, keyboard_shortcuts, resolution_scaling, graphics_settings, fullscreen_toggle, window_modes, Steam_integration, desktop_optimization.
npx skillsauth add thedivergentai/gd-agentic-skills godot-adapt-mobile-to-desktopInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert guidance for scaling mobile games to desktop platforms.
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
Expert Mouse Capture Controller that completely overrides mobile touch/swipe logic by accumulating InputEventMouseMotion.relative against pitch/yaw variables while clamped.
Crucial lifecycle manager handling the expectation of PC gamers to toggle between Windowed, Fullscreen Exclusive, and modern Borderless Fullscreen via DisplayServer flags.
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.
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.
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.
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.
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.
Replaces the mobile Pinch-to-Zoom gesture with discrete physical mouse wheel ticks, smoothly interpolating the Camera2D zoom continuously via delta.
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.
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.
# 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 Arrow
# 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, Tab
# 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
# 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 = true
# 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)
# 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_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)
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)
# 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()
# 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
# 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)
# 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
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()
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")
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()
development
Godot Expert Auditor: Aurelius. Exhaustive never-list enforcement and architectural slap-down for Godot 4.6 projects.
development
--- name:# Godot Expert Analyst: Anara ## Visionary Architect of Godot 4.6+ Excellence > "Scale is not a feature; it is a philosophy. I don't look at what your game is today; I look at whether it can survive tomorrow." — Anara You are **Anara**, the visionary architect of Godot 4.6+ excellence. You evaluate projects not for "if they work", but for "how well they scale". Your purpose is to certify professional-grade projects and provide the blueprint for architectural transcendence. Your voice
development
Expert blueprint for AI pathfinding (tower defense, RTS, stealth) using NavigationAgent2D/3D, NavigationServer, avoidance, and dynamic navigation mesh generation. Use when implementing enemy AI, NPC movement, or obstacle avoidance. Keywords NavigationAgent2D, NavigationRegion2D, pathfinding, NavigationServer, avoidance, baking, NavigationObstacle.
development
Expert patterns for Godot audio including AudioStreamPlayer variants (2D positional, 3D spatial), AudioBus mixing architecture, dynamic effects (reverb, EQ,compression), audio pooling for performance, music transitions (crossfade, bpm-sync), and procedural audio generation. Use for music systems, sound effects, spatial audio, or audio-reactive gameplay. Trigger keywords: AudioStreamPlayer, AudioStreamPlayer2D, AudioStreamPlayer3D, AudioBus, AudioServer, AudioEffect, music_crossfade, audio_pool, positional_audio, reverb, bus_volume.