Unity游戏开发学习记录

Unity练习2 - 《John Lemon's Haunted

2023-12-21  本文已影响0人  我阿郑

教程地址

✅ 导入资源

打开Unity编辑器 | 选择 Window > General > Asset Store:

  1. 在搜索栏中,输入“3D Beginner: Tutorial Resources”,然后单击搜索结果。

  2. 在“3D Beginner: Tutorial Resources”页面中,单击 Download 并等待下载完成。

3.单击 Import。

4.随后将出现一个警告对话框,提示您的项目设置 (Project Settings) 将被覆盖。这是您希望的结果,因此请单击 Import 以继续。随后将打开 Import Unity Package 窗口,其中列出了所有文件。

5.单击 Import 将文件导入到您的 Unity 项目中。

image.png

✅ 添加角色模型

选择 Assets > Models > Characters 文件夹,然后找到名为 JohnLemon 的模型 | 拖入 Scene 视图 :

image.png

在 Hierarchy 窗口中展开 JohnLemon 并查看其子项:

image.png

此游戏对象有两个子项:一个名称同为 JohnLemon 的游戏对象,还有一个名为 Root 的游戏对象。选择名为 JohnLemon 的子游戏对象

image.png

此游戏对象包含一个名为 Skinned Mesh Renderer 的组件。正是这个组件可让你看到角色。
Skinned Mesh Renderer 是一种特殊类型的 Mesh Renderer,可让Mesh 根据模型所有骨骼的位置和旋转来改变形状。这些骨骼是模型的【子游戏对象】 — JohnLemon 的骨骼是 Root游戏对象的所有子项。

➡️ 在 Hierarchy 中,选择 Root 游戏对象。

按住 Alt (Windows) 或 Option (macOS) 并单击该游戏对象的名称左侧的箭头以【展开 / 折叠】其所有子游戏对象。

image.png

Root中每个游戏对象都代表 JohnLemon 身体的一部分。

在 Hierarchy 中,选择名为 LeftForeArm 的游戏对象。在其 Transform 组件中,找到 Rotation 字段,并将 Y 值设置为 90。 应该会看到 JohnLemon 的左臂弯曲了 90 度。

image.png

JohnLemon 在本质上形成动画的方式如下:角色【父游戏对象上的 Animator 组件】将改变【所有骨骼游戏对象Transform 组件】的旋转,这些改变将一起发生以形成角色的动画。

✅ 创建角色预制体

✅ 角色动画

一个移动时的行走动画(walk)。一个不移动时的空闲动画 (idle)

选择 JohnLemon ,它身上有一个 Animator 组件:

image.png

➡️ 创建 Animator Controller

找到 Assets > Animation > Animators 文件夹。右键单击该文件夹,然后选择 Create > Animator Controller ,命名为“JohnLemon” ,双击它打开动画状态机:

image.png

选择 Parameters 选项卡,有4种参数类型:

一个 float 参数,该参数具有浮点变量值(带小数位的数字)。
一个 int参数,该参数具有整数值(不带小数位的数字)。
一个 bool 参数,该参数具有布尔值(可以是 true 或 false)。
一个 trigger 参数,这是一种特殊的参数,不包含任何值 — 这会导致从一段动画切换到另一段动画。

本例中,角色要么正在行走,要么处于静止状态,因此使用 bool 参数,名为IsWalking(正确拼写和区分大小写很重要):

image.png

➡️ 设置动画

打开 Assets > Animation > Animation, 会看到五个模型,其中两个以 John@ 开头。该命名约定可让您知道这些是 JohnLemon 的动画 :

image.png

展开 John@Idle 和 John@Walk:

image.png

将 Idle 和 Walk 动画拖到 Animator Controller 窗口,Animator Controller 创建了两个包含这些动画的【状态】:

image.png

默认状态显示为【橙色】。在本示例中,默认状态为 Idle,因为您首先拖入的就是这个状态。通过右键单击某个状态并选择 Set As Layer Default State,可以更改默认状态。

现在,您的状态机中有两个状态,但是没有逻辑用于定义应播放的状态。当前,状态机将以默认状态启动,并且永远不会改变,因此 JohnLemon 将始终处于空闲状态。

为了添加一些逻辑,需要创建 Animator 过渡

➡️ 创建 Animator 过渡

右键 Idle 状态,然后选择 Make Transition。Walk 状态同理。

image.png

在 Inspector 中,查看该过渡的设置:

image.png

如果 Has Exit Time 为 true(启用此复选框),则在经过一定时间后,将自动进行过渡,并且状态机将播放下一个状态。

