Skip to content

动画与动画控制器 ​

动画(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 表达式来实现数学驱动的动画。

动画文件格式 ​

一个完整的动画文件结构如下:

json
{
  "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]
          }
        }
      }
    }
  }
}

字段详解 ​

字段类型说明
loopboolean 或 stringtrue = 循环播放;false(默认)= 播放一次;"hold_on_last_frame" = 播放后停留在最后一帧
animation_lengthnumber动画总时长(秒)。如果省略,引擎会根据最后一个关键帧的时间自动推断
bonesobject每个键是骨骼名称(必须与几何体模型中的骨骼名称一致),值包含 rotation/position/scale
blend_weightstring(Molang)动画的混合权重,控制该动画对骨骼变换的影响比例。例如 "query.anim_time"
anim_time_updatestring(Molang)自定义动画时间的推进方式,替代默认的逐帧递增。例如 "query.modified_distance_moved"
override_previous_animationboolean为 true 时覆盖之前的动画变换,不做叠加
start_delaystring(Molang)动画开始前的延迟时间
loop_delaystring(Molang)循环动画每次重新开始前的延迟

loop 的三种模式 ​

json
"loop": false

播放一次后停止,骨骼回到初始姿态。适合攻击动画、受击动画等一次性动作。

json
"loop": true

循环播放。适合待机、行走等持续性动画。

json
"loop": "hold_on_last_frame"

播放完毕后停留在最后一帧。适合死亡动画。

关键帧 ​

关键帧(Keyframe)是动画的核心数据。每个关键帧指定一个时间点和该时间点的骨骼变换值。

基本格式 ​

关键帧以时间(秒,字符串格式)为键,值为 [X, Y, Z] 数组:

json
"rotation": {
  "0.0": [0, 0, 0],
  "0.5": [15, 0, 0],
  "1.0": [0, 0, 0]
}

这表示骨骼在 0 秒时无旋转,0.5 秒时绕 X 轴旋转 15 度,1 秒时回到原始状态。两个关键帧之间的值通过线性插值自动计算。

简写格式 ​

如果动画不使用关键帧时间轴(例如数学驱动的动画),可以直接写一个数组:

json
"rotation": [0, "query.target_x_rotation", 0]

此时该值在每一帧实时计算,不存在关键帧插值。

缩放的特殊写法 ​

缩放支持统一值(三轴等比缩放):

json
"scale": 1.5

等价于 [1.5, 1.5, 1.5],也支持关键帧形式:

json
"scale": {
  "0.0": 1.0,
  "0.5": 1.5,
  "1.0": 1.0
}

平滑插值(Catmull-Rom) ​

默认情况下,关键帧之间使用线性插值。如果你需要更平滑的曲线过渡,可以使用 Catmull-Rom 样条插值:

json
"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)的不同值,实现突变效果:

json
"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 — 从该关键帧出发时使用的起始值(从此处插值到下一帧)

这在需要在某个时间点"跳变"到不同姿态时非常有用。

完整多骨骼关键帧示例 ​

