安卓-Unity-游戏开发学习手册-二-

237 阅读1小时+

安卓 Unity 游戏开发学习手册(二)

原文:Learn Unity for Android Game Development

协议:CC BY-NC-SA 4.0

七、制作 Android 应用

在这一点上,我们的游戏还远未完成,在接下来的几章中,你将学习如何添加关卡、关卡、菜单、更多的 UI 元素和许多其他功能。

但是我们有一个基本的游戏,现在可以作为一个游戏来玩,而且肯定是可以玩的。我们的画布已经就位——这将是重要的一点。简而言之,我觉得你已经等得够久了。是时候让这个东西在你的 Android 设备上运行了。

在本章中,您将学习如何创建 APK,如何在手机或平板电脑上测试游戏,以及如何在游戏中添加触摸控制。最终,你将能够把你制作的游戏带到任何地方,并把它放进你的口袋。理论上,你甚至可以把它公之于众。但是我不建议现在就这么做....

添加触摸控制

在我们开始构建 APK 之前,添加触摸控件是个好主意。现在,你可以在带有蓝牙键盘的 Android 设备上使用你的应用,但对大多数人来说,这不是一个非常方便的游戏方式。大多数人连蓝牙键盘都没有。你想让他们只用手机就能玩。幸运的是,添加触摸控件并不是一个太复杂的过程。

如果我们要做一个无止境的转轮,增加触摸控制会非常简单。在这种情况下,我们需要做的就是使用线Input.GetMouseButtonDown(0)而不是Input.GetKey(KeyCode.Space)或者任何我们用于跳转的东西。在 Unity 中,鼠标点击和触摸屏幕被注册为完全相同的事情,因为我们不需要知道用户在屏幕上点击的位置,这对于控制我们的游戏来说已经足够了。

我们将在未来的章节中探讨如何创造一个无止境的跑步者。如果这是您感兴趣的事情,您可以直接跳到下一节构建 APK。否则,请继续关注我,我们将看看如何实现适当的触摸控制。

设计控件

你要做的第一件事是设计一些触摸控件,当放在你的游戏上面时,看起来像一个部件。它们需要清晰且容易找到,但不分散玩家的注意力或掩盖游戏的任何重要元素也很重要。出于这个原因,选择一些看起来温和半透明的东西是一个不错的选择。

同样重要的是,按钮要符合你游戏世界的审美。你选择的颜色需要突出不同的层次,但又不与花哨的风格相冲突。随着玩家在游戏中的进展,游戏世界的调色板会发生变化,这是很正常的:也许一个关卡设置在水下,有许多蓝色和绿色,另一个关卡设置在空间,有许多黑色和白色。如果你把你的按钮做成红色或绿色,你会发现它们有时在游戏世界里看起来很丑。

出于这些原因,我把我的按钮做成浅灰色,轮廓略深一点。我还用图像编辑器 GIMP 应用了像素化滤镜,并将不透明度设置为 80%。结果应该是看起来不会太分散注意力,也不会觉得格格不入。你可以在图 7-1 和 7-2 中创建我的东西。

A431865_1_En_7_Fig2_HTML.jpg

图 7-2。

A button

A431865_1_En_7_Fig1_HTML.jpg

图 7-1。

An arrow

注意,我只需要创建一个方向箭头。这是因为我可以简单地反转图像来创建相反的箭头——不需要花时间画两个。

添加我们的控件

现在,我们需要将这些添加到我们的游戏中,并让它们做一些事情。首先,像处理其他图片一样,将图片添加到项目的 Sprite 文件夹中。现在右键单击你的画布下面的层级——你希望这个新元素成为Canvas GameObject 的子元素——并选择 UI ➤图像。游戏中会出现一个看起来像白色大方块的图像。选择这个元素,在它显示源图像的地方,拖放你从你的精灵文件夹中创建的箭头精灵。上面写着锚的地方,选择左下角。拖动并定位箭头,使其位于画布的左上角(此时可能会显得很大),然后将水平刻度更改为负数,使箭头指向左边而不是右边。换句话说,将宽度从 1 更改为–1,这样它就会自己折回来。

取决于你画的箭头有多大,你需要试着确定这些图片的大小是正确的。一旦 APK 在你的手机上运行,你可以稍后对此进行调整,但现在我将我的设置为 X =–2 和 Y = 2(见图 7-3 )。

A431865_1_En_7_Fig3_HTML.jpg

图 7-3。

Positioning the first control

现在对第二个箭头做同样的事情。这次位置会稍微偏右,主播还是左下方。当然,这次的规模将会是正数。之后,您可以添加跳转按钮,这将是我们的通用“按钮”图像。这一个将被锚定在屏幕的右下角。见图 7-2 。根据需要重命名按钮。

你会发现跳转按钮和右箭头可能会在这一点上重叠,或者看起来非常接近(如图 7-4 ),但你不需要担心这一点。通过将图像设置为锚定到屏幕的底部角落,您声明所有的位置信息都是相对于那个角落的。Unity 不知道你将要玩的手机屏幕或任何设备的尺寸,因此画布的形状可能会有点奇怪。但是只要跳转按钮被设置在离右上角一定距离的地方,箭头和左上角也是一样,一旦你点击播放,它们就应该在正确的位置。

A431865_1_En_7_Fig4_HTML.jpg

图 7-4。

The buttons don’t look quite right yet, but have faith

当然,要进行预览,你可以点击播放,看看它在你的电脑屏幕上是什么样子(图 7-5 )。当放置你的箭头时,在边缘留一点空间是值得的,以确保它们不会太狭窄。

A431865_1_En_7_Fig5_HTML.jpg

图 7-5。

See? Our arrows look lovely!!

控件编码

现在你已经有了你的按钮,是时候让它们真正做点什么了。考虑到这一点,我们需要创建一个空的游戏对象,作为这些元素的容器。右键单击画布,选择 Create Empty,然后将这个新对象锚定到屏幕底部。单击拉伸,使其与屏幕一样宽,然后将元素拖动到层次结构中的此处。调用您的新容器 TouchController。

进入你的Player脚本(正如我们在第六章中了解到的,它实际上是一个类),我们将添加两个公共布尔。记住,bools 是可以为真或假的变量——1 或 0——因为它们是公共的,它们可以被我们游戏中的其他类(脚本)访问。

这些新变量将被称为moveRightmoveLeft,您将使用它们来完成这项工作(暂时不要粘贴这段代码):

if (moveright)
 {
 rb.velocity = new Vector2(movespeed, rb.velocity.y);
 }
 if (moveleft)
 {
 rb.velocity = new Vector2(-movespeed, rb.velocity.y);
 }

请注意,这与手动按下左右箭头非常相似。

这些可触摸的图像元素的工作方式是,它们只允许我们在被点击和被释放时进行注册。这意味着我们不能问 Unity 按钮是否“被按下”相反,我们需要根据按钮何时被点击和何时被释放来设置我们的布尔值为真或假。

我告诉你不要粘贴代码的原因是有一种更简单的方法可以做到这一点。我们已经有一堆代码来处理玩家左右行走,目前它包括动画之类的东西——所以我们不想重复。

相反,我们将使用一个名为或的命令。这基本上允许我们询问两件事情中的一件是否正在发生。在这种情况下,我们要问的是玩家是否按下了箭头键,或者我们的布尔函数之一是否为真。在 C#中,我们编写或使用符号||

因此,我们的代码现在应该是这样的:

if (moveLeft || Input.GetKey(KeyCode.LeftArrow))
        {
            rb.velocity = new Vector2(-movespeed, rb.velocity.y);
            anim.SetBool("Walking", true);
            if (facing == 1)
            {
                transform.localScale = new Vector3(-1f, 1f, 1f);
                facing = 0;
            }

        } else if (moveRight || Input.GetKey(KeyCode.RightArrow))
        {
            rb.velocity = new Vector2(movespeed, rb.velocity.y);
            anim.SetBool("Walking", true);
            if (facing == 0)
            {
                transform.localScale = new Vector3(1f, 1f, 1f);
                facing = 1;
            }

        } else
        {
            anim.SetBool("Walking", false);
        }

现在,当你按左右键时,你的角色应该还在移动。但是,如果您将其中一个布尔值设置为 true(记住,所有变量在第一次创建时默认为 0,即 false),那么玩家将自动移动。

同样,我希望您将处理玩家角色跳跃动作的代码移到一个新的公共方法中。公共方法是一种方法——一段指令代码——可以从其他类(脚本)中执行。这意味着我们现在可以通过从外部脚本激活来强迫玩家跳跃。

我们仍然希望在我们的Update方法中注册按钮按压,但是我们没有包含跳转代码,而是引用了包含所述代码的新公共方法。

因此,您将像这样创建公共方法:

public void jump() {

    if (onGround) {
        rb.velocity = new Vector2(rb.velocity.x, jumppower);
    }
}

然后在Update方法中,您可以简单地这样说:

if (Input.GetKey(KeyCode.Space))
{
    jump();
}

整个事情应该是这样的:

void Update() {

        if (moveLeft || Input.GetKey(KeyCode.LeftArrow))
        {
            rb.velocity = new Vector2(-movespeed, rb.velocity.y);
            anim.SetBool("Walking", true);
            if (facing == 1)
            {
                transform.localScale = new Vector3(-1f, 1f, 1f);
                facing = 0;
            }

        } else if (moveRight || Input.GetKey(KeyCode.RightArrow))
        {
            rb.velocity = new Vector2(movespeed, rb.velocity.y);
            anim.SetBool("Walking", true);
            if (facing == 0)
            {
                transform.localScale = new Vector3(1f, 1f, 1f);
                facing = 1;
            }

        } else
        {
            anim.SetBool("Walking", false);
        }

        if (Input.GetKey(KeyCode.Space))
        {
            jump();
        }
    }

    public void jump() {

if (onGround) {
rb.velocity = new Vector2(rb.velocity.x, jumppower);
}
        }

这很重要,因为我们实际上给了自己一些访问点,可以用来从脚本之外控制玩家。我们将在控制按钮的脚本中利用这一点。如果你很难理解这里发生了什么,可以考虑重读第六章中关于面向对象编程(OOP)的部分。

看到了吗?边走边学理论总是好的。

好了,现在我们已经完成了。是时候让按钮有反应了。首先创建另一个新脚本,这次名为TouchTouch将包含以下代码:

public class Touch : MonoBehaviour
{
    private Player player; 

    void Start()
    {
        player = FindObjectOfType<Player>();
    }

    public void PressLeftArrow()
    {
        player.moveRight = false;
        player.moveLeft = true;
    }
    public void PressRightArrow()
    {
        player.moveRight = true;
        player.moveLeft = false;
    }
    public void ReleaseLeftArrow()
    {
        player.moveLeft = false;
    }
    public void ReleaseRightArrow()
    {
        player.moveRight = false;

    }

    public void Jump()
    {
        player.Jump();

    }
}

这基本上是一个公共方法的集合,每个方法都会以某种方式与Player脚本(类)交互。你可能已经猜到了,我们现在要让每个屏幕按钮触发其中一个方法。

现在回到 Unity,添加这个新的Touch脚本作为我们之前创建的TouchController空游戏对象的组件(见图 7-6 )。

A431865_1_En_7_Fig6_HTML.jpg

图 7-6。

Add the Touch script to the empty GameObject

现在,我们将向左箭头添加一个组件—这次是一个称为事件触发器的新组件。转到添加组件➤事件➤事件触发器。现在点击添加新事件类型➤指针向下。点击出现在右边的小加号(+),然后拿起TouchController游戏对象并将其拖入无(对象)框。然后单击右侧的下拉菜单,选择触摸➤按左箭头()。基本上,你是在告诉 Unity 你希望指针向下事件(按下按钮的动作)触发Touch脚本中的公共方法PressLeftArrow

点按“添加新事件类型”,然后选取“指针向上”。这记录了手指从箭头上抬起的动作。现在选择触摸➤释放左箭头()进入这里。如果一切正常,它看起来应该如图 7-7 所示。

A431865_1_En_7_Fig7_HTML.jpg

图 7-7。

Event triggers added

正如您可能已经猜到的,您需要对右箭头做同样的事情,但是使用各自的右箭头方法。对于跳转按钮,您将做一些稍微不同的事情,忽略向上指针类型的事件,选择向下指针的Jump()方法。

单击“播放”,您应该能够对此进行测试。如果你没有触摸屏笔记本电脑来试用,那么只需用鼠标点击按钮就可以达到同样的效果。如果它现在感觉不太响应,也不要担心——一旦它在 Android 设备上运行,应该会是一个不同的故事。

说到这里....

创造你的第一个 APK

现在你有了合适的输入形式,你终于可以在 Android 设备上测试你所有的努力了。

首先,确保你已经通过按 Ctrl+S 再次保存了你的场景。接下来,前往文件➤构建设置。您会在该窗口的顶部看到一个框,显示“构建中的场景”,这基本上是向您显示您创建的哪些场景希望包含在最终产品中。要添加你的级别 1,只需将它从项目窗口的场景文件夹中拖放到构建区域的场景中。它应该如图 7-8 所示。

A431865_1_En_7_Fig8_HTML.jpg

图 7-8。

Level 1 is currently the only scene in our build

当你有更多的场景时(你会的),你需要确保顶部的场景是你想首先运行的场景。这通常意味着一个闪屏或某种菜单(但是记住,如果你有免费的 Unity 许可,你的闪屏之前会有一个 Unity)。

现在,不要担心纹理压缩。这对于创建 3D 游戏非常有用,并有助于优化您的应用。对于我们的目的来说,现在还没有必要(我们的应用很小,资源也不是很密集),并且不是所有的 Android 平台都支持所有类型的压缩。我将在本书的后面讨论纹理压缩。

你会注意到这个窗口也有选择平台的选项,现在它可能显示 PC、Mac 和 Linux 单机版。您需要通过单击 Android 选项,然后单击切换平台来更改这一点。

播放器设置

接下来,点击平台滚动框下面的播放器设置按钮,你会发现一些新的选项在检查器中为你打开。在这里,您可以定义将要构建的 APK 的许多属性:比如图标、包名和方向(参见图 7-9 )。

