Unity Avatar Cover System - 如何实现一个Avatar角色的智能掩体系统

news2024/7/4 5:54:09

文章目录

  • 简介
    • 变量说明
  • 实现
    • 动画准备
    • 动画状态机
    • State 状态
      • None
      • Stand To Cover
      • Is Covering
      • Cover To Stand
    • 高度适配
      • 高度检测
      • 脚部IK


简介

本文介绍如何在Unity中实现一个Avatar角色的智能掩体系统,效果如图所示:

效果图(一)

效果图(二)

效果图(三)

效果图(四)

初版1.0.0代码已上传至SKFramework框架Package Manager中:

SKFramework PackageManager

变量说明

Avatar Cover System

  • Cover Layer Mask:掩体物体的Layer层级
  • Shortcut Key:进入、退出掩体状态的快捷键
  • Box Cast Size:寻找掩体所用物理检测的Box大小
  • Box Cast Num:寻找掩体所用物理检测的Box数量(maxDistance = boxCastSize * boxCastNum)
  • Stand 2 Cover Speed:切换至掩体状态的移动速度
  • Cover 2 Stand Speed:退出掩体状态的移动速度
  • Stand 2 Cover Time:切换至掩体状态的时长(动画时长决定)
  • Cover 2 Stand Time:退出掩体状态的时长(动画时长决定)
  • Sneak Speed:掩体状态下移动的速度
  • Direction Lerp Speed:左右方向的插值速度
  • Head Radius:头部的半径 用于物理检测 未检测到碰撞时身体高度向下调整 并启用脚部IK
  • Head Down Cast Count Limit:头部下方物理检测的次数限制(每次下降一个半径的单位进行检测)
  • Ground Layer Mask:地面的Layer层级 用于脚部IK检测地面
  • Body Position Lerp Speed:身体高度插值的速度
  • Foot Position Lerp Speed:脚部IK插值的速度
  • Raycast Distance:脚部IK检测用的距离
  • Raycast Origin Height:脚部IK检测的高度

实现

动画准备

  • Mixamo:动作文件全部是在Mixamo网站上下载的:

Mixamo

  • Humanoid:Animation Type设为Humanoid人形动画:

Animation Type:Humanoid

  • Animation:调整相关设置:

Animation Settings

Root Transform Rotation Offset:此处设为-180,目的为了调整朝向,使其与Stand2Cover、Cover2Stand等动画连贯。

动画状态机

  • Animator Parameters:添加相关参数:
    • Stand2Cover:bool类型,用于进入、退出掩体状态;
    • Cover Direction:float类型,用于控制左右方向的混合树;
    • Cover Sneak:float类型,用于控制移动的混合树。
  • Sub-State Machine:创建一个子状态机,用于处于Cover相关状态:

Sub-State Machine

Cover子状态机中添加Stand2CoverCover2Stand动画状态及Cover Direction混合树:

Cover State Machine

Cover Direction混合树:包含Cover LeftCover Right子混合树,两个子混合树又分别包含其对应方向的IdleSneak动画。Cover Direction参数用于控制进入Cover Left还是Cover Right,Cover Sneak参数用于控制Idle和Sneak之间的混合:

Cover Direction

  • IK Pass:启用对应层级的IK Pass通道,计算脚部IK所需:

IK Pass

State 状态

定义相关状态:

  • None:未在任何状态;
  • Stand2Cover:正在切换至掩体状态(切换过程)
  • IsCovering:正处于掩体状态
  • Cover2Stand:正在退出掩体状态(切换过程)
public enum State
{
    None, //未在任何状态
    Stand2Cover, //正在切换至掩体状态
    IsCovering, //正处于掩体状态
    Cover2Stand, //正在退出掩体状态
}
//当前状态
private State state = State.None;

/// <summary>
/// 当前状态
/// </summary>
public State CurrentState
{
    get
    {
        return state;
    }
}

None

未处于任何状态时,向身体前方进行BoxCast物理检测寻找掩体,当检测到掩体时,按下指定快捷键则进入Stand2Cover切换过程:

