Skip to content

进阶生物开发 ​

在掌握自定义基础生物之后,本文将介绍更高级的生物开发技术,包括覆盖(重写)原版生物的行为与外观、骑乘系统配置、通过 Python API 动态修改玩家模型与动画,以及自定义坐骑系统的搭建。

前置条件

请先阅读 自定义基础生物 了解实体定义的基础知识。进阶功能通常需要结合 Python 脚本(ModAPI)来实现动态控制。

覆盖原版生物 ​

我们可以通过定义与原版生物相同 identifier 的实体 JSON,来重写原版生物的行为与外观。引擎在加载时,如果检测到行为包或资源包中存在与原版相同标识符的实体定义,会以自定义版本覆盖原版。

重写生物行为 ​

在 behavior_pack/entities/ 目录下创建一个 JSON 文件,将 identifier 设为你想覆盖的原版生物(如 minecraft:pig)。你可以在 components 中添加或替换组件,从而改变生物的行为逻辑。

覆盖规则:

  • components 中的同名组件会合并,即你只需要写想要修改/添加的组件
  • component_groups 中同名的组件组会被整体覆盖
  • events 中同名事件也会被整体覆盖

下面的示例让猪拥有苦力怕的攻击与爆炸行为——猪会主动寻找附近的玩家并接近,靠近后触发爆炸并消失:

json
{
  "format_version": "1.16.0",
  "minecraft:entity": {
    "description": {
      "identifier": "minecraft:pig",
      "is_spawnable": true,
      "is_summonable": true,
      "is_experimental": false
    },
    "components": {
      "minecraft:nearest_attackable_target": {
        "must_see": true,
        "reselect_targets": true,
        "within_radius": 25.0,
        "entity_types": [
          {
            "filters": {
              "test": "is_family",
              "subject": "other",
              "value": "player"
            },
            "max_dist": 25
          }
        ]
      },
      "minecraft:target_nearby_sensor": {
        "inside_range": 3.0,
        "outside_range": 7.0,
        "on_inside_range": {
          "event": "explode",
          "target": "self"
        }
      }
    },
    "component_groups": {
      "exploding": {
        "minecraft:explode": {
          "fuse_length": 1.5,
          "fuse_lit": true,
          "power": 3,
          "causes_fire": false
        }
      }
    },
    "events": {
      "explode": {
        "add": {
          "component_groups": ["exploding"]
        }
      }
    }
  }
}

将上述文件保存为 behavior_pack/entities/custom_pig.json(文件名不影响覆盖,关键是 identifier 一致)。进入游戏后,所有猪都会拥有主动攻击并爆炸的行为。

重写生物外观 ​

重写生物外观的方式是在 resource_pack/entity/ 目录下创建一个与原版生物相同 identifier 的客户端实体定义文件。你可以更改贴图、几何模型、动画以及渲染控制器。

以下示例将猪的贴图替换为自定义贴图:

json
{
  "format_version": "1.10.0",
  "minecraft:client_entity": {
    "description": {
      "identifier": "minecraft:pig",
      "materials": {
        "default": "entity_alphatest"
      },
      "textures": {
        "default": "textures/entity/custom_pig",
        "saddled": "textures/entity/custom_pig_saddle"
      },
      "geometry": {
        "default": "geometry.pig"
      },
      "animations": {
        "setup": "animation.pig.setup.v1.0",
        "walk": "animation.quadruped.walk",
        "look_at_target": "animation.common.look_at_target",
        "baby_transform": "animation.pig.baby_transform"
      },
      "scripts": {
        "animate": [
          "setup",
          { "walk": "query.modified_move_speed" },
          "look_at_target",
          { "baby_transform": "query.is_baby" }
        ]
      },
      "render_controllers": ["controller.render.pig"],
      "spawn_egg": {
        "texture": "spawn_egg",
        "texture_index": 2
      }
    }
  }
}

然后在 resource_pack/textures/entity/ 目录下放入你的 custom_pig.png 和 custom_pig_saddle.png 贴图文件。

TIP

重写外观时,建议从原版资源包中找到对应的 *.entity.json 文件作为模板进行修改,确保不遗漏必要的动画和渲染控制器定义。