A431865_1_En_7_Fig9_HTML.jpg

图 7-9。

Player Settings is where you set the properties for your new APK

在我们开始设置之前,请填写顶部的选项。在这里,您可以输入公司名称和应用名称。如果你让它保持原样,那么这个公司将会是 DefaultCompany,这个应用将会被叫做你的项目的名字。这里还有一个添加图标的选项。我们现在不会担心这个问题,我们将在讨论上传和营销您的应用时再次讨论这个问题。现在,我们将坚持使用默认的 Unity 图标。

现在,剩下的这些选项是做什么的?

分辨率和演示

我们首先要看的是分辨率和呈现方式。目前,默认方向可能设置为自动旋转,在它下面有一个勾框显示哪些方向是允许的——现在,答案可能是所有方向。

如果你想做一个益智游戏(在下一章讨论),你可能会想支持纵向。甚至还有少量类似 Fotonica 和 Sonic Jump 的人像动作游戏。但在大多数情况下,坚持横向更有意义,这将防止你的玩家感到太拥挤,这将显示最多的屏幕。在你玩游戏时握着手机的控制器也倾向于只支持横向。因此,要么在默认方向框中选择一个方向,要么取消选中下面的两个纵向选项。

图标

下一部分是图标部分。正如我前面说过的,图标是我们以后会用到的东西,但是正如你所看到的,这里有空间添加各种不同分辨率的图标。如果你想在这里放些东西,只使用一张图片就可以了,在这种情况下,最好使用高分辨率的图片,而不是低分辨率的。与缩小相比,放大会产生更好的图像质量。稍后我将对此进行更详细的讨论——暂时将它保留为空是很好的。图 7-10 显示了默认图标安装后的样子。

A431865_1_En_7_Fig10_HTML.jpg

图 7-10。

Soon this will be your app

飞溅图像

接下来是 Splash Image,我们将再次保留空白——特别是当你应该在免费许可证上保留默认图像时。

其他设置

其他设置给了我们很多可以玩的东西。您可以更改与渲染相关的设置,以及最低 API 级别、写权限、安装位置等。这其中的大部分是不言自明的,其余的我们将在后面的章节中再来讨论。

您完全可以跳过这一部分,将所有内容都保留为默认值,但是这里有一两件事情值得注意。例如,包标识符是您输入包名的地方。正确的命名如下:com . your company here . your app name here。您需要在应用构建之前设置这个名称,所以请继续使用您自己的详细信息输入一个包名称。你现在选择什么并不重要,但是在发表之前一定要好好想想。先不说别的,应用一旦上传到 Play Store,你就不能再更改这个了。

版本和版本代码分别是为了我们和 Android。版本就是我们和我们的用户所看到的版本。不过,每次你在 Play Store 更新应用时,版本代码都需要更改。即使您做了最微小的更改,然后上传了一个新的 APK,您也需要确保新版本的代码高于上一个版本。

同时,最低 API 级别定义了你想要支持的 Android 的最低版本。默认情况下,这大概设置为 Android 2.3.1(姜饼)。在撰写本文时,谷歌刚刚发布了 Android O 的开发者预览版,可供用户使用的最新版本是 7.1(牛轧糖)。

你的 API 等级越低,就有越多的人能够下载你的应用。但如果你把它定得太低,你将无法访问 Android 的一些后期功能。同样,我将在本书的后面部分详细讨论这些内容。

准备您的手机

在你尝试在手机上运行游戏之前,还有一件事要做,那就是准备好手机。首先,这意味着你需要允许 USB 调试。不幸的是,我不能给你一步一步的指导,因为每部 Android 手机都是不同的(这就是与 Android 合作的奇妙之处和沮丧之处)。

USB 调试让您可以通过 USB 连接安装应用,然后获得关于它们运行情况的反馈。见图 7-11 。

A431865_1_En_7_Fig11_HTML.jpg

图 7-11。

Allowing USB debugging

通常,这个选项可以在你的手机设置中的开发者选项下找到。在一些手机中,这是隐藏的,所以在谷歌上搜索一下,看看如何在你的特定硬件上打开 USB 调试。图 7-11 显示了三星 Galaxy 设备上的此选项。

您需要更改的另一个设置是“允许安装来自 Play Store 以外来源的应用”这通常有一个标题未知的来源,可以在您的设置的应用部分,或锁定屏幕和安全部分找到。再次,快速谷歌搜索将帮助你。正如您所料,此设置确保您的手机将接受来自其他来源的 APKs 如您的 PC-因此您需要将其打开。参见图 7-12 。

A431865_1_En_7_Fig12_HTML.jpg

图 7-12。

You need to tick the Unknown Sources option

最后,确保你已经在电脑上为你的手机安装了驱动程序。这可能会发生在你第一次连接它来传输照片时,但为了以防万一,你可能需要做另一次搜索,并为你的手机获取这些驱动程序文件。但如果你不能解决这个问题,还有其他方法可以让应用在你的手机上运行。

扣动扳机

现在剩下要做的就是构建你的应用并运行它。继续通过 USB 端口将手机插入 PC,然后点击“构建并运行”。如果一切按计划进行,Unity 将构建 APK,然后安装到您的手机上。在显示启动画面后,它应该会直接出现在你面前。成功!

Technical Difficulties

不幸的是,当我这样做的时候,我遇到了一些技术难题,需要一段时间才能解决。对你来说幸运的是,我的工作就是处理这些事情,让你的生活更轻松。

在最近升级 Android SDK 工具后,兼容性似乎已经被破坏,构建停止工作。这意味着,如果你最近才安装 Android SDK,事情可能不会像它们应该的那样工作。解决办法是找一个旧版本的 Android SDK 工具,替换掉 SDK 根目录下的文件夹(把旧的改名为 ToolsXXX 什么的)。

希望当你读到这里的时候,这个小问题已经解决了。如果没有,你可能要做更多的谷歌搜索。不幸的是,这是开发的本质,尤其是在 Android 上的开发。但当它最终发挥作用时,确实会让一切变得更有价值。

如果一切都按计划进行,你现在应该有一个带有触摸控制的手机应用的工作版本。您可能会发现 UI 元素有点小,所以移动它们,并根据您的需要调整它们的大小。

A431865_1_En_7_Fig13_HTML.jpg

图 7-13。

That UI is going to need to get a little larger

借此机会享受你的成就。你刚刚创建了你的第一个 Android 应用。去给妈妈看看。

但是不要对自己太满意——还有很长的路要走。在第八章中,我们将创建多个级别、菜单和保存文件。我们才刚刚开始。

A431865_1_En_7_Fig14_HTML.jpg

图 7-14。

We did it!

八、通过检查点、关卡和保存文件来扩展游戏世界

这本书的很多内容都是可选的。真的,你已经可以构建一个接近完成的游戏了。它现在运行在 Android 上,有动画和声音,通过从你已经学到的东西中推断,你可能可以创建一堆新元素,并将其包装成一个“完整”的游戏。当然,我希望你能坚持到最后,因为我认为这会给你带来更好的成品和更多的编码知识。(此外,你还将学习如何构建虚拟现实应用。)

也就是说,如果你想让你的游戏感觉完整的话,至少还有几个元素我们还没有涉及到。这就是本章的内容。

首先,如果你的关卡超过了几个平台的长度,你就需要引入关卡,这样你的玩家就不会因为不断被送回起点而沮丧。第二,您可能还想创建多个级别,并找到在它们之间过渡的方法。如果你有不止一个关卡,你需要一些关卡选择系统(菜单)和保存玩家进度的方法。在你完成一个有挑战性和有趣的游戏之前,这些是你唯一需要学习的东西。所以让我们开始吧。

添加检查点

在你开始增加关卡之前,让你的关卡长一点是有意义的。这是有趣的一点,所以复制和粘贴一些更多的地面精灵,添加更多的钉子和水池,让你的想象驰骋。在第十章中,我们将讨论什么是好的关卡设计——所以现在还不要投入太多的时间和精力。把这个关卡设计当作一个占位符,在你玩的时候给你一些东西。

不过,一定要确保沿途有一些可以杀死玩家的元素。这是检查站发挥作用的必要条件。你可以在图 8-1 中看到我是怎么做的。

A431865_1_En_8_Fig1_HTML.jpg

图 8-1。

My level 1 is very flat and horizontal, keeping things simple for new players

现在,我们将创建第一个检查点。这只是一个空的游戏物体,我们将把它放入游戏的不同位置。猜猜我们会把第一个叫做什么?1 号检查站。

处理关卡放置最明显的方法是在玩家遇到新的重大挑战之前或之后放置关卡。在游戏的后期,我们可能会尝试组合多种危险,以创造一系列的挑战,从而稍微增加难度。但是现在,让我们从一个新的检查点开始,并将其直接放置在第一个尖峰坑之前。

你想让这个空的物体成为一个触发器,你可能还记得,这意味着我们可以检测到什么时候有人通过碰撞笼,但不会撞到东西。首先,给它一个圆形碰撞器,然后勾选检查器中的触发框。这意味着我们可以知道用户何时越过检查点,但他们不会知道。

让这个对象变得相当大,因为玩家不意外跳过检查点是很重要的。您可以使用调整大小工具或在检查器中输入半径来完成此操作。我的是 3,大到足以防止它被绕过。参见图 8-2 。

A431865_1_En_8_Fig2_HTML.jpg

图 8-2。

Behold the glory of checkpoint 1

您可能已经猜到了,是时候多写一点脚本(类)了。所以创建一个脚本,命名为Checkpoint。我们还将编辑我们的Player脚本,所以也在 Visual Studio 中打开它。

编写一个更合适的死亡剧本

事实上,我们首先要编辑参与人 1 的剧本。如果你读过第六章中关于面向对象编程的部分,那么你就会知道我们的Player角色实际上是一个叫做对象的构造。这个对象有属性(变量)和方法(行为),正是通过这些,我们的其他对象才能与之交互。

如果我们想移动我们的球员,通过操纵Player脚本中的变量来做是有意义的。我们首先创建两个公共浮动:startxstarty。当重生时,这些将是我们玩家的起点。

游戏的开始实际上是我们的第一个检查点,所以我们在游戏开始时繁殖我们的玩家应该做的第一件事是找出他们在世界上的什么地方,这样我们就可以在他们每次死亡时将他们送回这个确切的点。目前,我们把玩家送回我们编辑过的一组特定的坐标,如果我们要在场景视图中移动玩家,我们必须每次都更新这些数字。当我们开始创建多个关卡并使用相同的脚本时,这将会是一个更大的问题。

我们要做的是检查玩家在物体第一次被创建时的位置,并把它作为重生的位置。

为此,您只需向Start方法添加以下代码:

startx = transform.position.x;
starty = transform.position.y;

前面的代码在第一次创建游戏对象时获取它的位置,并分别存储 X 和 Y 坐标。

现在找到你的Death方法。这将会出现在你的Hazards剧本或者你的Player剧本中(如果你是我的一个高材生并且你移动了它,就是这样)。不管怎样,你现在要把数字换成新的变量。如果Death方法在Player脚本中,您可以简单地编写如下:

transform.position = new Vector2(startx, starty);

否则,如果它仍然在Hazards脚本中,它将看起来像这样:

player.transform.position = new Vector2(player.startx, player.starty);

不管怎样,我们的球员现在回到了我们开始时读到的位置。我建议您现在移动您的Death方法,以便它在Player脚本中。如果你还没有找到这样做的方法,需要一点帮助,只需更新你的脚本如下。

Player脚本:

public class Player : MonoBehaviour {
    public Rigidbody2D rb;
    public int movespeed;
    public int jumppower;
    public Transform groundCheck;
    public float groundCheckRadius;
    public LayerMask whatIsGround;
    private bool onGround;
    public int coins;
    private Animator anim;
    private int facing;
    public bool moveLeft;
    public bool moveRight;
    public float startx;
    public float starty;
    public GameObject Blood;

    void Start () {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponent<Animator>();
        facing = 1;
        startx = transform.position.x;
        starty = transform.position.y;

    }

    void FixedUpdate()
    {
        onGround = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, whatIsGround);
    }

    void Update() {

        if (moveLeft || Input.GetKey(KeyCode.LeftArrow))
        {
            rb.velocity = new Vector2(-movespeed, rb.velocity.y);
            anim.SetBool("Walking", true);
            if (facing == 1)
            {
                transform.localScale = new Vector3(-1f, 1f, 1f);
                facing = 0;
            }

        } else if (moveRight || Input.GetKey(KeyCode.RightArrow))
        {
            rb.velocity = new Vector2(movespeed, rb.velocity.y);
            anim.SetBool("Walking", true);
            if (facing == 0)
            {
                transform.localScale = new Vector3(1f, 1f, 1f);
                facing = 1;
            }

        } else
        {
            anim.SetBool("Walking", false);
        }

        if (Input.GetKey(KeyCode.Space))
        {
            Jump();
        }
    }

    public void Jump() {

        if (onGround)

        {
            rb.velocity = new Vector2(rb.velocity.x, jumppower);
        }

        }

    public void Death()
    {
            StartCoroutine("respawndelay");

    }

    public IEnumerator respawndelay()
    {
        Instantiate(Blood, transform.position, transform.rotation);
        enabled = false;
        GetComponent<Rigidbody2D>().velocity = Vector3.zero;
        GetComponent<Renderer>().enabled = false;
        yield return new WaitForSeconds(1);
        transform.position = new Vector2(startx, starty);
        GetComponent<Renderer>().enabled = true;
        enabled = true;

    }
}

Hazards脚本:

public class Hazards : MonoBehaviour
{

    private Player player;
    // Use this for initialization
    void Start()
    {
        player = FindObjectOfType<Player>();
    }

    // Update is called once per frame
    void Update()
    {

    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player")
        {
            player.Death();
        }
    }

}

编写检查点脚本

你可能已经知道接下来会发生什么了。我们需要做的就是在检查点进入碰撞器时改变startxstarty的值。

我们的新Checkpoint脚本很简单:

public class Checkpoint : MonoBehaviour {

