English
Entity Models & Animations
A custom mob can be attached to a 3D model, with standard actions, K-frames, and distance tiers (LOD) configured alongside. EmakiMobs can attach custom mobs to 3D models rendered by ModelEngine or BetterModel, driving standard actions, keyframe (K-frame) events, and distance-based LOD. Both model backends are optional dependencies: with neither installed, the model layer is silently disabled and mobs run in vanilla form
Prerequisites
- Install the ModelEngine (paid) or BetterModel (free, open source) plugin itself
- Place Blockbench-exported
.bbmodelblueprints into that plugin'sblueprints/directory and wire up the resource pack containing the model textures - Pick the backend in EmakiMobs'
config.ymlundermodel:(see below) - Add a
model:section to the mob definition YAML (see below)
Blueprint name resolution inside each backend is that backend's own business; EmakiMobs only queries by string name
Main model: section
| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
api | string | No | auto | auto / model_engine / better_model (aliases modelengine / bettermodel also accepted; case-insensitive) | auto picks a backend (ModelEngine first, then BetterModel). An invalid value raises a precheck WARN and falls back to auto; when the selected backend is absent, models are disabled with a console note |
view_distance | double | No | 48.0 | — (blocks) | Model view range. NEAR renders at this range; at MID ModelEngine shrinks it to min(view_distance, mid) and BetterModel only spawns the model to players within that distance; at FAR ModelEngine zeroes the range and BetterModel pauses the tracker |
lod.near | double | No | 16.0 | must be ≤ lod.mid | LOD lower bound (blocks). A distance ≤ this is NEAR (full range) |
lod.mid | double | No | 32.0 | must be ≥ lod.near and ≤ lod.far | LOD middle bound (blocks). Between near and mid is MID (degraded range); anything beyond mid is FAR (so mid–far is also FAR), which makes it the real hide boundary |
lod.far | double | No | 48.0 | must be ≥ lod.mid | Far-side early-return bound: a nearest-player distance beyond it is treated as FAR immediately. Lowering it below lod.mid makes part of the MID tier unreachable |
movement_check_interval_ticks | int | No | 5 | ≥ 1 (ticks) | Period of the movement (idle/walk/run) and LOD check; the tier or state is pushed to the backend only when it changes |
death_detach_delay_ticks | int | No | 40 | ≥ 0 (ticks) | Ticks to wait after the death animation starts before detaching the model |
walk_speed_threshold | double | No | 0.12 | — (blocks/tick) | Horizontal speed at which the mob counts as walking |
run_speed_threshold | double | No | 0.28 | — (blocks/tick) | Horizontal speed at which the mob counts as running (checked before the walking threshold) |
transitions.<action>_fade_ticks | int | No | 5 for idle/walk/run, 0 for death, 2 for hurt, 3 for attack/skill/interact | ≥ 0 (ticks) | Blend-in length delegated to the engine's native fading. <action> only accepts the eight fixed keys idle, walk, run, death, hurt, attack, skill, interact; other keys are never read, and negative values are treated as 0 |
Tier behaviour: NEAR renders at full range; MID degrades (ModelEngine shrinks the range to min(view_distance, mid), BetterModel only spawns the model to players within that distance); at FAR ModelEngine zeroes the view range and BetterModel pauses that model's tracker
Force BetterModel and tighten the mid tier to 24 blocks:
yaml
model:
api: "better_model"
lod:
near: 12.0
mid: 24.0
far: 48.0Mob definition model: section
yaml
model:
blueprint: "bone_lord" # required: model blueprint name
api: "model_engine" # optional: overrides the global model.api, selecting the backend for this mob
scale: 1.2 # optional: model scale, default 1.0
animations: # optional: standard action → engine animation name
idle: "breath"
walk: "walk_slow"
run: "run_fast"
death: "die"
hurt: "flinch"
attack: "swing"
skill: "cast"
interact: "nod"
keyframes: # optional: K-frame timelines for any animation
attack:
duration_ticks: 24 # timeline length in ticks
loop: false # whether playback loops
priority: "action" # ambient|utility|action|reactive|terminal
conflict: "replace" # replace|reject policy for equal priorities
frames:
- tick: 0
action: "self | play_sound sound=minecraft:entity.wither.shoot"
- tick: 10
label: "impact"
action: "self | spawn_particle particle=flame"
lod: # optional: overrides the global model.lod, giving this mob its own tier bounds
near: 12.0
mid: 24.0
far: 40.0| Field | Type | Required | Default | Accepted values | Description |
|---|---|---|---|---|---|
blueprint | string | Yes (within this section) | none | a blueprint name present in the backend's blueprints/ | A missing or blank value makes the section ignored with a load-time warning (console.model_missing_blueprint). If the backend cannot find the blueprint at runtime, console.model_blueprint_missing is logged and the attach is skipped |
api | string | No | none (inherits the global model.api) | auto / model_engine / better_model | The model backend for this mob, overriding the global config. An invalid value raises loader.model_api_invalid and falls back to the global config |
scale | double | No | 1.0 | — | Model scale multiplier, used both on attach and on a mob_set_model swap |
animations.<action key> | string | No | none (uses the built-in default animation name) | an animation name present in the blueprint | Action key → engine animation name. Both key and value are normalised (key lower-cased, value trimmed); blank values are ignored |
keyframes.<action key> | section | No | none | — | K-frame timeline; the key is the action key (normalised to lower case). A non-standard action key (not one of idle... interact) is registered with utility priority and no looping |
keyframes.<action key>.duration_ticks | int | No | 20 | ≥ 1 | Total timeline length in ticks. A value below 1 discards the whole timeline with a warning; a looping animation wraps at this value |
keyframes.<action key>.loop | boolean | No | the action's built-in value for a standard action (idle/walk/run are true, the rest false); false for a non-standard key | true / false | Whether playback loops |
keyframes.<action key>.priority | string | No | the action's built-in priority for a standard action; utility for a non-standard key | ambient / utility / action / reactive / terminal (case-insensitive) | Playback priority; an unrecognised value is treated as ambient |
keyframes.<action key>.conflict | string | No | replace | replace / reject (case-insensitive) | Equal-priority conflict policy; an unrecognised value is treated as replace |
keyframes.<action key>.frames | list | No | [] | items are {tick, action, label} mappings | Frame list. Frames run in list order and are not sorted; a frame missing tick, or with a non-numeric tick, is dropped with a warning (loader.model_keyframe_missing_tick) |
keyframes.<action key>.frames[].tick | int | Yes (within a frame) | none | ≥ 0 (ticks) | Trigger tick; tick: 0 runs immediately when playback starts. It is not clamped to duration_ticks — a frame beyond the duration delays playback until that tick before continuing or finishing |
keyframes.<action key>.frames[].action | string | No | "" | one CoreLib action pipeline line | Frame payload; an empty string only notifies listeners and runs nothing, and a compile failure logs a warning |
keyframes.<action key>.frames[].label | string | No | "" | — | Frame label, passed to animation listeners with the frame (diagnostics only) |
lod.near / lod.mid / lod.far | double | No | 16.0 / 32.0 / 48.0 | — | This mob's LOD tier bounds, overriding the global config.yml model.lod; when only some are written, the missing ones fall back to the defaults on the left (they do not inherit the global values) |
- The whole
model:section is optional; without it the mob gets no model model.apiandmodel.lodtake effect per mob: the backend bridge is resolved and cached by theapivalue, and LOD tiers are decided by this mob's own bounds. Withoutapithe mob inherits the globalmodel.api; withoutlodit inherits the globalmodel.lod
Standard actions
| Action | Triggered by | Default priority | Loops |
|---|---|---|---|
idle | after attach / after a transient action finishes / standing still | ambient | yes |
walk | horizontal speed ≥ walk_speed_threshold | ambient | yes |
run | horizontal speed ≥ run_speed_threshold | ambient | yes |
attack | landing a hit (on_damage_give) | action | no |
skill | skill cast (via mob_play_animation) | action | no |
interact | interaction (via mob_play_animation) | utility | no |
hurt | taking damage (on_damage_take) | reactive | no |
death | death (on_death), detach follows after the delay | terminal | no |
Priority rules: a higher tier pre-empts the current playback; equal tiers resolve by the conflict policy (replace or reject); lower-tier requests are rejected. terminal (death) cannot be pre-empted. A standard action with no timeline uses the default duration (20 ticks) plus that action's built-in priority and looping setting
Keyframes (K-frames)
Timelines are driven by EmakiCoreLib's animation scheduler: when playback starts, tick: 0 frames run immediately and later frames fire at their exact tick on the entity's owner thread. A frame payload is one CoreLib action pipeline line, so any registered stage (spawn_particle, play_sound, attribute changes, state switches, ...) plus the whole expression/placeholder syntax is reusable as-is
Action stages
Any CoreLib action line (skills, loot actions, third-party plugins) can use:
text
self | mob_play_animation animation=spin
self | mob_stop_animation animation=spin
self | mob_set_model blueprint=bone_lord_phase2| Stage | Parameter | Type | Required | Default | Description |
|---|---|---|---|---|---|
mob_play_animation | animation | string | Yes | none | An action key defined in the mob's model.animations / model.keyframes; missing or blank returns a configuration error |
mob_stop_animation | animation | string | Yes | none | As above; stops playback of the matching definition |
mob_set_model | blueprint | string | Yes | none | The new blueprint name; missing or blank returns a configuration error, and a blueprint the backend cannot find logs a warning and keeps the current model |
- All three require the target to resolve to a managed mob entity; a non-managed entity returns
skippedwithout an error mob_set_modelhot-swaps the blueprint (reusing the definition'sscale) and falls back to idle afterwards
Third-party extensions
Other plugins can register animation definitions, play them, and observe playback through EmakiCoreLibApi (see CoreLib animation system). EmakiMobs consumes the same registry, so third-party mob:<id>:<key> definitions are directly referenceable by mob_play_animation
Troubleshooting
| Symptom | Check |
|---|---|
| No model on mob | Backend plugin installed; model.api (global or per-mob) points to an installed backend; startup log shows 未找到可用的模型后端 (auto and neither backend available) or 模型后端 '<名称>' 未安装 |
| Missing-blueprint warning | Is console.model_blueprint_missing logged? Does the name match the file (without extension) in the backend's blueprints/? Did you /meg reload or restart the backend? |
| Animation does not play | Does the engine animation name exist in the blueprint? Are the animations: action keys spelled right? |
| K-frames never fire | Is frames[].action a valid pipeline line (a compile failure logs Animation keyframe line did not compile)? Are frames[].tick values in ascending order (frames run in list order and are not sorted)? Is a tick larger than duration_ticks (it is not clamped)? |
| Model vanishes at range | This is LOD by design; the boundary is lod.mid, not lod.far. Raise lod.mid first, then lod.far (early-return bound) and view_distance (NEAR/MID tier range) as needed. If it only happens for one mob, check its model.lod overrides |