Unity Avatar Cover System - 如何实现一个Avatar角色的智能掩体系统
文章目录
- 简介
- 变量说明
 
- 实现
- 动画准备
- 动画状态机
- State 状态
- None
- Stand To Cover
- Is Covering
- Cover To Stand
 
- 高度适配
- 高度检测
- 脚部IK
 
 
简介
本文介绍如何在Unity中实现一个Avatar角色的智能掩体系统,效果如图所示:




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

变量说明

- 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网站上下载的:

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

- Animation:调整相关设置:

Root Transform Rotation Offset:此处设为-180,目的为了调整朝向,使其与Stand2Cover、Cover2Stand等动画连贯。
动画状态机
- Animator Parameters:添加相关参数:- Stand2Cover:bool类型,用于进入、退出掩体状态;
- Cover Direction:float类型,用于控制左右方向的混合树;
- Cover Sneak:float类型,用于控制移动的混合树。
 
- Sub-State Machine:创建一个子状态机,用于处于Cover相关状态:

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

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

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

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;//启用脚部IKenableFootIk = 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;

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);

按下快捷键时,退出掩体状态:
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;
}

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

高度检测
高度检测贯穿于Stand2Cover和IsCovering状态中,注意观察下图中红色球的变动,当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
相关文章:
 
Unity Avatar Cover System - 如何实现一个Avatar角色的智能掩体系统
文章目录简介变量说明实现动画准备动画状态机State 状态NoneStand To CoverIs CoveringCover To Stand高度适配高度检测脚部IK简介 本文介绍如何在Unity中实现一个Avatar角色的智能掩体系统,效果如图所示: 初版1.0.0代码已上传至SKFramework框架Package…...
 
steam/csgo搬砖项目到底真的假的?
搬砖是从国外steam市场置办游戏装备回来,在国内网易buff售卖,低买高卖,产生利润的一个项目。 但我真正上手后,才知道steam是面向全球的游戏平台,用户真的大的夸张!!市场非常巨大,一…...
 
【Python笔记20230307】
基础 编码、解码 str.encode(utf-8) # 编码 str.decode(utf-8) # 解码关键字 import keyword keyword.kwlist格式化输出 % 占位符:%s 字符串%d 整数%f 浮点数Hello, %s % world Hi, %s, you have $%d. % (Michael, 1000000) 占位符的修饰符 -左对齐 .小数点后位数 0左边补零…...
 
SBOM应该是软件供应链中的安全主食
当谈到软件材料清单(SBOM)时,通常的类比是食品包装上的成分列表,它让消费者知道他们将要吃的薯片中有什么。 美国机构有90天时间创建所有软件的清单 同样,SBOM是一个软件中组件的清单,在应用程序是来自多个来源的代码的集合的时…...
 
[计算机组成原理(唐朔飞 第2版)]第一章 计算机系统概论 第二章 计算机的发展及应用(学习复习笔记)
第1章 计算机系统概论 1.1 计算机系统简介 1.1.1 计算机的软硬件概念 计算机系统由“硬件”和“软件”两大部分组成。 硬件 是指计算机的实体部分,它由看得见摸得着的各种电子元器件,各类光、电、机设备的实物组成如主机、外部设备等 软件 软件看不见…...
Python的数据分析相关的框架
Python特别强大,也是一款可以实现可数据分析语言,它有很多开源的库和工具,可以帮助数据科学家处理和分析数据。 以下是一些常用的Python库和工具: NumPy:NumPy是一个Python库,用于处理大型多维数组和矩阵&…...
 
为什么会出现植物神经紊乱 总是检查不出来该怎么办
植物神经紊乱是一种很多人都害怕的疾病,你们知道是为什么吗? 植物神经紊乱是一种神经系统失调导致的多种症状的总称,这种疾病是由于社会因素所诱发的脏器功能的失调,是一种非常复杂的疾病。而这种疾病是可能会发生在任何年龄阶段的…...
 
宏任务和微任务
JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是: ① 宏任务(macrotask) 异步 Ajax 请求setTimeout、setInterval文件操作其它宏任务 ② 微任务(microtask) Promise.then…...
 
使用WebSocket、SockJS、STOMP实现消息实时通讯功能
客户端 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head><title>websocket client</title><script src"http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.min.js"></script>…...
 