    private Player player;

    void Start()
    {
        player = FindObjectOfType<Player>();
    }

    void Update()
    {

    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player")
        {
            player.startx = transform.position.x;
            player.starty = transform.position.y;
        }
    }

}

别忘了,您还需要将这个脚本附加到有问题的检查点。然后使它成为一个预置,这样你就可以在将来很容易地在关卡周围添加更多的关卡。试试看,你会发现你现在在关卡重生,而不是在游戏开始的时候。

事实上,你应该出现得足够快以至于被淋上你自己的血(见图 8-3 )。很好。

A431865_1_En_8_Fig3_HTML.jpg

图 8-3。

Kevin returns!

现在在聪明的地方多做几个检查点,稍微玩一玩,看看什么效果最好。在你的层次结构中用逻辑的方式组织它们,也许给它们一个名为Checkpoints的父对象。当然,你可以提供某种可见的指示器来指示你的检查点——比如在《刺猬索尼克》游戏中发现的帖子——但是现在玩家只是在游戏中的不同点重新出现是很常见的。当我们加载时,我们接受这一点作为暂停怀疑的一部分,它已经成为视频游戏语言的一部分。

更上一层楼

我们已经走了很长的路,但我们的游戏仍然只有一个水平。是时候引入某种形式的真正进展了。

要做到这一点,我们只需制作另一个代表关卡结束的游戏对象,并使其成为一个触发器。鉴于凯文是一名宇航员,他的关卡结束时成为某种太空火箭是有道理的。稍后,我们将制作动画。就目前而言,简单地到达太空火箭将结束这一水平。图 8-4 可以看到我的火箭。

A431865_1_En_8_Fig4_HTML.jpg

图 8-4。

This rocket ship signals the end of each level

创造一个新的水平

在添加脚本之前,我们首先需要创建另一个关卡。在你这么做之前,一定要让你的Player游戏对象成为一个预设(把Player拖到预设文件夹中,并且一定要带上主摄像机和检查地面)。这样,您所做的任何更改都会在您所做的所有级别中得到全面反映。为你的Canvas和它所有的孩子做同样的事情。

一旦你做到了这一点,尝试使用这个简单的小技巧,使一个新的水平:只需点击文件➤保存场景为,并呼吁它的水平 2。确保它和第一级放在同一个场景文件夹中(见图 8-5 )。

A431865_1_En_8_Fig5_HTML.jpg

图 8-5。

A quick way to make a new level

这是一个很好的捷径,因为这意味着你已经有了所有的预置,你可以更快地设置和运行。只需删除一些元素,移动一些东西,然后按 Ctrl+S 保存新布局。

(或者,只需右键单击场景文件夹,然后选择创建➤场景,即可创建一个全新的场景)。

现在,如果您导航到项目窗格中的 Scenes 文件夹并双击 level one,它应该会在两个布局之间跳转。请注意,新场景中的一切都是新的——甚至是玩家和摄像机。尽管如此,它们仍然是存在于你的预设文件夹中的相同对象的所有实例,所以编辑脚本或属性将影响所有级别的一切。

逃离关卡

既然我们的游戏有不止一个关卡,我们就可以在它们之间转换了。回到第一关,像平常一样将精灵添加到第一关。用多边形碰撞器使其成为 GameObject,tick 为 Trigger,创建一个名为EndLevel的新脚本,将其作为组件添加到火箭船中。

EndLevel会是这样的:

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

public class EndLevel : MonoBehaviour {
    public string nextLevel;

    void Start()
    {

    }

    void Update()
    {

    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player")
        {
            SceneManager.LoadScene(nextLevel);

        }
    }

}

再简单不过了!不过,请注意,这次我用using命令包含了代码的顶部。那是因为我使用了 Unity 的一个额外的类,叫做SceneManagement。这个类让我们使用加载下一个场景的命令。与此同时,正在讨论的场景是一个公共字符串,我们将在检查器中将其命名为 Level 2。这将使我们更容易更新每个场景的关卡目标,同时仍然使用相同的脚本和对象。

在你点击播放之前,你还需要做一件事:回到构建设置,将场景 2 添加到你的游戏中(只需将它从你的场景文件夹中拖放到窗口中即可——见图 8-6 )。

A431865_1_En_8_Fig6_HTML.jpg

图 8-6。

Drag Level 2 into the build settings so you can load it

现在尝试通过到达火箭完成水平。你会发现下一个关卡会立即加载,你已经准备好迎接下一个挑战:创建一个关卡选择。

构建级别选择屏幕

在大多数手机游戏中——包括 PC 和主机游戏——玩家可以直接进入给定的关卡,只要他们之前已经完成了上一关。这让他们重放他们最喜欢的时刻,回去寻找隐藏的秘密,并击败他们的最高分。为了实现这一点,我们需要为玩家提供一些查看和选择关卡的方法。换句话说,我们需要建立一个级别选择。

这意味着你需要创建另一个场景,但是这个场景将是完全空白。我们称之为等级选择。一旦准备好了,就该重新认识 Squarey 了,只是这次他失去了一点个性(图 8-7 )。

A431865_1_En_8_Fig7_HTML.jpg

图 8-7。

This will be our selector

实际上,这根本不是正方形,而是一个指示器或选择器,意味着我们需要在中心有一个透明度。这将显示我们希望选择哪个级别,因此我们还需要两个视图,每个级别一个,大小相同。我做了这些 500 x 500 的。你可以从你的两个关卡中截取截图(忽略它们在这一点上本质上是相同的)并保存为精灵。

现在,将两个级别的图像排列到场景中,使它们在相机的视野中,并很好地对齐。然后把你的选择器放在最上面(精确的相同坐标),确保它有一个更高的层排序值。你应该有类似图 8-8 的东西。

A431865_1_En_8_Fig8_HTML.jpg

图 8-8。

The beginnings of our Level Select scene

编写控制脚本

现在我们将创建一个新的控制脚本,它的工作方式很像Player脚本。在很大程度上,我们将像控制玩家一样控制选择器。这是我们暂时的玩家角色。

创建脚本并将其命名为Selector。然后使用下面的代码:

public class Selector : MonoBehaviour {

    public bool moveLeft;
    public bool moveRight;

    void Start()
    {

    }

    void Update()
    {

        if (transform.position.x > -5 && (moveLeft || Input.GetKeyDown(KeyCode.LeftArrow)))
        {
            transform.position = new Vector2(transform.position.x - 6, transform.position.y);
            moveLeft = false;

        }
        else if (transform.position.x < 1 && (moveRight || Input.GetKeyDown(KeyCode.RightArrow)))
        {
            transform.position = new Vector2(transform.position.x + 6, transform.position.y);
            moveRight = false;
        }

        if (Input.GetKey(KeyCode.Space))
        {
            Select();
        }
    }

    public void Select()
    {

    }

}

我的关卡图像间隔 6 个单位,所以这是选择器每一步移动的距离。

正如你所看到的,这与我们通常的Player脚本非常相似,尽管显然没有Death方法或动画。还有一个或两个其他的差异,你也需要知道。我们已经创建了一个Select方法,但是现在它还是空的。相反,当用户点击箭头键时,选择器向左或向右移动了 6 个单位。注意,我们现在使用的是GetKeyDown,所以用户必须点击而不是按住箭头。出于同样的原因,我也在方块移动一步后立即将moveLeftmoveRight设置为false

最后,我添加了一点代码来检查选择器不会离开屏幕的左边缘或右边缘。你需要在每次添加一个新的关卡时更新它,或者使用类似于numberOfLevels * 6的东西来计算选择器可以向右移动多远。

如果你拖动Main Camera对象使其成为层级中选择器的子对象,那么屏幕将随着选择器的移动而“滚动”。现在,如果你测试它,只要你在电脑上使用光标键,它就应该工作。

现在您需要专门为Selector创建一个新的Touch脚本。我们可以将这段代码添加到我们已经创建的同一个Touch脚本中,并检查它所附加的对象,但是创建新的东西可能更简单。

所以创建另一个新脚本,这次叫做LevelSelectTouch。这个实际上是上一个Touch的翻版,让事情变得简单明了:

public class LevelSelectTouch : MonoBehaviour {

    private Selector selector;

        void Start()
        {
            selector = FindObjectOfType<Selector>();
        }

        public void PressLeftArrow()
        {
            selector.moveRight = false;
            selector.moveLeft = true;
        }
        public void PressRightArrow()
        {
            selector.moveRight = true;
            selector.moveLeft = false;
        }
        public void ReleaseLeftArrow()
        {
            selector.moveLeft = false;
        }
        public void ReleaseRightArrow()
        {
            selector.moveRight = false;

        }

        public void Select()
        {
            selector.Select();

        }
    }

将这个脚本添加到你的层级中的TouchController游戏对象——而不是预置。请记住,我们只希望此更改影响此触摸控件实例,而不影响级别 1 和级别 2 中使用的控件。

现在您只需要设置控件来使用这个脚本。打开右箭头、左箭头,然后在检查器中跳转,并为每个重新配置事件触发器,以便它们与新脚本中的正确方法相对应。如果你卡住了,回头看看我们上次是怎么做的——过程是完全一样的(在第七章)。当然,在这种情况下,跳转按钮将被绑定到Select方法。

当我们破坏我们的预设时,从画布上删除LevelCoins游戏对象,因为它们在这个上下文中没有多大意义。

准备发射

尝试一下,你会发现你现在可以用光标或者屏幕上的控件来移动选择器。它看起来相当不错,尽管它可能会受益于某种更好的背景(图 8-9 )。

A431865_1_En_8_Fig9_HTML.jpg

图 8-9。

There we go, much better!

它真正需要的是实际选择级别的能力。一个好的方法是给我们的关卡图像起一个合适的名字,这个名字将会和我们的场景名字一样(所以,关卡 1 和关卡 2 ),并且使选择器本身成为一个带有碰撞器的触发器。我们还将添加刚体,用于我们的碰撞检测。我们显然不希望我们的级别落在屏幕的底部,所以点击体型旁边的下拉菜单来选择运动学(图 8-10 )。

A431865_1_En_8_Fig10_HTML.jpg

图 8-10。

Set Body Type to Kinematic .

现在像这样更新Selector脚本,记住在顶部添加新的using行:

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

public class Selector : MonoBehaviour {

    public bool moveLeft;
    public bool moveRight;
    private string levelChoice;

    void Start()
    {

    }

    void Update()
    {

        if (transform.position.x > -5 && (moveLeft || Input.GetKeyDown(KeyCode.LeftArrow)))
        {
            transform.position = new Vector2(transform.position.x - 6, transform.position.y);
            moveLeft = false;

        }
        else if (transform.position.x < 7 && (moveRight || Input.GetKeyDown(KeyCode.RightArrow)))
        {
            transform.position = new Vector2(transform.position.x + 6, transform.position.y);
            moveRight = false;
        }

        if (Input.GetKey(KeyCode.Space))
        {
            Select();
        }
    }

    public void Select()
    {
        SceneManager.LoadScene(levelChoice);

    }

    void OnTriggerEnter2D(Collider2D other)
    {

        levelChoice = other.name;

    }

}

这段代码只是寻找一个冲突,然后获取违规对象的名称,作为一个名为levelChoice的字符串进行存储。当你点击跳转按钮时,levelChoice会像我们之前加载的一样被加载。尝试一下,你会发现你现在可以跳到你选择的任何一个级别。不要忘记将关卡选择场景添加到构建设置中。

让我们花一点时间来思考一下你在这里的成就:你使用了你已经使用过的所有相同的技巧,但是这次你在游戏中制作了一个菜单而不是一个关卡。这是 Unity 给我们的工具是多么多才多艺的早期迹象。想象创建一个益智游戏或者某种生产力工具并不是太难。

保存我们的进度

在我们能够保存玩家的进度之前,关卡选择没有多大用处。我们希望他们在玩这些关卡时有成就感和进步感,这意味着随着每一关的完成,可以选择继续玩下去。这种进步应该从一个游戏阶段持续到另一个游戏阶段,因为每次都要从头开始并不有趣。

因此,我们需要一种方法来保存我们的进展,Unity 实际上给了我们许多选择,从使用玩家偏好到序列化或创建文本文件。

从技术上讲,我们应该使用的是序列化——它可以让你更快地保存更多的信息。这里不涉及太多细节,这意味着将一个对象转换成字节。不过这有点复杂,所以现在我们要用PlayerPrefs因为它又快又脏,而且更容易让你理解。

PlayerPrefs应该是用来保存像图像质量这样的偏好,或者你是否想要打开声音——换句话说,就是设置。但老实说,很多独立开发者专门使用这种方法,如果你需要做的只是存储一些最高分和级别名称,它会做得很好。

当我们加载关卡时,保存它非常简单。只需更新 1 级火箭附带的EndLevel脚本,如下所示:

public class EndLevel : MonoBehaviour {
    public string nextLevel;
    public int levelValue;

    void Start()
    {

    }

    void Update()
    {

    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player")
        {
            SaveLevel(levelValue);
            SceneManager.LoadScene(nextLevel);

        }
    }

    public void SaveLevel(int level)
    {
        PlayerPrefs.SetInt("farthestLevel", level);
    }
}

