Unity 如何实现游戏Avatar角色头部跟随视角转动

news2024/11/26 6:24:10

文章目录

  • 功能简介
  • 实现步骤
    • 获取看向的位置
    • 获取头部的位置
    • 修改头部的朝向
    • 限制旋转角度
    • 超出限制范围时自动回正
  • 如何让指定动画不受影响


功能简介

如图所示,当相机的视角转动时,Avatar角色的头部会同步转动,看向视角的方向。

功能示例

实现步骤

获取看向的位置

Avatar看向的位置即相机前方一定距离的某个坐标,该距离偏大于相机与Avatar角色的距离即可,可以取100来代表:

//获取看向的位置
private Vector3 GetLookAtPosition()
{
    //主相机前方100个单位的位置
    return mainCamera.transform.position + mainCamera.transform.forward * 100f;
}

获取头部的位置

头部位置可以通过Animator组件中的GetBoneTransform接口来获取

GetBoneTransform

示例如下:

using UnityEngine;

namespace SK.Framework.Avatar
{
    public class HeadTrack : MonoBehaviour
    {
        //动画组件
        [SerializeField] private Animator animator; 
        private Camera mainCamera; //主相机
        private Transform head; //头部

        private void Start()
        {
        	mainCamera = Camera.main ?? FindObjectOfType<Camera>();
            head = animator.GetBoneTransform(HumanBodyBones.Head);
        }

        //获取看向的位置
        private Vector3 GetLookAtPosition()
        {
            //主相机前方100个单位的位置
            return mainCamera.transform.position + mainCamera.transform.forward * 100f;
        }
    }
}

有了头部的位置后,就可以计算头部的高度,声明一个变量headHeight来记录头部高度:

headHeight = Vector3.Distance(transform.position, head.position);

修改头部的朝向

有了看向的坐标和头部的坐标,就取得了看向的朝向,在LateUpdate中赋值该头部朝向,注意一定要使用LateUpdate,因为Animator动画组件在控制Avatar各骨骼的朝向,使用LateUpdate可以确保我们的旋转值修改起作用。

using UnityEngine;

namespace SK.Framework.Avatar
{
    public class HeadTrack : MonoBehaviour
    {
        //动画组件
        [SerializeField] private Animator animator; 

        private Camera mainCamera; //主相机
        private Transform head; //头部
        private float headHeight; //头部的高度

        private void Start()
        {
            mainCamera = Camera.main ?? FindObjectOfType<Camera>();
            head = animator.GetBoneTransform(HumanBodyBones.Head);
            headHeight = Vector3.Distance(transform.position, head.position);
        }

        /// <summary>
        /// 看向某点
        /// </summary>
        /// <param name="position"></param>
        public void LookAtPosition(Vector3 position)
        {
            //头部位置
            Vector3 headPosition = transform.position + transform.up * headHeight;
            //朝向
            Quaternion lookRotation = Quaternion.LookRotation(position - headPosition);

            head.rotation = lookRotation;
        }
        
        private void LateUpdate()
        {
            Debug.DrawLine(transform.position + transform.up * headHeight, GetLookAtPosition());
            LookAtPosition(GetLookAtPosition());
        }

        //获取看向的位置
        private Vector3 GetLookAtPosition()
        {
            //主相机前方100个单位的位置
            return mainCamera.transform.position + mainCamera.transform.forward * 100f;
        }
    }
}

如图所示,我们已经实现了头部的转向,但是旋转值过大会导致反人类现象,因此需要将旋转值进行限制。

限制旋转角度

//水平方向上的角度限制
[SerializeField] private Vector2 horizontalAngleLimit = new Vector2(-100f, 100f); 
 //垂直方向上的角度限制
[SerializeField] private Vector2 verticalAngleLimit = new Vector2(-60f, 60f); 

封装一个角度标准化的函数,当角度大于180度时减360度,当角度小于180度时加360度:

//角度标准化
private float NormalizeAngle(float angle)
{
    if (angle > 180) angle -= 360f;
    else if (angle < -180) angle += 360f;
    return angle;
}

封装看向某点的函数:

/// <summary>
/// 看向某点
/// </summary>
/// <param name="position"></param>
public void LookAtPosition(Vector3 position)
{
    //头部位置
    Vector3 headPosition = transform.position + transform.up * headHeight;
    //朝向
    Quaternion lookRotation = Quaternion.LookRotation(position - headPosition);
    Vector3 eulerAngles = lookRotation.eulerAngles - transform.rotation.eulerAngles;
    float x = NormalizeAngle(eulerAngles.x);
    float y = NormalizeAngle(eulerAngles.y);
    x = Mathf.Clamp(x, verticalAngleLimit.x, verticalAngleLimit.y);
    y = Mathf.Clamp(y, horizontalAngleLimit.x, horizontalAngleLimit.y);
    Quaternion rotY = Quaternion.AngleAxis(y, head.InverseTransformDirection(transform.up));
    head.rotation *= rotY;
    Quaternion rotX = Quaternion.AngleAxis(x, head.InverseTransformDirection(transform.TransformDirection(Vector3.right)));
    head.rotation *= rotX;
}

超出限制范围时自动回正

当角度超出限制的范围时,将头部自动回正,可以在GetLookAtPosition函数中加入判断,声明autoTurnback变量标识是否自动回正:

//获取看向的位置
private Vector3 GetLookAtPosition()
{
    Vector3 position = mainCamera.transform.position + mainCamera.transform.forward * 100f;
    if (!autoTurnback) return position;
    Vector3 direction = position - (transform.position + transform.up * headHeight);
    Quaternion lookRotation = Quaternion.LookRotation(direction, transform.up);
    Vector3 angle = lookRotation.eulerAngles - transform.eulerAngles;
    float x = NormalizeAngle(angle.x);
    float y = NormalizeAngle(angle.y);
    bool isInRange = x >= verticalAngleLimit.x && x <= verticalAngleLimit.y
        && y >= horizontalAngleLimit.x && y <= horizontalAngleLimit.y;
    return isInRange ? position : (transform.position + transform.up * headHeight + transform.forward);
}

加入插值运算,使自动回正时有过渡过程,代码如下:

using UnityEngine;

namespace SK.Framework.Avatar
{
    public class HeadTrack : MonoBehaviour
    {
        
        [Tooltip("动画组件"), SerializeField] private Animator animator; 
        [Tooltip("水平方向上的角度限制"), SerializeField] private Vector2 horizontalAngleLimit = new Vector2(-70f, 70f); 
        [Tooltip("垂直方向上的角度限制"), SerializeField] private Vector2 verticalAngleLimit = new Vector2(-60f, 60f);
        [Tooltip("超出限制范围时自动回正"), SerializeField] private bool autoTurnback = true;
        [Tooltip("插值速度"), SerializeField] private float lerpSpeed = 5f;

        private Camera mainCamera; //主相机
        private Transform head; //头部
        private float headHeight; //头部的高度
        private float angleX;
        private float angleY;

        private void Start()
        {
            mainCamera = Camera.main ?? FindObjectOfType<Camera>();
            head = animator.GetBoneTransform(HumanBodyBones.Head);
            headHeight = Vector3.Distance(transform.position, head.position);
        }

        /// <summary>
        /// 看向某点
        /// </summary>
        /// <param name="position"></param>
        public void LookAtPosition(Vector3 position)
        {
            //头部位置
            Vector3 headPosition = transform.position + transform.up * headHeight;
            //朝向
            Quaternion lookRotation = Quaternion.LookRotation(position - headPosition);
            Vector3 eulerAngles = lookRotation.eulerAngles - transform.rotation.eulerAngles;
            float x = NormalizeAngle(eulerAngles.x);
            float y = NormalizeAngle(eulerAngles.y);
            angleX = Mathf.Clamp(Mathf.Lerp(angleX, x, Time.deltaTime * lerpSpeed), verticalAngleLimit.x, verticalAngleLimit.y);
            angleY = Mathf.Clamp(Mathf.Lerp(angleY, y, Time.deltaTime * lerpSpeed), horizontalAngleLimit.x, horizontalAngleLimit.y);
            Quaternion rotY = Quaternion.AngleAxis(angleY, head.InverseTransformDirection(transform.up));
            head.rotation *= rotY;
            Quaternion rotX = Quaternion.AngleAxis(angleX, head.InverseTransformDirection(transform.TransformDirection(Vector3.right)));
            head.rotation *= rotX;
        }

        //角度标准化
        private float NormalizeAngle(float angle)
        {
            if (angle > 180) angle -= 360f;
            else if (angle < -180) angle += 360f;
            return angle;
        }

        private void LateUpdate()
        {
            LookAtPosition(GetLookAtPosition());
        }