重写玩家客户端实体定义 ​

玩家是一种特殊的实体。如果需要重写玩家的客户端定义,必须基于 vanilla_netease 资源包中的 entity/player.entity.json 进行修改,而不是 vanilla 或其他原版资源包。

皮肤与个性化搭配的兼容问题 ​

引擎在支持玩家皮肤时,会对 player 的 render_controllers 进行硬编码修改:

  1. 自发光皮肤处理:引擎会根据玩家是否使用了拥有自发光效果的会员皮肤,移除相应的渲染控制器。具体来说:

    • 使用自发光皮肤时,移除 controller.render.player.first_person 和 controller.render.player.third_person
    • 未使用自发光皮肤时,移除 controller.render.player.first_person_bloom 和 controller.render.player.third_person_bloom
    • 因此,必须同时保留以上 4 个渲染控制器以兼容自发光
  2. 个性化搭配处理:如果玩家使用了个性化搭配(Persona),引擎会自动添加以下渲染控制器:

    • controller.render.player.first_person_with_persona
    • controller.render.player.third_person_with_persona
    • controller.render.persona_animated_face.third_person

当你想完全重写玩家外观时(例如把玩家模型替换为企鹅),这些自动添加的渲染控制器可能会导致渲染冲突。

使用 netease_override_persona 属性 ​

为了解决上述问题,可以在 description 中添加 netease_override_persona 属性。当该属性为 true 时,引擎不会对 player 的 render_controllers 做任何硬编码修改:

json
{
  "format_version": "1.10.0",
  "minecraft:client_entity": {
    "description": {
      "identifier": "minecraft:player",
      "netease_override_persona": true,
      "materials": {
        "default": "entity_alphatest"
      },
      "textures": {
        "default": "textures/entity/penguin/penguin"
      },
      "geometry": {
        "default": "geometry.penguin"
      },
      "animations": {
        "walk": "animation.penguin.walk",
        "idle": "animation.penguin.idle",
        "swim": "animation.penguin.swim"
      },
      "scripts": {
        "animate": [
          { "walk": "query.modified_move_speed" },
          { "idle": "!query.is_moving" },
          { "swim": "query.is_in_water" }
        ]
      },
      "render_controllers": ["controller.render.penguin"]
    }
  }
}

这样配置后,不论玩家是否使用了个性化搭配或自发光皮肤,渲染控制器都不会被引擎额外修改,确保你的自定义外观正常显示。

覆盖冲突

如果多个 Mod 同时覆盖同一个原版生物的定义,会产生冲突——只有最后加载的 Mod 的覆盖会生效。在开发时应注意避免与其他 Mod 覆盖相同的原版生物。如果你只需要修改局部行为,尽量使用 component_groups 和 events 进行增量修改,而不是整体替换 components。

骑乘系统(minecraft:rideable) ​

minecraft:rideable 组件用于定义实体的骑乘位置,让其他实体可以骑上该实体。船、马、矿车等原版实体都使用了该组件。

组件参数 ​

参数默认值说明
seat_count1该生物拥有的最大骑乘位置数量
crouching_skip_interacttrue为 true 时,蹲伏状态的实体无法与该实体交互(上骑)
interact_text""与玩家交互时显示在 UI 按钮上的文本(本地化键)
family_types[]能够骑乘该实体的实体种类列表(家族类型)
controlling_seat0控制该实体移动的座位索引(riderIndex)
pull_in_entitiesfalse为 true 时,该实体会将正确 family_type 的实体自动拉入可用座位
rider_can_interactfalse为 true 时,骑手查看该实体时可以选择(交互)该实体

座位参数(seats) ​

seats 字段可以是一个对象(单座位)或一个数组(多座位),每个座位的参数如下:

参数默认值说明
position[0, 0, 0]座位相对于实体原点的位置 [x, y, z]
min_rider_count0该座位启用所需的最少骑乘者数量
max_rider_countseat_count该座位启用所需的最多骑乘者数量
rotate_rider_by0骑乘者相对于坐骑的旋转角度(度)
lock_rider_rotation181骑乘者允许旋转视角的范围(度),181 表示不限制

座位启用条件 ​

