【案例设计】Free Camera 设计与实现思路

开发平台:Unity 2020
编程平台:Visual Studio 2020

前言

  在游戏世界中,摄像机作为最常见、最常使用的 ComponentGameObject 对象。经常因为项目开发类型,在原基础上进行二次开发。例如 RTS建造游戏、即时战略、模拟现实等需要第三人称摄像机(TP)。枪战对抗、恐怖解密等需要第一人称摄像机(FP)。必须时,如《坦克世界》、《战地风云》等经典游戏需要 TP、FP 两者的结合。本文主要探究 Camera Component 的基础上实现 TP 效果。

注意:Unity 在如今的版本中推广 Ciniemachine 工具包。强化了 Camera 的使用效率、呈现效果。本文不使用该工具包实现。

思考:Transform 与 Vector 的关系与区别


  摄像机的移动 关联 Unity 编辑器中的 Transform 组件信息。该组件用于定义与管理 GameObject 在世空间的位置信息。在游戏中,寄希望于通过不同角度获取虚拟世界中不同角度下的视觉体验。对 Transform 必须有较好的理解与认知。如下示意图所示:
在这里插入图片描述
  通过上视图,可以总结出以下几点:
   1)世界空间是以 Vector 系组建而成。且方向固定,各轴面互垂直。
   2)Transform 定义的前后左右方向在世界空间下不固定。即以所见方向为前向。

思考:Quaternion 与 EulerAngles 的关系与区别


在这里插入图片描述

四元数(Quaternion):在 Unity 引擎中,统一使用四元数(Quaternion)实现对 GameObject 的旋转。
特点:四元数具备不受 万向锁 影响的特点。
备注:默认下面板中 Rotation 使用的是 Quaternion数据类型变量。
相关API:Unity 建议使用 *= 方式实现旋转方式。(四元数值累加与 + 无关)

欧拉角(EulerAngle):围绕固定的 X Y Z 轴进行旋转移动。
特点:各轴值变化互不影响。但同时变化多轴值出现 万向锁 情况影响。
备注:默认为面板上具体的数值信息。(非归一化 Quaternion)使用transform.eulerAngles获取关于 X Y Z 轴轴值。
相关API:transform.Rotate() - Unity 封装的基于 EulerAngles 实现的旋转方式。


关于 EulerAngles 轴值限定范围问题的分析与解决方案
  值得注意的是 EulerAngles 的范围区间在 [0, 360]。即超过或低于范围内的数据将转换至范围内的相对数据值。例如 470° = 110°、-60° = 300°等情况。对 Inspector 面板有所了解的会注意到,任意的改变 Rotation 变量值均不会出现被处理至 [0, 360] 区间的范围。即是多少 = 值多少(这里值不为度数)。
  有时候为限制 EulerAngles 中具体某个轴值范围易出现问题,例如 限制 [-60, 60],实际上做不到区间 [-60, 0) 的限制。于是在 Inspector 面板上虽然显示数值 -50。但实际其欧拉角大小为(- 50 + 360)= 310。在实际操作中,其度数的变化是基于X正半轴顺时针开始计算角度值。为实现负数角度的限制,使用以下示例代码逻辑:

public float ClampEulerAngles(float eulerAnglesValue)
{
	var value = eulerAnglesValue <= 180 ? eulerAnglesValue : enlerAnglesValue - 360;
	return value;
}

  使用该方法对EulerAngles数值检测负数范围,再通过Mathf.Clamp()对值域范围进行限制,并重新赋值至EulerAnglesRotation属性上。

思考:Global 与 Local 两种环境下的数据差异


在这里插入图片描述
全局坐标:基于世界坐标轴(世界正前方、正右方、正上方)
局部坐标:基于自身坐标轴(自身正前方、正右方、正上方)
辅助理解:Transform 与 Vector3 = 世界坐标 与 自身坐标


其他
  在 Unity 中,为区别 Global 与 Local 两个环境下的数据。提供 position/localPositoneularAngles/localEulerAngles等变量加以区别使用。 更多参考 Unity 官方中文文档对 API 的介绍。

