写在前面
unity 之Final IK实际开发中的应用
-
需要 unity 开发工具、团结引擎
-
需要下载 Rider 开发工具开发(当然也可以用 vs studio)
-
需要导入Final IK插件
-
Final ik的下载网址
https://gitcode.com/open-source-toolkit/87bcc -
需要俩种设计模式:单例模式、外观模式
final IK 这个插件是一个让人头疼但又让人特别喜爱的插件,让人头疼的是这个插件官方的教程很老,而网上也没有人把这个插件讲的特别清楚,全是一些基础实例,请问基础实例能用在项目里吗?真是无语。接下来我通过官方视频的总结,帮友友们快速入门这款插件,
入门后我将在我实际项目中带友友们认识这款插件并且封装,使用,集成。
为什么要使用final IK插件,他有什么作用?
final Ik的强大之处在于它可以轻松去控制人拿起一个物体,控制人物的手指,控制人物的看向某个物体,接合动画做出更加逼真的,真实的效果。
一. final IK 交互系统的基础
final ik 一个重要的核心脚本 full Body Biped IK
这个脚本保证简单来说就是来保证final ik把人物的每个身体的每个部位都动态绑定调整关节权重,当某个部位移动或旋转所有部位都跟着移动或旋转。
当添加了这个脚本,就会自动绑定对应的部位骨骼,如图
使用代码动态绑定
FullBodyBipedIK ik = gameObject.AddComponent<FullBodyBipedIK>();
// 自动检测骨骼引用
BipedReferences references = new BipedReferences();
BipedReferences.AutoDetectReferences(ref references, transform, BipedReferences.AutoDetectParams.Default);
// 设置骨骼引用到FBBIK
ik.SetReferences(references, null);
** 注意点:有时候导入的模型不适配Final IK 需要动态调整骨骼的旋转,位置,使其变成蓝色点**
常用的主要是调节手或者腿的位置和旋转的权重,使其变化位置,而target 为设置一个目标位置,当目标位置及旋转发生变化,胳膊的旋转和位置也发生变化
二. Ami Ik使用
Ami Ik 我的理解是人物看向某一个点(或者跟踪某一个点),当这个点在的位置在变化,他的头部,胳膊,绑定的骨骼也在发生变化
2.1 先绑定脚本
2.2 绑定要跟随移动的骨骼
运行,这个发现人物的头是歪的,(解决方法是需要绑定一个人物的低头动画)具体可以看这个视频的3分40秒:
https://www.youtube.com/watch?v=wT8fViZpLmQ&list=PLVxSIA1OaTOu8Nos3CalXbJ2DrKnntMv6&index=3
2.3 Clamp weight属性
这个属性是值当跟随目标点他的旋转角度,当设为0.5的时候他只能已自身90度的夹角旋转
2.4 Pole Target属性
这个属性代表跟随任意点,相应骨骼发生变化
剩下一些属性不常用默认就好
三. Ik交互系统
ik交互系统代表人物的手部及身体和物体交互,比如说拿起一个物体,放下一个物体,扔掉某个物体,都是交互系统的一部分
3.1 认识人物效应器 InteractionSystem脚本(看似有很多参数,实则实际用到的只有FullBody,Character Collider)
FullBody : 绑定交互骨骼系统full Body Biped IK
Character Collider: 绑定需要交互的物体
3.2 创建交互物体
3.3 为交互物体绑定交互脚本 Interaciton Object
配置权重曲线 Weight Curves
🤔:权重曲线是干嘛的?
我也是研究了好久才知道,配置权重曲线其实是影响的角色身上的手臂身上的权重(如果交互物体绑定的是左手那么便是影响的左手的的位置权重)
这条线的意思是当人物拿到物体位置权重慢慢加大最大值1,拿回物体时,人物位置权重改为0骨骼位置也随之变为初始状态
配置权重比 Muitipliers
🤔:配置权重比又是干嘛的?
权重比意思是当角色的手部位置的权重发生变化,那么相应的手指的权重也跟随发生变化(这里到时候要调节手部抓住球的手指位置,所以需要手指比)
配置事件 event
时间: 一般设置为0.5
暂停: 角色拿到物体后停止不动
pickUp: 拿到物体后角色位置权重变为0,回复手臂初始位置
Animations:物体的动画触发器,比如是个按钮,当人物的手触碰到他的时候出发按下动画(我实际项目并未用到这个参数。。。。)
Message:当人物拿到物体时候触发的回调函数
回调函数的脚本必须绑定在角色身上,不然不触发(坑死。。。。)
Unity event 跟我们平时用到的event事件一样(如unity按钮的触发event)
🤔 如何去使用这俩个脚本来让人物拿到这个物体呢?请看我表演
- 将交互物体绑定在角色InteractionSystem脚本上
- 设置人的手臂放到球面上(这里又个小技巧,就是在复制一个相同的人来改变他骨骼的手臂位置)
2.1 先找到🫲左手
2.2 改变他左手的位置然他放到球面上
2.3 复制这只手放到交互物体的子级
2.4 删除刚刚复制的人物在原来角色的左手上添加Host Poser脚本
🤔:这个脚本是干嘛的?
这个脚本其实就是来控制手部的手指权重的 而poser root是用来绑定交互物体手部骨骼的
其实就是物体的身上有了角色的手部位置,当人物要拿物体时候先去找这只手找到了位置,他相应的手部也发生了变化跟踪到物体
2.5 人物绑定物体脚本
using System;
using System.Collections;
using System.Collections.Generic;
using RootMotion.FinalIK;
using Unity.VisualScripting;
using UnityEngine;
public class InteractionTestUI : MonoBehaviour
{
public InteractionSystem InteractionSystem;
public InteractionObject InteractionObject;
private void OnGUI()
{
if (GUILayout.Button("拿起小球"))
{
InteractionObject.Initiate();
InteractionSystem.StartInteraction(FullBodyBipedEffector.LeftHand, InteractionObject, true);
}
}
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
被拿到的物体放到了手部骨骼下
🤔 拿到小球后又是如何改变人物手部的位置呢?
需要利用offset Pose脚本
利用拿到物体后的回调函数这样就可以让人物拿到物体的时候改变他手部(左手骨骼的位置)
. 实战
🤔 如何使用Final Ik运用到项目中让他为我所用嘞,我来解密
需求1. 让一个人物的左手拿起一副牌左手的骨骼发生变化?
- 介绍我拆分的目录结构
- 思路
2.1 先使用设计模式门面模式将Ik主系统抽离出来挂载到一个空物体上
在使用单例模式把每个小模块拆分成独立的部分比如说拿牌(这里因为一些保密原因不与展示具体实现)
调整拿到牌后手臂的位置
当拿到牌后触发回调
执行拿牌的手部动画(这里的动画系统其实也是使用外观模式封装的)
小技巧拿牌的手部、和具体手部的参数可以抽离成俩个预制件代码中直接加载这个预制件就行
实现具体步骤
- 绑定手部调整手部位置设置预制件
- 抽离Final Ik主系统方便调用
- 调用拿牌函数,复制对应的交互物参数
- 绑定offect poser修改手部骨骼位置
- 拿到物体后,回调函数播放动画
封装的工具类
/*
*┌────────────────────────────────────────────────┐
*│ 描 述:配置全局IK工具类
*│ 作 者:张腾飞
*│ 版 本:1.0
*│ 创建时间:2025/3/27 10:33
*└────────────────────────────────────────────────┘
*/
using RootMotion.FinalIK;
using UnityEngine;
using UtillSyc.PEUnityInternal;
namespace FinalIkSyc.UtilIK
{
/// <summary>
/// 交互对象组件参数
/// </summary>
public struct InteractionObjectConfig
{
/// <summary>
/// 目标对象,需要添加 InteractionObject 的 Transform
/// </summary>
public Transform TargetObject;
/// <summary>
/// IKCopyObject 的资源路径
/// </summary>
public string IkCopyPath;
/// <summary>
/// 事件消息的回调函数名称
/// </summary>
public string MessageFunction;
/// <summary>
/// 事件消息的接收者
/// </summary>
public GameObject MessageRecipient;
/// <summary>
/// 是否暂停事件
/// </summary>
public bool PauseEvent;
/// <summary>
/// 是否拾取
/// </summary>
public bool PickUp;
}
public static class PeFinalIKHelper
{
/// <summary>
/// 动态配置交互对象组件
/// </summary>
/// <param name="config">包含配置参数的结构体</param>
/// <returns>配置完成的 InteractionObject,如果失败返回 null</returns>
public static InteractionObject ConfigureInteractionObject(InteractionObjectConfig config)
{
// 验证配置参数
if (config.TargetObject == null)
{
Debug.LogError("目标对象为空,无法添加 InteractionObject");
return null;
}
if (string.IsNullOrEmpty(config.IkCopyPath))
{
Debug.LogError("IKCopyObject 路径为空,无法加载资源");
return null;
}
// 添加 InteractionObject 组件
InteractionObject interactionObject = PeUnityInternal.GetOrAddComponent<InteractionObject>(config.TargetObject.gameObject);
if (interactionObject == null)
{
Debug.LogError("未能成功添加 InteractionObject 组件到目标对象");
return null;
}
// 加载 IKCopyObject 资源
GameObject ikCopyObject = PeUnityInternal.GetRes<GameObject>(config.IkCopyPath);
if (ikCopyObject == null)
{
Debug.LogError($"未能加载 IKCopyObject 资源,请检查路径是否正确: {config.IkCopyPath}");
return null;
}
// 获取 IKCopyObject 上的 InteractionObject 组件
InteractionObject interactionIkCopyObject = PeUnityInternal.GetOrAddComponent<InteractionObject>(ikCopyObject);
if (interactionIkCopyObject == null)
{
Debug.LogError("IKCopyObject 上未能获取 InteractionObject 组件");
return null;
}
// 复制配置
interactionObject.weightCurves = interactionIkCopyObject.weightCurves;
interactionObject.multipliers = interactionIkCopyObject.multipliers;
interactionObject.events = interactionIkCopyObject.events;
// 配置事件
if (interactionObject.events.Length > 0)
{
interactionObject.events[0].pause = config.PauseEvent;
interactionObject.events[0].pickUp = config.PickUp;
if (interactionObject.events[0].messages.Length > 0)
{
interactionObject.events[0].messages[0].function = config.MessageFunction;
interactionObject.events[0].messages[0].recipient = config.MessageRecipient;
}
else
{
Debug.LogError("当前无 messages 消息");
}
}
else
{
Debug.LogError("当前无 event 事件");
}
return interactionObject;
}
}
}