//未处于掩体状态
case State.None:
    {
    //Box检测的中心点
    Vector3 boxCastCenter = transform.position + transform.up;
    //最大检测距离
    float maxDistance = boxCastSize.z * boxCastNum;
    //向身体前方进行Box检测 寻找掩体 
    castResult = Physics.BoxCast(boxCastCenter, boxCastSize * .5f, transform.forward, out hit, transform.rotation, maxDistance, coverLayerMask);
    //调试:法线方向
    Debug.DrawLine(hit.point, hit.point + hit.normal, Color.magenta);

    //检测到掩体
    if (castResult)
    {
        //按下快捷键 进入掩体状态
        if (Input.GetKeyDown(shortcutKey))
        {
            //正在切换至掩体状态
            state = State.Stand2Cover;
            //播放动画
            animator.SetBool(AnimParam.Stand2Cover, true);
            //禁用其他人物控制系统
            GetComponent<AvatarController>().enabled = false;
            //默认右方(动画Stand2Cover默认右方)
            targetCoverDirection = 1f;
            //启用脚部IK
            enableFootIk = true;
            bodyYOffset = 0.04f;
        }
    }
}
break;

Stand To Cover

切换至掩体状态的过程中,向RaycastHit中的法线反方向移动,移动到掩体前方:

case State.Stand2Cover:
{
    //计时
    stand2CoverTimer += Time.deltaTime;
    if (stand2CoverTimer < stand2CoverTime)
    {
        //向法线反方向移动 到掩体前
        cc.Move(-hit.normal * Time.deltaTime * stand2CoverSpeed);
        //朝向 面向法线方向
        transform.forward = Vector3.Lerp(transform.forward, -hit.normal, Time.deltaTime * stand2CoverSpeed);
    }
    else
    {
        //重置计时器
        stand2CoverTimer = 0f;
        //切换完成 进入掩体状态
        state = State.IsCovering;
        bodyYOffset = 0.02f;
    }
}
break;

Stand To Cover

Is Covering

在掩体状态时,获取用户Horizontal水平方向上的输入,通过输入控制Avatar转向左侧或右侧并进行Sneak移动:

//获取水平方向输入
float horizontal = Input.GetAxis("Horizontal");
//目标方向 输入为负取-1 为正取1
if (horizontal != 0f)
{
    targetCoverDirection = horizontal < 0f ? -1f : 1f;
    castResult = Physics.BoxCast(transform.position + transform.up, boxCastSize * .5f, transform.forward, out hit, Quaternion.identity, boxCastSize.z * boxCastNum, coverLayerMask);
    Debug.DrawLine(hit.point, hit.point + hit.normal, Color.magenta);
    cc.Move(-hit.normal * sneakSpeed * Time.deltaTime);
    transform.forward = Vector3.Lerp(transform.forward, -hit.normal, Time.deltaTime * stand2CoverSpeed);
}
//方向插值运算
coverDirection = Mathf.Lerp(coverDirection, targetCoverDirection, Time.deltaTime * directionLerpSpeed);
//动画 方向
animator.SetFloat(AnimParam.CoverDirection, coverDirection);
//动画 掩体状态行走
animator.SetFloat(AnimParam.CoverSneak, Mathf.Abs(horizontal));
//通过输入控制移动
cc.Move(horizontal * sneakSpeed * Time.deltaTime * transform.right);

Cover Left/Right

按下快捷键时,退出掩体状态:

animator.SetBool(AnimParam.Stand2Cover, false);
state = State.Cover2Stand;

Cover To Stand

退出掩体状态的过程中,向身体后方移动:

//计时
cover2StandTimer += Time.deltaTime;
cover2StandTimer = Mathf.Clamp(cover2StandTimer, 0f, cover2StandTime);
if (cover2StandTimer < cover2StandTime)
{
    //后移
    cc.Move(cover2StandSpeed * Time.deltaTime * -transform.forward);
}
else
{
    //重置计时器
    cover2StandTimer = 0f;
    state = State.None;
    //启用其他人物控制脚本
    GetComponent<AvatarController>().enabled = true;
}

Cover To Stand

高度适配

如图所示,当掩体的高度降低时,角色会逐渐下蹲调整高度,实现该功能一方面需要在头部进行物理检测,另一方面需要启用脚部的IK。

高度检测

高度检测

高度检测贯穿于Stand2CoverIsCovering状态中,注意观察下图中红色球的变动,当SphereCast球形检测在初始高度未检测到掩体时,会下降一个球半径的单位再次进行检测,如果在限制次数中都未检测到掩体,则退出掩体状态,如果检测到掩体,则获取碰撞点和初始高度的delta差值,该差值就是身体要下降的高度:

高度检测

