找回密码
 立即注册
查看: 341|回复: 0

[笔记] 1.11 从0开始学习Unity游戏开发--移动你的相机

[复制链接]
发表于 2023-4-1 13:26 | 显示全部楼层 |阅读模式
上一篇文章介绍了如何在Unity中接收用户的输入,由于需要兼容各个设备的差异,Unity做了一套封装,初用起来可能会比较绕,本篇我们就直接用起来,我们会利用wasd来移动我们的游戏内的视线,有点类似FPS游戏里面的操作效果,同时我们也会学习如何让我们的代码控制场景内的物体,而非只是控制组件自己所在的物体。
组件接收WASD输入

我们新建一个代码资源文件叫CameraController:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKey(KeyCode.W))
        {
            // Move camera forward
        }
        if (Input.GetKey(KeyCode.A))
        {
            // Move camera left
        }
        if (Input.GetKey(KeyCode.S))
        {
            // Move camera backward
        }
        if (Input.GetKey(KeyCode.D))
        {
            // Move camera right
        }
    }
}
可以看到我们用上一篇讲的Input类来获取当前用户按下了什么按键,但是我们也说了,如果我们不是用的键盘,如果是手柄呢?那么我们最好还是使用Axis信息来代表类似移动输入的信息,我们让GPT简单帮我改改代码:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
    }
}
"Horizontal"和"Vertical"都是可以从InputManager配置文件里面找到对应的配置名的,很明显这两个分别代表水平和竖直方向上的输入量,float取值范围是[-1, 1]。
但是细心的人真的会看到InputManager里面针对这两个输入并没有配置具体是键盘上的哪个按键,怎么就一定会是WASD呢,那只能说是Unity的潜规则,可以从文档中了解到:
To read an axis useInput.GetAxiswith one of the following default axes: "Horizontal" and "Vertical" are mapped to joystick,A,W,S,Dand the arrow keys. "Mouse X" and "Mouse Y" are mapped to the mouse delta. "Fire1", "Fire2" "Fire3" are mapped toCtrl,Alt,Cmdkeys and three mouse or joystick buttons.
给相机加上我们的组件

现在我们是希望移动相机的位置,那么很显然我们需要修改相机的Transform组件的Position信息,我们先给相机加上我们自己的组件:


然后在我们的组件里面要访问同一个GameObject下的其他组件,那么我们可以直接使用GetComponent方法:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        Transform transform = GetComponent<Transform>();
    }
}
然后修改组件里面的值:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        Transform transform = GetComponent<Transform>();
        transform.position += new Vector3(horizontal, vertical, 0);
    }
}
这里我直接暴力的给坐标的x和y轴加上了水平和竖直方向上的输入量,z轴则保持不变。
如果用Rider做IDE的话,可能会看到transform被画了一个波浪线,那是因为所有GameObject一定有一个transform组件,所以我们可以不用使用GetComponent函数来获取,直接用transform成员变量就能访问到,代码就会变成这样:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        transform.position += new Vector3(horizontal, vertical, 0);
    }
}
移动速度太诡异了!

ok,那么我们保存一下代码,让Unity自动编译后,我们把游戏跑起来后试试看:


根据你的机器不同,可能按一下按键就会跑很远,非常灵敏(如果找不到方块了,可以停止游戏再重新运行),或者移动的异常慢(不太有这么差的机器了吧),那么问题出在哪里呢?
因为我们是每次Update函数调用都会进行位置移动,而Update是每帧都会调用,如果我们机器配置比较好,跑的帧率比较高,那么Update调用的次数非常多,就会导致每秒移动的距离越远了,我们可以点击Game窗口右上角的Stats按钮:


看里面Graphics右侧的FPS数值,在我的GTX1060+i7 8700的机器上,往往可以跑到300帧,那么意味着每秒Update调用三百次,我们稍微按久一点按键,那么相机的位置会移动的非常远,正常情况下,我们肯定希望移动速度是按现实时间来的,一秒移动一米这样的效果,那么移动的计算肯定要和当前游戏时间有关,那么代码可以这样改:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        transform.position += (new Vector3(horizontal, vertical, 0) * Time.deltaTime);
    }
}
可以看到我们对每帧移动的距离乘以了一个 Time.deltaTime,这个代表上一帧到这一帧实际过了多久,例如是300帧,那么这个时间则是1/300秒,也就是3.33毫秒左右。
如果换成概念理解,那么Time.deltaTime就是1秒/帧率。而每秒我们都会加上这么多次移动数据,也就是*帧率,那么一秒移动的距离 =  移动距离 * 帧率 * 1秒 / 帧率,两个帧率划掉,结果就只剩下移动距离,这样就相当于每秒固定移动这么多距离。我们改完再跑一下,效果就可控多了:



录屏压缩的有点问题,但是不要介意这些色块,正常应该是没有的

当然你可能觉得这个速度有点慢,那好说,我们再乘以一个系数,就叫Speed吧:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    public float speed = 1.0f;
   
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        transform.position += (new Vector3(horizontal, vertical, 0) * (Time.deltaTime * speed));
    }
}
这里为什么我单独加了个成员变量呢?因为我们不能以程序员的思维来考虑游戏开发里面所有的事情,更多的时候我们需要考虑如何快速的将内容产出,那么前面讲了那么多可以将参数现实在编辑器里面实时调节,现在这个speed参数就是一个绝佳的示例,我们这里的speed会显示在Inspector面板上:


如果我们修改这个值,则会实时的反应到我们编辑器里面正常运行的游戏中,并不需要停下游戏(当然改了代码肯定需要停了游戏编译后再开始游戏),可以试试看。
这样一来,我们可以非常直观快速的尝试出一个比较满意的移动速度的值,比如说我这里调整到11左右比较符合我的手感。
注意:游戏运行的状态下调整的任何参数值,在停止游戏后,都会恢复成运行之前的值,至于如何在游戏运行时也保存这些调整的内容,后面我们会有篇章进行讲解。
从Scene视角观察我们的移动行为:

我们在游戏运行的时候同样可以打开Scene窗口,我们可以将Game或者Scene按住页签然后拖动,则可以修改窗口的布局模式,我们将Game和Scene窗口同时显示出来:


然后我们可以在游戏运行时移动相机的时候观察Scene窗口里面的情况,记得点一下Game窗口以获取焦点,不然可能焦点在Scene窗口有焦点的话你就在输入WASD到Scene窗口了,可以从另外一个视角看到相机移动的情况,比Game窗口更加直观:



录屏压缩的有点问题,但是不要介意这些色块,正常应该是没有的

组件访问其他物体

通过上面的代码和修改,我们现在可以很好的控制我们组件所在的GameObject,但是如果我们希望控制其他GameObject呢?比较现实的问题就是一个RPG游戏,将移动组件放到控制的人物上是比较合理的,但是这个时候组件直接访问transform是访问的人物这个物体上,如果要访问相机呢?
首先我们先将我们的CameraController组件放到我们的Cube上:

  • 点击Main Camera里面我们这个CameraController组件右边的三个点,选择Remove Component移除我们之前添加的组件,避免同时有两个CameraController响应了逻辑。
  • 给Cube物体加上CameraController组件:


然后如果我们直接跑游戏,虽然Game窗口看起来相机仍然像是被移动的样子,但是通过Scene窗口观察,可以看到我们移动的其实是方块本身,并不是相机,只是因为目前移动的方式以及场景内就只有这个方块,看起来像是相机在移动而已。
那么为了从Cube这个物体上的组件访问其他场景内的物体,例如访问我们的Main Camera,我们可以通过Unity提供的方法:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    public float speed = 1.0f;
   
    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        GameObject mainCamera = GameObject.Find("Main Camera");
        mainCamera.transform.position += (new Vector3(horizontal, vertical, 0) * (Time.deltaTime * speed));
    }
}
GameObject的Find函数是个静态函数,到处都可以调用,参数接收一个字符串,也就是需要找的物体的名字,具体API说明可以参考官方文档:
代码也很简单,就是找到场景里面叫"Main Camera"的物体,然后修改它的transform的position值。
改好代码,我们可以运行看看,效果很ok。
但是这样就好了吗?
官方文档里面其实重点强调了,这个Find方法是比较慢的,不适合每帧调用,所以我们可以这样修改:
using UnityEngine;

public class CameraController : MonoBehaviour
{
    public float speed = 1.0f;

    private GameObject mainCamera;

