精通-Unity-安卓游戏开发-全-

精通 Unity 安卓游戏开发(全)

原文:zh.annas-archive.org/md5/287d0af1dda41532daae19cd39aaf62c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Unity 从其谦逊的起点已经走了很长的路;然而,在过去几年中,它几乎已经成为一个行业通用的工具,几乎所有独立游戏开发者都使用它来开发他们的游戏。它非常易于使用,可以快速开发原型,而且当你有一个成功的想法时,它足够灵活,可以将小型原型扩展成一个完整的游戏。

尽管 Unity 具有专业能力,但它使用起来非常简单,即使是完全的新手也能在几小时内开发出一个基本游戏。而且,只要稍加努力,就可以开发出一个非常精致的游戏,具有出色的光照和动画。

使用当前版本,Unity 使游戏开发对每个人来说都更加容易接近。

在这本书中,我们将涵盖 2D 和 3D 游戏开发的各种主题。我们将看到如何在 2D 中开发类似喷射背包冒险游戏的游戏,以及一个带有完整 3D 动画、光照和相机的 3D 战斗游戏。我们还将看到如何添加按钮、文本和屏幕过渡。最后,一旦我们创建了游戏,我们将看到如何通过添加应用内购买和广告来实现盈利。

本书涵盖的内容

第一章,使用 Unity3D 介绍 Android 游戏开发,涵盖了 Android 游戏开发的基本概念、Android 游戏简史、Unity3D 中 Android 游戏的基本构建块以及游戏的基本流程。

第二章,完成活泼企鹅 2D 游戏,通过完成喷射背包冒险游戏克隆游戏来扩展 2D 游戏开发。本章介绍了各种主题,如粒子系统、相机管理、预制体、动画、触发器、碰撞器和基本的 GUI 系统。

第三章,动作战斗游戏玩家角色,涵盖了 3D 动作战斗游戏的基本设置,包括导入模型和纹理、为角色设置绑定、在模型上应用动画,以及使用虚拟屏幕摇杆控制玩家角色。

第四章,具有 AI 的敌人角色,涵盖了从导入模型到应用动画再到使用 AI 进行决策的游戏敌人模型创建方面。

第五章,游戏玩法、UI 和效果,展示了如何完成游戏玩法循环,添加用户界面。

为游戏添加计分文本,并添加游戏粒子效果。

第六章,游戏场景和场景流,涵盖了创建主菜单场景,解释选项场景,并演示如何在游戏中切换场景。

第七章,游戏统计、社交、IAP 和广告集成,演示了如何保存游戏进度,添加社交媒体集成,如 Facebook 和 Twitter,以及广告集成。

并通过应用内购买来增加盈利。

第八章,声音、收尾工作和发布,让我们为游戏添加收尾工作和声音。我们将了解如何在设备上运行游戏并将游戏发布到 Android Play 商店。

阅读本书所需的条件

您需要最新版本的 Unity,您可以从他们的网站下载,并需要一个能够运行 Unity 的计算机。需要具备基本的 C# 知识,因为本书中的代码是用 C# 编写的。尽管游戏可以在 Android 模拟器上运行,但要看到游戏的实际性能,需要一个 Android 设备。

本书面向的对象

本书面向希望扩展 Unity3D 知识并创建高端、复杂 Android 游戏的初学者或中级 Unity3D 开发者。您应具备基本的或中级 Unity3D 理解,包括其环境、基本概念如游戏对象和预制体,以及使用 Unity 脚本。

C# 或 JavaScript,以及如何使用 Unity3D 开发基本的 2D/3D 游戏。

本书对那些为 Android 开发过基本/简单游戏的 Unity 开发者非常有用,他们想了解高端复杂游戏(如详细动画、多个关卡、角色能力、敌人弱点、智能 AI、成就、排行榜等)的细节和核心组件。

惯例

在这本书中,你会发现许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:“The”

bIsDefending 变量与我们定义的动画控制器参数相同。

代码块以如下方式设置:

    private Animator anim;
    // Use this for initialization

    void Start () {
        anim = GetComponent<Animator>();
    } // start

当我们希望将您的注意力引向代码块中的某个特定部分时,相关的行或项目将以粗体显示:

    if (pAnim.GetBool("tIsPunching")){
        if (anim.GetBool("bEnemyIsDefending") == false) {
            Debug.Log("enemy got hit");
            anim.SetTrigger("tEnemyGotHit");
            anim.SetBool("bEnemyIsDefending", true);
 health -= pScript.damage;
        }
    }

任何命令行输入或输出将以如下方式书写:

C:\Program Files\Unity\Editor\Unity.exe

新术语和重要词汇将以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:“Unity 允许开发者通过检查器面板管理每个游戏对象的这些组件。”

警告或重要注意事项将以如下方式显示。

小贴士和技巧将以如下方式显示。

读者反馈

我们欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们非常重要,因为它帮助我们开发出您真正能从中受益的标题。

要发送一般性反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及本书的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载。

文件下载后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-Android-Game-Development-with-Unity。我们还有其他来自我们丰富图书和视频目录的代码包可供选择,网址为github.com/PacktPublishing/。请查看它们!

下载本书的彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/MasteringAndroidGameDevelopmentwithUnity_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误表中的现有勘误列表。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support并在搜索字段中输入书籍名称。所需信息将出现在勘误表部分。

侵权

互联网上对版权材料的侵权是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过copyright@packtpub.com与我们联系,并提供疑似侵权材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面所提供的帮助。

询问

如果您对本书的任何方面有问题,您可以联系我们的questions@packtpub.com,我们将尽力解决问题。

第一章:使用 Unity3D 进行 Android 游戏开发简介

在当今智能手机时代,这曾经是电脑时代,几乎地球上每个人都在手中拿着一部智能手机。2014 年售出了约 10 亿部 Android 手机,这对在 Android 上工作的开发者来说是一个庞大的受众。这些开发者投入努力创建高实用性应用,解决用户的问题或制作吸引人且有趣的游戏,让玩家在享受乐趣的同时消磨时间,并获得良好的互动体验。本书主要关注后者,即通过一个非常著名的游戏引擎 Unity3D 创建吸引人的有趣游戏。

本章包括以下主题。

  • Android 简介

  • Unity3D 和游戏引擎

  • Unity 游戏开发基础

  • 空游戏项目的配置

  • 开始启动活泼的企鹅游戏

  • 将企鹅添加到游戏中

Android 简介

Android 是一个基于 Linux 的操作系统,由谷歌公司根据 Apache 许可协议分发的开源软件。由于其开源性质,其他手机厂商开始在其新创建的手机上移植 Android 操作系统,这为消费者提供了一个非常多样化的智能手机市场。从 Android 的第一个版本开始,这个操作系统就达到了一个很好的成熟度水平,使其成为更可靠、更安全、更稳定的智能手机操作系统。接下来,让我们看看下一节中一些流行的 Android 版本。

Android 版本

每年,随着每个新版本的推出,Android 的成熟度都在不断提高。每个版本都引入了从用户界面到定制、灵活性到安全性的新功能集合。在命名方面,这些版本基于糖果、巧克力和其他甜食的名称,如 Kitkat、Lollipop 和 Marshmallow,但这也使得 Android 对消费者和开发者来说更加易于理解。

一个有趣的事实是,Android 版本按字母顺序排列。从 Apple Pie 1.0Banana Bread 1.1 开始,它通过完全一致的字母顺序一直发展到 Nougat,保持了传统的延续性。

下表突出了不同 Android 版本的主要功能及其 API 级别:

Android 版本 版本名称 主要功能 API 级别 发布月份
1.0 G1 Banana Bread GPS、蓝牙、多任务处理、谷歌服务、Android 应用商店 2 2008 年 2 月
1.5 Cupcake 搜索框,改进的 Android 应用商店,相机,手势 3 2009 年 4 月
1.6 Donut 虚拟键盘,主屏幕小部件,文件夹 4 2009 年 9 月
2.0.x Éclair 多用户账户,Flash 支持,缩放功能,蓝牙 2.1 5、6 和 7 2009 年 10 月
2.2.x Froyo USB 热插拔,热点支持,Adobe Flash,语音拨号 8 2010 年 5 月
2.3.x Gingerbread 新的复制/粘贴、WebM、NFC、前置摄像头 9、10 2010 年 12 月
3.x Honeycomb 3D 图形、重新设计的用户界面、视频通话、蓝牙共享、3G、4G 11、12 和 13 2011 年 2 月
4.0.x Ice Cream Sandwich 虚拟按钮、面部解锁、原生相机功能、面部识别、Android Beam、Wi-Fi Direct 14 和 15 2011 年 10 月
4.1 - 4.3 Jelly Bean 可扩展的通知、Google Now 16、17 和 18 2012 年 7 月
4.4 KitKat 主要设计界面更新、半透明状态栏、沉浸模式、无线打印 19 和 20 2013 年 10 月
5.0 Lollipop 使用 Material 重新设计的用户界面、锁屏通知、访客模式、省电模式 21 2014 年 10 月
6.0 Marshmallow 指纹安全支持、省电的 Doze 模式、应用待机模式、增强的应用权限 23 2015 年 10 月 5 日
7.0 Nougat 多窗口视图、VR 支持 24 和 25 2016 年 8 月 22 日

表 1.1:从安卓 Cupcake 到 Lollipop 的操作系统时代

该表仅显示了安卓操作系统开发商 Google Inc. 所做的更改。但由于安卓的开源特性,其他移动制造公司也对安卓进行了更改并引入了新的功能和修改。例如,三星开发了名为 TouchWiz 的定制触摸界面,而 HTC 开发了名为 HTC Sense 的定制用户界面。同样,索尼引入了名为 TimeScape 的定制用户界面。

图 1.1 2014 年发布的最新安卓手机

Google Play - 安卓的市场商店

智能手机的出现使得市场商店的概念在软件技术中得以实现,这彻底改变了移动开发行业。在这一变革中,Google Play 在移动应用和游戏方面发挥了重要作用。Google Play 是智能手机上最大的市场商店,拥有超过 220 万个安卓应用、游戏、书籍、音乐、节目等。到目前为止,这些应用和游戏已被下载超过 500 亿次,这标志着 Google 在安卓仅 8 年的时间里取得的巨大里程碑。您可以在 play.google.com 查看 Google Play。

Google 音乐、Google 电影与电视、Google 书籍和 Google 杂志仅在少数国家提供。

图 1.2 安卓设备上的 Google Play

Unity3D 和游戏引擎

游戏引擎是一个为视频游戏创建和开发而设计的软件框架。许多工具和框架可供游戏设计师和开发者使用,以便快速、轻松地编写游戏代码,而无需从头开始构建。随着时间的推移,游戏引擎变得更加成熟,对开发者来说更加容易使用,拥有功能丰富的环境。从 Android 的原生代码框架如 Unity、Unreal、Cocos2D-x、LibGDX 等开始,游戏引擎开始提供干净的用户界面和拖放功能,使游戏开发对开发者来说更加容易。这些引擎包括许多不同的工具,这些工具在用户界面、功能、移植等方面有所不同,但它们都有一个共同点;那就是它们最终都用于创建视频游戏。

我们将在本节中通过比较以下方面来讨论一些最受欢迎的游戏引擎:

  • 用户界面和环境,比如学习和发展游戏有多容易。

  • 特性和功能,比如游戏引擎能实现什么,能做什么,以及达到什么质量水平。

  • 定价,比如是免费还是付费。

让我们更深入地探讨这个比较。

Unreal Engine

Unreal Engine (www.unrealengine.com) 是由 Epic Games 开发的游戏引擎。它曾是 Epic Games 的内部游戏引擎,并于 1998 年在第一人称射击游戏 Unreal 中首次展示。Unreal Engine 主要用于创建第一人称或第三人称射击游戏,但在其他类型如潜行、MMORPGs 等方面也表现出相当高的质量。Unreal Engine 具有高度的可移植性和易于使用的界面特性,其中大部分逻辑和行为是用 C++语言编写的。

最新发布的 Unreal Engine 4 支持 Unity 支持的所有平台,包括 Windows、Xbox One、Windows RT、OS X、Linux、PlayStation 4、iOS、Android、Ouya 以及使用 WebGL 的浏览器。

Unreal Engine 4 于 2014 年 3 月发布,供公众使用。Unreal Engine 拥有良好的用户界面,导航控制非常精致且易于使用。Unreal Engine 提供了非常容易的流程和界面来创建第一人称射击游戏,并包含产生 AAA 质量游戏的功能,包括使用体素锥体追踪进行实时全局照明,消除预计算照明。

您可以下载该引擎,并用于从游戏开发、教育、建筑和可视化到 VR、电影和动画的各个方面。当您发布游戏或应用程序时,您需要在每个产品每个季度收入超过 3,000 美元后支付 5%的版税。由于 Unreal Engine 的学习曲线略高,因此它并不最适合新晋的游戏开发者:

图 1.3 Unreal Engine 4 界面

Adobe Flash professional

Adobe Flash(以前称为 Macromedia Flash)(www.adobe.com) 是一个用于创建矢量图形、动画、游戏和富互联网应用(RIAs)的多媒体和软件平台,这些应用可以在 Adobe Flash Player 中查看、播放和执行。Flash 广泛用于创建动画和网页浏览器中的广告,但近年来,由于 HTML5 框架的使用,其创建游戏的应用大幅减少。

曾经有一段时间,Adobe Flash 是最受欢迎的在线浏览器游戏引擎,并引发了一波独立游戏开发者为在线门户(如 Kongregate 和 Miniclip)制作游戏的浪潮。

Adobe Flash 包含非常易于使用的界面,可以创建令人惊叹且强大的动画,允许艺术家在编辑器中直接创建矢量艺术。它还支持 Adobe Illustrator 和 Adobe Photoshop 层,以便轻松集成矢量艺术和动画。Adobe Flash 允许开发者使用 Adobe AIR 框架将他们的游戏和动画从浏览器移植到桌面(Mac 和 Windows)、Android 和 iOS,但由于在终端设备上的性能较低,因此无法吸引到游戏开发者和艺术家的广泛关注。

Adobe Flash 提供了 30 天的免费试用,但一旦试用结束,你必须购买许可证,这与本节中讨论的其他引擎不同。

图片

图 1.4 Adobe Flash CC 界面

游戏制作工作室

游戏制作工作室(最初命名为 Animo,后来更名为 Game Maker)是由 Mark Overmars 在 1999 年使用 Delphi 编程语言创建的一个事件驱动游戏创作系统。最初是为 2D 动画而创建的,它很快成为了一个非常强大且易于使用的拖放工具,用于创建 2D 游戏。

游戏制作工作室使用预定义的事件来创建游戏中的动作,这使得开发者可以非常容易地创建游戏,而无需具备编程和编码知识。该工具包含一个名为 Game Maker 语言GML)的沙盒语言,允许开发者为其游戏定义自定义和更复杂的操作。

游戏制作工作室提供了一个干净且流畅的用户界面,让开发者可以在 Windows、macOS X、Ubuntu、HTML5、Android、iOS、Windows Phone 8 和 Tizen 上构建和部署他们的游戏。最新版本还引入了 Xbox One 和 PlayStation 部署。

Tizen 是一个开源操作系统(OS),适用于所有设备,包括手机、可穿戴设备、车载娱乐系统和电视。

图片

图 1.5 游戏制作工作室界面

Unity3D

Unity(unity3d.com)是由 Unity Technologies 开发的跨平台游戏引擎。它在 2005 年苹果全球开发者大会上首次公开宣布,最初仅支持 Mac OS 的游戏开发,但自那时起已扩展到支持超过 15 个平台,包括桌面、移动和游戏机。它因其一键式跨多个平台(包括 BlackBerry 10、Windows Phone 10、Windows 10、OS X、Linux、Android、iOS、Unity Web Player(包括 Facebook)、Adobe Flash、PlayStation 3、PlayStation 4、PlayStation Vita、Xbox 360、Xbox One、Wii U 和 Wii)移植游戏的能力而闻名。

Unity 拥有一个出色的界面,让开发者从一开始就能非常高效地管理项目。它具有不错的拖放功能,以及用 C#和 Boo(JavaScript 的一种方言)编写的连接行为脚本,可以轻松地用视觉对象定义自定义逻辑和功能。Unity 已被证明对新开始游戏开发的开发者来说非常容易学习,现在越来越多的大型工作室也开始使用它,这也是有充分理由的。

Unity 是那些为 2D 和 3D 游戏提供支持而不让开发者感到麻烦和困惑的引擎之一。它拥有大量的在线教程、优秀的文档和一个非常有帮助的开发者社区。此外,Unity 还有资产商店,开发者可以在其中出售 Unity 的可重用组件,以减少其他开发者的开发时间和努力。您可以在assetstore.unity3d.com检查 Unity 资产商店。

Unity Plus 和 Pro 需要付费,而 Unity Personal 免费;它适用于年收入低于 10 万美元的个人或公司。更多信息,请访问 Unity 商店store.unity.com/

图片

图 1.6 Unity3D 引擎界面

Unity3D 的特点

Unity 是一个强大的渲染引擎、直观的工具、2D 和 3D 游戏快速工作流程、一站式部署支持以及数千个免费和付费资产的游戏开发生态系统。其功能列表包括以下内容:

  • 简便的工作流程,允许开发者在一个直观的编辑器工作空间中快速组装场景

  • 质量上乘的游戏制作,例如 AAA 级视觉效果、高清音频和全速动作,屏幕上无任何故障

  • 专为 2D 和 3D 游戏制作提供专用工具,并采用共享约定,使开发者易于使用

  • 一个非常独特且灵活的动画系统,可以在极短的时间内创建自然动画

  • 在所有开发者发布游戏的平台上,实现流畅的帧率和可靠的性能

  • 一键式部署到所有平台,从桌面到浏览器到移动设备再到游戏机,只需几分钟

  • 通过使用在庞大的资源库中可用的已创建的可重用资产,可以缩短开发时间

总结来说,与其他游戏引擎相比,Unity 对开发者友好,易于使用,对独立开发者免费,并且功能丰富的游戏引擎。在下一节中,我们将看到 Unity3D 的一些令人惊叹的功能。

Unity 游戏开发基础

在深入研究 Unity3D 和游戏开发概念之前,让我们先看看 Unity 5.6 的一些基础知识。我们将了解 Unity 界面、菜单项、使用资产、创建场景和发布构建。

本节是所有对 Unity 几乎一无所知或完全没有知识的新开发者必须阅读的,他们希望通过 Unity 学习游戏开发的基础知识。如果你已经熟悉 Unity 的基础知识,你可以跳过这一节。

Unity 编辑器界面

当你第一次启动 Unity 5.6 时,你将看到一个包含屏幕左侧、右侧和底部的几个面板的编辑器。这些面板无需担心。以下图片显示了首次启动时的编辑器界面:

图片 1.7 Unity 5 首次启动时的编辑器界面

图 1.7 Unity 5 首次启动时的编辑器界面

首先,花点时间熟悉编辑器,并对其有一个基本的了解。Unity 编辑器被划分为不同的小型面板和视图,可以拖动它们,从而根据开发者/设计师的需求定制工作区。Unity 5 提供了一些预构建的工作区布局模板,可以从屏幕右上角的布局下拉菜单中选择,如下截图所示:

图片 1.8 Unity 5 编辑器布局

图 1.8 Unity 5 编辑器布局

当前编辑器中显示的布局是默认布局。你可以选择这些布局,看看编辑器界面如何变化,以及不同的面板如何在每个布局中放置在不同的位置。本书使用 2x3 工作区布局进行游戏开发。

以下截图显示了带有视图和面板名称高亮的 2x3 工作区:

图片 1.9 Unity 5 2x3 布局与视图和面板名称

图 1.9 Unity 5 2x3 布局与视图和面板名称

如前一个截图所示,Unity 编辑器包含不同的视图和面板。每个面板和视图都有特定的用途,将在以下部分进行描述。

场景视图

场景视图是游戏开发的整个舞台,它包含了从微小的点到任何重型 3D 模型的游戏中所有资产。场景视图用于选择和定位环境、角色、敌人、玩家、摄像机以及所有可以放置在舞台上的其他对象。所有可以放置并在游戏中显示的对象统称为GameObject。场景视图允许开发者操纵游戏对象,例如选择、缩放、旋转、删除和移动。简单来说,场景视图是开发者和设计师的交互式沙盒。场景视图提供了一些控制,例如导航和变换。

变换工具

在 Unity 中开发游戏时,您将在场景中放置许多游戏对象,它们的定位、缩放和旋转,统称为变换,由变换工具管理。以下截图显示了变换工具:

图片

图 1.10 变换工具

您可以从这个工具栏中选择任何选定的变换操作,并相应地更改游戏对象。以下图显示了在选择了变换工具时所选游戏对象上的 Gizmo:

图片

图 1.11 变换工具的游戏对象上的 Gizmos

这些工具确实像它们的名称所暗示的那样执行相同的任务;移动用于平移,旋转用于旋转,缩放用于缩放。另一方面,矩形工具是在 Unity 4.3 中引入的,当时 Unity 获得了本地的 2D 支持和工具。这个工具仅用于 2D 精灵对象的位置、缩放和旋转。您也可以使用键盘快捷键选择这些工具。

场景视图导航

在上一节中,我们讨论了 GameObject 如何在场景中进行变换和导航。但 Unity 作为一个 3D 环境,它提供了一个简单的界面,使用鼠标和键盘快捷键从不同的角度、侧面和视角查看场景。您可以在场景视图的右上角观察到场景 Gizmo。这个 Gizmo 用于根据开发者的需求旋转视图。以下截图显示了场景的 Gizmo:

图片

图 1.12 场景视图导航的 Scene Gizmo

场景的每个视图都是以透视或等距的形式显示的。在下一节中,我们还将讨论场景的另一种视图。

场景视图控制栏

控制栏位于场景视图的顶部,它为开发者提供了更多的控制,以便轻松导航场景和创建游戏。此栏包括启用/禁用 Gizmos、声音和选择视图模式等选项。此栏的重要部分是 2D 模式按钮,如下面的截图所示:

图片

图 1.13 场景视图控制栏

2D 模式按钮是一个切换按钮;当开启时,它会禁用视图的 z 轴,并从 2D 视角显示游戏。在 Unity 中创建 2D 游戏时,这是一个高度使用的选项。在栏的右侧有一个用于搜索的文本框。这允许开发者从当前场景中搜索游戏对象,并使他们能够快速工作。

游戏视图

游戏视图是游戏在发布和部署到目标设备时的最终表现形式,它是从场景的相机渲染的。此视图与整个 Unity 工作区顶部的“播放模式”导航栏相连,如下截图所示:

图 1.14 播放模式栏

当在编辑器中玩游戏时,这个控制栏会变成蓝色。Unity 的一个非常有趣的功能是它允许开发者暂停正在运行的游戏和代码,开发者可以在运行时查看和更改属性、变换等,而无需重新编译整个游戏,以便快速工作流程。

游戏视图控制栏

与场景视图一样,游戏视图也在视图的顶部包括一个控制栏,如下图所示:

图 1.15 游戏视图控制栏

选项执行的动作正如其名称所暗示的那样。控制栏左侧的“自由纵横比”下拉菜单允许开发者选择任何特定的分辨率来测试他们的游戏。这些分辨率和下拉选项取决于所选平台。开发者还可以为他们的目标设备添加自定义分辨率和屏幕尺寸。Unity 还允许开发者指定纵横比,以便了解游戏将在具有相同纵横比的各种设备上如何运行。Unity 功能强大,可以轻松提供跨平台支持,使得开发者只需编写一次代码就能让他们的游戏在大多数类型的设备上运行得更好。

层级视图

层级视图是选择或处理场景中任何 GameObject 的第一个点。这包含当前场景中的所有游戏对象。这是一个树形结构,允许开发者轻松地在游戏对象上利用父级和子级概念。以下截图显示了一个简单的层级视图:

图 1.16 层级视图

项目浏览器面板

这看起来像是一个视图,但它被称为项目浏览器面板。这个面板是 Unity 中的一个嵌入式文件目录,包含游戏项目中包含的所有文件和文件夹。

下面的截图显示了一个简单的项目浏览器面板:

图 1.17 项目浏览器面板

面板左侧显示了一个层次目录,而面板的其余部分则是文件,或者如 Unity 中所称,这些被称为资源。Unity 使用不同的图标来区分这些文件类型。这些文件可以是精灵图像、纹理、模型文件、声音等等。您可以通过在搜索文本框中输入来搜索任何特定文件。在搜索框的右侧,有一些按钮控件,用于进一步筛选,例如动画文件、音频剪辑文件等。

项目浏览器面板的一个有趣之处在于,如果资源中不存在任何文件,Unity 会开始在 Unity Asset Store 中寻找它,并向您展示可用的免费和付费资源。

检查器面板

这是 Unity 开发中最重要的面板。Unity 以游戏对象和资源的形式构建游戏。这些游戏对象进一步包含变换、碰撞体、脚本和网格等组件。Unity 允许开发者通过检查器面板管理每个游戏对象的这些组件。

以下截图显示了一个游戏对象的简单检查器面板:

图 1.18 检查器面板

这些组件的类型各不相同,包括物理、网格、效果、音频和用户界面。这些组件可以通过从组件菜单中选择来添加到任何对象。以下截图显示了组件菜单:

图 1.19 组件菜单

在了解了 Unity 的一些基础知识之后,让我们继续进行开发者创建游戏时做的第一个任务;那就是创建一个空项目。接下来的一节中,我们将讨论 Unity 中空项目的配置。

空游戏项目的配置

当您开始一个新的游戏时,第一步是配置一个空游戏项目。对于 2D 游戏,创建空游戏并设置初始环境和相机管理有时可能是一个痛苦的过程。在本节中,我们将讨论如何为 2D 游戏配置空游戏项目。

如果您已经在 Unity 中制作过 2D 游戏,那么您可以跳过这一节,并使用下一节代码中的 Perky Penguin 的起始项目。

当您启动 Unity 5.6 时,它会显示一个项目向导,如下所示:

图 1.20 项目创建向导

Unity 在其最新版本 Unity 5.6 中拥有一个良好且流畅的用户界面。项目向导显示了所有最近的项目列表,包括它们的名称。最近的项目被突出显示,以便快速打开。在向导的右上角,有一些控件,用于从头创建新项目以及从任何目录打开任何特定项目。为了指导新开发者了解基本概念,"开始"选项卡提供了一个基本的视频教程。

点击右上角的“新建项目”按钮,创建新项目,您将看到一个如下对话框:

图 1.21 项目创建向导

有两个文本输入框,项目名称和位置。它们的名称是自解释的。您也会在向导中注意到 3D 和 2D,如下面的图所示:

图片

图 1.22 项目类型选择切换

此切换允许您告诉 Unity 您的项目是 2D 还是 3D。虽然这不会在您工作期间以任何方式影响项目,但它会影响默认项目设置,以便更容易地工作流程。例如,在 3D 模式下,当您将任何图像资产导入项目时,Unity 将其视为纹理;而在 2D 模式下,Unity 将其视为 Sprite 类型。您也可以在项目创建后随时更改模式,不需要在项目创建时选择。默认情况下,Unity 将在 3D 模式下创建项目。

除了 2D/3D 模式外,您还会在项目创建向导的底部注意到“资产包...”按钮。Unity 的最好特性之一是资产支持。Unity 允许开发者通过 Unity Asset Store 创建、分发和销售可重用的插件和附加组件,这些插件和附加组件被称为 Unity Assets,Unity Asset Store 可在assetstore.unity3d.com找到。Unity 附带大量免费资产,帮助您在几分钟内完成一些事情。此按钮允许您选择要导入新项目的哪些资产,如下所示:

图片

图 1.23 资产包对话框

您可以选择任何包或多个包,新项目将包含已导入的这些包。目前,您不需要导入任何包。您也可以在需要时再导入这些包。

因此,对于项目创建向导,我们已将我们的项目命名为“Perky Penguin”,并选择了 2D 模式。点击“创建项目”按钮,您将看到 Unity 界面,其中包含一个空场景和项目。

首步是确保您处于 2D 或 3D 模式。如果您选择了 2D 模式,那么您将在场景视图的控制栏上看到 2D 切换处于激活状态,如下面的截图所示:

图片

图 1.24 场景视图中的控制栏

此外,您还需要检查编辑器设置,以确保项目处于 2D 模式。您可以通过从编辑菜单中选择项目设置中的“编辑器”选项来进入编辑器设置,如下面的图所示:

图片

图 1.25 编辑器设置菜单

选择“编辑器”选项后,您将在检查器面板中看到设置。请确保默认行为模式中的模式设置为 2D,如下面的截图所示:

图片

图 1.26 检查器面板中的编辑器设置

如果你想让 Unity 将图像解释为纹理并默认启用其他 3D 设置,你也可以将其更改为 3D 模式。最后,确保场景处于 2D 模式,你需要检查相机属性。你可能已经注意到了,在创建新项目时,在空场景中放置了一个相机游戏对象。

选择相机对象,你将在检查器面板中看到其属性,如下面的截图所示:

图片

图 1.27 主相机设置

你会注意到它的位置将被设置为(0,0,-10),其投影设置将作为正交。

在 Unity 中,使用正交投影模式进行 2D 操作是一种良好的实践。

正交投影是将 3D 对象表示为 2D 的一种方法。正交视图在工程中常用作产生明确传达尺寸的对象规范的手段。例如,如果你正在查看一个包含建筑的较大场景,那么正交渲染可以清楚地测量建筑之间的距离及其相对大小。

因此,在确认项目处于 2D 模式后,让我们保存场景。Unity 项目包含一个根目录为 Assets,其中放置了项目中使用的所有资源。这些资源可以是场景、脚本、纹理、精灵、模型、预制件或材质。在 Unity 中没有传统或标准的方法来管理资源,每个开发者都有不同的方法。在这本书中,我们将遵循一种简单的资源管理方法。我们管理资源的方法是在Assets目录中为每种资源类型创建不同的文件夹。以下图显示了Assets文件夹中的目录结构:

图片

图 1.28 Unity 中的资源目录

在 Assets 中创建文件夹后,让我们将配置好的相机保存为名为PerkyPenguin_GameplayScene.unity的空场景到 Scenes 目录中。场景文件具有.unity扩展名,这些文件包含用于相机、玩家、敌人、障碍物、环境、控制等的不同游戏对象。场景就像游戏中的不同关卡。任何游戏都可以有一个或多个场景,并且没有必要为每个关卡创建不同的场景。非常重要的一点是,游戏中所有的场景都应该添加到构建设置中,以便在最终包中部署。我们将在后面的章节中更详细地讨论部署问题。