那一行:PlayerPrefs.SetInt ("farthestLevel", level);就是全部了。这用关键字farthestLevel创建了一个新的整数,并把它放在PlayerPrefs中。我们只需要在检查器中添加公共变量levelValue(图 8-11 ,现在触摸火箭将加载下一个场景并更新保存的变量。

A431865_1_En_8_Fig11_HTML.jpg

图 8-11。

The rocket has Level 2 value

为了利用这一点,我们需要让我们的关卡选择界面更智能一点,这样它就能告诉我们什么时候一个关卡还没有准备好被载入。创造一些东西,将显示在未来的水平。我使用了一个问号,它应该与我的背景很好地匹配(图 8-12 )。

A431865_1_En_8_Fig12_HTML.jpg

图 8-12。

What’s behind door number two?

创建其中的两个,并将第一个放在第二级后面,在顺序中靠后一点(如果你做对了,它就看不见了)。第二个沿着右边走,第三层就在右边。这些不应该有对撞机;它们只是图像,当前面的图像丢失时会显示出来。你应该有类似图 8-13 的东西。

A431865_1_En_8_Fig13_HTML.jpg

图 8-13。

There are actually two question marks here—the first one is behind Level 2

我们现在将创建另一个名为LevelLoader的脚本,并将其附加到 2 级图像:

public class LevelLoader : MonoBehaviour {

    public int thisLevel;
    private Selector selector;

    void Start () {
        selector = FindObjectOfType<Selector>();

    }

        void Update () {
                if (selector.farthestLevel < thisLevel) {
            this.tag = "off";
            GetComponent<Renderer>().enabled = false;
        } else
        {
            this.tag = "on";
            GetComponent<Renderer>().enabled = true;
        }
    }
}

所以,这些图像现在是我们的关卡加载器。当关卡选择场景被创建时,它们都会出现,然后它们会检查玩家是否走得够远。如果玩家没有,他们将消失(GetComponent<Rendered>().enabled = false)并将他们的标签设置为off

将这个附加到 GameObject,然后在 Inspector 中输入公共整数thisLevel,当然应该是 2。

如您所见,我们需要检查级别的代码将在Selector脚本中(因为我们只想做一次),它有公共属性loadLevel。要获得这个值,我们所要做的就是向脚本中的Start方法添加一行代码:

PlayerPrefs.GetInt("farthestLevel");

当然,我们还需要在顶层定义公共整数。

现在,当选择器被创建时,它将通过查看PlayerPrefs来检查玩家已经到达的最远等级,并将该值存储为一个公共整数。如果玩家还没有走得足够远,那么LevelLoader图像就会消失。当玩家选择。他们仍然选择了一个“不可见的第二层”,但是因为标签被设置为off,它不会被加载。

最后几点意见

试一试,你会发现一开始你只能玩 1 级。只有在你通过火箭到达 2 级,然后再次加载这个屏幕后,你才能选择任何一个。进步。

这不一定是最理想的处理方式。为了保持简单,我们的一些方法现在在不寻常的地方。相反,你可以做的是创建一个脚本,作为一种“游戏管理器”来存储进度,加载不同的级别,等等,这样可以让你的代码更整洁。我一直在威胁关于优化的那一章,这是我们将在那里触及的其他内容。

但是现在,我觉得你已经够努力了。这是一个复杂的章节,所以如果你挣扎过,试着不要担心。我实际上只教了你很少的新东西(如何加载场景以及如何在PlayerPrefs中保存和加载变量)。大多数情况下,这只是用新的方式运用你已经学到的东西。因此,如果你已经走了这么远,你已经有工具来制作检查点和等级选择屏幕——这只是一个简单的问题,应用一点独创性,以拿出一个你喜欢的系统。这就是编程的乐趣所在:它本质上是一种足智多谋的练习。

在接下来的几章中,事情又会变得不那么技术性了。我们将引入一些常见的障碍、能量和能力来创造更多有趣的游戏可能性。然后我们将讨论什么是好的游戏设计。你已经完成了最难的部分(就目前而言)。是时候找点乐子了!

九、加入更多的游戏元素:弹簧、移动平台、人工智能等等

你已经花了最后几章努力创造一个工作的游戏世界。您已经开发了引擎,创建了保存文件,并使您的角色以应有的方式移动并与世界互动。希望你能从中得到乐趣,但是可能会觉得有点挑战性,而且在过程中的某些点上很有技术含量。

好了,现在是时候享受一下你的成就了。你创造了这个世界。让我们在里面找点乐子吧。

毕竟,一个典型的游戏会涉及大量不同的障碍,危险和能量,每一个通常都会创造独特的游戏挑战和有趣的遭遇。索尼克有弹簧,戒指,柱子,巴德尼克,钉坑,混沌祖母绿,和循环。马里奥有蘑菇,子弹法案,鬼,问号框,和耀西(s)。超级肉仔有传送门,巨型锯,导弹,还有一堆用过的针。

是时候让你变得有创造力,开始在你自己的游戏中引入更多的元素了。最棒的是。创造这些挑战几乎和以后经历它们一样有趣。

在这一章中,你将学习如何创造各种各样的环境危害和敌人,并且当你想在自己的游戏世界中加入其他元素时,你可以随时回头。我希望它也能成为灵感的源泉,帮助你想出自己的障碍和挑战。当然,在这个过程中,我们也会学到一些新概念。

最后,您还将学习如何掠夺素材商店,以便您可以访问他人精心创建的粒子效果、脚本和精灵,并在您自己的游戏中使用它们。

准备好了吗?让我们找点乐子!

一些常见的游戏对象及其行为

虽然每个平台都是不同的,你应该尽你所能使自己与众不同,脱颖而出,但某些比喻确实会不时出现。这在任何类型、任何形式的媒体中都是正常的,所以如果你发现自己又回到了“老一套”,也不用担心

因此,假设你将在游戏设计中使用一些更常见的资源和对象,这一节将向你展示如何构建基本元素,如弹簧和移动块。

弹簧

我们要创建的第一个游戏对象是弹簧,或“弹跳垫”可以说是由刺猬索尼克推广开来的,弹簧现在是平台游戏中一个常见的比喻,用来推动玩家上升一个等级。

环顾一下 Unity IDE,你可能会发现它似乎可以完成这项工作:你可以给物理材质 2D 添加一个“弹性”属性。不幸的是,这不是我们想要的,因为这将使地面看起来更像一个真实的弹性表面。也就是说,它将推动角色越往下坠越高,最终返回的能量越来越少。你可以用它做一些有趣的事情,但是它不会按照我们想要的方式运行。

相反,我们将创建一个 spring sprite(图 9-1 )并将它添加到你的关卡中,就像你现在习惯做的那样(图 9-2 )。注意,我们沿着顶部边缘使用了一个边缘碰撞器(而不是通常的盒子碰撞器)。

A431865_1_En_9_Fig2_HTML.jpg

图 9-2。

A spring in a level

A431865_1_En_9_Fig1_HTML.jpg

图 9-1。

A spring

接下来,您将创建一个Spring脚本并添加以下代码:

public class Spring : MonoBehaviour {

    private Player player;
    // Use this for initialization
    void Start()
    {
        player = FindObjectOfType<Player>();
    }

    // Update is called once per frame
    void Update()
    {

    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.tag == "Player")
        {
            player.SuperJump();
        }
    }

}

正如您可能已经猜到的,您还将把SuperJump方法添加到您的Player脚本中:

public void SuperJump()
{
    rb.velocity = new Vector2(rb.velocity.x, jumpPower * 2);

}

当然,记得将新的Spring脚本添加到你的游戏对象中,并使它成为一个预置——通常的东西。

现在当你碰到弹簧时,凯文将会被发射到两倍于他正常跳跃高度的空中。我一直保持高度与他的跳跃高度成比例,以防我们决定改变关卡的比例。如果你愿意,你也可以给弹簧添加动画和声音。

移动平台

任何平台游戏中的一个常见比喻是移动平台。你有左右移动的平台,带你越过深沟,你有上下移动的平台,就像电梯一样。

我们已经可以让事物左右移动——我们已经对我们的敌人做到了。问题是,如果你把这个运动脚本贴在一块地上,凯文就不会跟着它一起动了。相反,地面会从他下面移开,他会从上面掉下来。不好。

与此同时,如果平台上下移动,而你的玩家在上面,他将会颤抖和抓狂,并可能从地板上摔下来。我们本质上需要修改这个脚本,这样我们就可以贴着顶部表面,跟着它走。

我们如何做到这一点?

我给你一点时间思考…在本书中,我们之前使用了什么来允许一个游戏对象相对于另一个游戏对象移动?

明白了吗?

答案是我们需要让凯文成为他所站的游戏对象的孩子。为此,打开你的运动脚本(我们称之为BackAndForth)并准备做一些改变。我们不仅改变了剧本,使我们的角色在接触平台时成为平台的孩子,我们还增加了另一个运动维度,使它也可以上下移动。direction整数变量现在是公共的,这意味着我们可以从检查器中编辑它。在onStart()方法中也不再设置为 0,但是请记住,如果未设置,整数总是从 0 开始。

这意味着我们的敌人行为不会改变——他们将继续左右移动,因为direction变量将默认为 0。不过,对于平台,我们可以选择将其设置为 2 或 3,这将使它先向上再向下移动,或者先向下再向上移动。

做完这一切后,BackAndForth现在应该是这样的:

public class BackAndForth : MonoBehaviour

{

    public double amounttomove;
    public float speed;
    private float startx;
    private float starty;
    public int direction;
    private Player player;

    // Use this for initialization
    void Start()
    {

        startx = gameObject.transform.position.x;
        starty = gameObject.transform.position.y;
        player = FindObjectOfType<Player>();

    }

    // Update is called once per frame

    void Update()
    {
        if (gameObject.transform.position.x < startx + amounttomove && direction == 0)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x + speed, gameObject.transform.position.y);

        }
        else if (gameObject.transform.position.x >= startx + amounttomove && direction == 0)
        {
            direction = 1;
        }
        else if (gameObject.transform.position.x > startx && direction == 1)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x - speed, gameObject.transform.position.y);
        }
        else if (gameObject.transform.position.x <= startx && direction == 1)
        {
            direction = 0;
        }

        if (gameObject.transform.position.y < starty + amounttomove && direction == 3)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y + speed);

        }
        else if (gameObject.transform.position.y >= starty + amounttomove && direction == 3)
        {
            direction = 2;
        }
        else if (gameObject.transform.position.y > starty && direction == 2)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x, gameObject.transform.position.y - speed);
        }
        else if (gameObject.transform.position.y <= starty && direction == 2)
        {
            direction = 3;
        }

    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.tag == "Player")
        {
            player.transform.parent = gameObject.transform;
        }
    }

    private void OnCollisionExit2D(Collision2D other)
    {
        if (other.gameObject.tag == "Player")
        {
            player.transform.parent = null;

        }
    }
}

我还建议你为平台创建一个新的物理材料 2D,并将摩擦力设置得高一些。这是为了防止凯文在平台上滑动太多,配合动作看起来有点古怪。

使用边缘碰撞器并添加平台效应器也是一个好主意。勾选效应器使用和使用一种方式,这将防止我们的玩家被平台压碎或粘在一边,并作为平台的孩子移动。如果这阻止了玩家从平台上跳下,你可以稍微增加你的地面检查的半径。还有其他方法来完成同样的事情,可能更优雅一点,但这是一个简单的“修复”,将让您的移动平台启动并运行。

如果一切正常,凯文现在应该随着平台一起移动,不管它是向左向右还是向上向下(图 9-3 )。这创造了大量的平台挑战机会,所以尽情享受吧。

A431865_1_En_9_Fig3_HTML.jpg

图 9-3。

Kevin going for a ride

折叠平台

你知道还有什么很棒吗?倒塌的平台。这些都是平台,当你在它们上面着陆时,它们会在脚下粉碎,从而鼓励你快速奔跑和跳跃,以避免落入你的厄运。

在这种情况下,向玩家传达他们即将面临的挑战的性质是非常重要的。在没有警告的情况下,让一个平台从你的球员下面掉出来,这是不公平的,因此,通常建议有一个视觉指示器来指示地面不太稳定。

出于这个原因,我设计了一个摇摇欲坠的平台,如图 9-4 所示。

A431865_1_En_9_Fig4_HTML.jpg

图 9-4。

A crumbling platform tile

我们希望这个平台瓷砖在我们着陆时开始破碎,所以我们将创建一个名为Crumble的新脚本。这个脚本将简单地在玩家触摸到物体时启动一个计时器,然后在计时器结束时让物体落下并消失。

代码如下所示:

public class Crumble : MonoBehaviour {
    private Player player;
    private Rigidbody2D rb;
    public int timeToCollapse;
    private int timeLeft;
    public int timeToRestore;
    private int restoreTime;
    private float startY;
    private float startX;
    // Use this for initialization
    void Start () {
        rb = GetComponent<Rigidbody2D>();
        player = FindObjectOfType<Player>();
        startX = transform.position.x;
        startY = transform.position.y;
        timeLeft = -70;
    }

     // Update is called once per frame
     void Update () {
           if (timeLeft > -70)
        {
            timeLeft = timeLeft - 1;
        }
        if (timeLeft == 0)
        {
            rb.constraints = RigidbodyConstraints2D.None;
        }
        if (timeLeft == -62)
        {
            GetComponent<Renderer>().enabled = false;
            restoreTime = timeToRestore;
        }
        if (restoreTime > 0)
        {
            restoreTime = restoreTime - 1;
        }
        if (restoreTime == 2)
        {
            transform.position = new Vector3(startX, startY);
            transform.rotation = Quaternion.identity;
            GetComponent<Rigidbody2D>().velocity = Vector3.zero;
            rb.constraints = RigidbodyConstraints2D.FreezeAll;
            GetComponent<Renderer>().enabled = true;
        }

        }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.tag == "Player")
        {
            timeLeft = timeToCollapse;
        }
    }

}

当玩家触摸碰撞器时,开始倒计时,这将是在检查器中选择的一个值。当倒计时过零时,刚体的约束被移除,允许它下落并在空中旋转。计时器继续从 0 倒数到-70,这样我们就有时间看到平台落下并消失。就在时间到达该点之前,对象将停止被渲染并变得不可见。这将开始一个新的倒计时:恢复计时器。这也是在检查器中设定的,当它倒数到零时,对象将返回到其原始位置,约束重新冻结,旋转设定为零,渲染返回。

从玩家的角度来看,这将创建一个瓷砖,在它从屏幕上掉落并最终消失之前,可以站立一段有限的时间。你可能想添加一个“隆隆”的动画和声音效果来增加戏剧性,然后你可以引入一些很酷的反身平台。它应该看起来像图 9-5 。

A431865_1_En_9_Fig5_HTML.jpg

图 9-5。

Running along collapsing blocks … action!

更好的人工智能