//头部物理检测的初始点
headSphereCastOrigin = transform.position + Vector3.up * headOriginPosY + transform.right * targetCoverDirection * headRadius * 2f;
//向前方进行球形检测(掩体状态下前方就是后脑勺的方向)
headCastResult = Physics.SphereCast(headSphereCastOrigin, headRadius, transform.forward, out RaycastHit headHit, coverLayerMask);
int i = 0;
if (!headCastResult)
{
    for (i = 0; i < headDownCastCountLimit; i++)
    {
        //每次下降一个半径的单位进行检测
        headSphereCastOrigin -= Vector3.up * headRadius;
        headCastResult = Physics.SphereCast(headSphereCastOrigin, headRadius, transform.forward, out headHit, coverLayerMask);
        if (headCastResult) break;
    }
}
if (headCastResult)
{
    Debug.DrawLine(headSphereCastOrigin, headHit.point, Color.green);
    float delta = headOriginPosY - headHit.point.y;
    targetBodyPositionY = originBodyPositionY - delta - headRadius;
    Debug.DrawLine(headSphereCastOrigin, headSphereCastOrigin - Vector3.up * (delta + i * headRadius), Color.red);
}

检测的位置受Cover Direction方向影响,当处于Cover Left时,会在头部左侧一定单位进行检测,相反,处于Cover Right时,会在头部右侧一定单位进行检测:

高度检测的左右方向

获取到身体要下降的高度后,在OnAnimatorIK函数中调整Animator组件的bodyPosition属性:

Vector3 bodyPosition = animator.bodyPosition;
bodyPosition.y = Mathf.Lerp(lastBodyPositionY, targetBodyPositionY, bodyPositionLerpSpeed);
animator.bodyPosition = bodyPosition;
lastBodyPositionY = animator.bodyPosition.y;

脚部IK

单纯的下调身体高度会导致脚穿模到地面以下,因此需要启用脚部IK,不断调整脚的位置,脚部IK在前面的文章中有介绍,这里不再详细说明,代码如下:

private void FixedUpdate()
{
    //未启用FootIK or 动画组件为空
    if (!enableFootIk || animator == null) return;

    #region 计算左脚IK
    //左脚坐标
    leftFootPosition = animator.GetBoneTransform(HumanBodyBones.LeftFoot).position;
    leftFootPosition.y = transform.position.y + raycastOriginHeight;

    //左脚 射线检测
    leftFootRaycast = Physics.Raycast(leftFootPosition, Vector3.down, out RaycastHit hit, raycastDistance + raycastOriginHeight, groundLayerMask);
    if (leftFootRaycast)
    {
        leftFootIkPosition = leftFootPosition;
        leftFootIkPosition.y = hit.point.y + bodyYOffset;
        leftFootIkRotation = Quaternion.FromToRotation(transform.up, hit.normal);
#if UNITY_EDITOR
        //射线
        Debug.DrawLine(leftFootPosition, leftFootPosition + Vector3.down * (raycastDistance + raycastOriginHeight), Color.yellow);
        //法线
        Debug.DrawLine(hit.point, hit.point + hit.normal * .5f, Color.cyan);
#endif
    }
    else
    {
        leftFootIkPosition = Vector3.zero;
    }
    #endregion

    #region 计算右脚IK
    //右脚坐标
    rightFootPosition = animator.GetBoneTransform(HumanBodyBones.RightFoot).position;
    rightFootPosition.y = transform.position.y + raycastOriginHeight;
    //右脚 射线检测
    rightFootRaycast = Physics.Raycast(rightFootPosition, Vector3.down, out hit, raycastDistance + raycastOriginHeight, groundLayerMask);
    if (rightFootRaycast)
    {
        rightFootIkPosition = rightFootPosition;
        rightFootIkPosition.y = hit.point.y + bodyYOffset;
        rightFootIkRotation = Quaternion.FromToRotation(transform.up, hit.normal);

#if UNITY_EDITOR
        //射线
        Debug.DrawLine(rightFootPosition, rightFootPosition + Vector3.down * (raycastDistance + raycastOriginHeight), Color.yellow);
        //法线
        Debug.DrawLine(hit.point, hit.point + hit.normal * .5f, Color.cyan);
#endif
    }
    else
    {
        rightFootIkPosition = Vector3.zero;
    }
    #endregion
}
#region 应用左脚IK
//权重
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1f);

Vector3 targetIkPosition = animator.GetIKPosition(AvatarIKGoal.LeftFoot);
if (leftFootRaycast)
{
    //转局部坐标
    targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
    Vector3 world2Local = transform.InverseTransformPoint(leftFootIkPosition);
    //插值计算
    float y = Mathf.Lerp(lastLeftFootPositionY, world2Local.y, footPositionLerpSpeed);
    targetIkPosition.y += y;
    lastLeftFootPositionY = y;
    //转全局坐标
    targetIkPosition = transform.TransformPoint(targetIkPosition);
    //当前旋转
    Quaternion currRotation = animator.GetIKRotation(AvatarIKGoal.LeftFoot);
    //目标旋转
    Quaternion nextRotation = leftFootIkRotation * currRotation;
    animator.SetIKRotation(AvatarIKGoal.LeftFoot, nextRotation);
}
animator.SetIKPosition(AvatarIKGoal.LeftFoot, targetIkPosition);
#endregion