保存场景后,我们只剩下最后一件事要完全配置一个空的游戏项目。那就是配置游戏视图,以便我们可以测试我们的游戏。由于这本书是关于 Android 游戏开发的,我们必须将我们的项目目标设置为 Android 设备。最初,默认的目标平台将被设置为 PC、iMac 和 Linux 独立。你可以在文件菜单中的构建设置中看到它,如下面的截图所示:

图片

图 1.29 Unity 中的资源目录

您将看到一个包含所有可能的构建游戏平台和游戏中的所有场景的对话框,如图所示:

图片

图 1.30 构建设置

从平台列表中选择安卓,然后点击切换平台按钮,项目将更改为安卓设备。在更改目标平台为安卓后,您可能不会注意到任何变化,但一个简单的方法是查看游戏视图中的分辨率列表。您可以通过在游戏视图的控制栏中点击自由宽高比来实现,如图所示:

图片

图 1.31 安卓平台分辨率列表

为了创建一个能在所有安卓设备上运行的游戏,我们必须为视口实现选择一个主要的目标尺寸。我们选择了 1280x800 作为我们的目标设备在横屏模式下的尺寸。我们将在后面的章节中详细讨论跨分辨率的方法。

您也可以通过在分辨率列表中选择小加号按钮来添加自己的自定义分辨率。它将显示一个对话框,让您输入分辨率的尺寸,可以是像素或以名称表示的宽高比。窗口如图所示:

图片

图 1.32 在游戏视图中添加自定义分辨率

最后,在将项目设置为 2D 模式并更改平台为安卓以及所需分辨率后,我们只剩下一个小任务,那就是根据我们的目标分辨率配置相机。从层级面板中选择主相机,并将相机组件中的尺寸更改为 3.2,如图所示:

图片

图 1.33 相机尺寸

到目前为止,已经创建了一个 2D Unity 游戏项目,其中包括一个初始目录结构和包含 2D 配置相机的空场景。在下一节中,我们将讨论本章将创建的游戏以及如何从头开始创建任何游戏。

Perky Penguin 游戏

本节将介绍我们将在本章和第二章“完成 Perky Penguin 2D 游戏”中创建的游戏。我们的游戏名称是Perky Penguin,它是一款基于 Jetpack Joyride 的游戏。本章的最终游戏如下所示:

图片

图 1.34 Perky Penguin 游戏玩法

Perky Penguin 游戏灵感来源于 Jetpack Joyride 游戏。Jetpack Joyride 是由 Halfbrick Studios 开发的一款 2011 年的侧滚动无尽跑酷和动作视频游戏。它最初于 2011 年在 App Store 上为 iOS 设备发布,但后来被移植到包括 Facebook、安卓、Flash、PlayStation、BlackBerry 和 Windows Phone 在内的许多系统。

在开发期间,Jetpack Joyride 游戏被命名为Machine Gun Jetpack

创建一个类似 Jetpack Joyride 的游戏的原因是为了教开发者如何从零开始创建 Unity 中的 2D 游戏的角度和方法。因为 Jetpack Joyride 游戏包含了几乎所有 2D 游戏都实现的基本功能,如侧滚动、视差滚动、精灵表、跳跃、随机障碍生成、敌人生成、敌人人工智能、粒子系统和动画。

活泼的企鹅游戏玩法

游戏特色是一只站在冰上的企鹅,非常酷(有意为之),因此被称为企鹅。为了在全球化石时代穿越冰原,她得到了一个绑在背上的喷气背包。游戏使用简单的单触控制系统来控制企鹅;当玩家在触摸屏上按下任何地方时,企鹅的喷气背包就会发射,企鹅升空,给人一种她正在飞行的感觉。当玩家松开手时,喷气背包关闭,企鹅落下。游戏以侧面视角持续进行,因此玩家不需要控制企鹅的速度。玩家只能通过打开和关闭喷气背包来控制企鹅的垂直移动。

游戏的目标是尽可能远地旅行,收集鱼币,并避开障碍物,如电击器、导弹和高强度激光束。

在下一节中,我们将开始开发活泼的企鹅游戏。我们将学习如何添加玩家,在我们的案例中,玩家是一只背部绑有喷气背包的企鹅。

添加企鹅

在本节中,我们将学习如何将我们的企鹅玩家添加到游戏中,以及我们如何通过编写脚本、应用物理和添加碰撞器来使她变得生动。

导入企鹅精灵

在我们做任何事情之前,我们需要一个玩家精灵或图像。对于我们的活泼的企鹅游戏,我们设计了一个企鹅精灵。以下图显示了包含在游戏玩家精灵中的企鹅:

图 1.35 企鹅精灵

要将图像导入 Unity,请在项目浏览器面板中的图形文件夹上右键单击,然后单击导入新资产...,如图以下截图所示:

图 1.36 在 Unity 中导入新资产

这些资产可以是 Unity 支持的所有内容,例如图像、音频文件、3D 模型、纹理、材质和脚本。

还可以通过将图像文件从资源管理器拖动到 Unity 的项目浏览器面板来导入资产。

Unity 以精美的预览显示了所有图像。在此必须指出,如果 Unity 处于 2D 模式,则 Unity 将图像显示为精灵;如果模式设置为 3D 模式,则显示为纹理。图 1.36显示了这两种情况。以下截图显示了作为精灵导入的图像,旁边的截图显示了作为纹理导入的图像:

图 1.37 作为精灵的图像(左)和作为纹理的图像(右)

如果企鹅图像被导入为纹理,那么就不用担心了。从项目浏览器面板中选择图像,并在检查器中将纹理类型更改为精灵,然后点击应用。如下面的图所示:

图片

图 1.38 在检查器面板中更改图像纹理类型

创建企鹅游戏对象

在资产导入之后,在我们的例子中是一个企鹅图片,我们必须创建一个玩家游戏对象。通常,游戏对象是通过在层级中右键单击并选择创建空对象来创建的,如下面的图所示:

图片

图 1.39 创建空游戏对象

但是,为了创建作为游戏对象的精灵,只需从项目浏览器面板将精灵拖动到层级或场景视图面板,就会创建一个名为图像文件的游戏对象,这里为 penguin_fly.png。如下面的图所示:

图片

图 1.40 创建精灵游戏对象

现在是时候配置层级中的 penguin_fly 游戏对象以用于游戏了。现在选择 penguin_fly 对象,并在检查器面板中进行以下更改。

  1. 将游戏对象的名字更改为 penguin

  2. 将位置值设置为 (0, 0, 0)。

图片

  1. 要在企鹅中添加碰撞器,请在检查器中点击添加组件,并从物理 2D 菜单中选择圆形碰撞器 2D,如下面的图所示。更多关于碰撞器的信息可以在这里阅读 docs.unity3d.com/ScriptReference/Collider.html

图片

图 1.41 添加圆形碰撞器 2D

  1. 将圆形碰撞器 2D 的半径值设置为 0.6。

  2. 要使企鹅表现得像一个物理对象,我们必须添加刚体组件。要添加它,请在检查器面板中点击添加组件,并从物理 2D 中选择刚体 2D,如下面的图所示:

图片

图 1.42 添加刚体 2D

  1. 将固定角度复选框的状态设置为选中,以避免企鹅在下降或跳跃时因物理动力学而旋转。

下面的图显示了在企鹅游戏对象上执行的所有步骤:

图片

图 1.43 企鹅检查器设置

从位置开始,我们为了测试目的将位置设置为 0。当我们把企鹅放在初始位置时,我们会稍后更改它。Sprite Renderer 组件已经添加到企鹅游戏对象中,因为企鹅游戏对象是通过从项目浏览器面板拖动精灵创建的。Sprite Renderer 允许任何游戏对象从任何精灵上显示图像。为了使任何游戏对象能够对物理碰撞做出反应并相互碰撞,游戏对象需要具有碰撞组件。我们在企鹅对象上添加了 Circle Collider 2D。你可以从列表中选择任何类型的碰撞体。但建议选择非常轻且填充整个碰撞区域的碰撞体。以下图显示了应用了不同碰撞体的企鹅:

图片

图 1.44 带有不同碰撞体的企鹅

使用复杂的碰撞体,如 Polygon Collider 2D,会使物理引擎检测碰撞变得更加困难,这反过来又会导致性能损失。最后,为了在企鹅上应用重力,我们添加了 Rigid Body 2D 组件。Rigid Body 2D 允许任何游戏对象对重力、摩擦、物理运动学等做出反应。我们已经将 Rigid Body 2D 的 Fixed Angle 设置为开启状态。如果设置为关闭,那么企鹅也会因为风、摩擦、重力或任何其他作用在其上的力而旋转。因此,为了避免它在跳跃时旋转,我们将 Fixed Angle 设置为勾选状态。

有物理组件和 Physics2D 组件。这两个组件在功能上非常不同,用于非常不同的目的。在将物理组件应用于游戏对象时,你必须非常小心。

当你运行项目时,你会注意到企鹅会从屏幕上掉下来。这是因为重力将企鹅向下拉,使其离开屏幕。默认情况下为 1 的重力比例值决定了企鹅的重力。如果我们移除或禁用 Rigid Body 2D 组件,那么企鹅将永远不会移动。因此,必须注意,没有刚体,就不会对游戏对象应用任何力和碰撞。

在企鹅对象上添加脚本行为

在设置好企鹅对象上的物理组件和位置后,是时候定义一些企鹅的逻辑行为。游戏的要求是当玩家触摸屏幕时,让企鹅通过喷射背包飞行,当触摸停止时,企鹅将落下。这类逻辑是通过 Unity 中的脚本定义的。Unity 支持两种脚本:C#和 JavaScript。你可以使用任何这些脚本。甚至你可以在同一个项目中使用一些 C#文件和一些 JavaScript 文件。在这本书中,我们将通过 C#来完成所有的脚本编写。

  1. 我们将首先在 Assets 文件夹中的 Scripts 文件夹上右键单击,并在创建菜单中选择 C# Script,如图所示来创建一个 C#脚本文件:

图 1.45 添加 C# 脚本

  1. 让我们将这个脚本命名为 PenguinController.cs,当你打开它时,Unity 的默认代码编辑器 Visual Studio 将启动并打开新创建的脚本文件,如图所示。

图 1.46 MonoDevelop 中的 C# 脚本文件

  1. 你可能会注意到这个文件中已经有一些代码。我们稍后会讨论这个问题。现在脚本已经创建,但这个脚本还没有链接或连接到我们的企鹅对象,甚至没有连接到游戏。为了将其应用于企鹅对象,选择企鹅游戏对象,在检查器面板中点击“添加组件”,然后从脚本菜单中选择 PenguinController.cs,如图所示:

图 1.47 在企鹅游戏对象上添加 C# 脚本

你也可以通过直接将脚本文件拖放到层次视图中的游戏对象上来将脚本应用于游戏对象。

现在,由于脚本已应用于企鹅游戏对象,让我们编写一些逻辑和代码来使企鹅飞翔。通过在 MonoDevelop 中双击它从项目浏览器面板打开 PenguinController.cs 文件。已经编写的代码看起来如下截图所示:

  1. PenguinController 类是从 MonoBehaviour 类继承而来的。任何要应用于场景中任何游戏对象的脚本都应该是一个 MonoBehaviour 类,这是通过从它继承类来实现的。MonoBehaviour 类为游戏对象提供了一些基本功能,例如其生命周期,如对象创建时、对象激活时、对象销毁时等。它还提供了一些功能,如交互,例如鼠标按下或鼠标抬起等。目前,我们只有两个方法 Start()Update()Start() 方法在游戏对象在运行时第一次在场景中激活时被调用,如果游戏对象被启用或激活,则 Update() 方法在每个帧上被调用。

你还可以通过安装 Visual Studio Unity Tools 并在项目中导入 UnityVS 打包来使用 Visual Studio 2012 与 Unity 进行 C# 脚本开发。

  1. 现在,让我们开始编写企鹅的逻辑。我们将从在类中创建一个变量 jetpackForce 开始,如下所示:
        public float jetpackForce = 75.0f;

  1. 我们将其初始浮点值设置为 75,f 字符是为了使其成为浮点字面量。关于 Unity 的一个有趣之处在于,任何类中的所有公共字段都会显示在游戏对象的检查器组件中,并且可以直接从编辑器中修改或更改,而无需打开代码文件。以下展示了检查器面板中的 jetpackForce 变量字段:

图 1.48 检查器面板中的 jetpackForce 字段

  1. 接下来是将这个jetpackForce的值用作在触摸按下时施加在企鹅上的力。为了检测触摸,我们必须使用Input.GetButton()方法。但是,这个方法最好用在MonoBehaviour类的Update()FixedUpdate()方法中。FixedUpdate()方法在每个固定帧被调用,而不是每个帧。这意味着如果游戏的 FPS 是 60,那么在 FixedUpdate 中,每秒这个函数将被调用 60 次。无论场景是否有变化。相比之下,Regular Update ()函数不会遵循 60fps 规则,并且会在场景有变化时更新。这应该用于处理 RigidBody。由于我们的企鹅是 RigidBody2D,所以使用FixedUpdate()方法比Update()方法更好。PenguinController.cs脚本不包含FixedUpdate()方法,所以现在让我们在这个类中添加以下代码:
        void FixedUpdate()
        {
            bool jetpackkActive = Input.GetButton ("Fire1);
            if (jetpackActive = true) {
                this.GetComponent<Rigidbody>().AddForce(new Vector2(0, 
                jetpackForce));
            }
        }

  1. 这里没有什么困难。我们正在轮询 Fire1 按钮的输入,在 PC、Linux 或 MAC 构建中,这是一个左键点击,在 Android、iPhone 或其他触摸设备上则变为触摸操作。如果屏幕被触摸,那么GetButton()将返回 true 值,这反过来又允许脚本通过调用 RigidBody2D 组件的AddForce()方法,并传递jetpackForce值在 y 方向上施加力。

下面是整个PenguinController.cs文件的代码:

  1. 现在,当你运行游戏时,企鹅将开始下落。在点击屏幕后,企鹅会稍微上升然后再开始下落。点击越频繁,企鹅上升得越高,它很容易在 2 或 3 次点击后离开屏幕。而且它下落得非常快。为了调整其速度,我们可以通过以下两种方式之一:要么从企鹅对象的 RigidBody2D 中减少重力比例,如图中左侧所示,要么在图中右侧的 Physics2D 设置中减少重力。

图 1.49 企鹅的重力比例(左侧)和 Physics2D 的重力(右侧)

我们将把 Physics2D 设置的重力值改为-15。你可以从 Edit - Project Settings - Physics2D 菜单中打开 Physics2D 设置。

现在运行游戏,你会注意到企鹅的下落速度比之前慢。在多次点击后,你可能会注意到企鹅离开了屏幕。在游戏中应该避免这种行为。因此,为了限制企鹅在屏幕边界内,我们将在游戏中添加地板和天花板。让我们看看在下一节中是如何实现的。

限制企鹅在屏幕边界内

添加地板和天花板是一个相对简单的工作。我们首先创建一个空对象。需要注意的是,我们在这个场景中并没有导入任何图像或精灵资源,因为我们只需要边界而不是这些资源的视觉表现。以下是创建我们的地板对象的方法。

  • 通过选择 GameObject - 创建空对象菜单来创建一个空游戏对象。

  • 在层次面板中选择新创建的空游戏对象。

  • 将其重命名为floor

  • 将其位置设置为(0, -3.25, 0)。

  • 将其缩放设置为(14.4, 1, 1)。

  • 通过点击添加组件,并选择 Physics2D - Box Collider 2D 选项来添加 Box Collider 2D 组件。

现在你应该在屏幕底部看到一个绿色的矩形。这是地板的盒子碰撞体,当你运行游戏时,企鹅永远不会掉出屏幕,并且当与地板碰撞时会停止。

我们没有在地板对象上添加 Rigid Body 2D,因为我们不希望由于地板对象的静态特性而对其应用重力。

类似地,现在添加一个名为ceiling的顶棚游戏对象,其位置为(0, 3.25, 0),其缩放为(14.4, 1, 1)。将 Rigid Body 2D 组件应用到顶棚对象上,现在运行项目。你会观察到企鹅现在永远不会离开屏幕,并且它被限制在屏幕的上下部分之间。

摘要

在本章中,我们学习了什么是 Android 以及它的不同版本。我们还学习了用于为 Android 设备创建游戏的多种游戏引擎,例如 Unity3D、Unreal Engine、Game Maker Studio 或 Adobe Flash。我们还学习了 Unity 的重要特性及其开发环境的基本知识。在学习了 Unity 游戏开发的基础之后,我们学习了为 2D 游戏配置任何空游戏项目的方法。然后,我们介绍了 Perky Penguin 游戏,该游戏在本章开始开发。在这一章中,添加了一只企鹅,并创建了其基本的飞行和下落功能。

在下一章中,我们将完成 Perky Penguin 游戏,并了解如何在 Unity 中开发粒子系统、动画、敌人等等。

第二章:完成 Perky Penguin 2D 游戏

上一章主要介绍了对 Unity 和游戏开发感兴趣的新手开发者。由于本书专注于 Android 平台的游戏开发,因此上一章,也就是本书的第一章,介绍了 Android 平台、其不同版本以及其市场,即 Google Play。然后本章回顾了游戏引擎的概念,并通过与其他主要游戏引擎(如 Unreal、Adobe Flash 和 Game Maker Studio)的比较,突出了 Unity 3D。本章通过介绍 Unity 游戏开发的基础知识,如 Unity 的界面、其面板(如检查器面板、层次结构面板等),以及场景视图和游戏视图的概念,以及它们如何相互作用以及这些视图如何帮助开发者使创建游戏变得简单而有趣。

本章包括以下主题:

  • 向游戏中添加粒子系统

  • 摄像机管理

  • 预制体和关卡管理

  • 游戏中的激光和敌人

在为 Android 平台的游戏开发提供了足够的 Unity 游戏开发理论方面的信息之后,上一章从配置空游戏项目开始,这些项目对于 2D 游戏特别有用。配置空项目是任何游戏的第一个步骤,我们在第一章,使用 Unity 3D 介绍 Android 游戏开发中,以非常实用的方式进行了介绍,并且我们还介绍了一个名为Perky Penguin的 2D 游戏。以下是从上一章中截取的游戏截图:

图 2.1 Perky Penguin 游戏

我们学习了如何在 Unity 中添加玩家,例如企鹅,以及如何将碰撞体和物理效果应用于企鹅。Unity 还支持使用 C#或 JavaScript 语言进行脚本编写和编程,以定义自定义行为,例如让企鹅跳跃、在飞行时避免重力、在游戏过程中阻止企鹅离开屏幕等等。本章以一个包含跳跃企鹅的 Perky Penguin 游戏结束。

如果你已经熟悉 Unity 的 2D 概念或者已经创建了 2D 游戏,那么你可以跳过这一章,直接进入第三维度的学习。

在本章中,我们将通过学习 Unity 2D 的一些其他高级概念来完成 Penguin Perky 游戏,例如添加粒子效果、相机管理、创建关卡、使用 Unity 4.x 版本中引入的动画和控制器等。到目前为止,Perky Penguin 游戏有一个可以飞和在屏幕上跳跃的可爱企鹅,但它不能向前行走,也不能探索游戏世界。在这里需要注意的是企鹅是如何飞行的,而在现实生活中,企鹅是不会飞的。在我们的游戏中,企鹅得到了一个红色火箭,它给企鹅提供了短暂的火焰助推,使其能够飞行。在游戏中,这个助推是通过在 Android 设备的屏幕上轻触一次来实现的。

在下一节中,我们将开始制作我们的游戏,并通过添加粒子效果使游戏栩栩如生。

添加粒子系统

在我们深入探讨粒子系统的细节之前,让我们讨论一下这些系统究竟是什么以及它们是如何被使用的。让我们从讨论粒子系统是什么开始。

什么是粒子系统?

在任何 3D 复杂游戏中,角色、道具和环境元素大多以 3D 网格和模型的形式创建,而在 2D 游戏中,则使用精灵和图像来达到相同的目的。现在,无论这些对象是网格还是精灵,它们大多代表具有明确形状的实体。但任何游戏都包含其他实体,如流体、液体、烟雾、云、火焰、魔法球等。这些都是某种特殊类型的动画和对象,并且使用特殊类型的属性和行为来处理,在 Unity 中称为粒子系统。

下面的截图展示了在 Unity 中创建的一些有趣的魔法法术粒子系统:

图 2.2:在 Unity 中创建的不同粒子效果或魔法法术

粒子是小型且简单的图像,甚至是网格,它们通过一个称为粒子系统的完整系统以大量显示和动画化。在任何粒子系统中,每个小粒子都扮演着微小的角色,整体上看起来像一些高度抛光的动画或效果。例如,下雪效果可以被视为一个粒子系统。单个雪花粒子如果单独动画化,不代表任何下雪,但如果这些单个雪花粒子以大量、随机速度、随机方向和随机大小进行动画化,所有这些粒子看起来就不会像某种图像动画;相反,它们将看起来像游戏中下雪的系统。这就是在 Unity 中使用粒子系统的关键优势——它允许以非常轻量和优化的方式批量操作重代码,从而产生非常美丽的游戏效果。

粒子系统的基础

在 Unity 中,粒子系统由所有粒子组成。开发者只需要管理粒子,其余的将由 Unity 处理。每个粒子都有预定的生命周期,通过各种变化,例如,任何雪落效果中的渐隐,或者烟雾效果中的放大和渐隐等。像所有其他物理游戏对象一样,这些粒子具有速度,可以在其生命周期内改变速度和方向,并且这些粒子可以受到环境物理动力学施加的力和重力的影响。

开发者的目标是管理和控制粒子的生命周期及其行为,例如,一个粒子会持续多少秒?它在其生命周期内会如何增长?它会放大还是不会?它在其生命周期内会逐渐消失吗?所有这些问题都由开发者回答,Unity 将授予他们一个以效果形式出现的令人惊叹的粒子系统。

粒子可以是简单的白色球体,也可以是具有高分辨率纹理和法线图的任何网格。

粒子系统的工作是从一个抽象的角度管理所有这些粒子。这个系统会告诉何时生成下一个粒子以及它应该在什么位置,以什么位置、旋转和缩放。例如,系统的发射形状应该是半球形、圆锥形还是简单的盒子?粒子的发射速率应该是多少,以及任何粒子效果应该持续多长时间来完成一个完整的循环?

为游戏创建火箭火焰粒子效果

因此,在掌握了 Unity 中粒子效果和粒子系统的一些基本知识后,让我们通过为我们的 Perky Penguin 游戏创建第一个粒子系统来将这些知识付诸实践。正如我们所知,企鹅背着一个火箭,玩家在屏幕上点击时,它可以让企鹅飞行并加速。现在,这个火箭在激活时将产生一个类似火焰的小效果。以下屏幕截图显示了火箭及其火焰效果作为这个小粒子系统练习的最终结果:

图片

图 2.3:火箭火焰效果最终结果

如前一个屏幕截图所示,你可以观察到火焰是如何从火箭边缘开始,并且如何以渐隐效果缓慢消失,这与现实生活相似。现在,让我们在我们的上一章创建的PerkyPenguin_PenguinMovement项目中创建这个简单的粒子效果,该项目来自第一章,《使用 Unity 3D 的 Android 游戏开发入门》。

让我们从选择 GameObject | 粒子系统开始创建一个粒子系统对象,如图下所示:

图片

图 2.4 创建粒子系统 GameObject

这将在层次结构面板中立即添加一个名为粒子系统的游戏对象。但你可能在这里注意到一个奇怪的现象,即对象创建并选择后,场景视图中白色球体向外移动。下面的截图显示了场景视图中这些白色球体的简单示例:

截图

图 2.5 在 Unity 中选中的粒子系统 GameObject

你可以看到,当我们创建了一个粒子系统对象后,一个简单的白色球体粒子系统开始在场景中播放。场景中显示了一个小型控制面板,包括暂停、停止、播放速度和播放时间等控件。当你选择任何其他游戏对象或取消选择粒子系统游戏对象时,粒子系统会自动停止。

Unity 界面的一项特性是,开发者无需玩游戏就可以测试他们的粒子系统。这些可以直接在场景视图中进行检查。

此外,你应该注意所选粒子系统的检查器面板,并查看对象中的粒子系统组件如何显示许多不同的属性,如下面的截图所示:

截图

图 2.6 检查器面板中的粒子系统组件

值得注意的是,Unity 的粒子系统几乎所有的内容都包含在这个粒子系统组件中,这是一个包含大量属性的集合。通过调整和更改这些属性的值,可以将下雪转换为燃烧的火焰,或者将其转换为任何飞机游戏的爆炸效果,或者将其转换为任何地牢角色扮演游戏的魔法球或魔法咒语,等等。要了解这些,让我们继续通过以下步骤学习火箭火焰粒子系统:

  • 为了使粒子系统始终位于火箭下方,它应该是企鹅对象的子对象。因此,将粒子系统对象拖动到层次结构面板中的企鹅游戏对象上,你将看到如下截图所示的内容:

截图

图 2.7 粒子系统 GameObject 作为企鹅对象的子对象

  • 将粒子系统重命名为 rocketFire。

  • 将其位置设置为(-0.62, -0.33, 0)以将其移动到火箭的喷嘴处。

  • 将其旋转设置为(65, 270, 270)以设置粒子在火箭方向上的方向。

以下截图突出了在检查器面板上述步骤中提供的所有更改:

截图

图 2.8 检查器面板中的 rocketFire 变换

仍然在检查器面板中,选择了 rocketFire 粒子系统对象,让我们更改粒子系统组件的值,为我们的企鹅火箭创建一种美丽的火焰效果。以下是需要遵循的步骤:

  • 设置起始生命周期为 0.5

  • 设置起始大小为 0.3

  • 点击“起始颜色”,将红色设置为 255,绿色设置为 135,蓝色设置为 40,Alpha 设置为 255,这将使我们的白色粒子变为橙色

  • 展开发射部分,将速率设置为 300

  • 展开形状部分,将形状设置为圆锥形,角度设置为 12,半径设置为 0.1

  • 将“随机方向”复选框设置为选中状态,以在火焰粒子中创建随机性

以下截图显示了粒子系统组件之前设置的设置,以及场景视图中企鹅下面的火箭火焰效果:

图 2.9 火箭火焰粒子系统

您可以观察到随机生成的白色球体以随机方向移动,并从企鹅背部绑定的火箭中变成了锥形火焰发射。我们所做的是在检查器面板中更改粒子系统组件的一些值。让我们简要看看这些属性实际上做什么:

  • 开始寿命是发射时粒子的总寿命(以秒为单位)

  • 开始大小是发射时粒子的初始大小

  • 开始颜色是发射时粒子的初始颜色

  • 发射率是每单位时间或移动距离发射的粒子数量

  • 形状是发射体积的形状:选项有球体半球体圆锥体盒子网格圆形边缘

  • 角度是圆锥在其顶点的角度(仅适用于圆锥)

  • 半径是形状圆形部分的半径

  • 随机方向启用意味着粒子的初始方向将被随机选择

您可以在 Unity 的官方手册或文档网站上找到更多这些属性及其用途的信息:docs.unity3d.com/Manual/ParticleSystemModules.html

因此,我们的火箭火焰粒子系统已经准备好了。但在现实生活中,火焰永远不会瞬间结束,它总是随着时间的推移逐渐消失。我们也可以通过在粒子系统组件中启用“开始颜色”属性来添加这个特性。然后点击颜色框,选择右侧顶部的滑块,这是用于结束颜色的 alpha 值,并将其设置为 0。

整个设置在以下截图中显示:

图 2.10 使用“颜色随时间变化”在火焰中添加渐变效果

如果您不确定“颜色随时间变化”在火焰效果中具体改变了什么,以下截图显示了带有和没有渐变效果的火焰粒子系统的差异:

图 2.11 带有和没有渐变效果的火焰效果

到目前为止,我们有一个企鹅在屏幕上轻触时飞行的效果,它的火箭一直在爆炸出火焰。现在,是时候在游戏中添加一些环境来营造游戏的主题了。这个环境将让企鹅能够通过,而我们的游戏需要一个无尽的房间作为背景。但是,为了测试企鹅的行走,我们只会添加背景,而不添加任何无限移动的功能。

添加游戏级别的背景

没有背景或主题的游戏根本不是游戏。到目前为止,企鹅没有理由在游戏中行走、飞翔或使用它的火箭助推,因为它处于蓝色的虚空空间中。让我们把这个企鹅带到它真正属于的雪地和山丘世界中。为了生存全球变暖导致的融冰,它也可以使用它的火箭稍微飞一下。在本节中,我们将学习如何添加背景,以及在任何不使用 z-顺序的 2D 游戏中如何管理这些背景和精灵的顺序。

我们将首先为游戏的不同级别创建背景。这些背景可以用您喜欢的图形工具创建,例如 Adobe Photoshop 等。以下截图显示了为活泼企鹅游戏创建的背景:

图片

图 2.12 鹦鹉螺活泼游戏的背景

我们已经学习了如何在 Unity 中将图像作为精灵导入。对于背景,我们必须再次执行完全相同的程序。我们已经准备了两个名为 bg_snow1.pngbg_snow2.png 的可重复图像,并将这些图像导入到项目浏览器面板中资产目录的图形文件夹中。以下截图显示了包含背景图像的项目浏览器面板:

图片

图 2.13 项目浏览器面板中的背景

必须注意的是,背景应该是可重复的,这样当这些图片放在一起时,它们应该合并并避免玩家识别出重复的顺序。以下截图显示了为鹦鹉螺活泼游戏设计的两个背景图片:

图片

图 2.14 鹦鹉螺活泼游戏的背景图

您可以从图片中看到,如果我们将这些两张图片并排重复,将创建一个非常平滑的背景,它将永远重复。我们将在本章后面通过一些脚本实现这一点。现在,让我们设置游戏场景的背景。

在将 Assets 文件夹中的图形背景图像导入后,在层次结构面板中放置两个 bg_snow1.png 对象和一个 bg_snow2.png 对象。将第一个 bg_snow1.png 的位置设置为 (0, 0, 0),然后 bg_snow2.png 的位置设置为 (4.8, 0, 0),最后另一个 bg_snow1.png 的位置设置为 (9.6, 0, 0),你将在场景中得到一个小背景。因此,现在你会观察到企鹅隐藏在这些背景之后。由于这是一个 2D 游戏,所以这里没有 z 轴的概念。我们可以设置 z 轴位置并前后移动精灵和图像,但对于 2D 游戏来说,这不是一个好的方法。Unity 为开发者提供了一个针对 2D 游戏优化的 2D 图像前后排序机制。这是通过排序图层来实现的。在层次结构面板中选择 bg_snow1 对象,并查看精灵渲染器组件中的检查器,你将看到一个排序图层选项,如下面的截图所示:

图片

图 2.15 检查器面板中的精灵渲染器排序图层

初始时,您在 Unity 中导入的所有精灵都有一个默认排序图层。点击下拉菜单,你将看到项目中所有排序图层的列表。目前,我们只有一个名为默认的图层,还有一个添加排序图层...选项来添加新图层。点击添加排序图层...选项,如下面的截图所示:

图片

图 2.16 添加排序图层...选项

这将在检查器面板的位置打开一个新的标签和图层面板。按照解释的顺序添加四个图层,即背景、周围环境、对象和玩家。如下面的截图所示:

图片

图 2.17 排序图层面板

您可以通过按住并拖动每个图层行左侧的两个线条图标来上下拖动这些图层。上下移动这些图层描述了图层的顺序。例如,当前默认图层是最低的图层,它将位于所有内容之后,而播放器图层将位于除播放器图层之外使用图层的所有内容之前。

排序图层和图层之间有很大的区别。排序图层仅用于排列 2D 游戏中的 2D 精灵,而图层不是用于排列目的。这是一个完全不同的概念,你必须在使用图层时小心,以避免图层和排序图层之间发生任何冲突。

目前,我们只需要玩家和背景排序图层;我们将在本章的后面使用其他图层。现在,在层次结构面板中选择企鹅游戏对象,并在精灵渲染器的排序图层选项中选择玩家图层,如下面的截图所示:

图片

图 2.18 选择企鹅的排序图层

同样,选择场景中所有雪背景对象的背景层。现在你会观察到企鹅将位于这些雪背景之前。

摄像机管理

因此,现在我们有一个游戏,其中有一个可以飞行和跳跃的企鹅,带有火箭喷射粒子系统效果。此外,场景中还有一个基本的雪背景环境。在本节中,我们将了解如何使企鹅在无限生成的水平面上移动,以及摄像机将始终聚焦在企鹅上。

使企鹅向前移动

让我们先让我们的企鹅向前移动。为此,打开在第一章,“使用 Unity3D 开发 Android 游戏入门”中创建的文件PenguinController.cs。此文件位于 Assets 目录的 Scripts 文件夹中。在类中添加一个公共字段前进速度,如下面的代码所示:

    public float forwardMovementSpeed = 3.0f;

请记住,添加任何公共字段都会在脚本的游戏对象中添加一个属性,如下面截图中的 Inspector 面板所示:

图片 2.20

图 2.19 Inspector 面板中的前进移动速度字段

现在,为了以任何速度移动企鹅,我们必须在Update()FixedUpdate()方法中更新企鹅的速度。由于我们正在使用FixedUpdate()方法来利用企鹅的物理行为,所以将此代码添加到FixedUpdate()方法的末尾:

图片 2.21

好吧,这段代码中没有太多新内容。由于我们通过喷气背包力量更新了企鹅的 y 位置,所以我们只更新了企鹅对象的 x 轴。看看我们是如何在每一帧上分配forwardMovementSpeed到企鹅游戏对象的速率。当你运行项目时,企鹅将开始向右移动,一分钟后,它将离开屏幕。我们的下一个任务是使摄像机跟随企鹅,这样企鹅就可以始终保持在屏幕上,永远不会超出摄像机的边界。

使摄像机跟随企鹅

企鹅游戏对象中的对象,如下面截图所示:

我们有很多方法可以使摄像机跟随企鹅。最简单的是将摄像机对象作为企鹅游戏对象的子对象,如下面截图所示:

图片 2.22

图 2.21 企鹅对象中的摄像机作为子对象

嗯,你可以这样做,但这个解决方案有一个问题。当企鹅跳跃或飞行时,摄像机也会上下移动。我们不需要这个在我们的游戏中。我们只需要摄像机水平移动,并持续向前移动,保持企鹅在边界内。所以,我们将通过在资产目录的脚本文件夹中创建另一个名为CameraFollow.cs的 C#脚本文件来实现这一点。现在,添加一个名为targetObject的公共GameObject字段。这个字段将告诉摄像机要跟随哪个对象。我们将在这个字段中保留企鹅游戏对象的引用。最后,为了使摄像机持续向前移动,我们将编写类似于企鹅向前移动的代码。这里唯一的区别是我们将代码放在Update()方法中而不是FixedUpdate()方法中。这样做的原因是FixedUpdate()应该在对象具有刚体和/或其他物理行为时使用。由于我们的摄像机与物理没有连接,我们可以通过在Update()方法中添加移动代码来实现这一点。以下是CameraFollow.cs文件的完整代码:

编写代码后,让我们将这个脚本放在摄像机对象上。在层次结构面板中选择主摄像机对象,然后在检查器中点击添加组件按钮,选择脚本 - CameraFollow.cs,如图以下截图所示:

图 2.22 在摄像机上添加 CameraFollow.cs

你也可以通过简单地从项目浏览器面板中将脚本文件拖动到层次结构面板中的游戏对象上来对游戏对象应用脚本。

当你运行项目时,尽管摄像机在移动,你会在调试日志面板中看到一个错误,如图以下截图所示:

图 2.23 调试日志中的摄像机错误

问题在于CameraFollow脚本中的变量targetObject没有被分配任何值,我们在Update()方法中访问它。回想一下,我们需要在targetObject值中分配企鹅游戏对象,以便告诉摄像机要跟随的目标。你可以使用 Unity 的公共检查器功能,通过将企鹅对象直接拖动到检查器面板中的摄像机targetObject字段来分配企鹅对象,如图以下截图所示:

图 2.24 在目标对象中分配企鹅对象

现在运行游戏,你会看到摄像机永远不会让企鹅离开屏幕。然而,移动一段时间后,企鹅会开始下落,因为它的地板已经被留下,企鹅没有物理支撑来抵抗重力。为了解决这个问题并继续游戏,我们将在下一节中创建无限关卡。

预制件和关卡管理

没有一款游戏不包含至少一个级别。级别是让游戏感觉像是一种体验和冒险的原因。级别带有不同类型的难度,玩家开始感受到完成某事的成就感和奖励。因此,为了给我们的企鹅活泼游戏注入灵魂,让我们在我们的游戏中添加级别。但这里出现的问题是我们应该添加什么类型的级别到游戏中。由于游戏是无尽的,因此添加带有随机障碍和敌人的自动生成级别将非常合适。为了做到这一点,我们需要一个脚本,该脚本将负责生成随机环境和房间,并放置敌人和障碍物以平衡游戏难度。

Unity 为开发者提供了在运行时实例化游戏对象并在其中定义行为的机会,这些行为也可以在运行时定义。我们可以在运行时单独添加房间、级别、金币、敌人等等,但 Unity 提供了一个更好、更有组织的处理这些类型情况的方法来管理运行时可重复使用的元素。这是通过预制件对象实现的。让我们在下一节中讨论预制件。

预制件

根据 Unity 的文档,预制件是一种资产类型——一个存储在项目视图中的可重复使用的GameObject。预制件可以被插入到任意数量的场景中,每个场景可以多次插入。当你将预制件添加到场景中时,你创建了一个它的实例。所有预制件实例都与原始预制件链接,并且本质上都是它的克隆。无论你的项目中存在多少个实例,当你对预制件进行任何更改时,你将看到更改应用于所有实例。

以下截图显示了项目浏览器视图中的空预制件:

图片

图 2.25 项目浏览器面板中的空 BoxPrefab

你会注意到在我们的资产目录中有一个名为“预制件”的文件夹。我们在配置项目时创建了此文件夹。我们将把游戏中使用的所有预制件存储在这个目录中。

预制件与游戏对象完全一样。它们可以有碰撞器、刚体、脚本、其他组件等等。预制件和游戏对象之间的唯一区别是预制件可以在运行时实例化并在运行时复制相同的行为,但游戏对象可以在运行时生成。

为了创建一个无尽级别,我们必须为我们的游戏创建一个小块级别的集合。这个块将在企鹅不断通过级别时在运行时不断地重复生成。让我们在下一节中创建这个小级别块,看看这个预制件理论是如何付诸实践的。

创建级别块预制件

级别块预制件将包含一个地面物体和一个天花板物体,以防止企鹅从屏幕边界外跑出去,并且它将有一个背景环境用于装饰和主题目的。

让我们从在层次结构中右键单击层次结构视图并单击创建空对象开始,如图以下截图所示:

图 2.26 创建空 GameObject

现在,将所有必要的游戏对象作为此空游戏对象的子对象放入预制件中。目前,我们正在添加snow_bg1snow_bg2背景,floor对象和ceiling对象。我们可以在任何时候更新它并添加新的对象和组件到预制件中。但您必须记住,这将更改所有现有的预制件对象,因此在更新预制件时必须小心。

所有这些对象都可以拖动到创建的空游戏对象上,使其成为子对象,然后将空游戏对象重命名为level_block。以下截图显示了level_block对象的游戏对象层次结构:

图 2.27 level_block 游戏对象

如果您用更多对象、装饰、环境等装饰了层级块,那么您也应该将它们添加到层级块空游戏对象中。

我们已经创建了一个层级块游戏对象。现在,是时候将其转换为预制件形式了。为此,在资产目录中的预制件文件夹上右键单击,并从菜单中选择创建 - 预制件,如图以下截图所示:

图 2.28 创建预制件

它将在预制件文件夹中创建一个空预制件。现在,将层级块游戏对象从层次结构视图拖动到创建的空预制件上,您的预制件就准备好了。以下截图显示了这一过程:

图 2.29 向预制件添加对象

预制件创建后,将在项目浏览器面板中显示游戏对象内容的小预览。您还会注意到其中有一个小箭头,它将探索放入预制件中的所有对象。以下截图显示了项目浏览器面板中层级块预制件的预览:

图 2.30 项目浏览器面板中的预制件

预制件可以在任何场景中使用。这些可以在整个项目中任何地方重复使用。

为了查看预制件在运行时的表现,只需将我们的层级块预制件对象拖入场景几次,在不同的位置,看看它在视图中是如何出现的。以下截图显示了几个放置在场景中的层级块:

图 2.31 随机放置在场景中的层级块预制件

您可以观察到,无论何时我们将任何预制件拖入场景,它都会创建一个包含所有功能、子对象、行为、脚本等的整个块。这就是在 Unity 中使用预制件的主要优势。

我们的关卡方块预制件已经准备好了。现在我们必须告诉 Unity 在何时以及在什么位置创建方块。我们将通过将一个名为 BlockGenerator.cs 的脚本添加到企鹅对象中来实现这一点。让我们在下一节中解释这个生成器。

水平方块生成器概念

生成器脚本背后的理念非常简单。脚本有一个可以生成的关卡方块数组(目前我们只创建了一个方块),一个当前已生成的方块列表,以及两个额外的方法。一个方法检查是否需要添加另一个方块,另一个方法实际上添加一个方块。

为了检查是否需要添加方块,脚本将枚举所有现有房间并查看是否存在一个比屏幕宽度更远的房间,以确保玩家永远不会看到关卡结束。为了更清楚地理解这一点,让我们看看下面的截图:

图片

图 2.32 关卡方块生成器想法

您可以在场景 1 中看到,还有足够的空间或宽度来覆盖方块的一些距离。但在场景 2 中,空间不足,因为随着企鹅向前移动,空隙将开始出现。所以在我们看到任何空白空间之前,我们需要在当前方块旁边添加一个新的关卡方块,以确保玩家永远不会在那里看到任何空隙。该图仅试图阐明新方块将如何添加以及何时添加。以下截图显示了方块的一个示例生成和我们的企鹅前进状态:

图片

图 2.33 企鹅前进和关卡方块生成

您可以看到,随着企鹅不断前进,新的方块会不断地生成。一旦任何方块离开屏幕,它将被删除以优化内存使用并提高游戏性能。

现在让我们通过在下一节中编写 BlockGenerator.cs 代码来将整个场景付诸实施。

BlockGenerator.cs 代码

让我们从在资产目录的脚本文件夹中创建一个名为 BlockGenerator.cs 的空 C# 脚本文件开始。然后将此脚本拖放到企鹅对象上以应用于企鹅。

任何游戏对象都可以应用多个脚本。对游戏对象上应用的脚本或组件没有限制。

通过在项目视图或检查器面板中双击 BlockGenerator.csMonoDevelop 中打开它。

如果您打算使用 List<T> 类,则必须添加 System.Collections.Generic 命名空间。

将以下字段变量添加到 BlockGenerator 类中:

    public GameObject[] availableBlocks;
    public List<GameObject> currentBlocks;
    private float screenWidthInPoints;

availableBlocks将包含脚本可以生成的 Prefab 数组。目前,我们只有一个 Prefab(LevelBlockPrefab)。但我们可以创建许多不同的方块类型并将它们全部添加到这个数组中,以便脚本可以随机选择要生成的方块类型。currentBlocks列表将存储实例化的方块,以便它可以检查最后一个方块在哪里结束以及是否需要添加更多方块。一旦方块在玩家角色后面,它也会将其移除。screenWidthInPoints变量只是用于缓存屏幕大小的点。

你可以在企鹅游戏对象的检查器视图中看到这些字段,如下面的截图所示:

图片

图 2.34 检查器中的方块生成器字段

现在,在BlockGenerator.cs文件的Start()方法中添加以下代码:

图片

在这里,你计算屏幕的大小(以点为单位)。屏幕大小将用于确定是否需要生成新的方块,如前所述。

将以下AddBlock()方法添加到BlockGenerator.cs

图片

此方法使用farthestBlockEndX点添加一个新的方块,这是到目前为止关卡的最右端点。以下是此方法每一行的描述:

  • 从方块类型(Prefab)的随机索引中选择。

  • 使用上面提到的随机索引从可用方块数组中创建一个方块对象。

  • 由于方块是空的,包含所有方块部分,你不能简单地取其大小。相反,你得到方块内部地板的大小,这等于方块宽度。

  • 当你设置方块位置时,你设置其中心的位置,因此将方块宽度的一半加到关卡结束的位置上。这样可以得到你应该添加方块的位置,以便它紧接在最后一个方块之后开始。

  • 这设置了方块的位置。你只需要更改 x 坐标,因为所有方块都具有相同的 y 坐标和等于零的 z 坐标。

  • 最后,你将方块添加到当前方块列表中。它将在下一个方法中被清除,这就是为什么你需要维护这个列表的原因。

AddBlock()方法之后,让我们深入了解GenerateBlockIfRequired()方法的细节:

图片

此方法是前一小节中解释的想法的实现:

  1. 创建一个新的列表来存储需要移除的方块。由于在迭代列表时不能从中移除项目,因此需要单独的列表。

  2. 这是一个标志,表示你是否需要添加更多方块。默认情况下,它被设置为 true,但大多数情况下它将在foreach内部设置为 false。

  3. 保存玩家位置。

  4. 这是方块应该被移除的点之后的位置。如果一个方块的位置在这个点(左侧)之后,它需要被移除。

  5. 如果在addBlockX点之后没有方块,你需要添加一个方块,因为关卡结束的位置比屏幕宽度更近。

  6. farthestBlockEndX中,你存储当前关卡结束的点。如果你需要添加一个新方块,你会使用这个变量,因为新方块应该从那个点开始,以使关卡无缝。

  7. foreach中,你只需枚举当前方块。你使用地板来获取方块宽度并计算BlockStartX(方块开始的位置,方块的最左点)和BlockEndX(方块结束的位置,方块的最右点)。

  8. 如果有一个方块在addBlockX之后开始,那么你现在不需要添加方块。然而,这里没有break指令,因为你仍然需要检查这个方块是否需要被移除。

  9. 如果一个方块在removeBlockX点的左侧结束,那么它已经不在屏幕上了,需要被移除。

  10. 在这里,你只需找到关卡的最右点。这将是目前关卡结束的点。它仅在需要添加方块时使用。

  11. 这会移除标记为移除的方块。鼠标GameObject已经飞过了它们,因此它们在后面很远,所以你需要移除它们。

  12. 如果此时addBlocks仍然是true,那么关卡结束就快了。如果它没有找到比屏幕宽度更远的方块开始,addBlocks将是true。这表明需要添加一个新的方块。

因此,在所有这些解释之后,让我们将我们的最终方法FixedUpdate()添加到BlockGenerator.cs文件中,如下所示:

    void FixedUpdate() 
    {
        GenerateBlockIfRequired(); 
    }

FixedUpdate()中生成方块将继续定期确保玩家在游戏中不会遇到空白空间。现在,回到 Unity,在层级中选择penguin GameObject。在检查器中,找到BlockGenerator组件。将层级中的 LevelBlockPrefab 拖到当前方块列表中。然后打开项目浏览器中的 Prefab 文件夹,将 LevelBlockPrefab 从其中拖到可用方块中。以下截图显示了添加 Prefab 到列表后企鹅对象的BlockGenerator组件:

图片

图 2.35 带有 Prefab 的方块生成器组件

现在运行项目,你会看到方块会持续生成。注意,当你飞行时,方块会在层级中不断出现和消失。为了增加更多乐趣,运行场景并切换到场景视图而不停止游戏。这样,你将看到方块是如何实时添加和移除的。

因此,在关卡生成后,让我们在下一节讨论如何在游戏中添加障碍物,如冰刺,使企鹅在通过关卡时更加小心。

添加冰刺到游戏中

飞越关卡的小企鹅看起来很棒,但游戏的核心是挑战和障碍。因此,本节将详细介绍可以添加到游戏中的障碍。我们将添加冰尖刺,这些尖刺将以与生成方块类似的方式随机生成。让我们首先创建冰尖刺。你需要为尖刺的开启和关闭状态准备两张图片。

以下截图显示了尖刺的开启和关闭状态:

图片 2.43

图 2.36 检查器面板中的方块生成器字段

将这些图片以 spike_on.pngspike_off.png 的名称导入 Unity,存放在 Assets 目录的 Graphics 文件夹中。然后我们必须为尖刺创建一个 Prefab。以下是步骤:

  1. 在项目视图中,找到 spike_on 精灵,并将其拖到场景中。

  2. 在层次结构中选择它,并将其重命名为 spike

  3. 将其排序层设置为 Objects.

  4. 添加一个 Box Collider 2D 组件。

  5. 在 Box Collider 2D 组件中启用 Is Trigger 属性。

当启用 Is Trigger 属性时,碰撞器将触发碰撞事件,但将被物理引擎忽略。

  1. 设置碰撞器的尺寸,X0.18Y3.1

  2. 在 Scripts 文件夹中创建一个新的 C# 脚本,命名为 SpikeScript.cs,并将其附加到 spike 游戏对象上。

以下截图显示了创建尖刺 GameObject 的所有步骤:

图片 2.44

图 2.37 添加尖刺游戏对象

现在打开 SpikeScript.cs 文件,并在类中添加以下字段:

图片 2.45

然后在 Start() 方法中添加以下代码:

图片 2.46

这将设置尖刺第一次切换状态的时间。然后,为了切换和旋转尖刺,添加 FixedUpdate() 并如下操作:

图片 2.47

现在,在层次结构中选择 spike。从项目浏览器中将 spike_on 精灵拖到检查器视图中 SpikeScript 组件的 Spike On Sprite 属性。同样,也为 spike_off 精灵做同样操作。将旋转速度设置为 30,位置设置为 (2, 0.25, 0)。运行项目,你将得到一个旋转的尖刺,最终将尖刺游戏对象转换为 spikes 文件夹中的 SpikePrefab,就像我们在前面的章节中所做的那样。

以下截图显示了游戏中运行的尖刺:

图片 2.48

图 2.38 游戏中的尖刺

摘要

在本章中,我们继续开发我们的“活泼企鹅”游戏,并向其中添加了粒子系统。然后我们学习了如何管理摄像机,并在整个游戏中使摄像机跟随企鹅。然后我们处理 Prefab,并创建了一个关卡方块 Prefab,我们在游戏中通过代码生成,以制作一个无限关卡生成游戏。然后我们创建了一个尖刺 Prefab,为游戏中的企鹅创建一个随机生成并具有不同运行时速度旋转的障碍。

在下一章中,我们将把制作游戏的概念扩展到 3D,并介绍如何在 Unity 3D 中创建 3D 复杂游戏的流程和系统。

第三章:为动作格斗游戏添加玩家角色

上一章涵盖了 Unity 中 2D 游戏开发的多项不同内容和概念,并遵循实用方法完成了一个名为活泼企鹅的简单 2D 游戏。章节继续了从第一章,《使用 Unity3D 开发 Android 游戏入门》开始的实践示例,并通过涵盖 2D 游戏的重要主题来完成。它从在 Unity 中添加粒子效果开始,然后详细解释了粒子系统的概念、粒子系统的基础以及如何在 Unity 中添加它们。然后我们继续讨论通过为游戏中的企鹅添加火箭火焰粒子系统来应用这些概念,并讨论了如发射、形状和颜色等属性。

在粒子系统之后,章节专注于创建游戏环境并添加背景。它解释了如何导入这些背景,以及如何在游戏中使它们可重复。还涵盖了排序层和标签的概念,章节接着讨论了相机管理以及如果相机始终聚焦于企鹅,企鹅将永远不会离开屏幕。

在这些常规主题之后,章节中讨论了一个非常有趣且实用的 Prefab 主题。在讨论 Prefab 的一些基本概念之后,游戏通过使用 Level Prefabs 得到了启发,这使得游戏能够在运行时重复背景并生成障碍物。这次讨论还包含了生成器概念及其代码实现。

最后,章节通过在游戏中添加障碍物和企鹅的基本碰撞检测来完成,这也引出了触发器概念,这在创建不可见碰撞体以检测游戏中某个位置的对象存在时非常有用。以下是在完成上一章后的游戏截图:

图片

图 3.1 鹦鹉螺活泼游戏的一张快照

如果你感兴趣想要在 Unity 中创建任何 2D 游戏,那么这些内容在第一章,《使用 Unity3D 开发 Android 游戏入门》和第二章,《完成活泼企鹅 2D 游戏》中有更详细的介绍。

与前几章相比,本章是一个全新的层次。在本章中,我们将扩展我们对 Unity 的知识,将其应用于 3D 游戏开发,这对于新游戏开发者来说常常会变得非常复杂和棘手。直到 2D 游戏开发,开发者和程序员的关注点围绕图像、精灵和二维概念,忽略了摄像机的深度。那些在 Adobe Photoshop 或 Gimp 等图形工具上工作的新开发者证明在 2D 游戏开发方面非常出色,并能快速掌握概念,但 3D 变得非常困难,因为它需要掌握 Autodesk Maya、3D Studio Max 等高度使用的先进商业工具的专业知识。这些工具需要多年的实践和经验,这使得 3D 游戏开发对于新程序员来说非常难以学习。

本书的范围不包括涵盖所有主题,但我们将通过从头开始创建 3D 动作格斗游戏来学习基本概念及其用法。本章将从配置 3D 游戏项目、导入 3D 模型以及如何将这些模型上应用纹理和材质开始讲解。接下来,本章将介绍骨架的概念以及 Unity 提供哪些接口来简化骨架配置,例如人形骨架、通用骨架等。这个骨架概念将通过在 Unity 中导入和配置动作格斗游戏的玩家角色模型来在实践中得到实现。我们还将应用动画以允许基本角色移动。

现在,让我们将我们的话语付诸实践,开始学习 3D 游戏开发,以下部分将重点关注 Unity 中 3D 游戏项目的配置。

本章包括以下主题:

  • 在 Unity 中为 3D 游戏配置项目

  • 导入 3D 模型

  • 应用纹理和材质

  • 通用和人形骨架

  • 配置人形角色

  • 旧版和 Mecanim 动画系统

  • 带有动画控制器的状态机

在 Unity 中配置 3D 游戏项目

在前几章中,我们学习了 2D 游戏,也学习了如何在 Unity 中创建和配置 2D 游戏项目。对于 3D 项目,流程几乎相同,只是需要做一些更改。让我们先启动 Unity 5,并使用它创建一个空项目。

如果你已经熟悉 3D 游戏项目的配置或者之前参与过任何 3D 游戏开发,你可以安全地跳过这一部分。当你启动 Unity 5 时,它会显示一个项目向导,如下面的截图所示:

图片

图 3.2 Unity 5 新项目向导

项目向导显示了一个包含所有最近项目及其名称的列表。最近的项目被突出显示以快速打开。在向导的右上角,有一些控制按钮,可以用来从头创建新项目或从任何目录打开任何特定的项目。让我们通过点击右上角的“新建项目”按钮开始创建一个新项目。您将看到一个对话框,如下面的屏幕截图所示:

图片

图 3.3 新建项目详情向导

输入项目名称并选择其路径位置。在这些文本输入下方,您将观察到两个选项的简单选择:2D 和 3D。由于我们正在创建一个 3D 游戏,我们将选择 3D。这在上面的屏幕截图中显示:

图片

图 3.4 2D 和 3D 项目类型选择器

此切换选项允许您告诉 Unity 您的项目是 2D 还是 3D。尽管在处理项目时这不会以任何方式影响或改变项目,但它会影响默认的项目设置,以便更容易地工作流程。例如,在 3D 模式下,当您将任何图像资产导入到项目中时,Unity 会将其视为纹理;而在 2D 模式下,Unity 会将其视为精灵。您也可以稍后从项目设置中更改模式;在项目创建时选择不是必需的。默认情况下,Unity 会以 3D 模式创建项目。

现在,让我们将项目命名为 Free Fighter 并点击创建项目。这将导入一些预定义的资产并启动 Unity 界面编辑器。您可以看到,将创建一个包含两个游戏对象的空新场景,这两个对象位于层次面板中:主摄像机和方向光。我们现在不会讨论这些对象,但它们将在后续章节中涵盖,当我们管理游戏中的摄像机和灯光时。

在这个空白未保存的场景中,我们首先需要的是玩家角色的模型。让我们在下一节中一般性地讨论模型或模型。

导入 3D 模型

在我们深入探讨 3D 建模和 Unity3D 的精彩细节之前,让我们首先讨论一下什么是 3D 模型。在计算机图形学中,3D 建模是通过任何 3D 建模软件构建和开发任何物体三维(3D)表面的数学和视觉表示的过程。该过程的最终产品被称为3D 模型,或在本书的上下文中简称为模型。3D 模型有趣的地方在于它们是三维的,而任何计算机或笔记本电脑屏幕都是二维表面,因此将 3D 模型显示在二维屏幕的视野和边界内变成了一项棘手的工作。因此,使用 2D 图像的形式显示 3D 模型的过程被使用,这被称为3D 渲染。它包括 3D 软件工具,包括我们自己的 Unity3D。这个渲染过程使用灯光和相机将 3D 模型渲染成真实的样子,并在观众心中呈现它的图像。我们可以进一步探讨这个永无止境的主题和 3D 模型的讨论,但这远远超出了本书的范围。因此,我们将只讨论有助于在 Unity 中构建 3D 游戏的重要细节,即模型和建模工具。

3D 建模是开发任何物体三维表面的数学表示的过程。记住,3D 模型是任何游戏的主要资产。模型文件可能包含 3D 模型,例如任何角色、建筑、家具等。模型文件也可能包含可用于动画此模型或其他模型的动画数据。动画数据以一个或多个动画剪辑的形式导入。我们无法在 Unity 中创建 3D 模型,因为 Unity 是用于开发游戏的引擎,而不是用于创建 3D 模型。我们只能导入它们并在游戏中使用它们。市场上有很多 3D 建模软件包,而且它们每天都在不断改进。

在 Unity 中,导入网格可以通过两种主要类型的文件实现:导出的 3D文件格式和专有 3D 应用程序文件。让我们进一步描述导出的 3D 文件格式。Unity 可以读取.fbx.dae.3DS.dxf.obj。以下文本中描述了导出的 3D 文件格式的优缺点:

优点

  • 只导出你需要的数据

  • 可验证的数据(在 Unity 之前重新导入到 3D 包中)

  • 通常文件较小

  • 鼓励模块化方法;例如,用于碰撞类型或交互的不同组件

  • 支持我们没有直接支持的专有格式的其他 3D 包

缺点

  • 可能是原型设计和迭代过程中的较慢流程

  • 更容易在源(工作文件)和游戏数据(例如,导出的 FBX)之间丢失版本跟踪

第二种类型的文件是专有 3D 应用程序文件,例如来自 3D Studio Max 或 Blender 的.Max.Blend文件格式。Unity 也可以导入 Max、Maya、Blender、Cinema4D、Modo、Lightwave 和 Cheetah3D 文件。例如,MAX、MB、MA 等。专有 3D 应用程序文件的优缺点如下:

优点:

  • 快速迭代过程(保存源文件,Unity 重新导入)

  • 简单易用

缺点:

  • 必须在所有使用 Unity 项目的机器上安装该软件的授权副本

  • 文件可能会因为不必要的数据而变得臃肿

  • 大文件可能会减慢 Unity 更新

  • 验证较少,因此更难解决问题

3D 模型

3D 模型是使用几何原语(如三角形、线条、曲面等)集合构建的任何物理对象的表示。以下图显示了用于创建 3D 模型的一些基本原语:

图 3.5 一些基本的 3D 建模原语

所有的 3D 模型和对象都是通过组装、修改、微调等操作这些基本形状和对象来创建的。为了更好地理解,请看以下图,它显示了在像 Photoshop 或 Gimp 这样的 2D 图形工具背后的场景中使用的 2D 原语:

图 3.6 一些 2D 原语

建模工具

现在你已经对 3D 模型有了很好的理解,让我们继续讨论建模工具。这些 3D 模型是在非常先进且主要商业的工具中创建的,例如 Autodesk Maya、Autodesk 3D Studio Max、Blender 等。这些工具为建模者提供了各种选择和功能,以创建和动画 3D 模型,并以图像或视频电影的形式渲染它们,或者直接将它们导出为模型文件。

以下表格显示了某些 3D 建模工具及其使用许可类型:

工具名称 许可类型
Autodesk Maya 商业
Autodesk 3D Studio Max 商业
Cinema 4D 商业
Cheetah 3D 商业
Blender 公众

表 3.1 一些 3D 建模工具

Unity3D 不是一个高级建模工具,但它提供了一些基本原语和一些基本的建模功能。但是,为了在 Unity3D 中创建任何 3D 游戏或电影,你需要更详细的 3D 模型。因此,Unity3D 允许开发者和建模者导入大多数类型的 3D 模型文件,以便自定义、动画并在游戏中或电影中使用它们,同时也可以通过编程来实现。这个功能使 Unity3D 与其他 2D 或 3D 游戏引擎相比确实非常强大。在导入模型时,Unity3D 支持两种类型的文件:

  1. 导出的 3D 文件格式,如.fbx.dae.obj

  2. 专有 3D 应用程序/工具文件或建模工具的原生文件,例如.max.mb

导出的文件通常体积较小,这些文件允许你在导出时选择你想要在 Unity3D 中使用的数据。Unity3D 推荐这些类型的模型用于游戏或电影,以便优化实时渲染和高性能。

后续的专有文件是 Maya 和 3D Studio Max 等工具的模型的原生源文件。这些文件的最大优势是它们允许在编辑时通过看似重新导入模型的方式快速迭代测试游戏/电影。但是,你需要在每个使用 Unity3D 和模型文件的计算机上拥有建模工具的授权副本。这些文件通常非常大,不建议在 Unity3D 中发布游戏或电影。

强烈建议在 Unity3D 中使用 FBX 文件,因为它允许直接在模型中嵌入动画和纹理。

Unity3D 中导入 3D 模型

因此,在详细讨论了 3D 模型和建模工具之后,让我们深入了解如何在 Unity3D 中导入 3D 模型。如果我们用导入精灵、图像和声音的说法来说,它听起来与将任何 3D 模型文件放入项目的 Assets 文件夹相似,但实际上由于 Unity3D 导入过程中涉及到的许多概念和设置,它实际上变得复杂。

在本节中,我们将讨论如何在 Unity 中导入任何 3D 模型。现在,我们将导入一个简单的 FBX 类型的房屋模型到 Unity 中。FBX 类型代表 Filmbox 文件,主要用于 AutoCAD 的房屋、建筑、汽车和工程建模的 CAD/CAM 设计。主要的 3D 建模工具也提供了将模型导出为 .fbx 格式的选项。使用 FBX 文件的主要优势是它允许开发者选择在文件中导出哪种类型的数据。这些数据可以包括网格、网格动画、骨架或骨骼动画。我们使用了一个简单的房屋模型,如图所示,从可供公众使用的 Free 3D Models (t3fm) 网站下载:

图 3.7 一座房屋模型

现在,首先在 Unity 中打开我们的 3D 自由战斗机项目,你将在项目面板中看到 Assets 文件夹。在 Assets 文件夹上右键单击,创建一个名为 Models 的新文件夹。将模型导入 Unity3D 的方法有很多。点击 Assets 菜单,选择 Import New Asset... 选项,然后选择 Farmhouse.fbx 文件,最后点击 Import 按钮。

整个过程如图所示:

图 3.8 Unity3D 中导入 3D 模型

导入后,你会看到文件将被放置在 Models 文件夹中。但你也注意到了,在项目面板中,与模型文件一起自动创建了一个名为 Materials 的文件夹,如图所示:

图 3.9 项目面板中的房屋模型

现在,模型已导入 Unity,可以在游戏中使用。你可以以与在 Unity3D 中添加图像和精灵类似的方式将其拖入场景。将其拖入场景后,你会注意到模型没有着色。它只是一个单色的白灰色模型,如下面的截图所示:

图片

图 3.10 场景中的房屋模型

颜色消失的原因,在 Unity3D 中称为纹理和材质,是因为这个模型的.FBX 文件只包含场景中显示的模型网格。纹理和材质也独立于模型存在,也可以嵌入到模型文件本身中。将文件分开到 Unity3D 中是一种良好的实践。

将手动放置在Assets文件夹中的模型文件自动导入到 Unity3D 中。

模型文件可以包含任何 3D 模型,如任何角色、建筑或任何几何形状。我们已经在 Unity3D 中导入了一个农舍模型。在场景视图或层次结构面板中,导入的模型被放置为模型预制件的游戏对象,这是 Unity3D 在导入模型时自动创建的。这个游戏对象将包含所有模型数据,如网格、灯光等,作为其子对象。以下图显示了层次结构面板中的农舍模型游戏对象:

图片

图 3.11 农舍模型预制对象层次结构

开发者在导入模型后经常会对涉及的不同问题感到困惑。一些模型在导入后变得非常小,或者一些模型在 Unity3D 中旋转方向相反。这都是关于模型设置的问题。由于这些模型是在其他工具(如 Maya3D)中创建的,缩放和场景设置的不同可能导致这些问题。

因此,为了更详细地自定义这些设置,Unity 提供了检查器面板中的模型属性,这有助于在使用场景中的模型之前设置模型的属性。当你选择Assets目录中的导入模型时,你将在检查器面板中看到一些其他设置,如下面的截图所示:

图片

图 3.12 农舍模型设置

如前一个截图所示,模型文件包含三种在设置中以标签形式呈现的数据:模型、绑定和动画。模型标签包括与网格、材质和模型本身相关的所有数据。绑定标签和动画标签允许开发者设置模型的动画行为,无论是使用模型文件本身嵌入的动画,还是从 Unity3D 中的其他动画剪辑中设置。我们将在本章后面讨论这些细节。尽管模型标签本身有很多设置,但我们只讨论一些在 Unity3D 中导入 FBX 模型时通常使用的设置:

  1. 缩放:此缩放因子用于消除 Unity3D 中的单位系统与创建模型时所使用的建模工具中的单位系统之间的间隙。Unity3D 通常将缩放因子的一个单位视为一米。因此,开发人员应相应地设置缩放因子。通常,这设置为 1。

  2. 生成碰撞体:此复选框允许开发者为网格和模型自动生成碰撞检测的碰撞体。请注意,启用此复选框时,Unity3D 将创建网格碰撞体,这有时会导致非常重的处理,具体取决于网格本身。因此,如果您想自定义碰撞体,您可以在 Unity 中自定义,或者创建自己的网格碰撞体。

这两个设置将使模型处于良好状态。我们应该提醒自己,我们的模型仍然是单色的,我们还没有将其导入纹理和材质。因此,在下一节中,我们将讨论在模型对象上应用纹理和材质。

不同 3D 模型的默认缩放因子如下:.fbx, .max, .jas, .c4d = 0.01, .mb, .ma, .lxo, .dxf, .blend, .dae = 1, .3ds = 0.1。

导入 FBX 模型

FBX 是 Filmbox 的缩写。Unity 支持可以由许多流行的 3D 应用程序生成的 FBX 文件。请注意导出范围,例如网格、相机、灯光、动画装置等:

  1. 应用程序通常允许您导出选定的对象或整个场景

  2. 确保您只导出场景中需要使用的对象,可以通过导出选择的数据或从场景中移除不需要的数据来实现。

  3. 良好的工作习惯通常意味着保留一个包含所有灯光、引导、控制装置等的工作文件,但只通过选择导出数据、使用导出预设或甚至自定义场景导出器来导出所需的数据。

参考以下图以确定 OBJ 和 FBX 之间的差异:

在 Unity 资产商店中,许多 3D 模型免费或付费提供,可以轻松地用于任何游戏。您还可以在互联网上找到许多网站,用户在那里出售或提供免版税 3D 模型(tf3dm.com

因此,我们在 3D Max 中建模了一个房子,并将其导出为 FBX 格式;记住,在那个模型中我们只有网格,然后在项目面板中右键单击,然后点击“导入新资产...”,然后浏览您的 FBX 模型路径并点击导入按钮。Unity 还提供了一个拖放功能,我们可以将 fbx 模型拖放到项目面板中。以下图显示了如何通过点击“导入新资产...”按钮将模型导入场景:

您可以在项目面板中看到 FBX 模型,这是以管理方式推进项目的更好方式,因此创建一个名为“模型”的空文件夹,然后在根目录中将其拖放到模型文件夹中。以下图显示了将 FBX 模型拖放到模型文件夹中的情况:

现在转到模型文件夹,将 Farmhouse FBX.fbx 文件拖动到层次面板中;如果显示的模型很小,这意味着模型的缩放比例很小,要增加模型缩放大小,请从项目面板中选择 Farmhouse FBX.fbx,你将在检查器面板中看到 3 个标签页,分别命名为模型、绑定和动画,我们现在专注于模型,模型默认被选中。在第一个文本框中,缩放因子,将值 1 改为你需要的任何值,比如 10,然后点击模型面板末尾的“应用”按钮。

考虑以下图示来说明模型缩放大小的增加:

图片

应用纹理和材质

当涉及到 3D 建模时,纹理和材质变得复杂,因为它们赋予了模型生命。3D 建模工具为建模者和设计师提供了相当多的灵活性,以便他们可以一起工作并创建令人惊叹的纹理,并将它们从简单的图像转换为 3D 工作模型。从图形和设计角度来看,有三种组件可以实体化模型:纹理、着色器和材质。Unity3D 提供了许多选项来自定义这三个部分,并允许开发者创建他们想要的模型外观。这三个组件以这种方式连接,使得开发者可以轻松地单独更改和修改每个组件,并实时看到它们对模型的影响。以下图示展示了这三个组件的连接:

图片

图 3.13 纹理、阴影和材质之间的连接

你可以在前面的图中看到纹理不是直接应用于模型。相反,它们应用于材质,然后材质再应用于模型。材质还添加了一些材质化属性,以便与场景中的灯光相互作用,使模型在计算机屏幕的二维世界中看起来像 3D。以下小节中我们将对每个组件进行详细说明。

纹理

通常,网格对象给出了模型形状的大致近似以及它将看起来如何,但定义形状本身并将其以观众视角的真实形式呈现的是纹理。纹理是包裹在模型网格表面的平面位图图像。最简单的例子就是想象它们是打印在任意 3D 模型上的图像,比如任何圆柱体、立方体或任何复杂模型,比如我们的农舍。

以下图示展示了这一场景:

图片

图 3.14 3D 模型上的纹理

纹理导入器

所有纹理都以图像形式存在于项目文件夹中。Unity 允许你更改纹理类型--在导入纹理后,你只需从项目面板中选择纹理,并在检查器中的最顶部进行修改。这允许你从源图像文件中选择你想要创建的纹理类型。以下图示显示了所有纹理类型:

图片

在着色器中应用纹理

你为对象使用的着色器对所需的纹理有特定的要求,但基本原理是你可以将任何图像文件放入你的项目中。如果它满足以下文本中指定的尺寸要求,它将被导入并优化。

将纹理放入着色器中有一系列步骤:

  1. 在选择模型后,着色器将在检查器面板中可见。

  2. 材质中包含纹理按钮,点击它以应用纹理。

  3. 选择纹理,这样我们的 3D 模型就会被我们选择的纹理包裹。

这在以下图中表示:

图片

Unity 可以读取以下文件格式作为纹理:PSD、TIFF、JPG、TGA、PNG、GIF、BMP、IFF 和 PICT。应注意的是,Unity 可以导入多层的 PSD 和 TIFF 文件。

着色器

在计算机图形学中,着色器对于新手来说有点难以理解。纹理告诉观众 3D 模型对象表面和网格上绘制了什么,但着色器告诉如何绘制该纹理。正如我们之前在图 3.13中看到的,材质是纹理和着色器的混合物。材质包含属性和纹理,着色器告诉我们材质可以具有哪些属性和着色器。让我们通过一个例子来更好地理解这个概念。想象你有一块木头。这块木头的形状是 3D 模型的网格对象。颜色、木纹和其他可见元素是 3D 模型的纹理。现在,如果你把这块木头放入水中,它看起来会比原来的木头略有所不同。请注意,它仍然是相同的网格和相同的纹理,但放入水中时看起来会不同。这种可见性的差异是由 Unity3D 中的着色器定义的。在这个例子中,水就是着色器。我们将在本书的后面部分讨论如何在 Unity3D 中制作和应用这些着色器。

材质

简而言之,材质定义了在计算机图形中表面应该如何渲染。它不仅仅是一个纹理和着色器的容器。相反,它带有许多不同的属性,这些属性根据着色器本身而变化,并获取纹理的引用以进一步定义模型的最终可见性。以下图显示了两种不同着色器的材质差异:

图片

图 3.15 Unity 中不同着色器的材质

注意左侧材质中的微光。这是因为高光着色器,而右侧材质使用的是漫反射着色器。两者都有共同的属性,如颜色、基础纹理等。Unity 自带了许多内置的着色器,你也可以编写自己的自定义着色器来进一步润色和定义模型的视觉效果。

在农场模型上应用纹理

到目前为止,我们已经概述了纹理、着色器和材质,以及它们如何相互关联。但不要忘记,在我们的场景中有一个导入的农舍模型,它以白色和灰色阴影的形式出现。现在,我们必须在它上面应用纹理并为模型创建材质。Unity 提供了一个非常简单且方便的方法来在模型上应用纹理。

Assets文件夹中创建一个名为Textures的新文件夹,并使用常规文件浏览器或通过将图像拖动到文件夹中在 Unity3D 中加载以下图示中的图像:

图片

图 3.16 农舍模型纹理

一旦它被导入到Assets文件夹中,你必须将其应用到场景视图中放置的模型上。从Assets文件夹中拖动纹理并将其放置在场景视图中的模型上,Unity 将自动创建材质并为你完成渲染工作。然后你可以根据你的需求进一步自定义它。以下图示展示了在材质上应用纹理的过程:

图片

图 3.17 农舍模型纹理

在应用纹理后,你会观察到农舍将呈现为彩色形式,现在看起来更加逼真。以下图示展示了农舍模型在纹理前后以及其在检查器面板中的属性之间的简单比较:

图片

图 3.18 纹理前后对比

注意,在应用纹理之前,Unity3D 已经为模型应用了一个默认的农舍纹理,并使用了标准着色器,这使得它看起来是白色的。而纹理应用之后,Unity 会在纹理上放置一个编号的名称(在图中称为Farmhouse Texture 6)。参考前图中标签编号 1 处的着色器。你还可以注意到,Albedo 属性(如图中标签编号 2 所示)在之前是空的,但现在它被我们的纹理图像填充了。你还可以通过点击 Albedo 旁边的方形图标来应用纹理。这将打开一个Assets资源管理器面板的弹出窗口,通过该窗口你可以从资源中选择所需的纹理,并将其应用到模型上。

通用和人类机器人

虽然我们已经讨论了模型以及如何在 Unity3D 中导入这些模型,但我们还没有谈到这些模型应该是什么,或者游戏引擎更倾向于哪种类型的模型。一个模型可以包括从简单的带纹理的立方体到家具和住宅,再到生命角色和外星人,以及太空轨道和行星。在大多数情况下,游戏更注重交互式剧情,这涉及到许多角色(主要是人类),他们有着各种各样的情感和表情。Unity3D 作为一个非常容易学习和使用的游戏开发平台,承担了提供角色和人类支持的任务,并提供了一套高度灵活且功能齐全的工具,可以非常容易地处理、动画化和管理角色模型。

人形角色是什么?

正如“人形”这个词本身所清楚的那样,人形角色是基于人类的角色。不一定是人类,但应该基于人类的物理形状。猴子模型是人形模型的一个很好的例子。它可以是两足外星人等等。由于大多数游戏都包含人形模型,为了简化问题,Unity3D 为人形模型提供了良好的功能集。

从互联网上可以获取大量免费和付费的人形模型资源。或者,您可以使用 Poser、MakeHuman 或 Mixamo 等工具创建自己的人形模型。其中一些工具还提供了绑定和为模型添加皮肤的功能。

简单来说,绑定是指为任何 3D 模型添加骨骼并连接骨骼的过程。

您还可以从 Asset Store 获取许多免费和付费的各种各样的人形模型,如男性、女性、老角色、战士、格斗家、力量角色等。人形角色通常具有骨骼结构、皮肤肌肉和用于服装或衣物的纹理。骨骼结构或骨骼,在现实生活中被称为骨骼,是人形角色的关键部分。它允许我们使用相同的骨骼或称为绑定来重新定位和动画化其他人形角色。以下图示显示了一个简单的人形角色及其皮肤、绑定和肌肉:

图 3.18 一个简单的人形角色,带有皮肤网格(左)、绑定(中)和肌肉(右)

应该注意的是,任何人形角色都将具有如图所示的结构。我们将在后面的章节中更详细地讨论绑定。现在,我们将讨论 Unity3D 为人类角色模型提供的工具类型以及它们的使用方法。我们将通过从 Unity Asset Store 导入一个免费的角色模型来学习这一点。

Unity Asset Store (assetstore.unity3d.com) 是 Unity3D 的一个在线市场,提供用于 Unity 游戏的资产和现成工具包、插件、扩展和代码,有免费和付费版本。

导入人形模型

让我们从学习如何将人形模型导入 Unity 开始。我们将从创建一个名为 Humanoid Character 的空项目开始。随着项目的成功构建和编译,我们就像往常一样打开一个空场景,得到了一个空项目。

现在,当涉及到建模格式时,人形模型并没有什么不同。人形模型是我们之前章节中讨论的简单模型文件类型,例如 FBX、MB、3DS 等。我们可以通过从文件资源管理器拖放文件到 Unity 中,或者通过使用菜单选项导入新资产...手动导入。在这个例子中,我们将使用 Unity Asset Store 中免费提供的模型。资产名称为 Raw Mocap Data for Mecanim,它包括由 Unity3D 开发的角色以及大量动画,如向前移动、向后移动、跳跃、匍匐前进等。可以在以下位置找到:www.assetstore.unity3d.com/en/#!/content/5330。将此 URL 粘贴到你的浏览器中,然后点击 Open in Unity 按钮,它将打开 Unity3D 并显示带有资产的 Asset Store 面板,如下截图所示:

图片

图 3.19 Unity3D 中的 Unity Asset Store 面板

现在,如果你已经将此资产下载到你的本地计算机上,那么它将显示导入按钮;否则,它将显示下载按钮。下载并将此资产导入到你的 Unity 项目中,它将显示一个包含所有要导入的文件列表的对话框面板。这是为了让开发者知道哪些文件是新的,哪些文件将被此资产包替换。如下截图所示:

图片

图 3.20 导入前的原始 Mocap 数据文件

我们有一个空项目,因此它显示了所有文件前面的 NEW 标签。现在,点击面板左下角的 All 按钮,然后点击 Import 按钮。这将花费一些时间来导入、解压缩并将所有资产导入到项目中。一旦完成,你会在Assets文件夹中注意到一个新的目录叫做 Raw Mocap Data。这就是所有文件和资产数据被放置的地方。如下图所示:

图片

图 3.21 导入后原始 Mocap 数据文件放入 Assets 中

由于本节全部关于模型及其设置,我们现在暂时忽略动画目录。在原始 Mocap 数据目录中,你会看到一个名为 DefaultAvatar 的模型文件。从技术上讲,这是一个 FBX 文件,如果你在文件资源管理器中看到它,并且 FBX 文件是 Unity3D 中最受欢迎的模型文件。当你选择它时,你会在检查器面板中看到选中的设置标签页,以及一些预定义的导入选项,如下图所示:

图片

图 3.22 检查器中默认头像模型的导入设置

此模型包含许多材料,它们放置在“材料”目录中,并分配到角色上,显示其皮肤和服装,正如我们可以在前一个图中的小预览部分中看到的那样。还有一个包含动画列表文本文件的“动画”目录。我们将在后面的章节中讨论这些动画。

因此,让我们选择“骨架”选项卡,如果它还没有从检查器面板中选择,然后讨论那里的不同选项。你会注意到“动画类型”已经选为“人类”选项。当任何模型文件被导入 Unity 时,它会自动尝试通过将其骨架结构与预定义的人类结构进行比较来检测模型类型,并为它创建一个自动的化身。我们将在后面的章节中讨论这个预定义的人类骨骼结构。如果匹配成功,Unity 会将动画类型关联到人类;否则,它将其设置为通用。还有两个其他选项,即“无”和“旧版”。“无”选项允许 Unity 忽略整个骨架,让人类没有骨骼和动画。而“旧版”选项是处理人类角色的较老方法,现在已弃用,不推荐使用。你还可以手动更改模型的动画类型选项。通用类型选项用于告诉 Unity 该角色不是人类。例如,它可以是某种六足外星生物或任何马模型,或者任何奇怪的蜘蛛,或者任何静态的树,等等。

在“动画类型”下方,你会看到“化身定义”选项。基本上,当任何人类模型被导入 Unity 时,它会开始寻找该模型的化身。在 Unity 中,化身是一个简单的骨骼,它提供了一种控制模型网格、皮肤和材料以进行动画、移动等方法。如果模型已经正确设置骨架并且是人类,Unity 将自动从这个模型文件创建化身。我们还可以使用“从其他化身复制”选项从任何其他模型文件复制化身。复制选项将要求源化身,并将新的骨骼放入当前模型中。这就像将一个角色的骨架放入另一个角色的身体网格中。这是 Unity3D 的一个非常强大的功能,使得角色处理变得容易得多。

下一个选项是带有小勾选图标的“配置”按钮。这个图标告诉我们我们的模型是否可以使用。简单来说,Unity 试图从模型中自动创建一个虚拟形象,这个图标显示 Unity 是否能够完成这项任务。从动画类型中选择“无”选项,然后再次选择“人类”选项。现在你会在“配置”按钮前看到三个点符号。这表示模型尚未配置,虚拟形象也尚未创建。这意味着 Unity 尚未确认模型文件是否为人类。您可以点击“应用”,Unity 将尝试自动检测其人类特征并创建自动虚拟形象。如果模型是人类,则图标将更改为勾选图标;否则,它将变为叉号图标,表示模型配置不正确,不是人类。

下一个图显示了人类模型配置的所有不同状态:

图片

图 3.23 人类模型配置的不同状态

我们已经了解到 Unity 会自动尝试检测模型是否为人类,并尝试通过创建具有预定义人类绑定的自动虚拟形象来正确配置它。但是,我们仍然不知道 Unity 是如何做到这一点的,以及在进行 Unity 配置过程中可以做出哪些改进和定制。在下一节中,我们将讨论任何角色如何在 Unity 中配置为人类,以及自动虚拟形象创建是如何工作的。

配置人类模型虚拟形象

由于虚拟形象定义了 Unity 中人类模型的整体绑定和骨骼结构,因此确保其正确配置对于模型来说非常重要。我们不应该依赖于 Unity 自动创建虚拟形象的结果。无论是否带有勾选图标配置正确,还是带有叉号图标未配置,您都需要进入“配置虚拟形象”模式,亲自确保虚拟形象设置正确且适用于游戏。这是整个角色动画系统MecAnim 系统的一个重要要求,我们将在下一节中讨论。

现在,如图所示,点击“配置...”按钮。如果当前场景尚未保存,它将要求您保存当前场景,并打开一个新场景,如图下截图所示:

图片

图 3.24 虚拟形象配置场景

您可以观察到,打开了一个新场景,我们的角色模型位于场景视图的中心。在检查器面板中,显示了一个 2D 人类角色,其体内有许多绿色圆圈,如图下所示:

图片

图 3.25 人类角色虚拟形象视图

此区域由两个标签页组成,映射和肌肉与设置,其中第一个已选中。在映射标签页中,有一个带有绿色实线和虚线圆圈的人体,圆圈映射了骨骼的游戏对象与人体关节,以便它能够那样表现。实线圆圈需要与任何骨骼游戏对象关联。即使单个必需的圆圈没有正确映射,角色配置也不会设置,并显示一个红色圆圈,如图 3.25 右侧所示。

为了提高找到匹配角色的机会,请以反映它们所代表的身体部位的方式命名你的骨骼,例如 LeftArm(左臂)、RightForearm(右前臂)等。

在检查器视图中,你还可以观察到垂直对齐的其他按钮:Body(身体)、Head(头部)、Left Hand(左手)和 Right Hand(右手)。正如在检查器中显示的那样,其他按钮显示人体其他部分以映射更多骨骼,为角色提供动画中使用的微小细节。这些在以下图中显示:

图 3.26 所有部分的角色映射

你可以看到,除了身体部分外,所有其他部分都只有可选骨骼。一般来说,Unity 覆盖了人体的所有骨骼。可选骨骼在 Unity 中自动映射和动画,但必需骨骼需要从模型中映射。如果在 Autodesk Maya、3D Studio Max 或任何其他 3D 建模工具中绑定角色时遗漏了任何骨骼,Unity 将在那里显示一个红色圆圈,这是开发者的责任,通过从层次结构面板中选择游戏对象并将其放置在检查器面板中骨骼列表下方来提供缺失的骨骼。

现在,如果你在场景视图中查看,可能会注意到我们的角色处于 T 形姿态。如果你的模型不是 T 形姿态,Unity 将在场景视图中打印消息来强制模型采用 T 形姿态。T 形姿态是 Unity 将正确骨骼与正确游戏对象映射的模型原点。你可以手动旋转骨骼的游戏对象以形成 T 形姿态,或者你可以通过在检查器面板底部选择“强制 T 形姿态”从姿态下拉菜单中自动完成,如图所示:

图 3.27 检查器面板中的强制 T 形姿态选项

一旦选择了该选项,如果所有骨骼都正确绑定并分配,模型对象将自动定位到 T 形姿态。此下拉菜单中的其他选项包括重置,它将重置所有骨骼并将角色配置设置为初始状态,以及采样绑定姿态,它将强制模型设置为导入模型时的相同姿态。

你可以通过在检查器面板中姿态下拉菜单旁边的映射下拉菜单中选择自动映射选项来重置整个映射并使用 Unity 的自动角色配置。

在您所做的任何更改之后,您可以点击“应用”或“还原”来应用或取消更改。一旦您完成了配置和角色,您可以点击“完成”按钮来关闭角色场景并导航回您原来的工作场景。

另一个选项卡“肌肉与设置”超出了本书的范围,但您可以继续尝试一下。它允许您验证所有骨骼与肌肉的关系,并且您还可以通过在其中定义范围来限制骨骼的运动,例如头部不能完全旋转。

现在,我们的模型已经导入,并且已经正确配置到人形角色上。是时候让它栩栩如生了,并添加一些行走、跑步、跳跃等动作动画。在本节中,我们将讨论 Unity 如何让我们管理动画并将它们应用到人形模型对象上。

使用 Unity 进行人形动画

动画是任何游戏的核心组件。没有它们,游戏只是一系列图片,用户将无法了解游戏中正在发生什么。我们已经在第二章“完成活泼企鹅 2D 游戏”中学习了动画,为了我们的 2D 游戏“活泼企鹅”,我们使用状态机和控制器来控制企鹅和激光的动画。但是,当涉及到 3D 时,管理和处理的事情会变得更加复杂。这种复杂性的原因取决于我们想在 Unity 中创建和管理的动画类型,但通常创建如行走循环或跑步循环这样的角色动画需要大量的努力,并且不能在 Unity 中完成。这些动画需要更多的细节,并且是在高级工具(如 Autodesk Maya、3D Studio Max 等)中开发的。在本节中,我们不会讨论这些动画是如何创建的,因为这超出了本书的范围,但我们将讨论如何将这些动画导入 Unity,并通过编程和状态机控制器进一步管理,以实现我们的游戏需求,为格斗游戏中的玩家角色进行动画处理。

Unity 的动画系统通过支持动画混合、混合、加法动画、循环动画、时间同步(如行走循环)、分层动画、基于速度、时间等因素的动画控制回放,以及基于物理的布娃娃支持,让您能够创建令人惊叹的动画角色。与之前的动画工具(如 Adobe Flash)相比,Unity 提供了扩展的图形用户界面,使事情变得更加简单。

Legacy Animation System

在 Unity 4.x 版本发布之前,Unity 提供了一种更简单的动画系统,现在称为Legacy Animation System。为了保持向后兼容性,这个系统仍然可用。您可以在旧项目中使用遗留系统,而无需将其更新到新系统。但是,不建议在任何新项目中使用遗留系统。

我们不会深入探讨旧版动画系统的工作原理,因为它在 Unity 游戏中不是一个好的选择,但我们将讨论在 Unity 中使用旧版动画系统的基本概述。旧版动画系统基于以下步骤在 Unity 中创建和管理动画:

  1. 您准备要动画化的游戏对象。例如,让我们以一个战斗角色为例。现在,为了在 Unity 中对其进行动画处理,您必须创建一个模型,对其进行绑定,添加纹理,然后将其导入 Unity。这一步骤由艺术家、3D 模型师和动画师完成。

  2. 在导入模型后,您必须手动将其动画类型字段设置为 Legacy,因为 Unity 不会自动执行此操作,您需要在模型的检查器设置中的 Rig 选项卡中完成此操作。这如图所示:

图 3.28 设置动画类型字段为 Legacy

  1. 现在,您可以在动画选项卡中设置所有动画剪辑,如向前走、向后走、跑步、跳跃等,并设置这些剪辑的时间和速度。您可以通过裁剪和剪切动画剪辑中的帧、修改速度、创建循环或 Ping-Pong 序列等功能在这里自定义和编辑剪辑。

动画剪辑也可以使用 Autodesk Maya 或 3D Studio Max 等工具嵌入到模型文件中。此外,动画也可以是模型文件本身的独立文件。Unity3D 在这里显示模型文件设置中的动画选项卡中的嵌入动画。

  1. 一旦所有动画剪辑都设置好并准备就绪,就到了创建控制器的时候了。旧版动画系统允许您使用 C#或 JavaScript 创建控制器,这将告诉 Unity 在何时播放哪个剪辑以及播放多长时间等。这个控制器基本上由游戏输入管理,例如,按空格键时,角色应该跳跃。

尽管这四个步骤没有详细解释如何使用旧版动画系统,但它足以让您了解在 Unity 中如何使用旧版系统。旧版系统在很大程度上依赖于脚本部分来管理动画,其控制器完全由开发者自己编程和编码。这使得与下一节将要讨论的新 Mecanim 动画系统相比,旧版系统更加复杂。

Mecanim 动画系统

虽然 Unity 已经有一个现成的动画系统,并且开发者正在使用它,但 Unity3D 发布了一个全新的系统,从头开始创建和管理更复杂的动画,而且更加容易。该系统被称为 Mecanim 动画系统。以下是 Mecanim 动画系统的几个特点:

  • 动画工作流程和设置的简便性

  • 支持在 Unity 内部创建或外部导入的动画剪辑

  • 可以将一个角色模型的动画重定向到另一个角色模型

  • 用于管理和预览动画剪辑的可视化编程工具

  • 层叠和遮罩功能

有可能您可能没有完全理解这些功能的一些内容。然而,无需担心,因为在本节中我们将详细讨论这些功能和系统。

我们将首先从上一节中的人形角色项目开始讨论 Mecanim。

在上一节中,我们导入了一个免费的角色以及许多动画剪辑。我们讨论了模型文件及其材质文件夹,然后我们还为它配置了一个人形头像。现在,我们将从那个人形头像设置继续,并使用 Mecanim 动画系统来为角色动画化。为此,首先我们必须查看我们从原始 Mocap 动画数据资产中获得了哪些类型的动画。在项目资源管理器面板中展开动画文件夹,然后再次展开“行走”文件夹以查看一些行走动画。

现在,从该文件夹中选择任何随机的动画剪辑,然后在检查器面板中您将看到一个带有小播放按钮的小预览窗口,该按钮可以暂停/播放动画剪辑。整个过程如下面的图所示:

图片

图 3.29 预览“行走”文件夹中的任何动画剪辑

这个预览窗口显示了动画剪辑,以便您更好地了解哪个动画更适合您的游戏需求。您还可以从其他文件夹播放更多动画。

这个动画预览系统仅在 Unity 4.0 版本引入后,Mecanim 中才可用。

Unity 的 Mecanim 动画系统基于动画剪辑的概念,它包含有关不同对象如何随时间移动、旋转或变换的信息。每个剪辑可以是对同一对象的独立、单独的录制。这些动画剪辑大多由 3D 动画师使用第三方工具(如 Autodesk Maya、3D Studio Max 或其他来源)创建。然后,可以使用这些工具将这些剪辑导出为单独的剪辑,或者也可以将它们嵌入到模型文件本身中。为了更好地理解这一点,请在项目面板中选择“原始 Mocap 数据”目录中的 DefaultAvatar.fbx,然后在检查器面板中选择动画选项卡。您将观察到导入动画的复选框已经被选中,下面有一个信息框告诉您模型文件不包含任何动画数据。这意味着模型文件本身没有嵌入任何动画剪辑。整个过程如下面的截图所示:

图片

图 3.30 在模型中查看嵌入的动画数据

无论您是在模型中嵌入动画剪辑,还是单独导入动画剪辑(如本例所示),这些动画剪辑随后将被组织成一个类似于结构流程图的系统,称为动画控制器。这个动画控制器扮演着状态机的角色,负责跟踪哪个动画剪辑应该播放,何时停止播放动画剪辑等等。这是 Mecanim 系统最重要的功能之一,它将 Mecanim 系统与传统的动画系统区分开来,并为开发者提供了许多便利和控制。在传统的系统中,开发者通常使用 C# 或 JavaScript 编程语言编写整个动画控制器,但在 Mecanim 中,Unity3D 提供了一个可视化界面,可以创建整个控制器,无需一行代码,并具有许多内置功能。

任何简单的动画控制器都必须至少包含一个或两个动画剪辑。这些动画剪辑可以像任何门的开关动画或任何类人角色的前后走动动画那样简单。更高级的动画控制器可以包含数十个类人动画,用于角色的所有主要动作,如行走、跑步、跳跃、战斗等等。我们的游戏项目包含具有行走、跑步、跳跃、用手、腿和超级技能的角色。因此,我们单个角色的动画控制器非常详细且高级。在下一节中,我们将讨论如何创建动画控制器,并为我们的格斗游戏玩家角色创建一个简单的控制器。

为格斗游戏创建分层角色

在上一节中,我们讨论了 Unity 的动画系统;传统和 Mecanim。在本节中,我们不仅将讨论,还将实际创建格斗游戏玩家角色的动画控制器。我们将使用 Unity 提供的免费角色,该角色包含在原始 mocap 数据资产项目中。

首先,在场景视图中,我们需要一个类人角色。从项目面板中的原始 mocap 数据目录拖动 DefaultAvatar.fbx 预制体到层次结构面板,它将显示一个穿着黑色西装的 T-Posed 角色如图以下截图所示:

图片 3.32

图 3.31 场景视图中的 T-Posed 角色

现在,在层次结构面板中选择 DefaultAvatar,并查看其检查器设置。在变换组件下方将添加一个新的组件,称为动画组件,如图以下截图所示:

图片 3.33

图 3.32 角色模型的动画组件

你可以观察到在组件中有一个信息提示,指出控制器尚未初始化。当任何类人角色首次放置到场景中时,Unity 会创建一个默认的运行时动画控制器并将其分配给该角色。我们也可以随时使用我们的自定义控制器来更改或修改其控制器。Animator 组件中包含多个属性,如前图所示。在继续前进之前,让我们讨论一下这些属性的细节:

  1. 控制器属性用于设置 Animator 组件的动画控制器。这是最重要的属性,通过这个控制器,整个角色的动画在 Unity 中得以管理和处理。

  2. Avatar属性告诉 Unity3D 用于角色的绑定和 Avatar 配置。这与我们在之前章节中为类人角色设置的相同的 Avatar 配置相同。我们也可以为其他角色的 Animator 使用其他角色的 Avatar。这就是 Mecanim 动画之所以如此强大的原因,它允许开发者无需额外努力即可将他们的 Avatar 定义重新定位到其他角色上。

现在,随着 Avatar 属性已经设置为我们配置的 Avatar;让我们创建我们的第一个动画控制器。在 Assets 文件夹上右键单击,然后选择创建 | 动画控制器,并将其命名为 PlayerAnimController,如下面的截图所示:

图 3.33 为玩家创建动画控制器

你会注意到在 Assets 目录的项目面板中会添加一个不同的文件。双击它,你将看到一个 Animator 窗口,显示一个类似于状态机的用户界面,如下面的截图所示:

图 3.34 Unity3D 中的 Animator 面板

如您在此面板中所见,已添加了三个状态,分别称为任何状态、进入和退出。此面板的主要目的是组织所有动画剪辑,在这里称为状态,并使用类似于流程图的顺序、并行或更复杂的关系将它们连接起来。为了创建一个简单的状态机,我们至少需要在 Animator 面板中有一个动画状态。我们已经在项目资产中导入了很多动画;我们只需从那些动画中选择一些用于我们的简单动画控制器。

现在,在 Assets 中创建一个新的名为 Player Animations 的目录。我从我们的 Raw Mocap Data 文件夹中选择了四个动画剪辑,并将它们放置在我们的新文件夹中:

图 3.35 Player Animations 目录中的动画状态

一旦您的动画片段都设置在单个目录中,让我们更深入地探索这些动画。在项目面板中单击 Idle_Neutral_2 动画片段,然后在检查器中选择“Rig”选项卡。您会注意到动画类型设置为“Humanoid”,而头像定义设置为从另一个头像复制。您可能想知道另一个头像在哪里。您可以在“源”属性中选择这个其他头像,然后点击“应用”以保存更改。源属性设置为我们的配置头像定义,称为 DefaultAvatarAvatar,用于我们的角色模型。通过此属性,您可以选择任何其他头像的定义,并非常容易地将相同的动画片段重定向到多个角色上。这些属性如图所示:

图片

图 3.36 动画片段的“Rig”选项卡

我们项目中所有动画片段的“Rig”选项卡设置几乎完全相同。现在,选择“动画”选项卡以根据我们自己的项目需求自定义动画。部分“动画”选项卡如图所示:

图片

图 3.37 部分动画选项卡

如果您不想导入任何动画,只需关闭“导入动画”复选框,Unity 将忽略该片段的动画。其余的设置在此刻并不重要,我们将在本书的后面讨论它们。现在,最重要的设置是“片段”属性。这是创建整个动画时间线的位置。您可以观察动画帧的起始和结束值。您可以单击加号(+)来添加一个新的子动画,通过定义起始和结束帧,Unity 将把动画的这部分裁剪到单独的片段中。您可以在自己的面板上尝试这个功能,看看会发生什么。此外,您还可以观察到一个小的预览窗口,允许您播放和停止自定义动画。

目前,我们在“片段”列表中只有一个片段,并且默认选中。在“片段”属性下方,Unity 显示了此特定片段的更多详细信息,如图所示:

图片

图 3.38 任何选定的子动画片段的更多属性

这里有很多选项。我们不会详细讨论所有这些,但我们将概述这个面板。有各种部分,如 Loop Time、Root Transform Rotation 等。这些部分定义了动画的特定部分。例如,Loop Time 定义了整个动画是否为循环动画。在每个部分之前,都有一个带有红色、黄色或绿色填充的颜色圆圈。这个圆圈告诉我们动画的循环匹配状态。红色表示它不是循环,黄色表示它有些循环但不是完美同步,绿色表示它是完美循环。这些属性允许开发者以非常详细的方式定义动画,以管理动画时角色的Y轴位置等。你可以改变开始和结束值,以查看动画在哪里是完美循环,所有绿色圆圈都相应地定制。

最后,在讨论了许多关于动画片段的细节和属性之后,让我们继续创建我们的简单动画控制器。从 Assets 目录中双击 PlayerAnimController 文件。它将显示 Animator 视图,其中包含我们之前讨论的三个状态。现在,按照以下步骤创建一个行走循环动画状态机:

  1. 将 Idle_Neutral_2 动画片段从 Player Animations 目录拖动到 Animator 视图中。你会看到一个新添加的橙色矩形,其中包含动画片段的名称。这个矩形显示了该动画片段的状态。如下图所示:

图片

图 3.39 在 Animator 视图中添加的新动画

可以通过右键单击动画并从菜单中选择设置为默认层状态来将任何动画设置为默认动画。Unity 将改变其颜色为橙色。任何单个层中只能有一个默认状态。

现在,如果你玩游戏,你会看到场景中的角色没有进行动画。尽管我们已经将其默认动画状态设置为空闲动画,但角色仍然没有移动。这是因为我们只创建了 Animation Controller,但我们仍然需要将控制器与我们的角色关联:

  • 要将我们的角色与新建的动画控制器关联,从 Hierarchy 面板中选择角色。现在,将我们的 PlayerAnimController 从 Assets 拖动到 Inspector 中 Animator 组件的 Controller 属性,如下图所示:

图片

图 3.40 将动画控制器与角色关联

现在,玩游戏,你会看到角色正在跟随空闲动画。但是,一旦动画结束,它就会停止。我们希望在我们的情况下无限循环动画。

  • 要设置动画循环,从玩家动画目录中选择 Idle_Neutral_2,然后在检查器设置中的动画标签页中。在剪辑下面,我们首先需要将动画设置为完美循环,并使所有圆圈变为绿色。所以,将起始值设置为 265,它将变为绿色。然后,切换 Loop Time 选项为开启,并点击应用。现在,播放游戏,你的角色将在动画结束后持续重复动画。此外,你将无法观察到动画的结束,因为在动画中创建了一个完美循环。这在上面的图中有所展示:

图片

图 3.41 设置动画循环

  • 现在让我们再添加一个名为 walk 的动画状态。就像我们之前做的那样,从资产拖动 WalkFWD 到动画器视图中。它将创建另一个矩形状态,这次是灰色。这个动画状态已经添加到控制器中,但还没有与动画器链接。要将此状态与我们的默认 Idle 状态链接,右键单击 Idle_Neutral_2 状态,并选择创建过渡。这将允许您从 Idle 状态创建一条到另一个状态的白色线条。现在,点击 WalkFWD 状态,将在 Idle 和 WalkFWD 状态之间绘制一条带有指向 WalkFWD 状态的箭头的线条。这在上面的图中有所展示:

图片

图 3.42 在两个状态之间创建过渡

现在,播放游戏,你会观察到一旦空闲动画完成,它将开始 WalkFWD 动画。在走动动画之后,它将停止。我们还可以从 WalkFWD 到 Idle_Nuetral_2 状态创建另一个过渡,这将使角色持续动画。一旦空闲动画完成,它将开始行走,一旦行走完成,它将回到空闲动画,这将持续运行,形成一个循环动画。

现在,是时候在动画器中添加一些控制了。在动画器面板的左侧,有两个标签页:图层和参数。点击参数标签页,然后点击加号(+)按钮来添加一个新的参数。它会显示一个下拉菜单,从中选择我们想要添加的参数类型。选择布尔值(Bool)并命名为 ShouldWalk。这在上面的截图中有展示:

图片

图 3.43 在动画器中添加新参数

现在,你将看到在参数列表中添加了一个新的参数。参数允许开发者根据它们的值来控制动画和状态流。此时,空闲动画完成后,走动动画会自动开始。现在,我们将这个 ShouldWalk 参数与动画器集成,以便如果 ShouldWalk 参数为真,则开始行走动画;否则,它将回到空闲状态:

要这样做,点击从空闲到行走的过渡箭头,并检查检查器面板。你会在那里看到各种设置。最后,你将看到一个条件的小视图。点击那里的加号(+)图标,并将 ShouldWalk 设置为 true。这如图所示:

图 3.44 添加过渡条件

同样,现在为其他反向过渡重复相同的步骤,但这次将 ShouldWalk 值设置为 false。现在播放游戏,你会观察到角色不会进入行走动画,它将不断地重复相同的空闲动画。如果将 ShouldWalk 参数设置为true,它将播放行走动画。这可以通过代码非常容易地完成。

摘要

本章介绍了 3D 游戏的基本概念。我们讨论了任何 Unity 项目如何配置为 3D 游戏,以及如何在 Unity 中导入 3D 模型文件。我们还讨论了 3D 模型的材料和纹理。为了专注于我们的战斗动作游戏,我们讨论了人形模型或角色模型以及它们的导入方式。然后我们讨论了用于动画系统的人形模型的化身配置。Unity 提供两种类型的动画系统;旧的和 Mecanim。我们简要讨论了旧系统,因为它现在已经在 Unity 中弃用,随后对 Mecanim 动画系统进行了更详细的讨论。我们导入了一些动画,并努力创建了一个简单的动画控制器,以及一个简单的参数来控制动画的流程。

在下一章中,我们将从控制器开始,创建一个包含许多动画和参数的详细动画控制器,该控制器将用于玩家和敌人角色。然后我们将使用虚拟摇杆控制来控制玩家移动和动画,并简要讨论敌人角色背后的人工智能。

第四章:带有 AI 的敌人角色

在上一章中,我们学习了如何将 3D 几何和纹理导入到项目中,设置角色并给角色添加动画。在这一章中,我们将运用这些知识,开始制作一个具有基本控制方案的动作游戏。

我们将首先导入玩家角色,并为不同的战斗动画设置所需的动画帧,如空闲、出拳、阻挡和被击中。我们还将使用动画控制器创建不同的状态。动画控制器让我们可以根据角色的行为来决定动画流程。

我们还将探讨游戏平衡以及如何公平对待玩家。如果游戏不公平,玩家会感到沮丧并退出游戏,并且不会向任何人推荐这款游戏。

我们将使用鼠标和键盘来实现控制,但在后面的章节中,我们将看到如何为手持设备实现触摸控制。

在本章结束时,我们将拥有一个基本的战斗系统,其中我们可以击打敌人,敌人会受到打击或阻挡我们的攻击。

本章包括以下主题:

  • 导入玩家模型

  • 使用动画控制器创建玩家

  • 编写玩家控制脚本

  • 添加敌人角色

  • 敌人行为和 AI

  • 完成战斗

导入玩家模型

首先,确保你创建一个新的 Unity 3D 项目。这不像我们之前创建的第一个项目,因为这个项目是一个 3D 项目,而不是 2D 项目。

一旦创建项目,你将在本章的资产中找到Dude.FBX文件,所以将文件拖放到 Unity 项目中:

在你的情况下,模型将是灰色,而不是像这里显示的红色。要使其变红,双击材质文件夹。你会看到01- Default文件。当你选择它时,你会得到一个选项来更改物体的颜色。点击检查器面板中 Albedo 选项旁边的灰色方块,并选择红色来更改角色的颜色:

我们接下来需要做的是为游戏中所有的动画创建动画剪辑。点击项目“资产”文件夹中的 Dude 角色。现在查看检查器面板。在检查器面板中,你会看到三个标签页,分别称为模型、绑定和动画,如下面的截图所示。点击动画标签页:

这显示了当前存在的所有动画剪辑。到目前为止,只有一个动画剪辑,名为Take 001,从第 0 帧开始,到第 80 帧结束。你甚至可以通过点击面板底部的播放按钮来预览动画。

你会看到Take 001包含了所有导入的动画,以及 FBX 文件,包括空闲、防御、出拳和被击中的动画。由于所有动画都在一个剪辑中,我们必须将动画拆分成单独的剪辑。

要做到这一点,首先我们需要提取从帧 0 到帧 29 的空闲剪辑。因此,我们将名称更改为空闲,并将动画剪辑的结束帧设置为 29,如下面的截图所示:

图片

确保点击底部的应用按钮以使更改生效。

此外,请注意,循环时间复选框已被勾选。这是因为动画需要循环播放,如果我们不勾选此框,则动画将只播放一次并停止。

现在,让我们提取第二个动画剪辑,即“守卫”/“防御”动画。为此,在剪辑菜单中点击加号图标。这将创建一个默认动画,称为Take 001,如下面的截图所示:

图片

现在,再次选择“Take 001”剪辑,将其重命名为 defend,并将开始和结束时间更改为 32 和 49。点击循环时间,并在最后点击应用按钮:

图片

现在添加打击被击中动画的动画剪辑。打击动画从 51 开始,到 60 结束,被击中动画从 71 开始,到 75 结束。

对于这些动画,不需要勾选循环时间选项,因为它们不需要循环。一旦你有了所有所需的动画,剪辑窗口应该看起来像下面的截图:

图片

一旦我们完成了动画的提取,我们就可以为玩家角色创建动画控制器。

玩家动画控制器

要创建新的动画控制器,请在项目中的“资产”文件夹上右键单击,选择创建选项,然后从列表中选择动画控制器:

图片

将文件重命名为 dudeAC,并双击它:

图片

当你双击它时,将打开一个名为动画的新面板,如下面的截图所示:

图片

动画控制器基本上控制动画的流程。默认添加了三个状态;这些是入口、任何状态和退出:

  • 入口状态指定在场景开始时播放哪个动画。因此,在大多数情况下,首先播放的是空闲动画。

  • 任何状态指定无论之前播放的动画是什么,都需要执行哪个动画。

  • 退出状态是在场景退出时播放的动画。

让我们设置玩家的状态,敌人的设置也将相同。

首先,我们将创建默认动画,以便当场景开始时,播放空闲动画。在动画器面板中,右键单击任何位置,从列表中选择创建状态选项,然后选择空选项,如下面的截图所示:

图片

选择创建的新状态,并打开检查器面板。

接下来,我们需要从入口状态创建一个转换。右键单击它,将打开“创建转换”选项,因此点击此选项。从点击的状态开始出现一个箭头。现在点击我们需要转换到的状态。这将创建到该新状态的转换,如以下截图所示:

图片

接下来,将新状态重命名为空闲。点击新状态,将状态重命名为 Idle,并通过选择它旁边的小圆圈将运动字段更改为空闲:

图片

要测试控制器是否工作,将theDude角色拖放到层次结构中,并将其放置在相机垂直的位置,如图以下截图所示:

图片

在场景中选择 Dude 角色,然后在检查器面板中,将动画控制器拖放到动画组件中的控制器字段,如图以下截图所示:

图片

点击顶部的播放按钮,你应该会看到带有空闲动画的角色正在播放。现在我们已经确认它工作正常,我们可以进入动画控制器并添加游戏中所需的状态。

在任何状态状态下,我们应该能够拥有防御、拳头和被击中动画。如果玩家不处于这些状态中的任何一个,他应该回到空闲状态。

因此,在动画控制器中,添加三个状态,将相应的动画附加到运动中,并相应地更改每个状态的名字。在创建三个状态后,我们还需要在“任何状态”状态和这些状态之间进行转换,并且当每个状态完成时,它需要转换回空闲状态。

要从给定状态创建转换,右键单击它:将打开“创建转换”选项。点击它,从点击的状态开始出现一个箭头。接下来,点击我们想要转换到的状态。这将创建到该状态的转换。转换在以下图中显示:

图片

每个这样的转换都将通过脚本进行控制。为了触发每个状态,我们将使用布尔值或触发器来启用这些状态变化。

要创建这些参数,点击动画面板旁边的“参数”选项卡。

要创建一个新参数,点击搜索栏旁边的加号。你可以创建浮点数、整数、布尔值或触发器参数。

为了我们的目的,我们创建了一个布尔参数,称为 bIsDefending,以及两个触发器参数,称为 tGotHit 和 tIsPunching,如图以下截图所示:

图片

布尔值和触发器的区别在于,触发器一旦被激活就会将自己设置为 false,而布尔值必须通过程序设置truefalse

让我们看看它在当前游戏中的实现方式。首先,我们将设置打击和被击动画的过渡;然后我们将查看防御状态。选择从任何状态到打击的过渡箭头。

当箭头变成蓝色时,你知道已经选择了过渡。截图显示了默认状态的外观。我们在这里要做一些更改:

图片

首先,我们希望这个过渡只在玩家正在打击时发生。所以,在条件选项卡中,按下加号并添加 tInPunching 条件。

第二点,取消勾选“具有退出时间”选项。如果勾选此选项,则过渡将仅在之前的动画播放完成后才会发生。我们不希望这样。我们希望玩家在点击打击按钮后立即开始播放打击动画。

第三点,也是最重要的一点,动画预览窗口显示了过渡过程中播放的动画。最初它显示动画将从零开始,播放空闲动画,然后过渡到打击动画。我们实际上只想让打击动画立即开始,并且我们只想在空闲动画和打击动画之间有一个过渡帧。

因此,我们将打击动画放回开始位置,并将开始和停止时间设置得非常小,如下截图所示:

图片

现在我们来看一下打击到空闲动画:

图片

在这里,我们保持“具有退出时间”选项勾选,因为我们希望动画播放完毕后播放空闲动画。此外,空闲动画被拉回开始位置,动画播放时间也减少到一帧。

同样的操作也应用于从任何状态到 GetHit 的过渡,如下截图所示。但在这里,条件更改为 tGotHit:

图片

同样,从被击到空闲状态:

图片

让我们看看如何从任何状态到防御状态,然后从防御状态到空闲状态的过渡。从任何状态到防御状态的过渡应更改为以下截图所示:

图片

在这里,我们再次将防御动画拉回,并将播放时间更改为一帧。勾选“具有退出时间”选项,并将条件更改为 bIsDefending,设置为 true。对于从防御到空闲动画的过渡,我们将 bIsDefending 设置为 false,并像往常一样更改动画宽度:

图片

对于从空闲到防御的过渡,我们以与从任何状态到防御相同的方式进行,只是取消勾选“具有退出时间”复选框,如下截图所示:

图片

这样我们就完成了动画控制器。

编写玩家控制脚本

为了控制状态,我们需要将脚本附加到玩家上。我们将使用鼠标的左右键来控制玩家。左键点击用于攻击,右键点击用于防御。如果两者都没有点击,则播放空闲动画。

可以在编辑菜单的 Unity 项目设置选项下更改控制。从列表中选择输入选项,如下面的截图所示:

图片

记住名称,因为在代码中引用时,我们将使用按钮的名称。所以,对于左键点击,我们将引用 Fire1。你可以在检查器面板中检查不同按钮的名称:

图片

通过在资产文件夹选项中右键单击 | 创建 | C# 脚本来创建一个新的脚本,并将其命名为 playerScript。双击它,它应该在 Visual Studio 中打开。

将以下代码添加到脚本中:

    using UnityEngine; 
    using System.Collections; 

    public class playerScript : MonoBehaviour { 
        private Animator anim; 

        // Use this for initialization 
        void Start () { 
            anim = GetComponent<Animator>(); 
        } // start 

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

            //Defending 
            if (Input.GetButtonDown("Fire2")){ 
                // Debug.Log("Jump pressed"); 
                anim.SetBool("bIsDefending", true); 
            } else if (Input.GetButtonUp("Fire2")) { 
                anim.SetBool("bIsDefending", false); 
            } 

            //Attacking 
            if (Input.GetButtonDown("Fire1")){ 
                anim.SetBool("bIsDefending", false); 
                anim.SetTrigger("tIsPunching"); 
                //Debug.Log("Fire pressed"); 
            } 
        } // update 
    } 

在顶部,我们创建一个 private 变量来获取动画器组件并将其存储在该变量中。

接下来,在 start 函数中,我们获取动画器组件并将其分配给 anim,否则我们每帧都要做这件事,这将非常耗时。

Update 函数中,我们首先检查防御状态。如果按下 Fire 2 按钮,即鼠标的右键,我们将 bIsDefending 设置为 truebIsDefending 变量与我们定义在动画控制器选项中的参数相同。

如果按钮被释放,我们将 bIsDefending 变量设置为 false

接下来,我们设置攻击状态。如果按下 Fire1 按钮,我们首先将防御设置为 false,然后将 tIsPunching 设置为 true。我们还进行调试注销,以便我们可以看到击打的特定代码。

为了查看这确实是否有效,我们必须将其附加到场景中的玩家角色上,并将其添加为组件:将代码拖动并添加到 theDude 角色上。现在,如果你查看检查器,你可以看到脚本已被添加:

图片

现在,如果你右键点击鼠标,玩家应该进行防御,如果你用鼠标左键点击,玩家应该进行击打。

图片

让我们给玩家一个敌人来击打。

添加敌人角色

就像我们将玩家拖入场景一样,将 Assets 中的 Dude 角色拖入 Hierarchy。在 Hierarchy 中将此角色重命名为敌人。这个家伙也会是红色,这不是我们想要的,所以我们将创建一个新的材料。

右键点击材料文件夹,然后选择创建 | 材料项目。将材料命名为 Material。将 Albedo 颜色控制旁边的颜色从默认值改为蓝色。

现在,将材料拖放到敌人角色上。同时放置并旋转角色,使其与玩家角色相对,如下面的截图所示:

图片

还需要将 dudeAC 动画控制器拖到敌人 Animator 组件的 Controller 组件中:

敌人行为和 AI

当你玩游戏时,敌人将开始于空闲动画。让我们给敌人角色添加一些行为。

通过创建一个模式来实现 AI,该模式将确定敌人下一次状态改变的时间,以及敌人将处于空闲、防御或攻击状态的时间段。

模式是一个数组,包含 20 个元素。当所有元素耗尽后,模式将被随机化,计数器再次设置为 0。

一些间隔持续 10 帧,而其他间隔持续长达 120 帧,即两秒钟。玩家必须判断并确保自己不被击中,并且必须在敌人杀死他之前击败 enemy。一旦我们通过代码,这会更有意义。

创建一个新的 C# 脚本并将其命名为 enemy。将以下脚本添加到代码中。

首先,我们初始化我们的变量:

    using UnityEngine; 
    using System.Collections; 

    public class enemy : MonoBehaviour { 

    private Animator anim; 

    int myTick = 0; 
    int currentTick = 0; 
    int prevTick = 0; 
    int nextTick = 0; 
    int patternLength = 0; 
    int patterCount = 0; 

    int[] pattern = new[] {10, 10, 10,30, 60, 10, 40, 60, 120, 30, 
                           10, 10,10, 60, 60, 120, 30, 10, 10, 10}; 
    // Use this for initialization 

Start 函数中,我们获取 Animator 组件并设置模式,稍后我们将对其进行随机化:

    void Start () { 

    anim = GetComponent<Animator>(); 
    anim.SetBool("bIsDefending", true); 

    Shuffle(pattern); 

    patternLength = pattern.Length; 
    nextTick = pattern[0]; 

    } // start 

Update 函数中,我们更新我们最初设置的值:

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

        myTick++; 
        currentTick = myTick;        

        if (currentTick == prevTick + nextTick) { 
            int choice = Random.Range(1, 3); 

            switch (choice) { 

                //will punch 
                case 1: anim.SetBool("bIsDefending", false); 
                    anim.SetTrigger("tIsPunching"); 
                    break; 

                //will defend 
                case 2: anim.SetBool("bIsDefending", true); 
                    break; 

                //will be idle  
                case 3: anim.SetBool("bIsDefending", false); break; 
            } 

            prevTick = currentTick; 
            nextTick = pattern[patterCount]; 

            if ((patterCount + 1) >= pattern.Length){ 
                patterCount = 0; 
                Shuffle(pattern); 
            } 
            else { 
                patterCount++; 
            } 
        } 
    } // Update 

Shuffle 函数随机化我们创建的初始模式:

    void Shuffle(int[] a){ 

        for (inti = a.Length - 1; i> 0; i--){ 
            int rnd = Random.Range(0, i); 
            int temp = a[i]; 
            a[i] = a[rnd]; 
            a[rnd] = temp; 
        } 

        for (inti = 0; i<a.Length; i++){ 
            Debug.Log(a[i]); 
        } 
     } // shuffle    

就像代码顶部的玩家一样,我们创建一个 private 变量来存储敌人 animator 变量。

我们还创建了一组名为 myTickcurrentTickprevTicknextTickpatternLengthpatternCount 的整数变量。

myTick 变量只是一个计数器,它不断递增。CurrentTickprevTick 变量分别跟踪当前帧的 tick 和 prevTick 跟踪上一次动作发生的 tick。

patternLength 变量跟踪模式数组中的项目数量,而 patternCount 跟踪正在使用的当前模式编号。

模式是一个包含动作间隔的数组。

Start 函数中,我们获取 Animator 组件并将 isDenfendingbool 变量设置为 true,这样敌人一开始就会开始防御。我们打乱模式并将 patternLengthnextTick 赋值给模式的第一个元素。

接下来在 Update 函数中,我们首先递增 tick 并将 myTick 赋值给 currentTick

然后我们检查是否是执行下一个动作的时间,通过检查 currentTick 是否等于上一个 tick 和 nextTick 的和。

如果它们相等,那么我们生成一个从 1 到 3 的随机数。根据返回值是 1、2 还是 3,将有一个 switch 语句,敌人将出拳、防御或保持空闲。

最后,我们将 currentTick 赋值给 previousTick,将 nextTick 赋值给下一个 patternCount

我们还需要递增 patternCount 并检查 patternCount 变量的值是否超过了长度。如果超过了,那么我们需要将其重置。

因此,如果patternCount++的值大于或等于patternLegth的值,则将模式重置为零并打乱模式,否则我们只需递增patterCount

最后,我们还有一个函数可以随机打乱模式,这样就不会重复。将此脚本组件添加到场景中的敌人对象,并观察他闲置、阻挡和出拳:

图片

结束战斗

让我们让玩家通过改变玩家的行为来对敌人的攻击做出反应。在playerScript中添加以下代码行。在类顶部创建一个新的public变量,类型为GameObject,并将其命名为enemy

    public GameObject enemy; 

接下来,在Update函数的开始处,获取敌人的Animator组件:

    Animator eAnim = enemy.GetComponent<Animator>(); 

接下来,在更新函数中的攻击函数之后,添加以下内容:

    // Getting Hit 
    if (eAnim.GetBool("tIsPunching")){ 
        if (anim.GetBool("bIsDefending") == false){ 
            //Debug.Log("I got hit"); 
            anim.SetTrigger("tGotHit"); 
        } 
    }  

在这里,我们检查敌人是否在出拳。如果敌人正在出拳而玩家没有防守,那么我们将gotHit触发器设置为true

theDude角色拖放到如图所示的敌人脚本组件中:

图片

类似地,在敌人脚本中添加一个名为player的公共GameObject变量。

    public GameObject player; 

Update函数结束之前,添加以下内容。

    Animator pAnim = player.GetComponent<Animator>(); 

    // Getting Hit 
    if (pAnim.GetBool("tIsPunching")) { 
        if (anim.GetBool("bIsDefending") == false) { 
            //Debug.Log("I got hit"); 
            anim.SetTrigger("tGotHit"); 
        } 
    } 

将玩家拖放到敌人脚本中的GameObject,当他不防守时受到攻击:

图片

现在玩游戏,你会看到当敌人不防守时会被击中:

此外,到目前为止,玩家可以持续左键点击,英雄角色可以持续出拳。为了限制玩家可以出的拳数,请添加以下代码。

在顶部添加以下内容:

    float totalTime = 0.0f; 
    float timeSinceLastHit = 0.0f; 
    float hitTimeInterval = 30.0f * .016f; 

Update函数的开始处,增加时间:

    totalTime += Time.deltaTime; 

攻击代码需要更改为:

    // Attacking 
    if (totalTime>= timeSinceLastHit + hitTimeInterval){ 
        if (Input.GetButtonDown("Fire1")){ 
            anim.SetBool("bIsDefending", false); 
            anim.SetTrigger("tIsPunching"); 

            timeSinceLastHit = totalTime; 
            //Debug.Log("Fire pressed"); 
        } 
    } 

概述

在本章中,我们看到了如何导入 FBX 模型,导入动画,并为单个动画设置名称。我们创建了一个动画控制器和动画之间的转换。一旦创建了动画控制器,我们就使用代码来控制动画。

我们还创建了玩家控制,创建了一个非常基本的 AI,并使用随机化的模式来避免模式重复,用模式来控制 AI 行为。

现在我们有一个基本的工作框架,我们将对其进行改进,并在下一章中添加一个合适的游戏循环。

第五章:游戏玩法、UI 和效果

在上一章中,我们创建了基本游戏玩法并确保触发了正确的动画,控制也到位。在本章中,我们将通过添加适当的游戏循环来完成游戏玩法,以便有开始、得分和游戏结束。

对于得分,我们最初将使用 debug 来记录玩家和 AI 的健康状态。稍后,我们将探讨 Unity 的图形用户界面GUI)系统并为玩家和敌人添加健康指示器。

