动画与动画控制器
动画(Animation)和动画控制器(Animation Controller)是基岩版实体渲染系统的核心。动画定义了骨骼随时间的旋转、位移和缩放变化;动画控制器是一个状态机,负责在合适的时机播放合适的动画。
前置条件
建议先了解自定义基础生物,并安装 Blockbench 用于可视化制作模型与动画。
动画概述
动画本质上是对实体模型骨骼(Bone)属性的关键帧插值。你可以控制每根骨骼在每个时间点的:
- rotation — 旋转(度,XYZ 三轴)
- position — 位移(像素,XYZ 三轴)
- scale — 缩放(倍率,XYZ 三轴或统一值)
动画文件放在资源包的 RP/animations/ 目录下,使用 format_version 1.8.0。
| 属性 | 说明 |
|---|---|
format_version | 固定为 "1.8.0" |
animations | 包含一个或多个动画定义的对象 |
| 动画 ID | 必须以 animation. 开头,如 animation.mymod_ghost.walk |
动画通常在 Blockbench 中以可视化方式制作,导出后得到 JSON 文件。当然,你也可以直接手写 JSON 或使用 Molang 表达式来实现数学驱动的动画。
动画文件格式
一个完整的动画文件结构如下:
{
"format_version": "1.8.0",
"animations": {
"animation.mymod_ghost.idle": {
"loop": true,
"animation_length": 2.0,
"bones": {
"body": {
"rotation": {
"0.0": [0, 0, 0],
"1.0": [5, 0, 0],
"2.0": [0, 0, 0]
}
},
"head": {
"position": {
"0.0": [0, 0, 0],
"1.0": [0, 1, 0],
"2.0": [0, 0, 0]
}
}
}
}
}
}字段详解
| 字段 | 类型 | 说明 |
|---|---|---|
loop | boolean 或 string | true = 循环播放;false(默认)= 播放一次;"hold_on_last_frame" = 播放后停留在最后一帧 |
animation_length | number | 动画总时长(秒)。如果省略,引擎会根据最后一个关键帧的时间自动推断 |
bones | object | 每个键是骨骼名称(必须与几何体模型中的骨骼名称一致),值包含 rotation/position/scale |
blend_weight | string(Molang) | 动画的混合权重,控制该动画对骨骼变换的影响比例。例如 "query.anim_time" |
anim_time_update | string(Molang) | 自定义动画时间的推进方式,替代默认的逐帧递增。例如 "query.modified_distance_moved" |
override_previous_animation | boolean | 为 true 时覆盖之前的动画变换,不做叠加 |
start_delay | string(Molang) | 动画开始前的延迟时间 |
loop_delay | string(Molang) | 循环动画每次重新开始前的延迟 |
loop 的三种模式
"loop": false播放一次后停止,骨骼回到初始姿态。适合攻击动画、受击动画等一次性动作。
"loop": true循环播放。适合待机、行走等持续性动画。
"loop": "hold_on_last_frame"播放完毕后停留在最后一帧。适合死亡动画。
关键帧
关键帧(Keyframe)是动画的核心数据。每个关键帧指定一个时间点和该时间点的骨骼变换值。
基本格式
关键帧以时间(秒,字符串格式)为键,值为 [X, Y, Z] 数组:
"rotation": {
"0.0": [0, 0, 0],
"0.5": [15, 0, 0],
"1.0": [0, 0, 0]
}这表示骨骼在 0 秒时无旋转,0.5 秒时绕 X 轴旋转 15 度,1 秒时回到原始状态。两个关键帧之间的值通过线性插值自动计算。
简写格式
如果动画不使用关键帧时间轴(例如数学驱动的动画),可以直接写一个数组:
"rotation": [0, "query.target_x_rotation", 0]此时该值在每一帧实时计算,不存在关键帧插值。
缩放的特殊写法
缩放支持统一值(三轴等比缩放):
"scale": 1.5等价于 [1.5, 1.5, 1.5],也支持关键帧形式:
"scale": {
"0.0": 1.0,
"0.5": 1.5,
"1.0": 1.0
}平滑插值(Catmull-Rom)
默认情况下,关键帧之间使用线性插值。如果你需要更平滑的曲线过渡,可以使用 Catmull-Rom 样条插值:
"rotation": {
"0.0": {
"lerp_mode": "catmullrom",
"value": [0, 0, 0]
},
"0.5": {
"lerp_mode": "catmullrom",
"value": [15, 0, 5]
},
"1.0": {
"lerp_mode": "catmullrom",
"value": [0, 0, 0]
}
}lerp_mode 支持两个值:
"linear"— 线性插值(默认)"catmullrom"— Catmull-Rom 样条插值,曲线更平滑
Pre/Post 值
你可以为同一个关键帧指定进入前(pre)和进入后(post)的不同值,实现突变效果:
"rotation": {
"0.2": {
"pre": [36.0, 0.0, 0.0],
"post": [
"180.0 * (0.2 + 0.07 * math.sin(query.key_frame_lerp_time * 1644.39))",
0.0,
0.0
]
},
"1.8": {
"pre": [
"180.0 * (0.2 + 0.07 * math.sin(query.key_frame_lerp_time * 1644.39))",
0.0,
0.0
],
"post": [36.0, 0.0, 0.0]
}
}- pre — 插值到该关键帧时使用的目标值(从前一帧插值到此处)
- post — 从该关键帧出发时使用的起始值(从此处插值到下一帧)
这在需要在某个时间点"跳变"到不同姿态时非常有用。
完整多骨骼关键帧示例
{
"format_version": "1.8.0",
"animations": {
"animation.mymod_ghost.attack": {
"loop": false,
"animation_length": 0.8,
"bones": {
"body": {
"rotation": {
"0.0": [0, 0, 0],
"0.2": [10, 0, 0],
"0.4": [-15, 0, 0],
"0.8": [0, 0, 0]
}
},
"right_arm": {
"rotation": {
"0.0": [0, 0, 0],
"0.2": [-80, 0, 20],
"0.4": [30, 0, -10],
"0.8": [0, 0, 0]
},
"position": {
"0.0": [0, 0, 0],
"0.2": [0, 2, -4],
"0.4": [0, -1, 2],
"0.8": [0, 0, 0]
}
},
"left_arm": {
"rotation": {
"0.0": [0, 0, 0],
"0.4": [-20, 0, -10],
"0.8": [0, 0, 0]
}
}
}
}
}
}Molang 表达式
Molang 是基岩版的表达式语言,可以在动画的关键帧值中使用。这允许你创建数学驱动的连续动画,而不是依赖离散的关键帧。
基本语法
在关键帧值中,将数字替换为字符串形式的 Molang 表达式:
"rotation": [
"math.sin(query.anim_time * 120) * 15",
0,
"math.cos(query.anim_time * 120) * 5"
]这会让骨骼在 X 轴和 Z 轴上做正弦/余弦运动,振幅分别为 15 度和 5 度。
常用 Molang 模式
| 表达式 | 效果 |
|---|---|
math.sin(query.anim_time * 120) * 10 | 持续的正弦摆动,每秒约 0.33 个周期,振幅 10 度 |
query.modified_distance_moved * -30 | 根据移动距离旋转(车轮滚动效果) |
math.sin(query.life_time * 360) * 5 | 基于实体存在时间的摆动 |
query.modified_move_speed * 60 | 根据移动速度调整旋转角度 |
math.abs(math.mod(query.modified_distance_moved, 13) - 6.5) - 3.25 | 复杂的步行循环模式(原版常用) |
query.target_x_rotation | 跟踪目标的 X 轴旋转 |
query.head_x_rotation(0) | 实体头部的俯仰角 |
query.head_y_rotation(0) | 实体头部的偏航角 |
variable.attack_time * -45 | 根据攻击时间控制手臂挥动 |
数学驱动的车轮动画示例
{
"format_version": "1.8.0",
"animations": {
"animation.mymod_car.wheel_spin": {
"loop": true,
"bones": {
"front_wheels": {
"rotation": ["query.modified_distance_moved * -30", 0, 0]
},
"back_wheels": {
"rotation": ["query.modified_distance_moved * -30", 0, 0]
}
}
}
}
}车辆静止时车轮不转,移动时车轮根据移动距离自动旋转,速度越快旋转越快。
anim_time_update 的应用
通过 anim_time_update 字段可以改变动画时间轴的推进方式:
{
"format_version": "1.8.0",
"animations": {
"animation.mymod_ghost.walk": {
"loop": true,
"animation_length": 1.0,
"anim_time_update": "query.modified_distance_moved",
"bones": {
"left_leg": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [-30, 0, 0],
"0.5": [0, 0, 0],
"0.75": [30, 0, 0],
"1.0": [0, 0, 0]
}
},
"right_leg": {
"rotation": {
"0.0": [0, 0, 0],
"0.25": [30, 0, 0],
"0.5": [0, 0, 0],
"0.75": [-30, 0, 0],
"1.0": [0, 0, 0]
}
}
}
}
}
}动画时间不再按帧递增,而是根据实体移动距离推进。实体静止时动画暂停,实体移动时动画播放。
Blockbench 动画制作
Blockbench 是制作基岩版模型和动画的首选工具。以下是制作动画的步骤:
步骤一:切换到动画模式
- 打开你的
.bbmodel模型文件 - 在 Blockbench 顶部点击 Animate(动画)选项卡,切换到动画编辑模式
步骤二:创建动画
- 在左侧 Animations 面板中,点击 +(加号)按钮
- 输入动画名称,如
animation.mymod_ghost.idle - 设置循环模式:勾选 Loop 表示循环播放
步骤三:设置关键帧
- 在底部时间轴上,将播放头拖到目标时间点(如 0.0 秒)
- 在右侧选择一个骨骼(如
body) - 调整骨骼的旋转、位移或缩放
- Blockbench 会自动在当前时间点创建关键帧
- 将播放头移到下一个时间点,再次调整骨骼属性
步骤四:预览动画
- 点击时间轴下方的 Play(播放)按钮预览动画效果
- 调整关键帧位置和数值,直到效果满意
步骤五:使用 Molang 表达式
- 选中一个关键帧
- 在左侧关键帧面板中,将数值替换为 Molang 表达式
- 注意:在 Blockbench 中编写 Molang 表达式不需要加引号,引号只在直接编辑 JSON 时需要
- 某些 Molang 查询(如
query.is_moving)在 Blockbench 中无法预览,你可以在 Variable Placeholders 中添加模拟值,例如:query.modified_distance_moved = time * 8
步骤六:导出动画
- 点击 File > Export > Export Bedrock Animations
- 保存到资源包的
RP/animations/目录 - 文件名建议使用
实体名.animation.json格式,如ghost.animation.json
注意
Blockbench 默认可能导出较高的 format_version。如果导出的 format_version 不是 1.8.0,手动修改为 "1.8.0" 即可,动画格式自该版本以来没有破坏性变更。
动画控制器概述
动画控制器(Animation Controller)是一个状态机(State Machine)。它管理实体在不同状态间的切换,每个状态播放不同的动画。
动画控制器文件放在 RP/animation_controllers/ 目录下,使用 format_version 1.10.0。
核心概念
- 状态机同一时间只能处于一个状态
- 每个状态定义了:要做什么(播放哪些动画)和如何切换(转换条件)
- 每一个 tick,引擎先执行当前状态的动画,再检查转换条件
- 如果满足某个转换条件,状态机切换到目标状态
- 每 tick 最多切换一次状态
基本结构
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.mymod_ghost.general": {
"initial_state": "idle",
"states": {
"idle": {
"animations": ["idle"],
"transitions": [
{ "walk": "query.is_moving" }
]
},
"walk": {
"animations": ["walk"],
"transitions": [
{ "idle": "!query.is_moving" }
]
}
}
}
}
}| 字段 | 说明 |
|---|---|
format_version | 固定为 "1.10.0" |
animation_controllers | 包含一个或多个控制器的对象 |
| 控制器 ID | 必须以 controller.animation. 开头 |
initial_state | 初始状态名称。省略时默认为 "default" |
states | 所有状态的集合 |
状态 (States)
每个状态可以包含以下字段:
animations — 动画列表
当前状态下播放的动画。可以是字符串(动画短名称)或带条件的对象:
"animations": [
"idle_body",
{ "idle_head": "query.is_alive" }
]动画短名称需要在客户端实体定义中注册(详见客户端实体绑定)。
transitions — 转换列表
一个数组,每个元素是 { "目标状态名": "Molang 条件" } 格式的对象:
"transitions": [
{ "walk": "query.is_moving" },
{ "attack": "query.is_delayed_attacking" },
{ "death": "!query.is_alive" }
]引擎按数组顺序检查,第一个条件为真的转换会被执行。因此,更高优先级的转换应放在前面。
on_entry / on_exit — 进入/退出命令
进入状态或退出状态时执行的命令(仅在行为包动画控制器中有效):
"idle": {
"on_entry": [
"/say 进入待机状态",
"@s mymod:on_idle"
],
"on_exit": [
"/say 离开待机状态"
]
}命令支持三种形式:
- 斜杠命令:
"/say Hello" - 实体事件:
"@s mymod:some_event" - Molang 表达式:
"variable.state = 1;"
blend_transition — 混合过渡
控制从前一个状态过渡到当前状态时的动画混合时长(秒):
"idle": {
"blend_transition": 0.2,
"animations": ["idle"]
}值越大过渡越平滑。建议设为 0.1 ~ 0.3。
blend_via_shortest_path
设为 true 时,旋转混合通过最短路径计算(避免骨骼绕远路旋转):
"idle": {
"blend_transition": 0.2,
"blend_via_shortest_path": true,
"animations": ["idle"]
}完整状态示例
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.mymod_ghost.general": {
"initial_state": "idle",
"states": {
"idle": {
"blend_transition": 0.2,
"blend_via_shortest_path": true,
"animations": ["idle"],
"transitions": [
{ "walk": "query.is_moving" },
{ "attack": "query.is_delayed_attacking" },
{ "death": "!query.is_alive" }
]
},
"walk": {
"blend_transition": 0.2,
"animations": ["walk"],
"transitions": [
{ "idle": "!query.is_moving" },
{ "attack": "query.is_delayed_attacking" },
{ "death": "!query.is_alive" }
]
},
"attack": {
"animations": ["attack"],
"transitions": [
{ "idle": "query.any_animation_finished" },
{ "death": "!query.is_alive" }
]
},
"death": {
"animations": ["death"]
}
}
}
}
}转换条件 (Transitions)
转换条件使用 Molang 查询表达式。以下是常用的实体查询:
常用 Query 速查表
| Query | 返回值 | 说明 |
|---|---|---|
query.is_moving | 0.0/1.0 | 实体是否正在移动 |
query.is_on_ground | 0.0/1.0 | 实体是否在地面上 |
query.is_in_water | 0.0/1.0 | 实体是否在水中 |
query.is_in_water_or_rain | 0.0/1.0 | 实体是否在水中或雨中 |
query.is_swimming | 0.0/1.0 | 实体是否在游泳 |
query.is_sneaking | 0.0/1.0 | 实体是否在潜行 |
query.is_sprinting | 0.0/1.0 | 实体是否在疾跑 |
query.is_jumping | 0.0/1.0 | 实体是否正在跳跃 |
query.is_alive | 0.0/1.0 | 实体是否存活 |
query.is_baby | 0.0/1.0 | 实体是否为幼年状态 |
query.is_sleeping | 0.0/1.0 | 实体是否在睡觉 |
query.is_riding | 0.0/1.0 | 实体是否在骑乘 |
query.has_rider | 0.0/1.0 | 实体是否有骑乘者 |
query.is_angry | 0.0/1.0 | 实体是否处于愤怒状态 |
query.is_charged | 0.0/1.0 | 实体是否被充能(如苦力怕) |
query.is_sheared | 0.0/1.0 | 实体是否被剪过毛 |
query.is_saddled | 0.0/1.0 | 实体是否装了鞍 |
query.is_delayed_attacking | 0.0/1.0 | 实体是否正在延迟攻击 |
query.is_first_person | 0.0/1.0 | 当前是否为第一人称视角 |
query.modified_move_speed | float | 修正后的移动速度(含移速加成) |
query.ground_speed | float | 地面移动速度 |
query.modified_distance_moved | float | 修正后的已移动距离(持续递增) |
query.health | float | 当前生命值 |
query.max_health | float | 最大生命值 |
query.variant | int | 实体变体值(minecraft:variant 组件的值) |
query.mark_variant | int | 标记变体值(minecraft:mark_variant 组件的值) |
query.skin_id | int | 皮肤 ID |
query.anim_time | float | 当前动画已播放的时间(秒) |
query.life_time | float | 实体存在的时间(秒) |
query.any_animation_finished | 0.0/1.0 | 当前状态中是否有任何动画播放完毕 |
query.all_animations_finished | 0.0/1.0 | 当前状态中的所有动画是否都播放完毕 |
query.head_x_rotation(n) | float | 头部 X 轴旋转(俯仰角),参数为头的索引 |
query.head_y_rotation(n) | float | 头部 Y 轴旋转(偏航角) |
query.target_x_rotation | float | 朝向目标的 X 旋转 |
query.target_y_rotation | float | 朝向目标的 Y 旋转 |
组合条件
你可以用逻辑运算符组合多个条件:
"transitions": [
{ "swim": "query.is_in_water && !query.is_on_ground" },
{ "sprint": "query.is_moving && query.modified_move_speed > 0.8" },
{ "attack": "query.is_delayed_attacking && query.is_alive" }
]支持的运算符:&&(与)、||(或)、!(非)、<、>、<=、>=、==、!=。
嵌套控制器
动画控制器的 animations 数组中,不仅可以引用动画,还可以引用其他动画控制器。这允许你构建分层的状态机。
带条件的动画引用
"animations": [
"walk",
{ "look_at": "query.has_target" },
{ "attack_controller": "query.is_delayed_attacking" }
]当 query.has_target 为真时播放 look_at 动画,当 query.is_delayed_attacking 为真时运行 attack_controller(另一个动画控制器)。
分层控制器示例
以下结构将「基础移动控制」和「头部追踪」分成两个独立的控制器:
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.mymod_ghost.root": {
"initial_state": "default",
"states": {
"default": {
"animations": [
"base_controller",
{ "head_look": "query.has_target" }
]
}
}
},
"controller.animation.mymod_ghost.base": {
"initial_state": "idle",
"states": {
"idle": {
"blend_transition": 0.2,
"animations": ["idle"],
"transitions": [
{ "walk": "query.is_moving" }
]
},
"walk": {
"blend_transition": 0.2,
"animations": ["walk"],
"transitions": [
{ "idle": "!query.is_moving" }
]
}
}
}
}
}在客户端实体定义中,注册短名称映射:
"animations": {
"base_controller": "controller.animation.mymod_ghost.base",
"head_look": "animation.mymod_ghost.look_at_target",
"idle": "animation.mymod_ghost.idle",
"walk": "animation.mymod_ghost.walk"
}query.mod.* 自定义变量
中国版专属
query.mod.* 自定义变量是中国版(网易代理)特有的功能,通过 Python Mod SDK 的 CreateQueryVariable 组件进行注册和设置。国际版不支持此功能。
中国版允许你注册以 query.mod. 为前缀的自定义 Molang 变量,并在动画控制器的转换条件中使用。这是 Mod 开发中控制实体或玩家动画的核心手段。
工作流程
- 注册变量(客户端)— 使用
Register方法注册query.mod.xxx - 设置变量值(客户端)— 使用
Set方法在运行时更新变量值 - 在 JSON 中引用 — 在动画控制器的
transitions中使用query.mod.xxx
Python 注册与设置
# 客户端脚本
import mod.client.extraClientApi as clientApi
class MyClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, systemName):
super(MyClientSystem, self).__init__(namespace, systemName)
self.mPlayerId = clientApi.GetLocalPlayerId()
# 注册自定义变量(基于实体)
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(self.mPlayerId)
queryComp.Register("query.mod.attack_state", 0.0)
queryComp.Register("query.mod.is_casting", 0.0)
def SetAttackState(self, state):
"""设置攻击状态: 0=无, 1=普攻, 2=重击"""
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(self.mPlayerId)
queryComp.Set("query.mod.attack_state", float(state))
def SetCasting(self, isCasting):
"""设置是否正在施法"""
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(self.mPlayerId)
queryComp.Set("query.mod.is_casting", 1.0 if isCasting else 0.0)基于 Level 的全局变量
如果变量不是针对某个实体而是全局的,可以基于 levelId 注册:
levelId = clientApi.GetLevelId()
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(levelId)
queryComp.Register("query.mod.game_phase", 0.0)
queryComp.Set("query.mod.game_phase", 2.0)在动画控制器中使用
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.mymod_ghost.combat": {
"initial_state": "idle",
"states": {
"idle": {
"animations": ["idle"],
"transitions": [
{ "attack_normal": "query.mod.attack_state == 1" },
{ "attack_heavy": "query.mod.attack_state == 2" },
{ "cast": "query.mod.is_casting > 0" }
]
},
"attack_normal": {
"animations": ["attack_normal"],
"transitions": [
{ "idle": "query.mod.attack_state == 0" }
]
},
"attack_heavy": {
"animations": ["attack_heavy"],
"transitions": [
{ "idle": "query.mod.attack_state == 0" }
]
},
"cast": {
"animations": ["cast"],
"transitions": [
{ "idle": "query.mod.is_casting == 0" }
]
}
}
}
}
}注意事项
- 变量名必须以
query.mod.开头 - 变量值使用单精度浮点数存储,整数值大于 16777216 时会有精度损失
- 必须先调用
Register再调用Set,否则设置无效 - 使用完毕后可调用
UnRegister注销
客户端实体绑定
创建好动画和动画控制器后,需要在客户端实体定义文件中注册并激活它们。
注册动画与控制器
在 RP/entity/ 下的客户端实体 JSON 中,通过 description 的 animations 字段注册短名称映射:
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "mymod:ghost",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/entity/ghost"
},
"geometry": {
"default": "geometry.mymod_ghost"
},
"animations": {
"idle": "animation.mymod_ghost.idle",
"walk": "animation.mymod_ghost.walk",
"attack": "animation.mymod_ghost.attack",
"death": "animation.mymod_ghost.death",
"look_at_target": "animation.mymod_ghost.look_at",
"general": "controller.animation.mymod_ghost.general"
},
"scripts": {
"animate": [
"general"
]
},
"render_controllers": [
"controller.render.mymod_ghost"
]
}
}
}scripts/animate 节点
scripts/animate 数组定义了每 tick 自动运行的动画或控制器。每个元素可以是:
无条件运行:
"animate": [
"general"
]带条件运行:
"animate": [
{ "general": "query.is_alive" },
{ "swim_controller": "query.is_in_water" }
]当 Molang 表达式为真时才运行对应的动画/控制器。
音效与粒子绑定
如果动画中包含音效或粒子效果,也需要在客户端实体中注册:
"sound_effects": {
"attack_swing": "mob.mymod_ghost.attack"
},
"particle_effects": {
"ghost_trail": "mymod:ghost_trail_particle"
}离屏更新
默认情况下,实体不在屏幕内时动画效果(粒子、音效)会停止。如果需要持续更新:
"scripts": {
"animate": ["general"],
"should_update_bones_and_effects_offscreen": true
}玩家动画控制器
中国版专属
玩家动画注入 API 是中国版特有功能,通过 ActorRenderCompClient 组件实现。
与自定义实体不同,玩家的动画和动画控制器无法直接通过 JSON 编辑来修改(因为玩家实体由引擎内部管理)。中国版提供了一组 Python API 来向玩家注入自定义动画。
核心 API
| 方法 | 说明 |
|---|---|
AddPlayerAnimation(key, name) | 注册一个动画到玩家实体 |
AddPlayerAnimationController(key, name) | 注册一个动画控制器到玩家实体 |
AddPlayerAnimationIntoState(controller, state, anim, condition) | 向指定控制器的指定状态添加动画 |
AddPlayerScriptAnimate(name, condition) | 向 scripts/animate 添加动画/控制器 |
RebuildPlayerRender() | 重建渲染器,使上述修改生效 |
重要
以上所有 Add 方法调用后,必须调用 RebuildPlayerRender() 才会实际生效。
完整 Python 示例
以下示例为玩家添加一个自定义攻击动画,通过 query.mod.custom_attack 控制播放:
资源包动画文件 RP/animations/player_custom.animation.json:
{
"format_version": "1.8.0",
"animations": {
"animation.player.custom_attack": {
"loop": false,
"animation_length": 0.5,
"bones": {
"rightarm": {
"rotation": {
"0.0": [0, 0, 0],
"0.15": [-90, 20, 0],
"0.3": [-120, -10, 0],
"0.5": [0, 0, 0]
}
}
}
}
}
}资源包动画控制器 RP/animation_controllers/player_custom.ac.json:
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.player.custom_attack": {
"initial_state": "idle",
"states": {
"idle": {
"transitions": [
{ "attacking": "query.mod.custom_attack > 0" }
]
},
"attacking": {
"animations": ["custom_attack_anim"],
"transitions": [
{ "idle": "query.mod.custom_attack == 0" }
]
}
}
}
}
}客户端 Python 脚本:
import mod.client.extraClientApi as clientApi
class MyClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, systemName):
super(MyClientSystem, self).__init__(namespace, systemName)
self.mPlayerId = clientApi.GetLocalPlayerId()
# 注册自定义 Molang 变量
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(self.mPlayerId)
queryComp.Register("query.mod.custom_attack", 0.0)
# 注入玩家动画
self.SetupPlayerAnimation()
def SetupPlayerAnimation(self):
comp = clientApi.GetEngineCompFactory().CreateActorRender(self.mPlayerId)
# 注册动画
comp.AddPlayerAnimation(
"custom_attack_anim",
"animation.player.custom_attack"
)
# 注册动画控制器
comp.AddPlayerAnimationController(
"custom_attack_ctrl",
"controller.animation.player.custom_attack"
)
# 将控制器添加到 scripts/animate(无条件运行)
comp.AddPlayerScriptAnimate("custom_attack_ctrl")
# 重建渲染器使修改生效
comp.RebuildPlayerRender()
def OnPlayerAttack(self):
"""由某个事件触发,开始播放攻击动画"""
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(self.mPlayerId)
queryComp.Set("query.mod.custom_attack", 1.0)
def OnAttackFinished(self):
"""攻击动画结束后,由定时器或回调触发"""
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(self.mPlayerId)
queryComp.Set("query.mod.custom_attack", 0.0)AddPlayerAnimationIntoState 详解
如果你不想创建全新的控制器,而是想在已有的玩家控制器状态中追加动画,可以使用此 API:
comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)
# 注册动画
comp.AddPlayerAnimation("my_hold_anim", "animation.player.my_hold")
# 将动画插入到玩家 root 控制器的 third_person 状态中
# 参数:控制器短名, 状态名, 动画短名, Molang 条件
comp.AddPlayerAnimationIntoState(
"root", # 对应 controller.animation.player.root
"third_person", # 状态名
"my_hold_anim", # 动画短名
"query.mod.is_holding > 0" # 播放条件
)
comp.RebuildPlayerRender()注意事项
AddPlayerAnimation的animationName参数不能包含大写字母,否则注册会失败- 所有 Add 方法在玩家重新进入视野时会由引擎自动重新调用,无需手动处理
- 修改玩家渲染后从后台切回不会重置(1.21 版本修复)
完整示例
以下是一个自定义幽灵(Ghost)生物的完整动画配置,包含待机、行走、攻击三个状态。
文件结构
RP/
├── animations/
│ └── ghost.animation.json
├── animation_controllers/
│ └── ghost.ac.json
├── entity/
│ └── ghost.entity.json
├── models/entity/
│ └── ghost.geo.json
└── textures/entity/
└── ghost.png动画文件
RP/animations/ghost.animation.json
{
"format_version": "1.8.0",
"animations": {
"animation.mymod_ghost.idle": {
"loop": true,
"animation_length": 3.0,
"bones": {
"body": {
"position": {
"0.0": [0, 0, 0],
"1.5": [0, 1.5, 0],
"3.0": [0, 0, 0]
}
},
"head": {
"rotation": {
"0.0": [0, 0, 0],
"1.0": [5, 0, 3],
"2.0": [5, 0, -3],
"3.0": [0, 0, 0]
}
},
"left_arm": {
"rotation": {
"0.0": [0, 0, -5],
"1.5": [0, 0, -10],
"3.0": [0, 0, -5]
}
},
"right_arm": {
"rotation": {
"0.0": [0, 0, 5],
"1.5": [0, 0, 10],
"3.0": [0, 0, 5]
}
}
}
},
"animation.mymod_ghost.walk": {
"loop": true,
"animation_length": 1.0,
"anim_time_update": "query.modified_distance_moved * 0.5",
"bones": {
"body": {
"position": [0, "math.sin(query.anim_time * 360) * 0.5", 0]
},
"left_arm": {
"rotation": {
"0.0": [30, 0, -5],
"0.25": [0, 0, -5],
"0.5": [-30, 0, -5],
"0.75": [0, 0, -5],
"1.0": [30, 0, -5]
}
},
"right_arm": {
"rotation": {
"0.0": [-30, 0, 5],
"0.25": [0, 0, 5],
"0.5": [30, 0, 5],
"0.75": [0, 0, 5],
"1.0": [-30, 0, 5]
}
}
}
},
"animation.mymod_ghost.attack": {
"loop": false,
"animation_length": 0.6,
"bones": {
"body": {
"rotation": {
"0.0": [0, 0, 0],
"0.15": [15, 0, 0],
"0.3": [-20, 0, 0],
"0.6": [0, 0, 0]
}
},
"right_arm": {
"rotation": {
"0.0": [0, 0, 0],
"0.1": [-20, 0, 10],
"0.2": [-100, 30, 0],
"0.35": [20, -10, 0],
"0.6": [0, 0, 0]
}
},
"left_arm": {
"rotation": {
"0.0": [0, 0, 0],
"0.2": [-40, 0, -15],
"0.35": [10, 0, -5],
"0.6": [0, 0, 0]
}
},
"head": {
"rotation": {
"0.0": [0, 0, 0],
"0.15": [-10, 0, 0],
"0.3": [15, 0, 0],
"0.6": [0, 0, 0]
}
}
}
}
}
}动画控制器文件
RP/animation_controllers/ghost.ac.json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.mymod_ghost.general": {
"initial_state": "idle",
"states": {
"idle": {
"blend_transition": 0.2,
"blend_via_shortest_path": true,
"animations": [
"idle"
],
"transitions": [
{ "attack": "query.is_delayed_attacking" },
{ "walk": "query.is_moving" },
{ "death": "!query.is_alive" }
]
},
"walk": {
"blend_transition": 0.2,
"animations": [
"walk"
],
"transitions": [
{ "attack": "query.is_delayed_attacking" },
{ "idle": "!query.is_moving" },
{ "death": "!query.is_alive" }
]
},
"attack": {
"animations": [
"attack"
],
"transitions": [
{ "idle": "query.any_animation_finished && !query.is_moving" },
{ "walk": "query.any_animation_finished && query.is_moving" },
{ "death": "!query.is_alive" }
]
},
"death": {
"blend_transition": 0.1,
"animations": []
}
}
}
}
}客户端实体文件
RP/entity/ghost.entity.json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "mymod:ghost",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/entity/ghost"
},
"geometry": {
"default": "geometry.mymod_ghost"
},
"animations": {
"idle": "animation.mymod_ghost.idle",
"walk": "animation.mymod_ghost.walk",
"attack": "animation.mymod_ghost.attack",
"general": "controller.animation.mymod_ghost.general"
},
"scripts": {
"animate": [
"general"
]
},
"render_controllers": [
"controller.render.default"
]
}
}
}行为包实体文件(关键片段)
BP/entities/ghost.json
{
"format_version": "1.10.0",
"minecraft:entity": {
"description": {
"identifier": "mymod:ghost",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"components": {
"minecraft:health": {
"value": 20,
"max": 20
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:navigation.walk": {
"can_walk": true,
"avoid_water": true
},
"minecraft:movement.basic": {},
"minecraft:jump.static": {},
"minecraft:attack": {
"damage": 4
},
"minecraft:behavior.nearest_attackable_target": {
"priority": 2,
"must_see": true,
"entity_types": [
{
"filters": {
"test": "is_family",
"subject": "other",
"value": "player"
},
"max_dist": 16
}
]
},
"minecraft:behavior.melee_attack": {
"priority": 3,
"speed_multiplier": 1.2,
"track_target": true
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"speed_multiplier": 0.8
},
"minecraft:behavior.look_at_player": {
"priority": 7,
"look_distance": 8
},
"minecraft:behavior.random_look_around": {
"priority": 8
},
"minecraft:physics": {},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": true
},
"minecraft:collision_box": {
"width": 0.6,
"height": 1.8
},
"minecraft:delayed_attack": {
"priority": 3,
"attack_once": true,
"track_target": true,
"speed_multiplier": 1.0,
"reach_multiplier": 1.5,
"hit_delay_pct": 0.5
}
}
}
}常见问题
动画没有播放
检查清单:
- 动画短名称是否正确 — 确认客户端实体
animations中注册的短名称与动画控制器中引用的一致 - 动画控制器是否在
scripts/animate中激活 — 只注册不激活等于没用 - format_version 是否正确 — 动画用
1.8.0,控制器用1.10.0 - 骨骼名称是否匹配 — 动画中的骨骼名必须与模型几何体中的骨骼名完全一致(区分大小写)
- 文件路径是否正确 — 动画放在
RP/animations/,控制器放在RP/animation_controllers/
动画停留在错误的状态
可能原因:
- 转换条件写反了。例如应该写
!query.is_moving却写了query.is_moving - 缺少返回默认状态的转换。确保每个非终结状态都有回到其他状态的路径
- 动画控制器的
initial_state指向了不存在的状态名
转换条件不触发
排查步骤:
- 确认 Molang 查询在当前实体上是否有效。例如
query.is_delayed_attacking需要实体配置了minecraft:delayed_attack组件 - 使用中国版 Mod SDK 的
GetMolangValue方法在运行时查询变量值,确认其返回预期的值:
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(entityId)
value = queryComp.GetMolangValue("query.is_moving")
print "is_moving =", value- 检查转换数组的顺序 — 高优先级的转换要放在数组前面
query.mod.* 变量不生效
排查步骤:
- 确认变量名以
query.mod.开头 - 确认先调用了
Register再调用Set - 确认
Set调用发生在客户端(query.mod.*只能在客户端注册和设置) - 如果用于玩家,确认调用了
RebuildPlayerRender()
Molang 表达式报错
常见错误:
- 缺少引号 — 在 JSON 中,Molang 表达式必须用字符串形式书写:
"math.sin(query.anim_time * 120) * 15" - 使用了 Blockbench 语法 — Blockbench 中不加引号的表达式,导出到 JSON 后必须用引号包裹
- 括号不匹配 — 复杂的嵌套表达式容易遗漏括号
- 使用了不支持的查询 — 不是所有 query 在所有上下文中都有效。服务端的 Molang 能使用的 query 范围远小于客户端
动画混合不自然
优化建议:
- 为每个状态添加
blend_transition(建议0.1~0.3) - 添加
blend_via_shortest_path: true避免旋转绕远路 - 确保循环动画的首尾帧一致,避免循环时出现跳变
- 使用 Catmull-Rom 插值模式让关键帧过渡更平滑
动画控制器在实体重载后重置
动画控制器在实体重新加载时(区块卸载/加载、玩家退出重连等)会回到 initial_state。你的默认状态应该具备重新初始化的能力,不要假设任何 variable 在重载后仍然保持之前的值。
总结
| 主题 | 要点 |
|---|---|
| 动画文件 | format_version: 1.8.0,放在 RP/animations/,定义骨骼关键帧 |
| 动画控制器 | format_version: 1.10.0,放在 RP/animation_controllers/,是状态机 |
| 关键帧 | 支持线性和 Catmull-Rom 插值,支持 pre/post 突变 |
| Molang | 可在关键帧和转换条件中使用,math.* 和 query.* 是核心 |
| query.mod.* | 中国版特有,Python SDK 注册/设置,用于 Mod 控制动画 |
| 玩家动画 | 通过 AddPlayerAnimation 等 API 注入,必须调用 RebuildPlayerRender |
| 客户端实体 | 在 animations 注册短名称,在 scripts/animate 激活 |