#region 应用右脚IK
//权重
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1f);
targetIkPosition = animator.GetIKPosition(AvatarIKGoal.RightFoot);
if (rightFootRaycast)
{
    //转局部坐标
    targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
    Vector3 world2Local = transform.InverseTransformPoint(rightFootIkPosition);
    //插值计算
    float y = Mathf.Lerp(lastRightFootPositionY, world2Local.y, footPositionLerpSpeed);
    targetIkPosition.y += y;
    lastRightFootPositionY = y;
    //转全局坐标
    targetIkPosition = transform.TransformPoint(targetIkPosition);
    //当前旋转
    Quaternion currRotation = animator.GetIKRotation(AvatarIKGoal.RightFoot);
    //目标旋转
    Quaternion nextRotation = rightFootIkRotation * currRotation;
    animator.SetIKRotation(AvatarIKGoal.RightFoot, nextRotation);
}
animator.SetIKPosition(AvatarIKGoal.RightFoot, targetIkPosition);
#endregion

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/393962.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据库系统-关系模式

二、DB的抽象与演变 2.1 DB的标准结构 DBMS管理数据的三个层次 ● (局部模式)Entrenal Level 外部层次 User Level 用户层次 ○ 用户能够看到雨处理的数据&#xff0c;全局数据中的某一部分 ● (全局模式)Conceptual Level 概念层次 Logic Level 逻辑层次 ○ 从全局角度理解…

【Linux】编译器gcc g++和调试器gdb的使用

文章目录1.编译器gcc/g1.1C语言程序的翻译过程1.预处理2.编译3.汇编4. 链接1.2 链接方式与函数库1.动态链接与静态链接2.动态库与静态库1.3 gcc与g的使用2.调试器gdb2.1debug和release2.2gdb的安装2.3gdb的使用2.4gdb的常用指令3.总结1.编译器gcc/g 1.1C语言程序的翻译过程 1…

一个ELF文件分析和逆向的过程

CrackMe1、2分析和逆向的过程 1. CrackMe1、2相关信息 CrackMe1 1、CrackMe1是一个ELF可执行文件&#xff0c;可在Android中独立执行 2、通过adb(Android SDK中默认带有adb工具)将CrackMe1 push到远程Android设备中&#xff0c;建议放在/data/local/tmp目录下 3、打开adb shel…

关于MSVCR100.dll、MSVCR100d.dll、Msvcp100.dll、abort()R6010等故障模块排查及解决方法

一、常见故障介绍  最近在开发相机项目&#xff08;项目细节由于公司保密就不介绍了&#xff09;&#xff0c;程序运行5个来月以来首次出现msvcr100.dll故障等问题&#xff0c;于是乎开始了分析之路&#xff0c;按照度娘上的一顿操作&#xff0c;期间也是出现了各种不一样的问…

Lombok常见用法总结

目录一、下载和安装二、常见注释&#xff08;一&#xff09;Data&#xff08;二&#xff09;Getter和Setter&#xff08;三&#xff09;NonNull和NotNull&#xff08;不常用&#xff09;&#xff08;四&#xff09;ToString&#xff08;不常用&#xff09;&#xff08;五&#…

一款丧心病狂的API测试工具:Apifox!

你好&#xff0c;我是测试开发工程师——凡哥。欢迎和我交流测试领域相关问题&#xff08;测试入门、技术、python交流都可以&#xff09; 我们平时在做接口测试的时候&#xff0c;对于一些常用的接口测试工具的使用应该都非常熟悉了&#xff1a; 接口文档&#xff1a;Swagge…

Databend 开源周报 第 83 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.com 。Whats New探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。Support for WebHDFSHDFS 是大数…

ArrayList集合底层原理

ArrayList集合底层原理ArrayList集合底层原理1.介绍2.底层实现3.构造方法3.1集合的属性4.扩容机制5.其他方法6.总结ArrayList集合底层原理 1.介绍 ​ ArrayList是List接口的可变数组的实现。实现了所有可选列表操作&#xff0c;并允许包括 null 在 内的所有元素。 每个 Array…

静态库和动态库的打包与使用