座位是否启用取决于当前坐骑上骑乘者的数量是否处于该座位的 [min_rider_count, max_rider_count] 范围内。这个机制允许实现复杂的座位切换逻辑,例如原版船在只有 1 名乘客时居中坐,2 名乘客时分坐两侧。

示例:船(2 座位) ​

原版船定义了 3 个座位配置,但最多只能容纳 2 名骑乘者。通过 min_rider_count 和 max_rider_count 实现了座位动态切换:

json
{
  "format_version": "1.16.0",
  "minecraft:entity": {
    "description": {
      "identifier": "minecraft:boat",
      "is_spawnable": false,
      "is_summonable": true,
      "is_experimental": false
    },
    "components": {
      "minecraft:rideable": {
        "seat_count": 2,
        "interact_text": "action.interact.ride.boat",
        "pull_in_entities": true,
        "seats": [
          {
            "position": [0.0, -0.2, 0.0],
            "min_rider_count": 0,
            "max_rider_count": 1,
            "rotate_rider_by": -90,
            "lock_rider_rotation": 90
          },
          {
            "position": [0.2, -0.2, 0.0],
            "min_rider_count": 2,
            "max_rider_count": 2,
            "rotate_rider_by": -90,
            "lock_rider_rotation": 90
          },
          {
            "position": [-0.6, -0.2, 0.0],
            "min_rider_count": 2,
            "max_rider_count": 2,
            "lock_rider_rotation": 90
          }
        ]
      }
    }
  }
}

工作逻辑:

  • 1 名乘客时:骑乘者数量为 1,只有第一个座位满足条件(min=0, max=1),乘客坐在船的正中间
  • 2 名乘客时:骑乘者数量为 2,第一个座位不满足条件(max=1),第二和第三个座位启用(min=2, max=2),两名乘客分别坐在船的前后两侧

示例:马(1 个控制座位) ​

马只需要 1 个座位,且限制只有玩家和僵尸类型可以骑乘:

json
{
  "minecraft:rideable": {
    "seat_count": 1,
    "family_types": [
      "player",
      "zombie"
    ],
    "interact_text": "action.interact.mount",
    "seats": {
      "position": [0.0, 1.1, -0.2]
    }
  }
}

当 seats 只有一个位置时,可以直接用对象而非数组。controlling_seat 默认为 0,意味着第一位骑乘者即为控制者。

示例:自定义 4 座载具 ​

下面展示一个拥有 4 个固定座位的自定义载具配置,不需要动态切换座位:

json
{
  "format_version": "1.16.0",
  "minecraft:entity": {
    "description": {
      "identifier": "custom:vehicle",
      "is_spawnable": true,
      "is_summonable": true,
      "is_experimental": false
    },
    "components": {
      "minecraft:rideable": {
        "seat_count": 4,
        "crouching_skip_interact": true,
        "interact_text": "action.interact.ride.vehicle",
        "family_types": ["player"],
        "controlling_seat": 0,
        "rider_can_interact": false,
        "seats": [
          {
            "position": [0.5, 1.0, 0.8],
            "lock_rider_rotation": 45
          },
          {
            "position": [-0.5, 1.0, 0.8],
            "lock_rider_rotation": 45
          },
          {
            "position": [0.5, 1.0, -0.5],
            "lock_rider_rotation": 90
          },
          {
            "position": [-0.5, 1.0, -0.5],
            "lock_rider_rotation": 90
          }
        ]
      },
      "minecraft:health": {
        "value": 40,
        "max": 40
      },
      "minecraft:physics": {},
      "minecraft:pushable": {
        "is_pushable": false,
        "is_pushable_by_piston": false
      },
      "minecraft:collision_box": {
        "width": 2.0,
        "height": 1.5
      }
    }
  }
}

在不设置 min_rider_count 和 max_rider_count 的情况下,座位与骑乘者是一一对应的:第一个上车的乘客坐第一个位置,第二个坐第二个位置,以此类推。

自定义坐骑 API ​

中国版提供了一系列 Python API 用于在运行时动态控制坐骑的骑乘者和座位,实现更灵活的载具系统。

座位与乘客队列的概念 ​

