> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/all-out-docs/docs-es/conceptos-centrales-del-motor/spine.md).

# Animaciones de Spine

Puedes añadir objetos interactivos animados a tu juego usando nuestra biblioteca existente de más de 50.000 rigs de Spine.

Spine es cómo funcionan los personajes animados y los props en All Out (cofres, animales, props, rigs de VFX y el jugador).

Para hacer que Spine se sienta más simple, ten este modelo mental:

* **Rig de Spine (`.spine`)**: el “plano” (huesos + nombres de animaciones + nombres de skins)
* **`Spine_Animator`**: un componente en una entidad que renderiza/actualiza un rig de Spine en el mundo
* **`Spine_Instance`**: una instancia independiente (no componente), usada para animaciones de UI o control manual del ciclo de vida
* **Skins**: “atuendos/variantes” que deciden qué imágenes son visibles
* **Animaciones**: líneas de tiempo con nombre (por ejemplo `"idle"`, `"walk"`, `"open"`)

`Spine_Bone_Transform` describe la base afín actual de un hueso en el espacio local del esqueleto. Un punto local del hueso `{x, y}` se asigna a `posición + x_axis*x + y_axis*y`.

### Cuándo usar `Spine_Animator` vs `Spine_Instance`

* Usa **`Spine_Animator`** para cualquier cosa que exista en el mundo (props, NPCs, objetos interactivos).
* Usa **`Spine_Instance`** directamente cuando quieras dibujar un Spine en la UI o necesites control manual del ciclo de vida.

El motor actualiza una `Spine_Animator` automáticamente. Una instancia independiente debe ser actualizada y destruida por tu código.

{% hint style="warning" %}
Si llamas a `Spine_Instance.create()`, debes llamar a `instance.destroy()` cuando termines (si no, filtras memoria).
{% endhint %}

### Referencia de la API de Spine (CSL)

```go
// Componente — adjunto a entidades del mundo
Spine_Animator :: class : Component {
    depth_offset: float;
    layer: s32;
    mask_in_shadow: bool;
    instance: Spine_Instance #read_only;

    // Visuales
    color: v4; // Multiplicador RGBA; {1, 1, 1, 1} deja la apariencia sin cambios.
    scale: v2;
    speed_multiplier: float;
    state_machine: State_Machine #read_only;

    set_skeleton :: method(asset: Spine_Asset) -> u64;
    get_skeleton :: method(id: u64 = 0) -> Spine_Asset;
    set_animation :: method(animation: string, loop: bool, track: s64, speed: float = 1);

    // Skins
    set_skin          :: method(skin: string);
    enable_skin       :: method(skin: string);
    disable_skin      :: method(skin: string);
    disable_all_skins :: method();
    refresh_skins     :: method();
    get_skins         :: method() -> []string;
    set_to_setup_pose :: method();

    // Desplazamientos locales de huesos (avanzado)
    get_bone_local_position :: method(bone_name: string) -> v2;
    try_get_bone_local_transform :: method(bone_name: string) -> (Spine_Bone_Transform, bool);
    set_bone_local_position :: method(bone_name: string, position: v2);

    // Opcional: controla las animaciones mediante una máquina de estados
    set_state_machine       :: method(machine: State_Machine, transfer_ownership: bool);
    set_color_replace_color :: method(color: Color_Replace_Color);
    set_material            :: method(material: Material, transfer_ownership: bool);
    get_tint                :: method() -> v4;
    set_tint                :: method(tint: v4);
    set_destroy_entity_when_done_current_animation :: method(enabled: bool);
}
```

`Spine_Animator` hereda `Component.awaken()`. Cuando tu script y el animador comienzan en la misma entidad, llama a `awaken()` antes de acceder a la instancia del animador.

### Añadiendo objetos animados a tu mundo

Hay 3 formas de añadir objetos animados a tu escena, y todas crearán un componente Spine\_Animator.

* Arrastra cualquier asset animado desde el [Catálogo de recursos](/all-out-docs/docs-es/usar-el-editor/asset-catalog.md) a tu escena
* Crea una nueva entidad y añade el componente Spine\_Animator
  * Establece el campo `Skeleton Data Asset` en un archivo .spine en tus assets
