3D Game DevelopmentLoading Models and Playing AnimationsOn this pageLoading Models and Playing Animations Animated glTF models don't play themselves. The file stores named animation clips as authoring data, not engine conventions, so you need to know the exact name of the clip you want and call play explicitly. Meanwhile, loading geometry, textures, and materials is expensive. When you need the same prop repeated across a scene, you want the engine to load the data once and reuse it for every instance. This tutorial loads an animated character into your lit scene, plays one of its animation clips by name, and demonstrates asset reuse by creating multiple crate instances from a single source file. By the end you'll understand how animation names work, how to play them, and how Dora's caching system keeps memory usage efficient when you reuse assets. Tutorial goalLoad an animated character into your lit scene, play one confirmed animation clip, and add repeated props without duplicating their imported resources. Before you start Continue from lighting and materials. The verified sample uses Assets/Model/Kenney/character-oopi.glb and Assets/Model/Kenney/crate.glb from Dora-Platformer3D-Demo. For your own assets: Use glTF 2.0: .glb or .gltf. Keep .gltf, buffers, and textures in their original relative layout. Prefer .glb while learning; it has fewer paths to break when moved between projects. 1. Load an animated character Character models come with embedded animation clips stored in the glTF file. Each clip has a name assigned by the artist in their DCC tool (Blender, Maya, etc.). To play a clip, you need to load the model first, then call play with the exact clip name and a boolean flag for looping. TypeScriptLuaYueScriptTealinit.tsimport {Camera3D, Color3, DirectionalLight3D, Director, Model3D, Vec3} from "Dora";const view = Director.entry;const camera = Camera3D();camera.lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0));Director.pushCamera(camera);const character = Model3D("Assets/Model/Kenney/character-oopi.glb");character.scale = Vec3(0.8, 0.8, 0.8);character.play("idle", true);view.addChild(character);const light = DirectionalLight3D();light.color = Color3(0xfff3da);light.intensity = 3.2;light.angleX = -48;light.angleY = 28;view.addChild(light);init.lualocal _ENV = Doralocal view = Director.entrylocal camera = Camera3D()camera:lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0))Director:pushCamera(camera)local character = Model3D("Assets/Model/Kenney/character-oopi.glb")character.scale = Vec3(0.8, 0.8, 0.8)character:play("idle", true)view:addChild(character)local light = DirectionalLight3D()light.color = Color3(0xfff3da)light.intensity = 3.2light.angleX = -48light.angleY = 28view:addChild(light)init.yue_ENV = Doraview = Director.entrycamera = Camera3D!camera\lookAt Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0)Director\pushCamera cameracharacter = Model3D "Assets/Model/Kenney/character-oopi.glb"character.scale = Vec3 0.8, 0.8, 0.8character\play "idle", trueview\addChild characterlight = DirectionalLight3D!light.color = Color3 0xfff3dalight.intensity = 3.2light.angleX = -48light.angleY = 28view\addChild lightinit.tllocal Camera3D <const> = require("Camera3D")local Color3 <const> = require("Color3")local DirectionalLight3D <const> = require("DirectionalLight3D")local Director <const> = require("Director")local Model3D <const> = require("Model3D")local Model3DType = require("Model3D").Typelocal Vec3 <const> = require("Vec3")local view = Director.entrylocal camera = Camera3D()camera:lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0))Director:pushCamera(camera)local character = Model3D("Assets/Model/Kenney/character-oopi.glb") as Model3DTypecharacter.scale = Vec3(0.8, 0.8, 0.8)character:play("idle", true)view:addChild(character)local light = DirectionalLight3D()light.color = Color3(0xfff3da)light.intensity = 3.2light.angleX = -48light.angleY = 28view:addChild(light) Four things are happening here: Director.entry gets the default 3D view root. This is where you add all your 3D nodes. camera.lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0)) positions the camera at (3.5, 2.5, 5.5) and aims it at (0, 0.8, 0) — roughly where the character's chest will be after we scale it down. character.scale = Vec3(0.8, 0.8, 0.8) scales the character to 80% of its authored size. Scaling at the instance level lets you tune the visual fit without modifying the source asset. character.play("idle", true) plays the animation clip named "idle" and loops it forever because the second argument is true. The DirectionalLight3D setup adds a warm, slightly orange-tinted light (Color3(0xfff3da)) at intensity 3.2, rotated to -48 degrees around the X-axis and 28 degrees around the Y-axis. Checkpoint The character should be visible, scaled sensibly, and repeating its idle motion. If it's visible but not moving, confirm the clip name in your asset viewer before changing camera or light settings. Animation names are artist-chosen data, not engine conventions. play(name, loop) plays an animation clip embedded in the glTF by its exact name. Names are asset data, not a Dora convention: another model might use Idle, stand, or no animation at all. Inspect the asset in your DCC tool or an asset viewer and copy the clip name exactly. If you pass a name that doesn't exist, the call silently does nothing. Use play(name, false) when you want a one-shot animation that stops when it finishes, such as a melee attack or a door opening. Use play(name, true) for continuous loops like idle or run states. 2. Reuse the same asset for multiple props Loading a 3D model means parsing geometry, decoding textures, compiling materials, and caching the data in memory. That's expensive. When you need the same prop repeated across a scene — crates, trees, rocks — you don't want to pay that cost twice. Dora solves this by caching loaded data keyed to the file path: create multiple Model3D instances from the same path and they all share the underlying meshes, textures, and materials, while each instance keeps its own independent transform. TypeScriptLuaYueScriptTealinit.tsconst firstCrate = Model3D("Assets/Model/Kenney/crate.glb");firstCrate.position = Vec3(-1.2, 0, 0);view.addChild(firstCrate);const secondCrate = Model3D("Assets/Model/Kenney/crate.glb");secondCrate.position = Vec3(1.2, 0, 0);view.addChild(secondCrate);init.lualocal firstCrate = Model3D("Assets/Model/Kenney/crate.glb")firstCrate.position = Vec3(-1.2, 0, 0)view:addChild(firstCrate)local secondCrate = Model3D("Assets/Model/Kenney/crate.glb")secondCrate.position = Vec3(1.2, 0, 0)view:addChild(secondCrate)init.yuefirstCrate = Model3D "Assets/Model/Kenney/crate.glb"firstCrate.position = Vec3(-1.2, 0, 0)view\addChild firstCratesecondCrate = Model3D "Assets/Model/Kenney/crate.glb"secondCrate.position = Vec3(1.2, 0, 0)view\addChild secondCrateinit.tllocal firstCrate = Model3D("Assets/Model/Kenney/crate.glb") as Model3DTypefirstCrate.position = Vec3(-1.2, 0, 0)view:addChild(firstCrate)local secondCrate = Model3D("Assets/Model/Kenney/crate.glb") as Model3DTypesecondCrate.position = Vec3(1.2, 0, 0)view:addChild(secondCrate) Two things are happening here: Model3D("Assets/Model/Kenney/crate.glb") is called twice, but the file is loaded only once. Dora caches the data on the first call and returns a new instance sharing that cache on the second call. Each crate gets its own position — one at (-1.2, 0, 0) and one at (1.2, 0, 0). Transforms (position, rotation, scale) are per-instance, so you can place the same model anywhere in the scene without affecting other instances. Treat Model3D as a scene instance factory: give each instance its own transform and only customize materials where the visual difference is needed. If you need unique materials for specific instances, clone or replace the material after loading. But for the common case of repeated props, sharing the cache is exactly what you want. When you call Model3D("path") multiple times, Dora loads the file once and shares the data between all instances. This is efficient: you get multiple distinct objects at different positions, rotations, and scales, but only one copy of the model data in memory. The savings compound with large scenes containing hundreds of identical trees, crates, or rocks. Tips for importing your own models Add one model to an empty test scene. Start fresh so you can isolate any problems to the model itself, not interactions with other scene elements. Confirm it renders with a camera and light. A model that doesn't appear might be outside the camera view or unlit — verify the basics before adding complexity. Confirm its scale and origin are practical. A model authored in centimeters will look tiny if your game uses meters. Check the scale relative to your other assets and adjust the instance scale if needed. Test one animation by its exact name. Open the asset in your DCC tool, copy the clip name character-for-character, and call play with that exact string. Get one working before testing others. Only then move the asset into gameplay and add materials, physics, or UI. Once you know the model loads, scales correctly, and animates as expected, integrate it into your actual game logic. This order saves time because asset-path failures, scale mismatches, and wrong animation names stay separate from scene-logic failures. You'll know whether a problem is "the file won't load" or "my collision code is broken" without guessing. Common mistakes SymptomLikely causeFixModel is missingBroken .gltf companion pathsPreserve the model folder layout or use .glb.Model is tiny or hugeAuthoring units differAdjust scale on the instance, then document the convention.play() does nothingWrong or absent clip nameInspect the source asset and use its exact clip name.Many identical props cost too muchEach has unique material workShare the source path and customize only exceptions. Complete examples The complete scripts below follow this tutorial and have been verified by the Dora engine. TypeScriptLuaYueScriptTealinit.tsimport {Camera3D, Color3, DirectionalLight3D, Director, Model3D, Vec3} from "Dora";const view = Director.entry;const camera = Camera3D();camera.lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0));Director.pushCamera(camera);// Animated character. The animation name comes from the glTF asset itself.const character = Model3D("Assets/Model/Kenney/character-oopi.glb");if (!character) throw new Error("failed to load character-oopi.glb");character.scale = Vec3(0.8, 0.8, 0.8);character.play("idle", true);view.addChild(character);const light = DirectionalLight3D();light.color = Color3(0xfff3da);light.intensity = 3.2;light.angleX = -48;light.angleY = 28;view.addChild(light);// Two crates that share the same cached source data.const firstCrate = Model3D("Assets/Model/Kenney/crate.glb");if (!firstCrate) throw new Error("failed to load crate.glb");firstCrate.position = Vec3(-1.2, 0, 0);view.addChild(firstCrate);const secondCrate = Model3D("Assets/Model/Kenney/crate.glb");if (!secondCrate) throw new Error("failed to load crate.glb");secondCrate.position = Vec3(1.2, 0, 0);view.addChild(secondCrate);init.lualocal _ENV = Doralocal view = Director.entrylocal camera = Camera3D()camera:lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0))Director:pushCamera(camera)-- Animated character. The animation name comes from the glTF asset itself.local character = Model3D("Assets/Model/Kenney/character-oopi.glb")if not character then error("failed to load character-oopi.glb")endcharacter.scale = Vec3(0.8, 0.8, 0.8)character:play("idle", true)view:addChild(character)local light = DirectionalLight3D()light.color = Color3(0xfff3da)light.intensity = 3.2light.angleX = -48light.angleY = 28view:addChild(light)-- Two crates that share the same cached source data.local firstCrate = Model3D("Assets/Model/Kenney/crate.glb")if not firstCrate then error("failed to load crate.glb")endfirstCrate.position = Vec3(-1.2, 0, 0)view:addChild(firstCrate)local secondCrate = Model3D("Assets/Model/Kenney/crate.glb")if not secondCrate then error("failed to load crate.glb")endsecondCrate.position = Vec3(1.2, 0, 0)view:addChild(secondCrate)init.yue_ENV = Doraview = Director.entrycamera = Camera3D!camera\lookAt Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0)Director\pushCamera cameracharacter = Model3D "Assets/Model/Kenney/character-oopi.glb"error "failed to load character-oopi.glb" unless charactercharacter.scale = Vec3 0.8, 0.8, 0.8character\play "idle", trueview\addChild characterlight = DirectionalLight3D!light.color = Color3 0xfff3dalight.intensity = 3.2light.angleX = -48light.angleY = 28view\addChild lightfirstCrate = Model3D "Assets/Model/Kenney/crate.glb"error "failed to load crate.glb" unless firstCratefirstCrate.position = Vec3 -1.2, 0, 0view\addChild firstCratesecondCrate = Model3D "Assets/Model/Kenney/crate.glb"error "failed to load crate.glb" unless secondCratesecondCrate.position = Vec3 1.2, 0, 0view\addChild secondCrateinit.tllocal Camera3D = require("Camera3D")local Color3 = require("Color3")local DirectionalLight3D = require("DirectionalLight3D")local Director = require("Director")local Model3D = require("Model3D")local Model3DType = require("Model3D").Typelocal Vec3 = require("Vec3")local view = Director.entrylocal camera = Camera3D()camera:lookAt(Vec3(3.5, 2.5, 5.5), Vec3(0, 0.8, 0))Director:pushCamera(camera)local character = Model3D("Assets/Model/Kenney/character-oopi.glb") as Model3DTypecharacter.scale = Vec3(0.8, 0.8, 0.8)character:play("idle", true)view.scene:addChild(character)local light = DirectionalLight3D()light.color = Color3(0xfff3da)light.intensity = 3.2light.angleX = -48light.angleY = 28view.scene:addChild(light)local firstCrate = Model3D("Assets/Model/Kenney/crate.glb") as Model3DTypefirstCrate.position = Vec3(-1.2, 0, 0)view.scene:addChild(firstCrate)local secondCrate = Model3D("Assets/Model/Kenney/crate.glb") as Model3DTypesecondCrate.position = Vec3(1.2, 0, 0)view.scene:addChild(secondCrate) Try it yourself Add a third crate, rotate it with angles = Vec3(0, 25, 0), and keep the other two unchanged. You should see three distinct placements while still loading one source asset. Summary You now know how to work with animated 3D models and reuse assets efficiently: Animation clips: glTF models store named animation clips as authoring data. Play them with play(name, loop) where name is the exact clip name from your DCC tool and loop is true for continuous playback or false for one-shot animations. Instance scaling: Use the scale property on a model instance to adjust its size without modifying the source asset. This lets you tune the visual fit of imported models to your game's world scale. Asset caching: When you create multiple Model3D instances from the same file path, Dora loads the data once and shares it between all instances. Each instance has its own independent transform (position, rotation, scale) but shares the underlying geometry, textures, materials, and animations. Import workflow: Follow the five-step process (test scene, camera/light check, scale check, animation test, then integration) to isolate asset problems from logic problems and save debugging time. Animation names are not engine conventions — they're whatever the artist named them in their modeling tool. Always verify the exact name in your asset viewer before calling play. The caching system makes repeated props like crates, trees, and rocks essentially free after the first load, so build your scenes with confidence that memory usage stays efficient. Next tutorial Continue with 3D physics and character movement to make scene objects respond to gravity and gameplay movement.