在使用 API 之前,需要理解两个关键概念:

  • seatIndex(座位序号):指 seats 数组中当前已启用的座位的索引,从 0 开始
  • riderIndex(骑乘者序号):指骑乘者在骑乘者队列中的位置,从 0 开始

在大多数自定义坐骑中(不设置 min_rider_count / max_rider_count),seatIndex 和 riderIndex 是一一对应的。但在原版船这样有动态座位切换的实体中,两者的对应关系会随骑乘者数量变化而变化。

controlling_seat 参数默认为 0,表示 riderIndex 为 0 的骑乘者来控制坐骑。

GetRiders - 获取骑乘者信息 ​

获取坐骑上所有骑乘者的信息,包括实体 ID、在骑乘者队列中的位置以及当前座位编号。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)
riders = comp.GetRiders()
# riders 返回值示例:
# [
#     {"id": playerId1, "riderIndex": 0, "seatIndex": 0},
#     {"id": playerId2, "riderIndex": 1, "seatIndex": 1}
# ]
if riders:
    for rider in riders:
        print "骑乘者ID: %s, riderIndex: %d, seatIndex: %d" % (
            rider["id"], rider["riderIndex"], rider["seatIndex"]
        )

SetEntityLockRider - 锁定骑乘者队列 ​

在原版坐骑中,当一个骑乘者离开坐骑时,后面所有骑乘者会往前移动一位。SetEntityLockRider 可以屏蔽这种逻辑——当一个骑乘者离开时,不会变动坐骑上已有乘客的 riderIndex。

这对于实现类似车辆载具的效果非常重要:驾驶位上的司机下车后,副驾驶的玩家仍然在原来的位置上。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)
# 锁定骑乘者队列
comp.SetEntityLockRider(True)

锁定后的行为变化:

场景未锁定(默认)锁定后
实体上骑清理队列空位后,玩家优先插入队首从队首寻找空位,无空位则插入队尾
实体下骑后面的骑乘者依次前移一位原位置变为空,其他骑乘者位置不变
存档重载空位被删除,玩家移至最前空位被删除,玩家移至最前(与未锁定一致)

WARNING

如果坐骑未锁定,重载存档后空的位置将被删除,并且坐骑上如果有玩家,会按照原版逻辑将玩家移动到最前面。

SetPlayerRideEntity / SetRiderRideEntity - 指定骑乘位置 ​

通过这两个接口可以让玩家或实体骑乘坐骑,并直接成为骑乘者队列中指定序号的骑乘者。建议搭配 SetEntityLockRider 使用。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)

# 让玩家骑乘坐骑,成为 riderIndex=1 的骑乘者(第 2 个位置)
comp.SetRiderRideEntity(playerId, rideEntityId, 1)

# 也可以使用玩家专用接口
# comp.SetPlayerRideEntity(playerId, rideEntityId, 1)

注意事项:

  • 目标坐骑上指定的序号位置必须没有骑乘者
  • 如果玩家/实体当前已在另一个坐骑上,会先离开当前坐骑再骑乘到目标坐骑
  • 该接口不受 SetEntityLockRider 锁定状态的影响

ChangeRiderSeat - 交换骑乘者位置 ​

ChangeRiderSeat 可以改变骑乘者的 riderIndex。如果目标 riderIndex 上已有骑乘者,则交换两个骑乘者的位置。需要实体已处于骑乘状态。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)

# 将 entityId 的 riderIndex 改为 1
# 如果位置 1 上已有骑乘者,则与其交换位置
comp.ChangeRiderSeat(1)

动态座位管理 ​

以下三个接口用于在运行时动态增删改座位。

AddEntitySeat - 添加座位 ​

在坐骑上动态添加一个新座位。该接口同时会使 seat_count 加 1。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)

# 参数:(position, rotateRiderBy, lockRiderRotation)
# 返回值:新座位的 seatIndex
seatIndex = comp.AddEntitySeat((1.0, 1.0, 1.0), 90.0, 90.0)
print "新座位索引: %d" % seatIndex

SetEntitySeat - 修改座位 ​

修改一个已有座位的位置、旋转和视角范围。处在该座位上的骑乘者会立即移动到新位置。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)