        //获取看向的位置
        private Vector3 GetLookAtPosition()
        {
            Vector3 position = mainCamera.transform.position + mainCamera.transform.forward * 100f;
            if (!autoTurnback) return position;
            Vector3 direction = position - (transform.position + transform.up * headHeight);
            Quaternion lookRotation = Quaternion.LookRotation(direction, transform.up);
            Vector3 angle = lookRotation.eulerAngles - transform.eulerAngles;
            float x = NormalizeAngle(angle.x);
            float y = NormalizeAngle(angle.y);
            bool isInRange = x >= verticalAngleLimit.x && x <= verticalAngleLimit.y 
                && y >= horizontalAngleLimit.x && y <= horizontalAngleLimit.y;
            return isInRange ? position : (transform.position + transform.up * headHeight + transform.forward); 
        }
    }
}

自动回正

如何让指定动画不受影响

如果我们想要在播放某个动画时不让头部转动,可以通过Tag标签来解决,如下图所示,为Hi动画增加IgnoreHeadTrack标签:

Tag
在代码中加入判断:

//获取看向的位置
private Vector3 GetLookAtPosition()
{
    AnimatorStateInfo animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
    if (animatorStateInfo.IsTag("IgnoreHeadTrack"))
        return transform.position + transform.up * headHeight + transform.forward;

    Vector3 position = mainCamera.transform.position + mainCamera.transform.forward * 100f;
    if (!autoTurnback) return position;
    Vector3 direction = position - (transform.position + transform.up * headHeight);
    Quaternion lookRotation = Quaternion.LookRotation(direction, transform.up);
    Vector3 angle = lookRotation.eulerAngles - transform.eulerAngles;
    float x = NormalizeAngle(angle.x);
    float y = NormalizeAngle(angle.y);
    bool isInRange = x >= verticalAngleLimit.x && x <= verticalAngleLimit.y
        && y >= horizontalAngleLimit.x && y <= horizontalAngleLimit.y;
    return isInRange ? position : (transform.position + transform.up * headHeight + transform.forward);
}

完整代码:

using UnityEngine;

namespace SK.Framework.Avatar
{
    public class HeadTrack : MonoBehaviour
    {
        
        [Tooltip("动画组件"), SerializeField] private Animator animator; 
        [Tooltip("水平方向上的角度限制"), SerializeField] private Vector2 horizontalAngleLimit = new Vector2(-70f, 70f); 
        [Tooltip("垂直方向上的角度限制"), SerializeField] private Vector2 verticalAngleLimit = new Vector2(-60f, 60f);
        [Tooltip("超出限制范围时自动回正"), SerializeField] private bool autoTurnback = true;
        [Tooltip("插值速度"), SerializeField] private float lerpSpeed = 5f;

        private Camera mainCamera; //主相机
        private Transform head; //头部
        private float headHeight; //头部的高度
        private float angleX;
        private float angleY;

        private void Start()
        {
            mainCamera = Camera.main ?? FindObjectOfType<Camera>();
            head = animator.GetBoneTransform(HumanBodyBones.Head);
            headHeight = Vector3.Distance(transform.position, head.position);
        }

        /// <summary>
        /// 看向某点
        /// </summary>
        /// <param name="position"></param>
        public void LookAtPosition(Vector3 position)
        {
            //头部位置
            Vector3 headPosition = transform.position + transform.up * headHeight;
            //朝向
            Quaternion lookRotation = Quaternion.LookRotation(position - headPosition);
            Vector3 eulerAngles = lookRotation.eulerAngles - transform.rotation.eulerAngles;
            float x = NormalizeAngle(eulerAngles.x);
            float y = NormalizeAngle(eulerAngles.y);
            angleX = Mathf.Clamp(Mathf.Lerp(angleX, x, Time.deltaTime * lerpSpeed), verticalAngleLimit.x, verticalAngleLimit.y);
            angleY = Mathf.Clamp(Mathf.Lerp(angleY, y, Time.deltaTime * lerpSpeed), horizontalAngleLimit.x, horizontalAngleLimit.y);
            Quaternion rotY = Quaternion.AngleAxis(angleY, head.InverseTransformDirection(transform.up));
            head.rotation *= rotY;
            Quaternion rotX = Quaternion.AngleAxis(angleX, head.InverseTransformDirection(transform.TransformDirection(Vector3.right)));
            head.rotation *= rotX;
        }