另一件你可能想在游戏中加入的东西是一个更好的敌人 AI。现在,我们的敌人只是左右移动,并不比我们的移动平台更聪明。对玩家来说,更具挑战性的是一个会真正找到玩家并追捕他们的敌人。

我认为地面上的东西会更有趣,所以我创造了另一个敌人。这次我要带一种长相很贱的机械鼠(见图 9-6 )。为什么呢?可能是因为太晚了,我把它弄丢了。

A431865_1_En_9_Fig6_HTML.jpg

图 9-6。

Look, it’s an alien planet. It doesn’t need to make sense

这个小家伙要用一个新的脚本,名字就叫GroundEnemy。这个角色的基本行为是沿着地面跟随玩家。所以,如果我们的水平位置比玩家的大,我们减少 x 的值。如果它小,我们增加那个值。我们还需要老鼠在改变方向时翻转,就像玩家一样。

这个简单的脚本看起来像这样:

public class GoundEnemy : MonoBehaviour {
    private Player player;
    private int facing;
    public float enemySpeed;

void Start () {
        player = FindObjectOfType<Player>();

    }

void Update () {

        if (gameObject.transform.position.x > player.transform.position.x)
            {
                gameObject.transform.position = new Vector2(gameObject.transform.position.x - enemySpeed, gameObject.transform.position.y);
                if (facing == 0)
                {
                    facing = 1;
                    transform.localScale = new Vector3(.2f, .2f, 1f);
                }
            }

            if (gameObject.transform.position.x < player.transform.position.x)
            {
                gameObject.transform.position = new Vector2(gameObject.transform.position.x + enemySpeed, gameObject.transform.position.y);
                if (facing == 1)
                {
                    facing = 0;
                    transform.localScale = new Vector3(-.2f, .2f, 1f);
                }
            }
        }

}

有趣的事实:我制作的第一个游戏就是基于这个脚本的(除了 ZX 基础版)。这是我“了解”编程的起点。我做了一个可以在屏幕上移动的点,它会被第二个点追赶。玩家的目标是诱骗敌人降落在一个小地雷上(一个红点)。也许这不是一个有趣的事实…有时我会感到困惑。

咳咳。当然,如果你想让坏人真正致命,你也应该添加Hazards脚本(你需要添加一个onCollissionEnter2D方法,这样碰撞器和触发器都会杀死玩家)。图 9-7 中可以看到敌人在追击。

A431865_1_En_9_Fig7_HTML.jpg

图 9-7。

Run Kevin, it's some kind of robot rat

无论如何,这个脚本目前有点太简单了。事实上,游戏一开始,敌人就会开始追逐玩家,很可能会被困在某个坑里。不仅如此,他很容易被愚弄,几乎会被任何障碍所阻碍。

为了解决这个问题,我们首先要让他在玩家到达一定距离后立即行动,然后在玩家离开后停止跟随。接近度将是一个公共整数,我们可以在检查器中设置。一个有用的提示是确保你在不同的范围和不同的速度下玩耍。理想情况下,你不希望敌人开始移动,直到玩家可以在屏幕上看到他们。同样,理想的速度应该是能让玩家在紧张的追逐后逃脱的速度。

请注意调整这些数字是如何稍微改变游戏的节奏和紧张程度的。这类似于成为一名电影导演,我们将在下一章更多地讨论这些方面。

既然如此,为什么我们不多一点创意,让我们的敌人有更多穿越环境的能力呢?例如,如果 Roborat(是的,我应该叫他 Roborat)能跳过障碍物试图够到玩家,如果它能自己从坑里出来,那就太好了。为此,我们将使用一个新功能:光线投射。

使用光线投射

光线投射有点像你车上的倒车传感器;它们发出一束检查碰撞的光束,如果有碰撞就返回“真”。我们想做的是给我们的敌人一种能力,看看是否有什么东西挡住了它的道路,然后跳过它。这意味着它也需要自己的groundCheck(这样它就不会一直跳来跳去)。用和你为玩家做的完全一样的方法来处理这个:创建一个小半径的空游戏对象,然后让它检查地面来设置一个布尔值。您可以直接复制并粘贴代码,一旦代码就位,它应该看起来像这样:

public class GoundEnemy : MonoBehaviour {
    private Player player;
    private int facing;
    public float enemySpeed;
    private bool chaseIsOn;
    public int attackRange;
    public Transform groundCheck;
    public float groundCheckRadius;
    public LayerMask whatIsGround;
    private bool onGround;

    void Start () {
        player = FindObjectOfType<Player>();

    }

    void FixedUpdate()
    {
        onGround = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, whatIsGround);

    }

在检查器中你也应该有类似图 9-8 的东西。

A431865_1_En_9_Fig8_HTML.jpg

图 9-8。

Remember, you’re creating an empty GameObject just below the character to use as a transform and then adding it to the Inspector

光线投射是一条看不见的线,我们将使用更多的变换和空的游戏物体来定义它的位置。为了检查这是否有效,我们将使用一个debug函数在两点之间画一条直线。这是 Unity 的一个便利特性,可以让你以一种只在场景视图中可见的方式直接在屏幕上绘图。玩家不会看到它,但我们可以用它来测试我们的游戏。

因此,创建两个新的空游戏对象,它们是老鼠的孩子(这听起来像一本奇怪的书名:老鼠的孩子)。第一个应该是死点,我们就叫Sight Start。第二个将在 rat 前面两个单位,称为Sight End

现在我们要创建两个新的公共转换,分别是enemySightStartenemySightEnd。我们将再次使用检查器将刚刚创建的两个空对象放入其中。如果你做对了,你应该可以添加这一行:

Debug.DrawLine(enemySightStart.position, enemySightEnd.position, Color.red);

然后看到场景视图中两点之间出现一条红线(见图 9-9 )。现在我们要把这条线换成光线投射。

A431865_1_En_9_Fig9_HTML.jpg

图 9-9。

Once the transforms are set, your rat should look like he's jousting

我们的光线投射将精确地到达线当前所在的位置,但是无论如何保持线是有用的,因为光线投射是完全不可见的,否则很难可视化。

幸运的是,使用我们的光线投射非常简单——特别是因为你熟悉使用重叠圆。

我们将使用Physics2D.Linecast来完成这项工作。还有其他类型的光线投射,如Circlecast,但对于一个规则简单的 2D 游戏来说,一条线是最有效的选择。我们需要给这个函数一个起点和一个终点(就像我们对线条所做的那样),然后我们还要提供一个图层蒙版。我们不希望敌人跳过玩家,所以它要找的层是地面。

这将进入更新,并且只有当chaseIsOn布尔值为真时(也就是说,如果玩家已经被看见)才会起作用:

if (Physics2D.Linecast(enemySightStart.position, enemySightEnd.position, whatIsGround)) {
                Jump();
            }

如你所见,我也创建了一个Jump方法,就像我们为玩家做的一样。这应该很熟悉:

private void Jump()
    {

        if (onGround)
        {
            rb.velocity = new Vector2(rb.velocity.x, jumpHeight);
        }

    }

这实际上是我们的敌人现在能够跨越障碍所需要的一切(见图 9-10 )。现在没有什么能阻止他,他就像一个终结者。

A431865_1_En_9_Fig10_HTML.jpg

图 9-10。

Leaping rats—a common sight for an ex-Londoner like myself

好消息是,当老鼠转身时,Sight End对象也会翻转,因为它是老鼠的孩子。让老鼠尝试跳过裂缝也不需要太多的代码;我们只需要第二个光线投射来观察第一个下方的地面。加上这一点,并确保敌人在该点不与地面重叠时跳跃(图 9-11 )。

A431865_1_En_9_Fig11_HTML.jpg

图 9-11。

Our rat friend looking for the floor

编码敌人的行为

我还添加了一些东西,本质上是来自BackAndForth脚本的相同代码。我想让老鼠左右移动,这样它就“巡逻”了,直到它开始追逐玩家。让一个完美的敌人一直保持到玩家被看到,这看起来并不特别自然…尽管这可能会令人毛骨悚然,我承认。不过,我们会让这个代码更智能一点,如果敌人接近边缘,我们会让它改变巡逻方向,这样它就不会离开平台或撞到墙上。

我还将稍微移动一下代码,这样它就不会全部位于Update函数中——这看起来有点难看。如果您想走简单的路线,您可以复制并粘贴这段代码来创建您自己的地面敌人:

public class GoundEnemy : MonoBehaviour {
    private Player player;
    private int facing;
    public int jumpHeight;
    public float enemySpeed;
    private bool chaseIsOn;
    public int attackRange;
    public Transform groundCheck;
    public Rigidbody2D rb;
    public float groundCheckRadius;
    public LayerMask whatIsGround;
    private bool onGround;
    public Transform enemySightStart;
    public Transform enemySightEnd;
    public Transform enemySightEnd2;
    private float startX;
    public double amountToMove;

    void Start () {
        player = FindObjectOfType<Player>();
        rb = GetComponent<Rigidbody2D>();
        startX = gameObject.transform.position.x;
        facing = 3;
    }

    void FixedUpdate()
    {
        onGround = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, whatIsGround);
        Debug.DrawLine(enemySightStart.position, enemySightEnd.position, Color.red);
        Debug.DrawLine(enemySightStart.position, enemySightEnd2.position, Color.green);
    }

    void Update()
    {

        if (gameObject.transform.position.x - player.transform.position.x < attackRange && gameObject.transform.position.x - player.transform.position.x > -attackRange && chaseIsOn == false)
        {
            chaseIsOn = true;

        }
        if (gameObject.transform.position.x - player.transform.position.x > attackRange || gameObject.transform.position.x - player.transform.position.x < -attackRange && chaseIsOn == true)
        {
            if (chaseIsOn)
            {
                startX = gameObject.transform.position.x;
            }
            chaseIsOn = false;
        }

        if (chaseIsOn)
        {
            Pursuit();
        } else
        {
            Patrol();
        }
    }

    private void Patrol()
    {
        if (facing == 3)
        {
            facing = 0;
            transform.localScale = new Vector3(-.2f, .2f, 1f);
        }

        if (gameObject.transform.position.x < startX + amountToMove && facing == 0)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x + enemySpeed / 2, gameObject.transform.position.y);

        }
        else if (gameObject.transform.position.x >= startX + amountToMove && facing == 0)
        {
            facing = 1;
            transform.localScale = new Vector3(.2f, .2f, 1f);
        }
        else if (gameObject.transform.position.x > startX && facing == 1)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x - enemySpeed / 2, gameObject.transform.position.y);
        }
        else if (gameObject.transform.position.x <= startX && facing == 1)
        {
            facing = 0;
            transform.localScale = new Vector3(-.2f, .2f, 1f);
        }

        if (Physics2D.Linecast(enemySightStart.position, enemySightEnd2.position, whatIsGround) == false || Physics2D.Linecast(enemySightStart.position, enemySightEnd.position, whatIsGround))

        {
            if (facing == 1)
            {
                facing = 0;
                transform.localScale = new Vector3(-.2f, .2f, 1f);

            }
            else
            {
                facing = 1;
                transform.localScale = new Vector3(.2f, .2f, 1f);

            }
        }
    }

    private void Pursuit()
    {

        if (Physics2D.Linecast(enemySightStart.position, enemySightEnd.position, whatIsGround) || Physics2D.Linecast(enemySightStart.position, enemySightEnd2.position, whatIsGround) == false)
        {
            Jump();
        }

        if (gameObject.transform.position.x > player.transform.position.x)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x - enemySpeed, gameObject.transform.position.y);
            if (facing == 0 || facing == 3)
            {
                facing = 1;
                transform.localScale = new Vector3(.2f, .2f, 1f);
            }
        }

        if (gameObject.transform.position.x < player.transform.position.x)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x + enemySpeed, gameObject.transform.position.y);
            if (facing == 1 || facing == 3)
            {
                facing = 0;
                transform.localScale = new Vector3(-.2f, .2f, 1f);
            }
        }
    }

    private void Jump()
    {

        if (onGround)
        {
            rb.velocity = new Vector2(rb.velocity.x, jumpHeight);

        }

    }

void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            Physics2D.IgnoreCollision(collision.collider, GetComponent<Collider2D>());
        }
    }
}

这仍然是相当简单的敌人人工智能去,但它导致一些相当愉快的行为。我们的坏蛋现在会慢慢地巡逻(我把这个设置为半速)直到玩家接近。作为一只老鼠,他能闻到凯文的气味,所以一旦凯文靠得太近,老鼠就会紧追不舍,跳过障碍物和深坑紧追不舍。如果他碰凯文,我们就死定了。如果凯文及时逃走,老鼠就会失去兴趣,在他所在的任何地方巡逻。

最后一种方法——onCollission2D方法——是为了防止老鼠相互碰撞。我把这个包括进来,这样你就可以为毛出因子做一个老鼠的“坑”。不过,你需要将老鼠标记为敌人,它才能工作。

如果 Roborat 发现自己被困在一个平台上(见图 9-12 ),他通常只会僵住。所以,他并不完美。但他仍然很有趣,而且充满活力,足以创造许多游戏机会。

A431865_1_En_9_Fig12_HTML.jpg

图 9-12。

So long, sucker!

并感到自豪:你刚刚创造了你的第一个人工智能。一天的工作。

武装玩家

我们的老鼠已经被证明是一个相当卑鄙的威胁,肯定是一个足够的挑战,给我们的球员一段艰难的时间。是时候给我们的球员一个反击的机会了。

创建一个玩家可以发射的子弹是相对容易的,尽管我们需要做一些杂耍来确保我们引用了正确的Bullet对象实例。请允许我解释。首先,我们需要创建一个新的游戏对象,名为Bullet。这是我们的子弹,它的方向和速度有公共变量。它也将有一个对撞机。该脚本如下所示:

public class Bullet : MonoBehaviour {

    public float speed;
    public int direction;
    private int timeLeft;
    public GameObject Blood;

    void Start () {
        timeLeft = 100;
        }