json
{
  "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 表达式:

json
"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根据攻击时间控制手臂挥动

数学驱动的车轮动画示例 ​

json
{
  "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 字段可以改变动画时间轴的推进方式:

json
{
  "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 是制作基岩版模型和动画的首选工具。以下是制作动画的步骤:

步骤一:切换到动画模式 ​

  1. 打开你的 .bbmodel 模型文件
  2. 在 Blockbench 顶部点击 Animate(动画)选项卡,切换到动画编辑模式

步骤二:创建动画 ​

  1. 在左侧 Animations 面板中,点击 +(加号)按钮
  2. 输入动画名称,如 animation.mymod_ghost.idle
  3. 设置循环模式:勾选 Loop 表示循环播放

步骤三:设置关键帧 ​

  1. 在底部时间轴上,将播放头拖到目标时间点(如 0.0 秒)
  2. 在右侧选择一个骨骼(如 body)
  3. 调整骨骼的旋转、位移或缩放
  4. Blockbench 会自动在当前时间点创建关键帧
  5. 将播放头移到下一个时间点,再次调整骨骼属性

步骤四:预览动画 ​

  1. 点击时间轴下方的 Play(播放)按钮预览动画效果
  2. 调整关键帧位置和数值,直到效果满意

步骤五:使用 Molang 表达式 ​

  1. 选中一个关键帧
  2. 在左侧关键帧面板中,将数值替换为 Molang 表达式
  3. 注意:在 Blockbench 中编写 Molang 表达式不需要加引号,引号只在直接编辑 JSON 时需要
  4. 某些 Molang 查询(如 query.is_moving)在 Blockbench 中无法预览,你可以在 Variable Placeholders 中添加模拟值,例如:query.modified_distance_moved = time * 8

步骤六:导出动画 ​

  1. 点击 File > Export > Export Bedrock Animations
  2. 保存到资源包的 RP/animations/ 目录
  3. 文件名建议使用 实体名.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 最多切换一次状态

基本结构 ​

json
{
  "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 — 动画列表 ​

当前状态下播放的动画。可以是字符串(动画短名称)或带条件的对象:

json
"animations": [
  "idle_body",
  { "idle_head": "query.is_alive" }
]

动画短名称需要在客户端实体定义中注册(详见客户端实体绑定)。

transitions — 转换列表 ​

一个数组,每个元素是 { "目标状态名": "Molang 条件" } 格式的对象:

json
"transitions": [
  { "walk": "query.is_moving" },
  { "attack": "query.is_delayed_attacking" },
  { "death": "!query.is_alive" }
]

引擎按数组顺序检查,第一个条件为真的转换会被执行。因此,更高优先级的转换应放在前面。

on_entry / on_exit — 进入/退出命令 ​

进入状态或退出状态时执行的命令(仅在行为包动画控制器中有效):

json
"idle": {
  "on_entry": [
    "/say 进入待机状态",
    "@s mymod:on_idle"
  ],
  "on_exit": [
    "/say 离开待机状态"
  ]
}

命令支持三种形式:

  • 斜杠命令:"/say Hello"
  • 实体事件:"@s mymod:some_event"
  • Molang 表达式:"variable.state = 1;"

blend_transition — 混合过渡 ​

控制从前一个状态过渡到当前状态时的动画混合时长(秒):

json
"idle": {
  "blend_transition": 0.2,
  "animations": ["idle"]
}

值越大过渡越平滑。建议设为 0.1 ~ 0.3。

blend_via_shortest_path ​

设为 true 时,旋转混合通过最短路径计算(避免骨骼绕远路旋转):

json
"idle": {
  "blend_transition": 0.2,
  "blend_via_shortest_path": true,
  "animations": ["idle"]
}

完整状态示例 ​

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": [
            { "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_moving0.0/1.0实体是否正在移动
query.is_on_ground0.0/1.0实体是否在地面上
query.is_in_water0.0/1.0实体是否在水中
query.is_in_water_or_rain0.0/1.0实体是否在水中或雨中
query.is_swimming0.0/1.0实体是否在游泳
query.is_sneaking0.0/1.0实体是否在潜行
query.is_sprinting0.0/1.0实体是否在疾跑
query.is_jumping0.0/1.0实体是否正在跳跃
query.is_alive0.0/1.0实体是否存活
query.is_baby0.0/1.0实体是否为幼年状态
query.is_sleeping0.0/1.0实体是否在睡觉
query.is_riding0.0/1.0实体是否在骑乘
query.has_rider0.0/1.0实体是否有骑乘者
query.is_angry0.0/1.0实体是否处于愤怒状态
query.is_charged0.0/1.0实体是否被充能(如苦力怕)
query.is_sheared0.0/1.0实体是否被剪过毛
query.is_saddled0.0/1.0实体是否装了鞍
query.is_delayed_attacking0.0/1.0实体是否正在延迟攻击
query.is_first_person0.0/1.0当前是否为第一人称视角
query.modified_move_speedfloat修正后的移动速度(含移速加成)
query.ground_speedfloat地面移动速度
query.modified_distance_movedfloat修正后的已移动距离(持续递增)
query.healthfloat当前生命值
query.max_healthfloat最大生命值
query.variantint实体变体值(minecraft:variant 组件的值)
query.mark_variantint标记变体值(minecraft:mark_variant 组件的值)
query.skin_idint皮肤 ID
query.anim_timefloat当前动画已播放的时间(秒)
query.life_timefloat实体存在的时间(秒)
query.any_animation_finished0.0/1.0当前状态中是否有任何动画播放完毕
query.all_animations_finished0.0/1.0当前状态中的所有动画是否都播放完毕
query.head_x_rotation(n)float头部 X 轴旋转(俯仰角),参数为头的索引
query.head_y_rotation(n)float头部 Y 轴旋转(偏航角)
query.target_x_rotationfloat朝向目标的 X 旋转
query.target_y_rotationfloat朝向目标的 Y 旋转

组合条件 ​

你可以用逻辑运算符组合多个条件:

json
"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 数组中,不仅可以引用动画,还可以引用其他动画控制器。这允许你构建分层的状态机。

带条件的动画引用 ​

json
"animations": [
  "walk",
  { "look_at": "query.has_target" },
  { "attack_controller": "query.is_delayed_attacking" }
]

当 query.has_target 为真时播放 look_at 动画,当 query.is_delayed_attacking 为真时运行 attack_controller(另一个动画控制器)。

分层控制器示例 ​

以下结构将「基础移动控制」和「头部追踪」分成两个独立的控制器:

json
{
  "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" }
          ]
        }
      }
    }
  }
}