    private void Start()
    {
        mainCamera = GameObject.Find("Main Camera");
    }

    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        mainCamera.transform.position += (new Vector3(horizontal, vertical, 0) * (Time.deltaTime * speed));
    }
}
也就是说,我们在Start的时候找一下,Start只会在第一次调用Update之前调用唯一一次,然后存到成员变量里面,这样Update每次直接取成员变量,这样就避免了每帧重复Find的性能开销。
性能问题解决了,但是我们现在还有一个新的问题,有一定经验的开发者肯定都能意识到,我们这里直接用物体名字来Find,如果哪天这个名字变了呢?很显然这里就找不到了,这样就形成了一个代码逻辑和场景内资源命名的耦合关系。
更进一步,如果是多人协作,场景编辑往往是美术和策划在处理,如果我们在代码里面写死了某个名字,要么是需要培训所有会编辑场景的人员告诉他们这些名字是有特殊意义的,不能改,要么就是不知道的人改了之后逻辑出错。
很显然这些都是不可接受的,我们需要避免直接使用名字来引用场景内的物体,所以我们再继续修改我们的代码。
使用引用关联其他物体

using UnityEngine;

public class CameraController : MonoBehaviour
{
    public float speed = 1.0f;
    public GameObject mainCamera;

    void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        mainCamera.transform.position += (new Vector3(horizontal, vertical, 0) * (Time.deltaTime * speed));
    }
}
这次我们删掉了Start函数里面Find的逻辑,并且把mainCamera这个成员修改成public,之前讲数据序列化的时候我们了解过,public的成员变量会默认被序列化并展示在Inspector的面板上,GameObject这个类型当然是默认支持序列化的:


可以看到针对GameObject物体,这里出现的是一个像输入框的东西,但我们又不能手动输入,点击一下右侧的小圆圈可以显示出来一个新的窗口:


可以看到有Assets和Scene两个页签,现在显示的是Scene下的,当前Scene下所有的GameObject都显示出来了,我们可以双击一下MainCamera选中,那么Inspector上显示的就变成了这样:


可以看到这个输入框比较特殊,只能输入GameObject,有点类似下拉选择框。现在我们再跑一下游戏,可以发现我们可以成功的用Cube上的组件控制Main Camera的移动了。
除了点击小圆圈来赋值GameObject,我们还可以直接将Hierarchy窗口里面的GameObject点击拖动到这个输入框内进行赋值。
而这个编辑流程,其实就是一般情况下,我们通过组件的成员变量引用外部资源的方法,除了GameObject,我们还可以换成组件类型,甚至是自己写的组件类,都可以被序列化,并被展示在面板上可以进行这样的赋值。
这样做的好处很明显,我们引用的是物体本身,而非是名字,无论我们如何修改Main Camera的名字,我们的引用关系仍然是正确的。
深入理解GameObject引用

如何理解一个功能最好的办法就是深入理解它的原理,之前我们深入剖析过一般变量是如何序列化,如何存储到场景文件中,那么针对GameObject,显然也一样。我们再次以文本文件打开场景文件,找到我们这个Cube下的组件:


这个GameObject的序列化方式是一个fileID,很显然是一个引用,那么我们直接搜索这个ID值,则可以在场景里面搜到:


可以看到Unity以”--- !u!1 &”为前缀,后面跟上fileID的值来表示一个GameObject在场景里面数据的开头,后面可以看到GameObject类序列化出来的m_Name就是我们一直在说的Main Camera。
由此可以看到,Unity对场景内的所有GameObject都分配了一个唯一id,如果我们有任何组件里面有成员变量需要引用这个GameObject,则填入的是这个唯一id,当组件被反序列化回来创建真实的类的时候,则再通过这个唯一id找到GameObject赋值回来。
思考题

上面我们移动相机的时候直接暴力修改的X和Y的坐标,其实这是有问题的,左右移动的时候其实跟我们相机的朝向是有关系的,只不过我们默认的相机是面朝Z轴,所以正好左右方向就是X轴的正负轴,所以正确写法我们需要使用当前相机的左或者右方向来计算X和Z应该修改多少,那么具体要怎么做就留作思考题了。
下一章

本章从移动一个相机的需求起步,详细讲解了一个输入响应的真实实现应该怎么做,以及实现过程中会碰到的现实问题,并介绍了如何引用其他GameObject的组件,通过这个引用的能力,可以更好的维护逻辑,避免给编辑场景的人员增加不必要的心智负担。
组成游戏的基本要素还剩下画面呈现,下一章我们将会以最简单的方式了解游戏渲染的基本内容,因为渲染这个部分非常庞大,并且入门门槛比较高,所以只会讲解一些组件说明和大致的流程,具体的渲染详细讲解将会在未来的专门的篇章里面讲解。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-29 15:33 , Processed in 0.462198 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表