Plugins — 系统插件参考

RoninNetease 内置了系统插件($vendor.*),通过 conf.py 中的 PLUGINS 列表启用。本文档基于实际示例说明动画和输入插件的用法。


1. 事件系统插件 — $vendor.event

1.1 概述

封装了事件分发和管理。启用后自动注册客户端/服务端的 EventReader 单例组件。

1.2 启用

PLUGINS = [
    '$vendor.event',
]

1.3 EventReader 组件

EventReader 是一个单例组件,用于在 @Sched.Event 中获取当前事件数据。正确的用法是@Query 的位置参数中传入 EventReader,框架会自动注入

from architect.query import Query
from architect.plugins.event.client import EventReader
from architect.core import Sched

class MySystem(ClientSubsystem):
    @Sched.Event('EntityHurtEvent')
    @Query(EventReader)
    def onEntityHurt(self, reader):
        # reader 是 EventReader 实例,由 @Query 自动注入
        event = reader.event()
        entityId = event.id
        damage = event.damage
        print('Entity %s took %s damage' % (entityId, damage))

1.4 EventReader 属性与方法

event = reader.event()  # 获取当前 ChainedEvent

# ChainedEvent 的常用属性与方法
event.id           # 实体 ID
event.damage       # 伤害值
event.srcId        # 来源实体 ID
event.cause        # 伤害原因
event.stop()       # 停止事件传递
event.prevent()    # 阻止默认行为
event.setEvent(k, v)  # 修改事件字段
event.dict()       # 获取原始 dict

1.5 服务端用法

from architect.plugins.event.server import EventReader

class MyServerSystem(ServerSubsystem):
    @Sched.Event('EntityHurtEvent')
    @Query(EventReader)
    def onEntityHurt(self, reader):
        event = reader.event()
        if event.damage > 50:
            event.setEvent('damage', 10)

2. 动画扩展插件 — $vendor.animation

2.1 启用

PLUGINS = ['$vendor.animation']

2.2 动画映射定义

动画系统使用短键 → 完整动画路径的映射模式。在配置模块中定义一个字典:

# assets/animations.py
GenericMapping = {
    'idle':    'animation.standard_steve.idle',
    'walk':    'animation.standard_steve.walk',
    'diamond': 'animation.standard_steve.diamond',
    'stop':    'animation.standard_steve.stop',
    'attack':  'animation.standard_steve.attack.diamond',
}

2.3 动画元数据

# assets/animMeta.py
AnimMeta = {
    "animation.standard_steve.idle": {
        "loop": True,
        "length": 2
    },
    "animation.standard_steve.walk": {
        "loop": True,
        "length": 1
    },
    "animation.standard_steve.attack.diamond": {
        "loop": "hold_on_last_frame",
        "length": 0.4167,
        "notifies": {
            "0.25": [{"name": "attack", "state": 1}],
            "0.375": [{"name": "stun", "state": 0}],
        }
    },
}

2.4 注册和播放

在子系统中注册元数据、动画映射、配置缓动并播放动画。播放时使用映射字典的短键和槽位名

from architect.plugins.animation.components.animClient import (
    AnimationExComponent, AnimationEasingTypes, AnimationEasingConf
)
from architect.component import getOrCreateComponent, getOneComponent
from architect.core import localPlayerId
from .assets.animations import GenericMapping
from .assets.animMeta import AnimMeta

class MyAnimSystem(ClientSubsystem):
    def onPlayerCreated(self):
        entityId = localPlayerId()
        animEx = getOrCreateComponent(entityId, AnimationExComponent)

        # 必须先注册元数据,再注册动画映射
        animEx.registerMetadatas(AnimMeta)
        animEx.registerAnimations(GenericMapping)
        animEx.updateActorAnimDef()

        # 为每个动画键配置缓动(缓入/缓出)
        for animKey in GenericMapping.keys():
            animEx.registerEasing(
                animKey,
                AnimationEasingConf(1, 0.15, AnimationEasingTypes.SINE),
            )

    def start_walking(self):
        animEx = getOneComponent(localPlayerId(), AnimationExComponent)
        animEx.play('walk', 'loco')    # 键='walk', 槽='loco'

    def stop_walking(self):
        animEx = getOneComponent(localPlayerId(), AnimationExComponent)
        animEx.stop('walk', 'loco')

    def play_attack(self):
        animEx = getOneComponent(localPlayerId(), AnimationExComponent)
        animEx.play('attack', 'holding')  # 键='attack', 槽='holding'