思考:固定平面 与 自由平面 的区别


问:什么是固定平面?
:锁定 X Y Z 任意轴组合下的相机自由移动。其锁轴组合方式分为 单锁 与 组合锁 供两种方式。(可理解为在固定平面上移动,任何移动都无法离开限定的平面上)

问:什么是自由平面?
:跨维度变换(2D - 3D)。允许 XYZ 轴值任意组合或单个变换。(无轴平面限制)

public enum LockAxisMode {
	None,
	OnlyX, OnlyY, OnlyZ,
	OnlyXY, OnlyXZ, OnlyYZ,
	All,
}

  在程序设计上,为区别各类型采取 Enum(枚举)作出限定与锁轴设计。用于在执行 Translate / Rotate / Scale 前的判断。即若使用自由平面使用 LockAxisMode.None,若禁用 Free Camera 的使用,选择 LockAxisMode.All 或禁用该逻辑组件即可。

实践:空间移动 Translate


  Free Camera 移动是实现的首要。前后左右上下共计6个方向的位置偏移。根据期望效果方式,其大致的移动效果可总结为3种移动模式。

  • 固定平面移动:例如在同一面上的水平面移动。
    参考游戏:《模拟城市》、《红色警戒》、《王者荣耀》
  • 基于水平面的自由向移动:在固定平面移动基础上,添加垂直平面上的自由移动方式。
    参考游戏:《Battlefiled》、《CSOL》、《CSGO》 中死亡玩家的自由活动相机模式。
  • 全自由度移动:没有固定的平面方向,以自我为中心的前后左右上下。
    参考案例:太空失重环境下的宇航员。


关联参数

  • 按键绑定:前进、后进、左进、右进、上升、下降。
    问题原因
    1)用户操作习惯上不同,需要对按键进行调整。
    2)避免或快速解决 “一绑键,多响应” 的问题。
  • 移动速度:世界空间下 XYZ轴(Vector3) 移动速度
    问题原因:客户需求某一操作的速度变快。
    解决方法:参考 Unity 在 2019版本后提供的 可调节变速 与 过渡移速补值。(描述或不准确)
  • 轴向固定:不期望于部分行为操作可用。例如 仅禁用、禁用部分或全禁用 前后移动 左右移动 上升下降。

固定平面移动

public void DoTranslate()
{
	//transform.position += moveDir * Time.deltaTime;
	transform.Translate(moveDir * Time.deltaTime, Space.World);
}
  • Unity API 提供了transform.Translate(),其原理等价于 transform.position += ...
    唯一区别点在于Space空间不同。上图两行代码的运行效果是相同的。
  • Free Camera 运动方向完全按照世界空间下的 Vector3 前后左右上下方向移动。为避免出现 90°横向 所导致的移动轨迹与按钮反馈的体验度差问题。通常选择基于 Free Camera 朝向进行水平面方向计算,以让用户前后左右的移动与画面呈现相匹配。
  • 在进行水平运动方向的计算中,按照公式:
    前进/后退 方向 = 相机朝向(transform.forward)至 Local 水平坐标系方向前进的公式(该过程称为 “投影”)

  其他说明:在 Unity API 中分别提供 Project()ProjectOnPlane() 两种投影计算方式。其中三维世界中,向量至水平面的投影方向多使用第二种方式。经过 投影 + 归一化 的处理过程。得到 Free Camera 的运动方向。

public Vector3 GetForward(Vector3 forward)
{
	return Vector3.ProjectOnPlane(forward, Vector3.up).normalized;
}

基于水平面的自由向移动

  • 与 固定平面移动 大同小异。其主要不同之处在于 运动方向(前后)是按照自身 transform.forward 方向进行。
    可理解为 头脑转动到任意角度下,双眼正视前方的方向轨迹运动。(这类设计在 FP 上具有相当程度的体现)

全自由移动

  • 无固定平面,其运动方向完全基于自身(transform) 前后左右。仅从移动上,无法体现出其自由度。于是加入Rotation后,其三者的表现就清晰明了。