静态库和动态库 静态库和动态库的打包 生成可执行程序时链接使用 运行可执行程序时加载使用 提前声明&#xff0c;笔者示例的文件有mian.c/child.c/child.h。OK&#xff0c;我们先了解一下&#xff0c;库文件是什么&#xff1f;它其实就是打包了一堆实现常用功能的代码文件. ⭐…

Python之re库用法细讲

文章目录前言一、使用 re 模块的前期准备工作二、使用 re 模块匹配字符串1. 使用 match() 方法进行匹配2. 使用 search() 方法进行匹配3. 使用 findall() 方法进行匹配三、使用 re 模块替换字符串四、使用 re 模块分割字符串总结前言 在之前的博客中我们学习了【正则表达式】的…

C++ typedef用法详解

typedef的4种常见用法&#xff1a;给已定义的变量类型起个别名定义函数指针类型定义数组指针类型为复杂的声明定义一个新的简单的别名总结一句话&#xff1a;“加不加typedef&#xff0c;类型是一样的"&#xff0c;这句话可以这样理解&#xff1a;没加typedef之前如果是个…

云原生架构设计原则及典型技术

云原生是面向云应用设计的一种思想理念&#xff0c;充分发挥云效能的最佳实践路径&#xff0c;帮助企业构建弹性可靠、松耦合、易管理可观测的应用系统&#xff0c;提升交付效率&#xff0c;降低运维复杂度。代表技术包括不可变基础设施、服务网格、声明式 API 及 Serverless 等…

Apk加固后多渠道打包

之前一直使用360加固宝进行apk的加固打包&#xff0c;可以一键加固并打多渠道打包。但是&#xff0c;现在360加固宝收费了&#xff0c;在进行加固&#xff0c;多渠道打包&#xff0c;就得一步一步自己操作了&#xff0c;会很繁琐。所以&#xff0c;本文使用 360加固美团Wallet …

c++11 标准模板(STL)(std::unordered_map)(五)

定义于头文件 <unordered_map> template< class Key, class T, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator< std::pair<const Key, T> > > class unordered…

Java开发 - 单点登录初体验(Spring Security + JWT)

目录​​​​​​​ 前言 为什么要登录 登录的种类 Cookie-Session Cookie-Session-local storage JWT令牌 几种登陆总结 用户身份认证与授权 创建工程 添加依赖 启动项目 Bcrypt算法的工具 创建VO模型类 创建接口文件 创建XML文件 补充配置 添加依赖 添加配…

凭一部手机,7天赚20万?会剪辑的人有多吃香!

影视剪辑容易遇到哪些问题&#xff1a; 1、视频格式格式不对&#xff0c;剪辑软件不支持&#xff1b; 2、视频封面不会做&#xff1b; 3、PR导出视频时&#xff0c;没办法做其他事&#xff0c;效率不高&#xff1b; 4、自己配音不好听&#xff0c;配音软件又不好找&#xff1b;…

第14章 局部波动率模型

这学期会时不时更新一下伊曼纽尔德曼&#xff08;Emanuel Derman&#xff09; 教授与迈克尔B.米勒&#xff08;Michael B. Miller&#xff09;的《The Volatility Smile》这本书&#xff0c;本意是协助导师课程需要&#xff0c;发在这里有意的朋友们可以学习一下&#xff0c;思…

影响redis性能的一些潜在因素

影响 Redis 性能的 5 大方面的潜在因素&#xff0c;分别是&#xff1a; Redis 内部的阻塞式操作&#xff1b; CPU 核和 NUMA 架构的影响&#xff1b; Redis 关键系统配置&#xff1b; Redis 内存碎片&#xff1b; Redis 缓冲区。 先学习了解下 Redis 内部的阻塞式操作以及应对的…

【数据架构系列-03】数据仓库、大数据平台、数据中台... 我不太认同《DataFun数据智能知识地图》中的定义

关注DataFunTalk有2年多了&#xff0c;DataFun确实像创始人王大川讲的那样&#xff0c;践行选择、努力和利他原则&#xff0c;专注于大数据、人工智能技术应用的分享与交流&#xff0c;秉承着开源开放的精神&#xff0c;免费的共享了很多有营养的行业实践专业知识&#xff0c;对…

1.win10环境搭建Elasticsearch7.2.0环境

环境介绍jdk1.8安装Elasticsearch7.2.0下载安装包直接解压进入到bin目录&#xff0c;双击elasticsearch.bates启动成功访问http://localhost:9200/jdk版本1.8,很有可能因为jdk版本的问题es启动失败支持连接https://www.elastic.co/cn/support/matrix#matrix_jvm安装Kibana7.2.0…