# 参数:(seatIndex, position, rotateRiderBy, lockRiderRotation)
comp.SetEntitySeat(3, (1.0, 1.0, 1.0), 90.0, 90.0)

WARNING

SetEntitySeat 和 DeleteEntitySeat 只能修改/删除通过 AddEntitySeat 接口动态添加的座位,不能修改 JSON 中定义的原始座位。

DeleteEntitySeat - 删除座位 ​

删除一个动态添加的座位。处于该座位上的骑乘者会被强制下车。

python
import mod.server.extraServerApi as serverApi
comp = serverApi.GetEngineCompFactory().CreateRide(entityId)
comp.DeleteEntitySeat(3)

完整示例:多座位载具系统 ​

下面是一个完整的服务端脚本示例,实现一个可以动态添加座位的载具系统:

python
import mod.server.extraServerApi as serverApi

ServerSystem = serverApi.GetServerSystemCls()

class VehicleServerSystem(ServerSystem):

    def __init__(self, namespace, systemName):
        ServerSystem.__init__(self, namespace, systemName)
        self.ListenForEvent(
            serverApi.GetEngineNamespace(),
            serverApi.GetEngineSystemName(),
            "AddEntityServerEvent",
            self,
            self.OnEntityAdded
        )

    def OnEntityAdded(self, args):
        entityId = args["id"]
        engineType = args.get("engineTypeStr", "")
        if engineType != "custom:vehicle":
            return

        # 锁定骑乘者队列,防止下车后座位偏移
        rideComp = serverApi.GetEngineCompFactory().CreateRide(entityId)
        rideComp.SetEntityLockRider(True)

    def LetPlayerDrive(self, playerId, vehicleId):
        """让玩家作为司机(riderIndex=0)骑乘载具"""
        rideComp = serverApi.GetEngineCompFactory().CreateRide(vehicleId)
        rideComp.SetRiderRideEntity(playerId, vehicleId, 0)

    def LetPlayerRidePassenger(self, playerId, vehicleId, seatNum):
        """让玩家作为乘客骑乘载具的指定座位"""
        rideComp = serverApi.GetEngineCompFactory().CreateRide(vehicleId)
        rideComp.SetRiderRideEntity(playerId, vehicleId, seatNum)

    def SwapSeats(self, vehicleId, entityId, targetIndex):
        """交换骑乘者到指定位置"""
        rideComp = serverApi.GetEngineCompFactory().CreateRide(entityId)
        rideComp.ChangeRiderSeat(targetIndex)

    def AddExtraSeat(self, vehicleId, pos, rotation):
        """为载具动态添加一个新座位"""
        rideComp = serverApi.GetEngineCompFactory().CreateRide(vehicleId)
        newIndex = rideComp.AddEntitySeat(pos, rotation, 90.0)
        return newIndex

    def RemoveExtraSeat(self, vehicleId, seatIndex):
        """删除动态添加的座位"""
        rideComp = serverApi.GetEngineCompFactory().CreateRide(vehicleId)
        rideComp.DeleteEntitySeat(seatIndex)

    def PrintAllRiders(self, vehicleId):
        """打印载具上所有骑乘者信息"""
        rideComp = serverApi.GetEngineCompFactory().CreateRide(vehicleId)
        riders = rideComp.GetRiders()
        if not riders:
            print "载具上没有骑乘者"
            return
        for rider in riders:
            print "实体: %s, riderIndex: %d, seatIndex: %d" % (
                rider["id"], rider["riderIndex"], rider["seatIndex"]
            )

玩家外观 API ​

中国版提供了一套客户端 Python API,用于在运行时动态修改玩家的渲染外观,包括材质、几何体、贴图、动画、动画控制器和渲染控制器等。

所有修改都需要通过 ActorRender 组件进行,并在修改完成后调用 RebuildPlayerRender() 使其生效。修改会在玩家重新进入视野时自动重新应用。

修改材质 ​

使用 AddPlayerRenderMaterial 修改玩家的渲染材质。玩家的默认渲染材质为 entity_alphatest。

python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)

# 将玩家材质切换为自定义材质
comp.AddPlayerRenderMaterial("default", "custom_entity")
comp.RebuildPlayerRender()

恢复默认材质:

python
comp.AddPlayerRenderMaterial("default", "entity_alphatest")
comp.RebuildPlayerRender()

修改几何体与贴图 ​

使用 AddPlayerGeometry 和 AddPlayerTexture 修改玩家的模型和皮肤。

  • 玩家默认几何体:geometry.humanoid.custom
  • 玩家默认贴图:textures/entity/steve
python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)

# 将玩家改为僵尸模型和贴图
comp.AddPlayerGeometry("default", "geometry.zombie.v1.8")
comp.AddPlayerTexture("default", "textures/entity/zombie/zombie")
comp.RebuildPlayerRender()

恢复默认外观:

python
comp.AddPlayerGeometry("default", "geometry.humanoid.custom")
comp.AddPlayerTexture("default", "textures/entity/steve")
comp.RebuildPlayerRender()

WARNING

修改几何体时,动画和贴图都是与几何体密切相关的。如果只修改几何体而不调整动画与贴图,可能出现动画无法正常播放、贴图错位等问题。建议同时修改三者。

修改动画 ​

使用 AddPlayerAnimation 为玩家添加或覆盖动画。

首先在资源包中定义自定义动画(如 resource_pack/animations/player.custom.animation.json):

json
{
  "format_version": "1.8.0",
  "animations": {
    "animation.player.custom.move.arms": {
      "loop": true,
      "bones": {
        "leftarm": {
          "rotation": ["variable.tcos0 + 30", 0.0, 0.0]
        },
        "rightarm": {
          "rotation": ["-variable.tcos0 + 30", 0.0, 0.0]
        }
      }
    }
  }
}

然后通过 API 覆盖原版的 move.arms 动画:

python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)
comp.AddPlayerAnimation("move.arms", "animation.player.custom.move.arms")
comp.RebuildPlayerRender()

该示例会让玩家的手臂在走动时始终保持 30 度的额外抬起角度。

修改动画控制器 ​

使用 AddPlayerAnimationController 为玩家添加或替换动画控制器。

python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)
comp.AddPlayerAnimationController("root", "controller.animation.player.root_custom")
comp.RebuildPlayerRender()

向已有控制器状态注入动画 ​

AddPlayerAnimationIntoState 可以在玩家已有的动画控制器的指定状态中插入新的动画或动画控制器,而不需要完全替换整个控制器。

python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)

# 在 root 控制器的 first_person 状态中添加一个攻击动画控制器
# 该动画控制器仅在 query.mod.index > 0 时激活
comp.AddPlayerAnimationIntoState(
    "root",                                # 动画控制器名称(简称)
    "first_person",                        # 状态名称
    "first_person_attack_controller_new",  # 要添加的动画/控制器名称
    "query.mod.index > 0"                  # 激活条件(MoLang 表达式)
)
comp.RebuildPlayerRender()

参数说明:

  • animationControllerName:目标动画控制器的简称,如 root 代表 controller.animation.player.root
  • stateName:目标状态的名称,如 first_person、third_person
  • animationName:要注入的动画或动画控制器的简称
  • condition:MoLang 条件表达式,留空则无条件播放

添加渲染控制器 ​

使用 AddPlayerRenderController 为玩家添加自定义渲染控制器。

python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)
comp.AddPlayerRenderController(
    "controller.render.custom_weapon",  # 渲染控制器名称
    "query.mod.show_weapon > 0"         # 控制条件
)
comp.RebuildPlayerRender()

添加到 scripts/animate ​

使用 AddPlayerScriptAnimate 将动画或动画控制器添加到玩家实体定义的 scripts/animate 节点中,使其被引擎自动执行。

python
import mod.client.extraClientApi as clientApi

comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)

# 先添加动画控制器
comp.AddPlayerAnimationController(
    "custom_controller",
    "controller.animation.player.custom"
)
# 再将其添加到 scripts/animate 并设置激活条件
comp.AddPlayerScriptAnimate(
    "custom_controller",       # 动画/控制器简称
    "query.mod.enabled > 0"    # 条件表达式
)
comp.RebuildPlayerRender()

RebuildPlayerRender - 重建渲染 ​

所有上述修改完成后,必须调用 RebuildPlayerRender() 才能生效。 可以在执行完一批修改后只调用一次。