        //角度标准化
        private float NormalizeAngle(float angle)
        {
            if (angle > 180) angle -= 360f;
            else if (angle < -180) angle += 360f;
            return angle;
        }

        private void LateUpdate()
        {
            LookAtPosition(GetLookAtPosition());
        }

        //获取看向的位置
        private Vector3 GetLookAtPosition()
        {
            AnimatorStateInfo animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0);
            if (animatorStateInfo.IsTag("IgnoreHeadTrack"))
                return transform.position + transform.up * headHeight + transform.forward;

            Vector3 position = mainCamera.transform.position + mainCamera.transform.forward * 100f;
            if (!autoTurnback) return position;
            Vector3 direction = position - (transform.position + transform.up * headHeight);
            Quaternion lookRotation = Quaternion.LookRotation(direction, transform.up);
            Vector3 angle = lookRotation.eulerAngles - transform.eulerAngles;
            float x = NormalizeAngle(angle.x);
            float y = NormalizeAngle(angle.y);
            bool isInRange = x >= verticalAngleLimit.x && x <= verticalAngleLimit.y 
                && y >= horizontalAngleLimit.x && y <= horizontalAngleLimit.y;
            return isInRange ? position : (transform.position + transform.up * headHeight + transform.forward); 
        }
    }
}

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

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

相关文章

企业数字化转型的产品设计思路

数字化转型的核心是全面重塑企业的管理模式和经营模式&#xff0c;是迈向数字经济时代的方式。一、到底什么是数字化转型&#xff1f;数字化转型并不神秘。数字化转型是一种经营方式、一种经营理念&#xff0c;是将企业相关的人、物料、设备、资金等要素进行系统运转&#xff0…

命令模式包含哪些主要角色?怎样实现命令?

命令模式包含以下主要角色&#xff1a;抽象命令类&#xff08;Command&#xff09;角色&#xff1a; 定义命令的接口&#xff0c;声明执行的方法。具体命令&#xff08;Concrete Command&#xff09;角色&#xff1a;具体的命令&#xff0c;实现命令接口&#xff1b;通常会持有…

flex 布局相关属性的使用

简单概述 为元素添加 display:flex; 的属性后&#xff0c;当前元素被视为弹性布局的盒子容器(box)&#xff0c;其子元素被视为弹性布局项目(item)。item 会在 box 内灵活布局&#xff0c;解决了对齐、分布、尺寸等响应式问题。 演示 demo <template><div class&quo…

软中断在bottom-half中调用

https://www.bilibili.com/read/cv20785285/简介软中断可以在两个位置得到机会执行&#xff1a;硬中断返回前 irq_exit中断下半部 Bottom-half Enable后情景分析情景1spin_unlock_bh__raw_spin_unlock_bh__local_bh_enable_ip 打开Bottom-half&#xff0c;并让softirq有机会…

【NGINX入门指北】 进阶篇

nginx 进阶篇 文章目录nginx 进阶篇一、Nginx Proxy 服务器1、代理原理2、proxy代理3、proxy缓存一、Nginx Proxy 服务器 1、代理原理 正向代理 内网客户机通过代理访问互联网&#xff0c;通常要设置代理服务器地址和端口。 反向代理 外网用户通过代理访问内网服务器&…

xpath注入[NPUCTF2020]ezlogin

[NPUCTF2020]ezlogin 打开界面 如果发现自己输入的信息由这样构成&#xff0c;可以往xpath注入上靠一下。 不管输入什么&#xff0c;很容易发现登陆就超时了&#xff0c;说明这里token是不断刷新的。 这样构造也是一样的目的都是为了闭合后面的&#xff0c;为啥有两个or呢 us…

Redis中有常见数据类型

Redis的数据类型 string数据类型 string是redis最基本的类型&#xff0c;而且string类型是二进制安全的。意思是redis的string可以包含任何 数据&#xff0c;比如jpg图片或者序列化的对象 String类型是最基本的数据类型&#xff0c;一个redis中字符串value最多可以是512M r…

Allegro如何使用Snake命令走蛇形线操作指导

Allegro如何使用Snake命令走蛇形线操作指导 在做PCB设计的时候,遇到不规则BGA的时候,蛇形走线是惯用的走线方式,类似下图 Allegro支持蛇形走线,具体操作如下 首先把过孔打好,尽量上下左右间距一致,不容易出现偏差,如下图在Command命令栏下方输入snake,然后回车

常见字符串函数的使用,你确定不进来看看吗?

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前是C语言学习者 ✈️专栏&#xff1a;C语言航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&a…

