Unity 如何实现游戏Avatar角色头部跟随视角转动
文章目录
- 功能简介
- 实现步骤
- 获取看向的位置
- 获取头部的位置
- 修改头部的朝向
- 限制旋转角度
- 超出限制范围时自动回正
- 如何让指定动画不受影响
功能简介
如图所示,当相机的视角转动时,Avatar角色的头部会同步转动,看向视角的方向。

实现步骤
获取看向的位置
Avatar看向的位置即相机前方一定距离的某个坐标,该距离偏大于相机与Avatar角色的距离即可,可以取100来代表:
//获取看向的位置
private Vector3 GetLookAtPosition()
{//主相机前方100个单位的位置return mainCamera.transform.position + mainCamera.transform.forward * 100f;
}
获取头部的位置
头部位置可以通过Animator组件中的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标签:

在代码中加入判断:
//获取看向的位置
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); }}
}
相关文章:
Unity 如何实现游戏Avatar角色头部跟随视角转动
文章目录功能简介实现步骤获取看向的位置获取头部的位置修改头部的朝向限制旋转角度超出限制范围时自动回正如何让指定动画不受影响功能简介 如图所示,当相机的视角转动时,Avatar角色的头部会同步转动,看向视角的方向。 实现步骤 获取看向的…...
深度学习优化算法总结
深度学习的优化算法 优化的目标 优化提供了一种最大程度减少深度学习损失函数的方法,但本质上,优化和深度学习的目标不同。 优化关注的是最小化目标;深度学习是在给定有限数据量的情况下寻找合适的模型。 优化算法 gradient descent…...
CMake详细使用
1、CMake简介CMake是一个用于管理源代码的跨平台构建工具可以方便地根据目标平台和编译工具产生对应的编译文件主要用于C/C语言的构建,但是也可以用于其它编程语言的源代码。如同使用make命令工具解析Makefile文件一样cmake命令工具依赖于一个CMakeLists.txt的文件该…...
【数据结构与算法】前缀树的实现
🌠作者:阿亮joy. 🎆专栏:《数据结构与算法要啸着学》 🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录👉…...
canvas 制作2048
效果展示 对UI不满意可以自行调整,这里只是说一下游戏的逻辑,具体的API调用不做过多展示。 玩法分析 2048 的玩法非常简单,通过键盘的按下,所有的数字都向着同一个方向移动,如果出现两个相同的数字,就将…...
playwright: 全局修改页面等待超时时间
等待超时时间默认是30s, 可以通过以下几个方法设置: browser_context.set_default_navigation_timeout()browser_context.set_default_timeout()page.set_default_navigation_timeout()page.set_default_timeout() set_default_navigation_timeout set_default_n…...
C++类和对象(中)
✨个人主页: Yohifo 🎉所属专栏: C修行之路 🎊每篇一句: 图片来源 I do not believe in taking the right decision. I take a decision and make it right. 我不相信什么正确的决定。我都是先做决定,然后把…...
Docker安装EalasticSearch、Kibana,安装Elasticvue插件
使用Docker快速安装部署ES和Kibana的前提:首先需要确保已经安装了Docker环境。 如果没有安装Docker的话,先在Linux上安装Docker。 有了Docker环境后,就可以使用Docker安装部署ES和Kibana了 一、安装ES 1、拉取EalasticSearch镜像 docker p…...
算法训练营 day39 贪心算法 无重叠区间 划分字母区间 合并区间
算法训练营 day39 贪心算法 无重叠区间 划分字母区间 合并区间 无重叠区间 435. 无重叠区间 - 力扣(LeetCode) 给定一个区间的集合 intervals ,其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互…...
c/c++开发,无可避免的文件访问开发案例
一、缓存文件系统 ANSI C标准中的C语言库提供了fopen, fclose, fread, fwrite, fgetc, fgets, fputc, fputs, freopen, fseek, ftell, rewind等标准函数,这些函数在不同的操作系统中应该调用不同的内核API,从而支持开发者跨平台实现对文件的访问。 在Lin…...
MySQL学习笔记
MySQL学习笔记一、基础配置二、数据库操作三、表的操作1.创建表2.表选项3.查看表4.修改表5.删除表6.复制表7.检查优化修复表四、数据操作基础增删改查五、字符集编码六、数据类型(列类型)1.数值类型2.字符串类型3.日期时间类型4.枚举和集合七、列属性&am…...
ccs导入工程失败的处理方法
文章目录当导入CCS新工程时出现下述错误怎么办?方法一 从TI官网下载安装包进行安装,下载链接:软件下载完成 安装路径为上面的文件夹点击安装完成后,导入安装路径,并点击Refresh按钮,依据路径进行更新&#…...
探针台常见的故障及解决方法
症状、 可能原因、 解决方法 移动样品后画面变模糊 —显微镜不垂直,调垂直显微镜 样品台不水平 —调水平样品台 显微镜视场亮度不足,边缘切割或看不到像—转换器不在定位位置上 把转换器转到定位位置上 管镜转盘不在定位位置上 —把管镜转盘转到定…...
域内资源探测
✅作者简介:CSDN内容合伙人、信息安全专业在校大学生🏆 🔥系列专栏 :内网安全 📃新人博主 :欢迎点赞收藏关注,会回访! 💬舞台再大,你不上台,永远是…...
c# 将数据导出到EXCEL文件
第一步:项目中加入引用。 在鼠标右击项目,点击【添加】弹出菜单列表,选择【项目引用】弹出【引用管理器】对话框,选择【COM】-【Microsoft Excel 16.0 Object Library】,如图所示: 第二步,编辑…...
微服务 分片 运维管理
微服务 分片 运维管理分片分片的概念分片案例环境搭建案例改造成任务分片Dataflow类型调度代码示例运维管理事件追踪运维平台搭建步骤使用步骤分片 分片的概念 当只有一台机器的情况下,给定时任务分片四个,在机器A启动四个线程,分别处理四个…...
批量占满TEMP表空间问题处理与排查
批量占满TEMP表空间问题处理与排查应急处置问题排查查看占用TEMP表空间高的SQL获取目标SQL执行计划方法一:EXPLAIN PLAN FOR方法二:DBMS_XPLAN.DISPLAY_CURSOR方法三:DBMS_XPLAN.DISPLAY_AWR方法四:AUTOTRACE数据库跑批任务占满TE…...
Pytorch中的tensor和variable
Tensor与Variable pytorch两个基本对象:Tensor(张量)和Variable(变量) 其中,tensor不能反向传播,variable可以反向传播(forword)。 反向传播是为了让神经网络更新前面…...
暗月内网渗透实战——项目七
首先环境配置 VMware的网络配置图 环境拓扑图 开始渗透 信息收集 使用kali扫描一下靶机的IP地址 靶机IP:192.168.0.114 攻击机IP:192.168.0.109 获取到了ip地址之后,我们扫描一下靶机开放的端口 靶机开放了21,80,999,3389,5985,6588端口…...
【Java 面试合集】描述下Objec类中常用的方法(未完待续中...)
描述下Objec类中常用的方法 1. 概述 首先我们要知道Object 类是所有的对象的基类,也就是所有的方法都是可以被重写的。 那么到底哪些方法是我们常用的方法呢??? cloneequalsfinalizegetClasshashCodenotifynotifyAlltoStringw…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
系统掌握PyTorch:图解张量、Autograd、DataLoader、nn.Module与实战模型
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文通过代码驱动的方式,系统讲解PyTorch核心概念和实战技巧,涵盖张量操作、自动微分、数据加载、模型构建和训练全流程&#…...