在此游戏中,控制过渡的时间很重要,因此请【禁用 Has Exit Time】 复选框。

现在将显示一条警告消息:“Transition needs at least one condition or an Exit Time to be valid, otherwise it will be ignored”。由于您刚刚禁用了 Has Exit Time 复选框,因此目前没有理由进行此过渡。您需要添加一个条件以提供过渡。

image.png

添加过渡条件:IsWalking 为 true时,从 Idle 到 Walk 过渡。

从 Walk 到 Idle 的过渡同理,只是 IsWalking 为 false时,从 Walk 过渡到 Idle

➡️ 将 Animator Controller 分配给 JohnLemon 预制体

image.png

需要告诉 JohnLemon 预制体,这是它应该使用的 Animator Controller

然后运行起来,角色就开始执行idle动画了。

✅ 使角色对物理产生反应

您的角色将在有不同房间和走廊的鬼屋中游历探索。由于他不是幽灵,因此请务必确保他【不能穿透墙壁】!

编辑JohnLemon 预制体,给它增加 RigidbodyCollider 组件。Rigidbody 组件可以使角色移动,使其撞到墙壁时有所反应。

image.png

保存修改,运行发现角色在不断的向上移动。

这是 Animator 组件上的第三个属性是 Apply Root Motion 引起的,该属性当前已启用。

因为Apply Root Motion会在每一帧对角色图像进行更新,新添加的 Rigidbody 也会在每一帧更新角色图像,二者就会产生冲突。

这就是造成【问题的原因】,很容易解决!去掉Apply Root Motion的勾选就可以了。这时角色只对 Rigidbody 产生反应。

再次运行,人物不再向上移动,但是会在重力的所用下进行下落,因为在 Rigidbody 中勾选了 Use Gravity

禁用掉 Use Gravity 可以防止角色跌落,但这并不是我们想要的。禁用 Use Gravity 的情况下,如果碰撞将角色下推,则该角色仍可能跌落;如果碰撞将角色上推,则该角色会漂走。您肯定不希望发生这种情况,因此您需要使用物理来限制角色的移动。

展开RigidBody 组件上的 Constraints 属性:


image.png

这些 Constraint 属性会限制刚体 (Rigidbody) 的移动方向。

image.png

您的角色需要能够向前和左右移动,因此无需约束其 x 或 z 位置。但是,不能上下移动。勾选 Freeze Position Y 复选框【阻止上下移动】。

如果角色绕其 x 轴旋转,则看起来角色处于正面或背面躺下的状态:
如果角色绕其 z 轴旋转,则看起来角色处于侧面躺下的状态:
勾选 Freeze Rotation X 和 Z 复选框,禁掉这两种情况。

JohnLemon 需要转身,因此不需要勾选 Freeze Rotation Y

✅ 给角色添加Collider

Collider 组件具有许多不同的形状,但是最适合 JohnLemon 的最简单形状是胶囊碰撞体 (Capsule Collider) :

image.png

将 Capsule Collider 的 Height 属性更改为 1.4,并将 Center 属性更改为 (0, 0.7, 0)。 这些更改意味着碰撞体的中部离地面只有其高度的一半,由于 JohnLemon 高约 1.4 米,因此该形状可以很好地覆盖他。

image.png

✅ 给角色添加脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    Vector3 m_Movement; // Unity 中表示矢量的数据类型称为 Vector3,游戏对象的位置便是由这种数据类型所表示

    void Start()
    {
        
    }

    void Update()
    {
        // Horizontal 的轴,由 A 和 D 键以及向左和向右键表示
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        // 设置角色移动位置
        // 该方法有三个参数,矢量的每个坐标对应一个参数。
        // 现在,移动矢量的值在 x 轴上为水平输入,在 y 轴上为 0,在 z 轴上为垂直输入。
        // 第二个参数的 0 后面还有一个 f,用于指示计算机将该数字视为浮点数。
        m_Movement.Set(horizontal, 0f, vertical);
    }
}

现在您需要解决一个小问题。移动矢量由两个数字组成,这两个数字的最大值可以为 1。如果它们两者的值都为 1,则矢量的长度(称为其大小)将大于 1。这便是勾股定理描述的三角形的边之间的关系。

image.png

这意味着您的角色沿对角线移动的速度将比沿单个轴的移动速度更快。为了确保不会发生这种情况,您需要确保移动矢量始终具有相同的大小。为此,可对其进行【标准化】。对矢量进行标准化意味着保持矢量的方向相同,但是将其大小更改为 1。

 m_Movement.Normalize ();