最后,我们还将探讨 Unity 的粒子系统以及我们可以操纵的不同参数,以获得游戏所需的粒子效果。

本章包括以下主题:

  • 完成游戏玩法

  • 理解 Unity GUI

  • 添加健康和游戏结束条件的 GUI

  • 粒子效果简介

  • 创建烟花粒子效果

完成游戏玩法

为了追踪玩家和敌人的健康状态,我们需要变量来追踪他们各自的健康值以及他们能对彼此造成多少伤害。

为了做到这一点,打开playerScript并在类的顶部添加名为healthdamage的变量。然后,将health变量的值设置为 100,将damage变量的值设置为 10。因此,玩家将以 100 的健康值开始,当他们击中敌人时,将对敌人造成 10 点的伤害:

    using UnityEngine; 
    using System.Collections; 
    public class playerScript : MonoBehaviour { 
        public int health = 100; 
        public int damage = 20; 

        private Animator anim; 
        // other code 
    } 

类似地,将playerScript类的相同代码添加到enemyScipt类中。由于我们希望公平,我们将敌人的健康值也设置为 100,并将他们能造成的伤害设置为 10。请确保使用public访问修饰符,因为只有这样我们才能在其他类中访问健康变量:

    public class enemyScript : MonoBehaviour { 

        public int health = 100; 
        public int damage = 10; 

        private Animator anim; 
        // other code    
    } 