* Añade un spine animator a tu mundo usando scripting (se cubre en los ejemplos)

{% hint style="info" %}
Si tu código y el `Spine_Animator` están en la misma entidad y comienzan al mismo tiempo, llama a `spine.awaken()` antes de llamar a cualquier método de animación.
{% endhint %}

### Ejemplos

#### Cofre que se puede abrir

Un cofre interactivo que reproduce una animación de “open” y luego permanece abierto:

```go
Chest :: class : Interactable {
    opened: bool;

    ao_start :: method() {
        this.set_listener(this);
        this.set_text("Abrir");
        radius = 1.25;
        required_hold_time = 0.15;

        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_animation("idle", true, 0);
    }

    can_use :: method(player: Player) -> bool {
        return !opened;
    }

    on_interact :: method(player: Player) {
        opened = true;
        this.set_text("Abierto");

        spine := entity.get_component(Spine_Animator);
        spine.set_animation("open", false, 0);
    }
}
```

Los nombres de animaciones y skins deben coincidir con los nombres del rig.

#### Pollo caminante

Un simple ciclo “idle vs walk” basado en el movimiento:

```go
Chicken :: class : Component {
    @ao_serialize speed: float = 1.5;
    last_pos: v2;
    was_moving: bool;

    ao_start :: method() {
        last_pos = entity.world_position;

        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_animation("idle", true, 0);
    }

    ao_update :: method(dt: float) {
        // Movimiento de ejemplo (patrulla)
        entity.add_local_position({speed * dt, 0});

        moving := length_squared(entity.world_position - last_pos) > 0.0001;
        last_pos = entity.world_position;

        if moving != was_moving {
            was_moving = moving;
            spine := entity.get_component(Spine_Animator);
            spine.set_animation(moving ? "walk" : "idle", true, 0);
        }
    }
}
```

#### Coche conducible

Si quieres un comportamiento “conducible”, la forma más simple es controlar el *movimiento* con tu propia lógica de juego, y controlar la *animación* según si el coche se está moviendo.

```go
Car :: class : Component {
    @ao_serialize agent: Movement_Agent;
    @ao_serialize target: Entity;

    ao_start :: method() {
        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_animation("idle", true, 0);
    }

    ao_update :: method(dt: float) {
        if #alive(target) {
            agent.set_path_target(target.world_position, agent.movement_speed);
        }

        moving := length_squared(agent.velocity) > 0.01;
        spine := entity.get_component(Spine_Animator);
        spine.set_animation(moving ? "drive" : "idle", true, 0);
    }
}
```

{% hint style="info" %}
Si solo vas a alternar entre dos animaciones en bucle, las llamadas directas a `set_animation` suelen ser más simples que construir una máquina de estados.
{% endhint %}

### Skins (variantes/atuendos)

Algunos spines no comienzan con una skin predeterminada. Si tu entidad está “invisible”, probablemente necesites elegir una skin.

```go
spine := entity.get_component(Spine_Animator);
spine.awaken();

// Opción A: establecer una sola skin
spine.set_skin("default");
spine.refresh_skins();

// Opción B: combinar varias skins
spine.disable_all_skins();
spine.enable_skin("body");
spine.enable_skin("hat");
spine.refresh_skins();
```

{% hint style="warning" %}
Llama siempre a `refresh_skins()` después de cambiar las skins.
{% endhint %}

### Eventos de animación

Usa callbacks cuando un evento de la línea de tiempo de Spine deba activar gameplay o audio. Para un `Spine_Animator` componente, registra callbacks en su `instancia`.

```go
Spine_Event_Listener :: class : Component {
    ao_start :: method() {
        spine := entity.get_component(Spine_Animator);
        spine.awaken();

        spine.instance.set_on_event(this, proc(userdata: Object, event: Spine_Event_Data) {
            listener := userdata.(Spine_Event_Listener);
            listener.handle_spine_event(event);
        });
    }

    handle_spine_event :: method(event: Spine_Event_Data) {
        if event.event == "footstep" {
            desc := SFX.default_sfx_desc();
            desc.set_position(entity.world_position);
            SFX.play(get_asset(SFX_Asset, "sfx/footstep.wav"), desc);
        }
    }
}
```

