|
| 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 | + |
| 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. |
0 commit comments