Skip to content

Commit 2036b2e

Browse files
authored
Added 3D animations example. (#198)
1 parent e9c812c commit 2036b2e

13 files changed

Lines changed: 1893 additions & 0 deletions

File tree

7.74 MB
Binary file not shown.
119 Bytes
Loading
119 Bytes
Loading
119 Bytes
Loading

animation/3d_animations/example.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
title: 3D Animations - Skinned Model
3+
brief: Learn how to play 3D animations from a GLB model using skinned model material.
4+
author: Defold Foundation
5+
scripts: main.script, main.gui_script
6+
thumbnail: thumbnail.webp
7+
tags: animation, model, gui, input, message-passing
8+
---
9+
10+
This example shows how to play skeletal animations from a skinned GLB model using `model.play_anim()`.
11+
12+
Click or tap any button to play the matching animation on the character. The selected button is dimmed to show the currently chosen animation.
13+
14+
The project uses a character from the [Quaternius Universal Animation Library](https://quaternius.itch.io/universal-animation-library).
15+
16+
## What You'll Learn
17+
18+
* How to play skeletal model animations with `model.play_anim()`
19+
* How to use `model_skinned.material` for animated skinned models
20+
* How to build a simple GUI animation picker
21+
* How to detect GUI button clicks with `gui.pick_node()`
22+
* How to send animation commands from a GUI script to a game object script with `msg.post()`
23+
24+
## Setup
25+
26+
The collection contains 4 game objects:
27+
28+
`character`
29+
: Contains the model component and `main.script`. The model component uses the animated GLB model and the built-in `/builtins/materials/model_skinned.material`.
30+
31+
`gui`
32+
: 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.
33+
34+
`plane`
35+
: Contains a simple static model for the floor.
36+
37+
`camera`
38+
: Contains a camera component to show the setup in game.
39+
40+
![setup](setup.png)
41+
42+
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`.
43+
44+
## How It Works
45+
46+
The standard free file version contains 45 animation clips, including idle, walk, jog, sprint, crouch, jump, swimming, spell, pistol, punch, and sword animations.
47+
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.
48+
49+
The model script owns animation playback. It defines a message id called `play_model_animation` and starts a default animation in `init()`:
50+
51+
```lua
52+
local MSG_PLAY_MODEL_ANIMATION = hash("play_model_animation")
53+
local DEFAULT_ANIMATION = hash("Sword_Idle")
54+
55+
local function play_animation(animation_id)
56+
model.play_anim("#model", animation_id, go.PLAYBACK_LOOP_FORWARD)
57+
end
58+
59+
function init(self)
60+
play_animation(DEFAULT_ANIMATION)
61+
end
62+
```
63+
64+
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:
65+
66+
```lua
67+
function on_message(self, message_id, message, sender)
68+
if message_id == MSG_PLAY_MODEL_ANIMATION then
69+
play_animation(message.animation_id)
70+
end
71+
end
72+
```
73+
74+
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.
75+
76+
The GUI script also acquires input focus:
77+
78+
```lua
79+
msg.post(".", "acquire_input_focus")
80+
```
81+
82+
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:
83+
84+
```lua
85+
msg.post(MODEL_SCRIPT, MSG_PLAY_MODEL_ANIMATION, {
86+
animation_id = hash(button.animation_id),
87+
animation_name = button.animation_id,
88+
})
89+
```
90+
91+
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.
92+
93+
## Messages from GUI to Game objects
94+
95+
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:
96+
97+
```lua
98+
play_model_animation
99+
```
100+
101+
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.
102+
103+
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.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: "main"
2+
scale_along_z: 1
3+
embedded_instances {
4+
id: "camera"
5+
data: "embedded_components {\n"
6+
" id: \"camera\"\n"
7+
" type: \"camera\"\n"
8+
" data: \"aspect_ratio: 1.0\\n"
9+
"fov: 0.7854\\n"
10+
"near_z: 0.1\\n"
11+
"far_z: 6.0\\n"
12+
"auto_aspect_ratio: 1\\n"
13+
"\"\n"
14+
"}\n"
15+
""
16+
position {
17+
y: 2.0
18+
z: 1.0
19+
}
20+
rotation {
21+
x: -0.17364818
22+
w: 0.9848077
23+
}
24+
}
25+
embedded_instances {
26+
id: "plane"
27+
data: "embedded_components {\n"
28+
" id: \"model\"\n"
29+
" type: \"model\"\n"
30+
" data: \"mesh: \\\"/builtins/assets/gltf/quad_2x2.gltf\\\"\\n"
31+
"name: \\\"{{NAME}}\\\"\\n"
32+
"materials {\\n"
33+
" name: \\\"default\\\"\\n"
34+
" material: \\\"/builtins/materials/model.material\\\"\\n"
35+
" textures {\\n"
36+
" sampler: \\\"tex0\\\"\\n"
37+
" texture: \\\"/assets/textures/pixel_white.png\\\"\\n"
38+
" }\\n"
39+
"}\\n"
40+
"\"\n"
41+
"}\n"
42+
""
43+
position {
44+
z: -2.0
45+
}
46+
rotation {
47+
x: -0.70710677
48+
w: 0.70710677
49+
}
50+
scale3 {
51+
x: 2.0
52+
y: 2.0
53+
z: 2.0
54+
}
55+
}
56+
embedded_instances {
57+
id: "character"
58+
data: "components {\n"
59+
" id: \"main\"\n"
60+
" component: \"/example/main.script\"\n"
61+
"}\n"
62+
"embedded_components {\n"
63+
" id: \"model\"\n"
64+
" type: \"model\"\n"
65+
" data: \"mesh: \\\"/assets/meshes/UAL1_Standard.glb\\\"\\n"
66+
"skeleton: \\\"/assets/meshes/UAL1_Standard.glb\\\"\\n"
67+
"animations: \\\"/assets/meshes/UAL1_Standard.glb\\\"\\n"
68+
"default_animation: \\\"Sword_Idle\\\"\\n"
69+
"name: \\\"{{NAME}}\\\"\\n"
70+
"materials {\\n"
71+
" name: \\\"M_Joints\\\"\\n"
72+
" material: \\\"/builtins/materials/model_skinned.material\\\"\\n"
73+
" textures {\\n"
74+
" sampler: \\\"tex0\\\"\\n"
75+
" texture: \\\"/assets/textures/pixel_orange.png\\\"\\n"
76+
" }\\n"
77+
"}\\n"
78+
"materials {\\n"
79+
" name: \\\"M_Main\\\"\\n"
80+
" material: \\\"/builtins/materials/model_skinned.material\\\"\\n"
81+
" textures {\\n"
82+
" sampler: \\\"tex0\\\"\\n"
83+
" texture: \\\"/assets/textures/pixel_blue.png\\\"\\n"
84+
" }\\n"
85+
"}\\n"
86+
"\"\n"
87+
"}\n"
88+
""
89+
position {
90+
y: 0.2
91+
z: -2.0
92+
}
93+
}
94+
embedded_instances {
95+
id: "gui"
96+
data: "components {\n"
97+
" id: \"gui\"\n"
98+
" component: \"/example/main.gui\"\n"
99+
"}\n"
100+
""
101+
}

0 commit comments

Comments
 (0)