重要: registerMetadatas() 必须在 registerAnimations() 之前调用,否则动画元数据不存在会导致注册失败。元数据由 editor/tools/animExtractor 工具从动画 JSON 文件中提取生成 animMeta.py

2.5 @Dispatch 动画事件分发

@Dispatch 接收完整的动画路径作为参数,处理类建议继承 BaseActionDispatcher。通知回调命名规则为 notify{Name}{Start|End}

from architect.plugins.animation.utils import BaseActionDispatcher, Dispatch
from architect.plugins.animation.components.animClient import AnimationExComponent

@Dispatch('animation.standard_steve.attack.diamond')  # 完整动画路径
class DiamondAttackDispatcher(BaseActionDispatcher):
    def onEnded(self, entityId, animComp):
        # 动画结束(完成或中断)时调用
        animComp.play('diamond', 'holding')  # 回到持剑动画

    def notifyAttackStart(self, entityId, animEx):
        # 对应 AnimMeta 中的 notify 'attack' state=1
        pass

    def notifyStunStart(self, entityId, animEx):
        # 对应 notify 'stun' state=1
        self.movement(entityId, False)  # 禁止移动

    def notifyStunEnd(self, entityId, animEx):
        # 对应 notify 'stun' state=0
        self.movement(entityId, True)

    def onInterrupted(self, entityId, animEx):
        self.movement(entityId, True)

    def onFinish(self, entityId, animEx):
        self.movement(entityId, True)

AnimationEventDispatcher 事件回调:

回调 签名 触发时机
onFinish (self, entityId, animComp) 动画正常结束
onInterrupted (self, entityId, animComp) 动画被中断
onEnded (self, entityId, animComp) 动画结束(完成或中断)
notify{Name}Start (self, entityId, animEx) 通知状态变为 1
notify{Name}End (self, entityId, animEx) 通知状态变为 0

BaseActionDispatcher 提供的基础方法:

方法 说明
movement(entityId, canMove) 控制实体移动/跳跃
cam(entityId, lock) 控制摄像机拖拽

2.6 AnimationExComponent 核心方法

方法 说明
registerMetadatas(metadata) 注册动画元数据 dict(必须在 registerAnimations 之前调用)
registerAnimations(mapping) 注册动画映射 dict
updateActorAnimDef() 应用动画定义到实体
registerEasing(key, easeIn) 为动画键注册缓动配置
play(key, slot) 在指定槽位播放动画(key 为映射短键)
stop(key, slot) 停止槽位中的动画
stopAll() 停止所有动画
setPlaybackRate(key, rate) 设置动画播放速率
getPlayingAnimation(key) 获取当前播放的动画对象(含 .playRate 属性)

2.7 物品切换动画示例

根据手持物品切换动画:

ItemHoldingAnimMapping = {
    'minecraft:diamond_sword': 'diamond',
    'minecraft:wooden_sword': 'wood',
    'minecraft:iron_sword':   'iron',
}

class AnimClient(ClientSubsystem):
    @EventListener()
    def onPlayerSwap(self, ev=events.OnCarriedNewItemChangedClientEvent()):
        itemName = ev.itemDict['newItemName']
        animKey = ItemHoldingAnimMapping.get(itemName, 'idle')
        animEx = getOneComponent(localPlayerId(), AnimationExComponent)
        if animEx:
            animEx.play(animKey, 'holding')  # 在 holding 槽播放

槽位用于分层动画。同一实体可同时在 'loco' 槽(下半身)和 'holding' 槽(上半身)播放不同动画。

2.8 缓动类型

from architect.plugins.animation.components.animClient import AnimationEasingTypes

