Skip to main content

运动扭曲

概述

运动扭曲(Motion Warping)可以程序化地调整动画的根运动(Root Motion)以适配不同的起始位置和旋转,从而在不同的场景中复用根运动动画资产。

根据目标对象的位置和方向自动校正动画的移动和旋转量。例如,让玩家在不同位置都可通过跳跃动作翻越障碍物。

动画序列

在动画序列中可创建运动扭曲(Motion Warping)状态通知来指定运动扭曲的区域,根据需要调整根骨骼位移,在拥有有限动画资产的情况下,也可以创建出多种多样的动画效果。

image-20230602164433753

创建状态通知

动画编辑器(Animation Editor)中打开所需编辑的动画序列资产,在通知轨道上右击依次选择添加通知(Add Notify) -> 运动扭曲(Motion Warping)

添加运动扭曲状态通知(Motion Warping State Notify)后,可在轨道中左右拖拽通知标签来调整其开始和结束的时间,从而指定运用扭曲的区域。

属性

在轨道上选中运动扭曲状态通知后,可在详细面板(Details Panel)中对其相关属性进行编辑。

image-20230602164522337

属性说明
类型(Type)该参数指定应用算法的类型。
目标名称(Target Name)该参数指定场景中目标的标签名称。
扭曲平移(Warp Translation)该参数指定是否开启位移的调整。
扭曲旋转(Warp Rotation)该参数指定是否开启旋转的调整。
旋转扭曲类型(Rotation Warping Type)该参数指定用于调整旋转的方式。
  • 对齐目标(Align To Target):在过程中与目标保持朝向的一致。
  • 查看目标(Look At Target):在过程中始终朝向目标。

动画树

在动画树中可通过添加节点来控制管理动画,可将要用到运动扭曲(Motion Warping)的动画放入动画树中的插槽中。

示例

下面是运用运动扭曲功能完成角色翻越目标障碍物的示例。

组合体组装

参照Actor组装,创建一个包含蒙皮和骨架资产的组合体(Actor)

image-20230529093523691

设置动画序列

将跳跃的骨架资产转换为动画序列资产。在动画编辑器(Animation Editor)中的资产浏览器(Asset Browser)窗口中选中骨架资产,然后右击选择将骨架资产转为动画序列资产(Convert Skeleton To AnimSeq)

image-20230529093615311

在弹出的窗口中对动画序列资产进行命名,然后点击确定(OK)按钮完成转换。

启用根运动(Root Motion)。在动画序列资产的资产细项(Asset Details)面板中勾选启用根运动(Enable Root Motion)

在该资产编辑面板中的轨道上添加运动扭曲状态通知(Motion Warping State Notify),右击轨道然后依次点击添加通知(Add Notify) -> 运动扭曲(Motion Warping)

在弹出的新通知(New Notify)窗口中输入状态通知的名称,然后点击确定(OK)按钮完成添加。

根据所要扭曲的动画部分,调整该运动扭曲状态通知(Motion Warping State Notify)的位置和持续时间。有关状态通知(State Notify)的基本操作可详见动画通知

image-20230526155223209

选中运动扭曲状态通知(Motion Warping State Notify),在详细面板(Details Panel)中修改其属性,在目标名称(Target Name)中输入一个名称VaultPoint,用于标识该扭曲。

image-20230602164708788

可按照上述添加运动扭曲状态通知的方法继续添加状态通知(State Notifies),并为这些扭曲指定目标名称(Target Name)

image-20230602164837081

资产细项(Asset Details)面板中勾选结束触发器(End Trigger),在动画结束播放时触发通知。

配置动画树

在动画树中可通过添加节点对角色动画进行管理。

在动画编辑器中依次点击菜单栏文件(File) -> 创建(Create),点击弹出窗口中的下拉框,选择动画树(AnimTree),为其命名并选择创建的路径。

在所创建的动画树中右击打开节点选择面板,然后依次点击状态机(State Machine) -> 添加新的状态机(Add New State Machine)来添加一个状态机节点。

双击该状态机( State Machine)节点进入状态图编辑界面,在状态图中可以创建状态和组织结构,创建idle和run两个状态间的转换,具体创建流程可参考角色快速入门 -> 添加状态机节点