实践:空间旋转 Rotate


  在 Unity 中,实现物体的旋转通常是依赖于 Inspector 面板直接修改Rotation属性,或程序中调用transform.eulerAnglestransform.rotation进行重赋值行为。转动方向有助于改变 Translate 运动方向。按照其实现思路大致可有以下两种方式:

  • 欧拉角:围绕固定的X Y Z轴进行旋转(该轴可是 世界坐标轴 或是 局部坐标轴)
  • 四元数:按照 X Y Z W 共四个参数值进行计算。Unity 统一使用该类型实现旋转(见前文)。


关联参数

  • 旋转水平面:基于世界水平面 or 自身视线水平面
  • 旋转角度限制:垂直方向上仰角、俯角
  • 旋转灵敏度:即 X Y轴鼠标灵敏度

基于 四元数 的旋转

  四元数按照 d + ai + bj + ck(a、b、c、d 均为常数)存在。

public void DoRotate() => transform.rotation *= Quaternion.Euler(new Vector3(x, y ,z));
  • Quaternion.Euler():将 Vector3 类型转换为 Quaternion 类型。
  • 在 全自由移动 下的表现,是完全基于自身方向进行。如果刻意控制其左右转向围绕水平面方向进行,则其最后需添加transform.rotation = Quaternion.Euler(new Vector3(transform.rotation.x, transform.rotation.y, 0f))进行坐标处理即可。

基于 欧拉角 的旋转

public void DoRotate()
{ 
	//transform.eulerAngles += new Vector3(x, y, z);
	transform.Rotate(new Vector3(x, y, z));
}
  • 实质上该方法即 Unity API 中transform.Rotate()方法原理。即可使用transform.Rotate(new Vector3(x, y, z))进行表示。
  • 同理于 四元数 实现左右转向围绕水平面方向进行,应使用transform.enlerAngles = new Vector3(tranform.eulerAngles.x, transform.eulerAngles.y, 0f);进行角度重置。即使传入的值为负数,在 EulerAngles 中会处理至[0, 360]值范围内。

特别的transform.enlerAngles.x 这类具体到特定值上,在开发过程中无法直接修改该值以实现角度限制。则应追溯至上一层级进行数据类型赋值。

实践:视距缩放 Scale


  实现画面的放大与缩小效果,类似放大镜的作用。在 Unity 中这类实现方法无疑是将 Free Camera 沿 transform.forward 方向进行偏移。 当然,在一定范围内,使用 Camera.fieldOfView 属性进行值修改可达到同样的效果。其原理即是降低视野范围。

关联参数

  • 缩放阻尼:过渡效果(在移动、旋转中 亦可考虑添加)
    关联词Lerp()
  • 缩放速率:同距离下的最快移动速度。

基于 fieldOfView 的缩放

public void DoScale()
{
	var valueEnd = Input.mouseScrollDelta.y >=0f ? 20f : 60f;
	_Camera.fielfOfView = Mathf.Lerp(_Camera.fieldOfView, valueEnd, 1f);
}
  • Mathf.Lerp():插值过程,此处描述为1s内完成当前值至valueEnd值的变化。
  • 弊端:当值变化过于极端的情况下,画面视野扭曲程度较高,与实际放大缩小充满违和感。

基于 位置变化 的缩放

public void DoScale()
{
	var moveDir = transform.forward * Input.mouseScrollDelta.y * Time.deltaTime;
	_Camera.transfrom.position += moveDir;
	//_Camera.transform.Translate(moveDir);
}

其他实现层面:

  • 添加 Aim 模式,修改缩放策略。有时候期望于仅放大观察物体表面内容,则可使用 Physics.RayCasst() 物理射线检测对象(或由点击行为绑定的 GameObject 信息)。
    按照 (目标 至 主摄像机 的距离)* 0.5f 进行移动效果(或使用插值),以达到无限接近目标位置。但未穿过目标。

小结


  Free Camera 的应用环境广泛,也是入门级学习的典型案例。理解与深析是认识三维虚拟世界的重要点。


