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

文章目录

  • 简介
    • 变量说明
  • 实现
    • 动画准备
    • 动画状态机
    • 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Nginx】Nginx的学习(3.Nginx命令和nginx配置文件)

1. Nginx命令 1.1 启动nginx systemctl start nginx1.2 停止nginx systemctl stop nginx1.3 重载nginx # 重新加载配置文件 systemctl reload nginx1.4 查看nginx服务端口 netstat -anpl | grep nginx1.5 查看nginx进程 ps aux | grep nginx2. nginx的配置文件 2.1 查看…

git拉取github上的项目

git拉取github上的项目测试创建bash公钥&#xff0c;拉取代码1.先创建github账号和项目&#xff1b;系统安装git程序2.先配置ssh公钥,为了避免每次远程访问需要输密码&#xff0c;将使用ssh登陆。ssh应该与本机信息绑定&#xff0c;查看自己电脑 C:\Users\lenovo\.ssh 目录下是…

预训练语言模型(GPT,BERT)

文章目录GPT 模型预训练语言模型模型和学习BERT 模型去噪自编码器模型和学习模型特点References在自然语言处理中事先使用大规模语料学习基于 Transformer 等的语言模型&#xff0c;之后用于各种任务的学习和预测&#xff0c;称这种模型为预训练语言模型。代表性的模型有 BERT …

STA环境 - 时钟

目录1. 指定时钟create_clock1.1. 时钟延迟set_clock_latency 1.2. 时钟不确定度&#xff08;时钟抖动&#xff09;set_clock_uncertainty 1.3. 时钟过渡时间set_clock_transition 2. 衍生时钟create_generated_clock3. 划定时钟域set_clock_groupsSTA环境配置中对时钟如何约束…

【总结】爬虫4-selenium

爬虫4-selenium 1. selenium 基本操作 在使用selenium之前必须先配置浏览器对应版本的webdriver。才可以控制浏览器打开网页 1.1 创建浏览器对象 b Chrome()1.2 打开网页 &#xff08;需要哪个网页数据&#xff0c;就打开那个网页对应的网页地址&#xff09; b.get(https…

git 001--建本地仓库和远程仓库和拉代码

要使用Git对我们的代码进行管理&#xff0c;首先需要获得Git仓库。 获取Git仓库通常有两种方式&#xff1a; 在本地初始化Git仓库&#xff08;不常用&#xff09; 从远程仓库克隆&#xff08;常用&#xff09; 一.建本地仓库 方法一: 在自己电脑的任意目录下创建一个空目录…

字节测试总监,让我们用这份《测试用例规范》,再也没加班过

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 固然&#xff0c;编写一份好的测试用例需要&#xff1a;充分的需求分析能力 理论及经验加持&#xff0c;作为测试职场摸爬打滚的老人&…

为什么企业需要一个“企业办公浏览器”?

目前&#xff0c;大多数企业还在用着传统的Web浏览器&#xff0c;它是各行业企业办公最常用到的应用程序&#xff0c;搜索资料、打开其他应用工具、打开文档等等&#xff0c;企业员工几乎每天都在用它做这些工作。 但实际上&#xff0c;Web浏览器并不是一个企业专用的办公应用软…

炒黄金所需的k线图基础知识(上)

炒金&#xff0c;一般是指对杠杠式的黄金电子合约&#xff08;如伦敦金、黄金期货&#xff09;进行短线的多空操作&#xff0c;从中赚取波动价差的行为。无论投资者从事内盘还是外盘交易&#xff0c;K线图都是基础的、必备的知识。 1、什么叫K线图&#xff1f; K线图源于日本的…

计算机网络名词解释和简答题总结

名词解释 CSMA/CD&#xff08;载波监听多点接入/碰撞检测协议&#xff09; CSMA/CD是一种基于冲突检测的载波监听多路访问技术。CSMA/CD协议要求站点在发送数据之前先监听信道。如果信道空闲&#xff0c;站点就可以发送数据&#xff1b;如果信道忙&#xff0c;则站点不能发送…