        void Update () {
        timeLeft = timeLeft - 1;
        if (timeLeft < 1)
        {
            Destroy(gameObject);
        }
        if (direction == 0)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x - speed, gameObject.transform.position.y);
        } else if (direction == 1)
        {
            gameObject.transform.position = new Vector2(gameObject.transform.position.x + speed, gameObject.transform.position.y);
        }
    }

    void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.tag == "Enemy")
        {
            Destroy(other.gameObject);
            Instantiate(Blood, transform.position, transform.rotation);
        }
        else if (other.gameObject.tag == "Player")
        {
            Physics2D.IgnoreCollision(other.collider, GetComponent<Collider2D>());
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

请注意,杀死一只老鼠会造成与玩家死亡时相同的血腥粒子效果。这意味着你需要像以前一样在检查器中添加那个公共游戏对象。还要注意,我们的onCollision方法检查对象标签,这样老鼠会被血杀死,玩家会被忽略,其他任何东西都会破坏子弹。so 在设定的持续时间后超时,并自毁以将其自身从内存中移除,就像之前的粒子效果一样。

同样,我们还想在Player脚本中创建一个名为Bullet的新公共对象。你需要通过检查员把子弹预置放在那里。

然后,您将添加以下代码:

if (Input.GetKeyDown(KeyCode.LeftControl)) {
                var newBullet = Instantiate(bullet, transform.position, transform.rotation);
                var bulletScript = newBullet.GetComponent<Bullet>();
                bulletScript.direction = facing;
            }

这是新的部分。这里,我们不仅要实例化一个新对象,还要在创建对象时为它设置一些属性。为此,我们需要使用GetComponent来从这个实例中获取对脚本的引用。从那里,我们可以访问公共变量并更改它。

最终,你会得到一颗可以穿透敌人的子弹,如图 9-13 所示。

A431865_1_En_9_Fig13_HTML.jpg

图 9-13。

Alas, poor Roborat

(当然,稍后您会想要进入画布并添加一个“fire”按钮。我没有说这个,因为我认为子弹让我们的球员有点被制服了。这一部分只是供你参考,如果你想的话,你可以在游戏中添加子弹和枪支。同样,你也需要给你的玩家精灵添加一把枪,可能还需要一个新的动画。)

使用素材存储中的素材

我可以继续告诉你如何创建不同的对象和行为,直到我脸色发青,但我永远不会告诉你如何制作你可能需要的一切。传送玩家的传送门怎么样?开门的开关呢?会飞的敌人呢?还是双跳?还是开机?

希望你现在能自己弄清楚这些东西。本章介绍了光线投射,更详细地介绍了实例化,并增加了您的知识。利用这些新信息,在你已经知道的基础上,你应该能够对你能想到的几乎任何问题提出创造性的解决方案。记住,没有缺乏资源这种事,只有缺乏足智多谋。

但是如果你自己想不出来,或者你只是没有时间或者兴趣,你可以找到别人(包括 Unity Technologies)已经做好的预制体,添加到你自己的项目中。这就是素材存储的用武之地(图 9-14 )。

A431865_1_En_9_Fig14_HTML.jpg

图 9-14。

The Asset Store in all its glory

要开始浏览此处,只需选择“素材商店”选项卡并四处查看。你会看到各种各样的东西——从粒子效果,到脚本,到精灵包,到整个游戏演示。你会注意到有滑块让你设置价格(许多素材是免费的)和文件大小,你可以从右边的类别中选择。如果你点击一个出版商的名字,比如 Unity Technologies,你可以看到他们所有的素材和软件包。

我想让你从 Unity Technologies 找到名为 2D 平台的素材选择(图 9-15 )。这基本上是一个完整的 2D 游戏,但现在让我们试着选择一个我们想要的元素,而不是使用整个游戏。具体来说,我们抓一个音效文件:Player-jump1.wav。

A431865_1_En_9_Fig15_HTML.jpg

图 9-15。

This will do just nicely

在商店中点按“下载”,然后点按“导入”。Unity 会警告你,你可能会覆盖你的辛苦工作,但是不要担心——你可以在下一个屏幕上准确地选择你想要添加的内容。所以点击 OK,在打开的窗口中取消选择列表中的所有内容(点击 None),然后手动重新选择声音效果(图 9-16 )。

A431865_1_En_9_Fig16_HTML.jpg

图 9-16。

Only select what you want

现在点击导入,一秒钟后你会在你的音频文件夹中找到新的子文件夹。点击这些,你最终会得到你想要的声音效果。

您现在可以像以前一样创建一个音频源,并让它在 Kevin 跳跃时播放。这种声音对他来说绝对是错误的,但希望你看到这里的可能性——你可以在素材商店找到几乎任何你想要的东西,尽管你可能需要支付一些费用,但你会发现它通常不会太贵。通常,这里的资源的质量和专业性将超过你自己所能做的,这将加速开发,同时产生具有更高生产价值和更多“光泽”的最终产品我不想推荐任何具体的东西,因为商店的内容一直在变化,这可能会使这本书过时。然而,现在有一个 2D 精华精选,其中包括一些很酷的东西,如天气粒子效果,动态照明,反射 2D 水和“专业相机 2D”

希望你的头脑现在对这些可能性感到震惊,但最重要的是你不要忘乎所以。好的游戏设计不仅仅意味着向玩家扔你能扔的所有酷的东西——它和其他东西一样需要克制。权力越大,责任越大。

A431865_1_En_9_Fig17_HTML.jpg

图 9-17。

My Level 2 looks like this right now, which is all wrong. Find out why in the next chapter.

这就是为什么第十章为你提供了优秀游戏设计、技巧和风格的基本介绍。你已经掌握了基本技能——现在是时候学习如何利用它们了。

十、让游戏变得有趣和优化

恭喜你,你现在可以用 Unity 做游戏了!

不,说真的,如果你现在停止阅读,我相当有信心你可以建立一个完整的游戏关卡和一切。你甚至可以在浏览了一会儿之后找到如何在 Play Store 上发布它的方法(尽管我会在第十二章解释)。

是的,你可以建立一个游戏。但是你能开发一个好的游戏吗?因为这是两码事。记住:权力越大,责任越大。我觉得,如果我教你如何制作游戏,然后在没有任何关于如何让游戏变得有趣的指导的情况下让你自由,我会对这个世界造成伤害。

这就是我们将在这一章中探讨的内容。我们也会讨论一些关于优化的问题(让你的游戏运行更流畅,占用更少的空间),甚至如何让你的关卡看起来更漂亮。这是放在编程蛋糕上的樱桃。走吧!

入职和教程

还记得你从商店买了一个电脑游戏,然后在服装店等着你妈妈买完东西的日子吗?你可能很高兴坐在那里,因为你有手册可以阅读,里面充满了背景故事、提示和关于游戏中一切是如何工作的解释。它让你对游戏充满期待,并确保你在插入卡带/磁盘后就知道如何开始游戏。

如今,游戏很少配有手册,尤其是手机游戏。但这并不意味着你可以假设你的玩家马上就知道怎么玩了。事实上,你甚至不应该假设他们以前玩过电子游戏。因为你的一些玩家可能不会有,而那些仍然是你想留下来的客户。每个游戏都是某人的第一次。所以你的工作就是“在工作中”教玩家,这意味着你需要一个教程级别。

实际上,教程水平几乎和指导手册本身一样是一个时代错误。如果你还记得你真正喜欢的上一个教程级别,请举手。不,我不这么认为!

一个好的第一关应该在没有明确告诉玩家任何事情的情况下指导玩家如何操作一切。这意味着你将需要使用视觉线索,以及游戏性的比喻,创造一个知识的基础,然后在此基础上建立。

剖析完美的开放水平

虽然缺乏手册可能是一个现代问题,但在有史以来最经典的游戏之一《超级马里奥兄弟》中可以找到一个完美的开放级别的例子,它含蓄地教玩家如何玩。这款游戏的开放级别,称为世界 1-1,是游戏历史上最受分析和高度赞扬的级别之一,这是有充分理由的。让我们看看它是如何工作的。

为了您的方便,我已经使用来自我们简单平台的素材重新创建了第一级布局(图 10-1 )。这可能是也可能不是亵渎。

A431865_1_En_10_Fig1_HTML.jpg

图 10-1。

Hmm, this is oddly familiar…

从第一个屏幕开始,玩家就开始学习如何玩游戏。在这里,他们受到他们的主角马里奥(或者在我们的情况下,凯文)的欢迎。马里奥被放置在屏幕的最左边,镜头向前推到右边。这立即无声地告诉玩家:向右走。

当马里奥向右走时,他会看到一个愤怒的古姆巴向他走来(我们已经用我们的机器人代替了它)。你知道吗,从进化上来说,每当有东西直接向我们靠近时,我们就会感到压力。这就是为什么通勤是一场噩梦。这种运动模式结合古姆巴人愤怒的眉毛应该足以告诉我们需要避开敌人。我们唯一能做的就是跳——否则,我们会死(图 10-2 )。

A431865_1_En_10_Fig2_HTML.jpg

图 10-2。

Jump or die

跳跃是马里奥的主要机制,这种开放确保了玩家在进入下一步之前明白这是如何运作的。如果他们失去了所有的生命,回到起点,那么他们什么也没有失去,因为他们还没有取得任何进展。所以这是一个很好的实验场所。

马里奥接下来会遇到一个问号框。这个盒子正乞求被一个强有力的视觉暗示所触动,这个视觉暗示就是问号。这个通用符号在说,“哦,这是什么?”(“哦”是可选的)。

当马里奥通过弹跳进入问号来触摸它时,他会发现它会产生一个蘑菇。下一关的设计是马里奥几乎被迫收集蘑菇。它将从盒子中出现,在马里奥上方向右移动,然后从管道中反弹向左。马里奥此时很可能在下一个平台的下面,所以即使他试图跳起来避开蘑菇,它仍然可能击中他。在我们的游戏中,图 10-3 近似于这种情况。

A431865_1_En_10_Fig3_HTML.jpg

图 10-3。

Were this Mario, a mushroom would now be appearing

因此,玩家学会了如何成为“超级马里奥”

这一关以这种方式继续,以简单、无言的方式教玩家他们需要的每一项技能。接下来的几个关卡会给玩家足够的时间来练习这些技能,然后将一些障碍串联起来,以进行真正的挑战。随着游戏的进行,必须按顺序绕过的障碍数量会增加,最终会开始引入新的怪癖和曲折。

确保你的玩家理解你的游戏

这可能对你来说听起来像是常识(难道你不聪明),但是当你在关卡设计的阵痛中,很容易忽略这些点。

看看你已经创造了什么。我知道在这一点上你只是在玩,但我敢打赌你已经设置了一些相当残忍的陷阱。人们很容易被这种想法冲昏头脑,错误地认为辛苦=有趣。这种哲学会导致很多人在给你一个合适的机会之前就放弃你的游戏。如果玩家没有太多的游戏经验,这一点尤其正确。

那么,你必须经常做的事情就是把你的游戏交给人们去尝试。为 Android 开发的好处是,你可以把你的手机带到酒吧,在周围传递,看看你的朋友们过得怎么样。你可能会发现,对你来说似乎显而易见的事情对第一次玩游戏的人来说是迟钝或不公平的。你会看到你的玩家在哪里卡住了,在什么时候他们会考虑放弃。如果你做对了,他们至少应该能够通过最初的几关而不会过度沮丧。他们至少需要这么长时间才会上瘾。

马里奥和世界 1-1 背后的天才头脑 Shigeru Miyamotu 的建议是,最后设计你的第一关。这使得后退一步,避免在关卡设计中变成虐待狂的诱惑变得容易多了。

难度曲线

不过,你确实需要在游戏的某个阶段开始引入重大挑战,否则会变得很无聊。乐趣在于不可能和太容易之间的平衡点。

为什么呢?因为从神经学的角度来看,只要我们在学习,游戏就是有趣的。是的,我要深入了。

你的大脑进化来帮助你生存。是什么让人类如此擅长生存?我们适应和学习的能力。我们茁壮成长,因为我们学会了如何利用我们周围的环境,应对不断变化的气候和环境。我们的反应随着练习变得更好,固定的运动模式通过重复变得根深蒂固。

大脑想要不断学习,所以它通过释放某些神经递质和激素来奖励这种学习。当你朝着一个目标努力时,大脑会释放多巴胺让你保持专注。当你完成这个目标时,它会释放内啡肽——感觉很棒。这鼓励大脑重新连接自己,这样你就有更好的机会再次完成同样的事情。游戏使用声音效果来表示奖励,这加强了这种反应。

如果你给大脑的刺激或挑战太少,它就会变得无聊。无聊对我们有害,因为它会导致大脑萎缩。无聊的时候,我们会很快找点别的事情做。

同样,当你向大脑提出一个不可能的挑战时,它会很快泄气并放弃。

但是如果给你一个足够困难的挑战,需要做大量的工作,但又不会困难到不可能,那么这可以在大脑学习、适应和成长的过程中刺激和吸引你。如果一款游戏做到了这一点,那么玩家将进入神经科学中所谓的心流状态——一种我们完全专注于手头任务的精神状态,以至于我们周围的时间似乎都在膨胀和变慢。脑成像研究显示,大脑在这一点上的工作方式发生了一些令人着迷的变化;它进入了一种被称为额叶功能减退的状态,在这种状态下,大脑的额叶区域受到抑制,我们开始纯粹凭本能行事。这种平衡如图 10-4 所示。

A431865_1_En_10_Fig4_HTML.jpg

图 10-4。

For your players to be engaged and have fun, your difficulty curve must perfectly match their level of ability

你可能在某个时候经历过这种情况,在一场子弹地狱射击中,你恍惚地在屏幕上的数百颗子弹周围跳舞,或者在一场激烈的 boss 战斗中,你只剩下一条命了。

大脑参与其中是因为它在学习和成长,当你回到早期水平,发现新的肌肉记忆使以前看似不可能的挑战变得容易时,你可以感受到这种劳动的成果。

作为一名游戏设计师,你的工作是确保挑战的强度随着玩家技能和经验的提高而完美提升,最终目标是让他们保持在最佳状态。更好的是,你应该给你的游戏深度,以便他们可以回到早期水平,并使用他们的新技能获得更好的时间或找到隐藏的收藏品。

(也就是说,节奏也很重要,你确实需要给球员提供偶尔的喘息空间,以便他们能够恢复。)

让你的游戏变得有趣的其他方法

所以,当玩家在游戏中前进时,你要不断地教他们,并保持挑战的公平性和回报性,这是非常重要的。

但这并不是让你的游戏变得有趣的唯一方法。另一个有用的工具是多样性。在现实世界中刺激心流状态的一种方法是把一个人放在一个新奇的环境中,这是我们可以利用的优势。当周围环境不熟悉时,大脑会醒来并集中注意力,因为这再次代表了一个学习的机会。

这就是为什么您应该不断引入新的机制并升级您的环境。这也是为什么在游戏中看到“雪级”和“火山级”如此常见的原因。当然,你可以更有创造性,但最重要的是你要不断改变调色板和色调。这创造了一种发现的感觉,并鼓励你的球员想要不断向前推进。

解谜是玩家在游戏中喜欢的另一个比喻。再一次,这是一种神经上的回报,来自于那个“发现”的时刻,来自于让一切都到位。

那么如何设计一个好的拼图呢?答案是在你的游戏中引入一些元素,然后让玩家寻找新的方法来组合和使用这些元素。所以,你用来爬壁架的盒子变成了你可以扔向敌人的武器(见图 10-5 )。这需要横向思维,并挑战大脑克服功能固定性——只在最初介绍的背景下查看对象和元素的诱惑。

A431865_1_En_10_Fig5_HTML.jpg

图 10-5。

Tee hee!

增加谜题挑战性的最好方法是逐渐增加玩家解决谜题所需的步骤数。

最后,奖励你的玩家并进一步吸引他们的一个很好的方法是让他们以某种方式对他们周围的世界产生影响。这通常与你游戏的中心挂钩有关——使你的游戏与众不同并允许你的角色以独特的方式在世界中导航的机制。如果这种机制碰巧让玩家看到他们对周围环境的影响,那么这将有助于让他们感到更强大,从中可以获得很多乐趣。这就是为什么像《愤怒的小鸟》或《正义事业》这样的游戏本质上是围绕着造成大量破坏而展开的。它让玩家感到强大。其他游戏如 Godus 更进一步,让玩家扮演上帝。

也就是说,削弱玩家创造紧张、孤立和危险感的能力是增加他们注意力和注意力的好方法,并使他们的胜利更有回报。这一点在《地狱边缘》这样的游戏中得到了完美的体现。

紧急游戏

其他需要考虑的是你的游戏中你无法设计的方面。你的世界将是一个不断变化的排列系列,它将基于随机事件和你的玩家的行动。你不能预测每一个单一的场景,这意味着一些游戏的可能性将超出你的控制。但这并不是一件坏事。其实是一件很棒的事情。这就是紧急游戏是如何诞生的:当你创造的元素以意想不到的方式相互作用,为玩家创造新的挑战和独特的情况。例如,如果 Roborat 能够触发下落的方块,在正确的情况下,这可能会导致玩家和老鼠跳过下落的碎片。紧急游戏是惊人的,因为它给每个玩家自己独特的故事来讲述,并确保每个游戏环节都是不同的。你只需要创造元素,在一个大锅里搅拌它们,然后等待奇迹发生。

硬件、游戏引擎、格式和游戏性之间的相互作用

在第五章中,我提到你创造的游戏物理和元素将与游戏性和挑战密不可分。我的意思是,这里有一个双向互动,在你的设计过程中必须加以考虑。你对游戏世界的运作所做的决定将会对你的游戏方式和可能的挑战产生直接的影响。例如,你在一个表面上增加的摩擦力会改变一系列移动平台带来的困难,就像我们之前看到的那样。同样,屏幕上方向键的大小和玩家自己的手指也是如此。所有这些都需要在创建一个具有挑战性的序列和设计游戏物理的时候考虑在内。

正如我们将会看到的,这种双向关系要深入得多....

创造一个伟大的相机

游戏机制和你的游戏引擎的编程如何交叉的一个最好的例子可以用相机来看。

现在,你的Camera非常简单:它是Player的孩子,因此以与玩家完全相同的速度移动。你可能没有考虑这么多,但如果你现在回到你最喜欢的平台游戏,你可能会注意到这不是大多数游戏的行为方式。

例如,我们在早期马里奥的例子中看到,摄像机从玩家的右边开始,指示他们应该移动的方向。这也是你在任何“无止境奔跑游戏”中看到的相机位置,在这种情况下,相机采取这个位置是为了确保玩家有很多机会看到即将到来的障碍,因此有更多的时间做出反应。在这种类型的游戏中,玩家不能向后跑,那么在他们的左边有很多无用的空间有什么用呢?

游戏的节奏越快,为了展示更多即将发生的事情,摄像机应该越往后拉,FOV(视野)越宽。

在有很多平台的游戏中,防止恶心是很重要的。在这种情况下,相机有时会在中心有一个中立区,玩家可以在其中移动,然后只有当他们离开这个中心时才能滚动。其他平台解决这个问题的方法是让相机“捕捉”到角色在任何给定时间接触的平台。

在图 10-6 中,黑色方框表示我们的中立区。上下空间很大,左右空间就没那么大了。因此,如果玩家向左或向右移动,相机会很快跟踪,只有轻微的延迟(这意味着他们在躲避障碍时会感觉更快)。然而,会有更多的空间让玩家上下跳动,而不会让相机疯狂地上下摆动。这将有助于更少的垂直水平设计,有许多跨越间隙的跳跃。看到相机行为是如何反映关卡布局的了吗,反之亦然?这是在 2D 刺猬索尼克游戏中看到的相同类型的相机行为,这实际上是至关重要的,因为在那些游戏中有许多小山和梯度。如果摄像机只是简单地跟随音速,它会不停地上下移动到令人作呕的程度——尤其是在这样的速度下。

A431865_1_En_10_Fig6_HTML.jpg

图 10-6。

A different approach to our camera

在其他情况下,相机可以用来产生戏剧性的效果——暗示前方的危险,或者在玩家接近一个大挑战时放慢速度。如果镜头停止向前移动,玩家会立刻怀疑他们是否应该继续,并开始想知道他们视野之外是什么(FOV)。

所以,如果你的游戏设计不像你想象的那样,考虑一下你想象的世界和相机的运动之间是否有适当的协同。通过在你的相机中编码一些更高级的行为,甚至只是稍微向后移动,游戏会变得更有趣吗?

硬件和业务模式

在你的游戏设计中,不仅仅是物理和代码定义了什么是可能的,什么是有趣的。它也是你瞄准的硬件和你想要使用的商业模式。

要了解硬件和货币化如何直接影响游戏的玩法,只需看看你当地的游戏机就知道了。街机游戏通常非常困难,而且有生命系统,因为他们希望玩家投入更多的硬币。同样,它们必须易于学习,掌握起来具有挑战性,这样人们才会不断回到高分排行榜的首位。

当游戏迁移到 PC 上时,它们开始变得更加复杂和错综复杂。随着保存文件和更强大的硬件的引入,它们变得更加精细。

有意思的是,手机游戏把事情又往后退了一点。小屏幕上的移动游戏本身更适合“一口大小”的游戏(见图 10-7 ),而“免费游戏”等替代货币化选项的引入意味着游戏需要再次激励我们保持支出。

A431865_1_En_10_Fig7_HTML.jpg

图 10-7。

Breath of the Wild works well as a portable game due to the ability to so easily dip in and out

与此同时,在线功能意味着“高分”名单再次变得更加重要。这里的要点是,你的游戏中不应该包含任何东西,因为“游戏就是这样做的。”每件事都应该有一个目的,而这个目的将由多个不同的因素来定义。

无论你是想创建一个针对平板设备的一次性支付的“坐下来”游戏,还是为休闲游戏玩家创建一个免费的无休止的跑步者,都将彻底改变你的关卡设计方式。这意味着在你开始设计第一关之前,你需要对你的整个游戏有一些概念。

想想你就要开始把东西丢在某个地方。

让你的游戏看起来棒极了

虽然游戏性可以说比外观更重要,但两者兼而有之仍然非常重要。我们已经看到,游戏中的图形会对游戏的运行方式产生影响;图形可以传达一种场所感,并为互动提供线索。与此同时,虽然,它将是截图和游戏镜头比任何东西都更有助于你出售你的游戏。

换句话说,是时候给我们构建的游戏添加一点色彩了。我们有哪些方法可以改善它在照片中的表现?

让你的游戏更吸引人的简单方法

如果你看看我们创造的东西,公平地说,目前它并不那么有吸引力。它看起来还不太像一个职业游戏,这就是我们想要解决的问题(见图 10-8 )。

A431865_1_En_10_Fig8_HTML.jpg

图 10-8。

The current look

但是到底哪里出了问题?少了什么?

第一个问题是,一切都是非常无机的。这些平台是由直线构成的,而且都是一样的。改变这种情况的一个快速方法是将它们中的一些旋转 90 或 180 度。这是重用相同素材和保持较小文件大小的有效方法,但它仍然会给外观带来一些变化。同样,我们应该考虑在平台的边缘使用一些更细致的精灵。这将给出自然腐烂的效果,并迅速使事物看起来更真实。

我们可以添加更多的细节,就像我们之前使用的藤蔓,让每一块土地看起来都有点不同。基本上,我们希望一切看起来尽可能随机,我们可以用一点代码来实现。

图 10-9 好看多了。

A431865_1_En_10_Fig9_HTML.jpg

图 10-9。

Not much has changed, but it looks slightly more organic

事实上,我们游戏世界的另一个问题是它是静态的。看看你的窗外,你会发现有东西一直在动,不管是风中吹动的树枝,还是从管道滴落的雨水。最好的游戏也是如此,这就是为什么几乎所有的东西都是动画的,从背景中的花朵到星星。这不仅会让你的世界充满活力,还会给你的游戏带来更多的特色和个性,让它看起来更有趣。

然而,有一个分界点。我们不想让我们的球员在重要的元素上分心。

当然,目前我们在游戏中缺少动画。你将会想要给你的坏人动画和你的玩家动画做像跳跃或射击的事情。这是帮助玩家感受到他们正在与世界互动的另一种方式。当弹簧被弹开时,它会摆动。

最后一个问题是游戏缺乏深度。我们设计的背景非常平坦,在到达云层或太阳之前缺乏趣味性。这让你感觉一切都是从纸板上切下来的,所以你应该通过增加几层来改善它。

在图 10-10 中,我在前景添加了一些透明的云彩,当关卡滚动时,它们会比中间的地面移动得更快。我还在背景中添加了一些,并引入了一层山脉。这些山脉以另一种速度移动,它们有助于确保我们的游戏世界永远不会在背景中看起来完全空白。

A431865_1_En_10_Fig10_HTML.jpg

图 10-10。

With these few changes, our game is starting to look more interesting

如何创建好看的精灵并为你的游戏选择设计语言

虽然添加这些元素可以改善游戏的外观和感觉,但它们都要求你在创建自己的精灵时具备一些基本技能。如果你身体里没有艺术的骨头,你会怎么做?

一种选择是外包你的作品。像 Fiverr、Freelancer 和 UpWork 这样的网站可以让你与提供包括艺术和设计在内的广泛服务的自由职业者建立联系。这些也是获得背景音乐和音效的好地方。

选择二是制作一个风格化的游戏,使用独特的艺术风格,大大减少你需要做的工作量。如今很多游戏使用黑白艺术风格、轮廓(就像前面提到的地狱边缘),或者各种复古外观(就像在 VVVVVV 中看到的,它看起来像是根据 ZX 光谱设计的)。图 10-11 向我们展示了如果我们的游戏是为一个游戏男孩设计的,它会是什么样子。

A431865_1_En_10_Fig11_HTML.jpg

图 10-11。

Retro-style Kevin

使用像这样的特定艺术风格可以让你的游戏在 Play Store 中脱颖而出并吸引眼球,同时也给它一个强大的身份。如果你选择一些最简单的东西,你也会为自己节省很多时间,并且不再需要在设计上精益求精。

对于我们的游戏,我们采用了像素艺术风格。这是另一个复古风格的外观,让我们的游戏有一种怀旧的感觉,让我们不再需要创建逼真的精灵。

那么如何实现这种风格呢?答案很简单:使用任何图像编辑软件,如 GIMP 甚至 MSPaint,然后尽可能放大。如果可能,请在设置中选择“显示网格线”。现在,使用 100%不透明度的铅笔工具,你可以开始为你的精灵绘制轮廓了。您应该能够在绘图时看到单个像素。

当你画精灵的时候要花时间和精力,并确保留意任何形成的图案。例如,如果你正在画一个渐变,你可能会注意到像素向上移动一个,每次移动三个。这将导致一些看起来更加一致和可控的东西。幸运的是,如果你犯了任何错误,你可以按 Ctrl+Z。另一个技巧是考虑使用可用的图层(GIMP 和 Photoshop 提供这个功能,但 MSPaint 没有),这样就可以描绘出你想要变成像素艺术的图像。

A431865_1_En_10_Fig12_HTML.jpg

图 10-12。

An early app I made used a pastel color palette and a Sudoku-inspired look: Debugger: Brain Untraining

你可以勾勒出你的精灵或者你可以使用颜色块。我也推荐加底纹。这通常意味着你将使用三种颜色:一种用于主要填充,一种用于阴影,一种用于高光。确保你的所有精灵在同一个场景中的阴影都在同一边——否则会看起来很混乱,因为不清楚光源在哪里。

最后,导出你的图片。现在,你可能会发现当你这样做时,它看起来很小,但当你把它导入 Unity 并设置单位像素和比例时,这是可以解决的。把一个小图像放大,像素真的会变大。

最佳化

这一章是关于如何把你的功能游戏变成一个令人敬畏的游戏。为此,我们的议程上还剩下一个项目:优化。我们已经看了表面的细节——现在我们需要再看一看内在的东西。

首先,我说的优化到底是什么意思?本质上,我说的是让你的游戏运行流畅,易于编辑、改进和更新。好的代码应该使用尽可能少的行,一切都组织得井井有条,这样你就很容易找到你需要的任何元素。

tipsForBetterCode

无论何时你写代码,你都需要着眼于未来。有一天,你会想要更新你的游戏来修复一个 bug 或者增加一个新的功能(这在移动设备上也很常见),并且在离开一段时间后会回来。在理想的世界里,这应该是一种无痛的体验。一切都很容易理解,你不需要花时间眯着眼睛看屏幕。你应该知道一切都在哪里,你需要改变什么才能达到你想要的结果。如果你在团队中工作,这变得更加重要。

如前所述,更好的代码也意味着更少的代码。页面上的代码越多,就越难找到你要找的东西,每个过程可能需要的步骤也越多。步骤越多=执行越慢。

那么如何开始制作更优雅的程序呢?以下是一些帮助你开始的建议:

  1. 将多个变量放在一行:

    public float startX;    
    public float startY;
    
    

    变成

    public float startX, startY;
    
    
  2. 一定要确保使用合理的名称来描述变量的功能。这听起来很明显,但是你会惊讶程序员使用完全随机标签的频率。如果你的变量告诉角色他们应该跳多高,它应该被称为类似于jumpHeight的东西。这也意味着要避免缩写(jh),缩写会很快变得生硬和令人困惑。事实上,理想的情况是你的变量让你的代码读起来像英语。特别是在使用布尔值时,它可以是真或假,这意味着你可以创建这样的行:

    if (playerIsGrounded) {
    
    

    这告诉了我们需要知道的一切,即使我们不知道一行编程。

  3. 使用驼色外壳。这意味着变量中的每个新单词都以大写字母开头,以便帮助读者分解它(有时这不包括第一个单词)。比如:jumpheight要么写成jumpHeight要么写成JumpHeight。这不仅进一步提高了可读性,而且当您在检查器中查看变量时,您还会看到 Unity 将这些变量分解为单个单词。

  4. 避免使用“神奇数字”换句话说,不要通过赋予一个数字随机的重要性来规避编码挑战。我在第九章中这样做了,当时我使用了一个计时器,这个计时器对下落的砖块计时超过了零。计时器到达–70 时停止。为什么减七十?避免这种情况的一种方法是使用常量。常量是一种具有固定值的变量,一旦定义就不能更改。这没有内存开销,其主要目的通常是为了易读性。例如,我们可以创建一个值为–70 的常量整数,并将其命名为endOfFallAnimation。现在我们的滑车将停在endOfFallAnimation而不是–70。更有意义!还记得我们的Player脚本和它用 1 代表“右”,用 0 代表“左”吗?如果你从代码中抽出一段时间,然后再回来,这也可能会很混乱。所以为什么不用这个来代替:

    const int left = 0, right = 1;
    
    

    现在我们可以说

    if (facing == right)
    
    

    ,这对于我们来说更容易回读。(然而,这在检查器中仍然显示为 0 和 1。)使用常量的另一个优点是,如果以后需要修改,搜索和替换值要容易得多。

  5. 描述为什么而不是什么。写注释时,描述方法的目的比描述方法的作用重要得多。这个功能的相关性是什么?它和剧本的其他部分有什么关系?

  6. 尽可能避免重复编写相同的代码。可以放在不同方法中的代码越多,就越容易快速找到要找的内容,并且需要键入的内容也越少。使用方法还允许您将整个代码块从一个脚本复制并粘贴到下一个脚本。

  7. 使用循环!循环是一段不断重复的代码,直到满足或中断某个条件。比如一个while循环看起来是这样的:

    int count = 1;
            while (count <= 4)
            {
                count = count + 1;
            }
    
    

    这只是数到四然后停止,但是我们可以用这个结构执行同一个命令四次。然而实际上,对于使用增量变量的循环,使用"for"通常是有意义的。这是一个用更少的代码行完成同样事情的例子。一个for循环看起来是这样的:

    for ( init; condition; increment )
    {
       statement(s);
    }
    
    

    无论你使用哪种类型的循环,它们都有类似于方法的作用,帮助你将代码分段,防止你重复编写大量的函数。

  8. 使用智能标记和层。就像你需要对变量命名惯例有所了解一样,在 Unity IDE 中你也需要对你所分配的名字有所了解。到目前为止,你已经知道使用正确的养育方式和创建预设,而不是处理实例。

性能和兼容性

前面的技巧将有助于使你的代码更有逻辑性和可读性,在某些情况下还会更快。不过,实际上,速度方面的主要瓶颈将在您的脚本之外。

较小的图像

例如,你需要确保你使用的图片不是太大。图像越大,应用的文件就越大,加载时间也越长。我想告诉你,你的应用的大小并不重要,但这是一个谎言:当 APK 尺寸变得太大时,我个人已经收到了来自我自己用户的多个负面评论,所以这是人们真正关心的事情。

注意,如果有必要,你可以显示一个加载屏幕,从一个协同例程中加载场景(就像我们在Player.Death方法中使用的那样),然后在旧场景上显示一个加载 UI。然而,我们仍然希望加载时间尽可能短,所以你应该避免在你的场景中粘贴不必要的大图片。这是选择像素艺术风格很有意义的另一个原因:它让你保持较小的文件大小,然后放大它们,而不必担心像素化。选择正确的图像压缩方式(JPG 图像格式,而不是 PNG 格式,这样可以降低一点质量)也会有所帮助。重用素材也是如此,这就是为什么之前旋转磁贴是一个好的举措。

Unity 将在您构建 APK 时为您的图像添加额外的压缩,您可以在构建设置中设置想要使用的纹理压缩类型。这种额外的压缩会影响应用的速度和大小,还会影响它的兼容性以及它是否支持 alpha(透明度)。来自 Unity 自己的文档:

| 纹理格式 | 纹理使用了什么样的内部表示。这是尺寸和质量之间的权衡。 | | RGB 压缩 DXT1 | 压缩的 RGB 纹理。由 Nvidia Tegra 支持。每像素 4 位(256 x 256 纹理为 32 KB)。 | | RGBA 压缩 DXT5 | 压缩 RGBA 纹理。由 Nvidia Tegra 支持。每像素 6 位(256 x 256 纹理为 64 KB)。 | | RGB 压缩等 4 位 | 压缩的 RGB 纹理。这是 Android 项目的默认纹理格式。ETC1 是 OpenGL ES 2.0 的一部分,受所有 OpenGL ES 2.0 GPUs 支持。它不支持 alpha。每像素 4 位(256 x 256 纹理为 32 KB) | | RGB 压缩 PVRTC 2 位 | 压缩的 RGB 纹理。由 Imagination PowerVR GPUs 支持。每像素 2 位(256 x 256 纹理为 16 KB) | | RGBA 压缩 PVRTC 2 位 | 压缩 RGBA 纹理。由 Imagination PowerVR GPUs 支持。每像素 2 位(256 x 256 纹理为 16 KB) | | RGB 压缩 PVRTC 4 位 | 压缩的 RGB 纹理。由 Imagination PowerVR GPUs 支持。每像素 4 位(256 x 256 纹理为 32 KB) | | RGBA 压缩 PVRTC 4 位 | 压缩 RGBA 纹理。由 Imagination PowerVR GPUs 支持。每像素 4 位(256 x 256 纹理为 32 KB) | | RGB 压缩 ATC 4 位 | 压缩的 RGB 纹理。由高通骁龙支持。每像素 4 位(256 x 256 纹理为 32 KB)。 | | RGBA 压缩 ATC 8 位 | 压缩 RGBA 纹理。由高通骁龙支持。每像素 6 位(256x256 纹理 64 KB)。 | | RGB 16 位 | 六万五千种没有 alpha 的颜色。使用比压缩格式更多的内存,但可能更适合 UI 或没有渐变的清晰纹理。256 x 256 纹理需要 128 KB。 | | RGB 24 位 | 真彩色但没有 alpha。256 x 256 纹理需要 192 KB。 | | 阿尔法 8 位 | 高质量的 alpha 通道,但没有任何颜色。256 x 256 纹理需要 64 KB。 | | RGBA 16 位 | 低质量真彩色。带有 alpha 通道的纹理的默认压缩。256 x 256 纹理需要 128 KB。 | | RGBA 32 位 | 带有 alpha 的真彩色-这是带有 alpha 的纹理的最高质量压缩。256 x 256 纹理需要 256 KB。 | | 压缩质量 | 选择“快速”可获得最快的性能,“最佳”可获得最佳的图像质量,“正常”可在两者之间取得平衡。 |
碰撞

如果你在 Unity 中制作 2D 游戏,性能应该不是一个大问题。除非你在屏幕上有无数的元素,都在运行复杂的动画和脚本,否则大多数 Android 手机将能够处理你扔给它们的大多数东西。

但这并不是说尽可能降低应用的要求没有好处(例如,考虑电池消耗和将其他应用保存在内存中),而且你肯定希望避免应用变得无响应的任何机会。

那么,在运行时,需要考虑的最重要的事情之一就是你有多少个碰撞器。对撞机的大小并不重要,但问题是对撞机的数量和这些对撞机的复杂性。例如,我们的瓷砖使用单独的碰撞器,这使得开发更容易,并允许我们使用预置。这在很大程度上是我们的最佳实践,因为我们添加未来更新的灵活性和方便性远远超过了性能成本。参见图 10-13 。

A431865_1_En_10_Fig13_HTML.jpg

图 10-13。

I have drawn a single box collider around a bunch of tiles here

你可以用碰撞器使积木成为孩子,并把它们保存为一个预置,以便快速地在你的游戏中实现它们。或者,你可以简单地用他们自己的更大的碰撞器画更大的平台盒子。

请记住,表面下的瓷砖实际上不需要碰撞器。从这里移除碰撞器可能是让我们的应用运行得更好的最快最简单的方法之一。

比拥有许多小型碰撞器更糟糕的是使用具有许多不同点和角度的复杂多边形碰撞器(见图 10-14 )。这为 Unity 创造了更多的数学,因为它需要计算出每个点如何与它碰到的表面相互作用。这就是为什么对你的角色使用一个长方体碰撞器(或者是一个稍微变形的长方体的多边形碰撞器)比使用一个完全符合角色轮廓的多边形碰撞器更有意义。

A431865_1_En_10_Fig14_HTML.jpg

图 10-14。

An overly complex collider

即使是图 10-14 中过于复杂的碰撞器也不太可能导致任何明显的减速,但是如果你有很多这样的碰撞器,事情可能会变得有点不稳定。归根结底,这是一种浪费,因为它不会对玩家实际玩游戏的方式产生任何有意义的影响。

制作其他类型的游戏

在这一章中,我们已经讨论了很多关于游戏机制、设计和硬件之间的相互作用。但到目前为止,我们还没有真正考虑我们正在开发的平台的性质。

毕竟,该平台可追溯到 NES 和其他早期计算机,并不自然地适合移动设备的触摸屏输入。Android 平台游戏肯定还有市场,这是一个特别好的教程选择,因为它允许我们尝试许多不同的概念。

但是如果你想涉足一个更适合手机的类型,你可能会选择开发一个无限跑者。除了一个关键的不同,这看起来和行为都像一个平台玩家:玩家不断向前跑。很好的例子包括 Canabalt、Sonic Runners、Super Mario Run、Temple Run 和 Jetpack Joyride。在这里,玩家只需要一个输入——跳转——就可以清理屏幕(不再有箭头键遮挡游戏空间),并提供完美的快速进出游戏。

要使您构建的程序成为无限运行程序,您只需修改Player脚本,使其自动向前运行。然后你可以在设计关卡时考虑到这一点,或者如果你想让关卡真正“无限”的话,让你的关卡自己动态生成(称为过程生成)。这意味着你需要引入一种算法来随机实例化新的平台(并且很可能破坏旧的),同时确保玩家总有一条穿越的路线。使用更大的平台瓷砖通常是一个好主意,当然你需要难度和速度来逐渐增加。

你同样可以把重力从物理引擎中移除,把它变成某种太空射击游戏,甚至是自上而下的游戏。

益智游戏和更多

Android 平台上的平台游戏、第一人称射击游戏和赛车游戏的潜在问题是,它们本质上涉及到在新硬件上改造旧游戏类型。相反,可以说最有创造性和最有趣的 Android 游戏是那些找到新方法来利用硬件的游戏。

《愤怒的小鸟》就是一个很好的例子,因为它以一种非常自然的方式利用触摸屏来开辟新的游戏可能性。房间和纪念碑谷甚至更进一步,让玩家通过伸出手触摸、扭转和拖动各种元素,甚至有时包括倾斜控制,直接与游戏世界互动。还记得我们说过玩家喜欢感觉他们正在影响游戏世界吗?

你可以让你的游戏像这样简单地使用手机的加速度计:

rb.velocity = new Vector2(Input.acceleration.x, rb.velocity.y);

如果倾斜手机会导致敌人和收藏品滑过屏幕怎么办?

同样,您也可以非常轻松地使用多点触控,这开启了一系列其他可能性:

void Update ()
    {
        Touch myTouch = Input.GetTouch(0);

        Touch[] myTouches = Input.touches;
        for(int i = 0; i < Input.touchCount; i++)
        {
            //Do something with the touches
        }

不要被“游戏”必须是什么的旧观念所限制。你可以设置任何条件来结束这一关,无论是让一个球滚到一个像触发器一样的目标上,还是当玩家收集到屏幕上的每一个硬币时进行计数。甚至根本不需要有一个“玩家”对象——看看俄罗斯方块就知道了,这是最早的移动热门游戏。

哦,说到不同种类的手机游戏,我在下一章为你准备了一些令人兴奋的东西。首先,我们将讨论如何为 Android 创建一个具有逼真图形的 3D 游戏(是的,你可以做到)。然后,我们将讨论如何使用三星 Galaxy Gear 或谷歌的 Daydream View 耳机进入那个世界。

对于移动开发者来说,这是一个令人兴奋的新领域,我们将确保你正处于这一浪潮的顶峰。

A431865_1_En_10_Fig15_HTML.jpg

图 10-15。

Now that's starting to look like a game I want to play!