2023年最强大的12款数据可视化工具,值得收藏

做数据分析也有年头了&#xff0c;好的坏的工具都用过&#xff0c;推荐几个觉得很好用的&#xff0c;避坑必看&#xff01; PS&#xff1a;一般比较成熟的公司里&#xff0c;数据分析工具不只是满足业务分析和报表制作&#xff0c;像我现在给我们公司选型BI工具&#xff0c;是做…

差分模拟信号转单端输出电路设计

需求分析&#xff1a; 1.差分输入0~16V -Vpp电压量&#xff1b; 2.输入频率0~1.2KHz&#xff1b; 3.单端对应输出0~3V的模拟量&#xff1b; 4.输出频率对应0~1.2KHz&#xff1b; 5.供电范围3~5V。 针对以上需求&#xff0c;设计如下图所示电路。 1.电路功能&#xff1a; …

Spring为什么这么火 之 Bean的6种作用域和Bean的生命周期

1、Bean的作用域 1.1、什么是作用域&#xff1f; 限定程序中变量的可用范围叫做作用域&#xff0c;或者说在源代码中定义变量的某个区域就叫做作用域 1.2、Bean的6种作用域 singleton&#xff1a;单例作用域prototype&#xff1a;原型作用域【多例作用域】request&#xff1…

Flowable进阶学习(九)数据对象DataObject、租户Tenant、接收任务ReceiveTask

文章目录一、数据对象DataObject二、租户 Tenant三、接收任务 ReceiveTask案例一、数据对象DataObject DataObject可以⽤来定义⼀些流程的全局属性。 绘制流程图&#xff0c;并配置数据对象&#xff08;不需要选择任意节点&#xff09; 2. 编码与测试 /*** 部署流程*/ Test…

C++类和对象(中)

✨个人主页&#xff1a; Yohifo &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 I do not believe in taking the right decision. I take a decision and make it right. 我不相信什么正确的决定。我都是先做决定&#xff0c;然后把…

java二叉排序树

1.先看一个需求 给你一个数列 (7, 3, 10, 12, 5, 1, 9)&#xff0c;要求能够高效的完成对数据的查询和添加 2.解决方案分析 使用数组 数组未排序&#xff0c; 优点&#xff1a;直接在数组尾添加&#xff0c;速度快。 缺点&#xff1a;查找速度慢. [示意图] 数组排序&#xf…

车道线检测-PolyLaneNet 论文学习笔记

论文&#xff1a;《PolyLaneNet: Lane Estimation via Deep Polynomial Regression》代码&#xff1a;https://github.com/lucastabelini/PolyLaneNet地址&#xff1a;https://arxiv.org/pdf/2004.10924.pdf参考&#xff1a;https://blog.csdn.net/sinat_17456165/article/deta…

Java中的clone方法

注解定义&#xff1a; 注解是一种注释机制&#xff0c;它可以注释包、类、方法、变量、参数&#xff0c;在编译器生成类文件时&#xff0c;标注可以被嵌入到字节码中。注解的分类&#xff1a;内置注解Override :重写方法&#xff0c;引用时没有该方法时会编译错误public class …

使用 ThreeJS 实现第一个三维场景(详)

文章目录参考描述index.html三维场景的基本实现导入 ThreeJS准备工作场景摄像机视锥体正交摄像机透视摄像机渲染器后续处理将摄像机添加至场景中移动摄像机设置画布尺寸将渲染器创建的画布添加到 HTML 元素中渲染物体结构材质合成将物体添加至场景中代码总汇执行效果动画reques…

Python基础及函数解读(深度学习)

一、语句1.加注释单行注释&#xff1a;&#xff08;1&#xff09;在代码上面加注释&#xff1a; # 后面跟一个空格&#xff08;2&#xff09;在代码后面加注释&#xff1a;和代码相距两个空格&#xff0c; # 后面再跟一个空格多行注释&#xff1a;按住shift 点击三次"&am…

蓝桥杯刷题023——机器人塔(DFS)

2016国赛 题目描述 X 星球的机器人表演拉拉队有两种服装&#xff0c;A 和 B。 他们这次表演的是搭机器人塔。 类似&#xff1a; A B B A B A A A B B B B B A B A B A B B A 队内的组塔规则是&#xff1a; A 只能站在 AA 或 BB 的肩上。 B 只能站在 AB 或 BA 的肩上。 你的任务…