可优化措施

  1. 使用 ScriptableObject 创建配置参数。
  2. 推荐配合 Cinemachine 工具包拓展 Free Camera 功能设计。
  3. 代码结构上,划分 主逻辑(移动 旋转 缩放)、助理(转换方法、枚举)、可配置(持久化数据)。切忌整合在同一脚本下,增加阅读难度。
  4. 扩展设计 Free Camera 的三维空间运动范围,限制其在某些区域内的移动。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
密勒编码是一种常用的图像压缩算法,通过对像素点进行编码来减小图像文件的大小。下面是C语言实现密勒编码的示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> // 定义一个结构体表示像素点 typedef struct Pixel { int x; int y; int value; } Pixel; // 定义一个结构体表示位编码 typedef struct BitCode { int length; char *bits; } BitCode; // 根据坐标和像素值创建一个Pixel结构体 Pixel *createPixel(int x, int y, int value) { Pixel *pixel = (Pixel *)malloc(sizeof(Pixel)); pixel->x = x; pixel->y = y; pixel->value = value; return pixel; } // 根据位编码字符串创建一个BitCode结构体 BitCode *createBitCode(char *bits) { BitCode *code = (BitCode *)malloc(sizeof(BitCode)); code->length = strlen(bits); code->bits = (char *)malloc((code->length + 1) * sizeof(char)); strcpy(code->bits, bits); return code; } // 释放Pixel结构体 void freePixel(Pixel *pixel) { free(pixel); } // 释放BitCode结构体 void freeBitCode(BitCode *code) { free(code->bits); free(code); } // 将十进制数字转换为二进制字符串 char *intToBinary(int num, int length) { char *str = (char *)malloc((length + 1) * sizeof(char)); for (int i = length - 1; i >= 0; i--) { str[i] = (num % 2) ? '1' : '0'; num /= 2; } str[length] = '\0'; return str; } // 将位编码字符串转换为十进制数字 int binaryToInt(char *str) { int num = 0; int length = strlen(str); for (int i = 0; i < length; i++) { num = num * 2 + (str[i] - '0'); } return num; } // 将像素点编码为位编码 BitCode *encodePixel(Pixel *pixel, int depth) { char *bits = (char *)malloc((depth + 1) * sizeof(char)); int x = pixel->x; int y = pixel->y; int value = pixel->value; for (int i = 0; i < depth; i++) { if (i % 3 == 0) { bits[i] = (value & 4) ? '1' : '0'; } else if (i % 3 == 1) { bits[i] = (value & 2) ? '1' : '0'; } else { bits[i] = (value & 1) ? '1' : '0'; value = x; x = y; y = value; } } bits[depth] = '\0'; BitCode *code = createBitCode(bits); free(bits); return code; } // 将位编码解码为像素点 Pixel *decodePixel(BitCode *code, int depth) { char *bits = code->bits; int x = 0; int y = 0; int value = 0; for (int i = 0; i < depth; i++) { if (i % 3 == 0) { value |= (bits[i] - '0') << 2; } else if (i % 3 == 1) { value |= (bits[i] - '0') << 1; } else { value |= bits[i] - '0'; int temp = y; y = x; x = value; value = temp; } } Pixel *pixel = createPixel(x, y, value); return pixel; } int main() { // 创建一个测试像素点 Pixel *pixel1 = createPixel(3, 7, 5); // 对像素点进行编码 BitCode *code1 = encodePixel(pixel1, 12); // 输出编码结果 printf("Pixel1: (%d, %d, %d)\n", pixel1->x, pixel1->y, pixel1->value); printf("Code1: %s\n", code1->bits); // 对编码结果进行解码 Pixel *pixel2 = decodePixel(code1, 12); // 输出解码结果 printf("Pixel2: (%d, %d, %d)\n", pixel2->x, pixel2->y, pixel2->value); // 释放内存 freePixel(pixel1); freeBitCode(code1); freePixel(pixel2); return 0; } ``` 以上代码仅是密勒编码的基础实现,实际应用中需要根据具体需求进行优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值