现在,每当玩家/敌人受到攻击时,我们必须确保他们的健康值会因其他玩家/敌人能对他们造成的伤害量而减少。因此,在Update函数的章节中,当我们检查玩家或敌人是否被击中时,我们必须通过伤害量来减少健康值。

enemyScript类中,为了检查敌人是否被击中,更改代码如下:

    GameObject player = GameObject.Find("theDude"); 
    Animator pAnim = player.GetComponent<Animator>(); 

    playerScript pScript = player.GetComponent<playerScript>(); 

    //Getting Hit 

    if (pAnim.GetBool("tIsPunching")){ 
        if (anim.GetBool("bEnemyIsDefending") == false) { 
            Debug.Log("enemy got hit"); 
            anim.SetTrigger("tEnemyGotHit"); 
            anim.SetBool("bEnemyIsDefending", true); 
 health -= pScript.damage; 
        } 
    }  

我们通过获取gameobject玩家和添加玩家脚本的GetComponent组件来获取对玩家脚本的访问权限。

一旦我们获得对脚本的访问权限,我们就可以获取玩家能造成的伤害量,并通过伤害量减少敌人的当前健康值。现在,转到playerScript,因为当玩家被敌人击中时,我们同样需要在玩家脚本中实现这一点:

    GameObject enemy = GameObject.Find("Enemy"); 

    Animator eAnim = enemy.GetComponent<Animator>(); 

    enemyScript eScript = enemy.GetComponent<enemyScript>(); 

    if (eScript.isPunching == true) { 
        if (anim.GetBool("bIsDefending") == false) { 
            Debug.Log("player got hit"); 
            anim.SetTrigger("tGotHit"); 
            health -= eScript.damage; 
        } 
    } 