动画树编辑器(Anim Tree Editor)中的插槽管理器(Slot Manager)窗口中创建一个插槽FullBody 。先在插槽管理器中右击插槽名称(Slot Name),然后在弹出的快捷菜单中点击添加默认组和插槽(Add Default Group And Slot)

默认插槽(DefaultSlot)上右击打开快捷菜单,选择添加插槽(Add Slot)来添加一个新的插槽,在弹出的窗口中输入插槽名称后按Enter键完成添加。

从状态机节点引脚处进行拖拽打开节点选择面板,展开蒙太奇(Montage)分类,选择FullBody插槽(Slot"FullBody")

将FullBody插槽节点连接到最终动画姿势(Final Animation Pose)节点上。

最后,在动画树编辑器中依次点击文件(File)-> 保存(Save)

配置组合体

在资源预览窗口中双击打开所创建的组合体文件,然后在组合体编辑器(Actor Editor)中点击资产细项(Asset Details),将默认动作(Default Action)设置为刚才所创建的动画树,并将根运动模式(Root Motion Mode)设置为来自任何动画(From Everything)

image-20230529094319407

修改完后,在组合体编辑器中点击保存(Save)

image-20230529153000436

创建角色

在组件编辑器中创建一个关卡,然后在关卡中添加一个角色(Character)并设置其属性,该角色的骨架网格体配置文件(Skeletal Mesh Config File)为刚才所创建的组合体(Actor)。具体操作步骤可参考角色快速入门设置角色部分。

image-20230529163840236

输入设置

输入设置参考角色快速入门输入设定编辑部分。

创建目标

从资源预览窗口中拖拽3个模型到关卡(Level)中,并调整其大小,将这三个静态网格体作为角色扭曲要避开的目标(三个静态网格体朝向需与角色保持一致)。

大纲(Hierarchy)面板中选中静态网格体对象右击打开快捷菜单,依次点击编辑(Edit)-> 重命名(Rename)对其进行重命名。

重命名后:

大纲(Hierarchy)面板中选中刚才所创建的静态网格体,在观察器(Inspector)面板下方的字符串标签(String Tag)属性中填入名称,此处的名称与动画序列中的运动扭曲状态通知目标名称(Target Name)保持一致。

image-20230529144646525

余下的两个静态网格体也按照上述操作,为其设定字符串标签(String Tag)FlyEndPoint和LandingPoint。

为角色绑定脚本

为角色绑定脚本。在大纲(Hierarchy)面板中选中LCharacter,在观察器(Inspector)面板中选中LCharacter(Instance),点击添加组件(Add Component)按钮,选择脚本组件(Script Component)

在脚本组件的属性(Property)面板中配置脚本文件,从资源预览窗口将脚本拖入到角色脚本组件的脚本文件(Script File)插槽中。

image-20230529153111175

拖入完成:

image-20230529154146504

脚本一

参考脚本:

motion_warping_character.lua

--脚本模板

require("public_attr")

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

--------------------------
--局部效用函数--
--------------------------

---------------------
--默认回调--
---------------------

