# Godot Pixel Art

> Configura Godot 4.x para pixel art nítido en PC y Android (viewport, zoom entero, stretch, cámara, velocidad, efectos). Usar al crear proyectos pixel art, exportar a móvil, o cuando haya píxeles deformados, blur, bandas irregulares o baja fluidez con time_scale.

- Skill: `capotedeveloper/godot-pixel-art` (Agent Skill)
- Install (CLI): `npx skillmds@latest add capotedeveloper/godot-pixel-art`
- Raw SKILL.md: https://api.skillmd.com/api/skills/capotedeveloper/godot-pixel-art/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: capotedeveloper (https://skillmd.com/u/capotedeveloper)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/capotedeveloper/godot-pixel-art

---


# 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

1. **Zoom de cámara SIEMPRE uniforme** (`Vector2(z, z)`) y **entero en móvil** (`floor`). Nunca `1.6`, nunca `zoom.y ≠ zoom.x`.
2. **1 píxel de textura = 1 unidad de mundo** + `TEXTURE_FILTER_NEAREST` en sprites.
3. **PC y móvil pueden usar viewports distintos**; el móvil debe evitar reescalar 675→1080 (×1.6 fraccional).
4. **Velocidad de juego**: `Engine.time_scale = 1` + `delta_juego()` + `speed_scale` en animaciones. No `time_scale` para x1.5/x2.
5. **Efectos `_draw()`**: bloques de píxeles enteros (`draw_rect` 1×1), posiciones `.round()`. No óvalos vectoriales pequeños.
6. **`queue_free()` en bucle**: usar `remove_child()` antes o un solo `if`, nunca `while` solo con `queue_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`

```ini
[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)

```gdscript
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`)

```gdscript
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`)

```gdscript
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 × zoom` sea 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) con `Z = 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
- [ ] `ConfigDisplay` autoload activo
- [ ] Overrides `.android` en 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=integer` con 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.y` distinto para “rellenar” pantalla alta.
- Viewport 540 + zoom 1.6 para “encuadre exacto” (deforma píxeles internos).
- `scale_mode=integer` en 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`.