python
comp.RebuildPlayerRender()

# 如果需要同时清除已绑定的粒子特效:
comp.RebuildPlayerRender(True)

使用 query.mod 变量控制动画 ​

通过 CreateQueryVariable 组件注册自定义的 query.mod.* 变量,可以在脚本中动态控制动画的播放条件。

注册与设置(客户端):

python
import mod.client.extraClientApi as clientApi

# 注册自定义变量(名称必须以 query.mod. 开头)
queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(playerId)
queryComp.Register("query.mod.anim_state", 0.0)

# 设置变量值来控制动画
queryComp.Set("query.mod.anim_state", 1.0)  # 激活动画
queryComp.Set("query.mod.anim_state", 0.0)  # 关闭动画

在 JSON 中配合使用:

json
{
  "scripts": {
    "animate": [
      { "custom_walk": "query.mod.anim_state > 0" },
      { "custom_attack": "query.mod.anim_state > 1" }
    ]
  }
}

完整示例:变身系统 ​

下面是一个完整的客户端脚本示例,实现将玩家变身为自定义模型,并添加自定义行走动画:

python
import mod.client.extraClientApi as clientApi

ClientSystem = clientApi.GetClientSystemCls()

class TransformClientSystem(ClientSystem):

    def __init__(self, namespace, systemName):
        ClientSystem.__init__(self, namespace, systemName)
        self.mTransformedPlayers = {}

    def TransformPlayer(self, playerId):
        """将玩家变身为自定义角色"""
        comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)

        # 1. 修改几何体
        comp.AddPlayerGeometry("default", "geometry.custom_character")

        # 2. 修改贴图
        comp.AddPlayerTexture("default", "textures/entity/custom_character")

        # 3. 修改材质
        comp.AddPlayerRenderMaterial("default", "entity_alphatest")

        # 4. 添加自定义动画
        comp.AddPlayerAnimation("idle", "animation.custom_character.idle")
        comp.AddPlayerAnimation("move.arms", "animation.custom_character.walk_arms")
        comp.AddPlayerAnimation("move.legs", "animation.custom_character.walk_legs")
        comp.AddPlayerAnimation("attack.rotations", "animation.custom_character.attack")

        # 5. 注册自定义 query 变量用于控制特殊动画
        queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(playerId)
        queryComp.Register("query.mod.is_transformed", 0.0)
        queryComp.Set("query.mod.is_transformed", 1.0)

        # 6. 添加动画控制器
        comp.AddPlayerAnimationController(
            "custom_root",
            "controller.animation.custom_character.root"
        )
        comp.AddPlayerScriptAnimate(
            "custom_root",
            "query.mod.is_transformed > 0"
        )

        # 7. 重建渲染(只需调用一次)
        comp.RebuildPlayerRender()

        self.mTransformedPlayers[playerId] = True
        print "玩家 %s 已变身" % playerId

    def ResetPlayer(self, playerId):
        """将玩家恢复为默认外观"""
        comp = clientApi.GetEngineCompFactory().CreateActorRender(playerId)

        # 恢复默认几何体和贴图
        comp.AddPlayerGeometry("default", "geometry.humanoid.custom")
        comp.AddPlayerTexture("default", "textures/entity/steve")
        comp.AddPlayerRenderMaterial("default", "entity_alphatest")

        # 恢复默认动画
        comp.AddPlayerAnimation("idle", "animation.player.idle")
        comp.AddPlayerAnimation("move.arms", "animation.player.move.arms")
        comp.AddPlayerAnimation("move.legs", "animation.player.move.legs")
        comp.AddPlayerAnimation(
            "attack.rotations",
            "animation.player.attack.rotations"
        )

        # 重置 query 变量
        queryComp = clientApi.GetEngineCompFactory().CreateQueryVariable(playerId)
        queryComp.Set("query.mod.is_transformed", 0.0)

        comp.RebuildPlayerRender()

        self.mTransformedPlayers.pop(playerId, None)
        print "玩家 %s 已恢复默认外观" % playerId

    def ToggleTransform(self, playerId):
        """切换变身状态"""
        if playerId in self.mTransformedPlayers:
            self.ResetPlayer(playerId)
        else:
            self.TransformPlayer(playerId)