在客户端实体定义中,注册短名称映射:

json
"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 开发中控制实体或玩家动画的核心手段。

工作流程 ​

  1. 注册变量(客户端)— 使用 Register 方法注册 query.mod.xxx
  2. 设置变量值(客户端)— 使用 Set 方法在运行时更新变量值
  3. 在 JSON 中引用 — 在动画控制器的 transitions 中使用 query.mod.xxx

Python 注册与设置 ​

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 注册:

python
levelId = clientApi.GetLevelId()
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(levelId)
queryComp.Register("query.mod.game_phase", 0.0)
queryComp.Set("query.mod.game_phase", 2.0)

在动画控制器中使用 ​

json
{
  "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 字段注册短名称映射:

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",
        "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 自动运行的动画或控制器。每个元素可以是:

无条件运行:

json
"animate": [
  "general"
]

带条件运行:

json
"animate": [
  { "general": "query.is_alive" },
  { "swim_controller": "query.is_in_water" }
]

当 Molang 表达式为真时才运行对应的动画/控制器。

音效与粒子绑定 ​

如果动画中包含音效或粒子效果,也需要在客户端实体中注册:

json
"sound_effects": {
  "attack_swing": "mob.mymod_ghost.attack"
},
"particle_effects": {
  "ghost_trail": "mymod:ghost_trail_particle"
}

离屏更新 ​

默认情况下,实体不在屏幕内时动画效果(粒子、音效)会停止。如果需要持续更新:

json
"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:

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:

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 脚本:

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:

python
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

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

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

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

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
      }
    }
  }
}

常见问题 ​

动画没有播放 ​

检查清单:

  1. 动画短名称是否正确 — 确认客户端实体 animations 中注册的短名称与动画控制器中引用的一致
  2. 动画控制器是否在 scripts/animate 中激活 — 只注册不激活等于没用
  3. format_version 是否正确 — 动画用 1.8.0,控制器用 1.10.0
  4. 骨骼名称是否匹配 — 动画中的骨骼名必须与模型几何体中的骨骼名完全一致(区分大小写)
  5. 文件路径是否正确 — 动画放在 RP/animations/,控制器放在 RP/animation_controllers/

动画停留在错误的状态 ​

可能原因:

  • 转换条件写反了。例如应该写 !query.is_moving 却写了 query.is_moving
  • 缺少返回默认状态的转换。确保每个非终结状态都有回到其他状态的路径
  • 动画控制器的 initial_state 指向了不存在的状态名

转换条件不触发 ​

排查步骤:

  1. 确认 Molang 查询在当前实体上是否有效。例如 query.is_delayed_attacking 需要实体配置了 minecraft:delayed_attack 组件
  2. 使用中国版 Mod SDK 的 GetMolangValue 方法在运行时查询变量值,确认其返回预期的值:
python
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(entityId)
value = queryComp.GetMolangValue("query.is_moving")
print "is_moving =", value
  1. 检查转换数组的顺序 — 高优先级的转换要放在数组前面

query.mod.* 变量不生效 ​

排查步骤:

  1. 确认变量名以 query.mod. 开头
  2. 确认先调用了 Register 再调用 Set
  3. 确认 Set 调用发生在客户端(query.mod.* 只能在客户端注册和设置)
  4. 如果用于玩家,确认调用了 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 激活