动画状态机
概述
在动画树编辑器( Anim Tree Editor)中添加状态机(State Machine)并暴露接口,可实现逻辑层的状态切换,从而由数据来驱动状态管理。
状态机(State Machine)节点包含若干状态和状态间的迁移。每个状态包含一个动画树,每个迁移包含一组迁移规则。在外部逻辑的驱动下,状态机将根据迁移规则在不同状态间切换。状态机将激活状态的输出动画融合,并作为自身的输出姿势,传递给自身所处的动画树做进一步处理。
状态机节点
创建状态机节点
在动画树(Anim Tree)空白处右击打开节点选择(Node Selection)面板,展开状态机(State Machine)分类并从中选取添加新状态机(Add New State Machine)。
创建完成:
编辑状态机节点
选中状态机节点,点击节点的标题区可修改状态机名。
双击节点可进入该状态机的状态图中,在状态图中可创建状态和组织结构。
状态机节点属性
在动画树(Anim Tree)中选择状态机(State Machine)节点,可在属性窗口(Property Window)中设置其属性。
属性 | 说明 |
---|---|
状态机名(State Machine Name) | 编辑状态机名。 |
单帧最大状态迁移数量(Max Amount Of State Transition Per Frame) | 一帧中最多运行状态迁移的数量。 |
再次进入是否重置(Whether Reset When Re-Entry) | 再次进入是否需要重置,如果不重置则从离开时的状态开始更新。 |
初始化时是否跳过第一次迁移(Whether Skip First Transition When Initializing) | 若勾选,则将直接满权重进入满足条件的状态;否则,将从入口状态过渡至满足条件的状态。 |
状态节点
创建状态节点
在状态图空白处右击打开节点选择面板,点击添加状态(Add State)。
创建完成:
编辑状态节点
选中状态节点,点击该节点的标题区后可修改状态名。
双击状态节点可进入该状态的动画树编辑界面。
根据需要,为该状态配置动画树。
状态节点属性
在状态图中选中状态节点,可在属性窗口(Property Window)中设置其属性。
属性 | 说明 |
---|---|
状态名(State Name) | 编辑状态名。 |
进入状态事件(Start State Event) | 进入状态事件回调函数名。 |
离开状态事件(End State Event) | 离开状态事件回调函数名。 |
完全混入状态事件(Fully Blend Into State Event) | 完全混入状态事件回调函数名。 |
每次进入状态都重置(Reset On Every Entry) | 是否每次进入状态都重置关联动画树的子节点。 |
创建迁移
从一个状态节点的外沿处按住鼠标左键不松开,向外拖拽,可拉出一根代表迁移的箭头,在合适的位置释放鼠标后,会弹出一个节点选择面板,选择想要创建的目标节点。此时,就一次性创建了一条迁移和一个目标状态。
可以从刚才的目标状态再创建一条迁移至原先的状态。
一个状态 -> 另一个状态的迁移可以有多个。
迁移属性
点击两个状态之间的迁移,可在属性窗口(Property Window)中设置其属性。
属性 | 说明 |
---|---|
过渡模式(Transition Mode) | 两个状态动画播放连接时的过渡模式。
|
持续时间(Duration) | 应用于淡入淡出的持续时间。 |
混合模式(Blend Mode) | 应用于淡入淡出的混合类型。 |
优先级(Priority) | 动画编辑器过渡优先级。 |
动作结束时自动迁移(Auto Transit When Action Ends) | 前一个状态的动作结束后是否自动迁移至目标状态。 |
回调字符串(Callback String) | 配置回调函数,根据返回值决定是否迁移。 |
是否使用条件(Use Conditions) | 勾选后可选择参数(除字符串型参数和向量型参数)用于条件表达式,并将最终结果用于控制状态迁移。 |
删除迁移
右击两个状态间迁移,选择删除迁移(Delete Transition)可删除该迁移。
状态机构建常规操作
实际操作中可根据角色的动作需求,在状态图中进行总体设计,然后逐个状态,逐个迁移进行实现。
双击Ground状态节点进入所关联的动画树(Anim Tree),在动画树中可添加各类动画树节点以实现预期的地面动作需求。例如,这里使用一个混合空间播放器(Blend Space Player)节点。
完成这个状态的设置后,可以借助导航栏(Navigation Bar)跳出这一层级的编辑,重新回到之前的状态图。
接下来可以对其他状态和迁移进行编辑,例如可以选中JumpStart状态 -> Jump状态的迁移,在属性窗口(Property Window)中编辑其属性完成迁移规则的配置。
管道节点
管道可以视为一种特殊的状态,与上述状态不同的是,它不包含动画树。除了通过迁移控制状态切换外,状态机还提供了一种被称为管道的节点,用于一对多,多对一或多对多的状态切换需求。管道类似于一个移除了动画树的状态。它内部具有一个“是否允许进入迁移”的规则,通过对这条规则的配置,当状态试图通过管道迁移时,可以决定是否能够通过其做进一步的迁移。
创建管道节点
在状态图空白处右击打开节点选择面板,点击添加管道(Add Conduit)。
创建完成:
选中管道节点,点击该节点的标题区可修改管道名。
管道节点属性
在状态图中选中管道节点,可在属性窗口(Property Window)中设置其属性。
属性 | 说明 |
---|---|
管道名(Conduit Name) | 编辑管道名。 |
是否能进入(Can Entry) | 是否能进入该节点。 |
管道节点用例
下图中的Killed节点即为管道(Conduit)节点,当角色处于Walk或Run的运动状态时,若遭受攻击,健康值低于死亡临界值,逻辑控制状态向Killed管道节点迁移,并由具体的迁移规则决定最终切换至何种状态。
迁移规则
状态迁移包含如下三种迁移规则:
状态动画播放完自动切换到下一状态
通过使用回调函数制定迁移规则以完成状态迁移
通过设置是否使用条件(Use Conditions)完成状态迁移。
使用回调函数和使用条件相比,推荐后者。
动画播放完自动迁移
设计如下图所示的状态图,完成起跳到落地的状态迁移。
Jump状态关联的动画树(Anim Tree):
JumpEnd状态关联的动画树(Anim Tree):
选中Jump状态 -> JumpEnd状态的迁移, 在其属性窗口(Property Window)中勾选动作结束时自动迁移(Auto Transit When Action Ends)。
按照上述操作,设置JumpEnd状态 -> Jump状态的迁移。
点击应用(Apply)按钮即可查看效果。Jump状态动画播放结束后自动过渡到目标JumpEnd状态动画,JumpEnd状态动画播放结束后自动过渡到Jump状态动画,依此循环。
回调字符串
可通过设置回调字符串(Callback String)来完成状态迁移。设计如下图所示的状态图,然后分别设置状态间的迁移,例如,选中Ground状态 -> JumpStart状态的迁移,在其属性窗口(Property Window)的回调字符串(Callback String)中输入jump_start。
状态迁移的回调
on_transition_event(callee, role, action_index, machine_index, prev_state, next_state, callback_name)
参数1:AnimCallee实体
参数2:目标角色实体
参数3:动作索引
参数4:状态机索引
参数5:过渡前状态索引
参数6:过渡后状态索引
参数7:回调事件名称字符串
使用条件
设计如下图所示的状态图,完成Idle和Run状态间的迁移。
Idle状态关联的动画树(Anim Tree):
Run状态关联的动画树(Anim Tree):
在动画参数(Animation Parameter)中点击 按钮添加一个参数,此处选择浮点型(Float) 参数。
添加完成后,修改参数名称(Parameter Name)为Speed。
选中Idle状态 -> Run状态的迁移,在其属性窗口(Property Window)中,勾选使用条件(Use Conditions),在第一个下拉框中选择刚才所创建的参数,并设置具体条件,当Speed参数值大于0.1时,则从Idle状态过渡至Run状态。
选中Run状态 -> Idle状态的迁移,同样勾选使用条件(Use Conditions),选择Speed参数用于条件表达式,当Speed参数值小于0.1时,则从Run状态迁移至Idle状态。
在动画树编辑器(Anim Tree Editor)中,在菜单栏下方的下拉框中选择预览对象(Preview Object),点击应用(Apply)按钮查看效果。
修改Speed参数值可切换状态。
有关使用状态机并通过使用条件制定迁移规则的具体应用可参考角色快速入门。
示例
以下为应用状态机(State Machine)完成控制角色行动的一个示例,迁移规则运用了动作结束时自动迁移(Auto Transit When Action Ends)和回调字符串(Callback String)。
创建组合体
参考Actor组装,创建一个组合体(Actor),其中包含一系列动画资产。
创建动画序列
在组件编辑器(Component Editor)中,双击任一骨架资产打开动画编辑器(Animation Editor)。
在动画编辑器(Animation Editor)中的资产浏览器(Asset Browser)中,选中所要转换的骨架资产,右击打开快捷菜单,选择将骨架资产转为动画序列资产(Convert Skeleton To AnimSeq),在弹出的窗口中输入动画序列的名称,点击确定(OK)按钮完成创建。
按照上述操作,将walk、run和stand相关的骨骼资产均转换为动画序列资产,后续用作混合空间(Blend Space)中的采样点。
在资产浏览器(Asset Browser)中,双击一个走路或跑步的动画序列,为其添加同步标记(Sync Marker)和同步组(Sync Group),可使不同动画混合时脚步同步。为其他walk和run动画序列同样添加同步标记和同步组。可通过动画同步文档了解同步标记和同步组的相关内容。
编辑完成后,依次点击菜单栏文件(File)-> 保存所有(Save All)进行保存。
创建混合空间
在动画编辑器(Animation Editor)中,依次点击菜单栏文件(File)-> 创建(Create),在弹出窗口的下拉框中选择混合空间(BlendSpace),设置名称和路径,点击是(Yes)完成创建。
可参考混合空间文档,创建如下图所示的混合空间资产。
编辑完成后,保存。
创建动画树
在动画编辑器(Animation Editor)中,依次点击菜单栏文件(File)-> 创建(Create),在弹出窗口的下拉框中选择动画树(AnimTree),设置名称和路径,点击是(Yes)完成创建。
在打开的动画树编辑器(Anim Tree Editor)中,右击空白处打开节点选择面板,展开混合空间列表(Blend Space List),选择混合空间播放器 "bs_run"(Blend Space Player "bs_run")节点。
选中所创建的混合空间播放器(Blend Space Player)节点,在属性窗口(Property Window)中勾选是否循环(Loop),并配置脚本中的回调字符串。(可通过混合空间播放器节点了解该节点。)
在动画树(Anim Tree)空白处右击打开节点选择面板,展开缓存姿势节点列表(Cached Poses Node List),选择新建缓存姿势(Create Cache Pose)。
选中所创建的缓存姿势(Cache Pose)节点,点击其标题区修改其名称为Ground。
将混合空间播放器(Blend Space Player)节点链接到Ground缓存姿势(Cache Pose)节点。
创建状态机
创建一个状态机(State Machine)节点,修改其名称为Locomotion。
双击该状态机(State Machine)节点进入到状态图的编辑界面中,设计如下图所示的状态图。
Ground状态所关联的动画树(Anim Tree):
JumpStart状态所关联的动画树(Anim Tree):
Jump状态所关联的动画树(Anim Tree):
JumpFall状态所关联的动画树(Anim Tree),其中混合(Blend)节点的混合权重(Blend Weight)设置为0.5,这样可以让落地到地面的姿势过渡更加自然。
设置状态迁移
选中Ground状态 -> JumpStart状态的迁移,在属性窗口(Property Window)中,设置状态迁移回调字符串(Callback String)为jump_start。
选中JumpStart状态,在属性窗口(Property Window)中,设置完全混入状态事件(Fully Blend Into State Event)回调函数名为fully_blend_state。
选中JumpStart状态 -> Jump状态的迁移,在属性窗口(Property Window)中,勾选动作结束时自动迁移(Auto Transit When Action Ends)、持续时间(Duration)为0.1。
选中Jump状态,在属性窗口(Property Window)中,分别设置进入状态事件(Start State Event)、离开状态事件(End State Event)和完全混入状态事件(Fully Blend Into State Event)的回调函数名为start_state、end_state和fully_blend_state。
选中Jump状态 -> JumpFall状态的迁移,在属性窗口(Property Window)中,设置状态迁移回调字符串(Callback String)为jump_end、持续时间(Duration)为0.1。
分别设置JumpFall状态 -> Ground状态的两个迁移,在属性窗口(Property Window)中,一个勾选启用动作结束时自动迁移(Auto Transit When Action Ends),另一个使用回调字符串(Callback String)jump_start。
状态图编辑完成后,返回到上一级动画树(Anim Tree)中。
将状态机(State Machine)节点链接到最终动画姿势(Final Animation Pose)节点。
点击保存(Save)按钮完成动画树(Anim Tree)整体的创建。
配置组合体
在组合体编辑器(Actor Editor)中,点击资产细项(Asset Details),设置该组合体的默认动作(Default Action)为动画树test_tree,然后点击保存(Save)。
创建角色
创建一个新的默认关卡,依次点击工具栏创建游戏对象(Create Game Object)-> 角色(Character),创建一个角色。
在大纲(Hierarchy)面板中选中所创建的角色,在观察器(Inspector)面板中选中CharacterMesh0(Inherited),将所创建的组合体(Actor)拖入到骨架网格体配置文件(Skeletal Mesh Config File)的插槽中。
拖入完成:
在观察器(Inspector)面板中,选中CollisionCylinder(Inherited)和CharacterMesh0(Inherited)分别调整该角色胶囊体和网格体的位置,使胶囊体尽可能在包裹角色的前提下,让角色蒙皮的脚部贴地,胶囊体也需在地面上方。
在大纲(Hierarchy)面板中,选中角色对象,然后在观察器(Inspector)面板中,选中CharacterMesh0(Inherited),将默认动作覆盖(Default Action Override)设置为所创建的动画树test_tree。
选中CollisionCylinder(Inherited),然后点击添加组件(Add Component)按钮,选择并添加弹簧臂组件(Spring Arm Component)。
勾选弹簧臂组件的是否使用Pawn控制旋转(Use Pawn Control Rotation)属性。
添加一个摄像机组件(Camera Component)为弹簧臂组件的子集,并调整摄像机至合适位置。
选中CharMoveComp(Inherited)组件,设置地面摩擦(Ground Friction)为3、最大步行速度(Max Walk Speed)为9、制动减速行走(Braking Deceleration Walking)为9、最大自定义移动速度(Max Custom Movement Speed)为9。
在观察器(Inspector)面板中选中LCharacter(Instance),将自动持有玩家(Auto Possess Player)设置为玩家0(Player 0)。
绑定脚本
添加两个脚本组件(Script Component)。选中该角色,然后在观察器(Inspector)面板中点击添加组件(Add Component)按钮,选择脚本组件(Script Component)。
在脚本组件的属性(Property)面板中配置脚本文件,从资源预览窗口(Resource Preview)中将所编写的脚本拖入到角色脚本组件的脚本文件(Script File)插槽中。
拖拽完成:
脚本
以下为参考脚本:
robot_character.lua
--script template
local IE_Pressed = 0 --按键按下
local IE_Released = 1 --按键抬起
local IE_Repeat = 2
local IE_DoubleClick = 3
local IE_Axis = 4
local IE_MAX = 5
local CAMERA_DISTANCE_CHANGE_SPEED = 0.5
local CAMERA_DISTANCE_MIN = -1
local CAMERA_DISTANCE_MAX = 50
--获取actor
function character_get_actor(character)
if nx_is_valid(character) then
local mesh_component = character.MeshComponent
if nx_is_valid(mesh_component) then
local vis_base = mesh_component.VisBase
if nx_is_valid(vis_base) and nx_is_kind(vis_base, "Actor") then
return vis_base
end
end
end
return nx_null()
end
--向前移动回调
function on_moveforward(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end
--获得控制器Y轴角度
local controller = owner.Controller
if nx_is_valid(controller) then
local yaw = controller.AngleY
--获得向前方向
local x, y, z = nx_function("ext_angle_get_forward_vector", 0.0, yaw, 0.0)
--增加移动量
owner:AddMovementInput(x, y, z, axis_value)
end
return 1
end
--向右移动回调
function on_moveright(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end
--获得控制器Y轴角度
local controller = owner.Controller
if nx_is_valid(controller) then
local yaw = controller.AngleY
--获得向右方向
local x, y, z = nx_function("ext_angle_get_right_vector", 0.0, yaw, 0.0)
--增加移动量
owner:AddMovementInput(x, y, z, axis_value)
end
return 1
end
--左右旋转(绕Y轴旋转)回调
function on_turn(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end
owner:AddControllerYawInput(axis_value)
return 1
end
--上下旋转(绕X轴旋转)回调
function on_lookup(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end
owner:AddControllerPitchInput(-axis_value)
return 1
end
--跳跃键按下回调
function on_jump_pressed(owner)
if nx_find_custom(owner, "JumpAgainLock") and owner.JumpAgainLock then
return 1
end
--角色跳跃
owner:Jump()
return 1
end
function input_comp_init_bind(component)
local owner = component.GameObjectOwner
if nx_is_valid(owner) then
local input_comp = owner.InputComponent
if nx_is_valid(input_comp) then
nx_bind_script(owner, nx_current())
input_comp:AddAxisBinding("MoveForward", true, false, true, "InputAxisEvent_MoveForward")
input_comp:AddAxisBinding("MoveRight", true, false, true, "InputAxisEvent_MoveRight")
input_comp:AddAxisBinding("Turn", true, false, true, "InputAxisEvent_Turn")
input_comp:AddAxisBinding("LookUp", true, false, true, "InputAxisEvent_LookUp")
input_comp:AddCombinationBinding("Jump", IE_Pressed, true, false, true, "InputActionEvent_Jump_Pressed")
nx_callback(owner, "InputAxisEvent_MoveForward", "on_moveforward")
nx_callback(owner, "InputAxisEvent_MoveRight", "on_moveright")
nx_callback(owner, "InputAxisEvent_Turn", "on_turn")
nx_callback(owner, "InputAxisEvent_LookUp", "on_lookup")
nx_callback(owner, "InputActionEvent_Jump_Pressed", "on_jump_pressed")
end
end
end
--用于初始化
function on_begin_play(component)
input_comp_init_bind(component)
nx_callback(component, "on_tick", "tick")
end
robot_character_animation.lua
--脚本模板
--------------------------
--局部效用函数--
--------------------------
local radian = 57.2958
local foot_rel_height = 0.14
local function get_movement_component(pawn)
if nx_is_valid(pawn) then
return pawn:GetMovementComponentID()
end
return nx_null()
end
local function get_speed(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)
if nx_is_valid(movement) then
local speed = nx_function("ext_vector_length", pawn.VelocityX, pawn.VelocityY, pawn.VelocityZ)
return speed
end
end
return 0.0
end
local function is_jumping(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)
if nx_is_valid(movement) then
if movement:IsFalling() then
return true
end
end
end
return false
end
local function is_flying(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)
if nx_is_valid(movement) then
if movement:IsFlying() then
return true
end
end
end
return false
end
function transition_callback_jump_start(pawn, role, action_index, machine_index, prev_state, next_state)
if is_jumping(pawn) then
return true
else
return false
end
end
function transition_callback_jump_end(pawn, role, action_index, machine_index, prev_state, next_state)
if not is_jumping(pawn) then
return true
else
return false
end
end
transition_event_callback =
{
["jump_start"] = transition_callback_jump_start,
["jump_end"] = transition_callback_jump_end,
}
function on_transition_event(mesh_comp, action_index, machine_index, prev_state, next_state, callback_name)
local pawn = mesh_comp.GameObjectOwner
local role = mesh_comp.VisBase
if not nx_is_valid(pawn) or not nx_is_valid(role) then
return false
end
local callback = transition_event_callback[callback_name]
if callback then
return callback(pawn, role, action_index, machine_index, prev_state, next_state)
end
return false
end
function state_machine_state_callback_EnterState(pawn, role, action_index, machine_index, state_index)
-- 执行
nx_log("state machine state enter jump")
end
function state_machine_state_callback_LeaveState(pawn, role, action_index, machine_index, state_index)
-- 执行
nx_log("state machine state leave jump")
end
function state_machine_state_callback_FullyBlendState(pawn, role, action_index, machine_index, state_index)
-- 执行
nx_log("state machine state fully blend jump")
end
state_machine_state_event_callback =
{
["start_state"] = state_machine_state_callback_EnterState,
["end_state"] = state_machine_state_callback_LeaveState,
["fully_blend_state"] = state_machine_state_callback_FullyBlendState,
}
function on_state_machine_state_event(mesh_comp, action_index, machine_index, state_index, callback_name)
local pawn = mesh_comp.GameObjectOwner
local role = mesh_comp.VisBase
if not nx_is_valid(pawn) or not nx_is_valid(role) then
return
end
local callback = state_machine_state_event_callback[callback_name]
if callback then
callback(pawn, role, action_index, machine_index, state_index)
end
end
function animtree_callback_bs_x_direction(pawn, role)
return pawn.direction
end
function animtree_callback_bs_y_speed(pawn, role)
return get_speed(pawn)
end
local function get_anim_tree_index(role)
if role:GetBlendActionCount() > 0 then
local action_name = role:GetBlendActionName(0)
local action_type = role:GetActionType(action_name)
if action_type == AT_ANIMTREE then
return role:GetActionIndex(action_name)
end
end
return -1
end
animtree_event_callback =
{
["bs_x_direction"] = animtree_callback_bs_x_direction,
["bs_y_speed"] = animtree_callback_bs_y_speed,
}
function on_animtree_event(mesh_comp, action_index, callback_name)
local pawn = mesh_comp.GameObjectOwner
local role = mesh_comp.VisBase
if not nx_is_valid(pawn) or not nx_is_valid(role) then
return
end
local callback = animtree_event_callback[callback_name]
if callback then
return callback(pawn, role)
end
return false
end
function anim_callee_init(mesh_comp)
nx_callback(mesh_comp, "on_transition_event", "on_transition_event")
nx_callback(mesh_comp, "on_animtree_event", "on_animtree_event")
nx_callback(mesh_comp, "on_state_machine_state_event", "on_state_machine_state_event")
nx_callback(mesh_comp, "on_state_action_event_begin", "on_state_action_event_begin")
nx_callback(mesh_comp, "on_state_action_event_update", "on_state_action_event_update")
nx_callback(mesh_comp, "on_state_action_event_end", "on_state_action_event_end")
end
--用于初始化
function on_begin_play(component)
local owner = component.GameObjectOwner
if nx_is_valid(owner) then
local mesh_comp = owner.MeshComponent
if nx_is_valid(mesh_comp) then
owner.meshcomp_init_relative_posy = owner.MeshComponent.RelativePositionY
mesh_comp.ActorCalleeType = "CComponentActorCalleeScript"
nx_bind_script(mesh_comp, nx_current(), "anim_callee_init")
end
end
nx_callback(component, "on_tick", "tick")
end
function tick(component, delta_time)
local owner = component.GameObjectOwner
local move_comp = get_movement_component(owner)
local speedx = move_comp.VelocityX
local speedz = move_comp.VelocityZ
local rate = speedx / speedz
local tan = math.atan(rate)
local velocity_angle = radian * tan
if speedx == 0 and speedz == 0 then
velocity_angle = 0
elseif speedx > 0 and speedz == 0 then
velocity_angle = 90
elseif speedx == 0 and speedz < 0 then
velocity_angle = 180
elseif speedx < 0 and speedz == 0 then
velocity_angle = 270
elseif speedx > 0 and speedz < 0 then
velocity_angle = 180 - math.abs(velocity_angle)
elseif speedx <= 0 and speedz < 0 then
velocity_angle = velocity_angle + 180
elseif speedx < 0 and speedz > 0 then
velocity_angle = 360 - math.abs(velocity_angle)
end
local cap_comp = owner.CapsuleComponent
local foward_angle = cap_comp.AngleY * radian
local relative_angle = velocity_angle - foward_angle
if relative_angle > 180 then
relative_angle = relative_angle - 360
elseif relative_angle < -180 then
relative_angle = relative_angle + 360
end
owner.direction = relative_angle
end
最终结果
在编辑器中点击运行后,可通过WASD和空格键(SpaceBar)控制角色行动。