```go
Spine_Event_Data :: struct {
    event: string;
    int_value: s64;
    float_value: float;
    string_value: string;
}
```

El callback del evento se ejecuta en la ruta predicha compartida. Llama directamente al gameplay y a los SFX; no lo protejas con `Game.is_server()` o `is_local()`.

## Instancias independientes y `UI.spine`

La API independiente proporciona control manual del ciclo de vida y actualizaciones de animación:

```go
Spine_Instance :: class {
    create  :: proc() -> Spine_Instance;
    destroy :: method();
    update  :: method(dt: float);

    set_skeleton      :: method(asset: Spine_Asset) -> u64;
    get_skeleton      :: method(id: u64 = 0) -> Spine_Asset;
    add_skeleton      :: method(asset: Spine_Asset) -> u64;
    remove_skeleton   :: method(id: u64);
    set_main_skeleton :: method(id: u64);

    set_animation :: method(animation: string, loop: bool, track: s64, speed: float = 1);
    set_skin      :: method(skin: string);
    refresh_skins :: method();

    get_bone_local_position       :: method(bone_name: string) -> v2;
    try_get_bone_local_transform  :: method(bone_name: string) -> (Spine_Bone_Transform, bool);

    set_on_event           :: method(userdata: Object, callback: proc(userdata: Object, event: Spine_Event_Data));
    set_on_animation_start :: method(userdata: Object, callback: proc(userdata: Object, animation: string));
    set_on_animation_end   :: method(userdata: Object, callback: proc(userdata: Object, animation: string));
}
```

Mantén la instancia en el estado persistente de la UI del jugador. Créala una sola vez, actualízala y dibújala desde `método ao_late_update`, y luego destrúyela desde `ao_end`:

```go
My_Player :: class : Player_Base {
    menu_spine: Spine_Instance;

    ao_start :: method() {
        menu_spine = Spine_Instance.create();
        menu_spine.set_skeleton(get_asset(Spine_Asset, "characters/shopkeeper.spine"));
        menu_spine.set_skin("default");
        menu_spine.refresh_skins();
        menu_spine.set_animation("idle", true, 0);
    }

    ao_late_update :: method(dt: float) {
        if !is_local_or_server() return;

        menu_spine.update(dt);

        UI.push_screen_draw_context();
        defer UI.pop_draw_context();
        UI.spine(UI.get_screen_rect().center(), menu_spine, {0.75, 0.75});
    }

    ao_end :: method() {
        if menu_spine != null {
            menu_spine.destroy();
            menu_spine = null;
        }
    }
}
```

No crees la instancia dentro de `método ao_late_update`; eso reiniciaría su animación en cada fotograma y filtraría la instancia anterior.

### Máquinas de estados (opcional, para lógica de animación compleja)

Puedes mantener la lógica de animación simple llamando a `set_animation` directamente. Si tienes muchos estados (idle/run/attack/hit/death), una `State_Machine` te ayuda a definir las transiciones una sola vez y luego solo establecer variables/triggers.

La idea clave: **los nombres de los estados deben coincidir con los nombres de las animaciones** en tu archivo Spine.

#### Qué hace una máquina de estados

Piensa en una máquina de estados como un pequeño “controlador de animación”:

* Defines **estados** (cada nombre de estado debe coincidir con un nombre de animación de Spine)
* Defines **variables** (`bool`, `disparadores`, `int`, `float`)
* Defines **transiciones** entre estados basadas en esas variables
* En tiempo de ejecución, solo actualizas variables (la máquina de estados elige la animación)

#### Ejemplo mínimo: idle/walk + disparador de ataque

