diff --git a/animation/3d_animations/assets/meshes/UAL1_Standard.glb b/animation/3d_animations/assets/meshes/UAL1_Standard.glb new file mode 100644 index 00000000..410b30c6 Binary files /dev/null and b/animation/3d_animations/assets/meshes/UAL1_Standard.glb differ diff --git a/animation/3d_animations/assets/textures/pixel_blue.png b/animation/3d_animations/assets/textures/pixel_blue.png new file mode 100644 index 00000000..5e924542 Binary files /dev/null and b/animation/3d_animations/assets/textures/pixel_blue.png differ diff --git a/animation/3d_animations/assets/textures/pixel_orange.png b/animation/3d_animations/assets/textures/pixel_orange.png new file mode 100644 index 00000000..32747cae Binary files /dev/null and b/animation/3d_animations/assets/textures/pixel_orange.png differ diff --git a/animation/3d_animations/assets/textures/pixel_white.png b/animation/3d_animations/assets/textures/pixel_white.png new file mode 100644 index 00000000..818c71d0 Binary files /dev/null and b/animation/3d_animations/assets/textures/pixel_white.png differ diff --git a/animation/3d_animations/example.md b/animation/3d_animations/example.md new file mode 100644 index 00000000..3c0a63fb --- /dev/null +++ b/animation/3d_animations/example.md @@ -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. diff --git a/animation/3d_animations/example/main.collection b/animation/3d_animations/example/main.collection new file mode 100644 index 00000000..ce29110f --- /dev/null +++ b/animation/3d_animations/example/main.collection @@ -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" + "" +} diff --git a/animation/3d_animations/example/main.gui b/animation/3d_animations/example/main.gui new file mode 100644 index 00000000..fe2de941 --- /dev/null +++ b/animation/3d_animations/example/main.gui @@ -0,0 +1,1506 @@ +script: "/example/main.gui_script" +fonts { + name: "always_on_top" + font: "/builtins/fonts/debug/always_on_top.font" +} +nodes { + position { + x: 85.0 + y: 700.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_01" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "A_TPose" + font: "always_on_top" + id: "button_text_01" + parent: "button_01" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 670.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_02" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Crouch_Fwd_Loop" + font: "always_on_top" + id: "button_text_02" + parent: "button_02" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 640.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_03" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Crouch_Idle_Loop" + font: "always_on_top" + id: "button_text_03" + parent: "button_03" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 610.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_04" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Dance_Loop" + font: "always_on_top" + id: "button_text_04" + parent: "button_04" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 580.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_05" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Death01" + font: "always_on_top" + id: "button_text_05" + parent: "button_05" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 550.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_06" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Driving_Loop" + font: "always_on_top" + id: "button_text_06" + parent: "button_06" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 520.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_07" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Fixing_Kneeling" + font: "always_on_top" + id: "button_text_07" + parent: "button_07" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 490.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_08" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Hit_Chest" + font: "always_on_top" + id: "button_text_08" + parent: "button_08" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 460.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_09" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Hit_Head" + font: "always_on_top" + id: "button_text_09" + parent: "button_09" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 430.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_10" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Idle_Loop" + font: "always_on_top" + id: "button_text_10" + parent: "button_10" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 400.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_11" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Idle_Talking_Loop" + font: "always_on_top" + id: "button_text_11" + parent: "button_11" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 370.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_12" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Idle_Torch_Loop" + font: "always_on_top" + id: "button_text_12" + parent: "button_12" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 340.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_13" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Interact" + font: "always_on_top" + id: "button_text_13" + parent: "button_13" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 310.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_14" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Jog_Fwd_Loop" + font: "always_on_top" + id: "button_text_14" + parent: "button_14" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 280.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_15" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Jump_Land" + font: "always_on_top" + id: "button_text_15" + parent: "button_15" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 250.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_16" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Jump_Loop" + font: "always_on_top" + id: "button_text_16" + parent: "button_16" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 220.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_17" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Jump_Start" + font: "always_on_top" + id: "button_text_17" + parent: "button_17" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 190.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_18" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "PickUp_Table" + font: "always_on_top" + id: "button_text_18" + parent: "button_18" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 160.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_19" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Pistol_Aim_Down" + font: "always_on_top" + id: "button_text_19" + parent: "button_19" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 130.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_20" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Pistol_Aim_Neutral" + font: "always_on_top" + id: "button_text_20" + parent: "button_20" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 100.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_21" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Pistol_Aim_Up" + font: "always_on_top" + id: "button_text_21" + parent: "button_21" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 70.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_22" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Pistol_Idle_Loop" + font: "always_on_top" + id: "button_text_22" + parent: "button_22" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 85.0 + y: 40.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_23" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Pistol_Reload" + font: "always_on_top" + id: "button_text_23" + parent: "button_23" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 700.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_24" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Pistol_Shoot" + font: "always_on_top" + id: "button_text_24" + parent: "button_24" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 670.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_25" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Punch_Cross" + font: "always_on_top" + id: "button_text_25" + parent: "button_25" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 640.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_26" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Punch_Jab" + font: "always_on_top" + id: "button_text_26" + parent: "button_26" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 610.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_27" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Push_Loop" + font: "always_on_top" + id: "button_text_27" + parent: "button_27" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 580.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_28" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Roll" + font: "always_on_top" + id: "button_text_28" + parent: "button_28" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 550.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_29" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Roll_RM" + font: "always_on_top" + id: "button_text_29" + parent: "button_29" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 520.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_30" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sitting_Enter" + font: "always_on_top" + id: "button_text_30" + parent: "button_30" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 490.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_31" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sitting_Exit" + font: "always_on_top" + id: "button_text_31" + parent: "button_31" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 460.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_32" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sitting_Idle_Loop" + font: "always_on_top" + id: "button_text_32" + parent: "button_32" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 430.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_33" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + scale { + x: 0.9 + y: 0.9 + } + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sitting_Talking_Loop" + font: "always_on_top" + id: "button_text_33" + parent: "button_33" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 400.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_34" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Spell_Simple_Enter" + font: "always_on_top" + id: "button_text_34" + parent: "button_34" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 370.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_35" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Spell_Simple_Exit" + font: "always_on_top" + id: "button_text_35" + parent: "button_35" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 340.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_36" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + scale { + x: 0.85 + y: 0.85 + } + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Spell_Simple_Idle_Loop" + font: "always_on_top" + id: "button_text_36" + parent: "button_36" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 310.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_37" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Spell_Simple_Shoot" + font: "always_on_top" + id: "button_text_37" + parent: "button_37" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 280.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_38" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sprint_Loop" + font: "always_on_top" + id: "button_text_38" + parent: "button_38" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 250.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_39" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Swim_Fwd_Loop" + font: "always_on_top" + id: "button_text_39" + parent: "button_39" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 220.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_40" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Swim_Idle_Loop" + font: "always_on_top" + id: "button_text_40" + parent: "button_40" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 190.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_41" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sword_Attack" + font: "always_on_top" + id: "button_text_41" + parent: "button_41" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 160.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_42" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sword_Attack_RM" + font: "always_on_top" + id: "button_text_42" + parent: "button_42" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 130.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_43" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Sword_Idle" + font: "always_on_top" + id: "button_text_43" + parent: "button_43" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 100.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_44" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Walk_Formal_Loop" + font: "always_on_top" + id: "button_text_44" + parent: "button_44" + layer: "texts" + inherit_alpha: true +} +nodes { + position { + x: 640.0 + y: 70.0 + } + size { + x: 150.0 + y: 24.0 + } + type: TYPE_BOX + id: "button_45" + layer: "boxes" + inherit_alpha: true + size_mode: SIZE_MODE_AUTO +} +nodes { + size { + x: 150.0 + y: 24.0 + } + color { + x: 0.0 + y: 0.0 + z: 0.0 + } + type: TYPE_TEXT + text: "Walk_Loop" + font: "always_on_top" + id: "button_text_45" + parent: "button_45" + layer: "texts" + inherit_alpha: true +} +layers { + name: "boxes" +} +layers { + name: "texts" +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT diff --git a/animation/3d_animations/example/main.gui_script b/animation/3d_animations/example/main.gui_script new file mode 100644 index 00000000..0489300d --- /dev/null +++ b/animation/3d_animations/example/main.gui_script @@ -0,0 +1,119 @@ +local TOUCH = hash("touch") -- <1> +local MSG_PLAY_MODEL_ANIMATION = hash("play_model_animation") -- <2> +local MODEL_SCRIPT = "/character#main" -- <3> + +local ANIMATIONS = { -- <4> + "A_TPose", + "Crouch_Fwd_Loop", + "Crouch_Idle_Loop", + "Dance_Loop", + "Death01", + "Driving_Loop", + "Fixing_Kneeling", + "Hit_Chest", + "Hit_Head", + "Idle_Loop", + "Idle_Talking_Loop", + "Idle_Torch_Loop", + "Interact", + "Jog_Fwd_Loop", + "Jump_Land", + "Jump_Loop", + "Jump_Start", + "PickUp_Table", + "Pistol_Aim_Down", + "Pistol_Aim_Neutral", + "Pistol_Aim_Up", + "Pistol_Idle_Loop", + "Pistol_Reload", + "Pistol_Shoot", + "Punch_Cross", + "Punch_Jab", + "Push_Loop", + "Roll", + "Roll_RM", + "Sitting_Enter", + "Sitting_Exit", + "Sitting_Idle_Loop", + "Sitting_Talking_Loop", + "Spell_Simple_Enter", + "Spell_Simple_Exit", + "Spell_Simple_Idle_Loop", + "Spell_Simple_Shoot", + "Sprint_Loop", + "Swim_Fwd_Loop", + "Swim_Idle_Loop", + "Sword_Attack", + "Sword_Attack_RM", + "Sword_Idle", + "Walk_Formal_Loop", + "Walk_Loop", +} + +local function set_button_enabled(button_node, enabled) + local color = enabled and vmath.vector4(1, 1, 1, 1) or vmath.vector4(0.7, 0.7, 0.7, 1) + gui.set_color(button_node, color) -- <5> +end + +function init(self) + msg.post(".", "acquire_input_focus") -- <6> + + self.buttons = {} -- <7> + self.selected_index = nil + + for index, animation_id in ipairs(ANIMATIONS) do -- <8> + local button_id = "button_" .. string.format("%02d", index) + local text_id = "button_text_" .. string.format("%02d", index) + local button_node = gui.get_node(button_id) + local text_node = gui.get_node(text_id) + + gui.set_text(text_node, animation_id) -- <9> + set_button_enabled(button_node, true) + + self.buttons[index] = { -- <10> + node = button_node, + animation_id = animation_id, + } + end +end + +function on_input(self, action_id, action) + if action_id ~= TOUCH or not action.pressed then -- <11> + return false + end + + for index, button in ipairs(self.buttons) do + if gui.pick_node(button.node, action.x, action.y) then -- <12> + if self.selected_index then + set_button_enabled(self.buttons[self.selected_index].node, true) + end + + self.selected_index = index + set_button_enabled(button.node, false) -- <13> + + msg.post(MODEL_SCRIPT, MSG_PLAY_MODEL_ANIMATION, { -- <14> + animation_id = hash(button.animation_id), + animation_name = button.animation_id, + }) + + return true + end + end + + return false +end + +-- <1> The input binding uses the touch action for both mouse clicks and touch input in this example. +-- <2> This is the same message id used by main.script, so both scripts agree on the command name. +-- <3> GUI scripts cannot address model components directly in a clean way here, so the GUI sends a message to the character script instead. +-- <4> The animation list is ordered to match the generated GUI buttons: button_01 plays the first clip, button_02 the second clip, and so on. +-- <5> The selected button is dimmed by changing the box node color. The text nodes stay unchanged and remain readable on the separate text layer. +-- <6> The GUI must acquire input focus before on_input() receives click/touch events. +-- <7> Store runtime references to the GUI nodes once during init instead of resolving them every click. +-- <8> Button and text node ids are generated from the animation index, matching the button_01/button_text_01 naming convention in main.gui. +-- <9> The visible label is filled from the animation list, so the GUI file does not need to hardcode animation names in every text node. +-- <10> Each button entry stores the clickable box node and the animation id it should request. +-- <11> Ignore all non-press input events. This prevents the same click/touch from triggering repeatedly during release or movement phases. +-- <12> gui.pick_node() tests whether the click position is inside the button box node. +-- <13> Re-enable the previously selected button and dim the newly selected one to show which animation is currently active. +-- <14> Send the selected animation to the model-owning script. The animation is sent as a hash for model.play_anim(), with the string kept only as readable debug/context data. diff --git a/animation/3d_animations/example/main.script b/animation/3d_animations/example/main.script new file mode 100644 index 00000000..5968baab --- /dev/null +++ b/animation/3d_animations/example/main.script @@ -0,0 +1,22 @@ +local MSG_PLAY_MODEL_ANIMATION = hash("play_model_animation") -- <1> +local DEFAULT_ANIMATION = hash("Sword_Idle") -- <2> + +local function play_animation(animation_id) + model.play_anim("#model", animation_id, go.PLAYBACK_LOOP_FORWARD) -- <3> +end + +function init(self) + play_animation(DEFAULT_ANIMATION) -- <4> +end + +function on_message(self, message_id, message, sender) + if message_id == MSG_PLAY_MODEL_ANIMATION then -- <5> + play_animation(message.animation_id) + end +end + +-- <1> This message id is shared with the GUI script. The GUI does not call the model API directly; it asks this script to play an animation. +-- <2> The character starts in a known default pose/animation before the user selects anything from the GUI. +-- <3> model.play_anim() is called on the model component attached to the same game object as this script. The animation id must match a clip imported from the GLB. +-- <4> Start the default animation when the game object is initialized. +-- <5> React only to the animation-selection message. The selected animation id is sent by the GUI script in the message table. diff --git a/animation/3d_animations/game.project b/animation/3d_animations/game.project new file mode 100644 index 00000000..5ad212f0 --- /dev/null +++ b/animation/3d_animations/game.project @@ -0,0 +1,38 @@ +[bootstrap] +main_collection = /example/main.collectionc + +[script] +shared_state = 1 + +[display] +width = 720 +height = 720 +high_dpi = 1 + +[android] +input_method = HiddenInputField +package = com.defold.example.basic3d + +[project] +dependencies = +title = UniversalAnimationsQuaternius +version = 1.0 +developer = Defold + +[physics] +type = 3D + +[osx] +bundle_identifier = com.defold.example.basic3d + +[ios] +bundle_identifier = com.defold.example.basic3d + +[html5] +scale_mode = stretch + +[render] +clear_color_red = 0.160156 +clear_color_green = 0.164063 +clear_color_blue = 0.183594 + diff --git a/animation/3d_animations/input/game.input_binding b/animation/3d_animations/input/game.input_binding new file mode 100644 index 00000000..8ed1d4e4 --- /dev/null +++ b/animation/3d_animations/input/game.input_binding @@ -0,0 +1,4 @@ +mouse_trigger { + input: MOUSE_BUTTON_1 + action: "touch" +} diff --git a/animation/3d_animations/setup.png b/animation/3d_animations/setup.png new file mode 100644 index 00000000..4f945e0a Binary files /dev/null and b/animation/3d_animations/setup.png differ diff --git a/animation/3d_animations/thumbnail.png b/animation/3d_animations/thumbnail.png new file mode 100644 index 00000000..943342b1 Binary files /dev/null and b/animation/3d_animations/thumbnail.png differ