# LINEAR, QUAD, CUBIC, QUART, QUINT, SINE, EXPO

2.9 clientOnly 动画与跨端同步

AnimationExComponent 在本地玩家 AddPlayerCreatedClientEvent 事件触发时无法修改本地玩家动画,但可以给其他玩家进入渲染时播放动画。如果需要在所有玩家加载完成时都播放动画,需同时在 AddPlayerCreatedClientEventOnLocalPlayerStopLoading 两个事件中播放动画。

这一限制对 clientOnly 动画影响最大:跨端同步动画即使在客户端播放失败,也会在广播到自己时重新尝试播放一次。

class MyAnimSystem(ClientSubsystem):
    @EventListener('AddPlayerCreatedClientEvent')
    def onPlayerCreated(self, ev):
        # 对其他进入渲染的玩家有效,但此时无法修改本地玩家动画
        animEx = getOrCreateComponent(ev.entityId, AnimationExComponent)
        animEx.registerMetadatas(AnimMeta)
        animEx.registerAnimations(GenericMapping)
        animEx.updateActorAnimDef()
        animEx.play('idle', 'loco')

    @EventListener('OnLocalPlayerStopLoading')
    def onLocalPlayerStopLoading(self, ev):
        # 本地玩家加载完成,补充播放本地玩家动画
        animEx = getOneComponent(localPlayerId(), AnimationExComponent)
        if animEx:
            animEx.play('idle', 'loco')

2.10 实体 Molang 变量控制

动画参数通过动画 JSON 文件中的 Molang 表达式绑定,不在 Python 代码中设置。动画 JSON 定义示例:

"animation.standard_steve.idle": {
    "loop": true,
    "blend_weight": "v.blendex.standard_steve.idle",
    "anim_time_update": "v.anim_timeex.standard_steve.idle",
    "bones": { ... }
}

3. 输入系统插件 — $vendor.input

3.1 概述

输入系统基于 Unreal Enhanced Input 模式,核心概念:

3.2 启用

PLUGINS = ['$vendor.input']