配合使用的动画控制器 JSON(resource_pack/animation_controllers/custom_character.ac.json):

json
{
  "format_version": "1.10.0",
  "animation_controllers": {
    "controller.animation.custom_character.root": {
      "initial_state": "idle",
      "states": {
        "idle": {
          "animations": [
            "idle"
          ],
          "transitions": [
            { "walking": "query.modified_move_speed > 0.1" }
          ]
        },
        "walking": {
          "animations": [
            "move.arms",
            "move.legs"
          ],
          "transitions": [
            { "idle": "query.modified_move_speed < 0.1" }
          ]
        }
      }
    }
  }
}

带宽优化 ​

在 2.2 及之后的版本中,引擎对自定义生物的移动流量进行了优化——距离玩家较远的生物会降低同步频率,表现上可能出现瞬移的情况。

如果你的自定义实体需要频繁的精确位置同步(如自定义坐骑、投射物、Boss 等),可以在 components 中手动添加以下组件来关闭该优化:

json
{
  "minecraft:entity": {
    "components": {
      "netease:ban_bandwidth_optimization": {}
    }
  }
}

该组件不需要任何参数,添加即生效。适用场景包括:

  • 自定义坐骑/载具(需要精确的骑乘位置同步)
  • 远距离活动的 Boss 实体
  • 需要平滑移动表现的自定义投射物
  • 任何对位置同步精度有较高要求的实体

常见问题 ​

覆盖原版生物不生效 ​

问题:在 behavior_pack/entities/ 下创建了与原版相同 identifier 的 JSON,但游戏中生物行为没有变化。

排查步骤:

  1. 确认 identifier 完全一致(包括 minecraft: 前缀)
  2. 确认行为包已正确应用到世界中
  3. 检查 JSON 格式是否有语法错误(多余的逗号、缺少引号等)
  4. 确认 format_version 使用正确版本(建议 1.16.0)
  5. 如果只修改了 component_groups 但没有对应的 events 去触发,组件组不会自动生效

玩家外观修改不生效 ​

问题:调用了 AddPlayerGeometry 等接口后,玩家外观没有变化。

排查步骤:

  1. 是否调用了 RebuildPlayerRender()?这是最常见的遗漏
  2. 确认接口是在客户端调用的(所有 ActorRender 接口都是客户端接口)
  3. 确认几何体和贴图资源路径正确且已包含在资源包中
  4. AddPlayerAnimation 的 animationName 参数不能包含大写字母,否则动画注册会失败
  5. 同时修改几何体时,必须一并修改贴图和动画以确保匹配

骑乘者位置不正确 ​

问题:骑乘者的显示位置与预期不符。

排查步骤:

  1. 检查 seats 中的 position 是否正确——坐标是相对于实体原点的偏移 [x, y, z]
  2. 确认 min_rider_count 和 max_rider_count 设置合理,座位是否真正被启用
  3. 如果使用了 SetEntitySeat 修改座位位置,确认修改的是通过 AddEntitySeat 添加的座位
  4. 使用 GetRiders() 检查骑乘者实际的 seatIndex 和 riderIndex

动画控制器冲突 ​

问题:通过 AddPlayerAnimationController 添加自定义控制器后,原版动画异常。

排查步骤:

  1. 检查是否使用了与原版相同的控制器 key(如 root),这会覆盖原版控制器
  2. 如果只需要在原版控制器某个状态中添加动画,使用 AddPlayerAnimationIntoState 而非替换整个控制器
  3. 检查 MoLang 条件表达式是否正确,确保不会与其他动画产生冲突
  4. 确认自定义的 query.mod.* 变量已通过 Register 注册

坐骑锁定后行为异常 ​

问题:调用 SetEntityLockRider(True) 后,骑乘行为与预期不符。

排查步骤:

  1. 锁定后,新骑乘者会从队首寻找空位——确保有足够的空位
  2. 重载存档后,空位仍会被删除——这是已知行为
  3. 使用 SetRiderRideEntity 指定 riderIndex 时,该位置必须为空
  4. ChangeRiderSeat 只能对已在骑乘状态的实体使用

下一步 ​