```go
NPC_Anim :: class : Component {
    state_machine: State_Machine;
    is_moving: bool;

    ao_start :: method() {
        // 1) Crea la máquina de estados + variables
        state_machine = State_Machine.create();
        moving_var := state_machine.create_variable("is_moving", .BOOL);
        attack_var := state_machine.create_variable("attack", .TRIGGER);

        // 2) Crea una capa (se mapea a una pista de Spine, normalmente 0)
        layer := state_machine.create_layer("main", 0);

        // 3) Crea estados (los nombres deben coincidir con las animaciones en el rig de Spine)
        idle := layer.create_state("idle", true);
        walk := layer.create_state("walk", true);
        attack := layer.create_state("attack", false);
        layer.set_initial_state(idle);

        // 4) Transiciones de movimiento
        idle_to_walk := layer.create_transition(idle, walk, false);
        idle_to_walk.create_bool_condition(moving_var, true);

        walk_to_idle := layer.create_transition(walk, idle, false);
        walk_to_idle.create_bool_condition(moving_var, false);

        // 5) Ataque desde cualquier estado, luego vuelve a idle al completarse
        to_attack := layer.create_global_transition(attack, true);
        to_attack.create_trigger_condition(attack_var);

        attack_to_idle := layer.create_transition(attack, idle, true); // require_state_complete = true

        // 6) Adjuntar al animador
        spine := entity.get_component(Spine_Animator);
        spine.awaken();
        spine.set_state_machine(state_machine, true); // transfer_ownership = true
    }

    ao_update :: method(dt: float) {
        state_machine.set_bool("is_moving", is_moving);
    }

    do_attack :: method() {
        state_machine.set_trigger("attack");
    }
}
```

{% hint style="info" %}
Si pasas `transfer_ownership = true` a `set_state_machine`, el animador destruirá la máquina de estados por ti. De lo contrario, debes destruirla tú mismo.
{% endhint %}

### Solución de problemas

P: Arrastré un asset de spine a mi escena y no veo ningún objeto, solo una entidad vacía

R: ¡Asegúrate de hacer clic en "Agregar skin" y seleccionar una variante de spine para aplicar! Algunos spines no comienzan con una skin predeterminada.

P: Mi script falla cuando llamo a métodos de animación en mi Spine\_Animator

R: Si tu script y el `Spine_Animator` comienzan al mismo tiempo, llama a `spine.awaken()` antes de llamar a cualquier método de animación.

P: Cambié las skins pero no pasó nada

R: Después de cualquier cambio de skin (`set_skin`, `enable_skin`, `disable_skin`, `disable_all_skins`), debes llamar a `refresh_skins()`.

### Skins/animaciones personalizadas del jugador

Si quieres añadir animaciones personalizadas a tu jugador, puedes hacerlo fusionando el rig base de Spine del jugador de All Out con un rig personalizado que contenga más animaciones. Podemos proporcionar muchas de estas; solo [Soporte para desarrolladores](/all-out-docs/docs-es/dandolo-todo/developer-support.md).

### El formato del rig de Spine

Los rigs de Spine incluyen:

* Un archivo de esqueleto (`.spine`) con los huesos, skins, animaciones y estructura.
* Un archivo atlas (`.atlas`) que asigna los attachments a imágenes.
* Cada página de imagen nombrada por el atlas. Una exportación de una sola página normalmente tiene un `.png`.

### Creando tus propias animaciones

{% hint style="warning" %}
Actualmente, para crear tus propias animaciones o rigs, necesitarás tener una copia de Esoteric Spine. Sin embargo, proporcionamos una enorme biblioteca de rigs de Spine y animaciones existentes como parte de [Catálogo de recursos](/all-out-docs/docs-es/usar-el-editor/asset-catalog.md) para que las uses, ¡y recomendamos empezar por ahí!
{% endhint %}

#### Formato de exportación

Para usar un rig personalizado de Spine en All Out, exporta el paquete estándar de Spine en tu `res` carpeta:

* `something.spine`
* `something.atlas`
* Cada archivo de imagen nombrado por `something.atlas`

Si no estás seguro de los ajustes de exportación, empieza a partir de un asset de Spine existente de All Out desde el Asset Catalog y replica su estructura, ¡o contacta con nosotros!