--用于初始化
function on_begin_play(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:AddAxisBinding("AdjustCameraDis", true, false, true, "InputAxisEvent_AdjustCameraDis")

input_comp:AddCombinationBinding("Jump", IE_Pressed, true, false, true, "InputActionEvent_Jump_Pressed")
input_comp:AddCombinationBinding("Jump", IE_Released, true, false, true, "InputActionEvent_Jump_Released")
input_comp:AddCombinationBinding("Dash", IE_Pressed, true, false, true, "InputActionEvent_Dash_Pressed")
input_comp:AddCombinationBinding("EnableLookAt", IE_Pressed, true, false, true, "InputActionEvent_EnableLookAt_Pressed")
input_comp:AddCombinationBinding("LeftClick", IE_Pressed, true, false, true, "InputActionEvent_LeftClick_Pressed")

input_comp:AddCombinationKeyBinding("R", false, false, false, false, 0, false, false, true, "r_key_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, "InputAxisEvent_AdjustCameraDis", "on_adjust_camera_dis")

nx_callback(owner, "InputActionEvent_Jump_Pressed", "on_jump_pressed")
nx_callback(owner, "InputActionEvent_Jump_Released", "on_jump_released")
nx_callback(owner, "InputActionEvent_LeftClick_Pressed", "on_leftclick_pressed")

nx_callback(owner, "InputActionEvent_Dash_Pressed", "on_dash_pressed")
nx_callback(owner, "InputActionEvent_EnableLookAt_Pressed", "on_enable_look_at_pressed")

nx_callback(owner, "on_velocity_changed", "on_velocity_changed")
nx_callback(owner, "r_key_pressed", "toggle_ragdoll")

end

-- 查询MotionWarping TargetPoint
local mw_target_name1 = "VaultPoint"
local mw_target_name2 = "FlyEndPoint"
local mw_target_name3 = "LandingPoint"

local scene = owner.Scene
local actor = character_get_actor(owner)

local vault_point_objs = scene:QueryObjectListByTag(mw_target_name1, "fx_component.LGameObject")
if #vault_point_objs > 0 then
local vault_point_actor = vault_point_objs[1]
actor:AddOrUpdateMotionWarpingTarget(mw_target_name1, vault_point_actor.PositionX, vault_point_actor.PositionY, vault_point_actor.PositionZ, vault_point_actor.AngleX, vault_point_actor.AngleY, vault_point_actor.AngleZ)
end

local fly_end_point_objs = scene:QueryObjectListByTag(mw_target_name2, "fx_component.LGameObject")
if #fly_end_point_objs > 0 then
local fly_end_point_actor = fly_end_point_objs[1]
actor:AddOrUpdateMotionWarpingTarget(mw_target_name2, fly_end_point_actor.PositionX, fly_end_point_actor.PositionY, fly_end_point_actor.PositionZ, fly_end_point_actor.AngleX, fly_end_point_actor.AngleY, fly_end_point_actor.AngleZ)
end

local landing_point_objs = scene:QueryObjectListByTag(mw_target_name3, "fx_component.LGameObject")
if #landing_point_objs > 0 then
local landing_point_actor = landing_point_objs[1]
actor:AddOrUpdateMotionWarpingTarget(mw_target_name3, landing_point_actor.PositionX, landing_point_actor.PositionY, landing_point_actor.PositionZ, landing_point_actor.AngleX, landing_point_actor.AngleY, landing_point_actor.AngleZ)
end
end

nx_callback(component, "on_tick", "tick")
end

--用于发布
function on_end_play(component)
end

--获取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 character_get_spring_arm(character)
if nx_is_valid(character) then
local spring_arm_component = character:FindComponentByClassName("LSpringArmComponent")

return spring_arm_component
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_adjust_camera_dis(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

local spring_arm_component = character_get_spring_arm(owner)

if nx_is_valid(spring_arm_component) then
local delta = axis_value * CAMERA_DISTANCE_CHANGE_SPEED

local cur_value = spring_arm_component.TargetArmLength
local new_value = cur_value + delta

if delta > 0 then
if new_value > CAMERA_DISTANCE_MAX then
new_value = CAMERA_DISTANCE_MAX
end

spring_arm_component.TargetArmLength = new_value
else
if new_value < CAMERA_DISTANCE_MIN then
new_value = CAMERA_DISTANCE_MIN
end
spring_arm_component.TargetArmLength = new_value
end
end

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 on_dash_pressed(owner)
local actor = character_get_actor(owner)
if not nx_is_valid(actor) then
return
end

local movement_comp = owner.CharacterMovement
--local mode = movement_comp.MovementMode
local new_mode = nx_enum("MOVEMENT_MODE::Flying")
movement_comp:SetMovementModeWithCustom(new_mode, 0)
actor:BlendAction("Super_Jumping_Over_sq", false, false, true, 1.0, true, "FullBody", true)
end

--骨骼朝向
function on_enable_look_at_pressed(owner)
local actor = character_get_actor(owner)
if not nx_is_valid(actor) then
return
end

local action_name = actor:GetBlendActionName(0)
local action_type = actor:GetActionType(action_name)
local action_index = actor:GetActionIndex(action_name)
if action_type == AT_ANIMTREE then
if actor:GetAnimTreeValue(action_index, "LookAtIndex") == 0 then
actor:SetAnimTreeValue(action_index, "LookAtIndex", 1)
else
actor:SetAnimTreeValue(action_index, "LookAtIndex", 0)
end
end
end

--跳跃键抬起回调
function on_jump_released(owner)
--角色停止跳跃
owner:StopJumping()

return 1
end

function on_leftclick_pressed(owner)
local actor = character_get_actor(owner)
if not nx_is_valid(actor) then
return
end

--actor:BlendAction("PrimaryMelee_D_Slow_sq", false, false, true, 1.0, true, "myslot", true)
--actor:BlendAction("PrimaryMelee_C_Slow2_MSA_Anim", false, false, true, 1.0, true, "myslot", true)
actor:BlendAction("PrimaryMelee_C_Slow_MSA_sq", false, false, true, 1.0, true, "myslot", true)
--actor:SetBlendActionPause("PrimaryMelee_C_Slow_MSA_sq", true)

--[[
local cur_percent = 0
while cur_percent < 100 do
actor:SetCurrentPercent("PrimaryMelee_C_Slow_MSA_sq", cur_percent)
nx_pause(0.016)
cur_percent = cur_percent + 0.016
end]]

--actor:SetBlendActionPause("PrimaryMelee_C_Slow_MSA_sq", true)

end

--速度变化回调
function on_velocity_changed(owner, component_id, new_x, new_y, new_z, old_x, old_y, old_z)
local actor = character_get_actor(owner)

if nx_is_valid(actor) then
if actor:GetBlendActionCount() > 0 then
local action_name = actor:GetBlendActionName(0)
local action_type = actor:GetActionType(action_name)
local action_index = actor:GetActionIndex(action_name)

if action_type == AT_ANIMTREE then
local speed = math.sqrt(new_x * new_x + new_y * new_y + new_z * new_z)
actor:SetAnimTreeValue(action_index, "Speed", speed)

nx_set_custom(actor,"CurSpeed",speed);
end
end
end

return 1
end

function on_rhand_ik_open(self, bOpen)
nx_set_custom(self, "rhand_ik", bOpen)
--[[local actor = self.Actor
if nx_is_valid(actor) then
if actor:GetBlendActionCount() > 0 then
local action_name = actor:GetBlendActionName(0)
local action_type = actor:GetActionType(action_name)
local action_index = actor:GetActionIndex(action_name)

if action_type == AT_ANIMTREE then
actor:SetAnimTreeValue(action_index, "bRhand", bOpen)
end
end
end--]]
end

function set_tree_state(actor, state_name, enable)
if not nx_is_valid(actor) then
return
end

-- 设置状态机变量
if actor:GetBlendActionCount() > 0 then
local action_name = actor:GetBlendActionName(0)
local action_type = actor:GetActionType(action_name)
local action_index = actor:GetActionIndex(action_name)
if action_type == AT_ANIMTREE then
actor:SetAnimTreeValue(action_index, state_name, enable)
end
end
end

-- 开关ragdoll
function toggle_ragdoll(owner, key_name)
local actor = character_get_actor(owner)
if not nx_is_valid(actor) then
return
end

nx_set_value("default_actor", actor)

local mesh_component = owner.MeshComponent
if not nx_is_valid(mesh_component) then
return
end

-- 记录mesh_component
actor.mesh_component = mesh_component

local enable_ragdoll = not mesh_component.SimulatePhysics

if (enable_ragdoll) then
mesh_component.SimulatePhysics = enable_ragdoll
set_tree_state(actor, "IsDead", true)
else
local capsule = owner.CapsuleComponent
if nx_is_valid(capsule) then
-- 记录capsule
actor.capsule = capsule
end

-- 保存快照
actor:SavePoseSnapshot("finalpose")

mesh_component.SimulatePhysics = enable_ragdoll

-- 通知混合快照
set_tree_state(actor, "IsStopped", true)

-- 播放起身动作
actor:BlendAction("GetUp_Back_montage", false, false, true, 1.0)

set_tree_state(actor, "IsDead", false)

-- 暂停两帧
nx_pause(0)
nx_pause(0)

-- 通知不再混合快照
set_tree_state(actor, "IsStopped", false)
end
end

function tick(component, delta_time)
update_capsule_location()
end

-- ragdoll状态下更新胶囊体的位置
function update_capsule_location()
local actor = nx_value("default_actor")
if not nx_is_valid(actor) then
return
end

if not nx_find_custom(actor, "mesh_component") then
return
end

local mesh_component = actor.mesh_component
if not nx_is_valid(mesh_component) then
return
end

if mesh_component.SimulatePhysics then
if not nx_find_custom(actor, "capsule") then
return
end

local capsule = actor.capsule
if not nx_is_valid(capsule) then
return
end

local x, y, z = actor:GetNodeWorldPosition("pelvis")
capsule:SetPosition(x, capsule.PositionY, z)
end
end

public_attr.lua

--资源管理器:全局变量

MODEL_PATH = "mdl\\"
MODEL_PATH_DX9 = ""
ACTOR_PATH = "ini\\actor\\"
ACTOR_PATH_DX9 = "ini\\actor\\"
LIGHT_PATH = "ini\\light\\"
LIGHT_PATH_DX9 = "ini\\light\\"
EFFECT_PATH = "ini\\effect\\"
PARTICLE_PATH = "ini\\particle\\"
SOUND_PATH = "snd\\"
REVERB_PATH = "reverb\\"
TRIGGER_PATH = "ini\\trigger\\"
PROBE_PATH = "ini\\light_probe\\"
VOLUME_FOG_PATH = "ini\\volume_fog\\"
MATERIAL_PATH = ""

--资源类型定义
TYPE_MODEL = "model"
TYPE_ACTOR = "actor"
TYPE_LIGHT = "light"
TYPE_EFFECT = "effect"
TYPE_PARTICLE = "particle"
TYPE_SOUND = "sound"
TYPE_REVERB = "reverb"
TYPE_TRIGGER = "trigger"
TYPE_PROBE = "light_probe"
TYPE_VOLUME_FOG = "volume_fog"
TYPE_DECAL = "decal"
TYPE_GROUP = "group"
TYPE_RIPPLE = "ripple"
TYPE_SNOW = "snow"
TYPE_RAIN = "rainlayer"
TYPE_UI3D = "ui3d"

--动画资源类型
AT_UNKNOWN = 0
AT_SKELETON = 1
AT_SKELETON_AS_ANIMSEQUENCE = 2
AT_ANIMSEQUENCE = 3
AT_MONTAGE = 4
AT_BLENDSPACE = 5
AT_BLENDSPACE1D = 6
AT_AIMOFFSETBLENDSPACE = 7
AT_ANIMTREE = 8

TYPE_LIST = {
"model",
"actor",
"light",
"effect",
"particle",
"decal",
"sound",
"reverb",
"trigger",
"light_probe",
"volume_fog",
"ripple",
"snow",
"rainlayer",
}

FORM_TREE_BROWSER = "common_form\\form_tree_browser"
SEARCH_PATH = "ini\\common_form\\form_tree_browser\\"
SEARCH_CONFIG = SEARCH_PATH .. "form_put_visual.ini"
SEARCH_CONFIG_TEMP = "cache\\common_form\\form_tree_browser\\form_put_visual_temp.ini"
GROUP_CONFIG = "ter\\visual_group.ini"

--获取visual_put文件
function get_search_file()
local ini = nx_create("IniDocument")

ini.FileName = nx_resource_path() .. SEARCH_CONFIG

if not ini:LoadFromFile() then
nx_destroy(ini)
return 0
end

local file = ini:ReadString("SEARCH_CONFIG", "search_file", "form_put_visual.ini")

nx_destroy(ini)

return nx_resource_path() .. SEARCH_PATH .. file, file
end

按照上述方法为该角色再绑定一个运动扭曲动画脚本

image-20230529155633335

脚本二

参考脚本:

motion_warping_character_animation.lua

--脚本模板

--------------------------
--局部效用函数--
--------------------------

require("public_attr")
require("scene_utils")

local function get_terrain(pawn)
if nx_is_valid(pawn) then
local lscene = pawn.Scene

if nx_is_valid(lscene) then
return scene_get_engine_terrain(lscene)
end
end

return nx_null()
end

local function get_movement_component(pawn)
if nx_is_valid(pawn) then
return pawn:GetMovementComponentID()
end

return nx_null()
end

local function is_moving(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)
if speed > 0.0 and movement:IsMovingOnGround() then
return true
end
end
end

return false
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 get_current_jump_count(pawn)
if nx_is_valid(pawn) then
return pawn.JumpCurrentCount
end

return 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_accelerating(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)

if nx_is_valid(movement) then
if math.abs(movement.CurrentAccelerationX) > 0.0
or math.abs(movement.CurrentAccelerationY) > 0.0
or math.abs(movement.CurrentAccelerationZ) > 0.0 then
return true
end
end
end

return false
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

local function get_foot_ik_weight(role)
local foot_ik_weight = -1.0

local foot_ik_id = role:GetCurveIDByNameInSkeleton("foot_ik")

if foot_ik_id >= 0 then
foot_ik_weight = role:GetCurveValue(foot_ik_id)
end

return foot_ik_weight
end

function transition_callback_IdleToJogStart(pawn, role, action_index, machine_index, prev_state, next_state)
if is_accelerating(pawn) and not is_jumping(pawn) and is_moving(pawn) then
return true
else
return false
end
end

function transition_callback_JogStartToJogStop(pawn, role, action_index, machine_index, prev_state, next_state)
if get_speed(pawn) < 1.0 then
return true
else
return false
end
end

function transition_callback_IdleToRun(pawn, role, action_index, machine_index, prev_state, next_state)
if get_speed(pawn) > 50.0 then
return true
else
return false
end
end

function transition_callback_RunToJogStop(pawn, role, action_index, machine_index, prev_state, next_state)
if not is_accelerating(pawn) then
return true
else
return false
end
end

function transition_callback_JogStopToIdle(pawn, role, action_index, machine_index, prev_state, next_state)
if is_accelerating(pawn) then
return true
else
return false
end
end

function transition_callback_JogStopToJogStart(pawn, role, action_index, machine_index, prev_state, next_state)
if is_accelerating(pawn) then
return true
else
return false
end
end

function transition_callback_IdleOrJogsToJumps(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_JumpsToJumpLand(pawn, role, action_index, machine_index, prev_state, next_state)
if not is_jumping(pawn) then
return true
else
return false
end
end

function transition_callback_JumpsToIdleOrJogs(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_JumpToSecondJump(pawn, role, action_index, machine_index, prev_state, next_state)
if is_jumping(pawn) and get_current_jump_count(pawn) > 1 then
return true
else
return false
end
end

function transition_callback_SecondJumpToThirdJump(pawn, role, action_index, machine_index, prev_state, next_state)
if is_jumping(pawn) and get_current_jump_count(pawn) > 2 then
return true
else
return false
end
end

function transition_callback_IdleToMultiDiectionMove(pawn, role, action_index, machine_index, prev_state, next_state)
if is_moving(pawn) then
return true
else
return false
end
end

function transition_callback_MultiDiectionMoveToIdle(pawn, role, action_index, machine_index, prev_state, next_state)
if not is_moving(pawn) then
return true
else
return false
end
end

transition_event_callback =
{
["IdleToJogStart"] = transition_callback_IdleToJogStart,
["JogStartToJogStop"] = transition_callback_JogStartToJogStop,
["IdleToRun"] = transition_callback_IdleToRun,
["RunToJogStop"] = transition_callback_RunToJogStop,
["JogStopToIdle"] = transition_callback_JogStopToIdle,
["JogStopToJogStart"] = transition_callback_JogStopToJogStart,
["IdleOrJogsToJumps"] = transition_callback_IdleOrJogsToJumps,
["JumpsToJumpLand"] = transition_callback_JumpsToJumpLand,
["JumpsToIdleOrJogs"] = transition_callback_JumpsToIdleOrJogs,
["JumpToSecondJump"] = transition_callback_JumpToSecondJump,
["SecondJumpToThirdJump"] = transition_callback_SecondJumpToThirdJump,

["IdleToMultiDiectionMove"] = transition_callback_IdleToMultiDiectionMove,
["MultiDiectionMoveToIdle"] = transition_callback_MultiDiectionMoveToIdle,

}

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)
if nx_is_valid(role) then
nx_log("state_machine_state_callback_EnterState event, role: " .. nx_string(role) .. " action_name: " .. role:GetActionName(action_index) .. ", machine index: " .. role:GetAnimTreeStateMachineName(action_index, machine_index) .. ", state name: " .. role:GetAnimTreeStateMachineStateName(action_index, machine_index, state_index))
end
end

function state_machine_state_callback_LeaveState(pawn, role, action_index, machine_index, state_index)
if nx_is_valid(role) then
nx_log("state_machine_state_callback_LeaveState event, role: " .. nx_string(role) .. " action_name: " .. role:GetActionName(action_index) .. ", machine index: " .. role:GetAnimTreeStateMachineName(action_index, machine_index) .. ", state name: " .. role:GetAnimTreeStateMachineStateName(action_index, machine_index, state_index))
end
end

function state_machine_state_callback_FullyBlendState(pawn, role, action_index, machine_index, state_index)
if nx_is_valid(role) then
nx_log("state_machine_state_callback_FullyBlendState event, role: " .. nx_string(role) .. " action_name: " .. role:GetActionName(action_index) .. ", machine index: " .. role:GetAnimTreeStateMachineName(action_index, machine_index) .. ", state name: " .. role:GetAnimTreeStateMachineStateName(action_index, machine_index, state_index))
end
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 false
end

local callback = state_machine_state_event_callback[callback_name]
if callback then
return callback(pawn, role, action_index, machine_index, state_index)
end

return
end

function has_fullbody_action(pawn)
return false
end

function animtree_callback_ShouldUseLayerBlend(pawn, role)
if not has_fullbody_action(pawn) and is_moving(pawn) then
return true
end

return false
end

function animtree_callback_CalcLeanAngle(pawn, role)
return 0.0
end

function animtree_callback_CalcSlopeAngle(pawn, role)
return 0.0
end

function animtree_callback_ShouldUseRHand(pawn, role)
if nx_is_valid(pawn) then
return nx_custom(pawn, "rhand_ik")
end

return false
end

function animtree_callback_GetPlayerWeaponState(pawn, role)

return 0
end

function animtree_callback_GetPlayerFightState(pawn, role)

return 0
end

function animtree_callback_IsPlayerMoving(pawn, role)
if is_moving(pawn) then
return true
end

return false
end

--动画树右脚IK节点回调
function animtree_callback_RRootIK(pawn, role)
local terrain = get_terrain(pawn)

if not nx_is_valid(terrain) then
return false
end

local foot_ik_weight = get_foot_ik_weight(role)

if foot_ik_weight < 0.0 then
--如果脚部IK权重小于0.0,则关闭脚部IK
return false
end

--右脚节点在模型空间中的位置
local obj_pos_x, obj_pos_y, obj_pos_z = role:GetNodeObjectPosition("foot_r")
local foot_rel_height = obj_pos_y

--右脚节点在世界空间中的位置
local position_x = 0.0
local position_y = 0.0
local position_z = 0.0
position_x, position_y, position_z = role:GetNodeWorldPosition("foot_r")

--世界空间中的地面高度
local groundheight = terrain:GetGroundHeight(position_x,position_z)

--右脚节点在世界空间中调整至地面
local strnewpos = nx_string(position_x) .. "," .. nx_string(groundheight+foot_rel_height) .. "," .. nx_string(position_z)

if (position_y-foot_rel_height) < groundheight or ((position_y-foot_rel_height)-groundheight) > 0.05 then
--右脚节点低于地面或高于地面0.05米,设置右脚IK参数
local action_index = get_anim_tree_index(role)

if action_index >= 0 then
role:SetAnimTreeValue(action_index, "footrpos", strnewpos)
role:SetAnimTreeValue(action_index, "foot_ik_weight", foot_ik_weight)
return true
end

return false
else
return false
end
end

function animtree_callback_LRootIK(pawn, role)
local terrain = get_terrain(pawn)

if not nx_is_valid(terrain) then
return false
end

local foot_ik_weight = get_foot_ik_weight(role)

if foot_ik_weight < 0.0 then
return false
end

local obj_pos_x, obj_pos_y, obj_pos_z = role:GetNodeObjectPosition("foot_l")
local foot_rel_height = obj_pos_y

local position_x = 0.0
local position_y = 0.0
local position_z = 0.0
position_x, position_y, position_z = role:GetNodeWorldPosition("foot_l")

local groundheight = terrain:GetGroundHeight(position_x,position_z)
local strnewpos = nx_string(position_x) .. "," .. nx_string(groundheight+foot_rel_height) .. "," .. nx_string(position_z)

if (position_y-foot_rel_height) < groundheight or ((position_y-foot_rel_height)-groundheight) > 0.05 then
local action_index = get_anim_tree_index(role)

if action_index >= 0 then
role:SetAnimTreeValue(action_index, "footlpos", strnewpos)
role:SetAnimTreeValue(action_index, "foot_ik_weight", foot_ik_weight)
return true
end

return false
else
return false

end
end

animtree_event_callback =
{
["ShouldUseLayerBlend"] = animtree_callback_ShouldUseLayerBlend,
["CalcLeanAngle"] = animtree_callback_CalcLeanAngle,
["CalcSlopeAngle"] = animtree_callback_CalcSlopeAngle,
["ShouldUseRHand"] = animtree_callback_ShouldUseRHand,
["RRootIK"] = animtree_callback_RRootIK,
["LRootIK"] = animtree_callback_LRootIK,
["GetPlayerWeaponState"] = animtree_callback_GetPlayerWeaponState,
["GetPlayerFightState"] = animtree_callback_GetPlayerFightState,
["IsPlayerMovingHorizontal"] = animtree_callback_IsPlayerMoving,
}

function on_animtree_event(mesh_comp, action_index, callback_name)
local callback = animtree_event_callback[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)
end

return false
end

local function get_current_time_scale(role)
local curve_id = role:GetCurveIDByNameInSkeleton("slowmo_time_scale")
--local curve_id = nx_custom(role, "slowmo_time_scale")
if curve_id < 0 then
--nx_log("invalid test_curve_id: " .. curve_id)
return
end

local curve_value = role:GetCurveValue(curve_id)
return curve_value
end

local function begin_bullut_time(pawn)
if nx_is_valid(pawn) then
local time_scale = 0.05

local world = nx_value("world")
world.GlobalTimeSpeed = time_scale
pawn.camera_rotated = true
end
end

local function end_bullut_time(pawn)
if nx_is_valid(pawn) then
local world = nx_value("world")
world.GlobalTimeSpeed = 1
pawn.camera_rotated = false
end
end

function on_action_event(mesh_comp, action_name, event_name, frame, info)

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

if event_name == "JumpOver" then

pawn.JumpAgainLock = true

elseif event_name == "JumpLand" then

pawn.JumpAgainLock = false

elseif event_name == "AttackHit" then
local terrain = nx_value("terrain")
local npc_story = nx_value("npc_story")

if nx_is_valid(terrain) then
local visual_table = terrain:GetRangeVisualList(role.PositionX, role.PositionZ, 2.0)
for k,v in pairs(visual_table) do
if nx_string(npc_story) == nx_string(v) then
npc_story.be_hurted = true
end
end
end
elseif event_name == "Melee_Start" then

begin_bullut_time(pawn)

elseif event_name == "Melee_End" then

end_bullut_time(pawn)

else
--nx_log("event_name = " .. event_name)
end

if event_name == "TRIGGER_ACTION_END" then
local movement_comp = pawn.CharacterMovement
--if movement_comp.MovementMode == nx_enum("MOVEMENT_MODE::Flying") then
local movement_mode = nx_enum("MOVEMENT_MODE::Walking")
movement_comp:SetMovementModeWithCustom(movement_mode, 0)
--end
end
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_action_event", "on_action_event")
nx_callback(mesh_comp, "on_state_machine_state_event", "on_state_machine_state_event")
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
mesh_comp.ActorCalleeType = "CComponentActorCalleeScript"
nx_bind_script(mesh_comp, nx_current(), "anim_callee_init")
end
end
end

--用于发布
function on_end_play(component)
end

scene_utils.lua

--场景utils脚本

function scene_get_engine_terrain(lscene)
local level = lscene.PersistentLevel

if nx_is_valid(level) then
local lterrain = level:GetGameObjectByType("fx_component_terrain.LTerrain")

if nx_is_valid(lterrain) then
return lterrain.EngineTerrain
end
end

return nx_null()
end

运行

点击运行按钮即可查看效果。

image-20230529160126958