Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 103 additions & 0 deletions animation/3d_animations/example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: 3D Animations - Skinned Model
brief: Learn how to play 3D animations from a GLB model using skinned model material.
author: Defold Foundation
scripts: main.script, main.gui_script
thumbnail: thumbnail.webp
tags: animation, model, gui, input, message-passing
---

This example shows how to play skeletal animations from a skinned GLB model using `model.play_anim()`.

Click or tap any button to play the matching animation on the character. The selected button is dimmed to show the currently chosen animation.

The project uses a character from the [Quaternius Universal Animation Library](https://quaternius.itch.io/universal-animation-library).

## What You'll Learn

* How to play skeletal model animations with `model.play_anim()`
* How to use `model_skinned.material` for animated skinned models
* How to build a simple GUI animation picker
* How to detect GUI button clicks with `gui.pick_node()`
* How to send animation commands from a GUI script to a game object script with `msg.post()`

## Setup

The collection contains 4 game objects:

`character`
: Contains the model component and `main.script`. The model component uses the animated GLB model and the built-in `/builtins/materials/model_skinned.material`.

`gui`
: Contains `main.gui` and `main.gui_script`. The GUI contains one button for each animation clip. The button boxes are placed on the `boxes` layer, and the text labels are placed on the `texts` layer so the labels render above the boxes.

`plane`
: Contains a simple static model for the floor.

`camera`
: Contains a camera component to show the setup in game.

![setup](setup.png)

The model must use a skinned material. If the model uses `/builtins/materials/model.material`, the animation can be started in code, but the mesh will not visibly follow the skeleton. For skeletal animation, use `/builtins/materials/model_skinned.material`.

## How It Works

The standard free file version contains 45 animation clips, including idle, walk, jog, sprint, crouch, jump, swimming, spell, pistol, punch, and sword animations.
The GUI lists them as buttons. Clicking a button sends a message from the GUI script to the model script, which then plays the selected animation on the model component.

The model script owns animation playback. It defines a message id called `play_model_animation` and starts a default animation in `init()`:

```lua
local MSG_PLAY_MODEL_ANIMATION = hash("play_model_animation")
local DEFAULT_ANIMATION = hash("Sword_Idle")

local function play_animation(animation_id)
model.play_anim("#model", animation_id, go.PLAYBACK_LOOP_FORWARD)
end

function init(self)
play_animation(DEFAULT_ANIMATION)
end
```

When the script receives a `play_model_animation` message, it reads the animation id from the message and plays that animation on the local model component:

```lua
function on_message(self, message_id, message, sender)
if message_id == MSG_PLAY_MODEL_ANIMATION then
play_animation(message.animation_id)
end
end
```

The GUI script owns the button list and input handling. It stores all animation names in an `ANIMATIONS` table. During `init()`, it finds the matching GUI nodes, sets the button text, and stores each button node together with its animation name.

The GUI script also acquires input focus:

```lua
msg.post(".", "acquire_input_focus")
```

When the user clicks or taps, `on_input()` checks every button with `gui.pick_node()`. If the pointer is inside a button, the GUI script sends a message to the model script:

```lua
msg.post(MODEL_SCRIPT, MSG_PLAY_MODEL_ANIMATION, {
animation_id = hash(button.animation_id),
animation_name = button.animation_id,
})
```

The GUI script does not call `model.play_anim()` directly. This keeps the GUI responsible only for interface and input, while the model game object remains responsible for model animation playback.

## Messages from GUI to Game objects

The GUI component and the model component live on different game objects. Instead of making the GUI script directly control the model component, the GUI sends a small command message:

```lua
play_model_animation
```

This is a common Defold pattern. The sender does not need to know how animation playback is implemented. It only sends the requested animation id. The receiver decides what to do with it.

This makes the example easy to extend. For example, the model script could later add animation blending, validation, transition rules, sound effects, or root motion handling without changing the GUI script.
101 changes: 101 additions & 0 deletions animation/3d_animations/example/main.collection
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: "main"
scale_along_z: 1
embedded_instances {
id: "camera"
data: "embedded_components {\n"
" id: \"camera\"\n"
" type: \"camera\"\n"
" data: \"aspect_ratio: 1.0\\n"
"fov: 0.7854\\n"
"near_z: 0.1\\n"
"far_z: 6.0\\n"
"auto_aspect_ratio: 1\\n"
"\"\n"
"}\n"
""
position {
y: 2.0
z: 1.0
}
rotation {
x: -0.17364818
w: 0.9848077
}
}
embedded_instances {
id: "plane"
data: "embedded_components {\n"
" id: \"model\"\n"
" type: \"model\"\n"
" data: \"mesh: \\\"/builtins/assets/gltf/quad_2x2.gltf\\\"\\n"
"name: \\\"{{NAME}}\\\"\\n"
"materials {\\n"
" name: \\\"default\\\"\\n"
" material: \\\"/builtins/materials/model.material\\\"\\n"
" textures {\\n"
" sampler: \\\"tex0\\\"\\n"
" texture: \\\"/assets/textures/pixel_white.png\\\"\\n"
" }\\n"
"}\\n"
"\"\n"
"}\n"
""
position {
z: -2.0
}
rotation {
x: -0.70710677
w: 0.70710677
}
scale3 {
x: 2.0
y: 2.0
z: 2.0
}
}
embedded_instances {
id: "character"
data: "components {\n"
" id: \"main\"\n"
" component: \"/example/main.script\"\n"
"}\n"
"embedded_components {\n"
" id: \"model\"\n"
" type: \"model\"\n"
" data: \"mesh: \\\"/assets/meshes/UAL1_Standard.glb\\\"\\n"
"skeleton: \\\"/assets/meshes/UAL1_Standard.glb\\\"\\n"
"animations: \\\"/assets/meshes/UAL1_Standard.glb\\\"\\n"
"default_animation: \\\"Sword_Idle\\\"\\n"
"name: \\\"{{NAME}}\\\"\\n"
"materials {\\n"
" name: \\\"M_Joints\\\"\\n"
" material: \\\"/builtins/materials/model_skinned.material\\\"\\n"
" textures {\\n"
" sampler: \\\"tex0\\\"\\n"
" texture: \\\"/assets/textures/pixel_orange.png\\\"\\n"
" }\\n"
"}\\n"
"materials {\\n"
" name: \\\"M_Main\\\"\\n"
" material: \\\"/builtins/materials/model_skinned.material\\\"\\n"
" textures {\\n"
" sampler: \\\"tex0\\\"\\n"
" texture: \\\"/assets/textures/pixel_blue.png\\\"\\n"
" }\\n"
"}\\n"
"\"\n"
"}\n"
""
position {
y: 0.2
z: -2.0
}
}
embedded_instances {
id: "gui"
data: "components {\n"
" id: \"gui\"\n"
" component: \"/example/main.gui\"\n"
"}\n"
""
}
Loading
Loading