在这里,我们将获取对敌人脚本的访问权限,以便我们可以获取与在敌人脚本中相同的方式获取敌人伤害。一旦我们获取了damage变量,我们将通过在敌人脚本中设置的伤害量减少玩家的健康值。现在,你可以运行游戏并执行Debug.log脚本。Player HealthEnemy Health实体似乎受到了影响,如下面的截图所示:

一旦我们计算出玩家和敌人的生命值,我们就可以设置游戏结束条件。一旦玩家的生命值或敌人的生命值小于或等于零,就是游戏结束。

游戏循环是由第三个脚本控制的。我们将把这个脚本称为gameScript

这是一个非常简单的脚本,它能够访问playerScriptenemyScript,并检查玩家和敌人的生命值。一旦玩家或敌人的脚本中的任何一个小于零,它就会宣布游戏结束。

因此,我们将创建一个新的脚本,称为gameScript,并将以下代码行添加到脚本中:

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

    public class gameScript : MonoBehaviour { 

        playerScript pScript; 
        enemyScript eScript; 
        public bool bGameover = false; 

        // Use this for initialization 
        void Start () { 

            GameObject player = GameObject.Find("theDude"); 
            pScript = player.GetComponent<playerScript>(); 

            GameObject enemy = GameObject.Find("Enemy"); 
            eScript = enemy.GetComponent<enemyScript>(); 
        } 

        // Update is called once per frame 
        void Update () { 
            if (!bGameover) { 

                int playerHealth = pScript.health; 
                int enemyHealth = eScript.health; 

                /* Debug.Log("PlayerHealth: " + playerHealth + "  
                EnemyHealth: " + enemyHealth); */ 

                if (playerHealth<= 0 || enemyHealth<= 0) { 
                    bGameover = true; 
                    Debug.Log(" +++++ GAMEOVER +++++"); 
                } 
            } 
        } 
    } 

在类的顶部,我们创建了三个变量。前两个是为了获取访问玩家和敌人脚本。第三个是一个公共布尔变量,用于设置游戏是否结束。由于游戏刚开始,还没有结束,我们在开始时将其设置为false

Start函数中,我们找到了玩家和敌人,并使用获取组件函数访问了各自的脚本。

然后,在Update函数中,我们首先检查游戏是否没有结束。如果没有结束,我们就获取玩家和敌人的生命值,并将其分别存储在名为playerHealthenemyHealth的局部变量中。

我们接着检查玩家生命值或敌人生命值是否小于或等于 0。如果是这种情况,我们将bGameover布尔变量设置为true,并调用Debug.log,表示游戏结束。

现在,为了让脚本真正运行,它需要附加到场景中的某个对象上。这可能是一个虚拟对象或场景中的任何其他对象。幸运的是,我们有一个就坐在那里并且是场景一部分的相机。因此,我们将gameScript附加到相机作为一个组件。

一旦脚本附加到相机上,运行游戏并查看它是否达到游戏结束条件:

你现在会注意到,尽管游戏已经结束,敌人仍然在攻击玩家,而玩家仍然可以攻击敌人。我们不希望这样,因为这可能会导致游戏中出现一些不必要的错误。

因此,在玩家和敌人脚本中,我们需要访问gameScript类,并确保一旦游戏结束,就没有任何更新。在玩家的类中,我们将在类的顶部创建一个新的GameObject,名为mainCamera,如下面的代码所示:

    public class playerScript : MonoBehaviour { 

        public int health = 100; 
        public int damage = 20; 

        float totalTime = 0.0f; 
        float timeSinceLastHit = 0.0f; 
        float hitTimeInterval = 0.0f; 

        private Animator anim; 

 public GameObject mainCamera; 

        // other code 
    } 

Update函数中,我们获取了相机的gameScript组件,然后无论Update函数中有什么,我们都将其放入一个if条件中,该条件将检查游戏是否结束。

如果游戏没有结束,那么if条件内的所有内容都将更新。否则,它将跳过并不会更新。以下是更新的Update函数(没有双关语的意思):

    void Update () { 
        gameScript gScript = mainCamera.GetComponent<gameScript>(); 

        if (!gScript.bGameover) { 
            totalTime += Time.deltaTime; 

            //Getting Hit  
            GameObject enemy = GameObject.Find("Enemy"); 
            Animator eAnim = enemy.GetComponent<Animator>(); 
            enemyScript eScript = enemy.GetComponent<enemyScript>(); 

            if (eScript.isPunching == true) { 
                if (anim.GetBool("bIsDefending") == false) { 

                    //Debug.Log("player got hit"); 
                    anim.SetTrigger("tGotHit"); 

                    health -= eScript.damage; 
                    Debug.Log("Player Health: " + health); 
                } 
            } 
            // Defending 
            if (Input.GetButtonDown("Fire2")) { 
                //Debug.Log("Jump pressed"); 
                anim.SetBool("bIsDefending", true); 
            } 
            else if (Input.GetButtonUp("Fire2")) { 
                anim.SetBool("bIsDefending", false); 
            } 

            // Debug.Log("Delta time" + timeChangeInMillis); 
            // Attacking 
            if (totalTime>= timeSinceLastHit + hitTimeInterval) { 
                if (Input.GetButtonDown("Fire1")) {     
                    anim.SetBool("bIsDefending", false); 
                    anim.SetTrigger("tIsPunching"); 

                    timeSinceLastHit = totalTime; 
                    //Debug.Log("Fire pressed"); 
                } 
            } 
        } // check if gameover 
    } // update  

现在,我们还需要对敌人脚本执行类似的操作。以下是敌人类的更新代码:

    using UnityEngine; 
    using System.Collections; 

    public class enemyScript : MonoBehaviour { 

        public int health = 100; 
        public int damage = 10; 

        private Animator anim; 

        public GameObjectmainCamera; 

        // public GameObject player; 

        int myTick = 0; 
        int currentTick = 0; 
        int prevTick = 0; 
        int nextTick = 10; 
        int punchTick = 0; 

        public bool isPunching = false; 

        int[] pattern = new[] {120, 30, 180, 30, 60, 30, 40, 60, 180,  
                               30, 30, 30 ,120, 60, 60, 180, 30, 30,  
                               120, 30 }; 
        int patternCount = 0; 
        // Use this for initialization 

        void Start () { 
            anim = GetComponent<Animator>(); 
            anim.SetBool("bEnemyIsDefending", true); 

            Shuffle(pattern); 

            nextTick = pattern[0]; 
        } //start 

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

            punchTick--; 
            myTick++; 
            currentTick = myTick; 

            gameScript gScript = mainCamera.GetComponent<gameScript>(); 

            if (!gScript.bGameover) { 

                GameObject player = GameObject.Find("theDude"); 
                Animator pAnim = player.GetComponent<Animator>(); 
                playerScript pScript = player.GetComponent<playerScript>(); 

                //Getting Hit 
                if (pAnim.GetBool("tIsPunching")) { 
                    if (anim.GetBool("bEnemyIsDefending") == false) { 
                        // Debug.Log("enemy got hit"); 
                        anim.SetTrigger("tEnemyGotHit"); 
                        anim.SetBool("bEnemyIsDefending", true); 

                        health -= pScript.damage; 

                        Debug.Log("Enemy Health: " + health); 
                    } 
                } 

                if (currentTick == prevTick + nextTick) { 
                    int choice = Random.Range(1, 4); 
                    // Debug.Log("Choice" + choice); 

根据从 1 到 3 的随机数,我们将选择 AI 是出拳、防御还是闲置:

                switch (choice) 
                { 
                    //will punch 
                    case 1: 
                        anim.SetBool("bEnemyIsDefending", false); 
                        anim.SetTrigger("tEnemyIsPunching"); 
                        anim.SetBool("bEnemyIsDefending", true); 
                        isPunching = true; 
                        punchTick = 1; 
                        break; 

                    //will defend 
                    case 2: 
                        anim.SetBool("bEnemyIsDefending", true); 
                        break; 

                    //will be idle  
                    case 3:  
                        anim.SetBool("bEnemyIsDefending", false);  
                        break; 
                } 

                prevTick = currentTick; 
                nextTick = pattern[patterCount];//Random.Range(20, 300); 

                if ((patterCount + 1) >= pattern.Length) { 
                    patterCount = 0; 
                    Shuffle(pattern); 
                }  
                else { 
                    patterCount++; 
                } 
            } 

            if (punchTick<= 0) { 
                punchTick = 0; 
                isPunching = false; 
            } 
        } // check if gameover 
    } // Update 

Shuffle函数重新排列初始数组,以便我们得到一组不同的随机数:

    void Shuffle(int[] a) {  
        for (int i = a.Length - 1; i > 0; i--){ 
            int rnd = Random.Range(0, i); 
            int temp = a[i]; 

            a[i] = a[rnd]; 
            a[rnd] = temp; 
        } 

        for (int i = 0; i <a.Length; i++){ 
            // Debug.Log(a[i]); 
        } 

    } // shuffle 

理解 Unity uGUI

随着 Unity 的uGUI系统的引入,在 Unity 中设置 GUI 元素变得非常方便。我们将探讨 uGUI 的工作原理以及如何在场景中显示图像和文本。我们还将看到如何在下一个主题中动态更改文本,当我们实现玩家和敌人的健康值时。

Unity 中的所有 UI 元素都显示在 Unity 菜单中的 GameObject 下拉列表中,如下面的截图所示:

图片

您可以创建三种基本 UI 元素:文本、图像和原始图像。

菜单中的 Text 选项是一个基本的文本元素,用于显示类似分数、健康、能量等的文本。因此,就像任何文本元素一样,您可以指定文本高度和字体,例如粗体或斜体。当您创建一个新的文本元素时,为每个添加的文本元素添加了 Canvas、Text 和 EventSystem 等选项:

图片

当创建一个新的文本 UI 元素时,它默认放置在中心,默认文本为“New Text”,如下面的截图所示:

图片

菜单中的 Canvas 选项表示文本组件在场景中的位置,并负责将文本渲染到场景中。也可以通过右键单击它直接向画布元素添加组件。您还可以向其中添加空游戏对象:

图片

Canvas 由三个脚本组成:Canvas、Canvas Scalar 和 Graphics Ray Caster。

默认情况下,画布设置为 ScreenSpace Overlay。还有 ScreenSpace Camera 和 World Space 选项。在 ScreenSpace Overlay 中,文本将置于场景中的所有内容之上,无论摄像头是否聚焦。这在传统游戏中使用,其中 UI 需要位于游戏之上。

如果您想要根据所选的摄像头来使用不同的用户界面,您可以使用 ScreenSpace Camera,然后可以为不同的摄像头附加不同的用户界面。要在不同的用户界面之间导航,您需要在两个用户界面对应的摄像头之间来回切换。

在世界空间中,画布是根据世界位置放置的。因此,您可以在 3D 空间中放置按钮或文本,并相应地定位它们。您还可以移动、旋转和缩放文本。用户仍然能够导航和使用 UI 元素。这为放置 UI 元素提供了在 3D 空间中任意位置的绝对自由。

如果您希望文本对齐到像素网格,可以选择 Pixel Perfect 选项。Canvas Scalar 会根据分辨率放大或缩小文本。默认情况下,它设置为常量像素大小。您也可以将其设置为与屏幕大小成比例或常量物理大小。

Graphics RayCaster 脚本负责从键盘、鼠标或触摸获取输入。如果移除,UI 元素将不再接受鼠标点击和键盘事件。

讨论事件系统选项时,让我们看看事件系统组件。

还创建了一个 EventSystem。事件系统处理来自鼠标、键盘和控制器的事件。如果您想在某些事件在鼠标点击时触发,这里将指定:

每次您创建一个新的 UI 组件和新的 Canvas 时,EventSystem 也会自动创建。选择文本,这样我们就可以详细查看组件:

UI 游戏对象就像任何其他游戏对象一样,但与它们不同的是,它有一个RectTransform而不是常规变换,有一个 Canvas Renderer,并且还附加了一个文本(脚本)选项。

Rect Transform 由不同的属性组成。其中重要的是位置;

其他的是宽度和高度。这些取决于我们的锚点如何设置。默认情况下,Rect Transform 设置为居中和中心。您可以点击锚点选项来更改预设:

更改锚点预设也会影响场景中按钮或文本的 X 和 Y 位置。锚点也可以根据您的需求手动更改:

接下来,我们将讨论枢轴字段。枢轴是 UI 将围绕其旋转的位置。然后,还有旋转和缩放字段,它们与其他变换对象类似。

现在,让我们看看文本脚本。文本脚本有一个文本框。这就是您可以输入要在场景中显示的任何文本的地方。

之后,是角色部分。在这里,您可以更改角色的属性。您可以更改字体、字体样式--即如果您想要正常、粗体或斜体;字体大小--以及行间距。请注意,Unity 不允许您设置字符的水平间距。

段落部分允许您控制段落本身的位置、颜色和材质。您可以居中对齐段落,设置水平和垂直溢出,或设置为最佳拟合。

如果您添加一个图像 UI 元素,Canvas 和 EventSystem 会保持不变,但图像本身具有通常的 Rect Transform 和 Canvas Renderer 属性。代替文本(脚本),有一个图像(脚本)被添加:

图像(脚本)包含较少的对象。我们有源图像选项,其中您可以指定要显示的图像。您还可以更改图像的颜色或材质。

在有了所有这些信息之后,让我们添加玩家和敌人文本,以及一个游戏结束文本覆盖层,该覆盖层将在游戏结束后出现。

添加健康和游戏结束的 GUI

在游戏场景中,添加三个文本并分别命名为enemyHealthTextplayerHealthTextgameOverText

敌人生命值文本(enemyHealthText)的位置,如以下截图所示,锚点位于中心,字体高度设置为 32。其余的设置为默认:

玩家生命值文本(playerHealthText)被设置在以下截图所示的位置,文本高度更改为 32:

敌人的文本颜色已更改为蓝色,玩家的文本颜色为红色。游戏结束文本(gameoverText)被设置为画布的中间,文本高度设置为 75,颜色设置为漂亮的紫色,以便易于识别。

在文本字段中添加文本 GAME OVER!!!,如下截图所示:

我们还没有看到的是如何通过代码控制文本。让我们看看接下来如何做到这一点。

在 gameScript 脚本中,添加三个类型为Text的公共变量,如下代码片段所示。这将保存从Text UI 元素创建的文本对象:

    public Text enemyTextInstance; 
    public Text playerTextInstance; 
    public Text gameOverText;  

你还需要在类的顶部添加 UI 命名空间,以便它能够正常工作,因此请在类的顶部添加以下行:

    using UnityEngine.UI; 

接下来,在Update函数中,在我们从敌人和玩家获取生命值之后,将值分配给新创建的文本变量,如下所示:

    int playerHealth = pScript.health; 
    int enemyHealth = eScript.health; 

    enemyTextInstance.text = "Health: " + enemyHealth.ToString(); 
    playerTextInstance.text = "Health: " + playerHealth.ToString(); 

MainCamera上,其中附加了gameScript,确保你将敌人生命值文本(Text)、玩家生命值文本(Text)和游戏结束文本(Text)从层次结构拖放到脚本组件上,如下截图所示:

最后,为了确保游戏结束文本仅在游戏结束时出现,在 gameScript 的 start 函数中禁用gameOverText,如下代码所示:

    gameOverText.enabled = false; 

Update函数中,一旦将bGameover布尔变量设置为true,启用gameOverText,如下代码所示:

    gameOverText.enabled = true; 

现在,如果你运行游戏,你会看到分数正在更新:

游戏结束后,你将看到游戏结束文本叠加:

现在我们已经完成了游戏玩法,让我们使用粒子效果为场景添加一些效果。

粒子效果简介

粒子效果是游戏体验的重要组成部分;它们为游戏增添氛围,或让我们知道即将发生或正在发生某些特别的事情。我们以尘埃、云、雨的形式看到粒子,也在游戏结束时的庆祝活动中看到。它们可以真正地成为你计划制作的一切。为了我们游戏的目的,我们将在游戏结束时添加纸屑。

Unity 中的粒子系统也是一个游戏对象,因此要创建粒子系统,请转到 GameObject 菜单并选择粒子系统选项:

按下键盘上的 F 键以突出显示粒子系统选项,因为它可能是在远离主相机的地方创建的。在我的例子中,它是在 900,300 和 -26 处创建的。不用担心粒子的定位。当我们想要粒子在最后被生成时,我们将手动更改其位置。

一旦你放大粒子系统,你会看到一个圆锥形,并看到粒子从形状中出来。圆锥指定了发射器的形状,而白色的粒子是实际从粒子系统中发射出来的粒子。这由以下截图表示:

就像任何游戏对象一样,你也会看到三个箭头,这些箭头让你能够定位粒子系统。你还需要按下 QWE 键来分别定位、旋转和缩放发射器的大小。

一旦你在层次结构中选择粒子系统,它将显示粒子在场景中是如何创建的。如果你选择其他对象或取消选择粒子系统,动画将停止。因此,如果你想预览粒子是如何创建的,那么请在层次结构中选择粒子系统以预览其效果。

让我们看看你可以在粒子系统的检查器面板中修改的一些重要参数:

就像所有其他游戏对象一样,你可以定位、旋转和缩放粒子系统,第一组参数如下所示。这些参数控制粒子最初是如何创建的:

这里给出了所有参数的简要描述:

  • 持续时间:此参数指定粒子最初创建的持续时间。默认情况下,它设置为 5 秒。目前,由于启用了循环,你不会看到任何区别。如果你禁用循环,那么如果你在层次结构中选择粒子系统,你会看到粒子发射 5 秒后停止。

  • 循环:如果你不希望粒子持续生成,则应禁用此选项。因此,如果你想产生粒子爆发,请禁用循环。

  • 预热:此参数在场景开始之前加载粒子,这样就不会看起来像粒子是在场景开始时生成的。例如,如果你有一个瀑布粒子系统,当场景加载时,它应该看起来像瀑布在场景加载之前就已经在运行了。如果瀑布是在场景加载后立即开始形成,那么它看起来就不太实际。

  • 开始延迟:你可以使用此参数设置粒子在一段时间后形成。目前,粒子在你点击层次结构中的粒子系统后立即开始形成,但如果你想让粒子在点击粒子系统后一秒形成,那么你需要输入 1.0,粒子将在你点击粒子系统后一秒形成。

  • 开始寿命:每个粒子持续一段时间后从场景中删除。默认情况下,粒子总是设置为在 5 秒后删除。如果您希望粒子在场景中持续更长时间,则可以更改此参数以满足您的需求。

  • 开始速度:这是每个粒子的初始速度。默认情况下,这也设置为 5。

  • 开始大小:这是粒子的初始大小。默认情况下,它设置为 1。这是在初始大小下创建的粒子对象。如果您创建了更大的粒子并希望将其大小减半,则输入 0.5。

  • 开始旋转:如果您希望粒子以创建时的角度生成,则可以在此设置角度。

  • 随机化旋转:如果您希望粒子以不同的角度创建,则可以在此指定角度。与开始旋转不同,开始旋转会创建所有粒子以相同的角度,而随机化旋转将为每个粒子创建不同的角度。

  • 开始颜色:这指定了每个粒子创建时的初始颜色。默认情况下,它设置为白色。您可以通过单击白色条并指定颜色来更改颜色。

图片

  • 重力修改器:这启用了重力。默认情况下,值设置为 0,表示没有重力。如果您将值设置为 1,则已启用最大重力。您还可以将值设置为介于零和一之间的适当重力水平,以满足您的需求。

  • 模拟空间:这指定了在每个粒子局部之后所做的所有更改;即,相对于单个粒子的原点。默认情况下,设置为局部。

  • 模拟速度:这指定了模拟计算的速度。您可以加快或减慢模拟速度。这对于查看您的粒子系统成熟所需时间的行为非常有用。

  • 缩放模式:将其设置为局部。这指定了缩放会影响每个粒子相对于粒子的原点,而不是发射器或世界的原点。

  • 在场景开始时播放:粒子系统会在场景开始时立即激活。如果您不希望粒子在场景开始时立即启动,则需要取消选中此参数。

  • 最大粒子数:这是场景中一次可以存在的最大粒子数。默认情况下,设置为 1,000。这应该尽可能保持最小,因为它会显着降低游戏性能,因为场景中的粒子越多,所需的渲染调用就越多,每个粒子的计算也越多。

  • 自动随机:种子生成一个种子以自动随机化每个粒子的生成和移动。

下一个参数集是可选的,如果您想启用或禁用这些功能,则需要检查和取消选中:

  • 发射量:这指定了每秒生成的粒子数量。时间速率选项设置为 10,意味着在 1 秒内生成 10 个粒子。如果您将其设置为 1,那么您将每秒看到 1 个粒子。距离速率选项仅在启用世界空间模拟时生效。在爆发模式中,您可以根据最初设置的持续时间创建粒子爆发。因此,在此处,当您点击加号图标时,它将每 5 秒生成 30 个粒子:

图片

  • 形状:这指定了粒子系统的形状。默认情况下,它设置为圆锥形。由于粒子系统的形状是圆锥形,您可以看到粒子从指定的形状中生成并向上移动。如果您想让粒子向所有方向扩散,则可以指定球体。您还可以选择盒子、网格、圆形和边缘。通过选择网格,您可以指定一个特定的网格,并让粒子从该指定的网格形状生成。

  • 还有其他参数,例如角度、半径。

  • 发射位置:这指定了粒子将从哪里生成,例如基础、基础壳体、体积和体积壳体。

  • 您还可以随机化方向或对齐到特定方向:

图片

  • 生命周期内速度:如果您想改变速度随时间变化,则可以在此处指定。如果没有指定,则速度将保持恒定,并且将与最初设置的速度相同。

  • 生命周期内限制速度:这可以限制粒子的速度,使得一旦粒子在生命周期内达到特定的速度,它将被设置为该速度,或者随着时间的推移缓慢地降低到初始设置的速度。

  • 继承速度:这根据发射器的速度来控制粒子的速度。发射器移动得越快,粒子就移动得越快。

  • 生命周期内力:这设置了每个粒子在生命周期内的力。因此,如果这个值被设置,粒子将在一段时间内移动得更快。

  • 生命周期内着色:每个粒子的颜色可以根据其生成的时间进行更改。

  • 根据速度着色:每个粒子的颜色由粒子自身的速度指定。

类似地,我们还有大小和旋转,这些由它们在其生命周期中的阶段或移动速度控制:

  • 外部力:您可以通过改变外部力的乘数来模拟对粒子施加的外部力,例如风的情况。

  • 噪声:您还可以使用噪声生成随机性,这将使用类似 Perlin 噪声的纹理来创建粒子的随机运动和行为:

图片

  • 碰撞:到目前为止,粒子不会对场景中的其他对象做出反应,所以如果你在粒子前面放置一个对象,粒子就会穿过该对象。当你启用碰撞时,粒子会与对象碰撞而不是穿过它:

  • 触发器:如果触发器选项设置被打开,粒子也可以用作触发器:

  • 子发射器:当你启用子发射器选项时,每个粒子也可以发射对象。此外,每个粒子也会发射其他粒子。

  • 纹理表动画:除了静态图像外,你还可以指定一个纹理表,以便每个粒子可以动画化,而不是显示静态图像:

  • 光源和尾迹:每个粒子也可以附加光源和尾迹,使场景更加引人注目和美观。

  • 渲染器:这指定了许多参数,例如粒子本身的形状,是否需要是广告牌或网格。因此,在网格中,你可以指定 3D 对象,如盒子、球体、圆锥体,以及粒子的法向和材质。

你可以指定排序模式、最小和最大粒子大小、对齐和中心点。每个粒子以及接收阴影的投射都有光探针、反射光探针等:

创建五彩纸屑粒子效果

对于五彩纸屑,我们像往常一样创建一个粒子系统。对于初始值,我们将位置保持在 0,0,-7.5。对于旋转和缩放值,我们保持默认设置。

我们将持续时间保持在 4,并取消选中“循环”选项,因为我们不希望五彩纸屑反复创建。起始延迟设置为 0。

起始速度设置为 5,3D 起始大小在XYZ方向上设置为 0.25。3D 起始旋转选项被禁用。起始旋转和随机旋转设置为 0。

起始颜色设置为在两种颜色之间随机,红色和蓝色。重力乘数设置为 0.125,我们希望五彩纸屑在达到最大高度后开始下落。

其余的初始值都设置为默认值:

持续时间设置为 20,形状设置为圆锥形。

为了使五彩纸屑更加多彩,并且随时间改变颜色,将“生命周期中的颜色”更改为所需颜色。因此,根据需要更改它。颜色随速度参数也以类似方式更改。两者都设置为“在两个渐变之间随机”。

随时间变化的大小被更改,以便粒子的尺寸随时间变化。大小设置为曲线,这样粒子在生命周期开始时较小,然后在生命周期结束时变为全尺寸。

曲线的形状如下截图所示:

生命周期中的旋转和速度旋转选项设置为 45,这样粒子在创建后会在空中旋转一次:

对于渲染器,我想将 3D 对象作为粒子,所以我选择了 Mesh,对于 Mesh,我通过点击底部的+号选择了 Cube、Cylinder 和 Sphere。其余的值保持默认:

图片

现在,我们的粒子系统已经创建。我们将在Assets文件夹中创建一个预制件对象,并将粒子系统拖放到其中,以便在游戏结束时实例化预制件。将预制件命名为 particleSystem。你会注意到,一旦将粒子系统拖放到预制件中,它就会变成蓝色,如下面的截图所示。现在,从层次结构中删除粒子系统:

图片

接下来在 gameScript 中,在顶部创建一个新的GameObject,命名为particlePrefab,并将变量设置为public,如下面的代码所示:

    public GameObject particlePrefab; 

Update函数中,在将gameover布尔变量设置为实例化粒子预制件之后:

    if (playerHealth<= 0 || enemyHealth<= 0) { 
        bGameover = true; 
        gameOverText.enabled = true; 
 Instantiate(particlePrefab); 
        Debug.Log(" +++++ GAMEOVER +++++"); 
    } 