C++回顾(十一)—— 动态类型识别和抽象类
11.1 动态类识别 11.1.1 自定义类型 C中的多态根据实际的对象类型调用对应的函数 (1)可以在基类中定义虚函数返回具体的类型信息 (2)所有的派生类都必须实现类型相关的虚函数 (3)每个类中的类型虚函数都需…...
 
雷电模拟器安卓7以上+Charles抓包APP最新教程
一、工具准备: 证书安装工具全局代理工具下载: https://download.csdn.net/download/weixin_51111267/87536481 二、Charles设置 (一)电脑上证书安装 (二)安卓模拟器上系统证书安装(RooT权限打…...
vsvode 配置sftp,连接远程linux全过程
在本地安装sftp插件,配置参数https://blog.csdn.net/u011119817/article/details/106630599在linux机台安装vscode-service服务https://zhuanlan.zhihu.com/p/294933020连接超时,将配置文件添加超时时间遇到的错误处理:(272条消息) 【vscode插…...
C++类转换为蓝图、打印日志、蓝图关卡、删除C++文件
蓝图宏 UCLASS(Blueprintable)//c脚本可转换为蓝图 UPROPERTY(BlueprintReadWrite)//蓝图中可创建set,get节点 UFUNCTION(BlueprintCallable)//可创建函数节点 UPROPERTY(BlueprintReadWrite,Category”My Variables”)//节点进行分类打印日志 UE_LOG(LogTemp, Lo…...
 
elasticsearch高级篇:核心概念和实现原理
1.elasticsearch核心概念1.1 索引(index)一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母&#…...
 
部署安装Nginx服务实例
其他服务: 搭建zabbix4.0监控服务实例 普罗米修斯监控mysql数据库实战 Linux安装MySQL数据库步骤 一. Nginx概念介绍 1.介绍Nginx程序 Nginx (engine x) 是一款开源且高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。主要特点是占用…...
 
云原生架构设计原则及典型技术
云原生是面向云应用设计的一种思想理念,充分发挥云效能的最佳实践路径,帮助企业构建弹性可靠、松耦合、易管理可观测的应用系统,提升交付效率,降低运维复杂度。代表技术包括不可变基础设施、服务网格、声明式 API 及 Serverless 等…...
 
【Linux】-- 工具介绍 vim_gcc/g++_gdb
目录 Linux中的软件管理工具 – yum 在Linux下安装软件的方式 认识yum 查找软件包 安装 卸载 lrzsz.x86_64 rz sz Linux中的编辑器 – vim vim的基本概念 vim各模式切换 vim命令模式命令 vim底行模式命令 gcc / g gcc / g的作用 gcc / g语法 预处理 编译 汇…...
 
JAVA SE: IO流
一、Java流式输入输出原理Java对于输入输出是以流(Stream)的方式进行的,JDK提供各种各样的“流”类,以获取不同类型的数据。可以理解为将管道插入到文件中,然后从管道获取数据。这个管道外边还可以套管道,外边的管道对数据进行处理…...
 
打破原来软件开发模式的无代码开发平台
前言传统的系统开发是需要大量的时间和成本的,如今无代码开发平台的出现就改变了这种状况。那么你知道什么是无代码开发平台?无代码开发对企业来说有什么特殊的优势么?什么是无代码平台无代码平台指的是:使用者无需懂代码或手写代码,只需通…...
06-redux中的hook
知识点06-redux的hook 在函数组件中要和redux连接,分为两个步骤 前提状态机已经主备就绪 注入store到根组件 在函数组件中,使用Provider包裹根组件,并将store注入这一步,依旧是不能少的 import store from "./redux/store…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
 
智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...
 
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
 
项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)
Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败,具体原因是客户端发送了密码认证请求,但Redis服务器未设置密码 1.为Redis设置密码(匹配客户端配置) 步骤: 1).修…...
 
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
多模态图像修复系统:基于深度学习的图片修复实现
多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent
安全大模型训练计划:基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标:为安全大模型创建高质量、去偏、符合伦理的训练数据集,涵盖安全相关任务(如有害内容检测、隐私保护、道德推理等)。 1.1 数据收集 描…...
掌握 HTTP 请求:理解 cURL GET 语法
cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...
文件上传漏洞防御全攻略
要全面防范文件上传漏洞,需构建多层防御体系,结合技术验证、存储隔离与权限控制: 🔒 一、基础防护层 前端校验(仅辅助) 通过JavaScript限制文件后缀名(白名单)和大小,提…...