使用CookieJar提取cookie信息

首先&#xff0c;推荐几个帖子&#xff0c;大伙可以先看看。国内通过cookiejar主要获取cookie的方法&#xff0c;大致都是如此的。 http.cookiejar库之CookieJar_pigYanYan的博客-CSDN博客 Python编程&#xff1a;cookiejar的使用_彭世瑜的博客-CSDN博客 再推荐一个资料帖&a…

Linux权限

Linux下有两种用户&#xff1a;超级用户(root)、普通用户。超级用户(root):可以在linux系统下做任何事&#xff0c;不受限制&#xff0c;只有1个。普通用户:在linux系统下做有限的事,有N个。超级用户的提示符#&#xff1b;普通用户的提示符$切换用户的命令:su切换root时可以直接…

MQ之kafka

一 概念 Kafka是最初由Linkedin公司开发&#xff0c;是一个分布式、支持分区的&#xff08;partition&#xff09;、多副本的&#xff08;replica&#xff09;&#xff0c;基于zookeeper协调的分布式消息系统&#xff0c;它的最大的特性就是可以实时的处理大量数据以满足各种需…

Visual Studio Code2023(VSCode2023)安装包下载及安装教程(最新版接入了chat GPT)

[软件名称]: Visual Studio Code2023 [软件大小]: 88.6 MB [安装环境]: Win11/Win10/Win7 [软件安装包下载]:https://pan.quark.cn/s/ee94a4aa2abc Visual Studio Code简称“VS Code”是Microsoft在2015年4月30日Build开发者大会上正式宣布一个运行于 Mac OS X、Windows和 Lin…

【Datawhale动手学深度学习笔记】多层感知机代码实践

多层感知机 激活函数 激活函数&#xff08;activation function&#xff09;通过计算加权和并加上偏置来确定神经元是否应该被激活&#xff0c; 它们将输入信号转换为输出的可微运算。 大多数激活函数都是非线性的。 由于激活函数是深度学习的基础&#xff0c;下面简要介绍一…

多线程进阶学习09------ThreadLocal详解

ThreadLocal&#xff1a;提供线程的局部变量&#xff0c;对于线程共享变量如果使用ThreadLocal则无需加锁&#xff0c;更省事省心。 ThreadLocal本地线程变量,线程自带的变量副本(实现了每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自…

FastReport .NET 2023.2.4 Crack

FastReport .NET Reporting and documents creation library for .NET 7 FastReport .NET适用于 .NET 7、.NET Core、Blazor、ASP.NET、MVC 和 Windows Forms 的全功能报告库。它可以在微软视觉工作室 2022 和 JetBrains Rider 中使用。 利用 .NET 7、.NET Core、Blazor、ASP.N…

React:九、组件的生命周期

1.生命周期的理解 组件从创建到死亡它会经历一些特定的阶段。React组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。我们在定义组件时&#xff0c;会在特定的生命周期回调函数中&#xff0c;做特定的工作。2.生命周期小案例 <!DOCTYPE html> <html…

操作系统权限维持(十五)之Linux系统-inetd远程后门

系列文章 操作系统权限维持&#xff08;一&#xff09;之Windows系统-粘贴键后门 操作系统权限维持&#xff08;二&#xff09;之Windows系统-克隆账号维持后门 操作系统权限维持&#xff08;三&#xff09;之Windows系统-启动项维持后门 操作系统权限维持&#xff08;四&…

leaflet加载GPX文件,第2种图形显示方法(119)

第119个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载GPX文件,将图形显示在地图上,这是另外一种方式,看前一种方式请从目录中查找。GPX文件是以GPS数据交换格式保存的GPS数据文件,是一种通用的地图信息文件,可以被众多GPS应用和Web服务更轻松地导入和…