不要忘记将 particleSystem 预制件拖放到 MainCamera 附加的游戏脚本组件中的 Particle Prefab 上:

图片

现在,当你运行游戏并且游戏结束时,你将有一个漂亮的纸屑粒子系统:

图片

摘要

在本章中,我们学习了如何添加一个 GUI 元素来显示玩家和敌人的生命值,并且添加了游戏结束文本,以便我们知道游戏已经结束。我们还添加了一个非常基础的粒子系统,只是为了展示添加粒子的能力。你可以进入并更改粒子系统,看看还能做些什么。

游戏玩法已经结束,接下来我们可以看看如何将场景添加到游戏中,因此我们将创建一个主菜单场景,它将在游戏开始时显示。我们还将了解如何添加按钮,以便在点击播放按钮时,场景将切换到游戏玩法场景。

第六章:GameScene 和 SceneFlow

到目前为止,我们只处理了 startScene,但在本章中,我们将看到如何向游戏中添加其他场景,并且我们需要一个 MainMenu 场景,从这里我们可以进入其他场景,如选项或成就场景。

每个场景都将有特定的游戏对象和按钮,这将使我们能够更改设置或按钮,这些按钮可以带我们回到 MainMenu 场景。

MainMenu 场景还需要一个播放按钮,该按钮应链接到 startScene,而 startScene 负责启动游戏。

显然,我们可以根据游戏需求添加更多场景和菜单。

让我们先看看如何向 startScene 添加按钮,这样在游戏结束时我们就可以重新启动游戏。

本章包括以下主题:

  • 按钮简介

  • 组织项目文件夹结构

  • 向 startScene 添加游戏重启按钮

  • 向 startScene 添加暂停按钮

  • 向 startScene 添加一个 MainMenu 按钮

  • 创建一个 MainMenu 场景

  • 添加一个选项场景

  • 添加一个成就场景

向 startScene 添加按钮

在上一章中,我们学习了如何向场景中添加文本。然而,我们也可以向场景中添加按钮。按钮的创建过程与创建文本 UI 的过程相同。前往 GameObjects | UI | Button:

场景中将会创建一个按钮,如下面的截图所示:

一个按钮将包含 Canvas、Button 和 Text 组件。如果你在一个已经存在 Canvas 组件的场景中创建按钮,它将直接使用当前 Canvas 并在其下方创建按钮。

我们有上一章中的 Canvas 组件。在 Hierarchy 窗口中点击按钮。

按钮包含 Rect Transform、Canvas Renderer、Image(脚本)和 Button(脚本)组件。我们已经遇到了 Rect Transform 和 Canvas Renderer:

你可以在 Image(脚本)组件中指定按钮的图像。要设置图像,你需要导入一个图像并将其转换为 Sprite 类型。你只能在将图像分配给 Sprite 类型后将其分配给源图像。

  • 颜色:你可以指定按钮颜色;否则,你可以将默认颜色设置为白色。

  • 材质:如果你想要分配一个特定的纹理,可以在这里完成。

  • Raycast Target:这允许目标可点击。

  • 图像类型:你可以使用原始图像,或者将其更改为平铺。

在按钮(脚本)部分的下一次,我们可以指定按钮在被点击后应该执行的操作。通过点击交互选项,我们指定按钮在被高亮、按下或禁用时将变为其他内容。

按钮被点击后改变的速度由 Fade Duration 参数指定。

文本(脚本)指定是否需要显示任何文本。如果文本是合适的,你可以在文本框中指定文本。否则,你可以将其留空。通常,最好有标准且易于理解的按钮:

组织文件夹结构

在本章的resources文件夹中,你会看到一些按钮的图像。将按钮拖放到名为buttons的文件夹中。

这也是开始组织你的文件夹的好时机。为了方便和个人精神健康,最好将资产放入文件夹:

我已经创建了以下文件夹结构的文件夹:

  • 3dAssets: 包含玩家 FBX,你也可以添加其他 3D 对象,如 3D 环境,和 3D 网格。

  • 动画控制器:我已经在这里放置了动画控制器。当你有很多敌人时,你可能需要为每个敌人分别创建单独的动画控制器,因此最好也为每个控制器创建一个单独的控制器。

  • 按钮:按钮的精灵可以放入这里。

  • 字体:虽然 Arial 是一种美丽的字体,但你可能想要选择一个更适合你游戏的字体。因此,创建一个文件夹来存放这些字体。

  • 材质:再次强调,更多的对象意味着更多的材质,因此要组织好!

  • 预制体:将所有预制体放入单独的文件夹,以便更容易访问。

  • 场景:场景是在创建游戏时最常访问的文件,因此将它们保存在单独的文件夹中以便于访问。

  • 脚本:随着时间的推移,你可能需要为不同的事情编写很多脚本,因此也为这些创建单独的文件夹。

当你打开按钮文件夹时,它看起来如下截图所示:

按钮图像的右侧边缘都有一个小箭头。这是因为它们已经被转换为精灵类型。要将图像转换为精灵,请选择图像并在检查器面板中,将纹理类型字段更改为精灵(2D 和 UI)选项:

为按钮文件夹中的所有按钮执行相同的操作。

在 startScene 中添加游戏重启按钮

现在在 startScene 中创建一个新的按钮,并将其重命名为 resetBtn,如下截图所示:

在检查器面板中,在源图像字段下,选择 resetBtn2 精灵类型:

将其余参数保持不变。你还会在检查器面板中看到重置按钮的预览以及其尺寸:

将按钮定位在 GAMEOVER!!!文本的正下方和右侧。通过这种方式,我们将使其对玩家的拇指可访问:

一旦定位,在游戏中应该看起来如下:

现在我们必须创建一个小脚本,以便当按钮被点击时,调用 startScene。为此,创建一个新的脚本,命名为 buttonClick,并将其保存在Scripts文件夹中。在脚本内部添加以下行:

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

    public class buttonClick : MonoBehaviour { 
        public void onButtonClick(string level) { 
            SceneManager.LoadScene(level); 
        } 
    } 

SceneManager.LoadScene函数负责在 Unity 中加载场景。

当按钮被点击时,我们将选择OnButtonClick函数,然后传入要加载的场景名称,这个字符串将被传递给LoadScene函数,该函数将加载场景本身。

现在通过 GameObject| CreateEmpty 创建一个空的游戏对象。将其命名为 buttonClickGo。这个游戏对象在场景中的位置无关紧要。现在将buttonClick脚本添加到这个游戏对象中,如下截图所示:

图片

在层次结构中选择resetBtn。在按钮的检查器面板中,您将在 Button (Script)部分下方看到一个 On Click ()部分:

图片

在第一个下拉菜单中,选择 Editor 和 Runtime 选项,以便在运行时可以在编辑器中调用该函数。

接下来,从下拉列表中选择 buttonClickGO 游戏对象,如下截图所示:

图片

然后,从下拉列表中选择buttonClick.onButtonClick函数。这位于 ButtonClick | onButtonClick (string):

图片

现在,输入字符串作为将被调用的场景名称--在这种情况下,我们想要调用 startScene:

图片

我们仍然需要在游戏结束前隐藏重置按钮。因此,在gameScript中创建一个公共的Button类型,命名为resetButton,如下所示:

    public Text enemyTextInstance; 
    public Text playerTextInstance; 
    public Text gameOverText; 
 public Button resetButton;

接下来,在Start函数中,我们将活动属性设置为false

    void Start () {  
        GameObject player = GameObject.Find("theDude"); 
        pScript = player.GetComponent<playerScript>(); 

        GameObject enemy = GameObject.Find("Enemy"); 
        eScript = enemy.GetComponent<enemyScript>(); 
        gameOverText.enabled = false; 

        resetButton.gameObject.SetActive(false); 
    } 

这将确保重置按钮最初不可见。最后,一旦游戏结束,将SetActive函数再次设置为true

    if (playerHealth <= 0 || enemyHealth <= 0) { 
        bGameover = true; 
        gameOverText.enabled = true; 
 resetButton.gameObject.SetActive(true); 

        Instantiate(particlePrefab); 
    }  

为了让 Unity 加载您想要的场景,您需要在构建设置选项中的 Scenes In Build 列表中添加它。因此,转到 File | Build Settings 并点击底部的 Add Open Scenes 按钮,如下截图所示:

图片

一旦场景被添加,关闭窗口。现在运行游戏,一旦游戏结束,您可以按重置按钮,游戏将重新开始。

在 startScene 中添加暂停按钮

由于我们处于 startScene,让我们添加另一种类型的按钮,称为切换按钮,用于暂停或恢复游戏。

前往 GameObject | UI 创建一个新的切换按钮。创建后,将其定位在游戏屏幕的右上角。将按钮重命名为 pauseBtn。

如同其他按钮和 UI 元素一样,它有一个 Rect Transform 部分,可以用来定位按钮:

图片

它还有一个 Toggle (Script),可以设置为默认值。选择 Background 选项卡,以选择用于按钮的背景图像。

在背景下的图像(脚本)中,选择 pauseBtnOff 精灵类型作为背景图像:

图片

对于任何文本脚本,还有一个标签页,您可以将其留空,因为我们只会使用图像。

每次点击按钮时,它都会在暂停图像上创建一个勾选标记,以指定按钮已被切换。如果您再次点击它,它将恢复到正常图像。

现在,在gameScript文件中,我们创建一个名为pauseButton的公共变量,类型为toggle,并创建一个名为bIsGamePausedpublic布尔变量,并将其初始化为false。我们将使用这个新创建的布尔变量来遍历玩家和 AI 更新循环。

因此,在gameScript中添加以下代码:

    public Text gameOverText; 
    public Button resetButton; 
 public Toggle pauseButton; public bool bIsGamePaused = false;

接下来,在Update函数中,将bIsGamePaused的值设置为pauseButtons.isOn变量的值。这将根据您是否点击了暂停按钮将值设置为truefalse

Update函数将如下所示:

    void Update () {     
        if (!bGameover) { 
 bIsGamePaused = pauseButton.isOn; 

            Debug.Log("isGamePaused: " + bIsGamePaused);        

            int playerHealth = pScript.health; 
            int enemyHealth = eScript.health; 

            // other code 
        } 
    } 

接下来,在enemyScript中,将Update函数括起来,并检查bIsGamePaused是否为false,方式与我们检查游戏是否结束类似:

    void Update () {    
        gameScript gScript = mainCamera.GetComponent<gameScript>(); 

        if (gScript.bGameover == false) 
        { 
            if (gScript.bIsGamePaused == false) 
            {
                punchTick--; 
                myTick++; 
                currentTick = myTick; 

                GameObject player = GameObject.Find("theDude"); 
                Animator pAnim = player.GetComponent<Animator>(); 
                playerScript pScript = player.GetComponent <playerScript>(); 

                // Other game code 
            } 
        } 
    } 

playerScript中也进行相同的操作:

    void Update () { 
        gameScript gScript = mainCamera.GetComponent<gameScript>(); 

        if (gScript.bGameover == false) 
        { 
            if (gScript.bIsGamePaused == false) 
            { 
                totalTime += Time.deltaTime; 

                GameObject enemy = GameObject.Find("Enemy"); 
                Animator eAnim = enemy.GetComponent<Animator>(); 
                enemyScript eScript = enemy.GetComponent
                                      <enemyScript>(); 

                // other game code 
            } 
        } 
    } 

现在,当您运行游戏时,您可以通过轻触暂停按钮来暂停游戏。

mainCamera中不要忘记将toggleBtn分配给gameScript脚本,因为它期望它与您附加resetBtn的方式相似:

图片

startScene中添加主菜单按钮

我们将创建一个主菜单按钮,以便在选中时可以按下它进入MainMenu场景。为此,我们将创建一个新的按钮并将其命名为mainMenuBtn。将mainMenuBtn定位在屏幕中间的左侧。

还需要在图像(脚本)部分选择 homeBtn2 选项作为源图像;并将其余值设置为默认值:

图片

与重置按钮一样,我们只能在游戏结束后显示主菜单按钮。因此,在gameScript中创建一个新的公共变量,名为mainMenuButton,如下所示:

    public Button resetButton; 
 public Button mainMenuButton; 
    public Toggle pauseButton; 

Start函数中将mainMenuButton的激活状态设置为false

    // Use this for initialization
    void Start () { 

        GameObject player = GameObject.Find("theDude"); 
        pScript = player.GetComponent<playerScript>(); 

        GameObject enemy = GameObject.Find("Enemy"); 
        eScript = enemy.GetComponent<enemyScript>(); 
        gameOverText.enabled = false; 

        resetButton.gameObject.SetActive(false); 
 mainMenuButton.gameObject.SetActive(false); 
    } 

当游戏结束时,将激活状态设置为true

    if (playerHealth <= 0 || enemyHealth <= 0) { 
        bGameover = true; 

        gameOverText.enabled = true; 
        resetButton.gameObject.SetActive(true); 
 mainMenuButton.gameObject.SetActive(true); 

        Instantiate(particlePrefab); 
        Debug.Log(" +++++ GAMEOVER +++++"); 
    } 

接下来,在mainMenu按钮对象中,转到按钮(脚本)标签,并在On Click ()标签中设置,以便在按钮被点击时调用mainMenuScene

图片

现在,mainMenuScene尚未创建,因此我们将创建一个mainMenuScene

创建MainMenu场景

在项目标签页中的场景文件夹中,右键单击并创建一个新场景:

图片

将场景命名为mainMenuScene。现在您将在项目的场景文件夹中有两个场景:

图片

双击mainMenuScene场景并打开它。这是一个新的空场景,因此我们现在将开始填充这个场景。

首先,我们将添加一个背景图片。转到 GameObject-> UI 创建一个 Image。它将创建一个新的画布和一个图片。保留画布值设置为默认值。

接下来,将图片重命名为bgImage。选择scenary-ipad文件作为背景图片,并点击设置原生大小选项,以便图片被调整大小。

接下来,在 titleText 中,我们可以添加游戏名称,我们将称之为 PunchyPunch。在文本框中,将文本重命名为 PunchyPunch。将字体从 Arial 更改为也是一个好主意。在Resources文件夹的此章节中,你可以找到一个 duncecapbb 字体。将字体复制并粘贴到Assets/fonts文件夹中。在 Character 部分,在 Font 选项中,选择 duncecapbb_re 作为字体。

最后,更改颜色,使其成为浅蓝色色调:

图片

在这些添加之后,MainMenu 场景应该开始成形,看起来如下所示:

图片

保存此场景。

接下来,为了确保在 startScene 中点击 mainMenuBtn 按钮时调用 mainMenuScene,我们必须将这些场景添加到构建设置中,因此通过进入文件并点击添加打开场景打开构建设置菜单:

图片

现在,打开 startScene 并点击播放按钮。一旦游戏结束,mainMenuBtn 按钮将出现。点击它,应该加载 MainMenu 场景。

现在,让我们向 mainScene 添加一个播放按钮,该按钮将调用 startScene。因此,再次打开 mainMenuScene。

前往 GameObject | UI 创建一个按钮。将此按钮命名为playBtn。它将在当前画布下创建。

将播放按钮放置在屏幕中心。在 Image (Script)中,对于 Source Image 字段,选择 playBtn_normal 选项。在 Button (Script)部分,选择 Sprite Swap 选项的 Transition type 字段。我们将交换图片以显示按钮被按下。

在目标图形字段中选择 platBtn (Image);在突出显示精灵字段中选择 playBtn_normal;在按下精灵字段中选择 playBtn_pressed;在禁用精灵字段中选择 playBtn_normal(或者你也可以留空,因为我们永远不会禁用此按钮):

图片

在添加播放按钮后,主菜单场景应该看起来如下所示:

图片

要使按钮交互式,我们必须创建一个空对象并将buttonClick脚本附加到它上。因此,创建一个空对象,命名为buttonClickGO,并将buttonClick脚本附加到它上。

现在,在playBtn游戏对象的 OnClick()部分,选择 buttonClickGO,调用onButtonClick函数,并将 startScene 变量传递给它:

图片

现在,当你点击播放按钮时,它将通过加载 startScene 来加载游戏。

我还创建了一个名为options的另一个场景。选项场景将包含静音或降低游戏音量的功能。

为了做到这一点,需要在MainMenu场景中再添加一个按钮,因为这些场景只能从MainMenu场景访问。此外,我们还将添加一个按钮,用于打开成就窗口。按钮图片已提供在资源文件夹中,您可以使用这些图片并将它们放置在MainMenu场景的任何位置,如下面的截图所示:

图片 _06_032

同样的buttonClickGO被使用,所以当点击任意一个按钮时,它将带您进入相应的场景。

显然,已经创建了一个场景,名为optionsScene。现在Scenes项目文件夹应该看起来如下:

场景图片

下面是选项场景。目前这里没有什么,但在下一章,当我们添加音频时,我们也将在这里创建一个静音按钮:

图片 _06_035

摘要

在本章中,我们学习了如何通过点击按钮来创建新场景并在场景之间进行切换...字面意义上!我们添加了按钮来启动游戏和在场景之间旅行。

在下一章中,我们将大量使用按钮,我们将看到如何添加成就、应用内购买和集成。几乎所有这些都将包括添加按钮。我们还将了解如何添加不同类型的按钮。

第七章:游戏统计数据、社交、内购和广告集成

由于我们已经完成了游戏玩法,现在我们可以看看如何在设备上运行应用程序。我们还介绍了已包含的 Unity Ads 和内购工具。我们还将包括游戏成就,最后,我们将看到如何上传 APK 到应用商店并发布游戏。

本章包括以下主题:

  • 在设备/模拟器上运行应用程序

  • Android 开发者控制台

  • 添加游戏成就

  • 保存游戏统计数据

  • 广告集成

  • 内购购买

  • 添加社交媒体集成

在设备/模拟器上运行应用程序

要在设备上运行应用程序,您必须获取 Android SDK 和 Java 开发工具包(JDK),并在 Unity 中设置 SDK 的位置。

让我们先下载 Android SDK。您可以从developer.android.com/studio/index.html下载 SDK。滚动到页面底部并下载命令行工具。您也可以下载 Android Studio,但仅当您打算使用 Android Studio 进行开发时,在这种情况下我们并不需要:

图片

为您的操作系统下载它。在我的情况下,我将向您展示如何在 Windows 上操作。

下载完文件夹后,在C:驱动器上创建一个新的文件夹,命名为AndroidSDK,并将Tools文件夹复制进去。在Tools文件夹中,右键单击Android.bat文件,并以管理员身份运行。

您将看到一个如下所示的界面。在Tools文件夹下的包中,您需要通过选择相应的复选框来下载 Android SDK 工具、Android SDK 平台工具和 Android SDK 构建工具,如下所示:

图片

在 Android SDK 版本中,选择运行在您手机上的版本。如果您有 Google Pixel,您可能运行的是 Android 7.1 或 7.0 版本。我有一部 Google Nexus 5 手机,它运行的是 Android 6.0 版本,因此我需要安装它。如果您运行的是较旧的 Android 版本,请检查您的设备需要哪个版本的 SDK:

图片

接下来,您还需要在“额外”文件夹下安装一些东西,这些内容稍后无论如何都是必需的。您需要从列表中检查 Android 支持库、Google Play 服务、Google 仓库和 Google USB 驱动器选项,以连接您的设备并开始安装:

图片

选择所有选项,接受条款,然后点击安装按钮以开始安装:

在安装过程中,我们还可以下载 JDK。要下载 JDK,请访问此链接 www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 并下载适用于您的操作系统的版本。

图片

没有必要下载演示和示例。一旦下载,就在默认位置安装它。

你还需要下载 Unity Android 模块。转到文件 | 构建设置,然后在平台部分选择 Android。它将显示未加载 Android 模块。点击打开下载页面。点击按钮后,下载应该开始,下载完成后,它应该位于下载文件夹中。你也可以从download.unity3d.com/download_unity/38b4efef76f0/TargetSupportInstaller/UnitySetup-Android-Support-for-Editor-5.5.0f3.exe下载。确保将版本号改为你当前运行的版本。下载完成后,打开 Unity 项目,然后双击 Android 模块以安装所需的包。

现在转到构建设置并选择 Android 平台,然后点击底部的切换平台按钮:

图片

现在,我们必须在 Unity 中设置AndroidSDK文件夹和 JDK 的路径。转到编辑菜单,然后在外部工具选项下打开首选项,然后浏览到 SDK 和 JDK 的位置:

图片

Android SDK 的位置在C:/ AndroidSDK,而对于 JDK,它位于C:/Program Files/Java/jdk1.8.0_121

现在,我们必须准备 Android 设备,启用设备中的开发者选项模式,并启用 USB 调试。在设备上,转到设置和关于手机,底部你会找到构建号,连续点击七次。然后它会告诉你你现在是一名开发者了:

图片

按下返回键进入开发者选项。启用 USB 调试和保持唤醒选项:

图片

这样就完成了。现在你的设备已经准备好在设备上构建了。

Android 开发者控制台

现在,为了测试/发布你的游戏或添加成就,你需要将你的应用到 Android 的开发者控制台上传。它有一个一次性费用为 25 美元。一旦支付,你可以发布和测试你想要的任何游戏/应用。

如果你准备好了 Gmail ID,前往play.google.com/apps/publish/signup/#注册为 Android 开发者。以下窗口将显示:

图片

点击继续支付按钮并准备好你的信用卡。一旦支付完成,你将能够访问开发者控制台。恭喜!你现在是一名 Android 开发者了。

你将看到以下屏幕。我已经在 Android 商店有一些游戏和应用程序,所以你的现有应用程序将显示在这里:

图片

当我们在这里时,让我们创建一个新的应用。点击右上角的+创建应用按钮:

图片

在对话框中,指定游戏的语言和标题。接下来在标题和简短描述字段中指定详细信息,然后点击窗口右上角的保存草稿按钮:

图片

接下来点击左侧的 APK 选项卡,因为我们需要上传 APK 来添加成就:

图片

点击上传您的第一个生产 APK。现在我们必须将 APK 上传到页面:

图片

让我们回到 Unity 并构建 APK,以便我们可以将其上传到开发者控制台。在 Unity 中,转到编辑 | 构建设置,然后点击 PlayerSettings 选项。

在 PlayerSettings 下,在 公司名称 和 产品名称 字段中输入详细信息。现在不用担心图标;我们稍后会添加它们。

在其他设置部分,添加一个捆绑标识符。这通常是公司网站的反序,后面跟着产品名称。添加版本号,为 1.0。捆绑版本代码可以是 1。最后添加应用程序可以支持的最低 Android API 级别。我选择了 Marshmallow,但数字越低越好,这样应用程序就可以被使用较老手机和较老 Android 版本的用户享受:

图片

接下来在发布设置部分,我们需要创建一个新的密钥库。在密钥库密码字段中输入密码,并确认密码。现在点击浏览密钥库按钮,选择您想要存储密钥库的位置。请将其保存在安全的地方,因为您可能稍后需要此文件。

接下来在密钥部分,不要选择未签名的(调试)选项,而是点击它并选择创建新密钥选项:

图片

接下来填写所需的详细信息:

图片

创建一个别名,通常是产品的名称。为密钥创建一个密码,然后确认它。添加您的名字、姓氏、公司地址和公司名称、城市、州和位置国家。

现在选择别名并输入密钥的密码:

图片

现在我们可以创建一个 APK 来上传。转到构建设置并点击构建。它将询问保存 APK 的位置,指定位置和名称。记住位置和名称:

图片

现在回到开发者控制台。将 build.apk 文件拖放到“在此处放下您的 APK 文件”或选择文件的位置,或者选择文件。选择文件后,它将开始上传:

图片

现在,如果您转到 ALL APPLICATIONS 页面,您将看到我们的 Punchypunch 应用程序列在列表中:

图片

现在让我们添加一些成就。

在游戏中添加成就

在开发者控制台中,点击 GAME SERVICES 选项卡:

图片

在右上角点击 + 添加新游戏。输入游戏名称和类型,然后点击屏幕底部的继续按钮:

图片

点击“成就”标签页,然后点击“添加成就”标签页。您至少需要添加五个成就才能使其工作。

图片

输入一个名称并添加简短描述。在我们的例子中,我将为用户玩游戏次数的成就添加成就。因此,对于第一次、第五次、第五十次、第一百次和第一千次玩游戏,我想要每次都弹出成就:

图片

接下来,添加一个新成就以及以下所有成就:

图片

点击底部的“获取资源”按钮,并在窗口中选择“安卓”标签。现在,复制所有数据:

图片

接着转到“测试”标签页,点击“添加测试者”按钮。在此处输入您将用于登录设备的电子邮件地址:

图片

现在,您已准备好在设备上测试成就。转到 Unity,转到“窗口”|“谷歌播放服务”|“设置”|“安卓设置...”:

图片

在窗口中,让“保存常数的目录”字段设置为Assets文件夹。

在“常量类名组件”中输入详细信息,并将从“获取资源”标签页复制的代码粘贴到这里,然后点击屏幕底部的“设置”按钮:

图片

现在当主菜单加载时,我们必须激活谷歌播放服务,并且每当达到一个成就时,我们必须发送一个通知告诉谷歌播放服务该成就已达成。

因此,在项目菜单中打开MainMenu场景。在Project/Scripts文件夹中创建一个名为MainMenuScript的新脚本。在脚本中,添加以下代码:

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

    using GooglePlayGames; 
    using UnityEngine.SocialPlatforms; 
    using GooglePlayGames.BasicApi; 

    using UnityEngine.UI; 

    public class mainMenuScript : MonoBehaviour 
    { 
        bool isUserAuthenticated = false; 

        // Use this for initialization 
        void Start(){ 
            PlayGamesPlatform.Activate(); 
            PlayGamesPlatform.DebugLogEnabled = true; 
        } 

        // Update is called once per frame 
        void Update() 
        { 
            if (!isUserAuthenticated){ 
                Social.localUser.Authenticate((bool success) => { 
                    if (success){ 
                        Debug.Log("You've successfully logged in"); 
                        isUserAuthenticated = true; 
                    } else { 
                        Debug.Log("Login failed for some reason"); 
                    } 
                }); 
            } 
        } 
    }  

在类顶部添加GooglePlayGamesUnityEngine.SocialPlatformSystem.Collections.Generic命名空间。在类中创建一个名为isUserAthenticated的布尔值,并将其初始化为false。在Update函数中,我们将检查用户是否已登录谷歌播放服务;否则,我们将等待用户登录。

我们激活谷歌播放服务并启用调试模式。在Update函数中,我们检查用户是否已登录。如果用户已登录,我们将布尔变量更改为true,否则我们将注销并说明用户未登录。将此脚本作为组件附加到场景中的MainCamera组件。

现在,将您的设备连接到计算机,转到“构建设置”并按“构建和运行”。它将连接到谷歌播放服务,如下面的截图所示:

图片

它将要求您使用电子邮件地址登录:

图片

一旦您登录,它将显示欢迎信息:

图片

现在为了在游戏结束条件下存储实际达成的成就信息,添加以下内容:

if (playerHealth <= 0 || enemyHealth <= 0) { 
    bGameover = true; 

    gameOverText.enabled = true; 
    resetButton.gameObject.SetActive(true); 
    mainMenuButton.gameObject.SetActive(true); 

    Instantiate(particlePrefab); 
    gameplayCount++; 

    if (gameplayCount == 1){ 
        Social.ReportProgress (PunchyPunchAchievements. achievement_played_first_time, 100, (bool sucsess) => { }); 
    } else if (gameplayCount == 5) { 
        Social.ReportProgress (PunchyPunchAchievements. achievement_played_5_times, 100, (bool sucsess) => { }); 
    } else if (gameplayCount == 50) { 
        Social.ReportProgress (PunchyPunchAchievements. achievement_played_50_times, 100, (bool sucsess) => { }); 
    } else if (gameplayCount == 100) { 
        Social.ReportProgress (PunchyPunchAchievements. achievement_played_100_times, 100, (bool sucsess) => { }); 
    } else if (gameplayCount == 1000) {
        Social.ReportProgress (PunchyPunchAchievements. achievement_played_1000_times, 100,(bool sucsess) => { }); 
    } else {
        ...   
    } 
    Debug.Log(" +++++ GAMEOVER +++++"); 
} 

在类中添加一个名为gameplayCount的全局整数。当游戏结束时,此变量会增加。

根据gameplayCount变量的值,执行if语句并调用Social.ProgressReport函数。它接受三个参数。第一个是存储在创建的类中的成就名称,第二个是进度级别,在这种情况下,当成就完成 100%时我们将调用它,第三个是回调函数。

现在再次构建并运行游戏。现在当你完成游戏时,你将收到你第一次玩游戏的通知。

接下来,我们将打开成就窗口以查看所有成就。在MainMenu场景中,我们有一个创建的按钮来显示成就。

buttonClick类中,创建一个名为openAchievements的新函数。它创建如下:

    public void openAchievements() { 
        Social.localUser.Authenticate((bool success) => { 
            if (success){ 
                Debug.Log("You've successfully logged in"); 
                Social.ShowAchievementsUI(); 
            } else { 
                Debug.Log("Login failed for some reason"); 
            } 
        });    
    } 

Social.showAchievementsUI()函数打开成就窗口。现在在成就按钮点击时调用此函数:

再次构建并运行你的项目,然后在主菜单上点击成就按钮,成就窗口将弹出,如下所示:

但这里有一个小问题。游戏只在你玩游戏时记住所有信息;一旦你关闭游戏,它就会忘记信息。现在让我们看看如何将游戏信息存储在设备上。

保存游戏统计数据

保存游戏信息实际上非常简单。这种方法适用于所有设备。PlayerPrefs函数可以在系统中保存和加载玩家信息。你只需要传递一个键,你将保存和检索数据,以及一个你想要存储的值。

所以在你的gameScript中,当你增加gameplayCount变量的值并在其后添加以下代码时:

    PlayerPrefs.SetInt("GameplayCount", gameplayCount); 

现在gameplayCount的值将被保存在GameplayCount键中。

要检索信息,你将使用PlayerPrefsGetInt函数来获取存储在键中的值。因此,在你增加gameplayCount变量之前,添加以下代码:

    int gameplayCount = PlayerPrefs.GetInt("GameplayCount"); 

现在系统存储在GameplayCount键中的值被检索并存储在一个名为gameplayCount的局部变量中。

你现在正在增加这个值,然后将新的值保存在系统中。现在系统将记住你玩游戏的次数。

为了方便起见,我在选项菜单中创建了一个按钮,使用它可以每次按下按钮时将GameplayCount键的值重置为0

我还添加了一个主页按钮,这样我就可以回到主菜单。现在让我们看看如何添加广告集成。

广告集成

