Godot 4 — Pixel art nítido (PC + Android)
Referencia validada en Godot 4.7, GL Compatibility, móviles 1080 px de ancho.
Reglas de oro
- Zoom de cámara SIEMPRE uniforme (
Vector2(z, z)) y entero en móvil (floor). Nunca1.6, nuncazoom.y ≠ zoom.x. - 1 píxel de textura = 1 unidad de mundo +
TEXTURE_FILTER_NEARESTen sprites. - PC y móvil pueden usar viewports distintos; el móvil debe evitar reescalar 675→1080 (×1.6 fraccional).
- Velocidad de juego:
Engine.time_scale = 1+delta_juego()+speed_scaleen animaciones. Notime_scalepara x1.5/x2. - Efectos
_draw(): bloques de píxeles enteros (draw_rect1×1), posiciones.round(). No óvalos vectoriales pequeños. queue_free()en bucle: usarremove_child()antes o un soloif, nuncawhilesolo conqueue_free().
Por qué fallan los píxeles (diagnóstico rápido)
| Síntoma | Causa habitual |
|---|---|
| Algunas filas/columnas más gruesas | Zoom no entero (ej. 1.6) o reescalado fraccional del framebuffer |
| Estirado solo en Y | zoom.y ≠ zoom.x (p. ej. compensar pantallas altas con expand) |
| PC bien, móvil mal | Viewport PC reescalado ×1.6; hace falta viewport nativo 1080 en móvil |
| Huellas más finas que sprites | _draw() vectorial vs sprites NEAREST |
| x1.5/x2 “a tirones” | Engine.time_scale en Android |
Configuración project.godot
[display]
window/size/viewport_width=675
window/size/viewport_height=1350
window/stretch/mode="viewport"
window/stretch/aspect="keep"
window/stretch/scale_mode="fractional"
window/size/viewport_width.android=1080
window/size/viewport_height.android=2160
window/stretch/scale_mode.android="integer"
[rendering]
textures/canvas_textures/default_texture_filter=0
renderer/rendering_method.mobile="gl_compatibility"
2d/snap/snap_2d_transforms_to_pixel=true
2d/snap/snap_2d_vertices_to_pixel=true
environment/defaults/default_clear_color=Color(0.101961, 0.101961, 0.180392, 1)
Autoload ConfigDisplay.gd (móvil)
extends Node
const VIEWPORT_MOVIL := Vector2i(1080, 2160)
func _ready() -> void:
if not (OS.has_feature("android") or OS.has_feature("mobile")):
return
var ventana := get_window()
ventana.content_scale_size = VIEWPORT_MOVIL
ventana.content_scale_mode = Window.CONTENT_SCALE_MODE_VIEWPORT
ventana.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_KEEP
ventana.content_scale_stretch = Window.CONTENT_SCALE_STRETCH_INTEGER
Registrar en [autoload] antes que otros scripts de juego.
Cámara (Camara.gd)
const ANCHO_MUNDO_VISIBLE := 337.5 # viewport_ancho / zoom_deseado (675/2)
func _actualizar_zoom_camara() -> void:
var ancho: float = get_viewport().get_visible_rect().size.x
var z: float = ancho / ANCHO_MUNDO_VISIBLE
if OS.has_feature("android") or OS.has_feature("mobile"):
z = maxf(1.0, floorf(z))
zoom = Vector2(z, z)
func _snap_a_pixel(pos: Vector2) -> Vector2:
return (pos * zoom).round() / zoom
- PC 675 px → zoom 2 → cada texel = 2 px en buffer (entero).
- Android 1080 px → zoom 3 → ~7 % más ancho visible; 1 px buffer = 1 px pantalla (sin ×1.6).
- Offset HUD:
(centro_viewport - zona_juego.get_center()).round().
Velocidad de juego (Juego.gd)
func aplicar_velocidad_juego() -> void:
Engine.time_scale = 1.0
func delta_juego(delta: float) -> float:
return delta * factor_velocidad_juego()
Aplicar delta_juego() en movimiento, timers de juego y speed_scale en animaciones al moverse.
Sprites y efectos
- Todos los
AnimatedSprite2D/Sprite2D:texture_filter = NEAREST. - Celdas del mundo (ej. 36 px): preferir zoom entero donde
celda × zoomsea entero. - Huellas / partículas
_draw(): rects 1×1 en unidades mundo; spawn con.round().
Elegir viewport base (nuevo proyecto)
Fijar ancho visible de mundo W (ej. 337.5) y zoom entero Z:
viewport_ancho = W × Z- PC ejemplo: 337.5 × 2 = 675
- Comprobar:
ancho_pantalla_objetivo / viewport_ancho— si no es entero, usar viewport nativo en móvil (1080) conZ = floor(1080/W).
Evitar 675 → 1080 con escala fraccional como única solución móvil.
Checklist antes de exportar Android
- Zoom cámara uniforme; entero en móvil
-
ConfigDisplayautoload activo - Overrides
.androiden project.godot - NEAREST en sprites; sombras incluidas
-
default_clear_color= color del fondo (franjas 20:9) - Velocidad sin
time_scale - Captura con zoom: píxeles cuadrados en personaje y efectos
Anti-patrones (no usar)
stretch scale_mode=integercon viewport 675 en 1080 px (bandas laterales) como única vía si el usuario rechaza bandas — usar viewport móvil nativo.stretch aspect=expand+zoom.ydistinto para “rellenar” pantalla alta.- Viewport 540 + zoom 1.6 para “encuadre exacto” (deforma píxeles internos).
scale_mode=integeren 540 + zoom 1.6 (mismo problema).
Referencia de proyecto
Implementación completa: Castle Survivor (castle-survivor) — ConfigDisplay.gd, Camara.gd, Huella.gd, EXPORTAR_ANDROID.md.