➡️ 识别是否有玩家输入

如果有任何玩家输入,则角色应该为行走状态,如果没有,则应该处于空闲状态。

public class PlayerMovement : MonoBehaviour
{
    Animator m_Animator;
    Vector3 m_Movement; // Unity 中表示矢量的数据类型称为 Vector3,游戏对象的位置便是由这种数据类型所表示

    void Start()
    {
        m_Animator = GetComponent<Animator>();
    }

    void Update()
    {
        // Horizontal 的轴,由 A 和 D 键以及向左和向右键表示
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        // 设置角色移动位置
        // 该方法有三个参数,矢量的每个坐标对应一个参数。
        // 现在,移动矢量的值在 x 轴上为水平输入,在 y 轴上为 0,在 z 轴上为垂直输入。
        // 第二个参数的 0 后面还有一个 f,用于指示计算机将该数字视为浮点数。
        m_Movement.Set(horizontal, 0f, vertical);
        // 标准化
        m_Movement.Normalize();

        // 当 horizontal 为非零值时,hasHorizontalInput 为 true。
        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0f);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);
        bool isWalking = hasHorizontalInput || hasVerticalInput;
        m_Animator.SetBool("IsWalking", isWalking);
    }
}

➡️ 为角色创建旋转

在此游戏中,角色应当只能向前走,因此其面向的方向需要与移动的方向相同。但是,如果 John Lemon 迅速转向所需的方向,看起来会很奇怪,因此您需要将其放慢一点。但是,应该放慢多少呢?

为了解决这个问题,稍微简单一点的方法是考虑角色应该旋转多快?

定义旋转速度

// public的变量会出现在 Inspector 窗口中
public float turnSpeed;

使用了 camelCase(而不是带有 m_ 前缀的 PascalCase)。这是因为该变量为公共变量,而 Unity 命名约定会对公共成员变量使用这种格式。

➡️ 计算角色的前向矢量

请记住:您需要让角色面对的方向与其移动的方向相同。所有的 Transform 组件都有一个前向矢量,因此一个很好的中间步骤是计算您希望角色具有的前向矢量。

Vector3 desiredForward = Vector3.RotateTowards (transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);

Time.deltaTime 是距上一帧的时间(也可以将其视为两帧之间的时间)。那么,为什么需要将 turnSpeed 乘以该数值?

每帧都会调用一次 Update 方法,如果您的游戏以 60 帧/秒的速度运行,则意味着该方法将在一秒钟内被调用 60 次。每次调用都会有很小的变化,这样在 60 帧内您可以得到想要的一秒钟内的变化。但是,以 30 帧/秒的速度运行的游戏呢?只有一半的方法调用将在同一时间进行,因此只会发生一半的旋转。您不希望每秒的帧数影响角色的旋转速度,因为这是不对的!

如果不是每帧进行更改而是每秒进行更改呢?这样,事情就变得容易得多了。为此,您需要将每秒所需的任何更改乘以一帧花费的时间。这段代码正是这样做的。

➡️ 调整 turnSpeed 变量

turnSpeed 变量是您希望角色每秒旋转的角度(以弧度为单位)。然后,此变量乘以 Time.deltaTime 即可得出角色在此帧应旋转的量。

弧度是角度的另一种度量方式;与度相似,但更自然。一个圆为 弧度,即大约为 6 弧度。您的角色总是选取最短的旋转长度,因此,角色转过的最大角度约为 3 弧度。

据此可知,turnSpeed 为 3 意味着角色转一整圈大约需要一秒钟。这样的速度实际上很慢。turnSpeed 为 6 意味着转一圈大约需要半秒,这仍然很慢。让我们尝试将值设置为 20,然后看看效果如何。

public float turnSpeed = 20f;

➡️ 创建和存储旋转

public class PlayerMovement : MonoBehaviour
{
    Animator m_Animator;
    // Unity 中表示矢量的数据类型称为 Vector3,游戏对象的位置便是由这种数据类型所表示
    Vector3 m_Movement;

    // 定义旋转速度
    // public的变量会出现在 Inspector 窗口中
    // 使用了 camelCase(而不是带有 m_ 前缀的 PascalCase)。
    // 这是因为该变量为公共变量,而 Unity 命名约定会对公共成员变量使用这种格式。
    public float turnSpeed = 20f;