在 Unity 中,使用 Unity Ads 集成广告非常简单。在 Unity 中,转到“窗口”|“服务”,右侧将打开一个新标签页:

在“服务”标签页中,使用你在 Unity 注册时获得的 Unity 登录名和密码登录。

点击创建按钮。这将显示您可用的不同服务:

点击 SERVICES 选项卡:

在右上角翻转开关以启用它。如果游戏针对 13 岁以下的儿童,请点击复选框并点击继续按钮:

选择 Android 作为目标平台,并勾选启用测试模式选项,这样我们可以在发布游戏之前测试它。现在,在gamescript类中,在类顶部添加以下代码:

    using UnityEngine.Advertisements;   

我们所需要做的就是在我们想要显示广告时调用ShowAd函数。我们不希望在玩家有成就时显示广告,因为我们也不希望在显示成就时每次调用ShowAd函数的else语句中显示广告:

    if (gameplayCount == 1) { 
        Social.ReportProgress (PunchyPunchAchievements. 
        achievement_played_first_time, 100, (bool sucsess) => { }); 
    } 
    else if (gameplayCount == 5) { 
        Social.ReportProgress (PunchyPunchAchievements. 
        achievement_played_5_times, 100, (bool sucsess) => { }); 
    } 
    else if (gameplayCount == 50) { 
        Social.ReportProgress (PunchyPunchAchievements. 
        achievement_played_50_times, 100, (bool sucsess) => { }); 
    }
    else if (gameplayCount == 100) { 
        Social.ReportProgress (PunchyPunchAchievements. 
        achievement_played_100_times, 100, (bool sucsess) => { }); 
    } 
    else if (gameplayCount == 1000) { 
        Social.ReportProgress (PunchyPunchAchievements. 
        achievement_played_1000_times, 100, (bool sucsess) => { }); 
    } else { 
        if (gameplayCount % 3 == 0){ 
            ShowAd(); 
        } 
    }  

如果gameplayCount能被 3 整除,我们就调用显示广告函数。然后,所有的魔法都在ShowAd函数内部发生。

创建ShowAd函数如下:

    public void ShowAd() 
    { 
        if (Advertisement.IsReady()) 
        { 
            Advertisement.Show("video", new ShowOptions() { 
                resultCallback = adViewResult}); 
        } 
    }  

有时广告可能暂时不可用,可能是在玩游戏或重新启动游戏时广告开始播放。我们不希望这样,所以我们首先检查是否有可显示的广告,因此我们调用Advertisement.Isready

如果有准备好的广告,我们调用Advertisement.Show函数,并指定我们想要显示的广告类型,即视频。我们添加一个回调函数,它会告诉我们玩家在观看广告时做了什么,或者广告是否播放过。

因此添加一个名为adViewResult的新函数,如下所示:

    public void adViewResult(ShowResult result) { 
        switch (result) { 
            case ShowResult.Finished: 
                Debug.Log(" Player viewed complete Ad"); break; 
            case ShowResult.Skipped: 
                Debug.Log(" Player Skipped Ad "); break; 
            case ShowResult.Failed: 
                Debug.Log("Problem showing Ad "); break;             
        } 
    } 

我们检查广告是否播放完毕、跳过或失败。在每种情况下,我们记录信息。构建并运行游戏。现在测试广告应按照我们的代码显示:

让我们看看如何添加应用内购买,这样如果玩家想要禁用广告,他们可以通过购买来禁用广告。

应用内购买

在 Unity 的Services选项卡中,点击In-App Purchasing组件:

再次翻转右上角的开关以启用 IN-APP PURCHASING:

点击导入按钮导入 IAP 库:

接下来,我们创建一个新的类来处理应用内购买;我们将把这个类称为IAPManager

在这里添加以下代码。代码是从 Unity 示例网站复制的,它包含详细且带注释的代码,展示了每个函数的作用。代码可以从unity3d.com/learn/tutorials/topics/ads-analytics/integrating-unity-iap-your-game访问:

代码已被修改以符合我们的目的。由于这是一段较长的代码,我添加了带编号的注释,稍后我会解释:

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

    public class IAPManager : MonoBehaviour, IStoreListener 
    { 
        public static IAPManager instance { set; get; } 

        private static IStoreController m_StoreController;              
        private static IExtensionProvider m_StoreExtensionProvider;  

        public static string kRemoveAds = "removeads"; // 1 

        void Awake() { 
            instance = this; 
        } 

        void Start(){ 
            if (m_StoreController == null){ 
                InitializePurchasing(); 
            } 
        } 

        public void InitializePurchasing(){ 
            if (IsInitialized()){ 
                return; 
            } 
            var builder = ConfigurationBuilder.Instance (StandardPurchasingModule.Instance()); 
            builder.AddProduct(kRemoveAds, ProductType.Consumable);// 2 
            UnityPurchasing.Initialize(this, builder);  
        } 

        private bool IsInitialized(){ 
            return m_StoreController != null && m_StoreExtensionProvider != null; 
        } 

        public void BuyRemoveAds(){ 
            BuyProductID(kRemoveAds); 
        } //3 

        void BuyProductID(string productId){ 
            // If Purchasing has been initialized ... 
            if (IsInitialized()){ 
                Product product = m_StoreController. 
                products.WithID(productId); 

                if (product != null && product.availableToPurchase){ 
                    Debug.Log(string.Format("Purchasing product 
                    asychronously: '{0}'", product.definition.id)); 
                    m_StoreController.InitiatePurchase(product); 
                } 
                else { 
                    Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase"); 
                } 
            } else { 
                Debug.Log("BuyProductID FAIL. Not initialized."); 
            } 
        } 

        public void RestorePurchases() 
        { 
            // If Purchasing has not yet been set up ... 
            if (!IsInitialized()){ 
                Debug.Log("RestorePurchases FAIL. Not initialized."); 
                return; 
            } 

            if (Application.platform == RuntimePlatform.IPhonePlayer || 
                Application.platform == RuntimePlatform.OSXPlayer) { 

                Debug.Log("RestorePurchases started ..."); 
                var apple = m_StoreExtensionProvider. GetExtension<IAppleExtensions>(); 
                apple.RestoreTransactions((result) => { 

                Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore."); 
            }); 
        } else { 
            Debug.Log("RestorePurchases FAIL. Not supported on this 
                       platform. Current = " + Application.platform); 
        } 
    } 

    // --- IStoreListener 
    public void OnInitialized (IStoreController controller, 
                               IExtensionProvider extensions) { 
        Debug.Log("OnInitialized: PASS"); 
        m_StoreController = controller; 
        m_StoreExtensionProvider = extensions; 
    } 
    public void OnInitializeFailed(InitializationFailureReason error){ 
        Debug.Log("OnInitializeFailed InitializationFailureReason:" + 
                   error); 
    } 

    public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs args) { 
        if (String.Equals(args.purchasedProduct.definition.id, kRemoveAds, StringComparison.Ordinal)){ 
            Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id)); 

            PlayerPrefs.SetInt("noads", 1); //4 
            mainMenuScript.noAdsButton.gameObject.SetActive(false); 
        } else { 
            Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id)); 
        } 
        return PurchaseProcessingResult.Complete; 
    } 

    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason) { 
        Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: 
                                 '{0}', PurchaseFailureReason: {1}", 
                                  0 product.definition.storeSpecificId, 
                                  failureReason)); 
        } 
    } 

产品可以分为三种类型:消耗品、非消耗品和订阅:

  • 消耗品只能使用一次,之后就不能再次购买了

  • 非消耗品可以反复购买

  • 订阅产品是基于订阅的,类似于 Netflix,你每个月都要支付费用。

在设置产品时,有四个关键步骤需要记住。它们在代码中注释为 1、2、3 和 4。

首先,我们设置一个字符串,它应该与我们设置在 Android 商店中的相同。

在初始化的 Purchasing 函数中,我们必须指定我们的产品,因为当商店建立时,产品需要存在。每次点击商店图标时都会建立商店。

构建器会将产品添加到商店中。在这里指定产品名称和产品类型:

    builder.AddProduct(kRemoveAds, ProductType.Consumable); 

我们将创建自己的函数,当我们要购买产品时将被调用:

    public void BuyRemoveAds() { 
        BuyProductID(kRemoveAds); 
    }  

这个实习生将调用 BuyProductID 函数,我们将传递产品名称,这将启动产品的购买。

最后,在初始化函数中,我们检查产品是否已购买。我们设置一个键,如果产品已购买,则不再显示无广告按钮:

    if (String.Equals(args.purchasedProduct.definition.id, kRemoveAds, 
                      StringComparison.Ordinal)) { 
        Debug.Log(string.Format("ProcessPurchase: PASS. Product: 
        '{0}'", args.purchasedProduct.definition.id)); 

        PlayerPrefs.SetInt("noads", 1); //4 
        mainMenuScript.noAdsButton.gameObject.SetActive(false); 
    }

现在在 MainMenu 场景中,在右上角创建一个新按钮,并在 buttonClick 脚本中添加一个函数,该函数将调用 IAPManager 类中的 BuyRemoveAds 函数:

    public void noAdsButton() { 
        IAPManager.instance.BuyRemoveAds(); 
    } 

还在 mainMenu 类中添加一个用于广告移除按钮的按钮对象,如果 noads 键等于 1,则禁用它。

mainMenu 脚本应如下所示:

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

    using GooglePlayGames; 
    using UnityEngine.SocialPlatforms; 
    using GooglePlayGames.BasicApi; 

    using UnityEngine.UI; 

    public class mainMenuScript : MonoBehaviour 
    { 
        bool isUserAuthenticated = false;  
        public static Button noAdsButton; 

        // Use this for initialization 
        void Start() 
        { 
            Debug.Log("[Application Launch] Awake"); 

            PlayGamesPlatform.Activate(); 
            PlayGamesPlatform.DebugLogEnabled = true; 

            int value = PlayerPrefs.GetInt("noads"); 
            if (value == 1) { 
                noAdsButton.gameObject.SetActive(false); 
            } 

        } 

        // Update is called once per frame 
        void Update() 
        { 
            if (!isUserAuthenticated) { 
                Social.localUser.Authenticate((bool success) => { 
                    if (success){ 
                        Debug.Log("You've successfully logged in"); 
                        isUserAuthenticated = true; 
                    } else { 
                        Debug.Log("Login failed for some reason"); 
                    } 
                }); 
            } 
        } 

我们必须做最后一件事。实际上,我们需要在开发者控制台中添加产品。转到所有应用 | PunchyPunch,然后在列表中点击“内购产品”选项。现在点击“+ 添加新产品”按钮:

在框中,选择“管理产品”选项,并在产品 ID 字段中添加 removeads。这与我们在 IAPManager 类中设置的字符串相同:

接下来,在标题和描述字段中分别添加标题和描述:

接下来,在底部点击“添加价格”按钮,并在默认价格字段中输入价格。在我的情况下是 INR,所以我将添加一个适当的值并点击“应用”按钮:

现在,应用程序将显示在“内购产品”中:

您现在可以构建和运行应用程序,但只有在应用程序发布后才能测试内购功能。所以我们将看看它在下一章是如何工作的。

添加社交媒体集成

首先,让我们设置一个 Facebook 分享集成。转到 developers.facebook.com/,这将打开 Facebook 开发者网站。使用您的 Facebook 登录名和密码登录:

点击顶部的“我的应用”框,并选择“添加新应用”选项:

在显示名称字段中输入游戏的名称。在类别列表中选择“页面应用”,并添加你的联系电子邮件地址。然后点击创建应用 ID 按钮。

在设置中,点击底部的+添加平台按钮并选择 Android。添加应用的包名,并在类名字段中添加,它也将以反向网站名结尾,以类名结尾。当我们创建 Unity 中的管理器时,必须确保类名与此匹配:

图片

你还会得到一个应用 ID,复制此信息,因为这将需要。接下来在右上角,有一个名为 DOCs 的按钮,点击它,因为我们需要下载 Unity 的 Facebook SDK。从列表中选择 Unity SDK,SDK 将开始下载:

图片

SDK 下载完成后,请确保你的当前 Unity 项目已打开,并导入该包:

图片

选择所有复选框并点击导入按钮:

图片

你会看到在你的项目中有一个新的 Facebook 选项卡,点击它并选择编辑设置选项:

图片

在检查器类型中,应用程序名称(可选)和应用程序 ID[?]字段与 Facebook 开发者控制台上的显示一致:

图片

接下来,创建一个名为fbManager的新类。在类中添加以下代码:

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

    using Facebook.Unity; 
    using System.Linq; 

    public class fbManager : MonoBehaviour 
    { 
        void Awake() { 
            if (!FB.IsInitialized) { 
                FB.Init(); 
            } else { 
                FB.ActivateApp(); 
            } 
        } 

        public void Share() { 
            if (FB.IsLoggedIn) 
            { 
                FB.ShareLink(contentTitle: "Growl Games Studio", 
                    contentURL: new System.Uri  
                    ("http://www.growlgamesstudio.com"), 
                    contentDescription: "Like and Share my page", 
                    callback: onShare);  
            } else { 
                // Debug.Log("User Cancelled Login"); 
                FB.LogInWithReadPermissions(null, callback: onLogin); 
            }         
        } 

        private void onLogin(ILoginResult result) { 
            if (result.Cancelled) 
            { 
                Debug.Log(" user cancelled login"); 
            } 
            else { 
                Share(); 
            } 
        } 

        private void onShare(IShareResult result) { 
            if (result.Cancelled || !string.IsNullOrEmpty 
               (result.Error)) 
            { 
                Debug.Log("sharelink error: " + result.Error); 
            } else if(!string.IsNullOrEmpty(result.PostId)) { 
                ... 
            } 
        } 
    } // class 

Awake函数中,首先检查 Facebook SDK 是否已初始化。如果没有,则初始化它并激活应用程序。

Share函数是我们将在主菜单中点击 Facebook 按钮时调用的函数。

一旦点击按钮,函数将检查用户是否已登录。如果已登录,则使用sharelink函数创建帖子。我们传递内容标题、网站链接和描述。我们还提供了一个回调函数,用于检查帖子是否已创建。

如果用户未登录,则使用loginWithPermissions函数进行登录。我们还传递了一个回调函数,用于检查用户是否已登录。一旦用户登录,函数将调用分享函数。

在分享过程中,如果取消分享,onShare回调函数将指定是否存在错误,否则将发布一条消息。现在在MainMenu中创建一个新的按钮用于 Facebook 分享,并在按钮被点击时调用Share函数。

你还需要安装OpenSSL,因为它是 Facebook 的要求。从code.google.com/archive/p/openssl-for-windows/downloads下载并安装 OpenSSL。下载、解压并安装它。接下来,转到开始并搜索“环境变量”。打开它。

在“系统变量”部分,查找路径并点击编辑。

在“编辑环境变量”下,点击新建并输入 OpenSSL 二进制位置。点击确定。然后再次点击新建,并添加 JDK 二进制位置。点击确定并退出:

图片

你可能会遇到构建错误,因为当你安装 Facebook SDK 时,可能存在较旧的support-annotations-23.4.0.jarsupport-v4-23.4.0.aar库版本。你必须手动进入 Facebook Android 库文件夹并删除这些文件。所以前往该位置并删除文件:

现在构建并运行应用程序,然后点击你创建的主菜单上的 Facebook 按钮。一旦登录,你就可以在 Facebook 上发帖了:

接下来我们将查看 Twitter 分享。Twitter 分享非常简单。在MainMenu上再创建一个按钮来调用 Twitter 分享,并附加一个在按钮被点击时将被调用的函数。我在buttonClick类中创建了一个名为openTwitter的函数,如下所示:

    public void openTwitter() {
        string appStoreLink = 
        "https://play.google.com/store/apps/details? 
        id=com.growlgamesstudio.pizZapMania"; 

        string twitterAddress = "http://twitter.com/intent/tweet"; 
        string descriptionParameter = "Punchy Punch"; 
        string message = "GET THIS AWERSOME GAME";//text string 

        Application.OpenURL(twitterAddress + "?text=" + 
            WWW.EscapeURL(message + "n" + descriptionParameter + "n" 
            + appStoreLink)); 
    }  

你将创建字符串来存储应用商店链接、Twitter 推文意图地址链接、描述和消息的值。然后你将调用Application.OpenURL并将信息作为字符串传递。

现在,当你构建应用程序并点击 Twitter 按钮时,你将能够分享一条推文:

摘要

在本章中,我们添加了声音和触摸输入,因为我们一直只使用鼠标点击。

我们还创建了一个开发者账户,并使应用程序本身准备就绪。我们看到了如何在设备上运行应用程序。我们还创建了商店中的应用程序,并将 APK 上传到商店。我们通过保存游戏游玩次数来添加成就。我们添加了内购和广告,以便我们可以使游戏货币化。在下一章中,我们最终将发布游戏。

第八章:声音,收尾工作和发布

在本章中,我们将添加声音,并了解如何使同一应用程序在不同的 Android 设备分辨率上运行。我们将优化 APK,使其文件大小更小。然后,我们将通过添加图标来准备应用程序本身以供发布。我们还将通过添加图标和截图来准备应用商店本身,最后点击发布按钮,以便发布应用程序。

本章包括以下主题:

  • 添加声音

  • 处理多个分辨率

  • 优化 APK 大小

  • 准备发布

  • 发布游戏

添加声音

为了添加声音,本章的Resources文件夹中包含了文件。总共有三个文件,其中两个是音效,一个是音乐文件。音效将用于游戏中。

当敌人或玩家被击中时,将播放打击声音。当玩家或敌人阻挡攻击时,将播放阻挡声音。bgMusic文件是在任何场景加载时要播放的背景音乐文件。将Audio文件夹拖入项目。

现在打开MainMenu场景。并将bgMusic文件拖入场景。看看检查器面板:

背景音乐的放置位置无关紧要,但如果你使用声音作为环境音,请确保设置声音源的位置。

默认情况下,已勾选“Play On Awake”选项,这正是我们想要的。然而,我们还想让音乐循环播放,所以请确保也勾选了“Loop”复选框。

如果你播放场景,你会自动听到声音播放。同样,对于游戏和选项场景也要这样做。让我们看看如何添加打击和阻挡音效。

每次我们点击按钮时,都给玩家提供音频反馈是个好主意,这样他们就知道按钮已被点击。

为了做到这一点,将打击音频效果也拖放到场景中。确保不要在效果上勾选任何选项,如“Play On Awake”,因为它会在场景开始时开始播放,而我们不希望这样:

我们将通过脚本播放声音。打开buttonClicks.cs文件。在类的顶部创建一个名为punchSound的公共变量,其类型为AudioSource,如下面的代码所示:

    public class buttonClick : MonoBehaviour {  
 public AudioSource punchSound; 
        public IAPManager iapManager; 

前往脚本附加的 GameObject,并将打击效果附加到打击音频源:

现在,你可以将音频源调用到你想播放的任何地方。假设,我们在onButtonClick类中调用函数,它加载一个新的场景:

    punchSound.Play();

但是,如果你添加它,你将不会听到场景变化时的声音,甚至游戏场景加载时你也不会听到打击音效。因此,我们将对函数做一些修改。

我们将使用协程等待一段时间,然后加载场景,以便播放音效:

    public void onButtonClick (string level){  
        punchSound.Play();  
 StartCoroutine (onSceneLoad(level)); 
    } 

    IEnumerator onSceneLoad (string sceneName) { 
 yield return new WaitForSeconds(0.5f); 
        SceneManager.LoadScene(sceneName); 
    }  

因此,在onButtonClick函数中,我们启动一个新的协程,并传入我们想要调用的函数。然后我们创建一个需要返回IEnumerator的函数。

然后,我们等待半秒钟,调用SceneManager.LoadScene函数,然后传入要加载的水平名称字符串。

这也可以在按下成就、Facebook 和 Twitter 按钮时对其他函数进行操作。我将把这个留给你,因为你会通过使用它来获得锻炼。这也需要在其他场景的其他按钮上做同样的事情。

现在,让我们看看如何在游戏过程中添加声音效果。在 startScene 中,将两个声音效果加载到场景中,并禁用两个文件的唤醒时播放。在 playerScript 中,为打击和阻挡声音效果添加公共变量:

    float totalTime = 0.0f; 
    float timeSinceLastHit = 0.0f; 
    float hitTimeInterval = 30.0f * .016f; 

    float screenWidth = Screen.width; 

 public AudioSource punchSound; public AudioSource blockSound; 

在玩家脚本附加的 On the Dude 游戏对象上链接打击和阻挡声音:

现在,每当玩家被击中时,我们必须播放打击声音效果,如果玩家正在阻挡,我们播放阻挡声音效果。因此,打开 playerScript,在检查敌人是否正在打击的地方,对代码进行以下更改:

    if (eScript.isPunching == true) { 
        if (anim.GetBool("bIsDefending") == false) { 
            //Debug.Log("player got hit"); 
            anim.SetTrigger("tGotHit"); 
            health -= eScript.damage; 
            Debug.Log("Player Health: " + health); 

 punchSound.Play(); 
        } else { 
 blockSound.Play(); 
        } 
    }  

效果播放得如预期。让我们也为敌人做同样的事情,如下所示:

当你玩游戏时,你会发现声音效果播放得如预期。让我们也为敌人做同样的事情,如下所示:

    if (pAnim.GetBool("tIsPunching")) {
        if (anim.GetBool("bEnemyIsDefending") == false) {
        // Debug.Log("enemy got hit");
        anim.SetTrigger("tEnemyGotHit");
        anim.SetBool("bEnemyIsDefending", true);         
        health -= pScript.damage;
        Debug.Log("Enemy Health: " + health);
        punchSound.Play();         
        } else {         
            blockSound.Play();
        }   
    }

处理多个分辨率

在 Unity 中处理多个分辨率非常简单。打开主菜单场景。在层次结构中选择 Canvas 组件:

Canvas Scaler 组件负责根据屏幕的宽度和高度缩放 UI 画布。将 UI 缩放模式字段设置为与屏幕大小缩放。这将根据屏幕的宽度和高度缩放 UI 元素。

我们还提供了一个基于 UI 缩放的参考分辨率。这里它是 800x600。如果它能在那个分辨率下适应 UI,那么它肯定也能适应 16:9 的分辨率。

下一个参数是匹配参数。在这里,我们将宽度设置为匹配,然后高度将相应缩放。这将对其他场景中的所有画布都要进行同样的操作。一旦这样做,游戏应该能够根据屏幕分辨率缩放 UI。

优化 APK

要获取单个文件的大小,我们使用 Unity 中的控制台。点击控制台右上角的向下箭头,然后选择打开编辑器日志:

这将打开一个包含日志信息的文本文件。在文本文件中向下滚动,直到它显示单个文件的大小。如你所见,Unity 已经很好地完成了移除当前游戏中未使用的资产的工作。

它还分解了资产并显示了哪些资产对大小有贡献。在这里,我们可以看到menuImage.jpgground.jpgwall.jpg图像的大小相当大:

减小 JPEG 大小的一个流行工具是 Paint.NET;它是一个用于减小文件大小的免费应用程序。您可以从 www.getpaint.net/index.html 下载此工具。

同样,对于 PNG 图片,您可以使用 PNG Crush 工具来减小图片大小。可以从 pmt.sourceforge.io/pngcrush 下载。

总体而言,建议尽可能使用矢量图形而不是位图图像,因为无论分辨率如何,图像质量都将清晰,并且还可以节省空间以进行优化。

准备发布构建

我们还没有做的事情之一是为应用添加图标。一旦您设计了应用图标,我们就会为不同的 Android 设备设置图标。

为了从单个图像创建不同大小的应用图标,我使用 makeappicon.com/ 网站。您只需将图标设计拖放到那里,它将生成不同大小的图标。您可以通过浏览到图标文件夹中的文件或将其拖放到网站上的链接来生成图标,然后可以将其通过电子邮件发送给您:

图片

一旦收到文件,请下载并在“项目”文件夹中新建一个名为“图标”的新文件夹中解压缩它们。

在 Unity 中打开 PlayerSettings。在 PlayerSettings 下,您可以添加公司标志。确保您在“公司名称”和“产品名称”字段中添加详细信息。在“分辨率和展示”部分,将“默认方向”字段设置为 Landscape Left:

图片

接下来,在图标组件下,勾选“为 Android 覆盖”选项并选择具有正确分辨率的图标:

图片

接下来是启动画面部分。如果您是 Unity 专业用户,则可以选择您选择的启动画面。否则,您可以将其留空。接下来,在“其他设置”字段中,确保您已添加正确的包名、版本和捆绑版本代码。

对于最小 API 级别,我选择了 4.0,因为我希望尽可能多地覆盖用户,但在设置最小 API 级别之前,请确保游戏在实际设备上运行良好。其余设置保持不变。

在发布设置中,选择您的密钥库并输入密码。选择密钥别名并在密钥库密码中输入密码:

图片

接下来,打开 Services 并选择 ADS 链接,取消选中“启用测试模式”选项以禁用测试:

图片

现在打开 Build Settings 并构建 APK 以上传到商店。

发布游戏

打开 Android 开发者控制台,选择“所有应用”,并选择您想要发布的应用。将新的 APK 上传到网站。

接下来,我们必须确保在发布游戏之前,所有部分都带有绿色的勾选标记:

图片

因此,选择商店列表链接,并填写标题、简短描述和完整描述字段:

  • 标题字段将显示在应用商店中的游戏名称,所以请确保名称独特、吸引人且易于记忆。

  • 简短描述也会在商店中显示。简短描述更像是游戏/asp 内容的预览。通常,您会在这里指定产品的独特卖点(USP),向读者说明您的游戏为何吸引人,以及与其他产品有何不同。您还会在这里指定游戏的类型,以便如果读者对这种类型感兴趣,他们可以进一步了解。一旦用户点击链接,就会显示完整的描述。

  • 在完整描述字段中,您应该告诉读者游戏能提供什么。这包括功能、故事的简要总结和玩家的动机等内容。以下截图提供了一个简短的示例:

图片

滚动到页面下方,我们需要提供图标、截图和功能图形。我真的很强调图标和截图选择对您游戏成功的重要性。

图标需要放在我们的优先列表之首,因为这是用户首次遇到应用时看到的第一件事。考虑到应用商店每天都有大量应用发布,这一点变得更加重要。在选择合适的图标时需要投入大量的思考和努力,这个图标能够概括您游戏的核心内容,同时足够独特,能够在数百万个应用图标中脱颖而出。

一旦用户对图标感兴趣并点击它,接下来会吸引他们注意的就是截图。至少需要上传三张截图。您可以上传更多。挑选并选择与完整描述中所述内容相匹配的截图。对于每个功能列表项,提供一张截图来告诉用户您提供的内容。

如果您针对的是平板电脑,请添加专门为平板电脑拍摄的截图。此外,如果您为 Android TV 和 Google Daydream 开发,也请添加相应的截图。

您还可以链接 YouTube 上的促销视频。请确保视频不超过 30 秒:

图片

接下来,在分类部分填写详细信息。在这里,您将指定它是一个应用程序还是一个游戏,以及游戏类型。提供您的公司网站、电子邮件地址,并提交隐私政策。

如果您没有提交隐私政策,您可以在隐私政策部分取消选中复选框,表示您不是:

图片

一旦所有信息都正确填写并令您满意,请点击页面右上角的保存草稿按钮。接下来,让我们继续到内容评级部分。

在这里,你将确保内容不包含冒犯性材料,并且产品的质量符合国际年龄评级联盟IARC)的评级标准。IARC 是一个为特定应用或移动游戏分配评级的机构。点击“继续”按钮以继续。

请填写你的电子邮件地址,以便在游戏中遇到问题时,IARC 可以与你联系以进行澄清。接下来,选择应用类别,并在其中选择“游戏”。

在“游戏”类别下,你必须说明你的游戏不包含与暴力、恐惧、性、赌博、语言、管制物质、粗俗幽默和其他杂项相关的内容。对于所有项目,直到“粗俗幽默”,指定“否”选项,如下截图所示:

对于“杂项”类别,我们除了数字购买外,对所有项目指定“否”:

由于我们有内购功能,我们必须指定“是”。完成后,通过点击“保存问卷”选项来保存。接下来,点击“计算评分”标签,屏幕将显示以下内容:

你将获得游戏的总结和评分。同样的信息也会通过提供的邮箱地址发送给你。

接下来,点击“应用评分”;你会看到你的评分已设置。点击“定价和分发”,你会看到以下窗口:

在这里,选择你希望应用是免费还是付费。如果你选择付费,则指定你希望销售应用的定价。

点击所有你希望游戏销售的国家的复选框。点击“可用”选项将选择列表中的所有国家。你可以取消选择你不想应用分发的国家。此外,在底部选择“包含广告”字段中的应用包含广告。

如果你批准应用在 Google Play Store 之外推广,并且应用符合 Android 内容指南和美国出口法律,请检查以下截图所示的“同意”清单:

在屏幕右上角点击“保存草稿”按钮。我们已指定了内购产品、服务和 API,因此我们在这里不需要做任何事情。但我们必须转到游戏服务并发布成就。因此,转到游戏服务并选择应用。

在主页上的“显示名称”和“描述”字段中提供详细信息,并将图标和功能图形上传到这里:

点击窗口左上角的“保存”按钮。接下来,转到“成就”标签,选择每个成就,并添加图标:

一旦为所有成就添加了图标,你将在右上角有发布游戏的选择。一旦发布,你的成就页面将如下所示:

现在我们可以发布应用本身了。转到“所有应用”,选择“管理发布”。它将显示您有一个准备就绪的应用:

图片

点击“恢复”按钮。在“发布名称”部分添加详细信息,并添加描述以显示本版本的更新内容:

图片

点击“审核”按钮,然后点击“开始部署到生产”。您将需要确认应用现在将在应用商店中可用:

图片

点击“确认”按钮,现在它将显示应用现在正在等待发布:

图片

应用发布大约需要一个小时,在应用商店中正确可见大约需要三到四小时。所以,请耐心等待。一旦发布,您可以在社交媒体上分发链接,并告诉您的朋友下载和评分。

在 Facebook 上还有一件小事需要注意。我们必须登录到我们的开发者账户,并启用 Facebook 上的 PunchyPunch 应用。

前往 Facebook 开发者账户,选择该应用。现在转到“应用审核”标签页,在“使 Punchypunch 公开”选项下,翻转开关以使其公开。

现在应用是公开的,您的朋友可以分享您的游戏:

图片

恭喜!您已经在 Unity 上成功开发了一个 Android 应用,并在 Google Play 商店发布了它。

摘要

在本章中,我们为游戏添加了声音,为发布准备应用,并对应用商店本身进行了更改。我们上传了最终构建版本,并将应用本身发布到商店。

posted @ 2025-10-26 09:04  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报