"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."
Resources
1Install
npx skillscat add thedivergentai/gd-agentic-skills/godot-adapt-mobile-to-desktop Install via the SkillsCat registry.
This skill provides patterns for adapting mobile games to desktop platforms by adding mouse and keyboard controls, increasing resolution and graphical fidelity, expanding UI layouts, and implementing settings menus and window management. It addresses the problem of mobile-first games feeling awkward on desktop due to touch-only controls and fixed mobile resolutions. Developers should use it when porting mobile games to desktop or creating cross-platform releases.
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.
desktop_input_adapter.gd
Bridges virtual joystick logic to keyboard WASD. Maps keys to input vectors and handles high-DPI UI scaling.
hover_bridge.gd
Re-enables desktop hover features in mobile-first codebases. Adds tooltips, mouse enter/exit signals, and hover visual feedback.
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.9Graphics 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 = trueWindow 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)
passPerformance 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 50Testing 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)
Reference
- Master Skill: godot-master