3.3 步骤一:声明输入动作(assets/inputAction.py

模块级别的独立调用InputAction() 会注册到全局注册表:

from architect.plugins.input.utils.inputAction import InputAction
from architect.plugins.input.enum import ValueType
from architect.plugins.input.utils.modifier import DeadZone

# 二维轴向输入(WASD 移动)
InputAction('laravelMovement', ValueType.Vector2, modifiers=[
    DeadZone()  # 归一化处理(键盘出现 (1.0,1.0) 时长度归一化)
])

# 双击输入
InputAction('jump', ValueType.Double)
InputAction('startFly', ValueType.Double)
InputAction('stopFly', ValueType.Double)

# 长按输入
InputAction('spying', ValueType.Double)

# 飞行移动
InputAction('flyMove', ValueType.Vector2)

InputAction 构造函数: InputAction(actionName, valueType, modifiers=[])

参数 说明
actionName 动作名称,用作 @InputAction 装饰器的匹配键
valueType ValueType.Double / ValueType.Vector2 / ValueType.Vector3
modifiers 修饰符列表(DeadZone, Negate, SwizzleAxis 等)

3.4 步骤二:定义输入映射(assets/inputMapping.py

模块级别的独立调用InputMapping() 注册上下文并绑定物理输入到 InputAction:

from architect.plugins.input.utils.mappingContext import InputMapping, InputBinding
from architect.plugins.input.enum import KeyboardKey, InputType, AxisSwizzleOrder, GamepadAxis, GamepadKey, MouseKey
from architect.plugins.input.utils.modifier import SwizzleAxis, Negate
from architect.plugins.input.utils.trigger import TriggerDown, DoubleTap, TriggerHold

InputMapping('move', [
    # W → laravelMovement (yxz 轴交换 + 按下触发)
    InputBinding(InputType.Key, KeyboardKey.W, 'laravelMovement',
        modifiers=[SwizzleAxis(order=AxisSwizzleOrder.YXZ)],
        triggers=[TriggerDown()]),
    # S → laravelMovement (反向)
    InputBinding(InputType.Key, KeyboardKey.S, 'laravelMovement',
        modifiers=[SwizzleAxis(order=AxisSwizzleOrder.YXZ), Negate()],
        triggers=[TriggerDown()]),
    # D → laravelMovement (x 轴正向)
    InputBinding(InputType.Key, KeyboardKey.D, 'laravelMovement',
        triggers=[TriggerDown()]),
    # A → laravelMovement (x 轴反向)
    InputBinding(InputType.Key, KeyboardKey.A, 'laravelMovement',
        modifiers=[Negate()],
        triggers=[TriggerDown()]),
    # 手柄左摇杆 → laravelMovement
    InputBinding(InputType.Axis, GamepadAxis.LS, 'laravelMovement',
        triggers=[TriggerDown()]),

    # 空格 → jump
    InputBinding(InputType.Key, KeyboardKey.Space, 'jump',
        triggers=[TriggerDown()]),
    # 手柄 A → jump
    InputBinding(InputType.Gamepad, GamepadKey.A, 'jump',
        triggers=[TriggerDown()]),

    # 双击空格 → startFly
    InputBinding(InputType.Key, KeyboardKey.Space, 'startFly',
        triggers=[DoubleTap()]),
    # 双击手柄 A → startFly
    InputBinding(InputType.Gamepad, GamepadKey.A, 'startFly',
        triggers=[DoubleTap()]),
])

# 飞行模式映射
InputMapping('fly', [
    InputBinding(InputType.Key, KeyboardKey.W, 'flyMove',
        modifiers=[SwizzleAxis(order=AxisSwizzleOrder.YXZ)],
        triggers=[TriggerDown()]),
    # ... (同 move 的 WASD 绑定)
    InputBinding(InputType.Key, KeyboardKey.Space, 'stopFly',
        triggers=[DoubleTap()]),
])

# 瞄具模式映射
InputMapping('spying', [
    InputBinding(InputType.Key, MouseKey.Right, 'spying',
        triggers=[TriggerHold(1)]),   # 长按右键 1 秒
    InputBinding(InputType.Axis, GamepadAxis.LT, 'spying',
        triggers=[TriggerHold(1)]),    # 长按 LT 1 秒
])

InputBinding 构造函数: InputBinding(inputType, key, actionName, modifiers=[], triggers=[])

参数 说明
inputType InputType.Key / InputType.Gamepad / InputType.Axis
key KeyboardKey.* / GamepadKey.* / GamepadAxis.* / MouseKey.*
actionName 目标 InputAction 名称
modifiers 输入值修饰符
triggers 触发条件

3.5 步骤三:启用映射(子系统中)

from architect.plugins.input.components.inputEx import InputExComponent
from architect.component import getOneSingletonComponent

class MySystem(ClientSubsystem):
    @EventListener('OnLocalPlayerStopLoading')
    def onLocalPlayerStopLoading(self, ev):
        inputEx = getOneSingletonComponent(InputExComponent)
        # 启用映射上下文
        inputEx.enableMappings('move', 'spying')

        # 屏蔽原版移动
        operation = LevelClient.getInstance().operation
        operation.SetCanMove(False)
        operation.SetCanJump(False)

InputExComponent 映射控制:

方法 说明
enableMapping(name) 启用单个映射上下文
enableMappings(*names) 启用多个映射上下文
disableMapping(name) 禁用单个映射上下文
disableMappings(*names) 禁用多个映射上下文

3.6 步骤四:消费输入(@InputAction 装饰器)

from architect.plugins.input.client import InputAction
from architect.plugins.input.enum import InputState

@SubsystemClient
class TestClient(ClientSubsystem):
    @InputAction('laravelMovement')
    def iaMove(self, ev):
        # ev.value 是 Vector2 (x, y)
        x, y = ev.value
        # 将输入转换为世界方向移动...
        motion.SetMotion((mx, my, mz))

    @InputAction('jump')
    def iaJump(self, ev):
        # ev.value 是 Double (0.0 或 1.0)
        if attr.isEntityOnGround():
            motion.SetMotion((x, JUMP_POWER, z))

    @InputAction('startFly')
    def iaStartFly(self, ev):
        inputEx = getOneSingletonComponent(InputExComponent)
        inputEx.disableMapping('move')
        inputEx.enableMapping('fly')

    @InputAction('spying', InputState.Completed)
    def iaCompleteSpying(self, ev):
        # 长按释放时恢复
        LevelClient.getInstance().playerView.SetPlayerFovScale(1)

@InputAction 装饰器:

@InputAction('actionName')                     # 持续触发(每帧)
@InputAction('actionName', InputState.Started)  # 开始按下
@InputAction('actionName', InputState.Completed) # 释放

被装饰方法接收一个事件对象 ev,其属性:

3.7 修饰符(Modifiers)

InputActionInputBinding 中用于处理原始输入值:

修饰符 说明
DeadZone() 死区 + 归一化(避免对角线长度超过 1)
Negate() 取反(A/D 键实现轴向反向)
SwizzleAxis(order) 轴交换(AxisSwizzleOrder.YXZ 等)

AxisSwizzleOrder XYZ, XZY, YXZ, YZX, ZXY, ZYX

3.8 触发器(Triggers)

InputBinding 中定义激活条件:

触发器 说明
TriggerDown() 按下时触发
DoubleTap() 双击触发
TriggerHold(seconds) 长按指定秒数后触发

3.9 输入类型枚举

from architect.plugins.input.enum import (
    InputType,      # Key(1), Touch(2), Gamepad(3), Axis(4)
    ValueType,      # Double(1), Vector2(2), Vector3(3)
    KeyboardKey,    # A-Z, Space, Lshift, Return, 等
    MouseKey,       # Left(-99), Right(-98), Middle(-97)
    GamepadKey,     # A(1), B(2), X(3), Y(4), Up/Down/Left/Right, LS, RS, LB, RB
    GamepadAxis,    # LS(4096), RS(4097), LT(256), RT(257)
    InputState,     # Empty, Started, Triggered, Completed, Canceled, Ongoing
    AxisSwizzleOrder,
)

3.10 完整示例

# conf.py
MOD_CLIENT_MODULES = ['assets.inputAction', 'assets.inputMapping', 'testClient']
PLUGINS = ['$vendor.input']
# assets/inputAction.py
from architect.plugins.input.utils.inputAction import InputAction
from architect.plugins.input.enum import ValueType
from architect.plugins.input.utils.modifier import DeadZone

InputAction('laravelMovement', ValueType.Vector2, modifiers=[DeadZone()])
InputAction('jump', ValueType.Double)
InputAction('startFly', ValueType.Double)
InputAction('stopFly', ValueType.Double)
InputAction('spying', ValueType.Double)
InputAction('flyMove', ValueType.Vector2)
# assets/inputMapping.py
from architect.plugins.input.utils.mappingContext import InputMapping, InputBinding
from architect.plugins.input.enum import KeyboardKey, InputType, AxisSwizzleOrder
from architect.plugins.input.utils.modifier import SwizzleAxis, Negate
from architect.plugins.input.utils.trigger import TriggerDown, DoubleTap, TriggerHold

InputMapping('move', [
    InputBinding(InputType.Key, KeyboardKey.W, 'laravelMovement',
        modifiers=[SwizzleAxis(order=AxisSwizzleOrder.YXZ)], triggers=[TriggerDown()]),
    InputBinding(InputType.Key, KeyboardKey.S, 'laravelMovement',
        modifiers=[SwizzleAxis(order=AxisSwizzleOrder.YXZ), Negate()], triggers=[TriggerDown()]),
    InputBinding(InputType.Key, KeyboardKey.D, 'laravelMovement', triggers=[TriggerDown()]),
    InputBinding(InputType.Key, KeyboardKey.A, 'laravelMovement', modifiers=[Negate()], triggers=[TriggerDown()]),
    InputBinding(InputType.Key, KeyboardKey.Space, 'jump', triggers=[TriggerDown()]),
    InputBinding(InputType.Key, KeyboardKey.Space, 'startFly', triggers=[DoubleTap()]),
])
InputMapping('spying', [
    InputBinding(InputType.Key, KeyboardKey.Lshift, 'spying', triggers=[TriggerHold(1)]),
])
# testClient.py
from architect.compact import *
from architect.plugins.input.client import InputAction, InputExComponent
from architect.plugins.input.enum import InputState

@SubsystemClient
class TestClient(ClientSubsystem):
    @EventListener('OnLocalPlayerStopLoading')
    def onLocalPlayerStopLoading(self, ev):
        inputEx = getOneSingletonComponent(InputExComponent)
        inputEx.enableMappings('move', 'spying')
        LevelClient.getInstance().operation.SetCanMove(False)

    @InputAction('laravelMovement')
    def iaMove(self, ev):
        x, y = ev.value
        # 将输入转换为世界方向移动

    @InputAction('jump')
    def iaJump(self, ev):
        if compClient.CreateAttr(localPlayerId()).isEntityOnGround():
            motion = compClient.CreateActorMotion(localPlayerId())
            motion.SetMotion((0, 0.5, 0))

4. 运动系统插件 — $vendor.motion

4.1 概述

封装了原生 ActorMotionComponent,提供玩家运动控制及自动客户端→服务端同步。

4.2 启用

PLUGINS = ['$vendor.motion']

conf.py 中默认已启用此插件。

4.3 PlayerMotionComponent

PlayerMotionComponent 是一个单例组件,自动在客户端就绪时创建并绑定到本地玩家。

from architect.component import getOneSingletonComponent
from architect.plugins.motion.playerMotionComp import PlayerMotionComponent

motionComp = getOneSingletonComponent(PlayerMotionComponent)

4.4 属性与方法

属性 类型 读写 说明
motion Vector3 读/写 设置运动向量,写入时自动同步到服务端
inputVector tuple 只读 获取输入向量
mousePosition tuple 只读 获取鼠标位置

4.5 基本用法

from architect.compact import *
from architect.plugins.motion.playerMotionComp import PlayerMotionComponent

@SubsystemClient
class MyMovement(ClientSubsystem):
    @Sched.Update
    def updateMovement(self):
        motionComp = getOneSingletonComponent(PlayerMotionComponent)
        # 读取当前运动
        currentMotion = motionComp.motion
        # 读取输入向量
        inputVec = motionComp.inputVector
        print('Input:', inputVec)

4.6 设置运动(自动同步)

motion setter 接受 Vector3 类型。如果只有 tuple,需用 compact 导出的 vec() 包装:

from architect.compact import vec
from architect.plugins.motion.playerMotionComp import PlayerMotionComponent

motionComp = getOneSingletonComponent(PlayerMotionComponent)
# 设置运动向量,自动同步到服务端
motionComp.motion = vec(1.0, 0.5, 0.0)

每次写入 motion 属性时,框架自动通过 remote.client.call 调用服务端的 PlayerMotionSyncServer.syncMotion,服务端再将运动状态应用到对应玩家的 ActorMotionComponent

4.7 配合输入系统使用

from architect.plugins.input.client import InputAction
from architect.plugins.motion.playerMotionComp import PlayerMotionComponent

@SubsystemClient
class TestClient(ClientSubsystem):
    @InputAction('laravelMovement')
    def onMove(self, ev):
        x, y = ev.value
        motionComp = getOneSingletonComponent(PlayerMotionComponent)
        motionComp.motion = vec(x, 0, y)

4.8 服务端同步

运动同步由 PlayerMotionSyncServer 自动处理,无需手动调用。客户端写入 motion 时立即触发:

# 位于 architect/plugins/motion/system/sync.py(自动处理,无需手动调用)
@SubsystemServer
class PlayerMotionSyncServer(ServerSubsystem):
    @Remote
    def syncMotion(self, playerId, motion):
        compServer.CreateActorMotion(playerId).SetPlayerMotion(motion)

5. 小队系统插件 — $vendor.squad

此插件源文件目前为空(预留),尚未实现。


下一步