生物实战案例
如果你已经读完自定义基础生物和进阶生物开发,最好的下一步就是照着完整案例做一遍。本页给你 3 个偏实战的生物方案:巡逻幽灵、变体蘑菇兽、可骑乘运输兽。
阅读方式
建议你先完整跑通 1 个案例,再把其中的 identifier、贴图、掉落表和脚本逻辑替换成自己的玩法需求。下面所有示例都兼容中国版开发习惯:服务端使用 Python 2,动画扩展使用 query.mod.*,运行时状态同步优先使用 CreateModAttr。
案例一:巡逻幽灵
这个案例适合学习 3 件事:
- 用
minecraft:behavior.python_custom做固定路线巡逻 - 用
CreateModAttr同步警戒状态 - 用
query.mod.*在客户端做半透明渐隐和警戒外观
效果目标
- 平时沿 4 个巡逻点来回移动
- 玩家靠近时外观从淡化切到清晰
- 进入战斗后切换为警戒贴图
BP:实体定义
BP/entities/patrol_ghost.json
{
"format_version": "1.10.0",
"minecraft:entity": {
"description": {
"identifier": "mymod:patrol_ghost",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"components": {
"minecraft:type_family": {
"family": ["ghost", "monster", "mob"]
},
"minecraft:collision_box": {
"width": 0.8,
"height": 1.9
},
"minecraft:health": {
"value": 24,
"max": 24
},
"minecraft:movement": {
"value": 0.28
},
"minecraft:movement.basic": {},
"minecraft:navigation.walk": {
"can_walk": true,
"avoid_water": true,
"avoid_damage_blocks": true
},
"minecraft:physics": {},
"minecraft:pushable": {
"is_pushable": false,
"is_pushable_by_piston": false
},
"minecraft:follow_range": {
"value": 20,
"max": 32
},
"minecraft:attack": {
"damage": 5
},
"minecraft:behavior.python_custom:ghost_patrol": {
"priority": 2,
"control_flags": ["move", "look"]
},
"minecraft:behavior.nearest_attackable_target": {
"priority": 1,
"within_radius": 16,
"entity_types": [
{
"filters": {
"test": "is_family",
"subject": "other",
"value": "player"
},
"max_dist": 16
}
]
},
"minecraft:behavior.melee_attack": {
"priority": 0,
"speed_multiplier": 1.15,
"track_target": true
},
"minecraft:behavior.hurt_by_target": {
"priority": 1
},
"minecraft:loot": {
"table": "loot_tables/entities/patrol_ghost.json"
}
}
}
}RP:客户端实体
RP/entity/patrol_ghost.entity.json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "mymod:patrol_ghost",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"fade_far": "textures/entity/patrol_ghost_fade_far",
"fade_mid": "textures/entity/patrol_ghost_fade_mid",
"normal": "textures/entity/patrol_ghost",
"alert": "textures/entity/patrol_ghost_alert"
},
"geometry": {
"default": "geometry.patrol_ghost"
},
"animations": {
"idle": "animation.patrol_ghost.idle",
"walk": "animation.patrol_ghost.walk",
"alert": "animation.patrol_ghost.alert",
"controller": "controller.animation.patrol_ghost"
},
"scripts": {
"animate": ["controller"]
},
"render_controllers": [
"controller.render.patrol_ghost"
],
"spawn_egg": {
"texture": "patrol_ghost_spawn_egg",
"texture_index": 0
}
}
}
}RP/animation_controllers/patrol_ghost.ac.json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.patrol_ghost": {
"initial_state": "default",
"states": {
"default": {
"animations": [
"idle",
{
"walk": "query.modified_move_speed > 0.01"
}
],
"transitions": [
{
"alert": "query.mod.alert > 0"
}
]
},
"alert": {
"animations": [
"alert",
{
"walk": "query.modified_move_speed > 0.01"
}
],
"transitions": [
{
"default": "query.mod.alert <= 0"
}
]
}
}
}
}
}RP/render_controllers/patrol_ghost.rc.json
{
"format_version": "1.10.0",
"render_controllers": {
"controller.render.patrol_ghost": {
"arrays": {
"textures": {
"Array.fade": [
"Texture.fade_far",
"Texture.fade_mid",
"Texture.normal"
]
}
},
"geometry": "Geometry.default",
"materials": [
{
"*": "Material.default"
}
],
"textures": [
"query.mod.alert > 0 ? Texture.alert : Array.fade[query.mod.fade_level]"
]
}
}
}Python:服务端巡逻与警戒状态
behavior_pack/scripts/server/ghost_system.py
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
CompFactory = serverApi.GetEngineCompFactory()
class GhostPatrolGoal(serverApi.GetCustomGoalCls()):
def __init__(self, entityId, argsJson):
super(GhostPatrolGoal, self).__init__(entityId, argsJson)
self.entityId = entityId
self.points = [
(100, 64, 100),
(108, 64, 100),
(108, 64, 108),
(100, 64, 108)
]
self.index = 0
def CanUse(self):
actionComp = CompFactory.CreateAction(self.entityId)
return not actionComp.GetAttackTarget()
def CanContinueToUse(self):
return self.CanUse()
def Start(self):
self.MoveNext()
def Tick(self):
posComp = CompFactory.CreatePos(self.entityId)
pos = posComp.GetFootPos()
target = self.points[self.index]
dx = pos[0] - target[0]
dz = pos[2] - target[2]
if dx * dx + dz * dz <= 1.5:
self.index = (self.index + 1) % len(self.points)
self.MoveNext()
def MoveNext(self):
moveComp = CompFactory.CreateMoveTo(self.entityId)
moveComp.SetMoveSetting(self.points[self.index], 1.0)
class GhostServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
serverApi.RegisterCustomGoal('ghost_patrol', GhostPatrolGoal)
serverApi.AddEntityTickEventWhiteList('mymod:patrol_ghost')
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
'EntityTickServerEvent',
self,
self.OnGhostTick
)
def OnGhostTick(self, args):
entityId = args.get('entityId')
if not entityId:
return
actionComp = CompFactory.CreateAction(entityId)
targetId = actionComp.GetAttackTarget()
modAttrComp = CompFactory.CreateModAttr(entityId)
modAttrComp.SetAttr('ghost_alert', 1 if targetId else 0)Python:客户端渐隐控制
resource_pack/scripts/client/ghost_client_system.py
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
ClientSystem = clientApi.GetClientSystemCls()
CompFactory = clientApi.GetEngineCompFactory()
class GhostClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ghosts = set()
self.ListenForEvent(
clientApi.GetEngineNamespace(),
clientApi.GetEngineSystemName(),
'AddEntityClientEvent',
self,
self.OnAddEntity
)
self.ListenForEvent(
clientApi.GetEngineNamespace(),
clientApi.GetEngineSystemName(),
'OnScriptTickClient',
self,
self.OnScriptTickClient
)
def OnAddEntity(self, args):
entityId = args.get('id')
engineType = args.get('engineTypeStr')
if engineType != 'mymod:patrol_ghost':
return
self.ghosts.add(entityId)
queryComp = CompFactory.CreateQueryVariable(entityId)
queryComp.Register('query.mod.fade_level', 2.0)
queryComp.Register('query.mod.alert', 0.0)
modAttrComp = CompFactory.CreateModAttr(entityId)
modAttrComp.RegisterUpdateFunc('ghost_alert', self.OnGhostAlertChanged)
def OnGhostAlertChanged(self, args):
entityId = args.get('entityId')
newValue = args.get('newValue', 0)
queryComp = CompFactory.CreateQueryVariable(entityId)
queryComp.Set('query.mod.alert', float(newValue))
def OnScriptTickClient(self):
playerId = clientApi.GetLocalPlayerId()
playerPosComp = CompFactory.CreatePos(playerId)
playerPos = playerPosComp.GetPos()
for entityId in list(self.ghosts):
posComp = CompFactory.CreatePos(entityId)
ghostPos = posComp.GetPos()
if not ghostPos:
continue
dx = ghostPos[0] - playerPos[0]
dy = ghostPos[1] - playerPos[1]
dz = ghostPos[2] - playerPos[2]
distanceSq = dx * dx + dy * dy + dz * dz
if distanceSq > 196:
fadeLevel = 0.0
elif distanceSq > 64:
fadeLevel = 1.0
else:
fadeLevel = 2.0
queryComp = CompFactory.CreateQueryVariable(entityId)
queryComp.Set('query.mod.fade_level', fadeLevel)测试步骤
- 使用
/summon mymod:patrol_ghost召唤幽灵。 - 观察幽灵是否按 4 个点循环巡逻;如果原地不动,优先检查路径点是否落在可寻路地面上。
- 从远处靠近,确认贴图会从淡化版本切到清晰版本。
- 进入仇恨范围后,确认
ghost_alert被同步,客户端切到警戒贴图。
常见问题
如果你已经设置了 query.mod.fade_level,但外观没有变化,通常是客户端没有对该实体执行 Register('query.mod.xxx')。query.mod.* 必须先注册,再调用 Set。
案例二:变体蘑菇兽
这个案例重点演示数据驱动变体:
- 生成时随机红/棕两种体色
- 进水后切换为潮湿状态
- 潮湿状态叠加孢子覆盖层,并通过脚本播放粒子
效果目标
- 同一个实体只维护一套行为逻辑
query.variant控制主体体色query.mark_variant控制额外覆盖层
BP:实体定义
BP/entities/variant_moobloom.json
{
"format_version": "1.10.0",
"minecraft:entity": {
"description": {
"identifier": "mymod:variant_moobloom",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"component_groups": {
"mymod:red": {
"minecraft:variant": {
"value": 0
}
},
"mymod:brown": {
"minecraft:variant": {
"value": 1
}
},
"mymod:dry": {
"minecraft:mark_variant": {
"value": 0
}
},
"mymod:wet": {
"minecraft:mark_variant": {
"value": 1
},
"minecraft:movement": {
"value": 0.18
}
}
},
"components": {
"minecraft:type_family": {
"family": ["moobloom", "animal", "mob"]
},
"minecraft:health": {
"value": 20,
"max": 20
},
"minecraft:collision_box": {
"width": 0.9,
"height": 1.4
},
"minecraft:movement": {
"value": 0.22
},
"minecraft:movement.basic": {},
"minecraft:physics": {},
"minecraft:navigation.walk": {
"can_walk": true,
"avoid_water": false,
"avoid_damage_blocks": true
},
"minecraft:behavior.random_stroll": {
"priority": 5,
"speed_multiplier": 0.8
},
"minecraft:behavior.look_at_player": {
"priority": 6,
"look_distance": 8
},
"minecraft:loot": {
"table": "loot_tables/entities/variant_moobloom.json"
}
},
"events": {
"minecraft:entity_spawned": {
"sequence": [
{
"randomize": [
{
"weight": 1,
"add": {
"component_groups": ["mymod:red"]
}
},
{
"weight": 1,
"add": {
"component_groups": ["mymod:brown"]
}
}
]
},
{
"add": {
"component_groups": ["mymod:dry"]
}
}
]
}
}
}
}RP:客户端实体与渲染控制器
RP/entity/variant_moobloom.entity.json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "mymod:variant_moobloom",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"red": "textures/entity/variant_moobloom_red",
"brown": "textures/entity/variant_moobloom_brown",
"dry_overlay": "textures/entity/variant_moobloom_overlay_dry",
"wet_overlay": "textures/entity/variant_moobloom_overlay_wet"
},
"geometry": {
"default": "geometry.variant_moobloom"
},
"render_controllers": [
"controller.render.variant_moobloom"
]
}
}
}RP/render_controllers/variant_moobloom.rc.json
{
"format_version": "1.10.0",
"render_controllers": {
"controller.render.variant_moobloom": {
"arrays": {
"textures": {
"Array.body": [
"Texture.red",
"Texture.brown"
],
"Array.overlay": [
"Texture.dry_overlay",
"Texture.wet_overlay"
]
}
},
"geometry": "Geometry.default",
"materials": [
{
"*": "Material.default"
}
],
"textures": [
"Array.body[query.variant]",
"Array.overlay[query.mark_variant]"
]
}
}
}Python:检测进水并切换潮湿状态
behavior_pack/scripts/server/moobloom_system.py
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
CompFactory = serverApi.GetEngineCompFactory()
class MoobloomServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
serverApi.AddEntityTickEventWhiteList('mymod:variant_moobloom')
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
'EntityTickServerEvent',
self,
self.OnEntityTick
)
def OnEntityTick(self, args):
entityId = args.get('entityId')
if not entityId:
return
queryComp = CompFactory.CreateQueryVariable(entityId)
inWaterResult = queryComp.EvalMolangExpression('query.in_water')
inWater = bool(inWaterResult and inWaterResult.get('value', 0))
eventComp = CompFactory.CreateEntityEvent(entityId)
modAttrComp = CompFactory.CreateModAttr(entityId)
if inWater:
eventComp.RemoveActorComponentGroup('mymod:dry')
eventComp.AddActorComponentGroup('mymod:wet')
modAttrComp.SetAttr('spore_state', 1)
self.EmitSporeParticle(entityId)
else:
eventComp.RemoveActorComponentGroup('mymod:wet')
eventComp.AddActorComponentGroup('mymod:dry')
modAttrComp.SetAttr('spore_state', 0)
def EmitSporeParticle(self, entityId):
posComp = CompFactory.CreatePos(entityId)
pos = posComp.GetFootPos()
if not pos:
return
cmdComp = CompFactory.CreateGame(serverApi.GetLevelId())
cmdComp.SetCommand(
'/particle mymod:moobloom_spore %.1f %.1f %.1f' % (
pos[0], pos[1] + 1.0, pos[2]
)
)测试步骤
- 连续召唤多个
mymod:variant_moobloom,确认会随机出现红/棕两种体色。 - 把它推入水中,确认覆盖层切到
wet_overlay。 - 观察日志或粒子效果,确认进水时执行了
/particle mymod:moobloom_spore。 - 离水后确认
query.mark_variant对应的覆盖层恢复为干燥版本。
设计建议
这个模式很适合做季节、地形、天气、成长阶段等外观切换。主体差异用 variant,附加状态用 mark_variant,通常最省文件数。
案例三:可骑乘运输兽
这个案例适合做开放世界坐骑、商队单位或多人载具:
- 1 个驾驶位 + 1 个乘客位
- 运行时锁定骑乘者队列,避免下车后座位错位
- 用
ExtraData保存货物数量 - 用
ModAttr同步耐力值
效果目标
- 玩家可以骑乘并控制运输兽
- 装货后切换为载货外观
- 冲刺或持续移动时消耗耐力,休息时恢复
- 耐力过低时进入疲劳状态并减速
BP:实体定义
BP/entities/pack_beast.json
{
"format_version": "1.10.0",
"minecraft:entity": {
"description": {
"identifier": "mymod:pack_beast",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"component_groups": {
"mymod:normal": {
"minecraft:movement": {
"value": 0.24
}
},
"mymod:tired": {
"minecraft:movement": {
"value": 0.14
}
},
"mymod:empty_pack": {
"minecraft:mark_variant": {
"value": 0
}
},
"mymod:loaded_pack": {
"minecraft:mark_variant": {
"value": 1
}
}
},
"components": {
"minecraft:type_family": {
"family": ["pack_beast", "mount", "mob"]
},
"minecraft:health": {
"value": 36,
"max": 36
},
"minecraft:collision_box": {
"width": 1.2,
"height": 1.8
},
"minecraft:physics": {},
"minecraft:movement.basic": {},
"minecraft:navigation.walk": {
"can_walk": true,
"avoid_water": true,
"avoid_damage_blocks": true
},
"minecraft:rideable": {
"seat_count": 2,
"family_types": ["player"],
"controlling_seat": 0,
"crouching_skip_interact": true,
"interact_text": "action.interact.mount",
"rider_can_interact": false,
"seats": [
{
"position": [0.0, 1.6, 0.2],
"lock_rider_rotation": 90
},
{
"position": [0.0, 1.6, -0.8],
"lock_rider_rotation": 90
}
]
},
"minecraft:behavior.random_stroll": {
"priority": 5,
"speed_multiplier": 0.8
},
"minecraft:behavior.look_at_player": {
"priority": 6,
"look_distance": 8
}
},
"events": {
"minecraft:entity_spawned": {
"add": {
"component_groups": ["mymod:normal", "mymod:empty_pack"]
}
}
}
}
}RP:客户端实体与载货外观
RP/entity/pack_beast.entity.json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "mymod:pack_beast",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"base": "textures/entity/pack_beast",
"loaded": "textures/entity/pack_beast_loaded"
},
"geometry": {
"default": "geometry.pack_beast"
},
"render_controllers": [
"controller.render.pack_beast"
]
}
}
}RP/render_controllers/pack_beast.rc.json
{
"format_version": "1.10.0",
"render_controllers": {
"controller.render.pack_beast": {
"arrays": {
"textures": {
"Array.pack": [
"Texture.base",
"Texture.loaded"
]
}
},
"geometry": "Geometry.default",
"materials": [
{
"*": "Material.default"
}
],
"textures": [
"Array.pack[query.mark_variant]"
]
}
}
}Python:骑乘、货物和耐力系统
behavior_pack/scripts/server/pack_beast_system.py
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
ServerSystem = serverApi.GetServerSystemCls()
CompFactory = serverApi.GetEngineCompFactory()
class PackBeastServerSystem(ServerSystem):
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
serverApi.AddEntityTickEventWhiteList('mymod:pack_beast')
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
'AddEntityServerEvent',
self,
self.OnAddEntity
)
self.ListenForEvent(
serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
'EntityTickServerEvent',
self,
self.OnEntityTick
)
def OnAddEntity(self, args):
entityId = args.get('id')
engineType = args.get('engineTypeStr')
if engineType != 'mymod:pack_beast':
return
rideComp = CompFactory.CreateRide(entityId)
rideComp.SetEntityLockRider(True)
exDataComp = CompFactory.CreateExtraData(entityId)
exDataComp.SetExtraData('cargo_count', 0, False)
exDataComp.SetExtraData('stamina', 100, False)
exDataComp.SaveExtraData()
modAttrComp = CompFactory.CreateModAttr(entityId)
modAttrComp.SetAttr('pack_stamina', 100)
def OnEntityTick(self, args):
entityId = args.get('entityId')
if not entityId:
return
exDataComp = CompFactory.CreateExtraData(entityId)
stamina = exDataComp.GetExtraData('stamina')
if stamina is None:
stamina = 100
rideComp = CompFactory.CreateRide(entityId)
riders = rideComp.GetRiders() or []
if riders:
stamina = max(0, stamina - 1)
else:
stamina = min(100, stamina + 2)
exDataComp.SetExtraData('stamina', stamina)
eventComp = CompFactory.CreateEntityEvent(entityId)
if stamina <= 20:
eventComp.RemoveActorComponentGroup('mymod:normal')
eventComp.AddActorComponentGroup('mymod:tired')
else:
eventComp.RemoveActorComponentGroup('mymod:tired')
eventComp.AddActorComponentGroup('mymod:normal')
modAttrComp = CompFactory.CreateModAttr(entityId)
modAttrComp.SetAttr('pack_stamina', stamina)
def LoadCargo(self, entityId, amount):
exDataComp = CompFactory.CreateExtraData(entityId)
cargoCount = exDataComp.GetExtraData('cargo_count') or 0
cargoCount += amount
exDataComp.SetExtraData('cargo_count', cargoCount)
eventComp = CompFactory.CreateEntityEvent(entityId)
eventComp.RemoveActorComponentGroup('mymod:empty_pack')
eventComp.AddActorComponentGroup('mymod:loaded_pack')
def UnloadCargo(self, entityId):
exDataComp = CompFactory.CreateExtraData(entityId)
exDataComp.SetExtraData('cargo_count', 0)
eventComp = CompFactory.CreateEntityEvent(entityId)
eventComp.RemoveActorComponentGroup('mymod:loaded_pack')
eventComp.AddActorComponentGroup('mymod:empty_pack')
def MountAsDriver(self, playerId, entityId):
rideComp = CompFactory.CreateRide(entityId)
rideComp.SetRiderRideEntity(playerId, entityId, 0)
def MountAsPassenger(self, playerId, entityId):
rideComp = CompFactory.CreateRide(entityId)
rideComp.SetRiderRideEntity(playerId, entityId, 1)测试步骤
- 召唤
mymod:pack_beast,确认交互按钮可骑乘。 - 两名玩家依次上骑,确认第一位在驾驶位,第二位在乘客位。
- 让脚本调用
LoadCargo(entityId, 8),确认贴图切到载货版。 - 持续骑乘 5 到 10 秒,确认
pack_stamina持续下降;低于 20 后移动速度应明显变慢。 - 下骑等待耐力恢复,再确认实体重新回到正常速度。
数据持久化
货物数量和耐力示例使用 ExtraData 保存,适合做存档持久化;如果你只关心实时同步到客户端,而不需要落盘,优先用 CreateModAttr 会更轻量。
你可以怎么改
- 把巡逻幽灵的路径点改成从配置文件或预设零件读取,做成地牢守卫
- 把蘑菇兽的潮湿状态改成昼夜、群系或方块接触状态,扩展更多变体
- 给运输兽补一个 UI 面板,把
cargo_count和pack_stamina直接显示在界面上