    // 四元数 (Quaternion) 是存储旋转的一种方式,可用于解决将旋转存储为 3D 矢量时遇到的一些问题。
    // Quaternion.identity 就是为其赋予无旋转的值
    Quaternion m_Rotation = Quaternion.identity;

    void Start()
    {
        m_Animator = GetComponent<Animator>();
    }

    void Update()
    {
        // Horizontal 的轴,由 A 和 D 键以及向左和向右键表示
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        // 设置角色移动位置
        // 该方法有三个参数,矢量的每个坐标对应一个参数。
        // 现在,移动矢量的值在 x 轴上为水平输入,在 y 轴上为 0,在 z 轴上为垂直输入。
        // 第二个参数的 0 后面还有一个 f,用于指示计算机将该数字视为浮点数。
        m_Movement.Set(horizontal, 0f, vertical);
        // 标准化
        m_Movement.Normalize();

        // 当 horizontal 为非零值时,hasHorizontalInput 为 true。
        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0f);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);
        bool isWalking = hasHorizontalInput || hasVerticalInput;
        m_Animator.SetBool("IsWalking", isWalking);

        Vector3 desiredForward = Vector3.RotateTowards(transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
        m_Rotation = Quaternion.LookRotation(desiredForward);
    }
}

➡️ 将移动和旋转应用于角色

//  移动
m_Rigidbody.MovePosition (m_Rigidbody.position + m_Movement * m_Animator.deltaPosition.magnitude);
// 旋转
m_Rigidbody.MoveRotation (m_Rotation);

MovePosition 唯一的参数:其新位置。
新位置从刚体的当前位置开始,在此基础上加上: m_Movement * m_Animator.deltaPosition.magnitude

➡️ 更改 Update 方法

Update 方法更改为 FixedUpdate 方法。这是 Unity 【自动调用】的另一个特殊方法,但是这个方法是通过物理适时调用的。FixedUpdate 不是在每个渲染的帧之前被调用,而是在物理系统处理所有碰撞和其他已发生的交互之前被调用。默认情况下,每秒正好调用 50 次这个方法。

public class PlayerMovement : MonoBehaviour
{
    Animator m_Animator;
    Rigidbody m_Rigidbody;
    // Unity 中表示矢量的数据类型称为 Vector3,游戏对象的位置便是由这种数据类型所表示
    Vector3 m_Movement;

    // 定义旋转速度
    // public的变量会出现在 Inspector 窗口中
    // 使用了 camelCase(而不是带有 m_ 前缀的 PascalCase)。
    // 这是因为该变量为公共变量,而 Unity 命名约定会对公共成员变量使用这种格式。
    public float turnSpeed = 20f;

    // 四元数 (Quaternion) 是存储旋转的一种方式,可用于解决将旋转存储为 3D 矢量时遇到的一些问题。
    // Quaternion.identity 就是为其赋予无旋转的值
    Quaternion m_Rotation = Quaternion.identity;

    void Start()
    {
        m_Animator = GetComponent<Animator>();
        m_Rigidbody = GetComponent<Rigidbody>();
    }

    void FixedUpdate()
    {
        // Horizontal 的轴,由 A 和 D 键以及向左和向右键表示
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        // 设置角色移动位置
        // 该方法有三个参数,矢量的每个坐标对应一个参数。
        // 现在,移动矢量的值在 x 轴上为水平输入,在 y 轴上为 0,在 z 轴上为垂直输入。
        // 第二个参数的 0 后面还有一个 f,用于指示计算机将该数字视为浮点数。
        m_Movement.Set(horizontal, 0f, vertical);
        // 标准化
        m_Movement.Normalize();

        // 当 horizontal 为非零值时,hasHorizontalInput 为 true。
        bool hasHorizontalInput = !Mathf.Approximately(horizontal, 0f);
        bool hasVerticalInput = !Mathf.Approximately(vertical, 0f);
        bool isWalking = hasHorizontalInput || hasVerticalInput;
        m_Animator.SetBool("IsWalking", isWalking);

        Vector3 desiredForward = Vector3.RotateTowards(transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
        m_Rotation = Quaternion.LookRotation(desiredForward);
    }

    void OnAnimatorMove()
    {
        //  移动
        m_Rigidbody.MovePosition(m_Rigidbody.position + m_Movement * m_Animator.deltaPosition.magnitude);
        // 旋转
        m_Rigidbody.MoveRotation(m_Rotation);
    }
}

✅ 调整 Game 视图设置

image.png

单击 Play 按钮运行,然后使用箭头键四处移动 John Lemon。

上一篇 下一篇

猜你喜欢

热点阅读