Moai-SDK-移动游戏开发-全-

Moai SDK 移动游戏开发(全)

原文:zh.annas-archive.org/md5/6af1afe68fcc3bc031d670fda7c397fc

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在过去几年里,跨平台开发已经成为游戏开发者技能集中的一个必备项。Moai SDK 是这一需求的一个优秀免费开源解决方案。本书将介绍使用 Lua 通过 Moai SDK 开发游戏的方法;阅读完毕后,你应该能够着手创建下一个热门移动游戏!

我们希望不久后能在这里见到你。

欢迎使用 Moai SDK!

本书涵盖内容

第一章, 为什么选择 Moai SDK?,讨论了多平台开发的重要性以及使用 Moai SDK 的优势。

第二章, 安装 Moai SDK,以逐步的方式帮助我们安装 Moai SDK 在 Windows 和 Mac OS X 上。

第三章, 基本 Moai SDK 概念,讨论了动作树、节点图和输入队列。

第四章, 我们的第一个 Moai 游戏,开始制作类似专注的游戏。它讨论了游戏玩法并实现了我们游戏的入口点。

第五章, 在屏幕上显示图像,介绍了牌组和道具的概念,并指导你如何在屏幕上显示第一张图像。

第六章, 资源管理器,指导你创建一个 Lua 模块,该模块将负责在游戏和资源之间进行接口。它将用于缓存纹理、声音和字体,并避免重复加载代码。

第七章, 专注游戏玩法,教我们如何使用网格和瓦片集,以及如何处理输入。

第八章, 让我们构建一个平台游戏!,指导你完成平台游戏原型的第一步。你将学习如何实现具有视差效果的分层背景和具有多个动画的主角色。

第九章, 使用 Box2D 进行现实世界物理,教我们如何将 Box2D 作为我们的物理引擎使用,并实现一个基本的物理世界。

第十章, 创建 HUD,教我们如何处理字体并在屏幕上显示它们。我们将使用它们来显示一些调试信息,但你可以从这里开始任何地方。

第十一章, 让正确的音乐进来!,指导你如何加载音频文件并播放它们。

第十二章, iOS 部署,提供了一个关于如何部署到移动设备的案例研究,并指导你完成使我们的专注游戏在 iOS 上运行所需的所有步骤。

第十三章,部署到其他平台,讨论了 Moai 官方支持的不同平台,并指导你如何将它们部署到这些平台。

你需要这本书的什么

对于这本书,你需要拥有 Windows XP(或更新版本)或 Mac OS X。

你需要下载并安装 Moai SDK 和 ZeroBrane Studio(请参阅第二章,安装 Moai SDK)。

这本书面向的对象

Moai SDK 是一个面向专业游戏开发者的极简框架。这意味着其中一些功能可能对初学者来说有些困难。然而,这本书旨在帮助初学者入门 Moai SDK。你需要对编程有一些基本理解(Lua 知识不是必需的,但强烈推荐)和基本的命令行熟悉度(以便运行我们的游戏)。

惯例

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

文本中的代码单词显示如下:“我们正在创建一个名为 Game:start() 的方法,它将负责初始化一切并控制游戏循环。”

代码块设置如下:

module(“Game”, package.seeall)
GRID_COLS = 5
GRID_ROWS = 4
GRID_TILE_WIDTH = 62
GRID_TILE_HEIGHT = 62
BACK_TILE = 1

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

WORLD_RESOLUTION_X = 320
WORLD_RESOLUTION_Y = 480
SCREEN_RESOLUTION_X = 2 * WORLD_RESOLUTION_X
SCREEN_RESOLUTION_Y = 2 * WORLD_RESOLUTION_Y

新术语重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“打开 ZeroBrane Studio,在左侧面板(项目)中点击带有省略号(...)的按钮,转到 moai-sdk/samples/anim/anim-basic,然后点击确定”。

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

小技巧和窍门看起来像这样。

读者反馈

我们读者的反馈总是受欢迎的。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大收益的标题非常重要。

如要向我们发送一般反馈,请简单地将电子邮件发送到 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

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

下载示例代码

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

错误

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

海盗行为

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

请通过链接版权@packtpub.com 与我们联系,并提供涉嫌盗版材料的链接。

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

询问

如果您在本书的任何方面遇到问题,可以通过链接询问@packtpub.com 与我们联系,我们将尽力解决。

第一章。为什么选择 Moai SDK?

在过去的二十年里,我们见证了软件平台的爆炸式发展,这主要表现为 Linux 和 Mac OS 的大规模采用,以及在过去几年中 Android 和 iOS 的兴起。在本章中,我们将探讨这其中的含义以及为什么 Moai SDK 会在这里帮助我们所有人,而且还是免费的。

多平台开发

所以,你想要制作游戏。你可能有一个很棒的的游戏想法,并计划将其发布在 Web、iOS 和 Android 平台上,如果能够获得足够的关注,也许还会在 Steam 上发布。我打赌你已经有了一些概念艺术,或者你的游戏设计文档GDD)已经准备好了。但是,你有没有计划如何制作你的游戏,以便能够支持所有这些平台?

现在你有三种选择来处理这个问题:

  • 构建游戏n次,其中n等于平台的数量

  • 忘记多平台,专注于一个或两个

  • 使用设计用于支持多平台的库/框架

第一个选择有一个优点,就是你可以为每个平台优化你的游戏,确保它运行流畅。此外,你可以处理平台限制,并根据它们改变游戏的外观。但是,你有没有想过拥有四个不同代码库的后果?每个都有自己的错误和实现逻辑的方式。此外,如果你找不到对所有平台都有深入了解的程序员,你可能需要雇佣多个程序员。这真是一场噩梦。

忘记多平台可能对你来说是个不错的选择,但最终,如果你制作了一个真正成功的游戏(制作游戏成功的一个可能方式是尽可能多地将其发布在各个平台上,因为这样可以增加知名度),你将希望以低成本将其移植到不同的平台上,而如果代码与特定平台紧密相连,移植成本会很高。

这引出了最后一个选择,采用支持多平台开发的库或框架。基本思想是,你将你的代码从特定平台中分离出来,使用广泛使用的语言(如 C、C++、JavaScript 或 Lua),并使用单个代码库为不同的平台构建游戏。

注意

多平台开发的实际例子是 Zipline Games 的狼掷。它有一个 3 个月的开发周期,并在 iOS 和 Android 上同时发布。如果为每个平台构建,每个平台大约需要三个月的时间,或者需要一个两倍大的团队。因此,通过使用 Moai SDK 开发狼掷,工作室节省了与平台特定方法相比一半的费用。

这种方法最近越来越受欢迎,因为它简单且效果强大。在过去的几年中,许多采用这种方法的游戏开发环境和框架都出现了。其中之一就是 Moai SDK。

为什么选择 Moai SDK?

可以安全地说,现在几乎每个平台都支持 C++和某种形式的 OpenGL。这是 Moai SDK 的起点。

Moai SDK是一个多平台游戏开发框架。它使用 C++开发,并且所有功能都可以通过 Lua 访问。这意味着什么?这意味着你可以完全使用 Lua(利用这种令人惊叹的语言的灵活性)来构建你的游戏,如果你需要低级访问,你总是可以切换到 C++并做你想做的任何事情。你甚至可以将你的 C++库绑定到 Lua 中,在你的游戏中使用。它还内置了对 Moai Cloud 的访问,这是一个云服务,允许你部署用 Lua 编写的服务器端代码,包括数据库、推送通知支持、排行榜和其他花哨的功能。请注意,游戏和服务器端代码使用的语言是相同的,这很棒。

简而言之,如果 XYZ 平台支持 OpenGL 和 C++,Moai SDK 很可能也支持它

目前,Moai SDK 在 Windows、Mac、Linux、iOS、Android 和 Native Client 上运行。

无论如何,有许多支持多平台开发的游戏引擎。其中许多都带有闪亮的界面,其他一些则解决了你一半的问题,比如一键部署。那么,Moai SDK 为这个世界带来了什么尚未实现的东西?自由

  • 它是开源的,你可以下载代码并随意使用它。

  • 它是免费的,没有数百美元的许可证费用。

  • 它不会告诉你如何做事。它是针对想要按自己的方式做事的专业开发者。

  • 它已经被用于几个大规模的商业游戏。

所以,Moai SDK 背后的主要原则就是这些。为打造一款伟大的游戏提供了绝佳的混合。

总结

现在,为了能够开发游戏,考虑多平台开发非常重要。Moai SDK 是一个游戏开发框架,它帮助我们以标准技术轻松地解决这个问题,这些技术几乎在所有平台上都可用。不仅如此,它是免费开源的。

第二章。安装 Moai SDK

在我们可以动手之前,我们需要采取一些步骤,以便拥有一个完全工作的环境来使用 Moai SDK 进行开发。

这在不同的平台上可能会有所不同,所以请随意跳转到您喜欢的平台。

我们将介绍如何在 Windows 和 Mac OS X 上安装 Moai SDK;对于 Linux 用户,这个过程更为复杂,我们在这里无法涵盖,但您可以在本章中找到更多关于它的信息。在本章结束时,您应该在自己的系统上安装好一个可工作的 Moai SDK 版本。

获取最新稳定构建

访问 getmoai.com/sdk/moai-sdk-download.html

您将找到两个下载选项,一个是发布版,另一个是夜间构建。

  • 发布版:这是 Moai SDK 的稳定版本。它包含了您开始使用 Moai SDK 进行开发所需的所有内容。本书撰写时的当前版本是 1.4p0。

  • 夜间构建:每晚(或根据您的位置是早上或晚上)都会自动构建整个 Moai SDK 项目。由于它包含了自上次稳定发布以来的所有更改,因此这个构建非常不稳定。如果您需要尚未包含在最新版本中的某些功能,请下载夜间构建,它应该在那里。

对于这些版本中的每一个,都有两种可能的软件包:.zip.tar.gz 软件包,下载您最喜欢的一种。

获取最新稳定构建

设置环境

下载 SDK 后,我们需要设置我们的环境。

Moai SDK 可以与任何文本编辑器一起使用,但如果您想要一些高级功能,如调试器,您就需要使用支持 Moai SDK 的 IDE 之一。有很多这样的 IDE,每个都有不同程度的集成。为了获取完整的列表,我建议您访问 Moai 的维基百科上的 与 Moai 一起工作的工具 getmoai.com/wiki/index.php?title=Tools_that_work_with_Moai

在本书中,我们将专注于使用 ZeroBrane Studio,因为它开源(您可以用 Lua 编程它)并且具有许多对我们有用的特性(如语法高亮、Moai SDK 的自动完成、调试和实时编码等)。要安装它,请访问 studio.zerobrane.com 并下载适用于您特定平台的最新软件版本。

Windows

本书假设您已经下载了 ZeroBrane Studio(无论是 zip 文件还是安装程序)并将其安装到 C:\Program Files\ZeroBraneStudio

现在,将 Moai SDK 软件包解压到 C:\moai-sdk

在完成此操作后,我们需要设置 ZeroBrane 以与我们的 Moai SDK 安装一起工作。

  1. C:\Program Files\ZeroBraneStudio\cfg\ 中创建一个名为 user.lua 的文件,并添加以下行:

    -- MOAI Path
    path.moai = 'c:/moai-sdk/bin/win32/moai.exe'
    
  2. 如果您选择在另一个位置安装 Moai SDK,请使用该位置。

  3. 为了支持 Moai SDK,您还需要做最后一件事。

  4. 导航到项目 | Lua 解释器 | Moai

现在您已经准备好了。您可以跳转到运行示例部分,看看一切是否正常工作。

Mac OS X

本书假设您已经下载了 Mac OS X 的 ZeroBrane Studio 并将其安装到/Applications

现在,将 Moai SDK 包解压到~/moai-sdk

  1. /Applications/ZeroBraneStudio.app/Contents/ZeroBraneStudio/cfg/中创建一个名为user.lua的文件,并添加以下行:

    -- MOAI Path
    path.moai = '~/moai-sdk/bin/osx/moai'
    
  2. 如果你选择在其他位置安装 MOAI,请使用该位置。

  3. 最后一步是导航到项目 | Lua 解释器 | Moai`。

现在您已经准备好了。

下一步是尝试一些示例,看看是否一切正常工作。

GNU/Linux

很遗憾,当前稳定的 Moai SDK 版本不包括预编译的 GNU/Linux 主机。因此,唯一的选择是从源代码构建自己的主机。为了做到这一点,你需要了解如何编译 C++以及如何处理 Git 仓库(至少要知道如何克隆它们)。

这本书的范围之外,但如果你迷路了,请随时跳转到 Moai 论坛寻求帮助。

只作为一个提示,你需要从github.com/moai/moai-dev获取源代码,切换到 Linux 分支,并使用CMake编译一切。

运行示例

为了查看您的安装是否正常工作,我们将从 Moai 的发布版中运行一个示例。

  1. 打开 ZeroBrane Studio,在左侧面板(项目)中,点击带有省略号(...)的按钮,转到moai-sdk/samples/anim/anim-basic,然后点击确定运行示例

    现在您的项目正在使用 Moai SDK 的示例作为项目根。您将在moai-sdk/samples/folder中看到很多如何使用 Moai SDK 的示例。在阅读完本书后,查看它们以了解更高级的主题。

  2. 现在按F6(或项目 | 运行)。

    如果打开了一个窗口并显示 Moai SDK 的标志旋转,太好了!您已经准备好了!

    运行示例

您将想要阅读这个示例;特别是看看以下行:

prop:moveRot ( 360, 1.5 )

这一行是使 Moai 标志移动的原因。第一个参数是旋转角度,第二个参数是完成整个运动所需的时间。试试看,改变角度和持续时间,看看会发生什么。

概述

在本章中,我们了解到,为了使用 Moai SDK 进行开发,我们需要使用预编译的主机之一,或者从源代码构建一个(在 Linux 开发的情况下这是强制性的,因为还没有预编译的构建)。

我们已经下载了框架的最新版本,安装了它,并运行了一个示例来查看它是否正常工作。

下一章将向您介绍 Moai SDK 背后的基本概念,所以如果您想了解 Moai 的运行时、Action Tree 和节点图,请继续阅读。

第三章. 基本 Moai SDK 概念

每个游戏引擎都有其背后的概念,这些概念定义了其内部工作方式。在本章中,我们将快速浏览 Moai SDK 使用的不同想法,以便更好地理解它。

Moai SDK 运行时

Moai 的运行时使用两个重要的数据结构,即对象/节点的依赖图动作树。作为开发者,你主要会修改这些数据结构(在 Lua 中使用 Moai SDK API)。

Moai 更新循环有三个步骤:

  • 处理输入队列

  • 处理动作树

  • 处理节点图

在 Moai 中,渲染与游戏循环分离,因为我们可能希望每个渲染帧有多个更新步骤。

输入队列

当你按下一个键、移动鼠标或在 iPhone 屏幕上轻触时,系统会生成一个输入事件,由主机传递给 Moai 并放入队列(使用 AKUEnqueue*方法)。在模拟的每个步骤中,Moai 处理所有输入回调,处理输入队列,然后继续游戏循环。

输入队列

在游戏循环期间发生的所有事件都会在下一个模拟步骤中排队。

动作树

节点基本上是所有为你游戏提供信息的对象,例如,子弹的实例在 Moai 中就是一个节点,具体来说是一个可以渲染的节点。

动作负责管理节点随时间的变化。

例如,位置、旋转或缩放的变化将创建一个动画。

动作树的作用是创建动作的层次结构。这样做的主要目的是让动作负责执行子动作。Moai 有一个主根动作,将运行附加到其上的所有动作。

动作是 Moai 中唯一接收时间步的对象。这意味着 Moai 对象的时间相关变化可能只发生在处理动作树时。节点图或渲染过程不应对节点的状态进行任何更改。

节点图

因此,正如我们之前所看到的,节点基本上是存在于你的游戏中的对象。这些对象相互连接形成一个图。在每次更新循环步骤中,Moai 处理与前一步状态不同的节点。这是通过一个修改节点的队列来实现的。每当一个动作修改一个节点(或者节点被链接到另一个被修改的节点,或者当一个节点的属性被直接设置时),它就会被安排(连同它连接的节点,等等)并在下一个模拟步骤中处理。

节点图

这种行为定义了一个在低级别构建的父/子关系,因此可以轻松创建场景和动画层次结构。

渲染

那么,渲染在哪里进行呢?在 Moai SDK 中,渲染与更新循环分离。渲染管理器是一个单例,负责渲染。它维护一个待渲染的对象表,你可以按需修改该表,并且它将渲染表中的所有对象。值得一提的是,子表也会被渲染,因此你可以创建一个待渲染对象的层次结构。

Moai 主机

Moai SDK 背后的一个关键概念是主机

这是由 Zipline 团队为 Moai SDK 提出的解决方案,基本上表明 Moai SDK 提供了一个 Lua 解释器和对外界的 C++接口。这个接口被称为AKU,可以在 Moai SDK 源代码中找到。

无论何时你想为特定平台创建游戏,你的主机负责配置输入并将输入事件发送到 Moai,解决该平台的所有线程问题,并提供所有平台特定的逻辑和 Lua 扩展,以及一个 OpenGL 画布用于渲染。

以这种方式,在 Moai 中构建的游戏有可能部署到所有支持 C++和 OpenGL(好吧,不是所有的 OpenGL,只是其中的一部分,如 OpenGL ES)的平台,而这些是行业标准,几乎没有限制。

Moai SDK 附带 Windows、Mac OS X、iOS、Android 和 Google 的 Native Client 的示例主机。你应该能够修改这些主机,并且通过一些小的调整,让你的游戏在这些主机上运行。

再次强调,你只需使用 Lua 构建一次游戏,就可以让所有这些平台运行。

Lua 和 C++

Moai 背后的另一个有趣特性是,你可以访问 Lua 和 C++来开发你的游戏。

使用 Lua 设置你的节点和动作后,Moai SDK 使用用 C++编写的本地代码运行模拟。这意味着你得到了两者的最佳结合——Lua 的灵活性和 C++的速度。

所有平台特定的东西(例如,处理智能手机上的加速度计)都应该以本地方式处理,并通过 Lua 扩展或输入事件连接到 Moai SDK(检查主机如何配置 AKU,搜索 AKUSetInputDevice*方法系列)。

这是 Moai SDK 开发的一个关键概念,因为你可以看到,你可以使用 Moai SDK 的库没有限制。这只是一个花时间编写 Lua 扩展的问题,然后 Bam!它就在脚本环境中可用;你可以将库功能与你的现有游戏结合起来。

除了这个之外,还有解释型代码与编译型原生代码的困境。你知道使用 Lua 进行编程比使用 C++ 更有效率,但有时由于性能要求,你就是在 Lua 中也做不了某些事情。(尽管这种情况非常罕见,Lua 还是相当快的,瓶颈通常出现在渲染上,逻辑执行花费的时间非常少)。你总是可以将你的代码移植到 C++(你可以使用性能分析工具来决定是否需要这样做),然后创建一个具有相同 API 的 Lua 扩展,你就不需要修改你代码的其他部分了。

摘要

在本章中,我们讨论了 Moai SDK 更新循环的细节,以便理解其内部工作原理。我们考察了更新循环中的三个主要步骤,即输入队列、动作树的处理以及节点图的处理。我们讨论了 Moai 的独立渲染特性、主机的重要性以及将 C++ 和 Lua 世界联系在一起的理念,以及如何明智地利用这一点。

好吧,关于幕后发生的事情我们已经说得够多了。现在去喝杯咖啡吧,因为接下来的一章,我们将最终开始实现我们的第一个游戏,你的好奇心不会让你那时停下来休息。

第四章.我们的第一个 Moai 游戏

冒险时间!我们将开始使用 Moai SDK 制作我们的第一个游戏,一个简单的类似注意力集中的游戏。为此,你需要对 Lua 语法有一定的了解,不需要太花哨,只是基础知识。如果你以前从未使用过 Lua,阅读一些快速介绍材料并在阅读这本书时准备一个语法速查表是个好主意。

注意力集中

对于那些不熟悉这个游戏的读者,让我们来描述一下它。

游戏需要一套拼图。拼图的数量必须是偶数。拼图的一面有图案(成对出现,因为我们将会匹配它们),另一面则是平坦的颜色或相同的图像(拼图的背面)。

游戏玩法如下:

  1. 将所有拼图面朝下并洗牌。这样你就可以隐藏成对的图案,不知道它们在哪里。注意力集中

  2. 然后,你需要选择两张拼图并翻转它们,以显示图案。注意力集中

  3. 如果它们不同,你必须再次翻转它们并选择另一对。注意力集中

  4. 如果它们相同,你将它们从板上移除,游戏继续进行,但拼图数量减少两张。注意力集中

当没有拼图剩下时,游戏结束。

我们将实现这个游戏,并增加一个滴答作响的时钟,使事情变得有点更具挑战性。

为了做到这一点,我们将检查 Moai SDK 的所有基本功能以及如何使用它们。

项目设置

为了设置项目,我们需要执行几个步骤:

  1. 在你的硬盘上某个位置创建一个用于我们项目的文件夹。

  2. 之后打开 ZeroBrane Studio 并导航到那个文件夹。

  3. 你现在会看到一个空文件夹和一个名为untitled.lua的空白文件。

  4. 将该文件保存为main.lua,这是游戏入口点文件的常用名称。你可以取任何你想要的名称,但我们将使用这个作为标准。

  5. 现在我们准备开始。

打开窗口

我们首先学习的是如何使用 Moai SDK 打开主游戏窗口。

这是一个相对简单的任务。代码如下:

MOAISim.openWindow ( "Concentration", 320, 480 )

我们在这里所做的是调用MOAISim.openWindow类方法,并使用必要的参数,即窗口的title(一个字符串)、widthheight。正如你可能已经注意到的,我们使用了 320 x 480,这是 iPhone 3GS(没有 Retina Display)的显示分辨率。

如果你现在运行脚本,你会看到一个窗口打开。所以,这就是我们的主窗口准备好了。但在继续之前,让我们看看 Moai SDK 的一个重要方面。

注意

下载示例代码

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

分辨率无关性

我们刚刚打开的窗口具有特定的宽度和高度。但是,正如我们所知,不同的设备有不同的分辨率,我们不能仅仅打开一个具有固定宽度和高度的屏幕,因为它在 iPhone 和 iPad 等设备上不会正确运行。

因此,我们将在这里介绍一个概念:世界分辨率屏幕/窗口分辨率的比较。

这个想法是,你的游戏将使用世界分辨率进行所有计算和对象放置,然后你将使用一个理解世界分辨率的视口,并将其转换为窗口分辨率。所以,让我们按照以下方式设置我们的脚本:

WORLD_RESOLUTION_X = 320
WORLD_RESOLUTION_Y = 480

SCREEN_RESOLUTION_X = 2 * WORLD_RESOLUTION_X
SCREEN_RESOLUTION_Y = 2 * WORLD_RESOLUTION_Y

-- Open main screen
MOAISim.openWindow ( "Concentration", SCREEN_RESOLUTION_X, SCREEN_RESOLUTION_Y )

-- Setup viewport
viewport = MOAIViewport.new ()
viewport:setSize ( SCREEN_RESOLUTION_X, SCREEN_RESOLUTION_Y )
viewport:setScale ( WORLD_RESOLUTION_X, WORLD_RESOLUTION_Y )

让我们逐行查看前面的代码:

WORLD_RESOLUTION_X = 320
WORLD_RESOLUTION_Y = 480

这些行定义了两个常量,即我们的世界分辨率沿 X 轴和 Y 轴。记住,这些不是屏幕的尺寸,只是我们的世界。

SCREEN_RESOLUTION_X = 2 * WORLD_RESOLUTION_X
SCREEN_RESOLUTION_Y = 2 * WORLD_RESOLUTION_Y

现在我们为屏幕的分辨率定义了XY维度。正如你所见,我们将屏幕的大小设置为世界大小的两倍。我们这样做是为了更好地说明屏幕分辨率与世界分辨率的独立性。在接下来的章节中,我们将看到你在世界中放置的任何东西在屏幕上看起来将是以下的两倍大小:

分辨率独立性

我们现在需要告诉 Moai 打开一个窗口,这在开发过程中是必需的,为了在我们的游戏设备上创建一个窗口,在设备上它将不会做任何特别的事情。

MOAISim.openWindow ( "Concentration", SCREEN_RESOLUTION_X, SCREEN_RESOLUTION_Y )

这是之前我们看到的那行代码,但现在,我们正在使用我们的SCREEN_RESOLUTION_XSCREEN_RESOLUTION_Y常量。请注意,在具有固定尺寸屏幕的设备上,这个方法不会做任何特别的事情,但我们需要它在计算机上运行我们的示例。

viewport = MOAIViewport.new ()

现在,这里有一些新东西(字面上的)。为了处理世界坐标和屏幕坐标之间的转换,我们将使用MOAIViewport实例。请注意,你可能有一个任意数量的视口,每个视口使用不同的坐标比例。例如,我们可以使用与窗口相同比例的另一个视口来实现某些特定效果或用户界面(GUI),而该界面不需要缩放。

viewport:setSize ( SCREEN_RESOLUTION_X, SCREEN_RESOLUTION_Y )

现在我们为视口设置大小。这通常与屏幕分辨率相同(在设备上,如果你希望它占据整个屏幕,这必须与屏幕分辨率匹配),但你也可以创建一个更小的视口来显示小地图或其他类似的东西,但请记住,整个大小的概念是它将成为目标分辨率。

viewport:setScale ( WORLD_RESOLUTION_X, WORLD_RESOLUTION_Y )

这就是完成魔法的地方。我们设置了一个比例,使用不同的分辨率。如果你这么想,我们正在做的事情就是告诉视口,无论我们在未来的层上放置什么,都需要从世界坐标(WORLD_RESOLUTION_XWORLD_RESOLUTION_Y)拉伸/缩小到另一组坐标(SCREEN_RESOLUTION_XSCREEN_RESOLUTION_Y)。

就这样,我们现在拥有了一个分辨率无关的屏幕。只需更改 SCREEN_RESOLUTION_X 和/或 SCREEN_RESOLUTION_Y,游戏就会相应调整。

摘要

因此,我们现在致力于使用 Moai SDK 制作一个 专注力 游戏。我们审查了 专注力 游戏的规则和目标。我们还用 Moai SDK 编写了我们的第一行代码来打开窗口,并讨论了如何使用世界坐标系统与屏幕坐标系统实现分辨率无关的游戏。

下一章将教你如何在屏幕上显示图像,这是今天游戏开发者最基本的技术。

第五章. 在屏幕上显示图像

为了开发游戏,我们需要获得的一项基本技能是图像处理。在本章中,我们将讨论一些对这个主题有用的方面,这将有助于我们的游戏。我们将介绍 Decks 和 Props 的概念,并讨论如何处理它们以制作我们的游戏图形。

Decks 和道具简介

在 Moai SDK 中,所有的资产(图像、瓦片集、网格和表面)都在 Decks 中定义。你可以将 Deck 视为一个对象的几何形状。Decks 存储了精灵的实际图像数据,例如。它们不会显示在屏幕上,直到你使用 Prop 引用它们。你可以将 Decks 视为 Prop 的绘制方法。

有许多类型的 Decks,例如 MOAIGfxQuad2D 处理单个图像。MOAITileDeck2D 用于加载瓦片集(或图集)并引用其瓦片(我们将在本书的后面使用它)。

Props 基本上是一组信息和给定 Deck 的引用。它们持有诸如位置、缩放和旋转等信息;并且是你在屏幕上看到的实际对象。

这种分离非常实用,因为它允许你重复使用分配给特定资产的记忆。我们将使用它来处理所有的瓦片,而不是加载瓦片背面的图片 20 次,我们只需加载一次并指向它。

我们的第一张图片

我们需要做的第一件事是有一个实际的图像。在这个例子中,我们将使用我们 Concentration 游戏的瓦片背面,你可以从本书的网站上获取它,以及这个例子的完整源代码。

首先,我们需要一个层。一个将一个或多个 Props 组合在一起,并作为在不同 Prop 集之间控制渲染优先级的一种方式。例如,你可以有一个用于背景的层,在其上方还有一个用于对象的层,当你向背景层添加一个新的 Prop 时,它将显示在对象层的 Props 下方。

注意

MOAILayer 继承自 MOAIProp,所以你可以将其视为一个将其他 Props 组合在一起的 Prop。你可以在一个层上调用 MOAIProp 的任何方法。

为了创建一个层,你必须按照以下方式实例化 MOAILayer2D(在代码中在视口初始化下方输入此内容,第四章,使用 Moai 的第一个游戏):

  1. 首先,我们创建我们的层:

    layer = MOAILayer2D.new ()
    
  2. 我们需要告诉那个层使用我们的视口。这是通过使用 setViewport() 方法完成的。

    layer:setViewport ( viewport )
    
  3. 现在我们必须创建我们的 Deck。由于我们只显示一个简单的图像,我们将使用 MOAIGfxQuad2D,它用于此目的。

    1. 我们创建 Deck 对象。

      imageGfx = MOAIGfxQuad2D.new ()
      
    2. 我们使用 setTexture() 方法并将我们资产的路径传递给它(在这种情况下是瓦片的背面)。这就是我们告诉 Deck 实际上使用我们的图像的地方。

      imageGfx:setTexture ( 'assets/tile_back.png' )
      
    3. 第三行设置几何形状。MOAIGfxQuad2D 有一个矩形几何形状。为了定义它,我们可以使用 setRect,它接收四个数字,xMinyMinxMaxyMax

      imageGfx:setRect ( -31, -31, 31, 31 )
      

    我们将采用约定,对象坐标 (0, 0) 位于中心。这样,由于我们希望以 62 x 62 的大小显示图像,实际包含它的矩形可以定义为具有 xMin = -31yMin = -31xMax = 31yMax = 31 的矩形,因为我们希望它的 (0,0) 位于图像的中心。我们可以用 setRect ( 0, 0, 62, 62 ) 来定义它,这样中心就会在左上角,但为了简单起见,让我们保持 (0,0) 在图像中心的约定。只需注意,这非常强大。你可以根据你想要如何操作它们来不同地定义矩形。

    我们的第一张图像

  4. 下一步是设置我们的属性。

    1. 首先,我们创建属性。

      imageProp = MOAIProp2D.new ()
      
    2. 然后我们告诉属性使用 imageGfx 作为牌组。这是我们将它们连接在一起的时刻。我们说的是这个属性将是我们瓷砖背面的图像实例。

      imageProp:setDeck (imageGfx)
      
    3. 第三行将属性移动到屏幕中心。

      imageProp:setLoc (0, 0)
      

      小贴士

      我们使用 setLoc() 方法将图像移动到瓷砖的背面。你可以更改参数并观察它的行为。默认情况下,(0,0) 是屏幕中心,所以 x 中的负值将对象移动到左边,正值将其移动到右边。y 中的负值将对象向下移动,正值将其向上移动。这与计算机图形学中使用的典型坐标系不同,它将 (0,0) 设置在左上角,并且 y 轴向下增长。

  5. 下一步是将我们的属性插入到我们创建的层中,这样它就会在层渲染时渲染。

    layer:insertProp (imageProp)
    
  6. 最后一步是使该层可渲染。

    renderTable = { layer }
    MOAIRenderMgr.setRenderTable (renderTable)
    

这是一件有趣的事情来讨论。我们在这里所做的是,首先创建一个包含层作为其第一个对象的表,然后告诉 MOAIRenderMgr 使用那个表进行渲染。

为了配置渲染,我们需要告诉 MOAIRenderMgr 我们想要渲染的内容。这是通过使用 渲染表 来完成的。正如我们在 第三章 中所讨论的,基本 Moai SDK 概念,使用 setRenderTable() 传递的表和子表将在下一个渲染步骤中渲染。所以,例如,如果你想移除所有正在渲染的对象,你不需要遍历它们,你只需要将一个空表传递给 setRenderTable() 方法。

在处理这个时,你必须小心。如果对象突然消失,请留意你正在渲染的表。

坐标系

总有一天,如果你在开发多平台游戏,你需要了解涉及的不同坐标系。

在 Moai SDK 中,我们至少有三种重要的坐标系需要记住并有效利用。

窗口/屏幕坐标

这是附加到您窗口上的坐标系。您通常在接收鼠标或触摸屏输入时需要处理它。您也可以使用它来设计您的 GUI。如果您在点击时遇到任何奇怪的行为,请务必检查您是否正确地将输入坐标转换为世界坐标或甚至模型坐标。

世界坐标

这是您在设置视口缩放时定义的坐标系,也是您在屏幕上定位 Prop 时通常会使用的坐标系。这是多平台魔法发生的地方,因为这个坐标系抽象了不同的屏幕分辨率,这样您就不需要考虑所有这些分辨率。需要处理纵横比变化,但这是一个单独的话题。

模型坐标

这个坐标系是 Prop 使用的坐标系。它与 Prop 几何相关。

在检查碰撞时非常有用。给定世界中的一个任意点,将其转换为模型坐标将得到一个在 xy 方向上从 Prop 的中心有一定偏移的点。如果您知道 Prop 的大小,您实际上可以检查该点是否在 Prop 的边界内。

在坐标系之间切换可以大大简化您的游戏逻辑。

模型坐标

幸运的是,在 Moai SDK 中处理这个坐标噩梦非常简单。

有一些方法将帮助我们完成这项任务:

  • MOAILayer2D 有方法在窗口和世界坐标之间进行转换(wndToWorldworldToWnd)。

  • MOAIProp2D 有方法在世界坐标和模型坐标之间进行转换(modelToWorldworldToModel)。

因此,您将想要使用这些方法,以便根据您正在处理的实体进行计算。当我们实现 集中注意力 游戏的游戏玩法时,我们会使用这些方法。

混合模式

您肯定会玩的一个很酷的功能是 混合模式

Moai SDK 允许您使用 setBlendMode() 方法为某个 Prop 定义混合模式。这将允许您修改其像素在屏幕上渲染的方式。

您可以选择 MOAIProp2D.BLEND_NORMALMOAIProp2D.BLEND_ADDMOAIProp2D.BLEND_MULTIPLY

MOAIProp2D.BLEND_NORMAL 是默认行为,基本上会覆盖像素,覆盖其下的一切。

MOAIProp2D.BLEND_ADD 执行加法混合。它将您试图绘制的像素的值添加到屏幕上该像素的先前值。这对于粒子系统以及创建类似 Tron 的发光效果非常有用。它还可以用于创建场景的静态照明。

MOAIProp2D.BLEND_MULTIPLY 将屏幕上像素和它试图绘制的像素的值相乘。这对于创建静态阴影非常有用。

混合模式

摘要

在本章中,我们学习了关于牌组(Decks)和道具(Props)的内容——Moai SDK 提供用于处理图像的对象。我们看到了如何将这些对象与视口结合,并在屏幕上显示图像。我们还讨论了坐标系及其重要性,以及何时使用哪种坐标系以便于计算(并且正确!)我们还介绍了混合模式的使用,这对于创建灯光和阴影的漂亮效果非常有用。

下一章将是我们极简游戏框架的基础,即资源管理器。这包含了处理内存中对象加载和缓存的代码。我们将使用它来避免在需要使用图像或声音时重复初始化代码。

第六章:资源管理器

如果我们开始开发我们的游戏,我们最终需要创建一个处理所有我们资产的实体。我们将称之为ResourceManager。我们将看到如何创建一个允许你将图像、字体和声音添加到你的游戏中的实体。

资源管理器背后的主要思想是缓存我们将多次使用的资产,并有一个集中化和抽象化的方式来创建资产。

资源定义

为了能够定义资源,我们需要创建一个负责处理此任务的模块。主要思想是在通过ResourceManager调用某个资产之前,它必须在ResourceDefinitions中定义。这样,ResourceManager将始终能够访问创建资产所需的某些元数据(文件名、大小、音量等)。

为了识别资产类型(声音、图像、瓦片和字体),我们将定义一些常量(请注意,这些常量的数值是任意的;你可以在这里使用任何你想要的)。让我们称它们为RESOURCE_TYPE_[type](如果你想使用其他约定,请随意)。为了使事情更简单,现在就遵循这个约定,因为这是我们将在本书的其余部分使用的约定。你应该在main.lua中按如下方式输入它们:

RESOURCE_TYPE_IMAGE = 0
RESOURCE_TYPE_TILED_IMAGE = 1
RESOURCE_TYPE_FONT = 2
RESOURCE_TYPE_SOUND = 3

如果你想要了解这些资源类型常数的实际原因,请查看下一节中我们的ResourceManager实体的load函数。

我们需要创建一个名为resource_definitions.lua的文件,并添加一些简单的处理方法。

向其中添加以下行:

module ( “ResourceDefinitions”, package.seeall )

前面的行表示文件中的所有代码都应该被视为一个module函数,通过代码中的ResourceDefinitions来访问。这是 Lua 创建模块的许多模式之一。

注意

如果你不熟悉 Lua 的module函数,你可以在模块教程中阅读有关它的内容,教程地址为lua-users.org/wiki/ModulesTutorial

接下来,我们将创建一个包含这些定义的表:

local definitions = {}

这将在内部使用,并且无法通过模块 API 访问,因此我们使用关键字local来创建它。

现在,我们需要为定义创建settergetterunload方法。

setter方法(称为set)将definition参数(一个表)存储在definitions表中,使用name参数(一个字符串)作为键,如下所示:

function ResourceDefinitions:set(name, definition)
    definitions[name] = definition
end

getter方法(称为get,当然!)使用name参数作为definitions表的键,检索之前存储的定义(通过使用ResourceDefinitions:set ()),如下所示:

function ResourceDefinitions:get(name)
    return definitions[name]
end

我们正在创建的final方法是remove。我们用它来清除定义所使用的内存空间。为了实现这一点,我们将nil赋值给definitions表中由name参数索引的条目,如下所示:

function ResourceDefinitions:remove (name)
  definitions[name] = nil
end

这样做,我们消除了对对象的引用,允许垃圾收集器释放内存。这在这里可能看起来没有用,但它是一个很好的例子,说明了您应该如何管理对象以便让垃圾收集器从内存中移除。而且除此之外,我们不知道信息来自资源定义;它可能很大,我们只是不知道。

这就是资源定义所需的所有内容。我们正在利用 Lua 提供的动态性。看看创建一个从每个定义的内容中抽象出来的定义存储库有多容易。我们将为每种资产类型定义不同的字段,而且我们不需要事先定义它们,就像我们可能在 C++ 中需要做的那样。

资源管理器

我们现在将创建我们的资源管理器。这个模块将负责创建和存储我们的牌组和一般资源。我们将使用一个单一的命令来检索资源,它们将来自缓存或根据定义创建。

我们需要创建一个名为 resource_manager.lua 的文件,并将其添加以下行:

module ( “ResourceManager”, package.seeall )

这与资源定义中的情况相同;我们正在创建一个模块,它将通过 ResourceManager 来访问。

ASSETS_PATH = ‘assets/’

我们现在创建一个名为 ASSETS_PATH 的常量。这是我们将存储我们的资源的地方。您可以为不同类型的资源设置多个路径,但为了保持简单,在这个例子中我们将它们全部保存在一个单独的目录中。使用这个常量将允许我们只使用文件名,而不是在创建实际资源定义时必须写出整个路径,这样可以节省我们一些麻烦!

local cache = {}

再次,我们正在创建一个作为局部变量的 cache 表。这将是我们存储初始化资源的变量。

现在我们应该注意实现重要的功能。为了使代码更易读,我将在以下页面中定义的方法中使用方法。因此,我建议您在尝试运行我们现在编写的代码之前阅读整个部分。完整的源代码可以从本书的网站上下载,其中包含内联注释。在书中,为了简洁起见,我们移除了注释。

获取器

我们将首先实现我们的 getter 方法,因为它足够简单:

function ResourceManager:get ( name )
    if (not self:loaded ( name )) then
        self:load ( name )
    end

    return cache[name]
end

这个方法接收一个 name 参数,它是我们正在处理的资源的标识符。在第一行,我们调用 loaded(我们很快将定义的方法)来查看由 name 标识的资源是否已经被加载。如果是的话,我们只需要返回缓存的值,但如果没有,我们需要加载它,这就是我们在 if 语句中做的。我们使用内部的 load 方法(我们稍后也将定义)来处理加载。我们将使这个 load 方法将加载的对象存储在 cache 表中。所以加载后,我们唯一要做的就是返回 cache 表中按 name 索引的对象。

我们在这里使用的辅助函数之一是 loaded。让我们来实现它,因为它真的很容易做:

function ResourceManager:loaded ( name )
  return cache[name] ~= nil
end

我们在这里所做的是检查由 name 参数索引的 cache 表是否不等于 nil。如果 cache 在该键下有一个对象,这将返回 true,这正是我们想要找到的,以决定由 name 参数表示的对象是否已经被加载。

加载器

load 及其辅助函数是这个模块最重要的方法。它们将比我们迄今为止所做的方法稍微复杂一些,因为它们实现了魔法。请特别注意这一部分。它并不特别困难,但可能会让人困惑。像之前的方法一样,这个方法只接收代表我们要加载的资源的 name 参数,如下所示:

function ResourceManager:load ( name )

首先,我们检索与 name 关联的资源定义。我们调用之前定义的 ResourceDefinitions 中的 get 方法,如下所示:

    local resourceDefinition = ResourceDefinitions:get( name )

如果资源定义不存在(因为我们忘记在之前定义它),我们将打印一个错误信息,如下所示:

    if not resourceDefinition then
        print(“ERROR: Missing resource definition for “ .. name )

如果成功检索到资源定义,我们创建一个变量来保存资源,并且(请注意)根据资产类型调用正确的 load 辅助函数。

    else
        local resource

记得我们在 ResourceDefinitions 模块中创建的 RESOURCE_TYPE_[type] 常量吗?这就是它们存在的原因。多亏了 RESOURCE_TYPE_[type] 常量的创建,我们现在知道如何正确地加载资源。当我们定义一个资源时,我们必须包含一个 type 键,并使用其中一个资源类型。我们很快就会坚持这一点。我们现在所做的是调用正确的 load 方法,用于图像、平铺图像、字体和声音,使用存储在 resourceDefinition.type 中的值,如下所示:

    if (resourceDefinition.type == RESOURCE_TYPE_IMAGE) then
        resource = self:loadImage ( resourceDefinition )
    elseif (resourceDefinition.type == RESOURCE_TYPE_TILED_IMAGE) then
        resource = self:loadTiledImage ( resourceDefinition )
    elseif (resourceDefinition.type == RESOURCE_TYPE_FONT) then
        resource = self:loadFont ( resourceDefinition )
    elseif (resourceDefinition.type == RESOURCE_TYPE_SOUND) then
        resource = self:loadSound ( resourceDefinition )
    end

在加载当前资源后,我们将其存储在 cache 表中,使用 name 参数指定的条目,如下所示:

    -- store the resource under the name on cache
    cache[name] = resource
    end
end

现在,让我们看看所有不同的加载方法。实际的函数之前的预期定义是为了在阅读时提供参考。

图像

加载图像是我们已经做过的事情,所以这看起来会有些熟悉。

在这本书中,我们将有两种定义图像的方法。让我们来看看它们:

{
    type = RESOURCE_TYPE_IMAGE
    fileName = “tile_back.png”,
    width = 62, 
    height = 62,
}

如你所猜,type 键是在 load 函数中使用的。在这种情况下,我们需要将其设置为 RESOURCE_TYPE_IMAGE 类型。

在这里,我们定义了一个具有特定 widthheight 值的图像,并且它位于 assets/title_back.png。记住,我们将使用 ASSET_PATH 以避免多次写入 assets/。这就是为什么我们在定义中不写它的原因。

另一个有用的定义是:

{
    type = RESOURCE_TYPE_IMAGE
    fileName = “tile_back.png”,
    coords = { -10, -10, 10, 10 }
}

当你想要在较大图像中特定矩形时,这很有用。你可以使用 cords 属性来定义这个矩形。例如,通过指定 coords = { -10, -10, 10, 10 },我们得到一个边长为 20 像素的正方形,其中心位于图像中。

现在,让我们看看实际的 loadImage 方法,看看这一切是如何结合在一起的:

function ResourceManager:loadImage ( definition )
    local image

首先,我们使用与之前相同的技术定义一个空变量,该变量将保存我们的图像:

    local filePath = ASSETS_PATH .. definition.fileName

我们通过将定义中 fileName 的值附加到 ASSETS_PATH 常量的值来创建实际的全路径。if 检查 coords 属性是否已定义:

    if definition.coords then
    image = self:loadGfxQuad2D ( filePath, definition.coords )

然后,我们使用另一个辅助函数,称为 loadGfxQuad2D。这个函数将负责创建实际图像。我们使用另一个辅助函数的原因是,用于创建图像的代码在两种定义风格中都是相同的,但定义中的数据需要以不同的方式处理。在这种情况下,我们只需传递矩形的坐标。

  else 
    local halfWidth = definition.width / 2
    local halfHeight = definition.height / 2
    image = self:loadGfxQuad2D(filePath, {-halfWidth, -halfHeight, halfWidth, halfHeight} ) 

如果没有 coords 属性,我们会假设图像是使用 widthheight 定义的。所以我们做的是定义一个覆盖整个宽度和高度的矩形。我们通过计算 halfWidthhalfHeight 并将这些值传递给 loadGfxQuad2D 方法来完成此操作。记住 Moai SDK 中关于纹理坐标的讨论;这就是为什么我们需要将维度除以 2 并将它们作为负数和正数参数传递给矩形的原因。这允许它在 (0, 0) 上居中。在加载图像后,我们返回它,以便它可以通过 load 方法存储在缓存中:

    end
    return image
end

现在我们需要编写的最后一个方法是 loadGfxQuad2D。这个方法基本上与我们在上一章中用于显示图像的方法相同:

function ResourceManager:loadGfxQuad2D ( filePath, coords )
    local image = MOAIGfxQuad2D.new ()

    image:setTexture ( filePath )
    image:setRect ( unpack(cords) )

    return image
end

小贴士

Lua 的 unpack 方法是一个很好的工具,它允许你将一个表作为单独的参数传递。你可以用它将一个表拆分成多个变量:

x, y = unpack ( position_table )

我们在这里所做的是实例化 MOAIGfxQuad2D 类,设置我们在上一个函数中定义的纹理,并使用我们构建的坐标来设置图像将从原始纹理中使用的矩形。然后我们返回它,以便 loadImage 可以使用它。

好吧!这就是关于图像的所有内容。一开始可能看起来很复杂,但实际上并不复杂。

其余的资产将比这更简单,所以如果你理解了这个,其余的就会变得容易。

瓦片图像

另一种有用的资产类型是瓦片图像。在游戏中,我们通常使用瓦片图像来加载单个纹理,并使用其片段来完成特定目的(例如,可以使用这种技术实现动画)。这将在我们的游戏 Concentration 中很有用,用于定义哪些瓦片应该放置在矩阵中。

瓦片图像定义如下:

{
    type = RESOURCE_TYPE_TILED_IMAGE, 
    fileName = ‘tiles.png’, 
    tileMapSize = {3, 2}
}

这基本上与正常图像相同,但我们需要添加 tileMapSize 并移除 widthheight。这个表将被解包为 MOAITileDeck2D:setRect 的参数(请参阅 getmoai.com/docs/class_m_o_a_i_tile_deck2_d.html#a504da8814f74038af9d453ebbc7fd5ae)。在这种情况下,它表示瓦片图有 3 列和 2 行,总共 6 个瓦片。

loadTiledImage 的代码如下:

function ResourceManager:loadTiledImage ( definition )

    local tiledImage = MOAITileDeck2D.new ()

    local filePath = ASSETS_PATH .. definition.fileName

    tiledImage:setTexture ( filePath )

这基本上与我们对图像所做的是相同的。我们将 fileName 添加到 ASSETS_PATH 以获取最终的纹理 filePath

    tiledImage:setSize ( unpack (definition.tileMapSize) )

唯一真正的区别是我们将 tileMapSize 属性解包以传递给 setSize。您可以使用所有可以传递给 setSize 的可能参数;这些在 Moai SDK API 文档中有解释。

    -- return the final image
    return tiledImage

end

这与正常图像的处理方式基本相同。我们将 tiledImage 返回以供后续使用。

字体

字体更容易,其定义如下:

{
    type = RESOURCE_TYPE_FONT,
    fileName = ‘myfont.ttf’,
    glyphs = “0123456789”,
    fontSize = 26,
    dpi = 160
}

除了类型和文件名之外,我们这里的具体属性还包括 glyphs(这是我们将会使用的字符;在这个例子中,我们只使用了从 0 到 9 的数字)、fontSize 和字体的大小 dpi

loadFont 方法的代码如下:

function ResourceManager:loadFont ( definition )
    local font = MOAIFont.new ()
    local filePath = ASSETS_PATH .. definition.fileName
    font:loadFromTTF ( filePath, definition.glyphs, 
    definition.fontSize, definition.dpi )
    return font
end

整个方法基本上与其他加载器相同;区别在于我们使用 MOAIFont 作为类,并使用 loadFromTTF 以及必要的参数来初始化它。

声音

声音的定义如下:

{
  type = RESOURCE_TYPE_SOUND, 
  fileName = ‘sugarfree.mp3’, 
  loop = true,
  volume = 0.6
}

声音的具体属性包括 loop(是否应该持续循环播放)和 volume(初始音量)。

Moai SDK 包含对两种不同声音引擎的支持,Untz,一个专门为 Moai SDK 创建的开源引擎,以及 FMOD,一个行业标准的声音库。值得一提的是,FMOD 需要付费,而 Untz 是免费的。

我们将在本书中向您展示如何使用 Untz,但您可以将 loadSound 编码为使用 FMOD(请参阅 Moai SDK API 文档中的 MOAIFmodExSoundMOAIFmodExChannel)。

function ResourceManager:loadSound ( definition )
    local sound = MOAIUntzSound.new ()

    local filePath = ASSETS_PATH .. definition.fileName
    sound:load ( filePath )

    sound:setVolume ( definition.volume )
    sound:setLooping ( definition.loop )

    return sound
end

这基本上与之前的加载器逻辑相同,但我们使用 setVolumesetLooping 来使用我们在资源定义中选择的属性。我们尚未初始化 Untz;这将在稍后完成,但这段代码已经在这里解释,以完善 ResourceManager

练习

将这些组合起来应该是你现在可以实现的。所以,取我们之前创建的 main.lua 文件的源代码,并对其进行修改以使用我们的 ResourceManagerResourceDefinitions 实体。您需要在 main.lua 中包含 resource_definition.luaresource_manager.lua,然后充分利用它们。如果您从网站上下载本章的源代码,main.lua 已经被修改为与 ResourceManager 一起工作。

另一个练习是编写一种方法,从ResourceManager中卸载资源。这一功能也在本章的代码中实现,您可以从网站上下载。

注意

unload方法非常重要,因为实际上需要它来释放为资源保留的内存。

另一个有趣的话题是增强我们的ResourceManager实体,即使用序列化字体,以便在运行时更快地加载字体。

摘要

在本章中,我们创建了一个简单的框架来处理资源。我们创建了定义图像、声音和字体的方法,并编写了ResourceDefinitions类来存储这些数据。我们还编写了ResourceManager类,该类负责加载不同的资源并对其进行缓存。我们还介绍了为这些不同资源创建加载函数的方法,并解释了如何定义它们。现在,我们已准备好开始开发我们游戏的游戏玩法;这有多么酷啊!

第七章. 集中游戏玩法

在简单讨论了资产并设置了我们的 ResourceManager 之后,我们将开始构建 集中 游戏玩法。我们将讨论输入处理、基本动画和游戏状态。这些都是游戏实现的基础,并且对于使用 Moai SDK 开发的任何大小游戏都将非常有用。

网格

我们将要做的第一件事是显示一个瓦片网格,这将是我们玩耍的游乐场。仅为了开始,我们将显示瓦片的背面。

瓦片图

游戏开发中常用的一种技术是实现 瓦片集

理念是创建一个包含多个资产的单一图像。

我们将创建一个名为 tiles.png 的瓦片集。它由 12 个瓦片组成,11 种不同的颜色(灰色用于瓦片的背面),以及“空瓦片”(白色)。它看起来有点像以下这样:

瓦片图

你应该意识到我们将要做的是创建一个索引如下所示的瓦片图像:

瓦片图

这是将实际图像分成两行和六列的结果。我们将最终拥有十二个 62 x 62 的瓦片。

实现

让我们从实现开始:

  1. 我们需要创建一个名为 game.lua 的文件,它将包含我们游戏玩法的所有代码。

  2. 打开它,让我们开始编码。

    module("Game", package.seeall)
    GRID_COLS = 5
    GRID_ROWS = 4
    GRID_TILE_WIDTH = 62
    GRID_TILE_HEIGHT = 62
    BACK_TILE = 1
    
  3. 这些常量将变得很有用,因为它们定义了网格尺寸(行、列和瓦片大小)。最后一个用于在设置默认网格状态时避免使用魔法数字。正如我们所见,我们将用作瓦片背面的灰色瓦片索引为数字 1(因为它在我们瓦片集中是第一个)。

    local resource_definitions = {
    
      tiles = {
        type = RESOURCE_TYPE_TILED_IMAGE, 
        fileName = 'tiles.png', 
        tileMapSize = {6, 2},
      },
    
    }
    
  4. 这现在应该对你来说很熟悉了。为了定义我们的瓦片集,我们将使用我们之前创建的名为 tiles.png 的瓦片图像,它有六列和两行。

    function Game:start ()
      -- Do the initial setup
      self:initialize ()
    
    end
    
  5. 我们正在创建一个名为 Game:start() 的方法,它将负责初始化一切并控制游戏循环。这将在稍后的 main.lua 中被调用。现在,我们只会显示初始化时的网格。所以在这里,我们将调用一个名为 initialize() 的函数,它将负责所有的实际初始化。我们稍后将在该方法中添加更多代码。

  6. initialize() 函数如下:

    function Game:initialize ()
    
      self.layer = MOAILayer2D.new ()
      self.layer:setViewport ( viewport )
    
    1. 我们创建一个层来处理游戏玩法中的所有渲染(包括我们的网格)并配置其视口。

        MOAIRenderMgr.setRenderTable ({ self.layer })
      
    2. 然后我们使用该层设置渲染表。

        ResourceDefinitions:setDefinitions (resource_definitions)
      
    3. 下一步是使用带有辅助方法 setDefinitions()ResourceDefinitions

        self:initializeTiles ()
      
    4. 在加载资源定义后,我们调用另一个辅助函数来初始化瓦片。

      end
      

    注意

    setDefinitions()背后的想法是取一个定义表,并一次性加载所有这些定义,而不是逐个加载。它在前一章中没有创建,但应该很容易实现。只需遍历参数表,并对每个条目调用setDefinitions()。请尝试自己实现它。记住,你总是可以下载本书的代码,方法就在那里。

  7. 现在我们需要处理initializeTiles()

    function Game:initializeTiles ()
    
      local grid = MOAIGrid.new ()  
      grid:setSize ( GRID_COLS, GRID_ROWS, 
                     GRID_TILE_WIDTH, GRID_TILE_HEIGHT )
    
    1. 首先,我们使用MOAIGrid类创建网格。然后我们设置它。我们使用之前定义的网格和瓦片尺寸来配置我们的新MOAIGrid

        grid:setRow ( 1, BACK_TILE, BACK_TILE, 
                         BACK_TILE, BACK_TILE, BACK_TILE )
        grid:setRow ( 2, BACK_TILE, BACK_TILE, 
                         BACK_TILE, BACK_TILE, BACK_TILE )
        grid:setRow ( 3, BACK_TILE, BACK_TILE, 
                         BACK_TILE, BACK_TILE, BACK_TILE )
        grid:setRow ( 4, BACK_TILE, BACK_TILE, 
                         BACK_TILE, BACK_TILE, BACK_TILE ) )
      
    2. 现在我们设置实际的网格。我们使用行号作为第一个参数调用setRow()方法。(从1开始;记住,我们使用 Lua!)以下参数是我们想要显示的每个列的瓦片值。我们将以初始状态在所有网格上显示背面瓦片,这就是为什么我们传递BACK_TILE作为瓦片编号的原因。

      小贴士

      好吧,先玩一会儿,改变这些参数的值。(不过第一个参数除外;那是行!)在211之间输入一些数字;你应该会看到灰色海洋上出现一些颜色。

        self.tiles = {}
        self.tiles.grid = grid
        self.tiles.tileset = ResourceManager:get ('tiles')
      
        self.tiles.prop = MOAIProp2D.new ()
        self.tiles.prop:setDeck ( self.tiles.tileset )
        self.tiles.prop:setGrid ( self.tiles.grid )
      
    3. 然后,我们创建一个名为tiles的表,并将瓦片集存储在那里,以及一个新的属性。属性的使用基本上与我们之前做的一样;唯一的区别是我们现在使用setGrid()来使属性跟随网格的大小和配置。

       self.tiles.prop:setLoc ( - GRID_COLS/2 * GRID_TILE_WIDTH,
                               - GRID_ROWS/2 * GRID_TILE_HEIGHT )
      
    4. 由于网格不会在其元素上居中(它们只是从偏移量绘制它们),我们需要自己居中它们。在这种情况下,(0,0)位于属性的左下角(而不是中间)。我们需要偏移它,这就是我们通过调用setLoc()方法来做的;我们通过瓦片的宽度的一半(在x轴上)和瓦片高度的一半(在y轴上)移动它。

      小贴士

      Moai SDK 有一系列处理位置、旋转和缩放的方法。有三个系列,即set*方法(强制设置特定值)、move*方法(通过在一段时间内应用增量生成动画)和seek*方法(通过在一段时间内从实际值到指定值生成动画)。要修改位置,使用Loc,旋转使用Rot,缩放使用Scl。所以,例如,如果你想将tiles属性在 10 秒内旋转 30 度,你会调用self.tiles.prop:moveRot(30, 10)

      这些方法还有一些其他内容(比如它们用来插值值的曲线);你应该在getmoai.com/docs/class_m_o_a_i_transform2_d.html上查看它们。

        self.layer:insertProp ( self.tiles.prop )
      end
      
    5. 我们将属性添加到层中,然后就可以完成了。

  8. 剩下的唯一一件事就是从main.lua中调用这个函数。在视口定义下方添加以下代码:

      function mainLoop ()
        Game:start ()
      end
      gameThread = MOAICoroutine.new ()
      gameThread:run ( mainLoop )
    

我们将在一个单独的 Lua 协程中运行我们的游戏;这样做是为了允许它有一个与主协程流程分离的游戏循环。这将便于处理输入。

这就足够了;如果你现在运行项目,你应该能看到带有所有 20 个背面瓷砖的网格。

输入

我们旅程的下一步是添加输入处理。我们必须创建一个名为input_manager.lua的文件;它将负责监听输入事件,并在需要时被我们的游戏查询。我们将创建它,使其可以与鼠标设备和触摸屏一起使用。

注意

一般而言,所有主机都应该实现它们自己的输入模式。在这本书中,我们不会详细看到这一点,但为了您理解这从何而来,请查看随 Moai SDK 一起提供的宿主。

moai-sdk/hosts/src/GlutHost.cpp中搜索所有对AKUReserveInputDevice*AKUSetInputDevice*方法调用的引用。

我们现在将检查最重要的方法;请查看完整的代码以了解一切是如何结合在一起的。

function InputManager:initialize ()

  if MOAIInputMgr.device.pointer then

  local pointerCallback = function ( x, y )
    previousX, previousY = pointerX, pointerY
    pointerX, pointerY = x, y

    if touchCallbackFunc then
      touchCallbackFunc ( MOAITouchSensor.TOUCH_MOVE, 1,
        pointerX, pointerY, 1 )
    end
  end

  MOAIInputMgr.device.pointer:setCallback ( pointerCallback )

end

如您所见,此方法检查我们是否有指针,如果有,它将创建一个回调函数(pointerCallback),并将其传递给该指针以更新xy位置。它还会与触摸传感器(MOAITouchSensor)通信,以便在不是使用鼠标而是触摸屏的情况下执行相同的操作。

此外,另一个重要的方法是isDown(),它用于知道我们是否在点击或触摸屏幕。

function InputManager:isDown ()

  if MOAIInputMgr.device.touch then

    return MOAIInputMgr.device.touch:isDown ()

  elseif MOAIInputMgr.device.pointer then

    return (
      MOAIInputMgr.device.mouseLeft:isDown ()
    )
  end
end

此方法正在查询鼠标和触摸传感器,询问我们是否在点击或触摸。我们将在游戏循环中使用此方法来识别输入。请记住,在main.lua中的require 'resource_manager'语句之后包含此文件。

游戏玩法

现在,让我们看看实现游戏玩法所需的元素。首先,我们需要创建游戏状态的所有结构(并初始化它们)。我们将使用另一个网格,这次用来存储数字的随机分布。

初始化

  1. Game:initialize()方法的底部添加对辅助函数的调用。

      self:restartGamePlay ()
    
  2. 还要添加辅助函数。

    function Game:restartGamePlay ()
      self.distributionGrid = MOAIGrid.new ()
      self.distributionGrid:setSize (GRID_COLS, GRID_ROWS)
    
    1. 首先,我们创建一个网格,它将持有我们颜色的分布。这个网格也有一个 5 x 4 的维度。请注意,我们现在不需要设置瓷砖大小,因为这个网格不会被渲染。

        local tiles = {
          2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 
          7, 7, 8, 8, 9, 9, 10, 10, 11, 11
        }
      
    2. 这个表只是一个临时瓷砖数组;如您所见,我们从2开始(记住,1是背面瓷砖),每个数字重复两次。这些数字代表我们的瓷砖集中的颜色(或瓷砖偏移量)。

          for x=1, GRID_COLS, 1 do
            for y=1, GRID_ROWS, 1 do
              local random =  math.random ( 1, #tiles )
              local value = tiles[random]
              table.remove ( tiles, random )
              self.distributionGrid:setTile ( x, y, value )
            end
          end
      
    3. 下一步我们要做的是遍历所有的网格单元,随机地将其中一个值分配给tiles表,并从表中移除它,这样它就不会被选中超过一次。通过这种方式,我们以随机顺序填充了distributionGrid中的颜色对。

        self.selectedCells = { nil, nil } 
      end
      
    4. 下一步我们需要的是一个地方来存储当前回合中选中的单元格。我们将使用一个名为selectedCells的表,并且它将显示两个nil单元格,因为我们还没有选择任何东西。

游戏初始化部分到此结束。

输入处理

现在我们需要处理用户的输入。

  1. 我们需要在Game:start ()方法中添加一些代码(在调用Game:initialize ()之后):

     self.wasClicking = false
    
    
    1. 我们想知道在之前的模拟步骤中,用户是否在点击或轻触屏幕。我们将该信息存储在wasClicking中。

        while (true) do
          self:processInput ()
          coroutine.yield ()
        end 
      
    2. 然后我们添加一个无限循环(注意算法!)来处理输入并使用神奇的方法coroutine.yield(),该方法将流程委托给主程序,允许MOAISim进行另一个模拟步骤。这样,我们避免了无限循环而使游戏冻结。相反,我们完成所有的渲染、动作树和节点图处理;当所有这些都完成时,它回到我们的协程再次处理输入。

      小贴士

      如果你期望你的游戏更新屏幕(渲染)但它没有这样做,那么很可能是你忘记运行coroutine.yield()来允许在继续代码之前进行渲染。例如,考虑加载屏幕。

  2. 现在我们将处理下一个方法,即实际处理输入的方法。

    function Game:processInput ()
      if InputManager:isDown () and not self.wasClicking then
    
    1. 如果玩家现在正在点击,而在之前的模拟步骤中没有点击(为了避免在按住鼠标按钮时多次选择),我们想要选择一个方块。

          x, y = InputManager:position ()
      
    2. 我们选择方块的方式是一个相当巧妙的技巧。我们收集表示在窗口坐标系中的鼠标位置。

          worldX, worldY = self.layer:wndToWorld ( x, y )
          modelX, modelY = self.tiles.prop:worldToModel ( worldX, worldY )
      
    3. 我们需要将它们转换为模型坐标,通过首先将它们转换为世界坐标(通过层)来完成我们的技巧。在这些转换之后,我们在modelXmodelY中得到的是从网格左下角开始的像素偏移。

        cellColumn = math.floor ( modelX / GRID_TILE_WIDTH ) + 1
        cellRow = math.floor ( modelY / GRID_TILE_HEIGHT ) + 1
      
    4. 如果我们将modelXmodelY除以单元格的尺寸,我们将得到单元格的列和行索引。这是很棒的数学魔法。现在只需要调用一个辅助函数,该函数将处理点击的后果:

          self:choseCell ( cellColumn, cellRow )
        end
      
        self.wasClicking = InputManager:isDown ()
      
      end
      
    5. 哦!别忘了更新wasClicking。我们需要在它上面存储InputManager:isDown ()的值,因为我们将在下一个模拟步骤中使用它。

选择单元格

现在我们必须实现以下单元格选择方法:

function Game:choseCell ( column, row )

  if not self.selectedCells[1] then
  1. 如果之前没有选中任何单元格(第一个是 nil),我们必须选中它。

        if not self:alreadySelectedTile ( column, row ) then
    
  2. 我们始终需要检查我们点击的单元格是否没有被从游戏中移除。

          self.selectedCells[1] = {column, row}
    
  3. 我们将选中的单元格的列和行放入selectedCells表中。

          self:swapTile ( column, row )
    
  4. 然后我们交换它以显示其颜色。

        end
      else
    
  5. 如果之前已经选中了方块,我们继续处理该情况。

        if (self.selectedCells[1][1] == column) and (self.selectedCells[1][2] == row) then
          self.selectedCells[2] = {column, row}
          self:resetTiles ()
    
  6. 如果玩家点击了相同的方块,我们希望将其换回。将相同的方块设置为第二个选中的单元格(selectedCells[2])并重置它们。

        else
    
          if not self:alreadySelectedTile ( column, row ) then
    
  7. 记住检查我们点击的单元格是否没有被从游戏中移除。

            self.selectedCells[2] = {column, row}
            self:swapTile ( column, row )
    
  8. 同样的逻辑也应用于此处以选择单元格并交换它。(哦,不!代码重复!)

  9. 现在是游戏的核心:

            local value1 = self.distributionGrid:getTile (
                             unpack(self.selectedCells[1]) )
    
            local value2 = self.distributionGrid:getTile (
                             unpack(self.selectedCells[2]) )
    
  10. 我们收集这两个值并将它们分别存储在 value1value2 中。这些变量包含用户所选瓷砖的颜色。

            if (value1 == value2) then
              self:removeTiles ()
    
  11. 如果颜色相同,我们移除瓷砖。

            else
              self:resetTiles ()
    
  12. 如果它们不同,我们将它们交换回来。就这样。

            end
          end
        end
      end
    end
    

在一段美好的 end 句尾之后,我们准备实现辅助方法。

瓷砖交换

为了交换我们的瓷砖,我们将创建以下 swapTile () 方法:

function Game:swapTile ( column, row )
  local value = self.distributionGrid:getTile ( column, row )
  self.tiles.grid:setTile ( column, row, value )
end

我们在这里做的是从我们的 distributrionGrid 中检索颜色值并将其设置到渲染网格中。这真的很神奇;你不爱它吗?

结果是瓷砖的颜色根据我们的 distributionGrid 上的分布而改变。

每个分布都是随机的,所以每次你玩的时候都会不同。

重置瓷砖

  1. 我们需要在 game.lua 的顶部添加两个更多常量,用于下一个方法(我们将使用这些常量来避免魔法数字):

    EMPTY_TILE = 12
    

    EMPTY_TILE 是我们在所有瓷砖之后剩下的空白空间;我们将用它来移除不在游戏中的瓷砖。这是索引 12

    DELAY_TIME = 0.5
    

    DELAY_TIME 是我们将用来延迟瓷砖交换回的时间(秒),以便用户可以看到他选择的第二种颜色。(实际上,在完成章节后将其更改为零并看看会发生什么)。

  2. 现在,我们创建我们的 resetTiles () 方法:

    function Game:resetTiles ()  
      sleepCoroutine(DELAY_TIME)
    
    1. 我们在 DELAY_TIME 秒内冻结协程。我们将在以下代码片段中实现 sleepCoroutine

        self.tiles.grid:setTile (
          self.selectedCells[1][1], 
          self.selectedCells[1][2], BACK_TILE )
      
        self.tiles.grid:setTile (
          self.selectedCells[2][1], 
          self.selectedCells[2][2], BACK_TILE )
      
    2. 这个可能看起来有点棘手,但我们所做的是告诉我们的渲染网格,selectedCells 中的单元格应该渲染 BACK_TILE。我们使用每个选中单元格的坐标并将它们传递给 setTile 作为前两个参数。第三个是颜色,所以我们使用 BACK_TILE

        self.selectedCells = {}
      
    3. 我们清除选中的单元格以重新开始。

      end
      

就这些了。现在我们需要注意移除瓷砖。

移除瓷砖

以下方法将用于移除相等的瓷砖并正确地交换用户。

function Game:removeTiles ()
  sleepCoroutine ( DELAY_TIME )

  self.tiles.grid:setTile ( self.selectedCells[1][1], self.selectedCells[1][2], EMPTY_TILE )
  self.tiles.grid:setTile ( self.selectedCells[2][1], self.selectedCells[2][2], EMPTY_TILE )
  1. 这与我们在重置瓷砖时所做的非常相似。唯一的区别是,我们在这里使用 EMPTY_TILE 常量来使瓷砖消失。

      self.selectedCells = {}
    
  2. 我们清除选中的单元格以重新开始。

    end
    

其他辅助方法

还有两个辅助方法待定;一个用于检查瓷砖是否已经被从游戏中移除,另一个用于使协程休眠。所以,它们就在这里:

function Game:alreadySelectedTile ( column, row )
  return self.tiles.grid:getTile ( column, row ) == EMPTY_TILE
end
  1. 此方法用于确定我们是否移除了给定的瓷砖。基本上,我们将其颜色值与 EMPTY_TILE 进行比较。如果它们相等,则该瓷砖之前已被移除。

  2. 现在是奇怪(但极其有用)的一个:

    function sleepCoroutine (time)
      local timer = MOAITimer.new ()
      timer:setSpan ( time )
      timer:start()
      MOAICoroutine.blockOnAction ( timer )
    end
    

在这种方法中,我们所做的是冻结给定时间的协程。我们使用MOAITimer来实现这一点。这里的魔法在于定时器继承自MOAIAction,因此我们可以使用内置的MOAICoroutine.blockOnAction()方法,它基本上会等待动作完成,然后继续在当前线程上执行。

小贴士

这是一个重要的方法,您很快就会看到。它用于在 Moai SDK 中创建动画序列,当您需要在另一个动作开始之前等待一个动作完成时(例如,按顺序而不是并行执行一个平移和一个旋转)。

好吧,游戏基本上已经准备好可以玩了。显然,您需要添加一些东西才能使其成为可以发布的游戏(例如,胜利条件、一个漂亮的菜单、一个滴答作响的时钟等等),但游戏玩法已经到位,这正是我们所寻找的。

摘要

在本章中,我们学习了如何使用网格在屏幕上显示瓦片,以及如何表示游戏数据。我们学习了输入处理的基础知识以及如何实现专注力的实际游戏玩法。我们还首次了解了 Moai SDK 如何使用协程。现在是一个休息的好时机,因为接下来的章节内容丰富;我们将构建一个使用 Box2D 进行物理运算的平台游戏原型!

第八章。让我们构建一个平台游戏!

既然我们已经熟悉了处理输入和资源,为什么不尝试构建一个平台游戏原型呢?这将是一个很好的挑战,将使我们接触到更多概念和与 Moai SDK 一起工作的方法。

为了构建这个游戏,我们将复制上一章代码的全部内容,并从game.lua中清除所有特定于Concentration游戏的代码。

  • 删除文件顶部的所有常量,对瓷砖的定义(但保留resource_definitions,因为我们将要使用它)。

  • Game:start ()方法中删除self.wasClicking = false

  • Game:initialize ()中删除对initializeTilesrestartGamePlay的调用。

  • 删除方法Game:initializeTiles()Game:restartGamePlay ()Game:choseCell ()Game:alreadySelectedTile()Game:swapTile ()Game:resetTiles ()Game:removeTiles()

这应该就足够了;现在您应该可以开始了。

背景

我们将向屏幕插入一个背景:

  1. 我们要做的第一件事是显示一个背景。现在这对您来说应该很容易。一个好主意是搜索www.opengameart.org以找到合适的背景。这正是本章代码中所做的,您将在其中找到一个背景。

  2. 让我们添加定义。如果您使用的是书中代码中未提供的图像,只需更改尺寸和文件名即可(此代码应放在resources_definition内部):

      background = {
        type = RESOURCE_TYPE_IMAGE, 
        fileName = 'background/wizardtower.png', 
        width = 1000, height = 700
      },
    
  3. 现在,让我们在initialize ()方法的底部添加一行来加载背景:

    self:loadBackground ()
    
  4. 现在让我们定义这个方法:

    function Game:loadBackground()
    
      self.background = {}
      self.background.deck = ResourceManager:get('background')
    
      local prop = MOAIProp2D.new ()
      prop:setDeck (self.background.deck)
      prop:setLoc (0, 0)
    
      self.background.prop = prop
    
      self.layer:insertProp (prop)
    
    end
    

这不是什么新东西,对吧?这是我们之前在第五章中加载图像所做的一样,在屏幕上显示图像

现在,让我们玩一下摄像机并滚动它。

小贴士

在加载背景时,请考虑您要针对的平台。

纹理的最大尺寸根据着色器模型而有所不同。如果您使用的是 1024 x 1024 或更小的纹理,那么您将是安全的;如果您发现有些纹理没有显示在屏幕上,请再次检查这一点。请记住,您始终可以将纹理分割成更小的部分,并在不同的 Props 中加载多个。我们在The Insulines的第二场景中遇到了这个问题;由于它太大,一些电脑上无法工作,因此我们不得不将其分割。

摄像机和滚动

如果您计划创建 2D 游戏,一个重要的事情是要掌握如何使用摄像机,这正是我们现在要做的。

game.lua中的Game:initialize ()的开始处输入以下代码:

  self.camera = MOAICamera2D.new ()
  1. 我们在这里所做的就是使用内置的MOAICamera2D类创建一个新的摄像机。

  2. 接下来,我们需要将摄像机分配给每一层。在视口分配(self.layer:setViewport ( viewport ))下方,添加以下代码行:

      self.layer:setCamera ( self.camera )
    

    这将层绑定到那个相机。就是这样;当你移动相机时,你会看到视口会跟随,如果你上下缩放相机,它会相应地放大和缩小。我们将写几行代码来演示这一点。

  3. Game:processInput ()中编写以下代码:

      local x, y = InputManager:deltaPosition () 
    

    我们正在使用InputManager中的一个方法,该方法返回当前鼠标位置和上一个位置之间的差异。

      self.camera:moveLoc ( x, 0, 0.5, MOAIEaseType.LINEAR )
    

我们使用moveLoc ()方法移动相机,并通过x传递增量,y不传递任何内容。第三个参数告诉moveLoc ()用半秒钟的时间来完成移动。第三个参数用于定义动画的曲线,在这种情况下,我们使其线性,所以它将产生速度恒定的移动。我们稍后会更深入地探讨这一点。

小贴士

MOAIEaseType定义了我们动画中一些有用的曲线;你可以在getmoai.com/docs/class_m_o_a_i_ease_type.html查看它们。

如果你现在运行游戏,你会看到当你移动鼠标时,背景向左和向右滚动。这是由于相机移动的结果。

所以,当你想要滚动时,你只需要移动相机,而不是移动屏幕上的所有对象。

我们稍后可以滚动相机以跟随角色的移动。

垂直透视

垂直透视是一种在游戏中经常使用(并且滥用)的效果,因为它在 2D 场景中创造出深度感。主要思想是创建不同距离的图像层,当滚动时,这些图像以不同的速度移动。这模仿了在现实中,从观众(观察者)距离不同的物体(远处的物体移动速度比近处的物体慢)发生的情况。

我们可以通过将背景分割成不同的图像(每个图像代表一个不同的平面)来实现这一点。

垂直透视

在这里,我们看到旧的背景被分割成三层:

  • 有云的背景

  • 一个较远的塔

  • 一些较近的小山丘

现在,我们需要做的是让它们以不同的速度移动。

设置层

在 Moai SDK 中,你可以使用一个名为MOAILayer2D的方法setParallax ()来实现这一点。

让我们来看看它。

  1. 正如我们所见,我们需要几个层来完成这个任务。我们将用层的表格替换当前的层。在Game:initialize ()中,用对self:setupLayers ()的调用替换层初始化的行。

  2. 然后我们创建实际的方法:

    function Game:setupLayers ()
    
      self.layers = {}
      self.layers.background = MOAILayer2D.new ()
      self.layers.farAway = MOAILayer2D.new ()
      self.layers.main = MOAILayer2D.new ()
    
    1. 我们将为每个我们想要的深度平面创建一个层。在这个例子中,我们将使用三个平面:一个背景平面,一个远处的平面和一个主要平面。(我们的角色和所有游戏对象都应该放在主要平面内)

       for key, layer in pairs ( self.layers ) do
          layer:setViewport ( viewport )
          layer:setCamera ( self.camera )
        end
      
    2. 然后我们遍历所有层,并正确地分配视口和相机。

        local renderTable = {
          self.layers.background,
          self.layers.farAway,
          self.layers.main
        }
      
    3. 现在,我们按顺序创建一个包含层的渲染表。

        MOAIRenderMgr.setRenderTable(renderTable)
      end
      
    4. 我们将使其变得活跃。

现在我们有了可以玩垂直透视的层。

向不同距离添加图像

现在,我们将显示每个层的图像,以便您可以看到这是多么酷:

local resource_definitions = {

  background = {
    type = RESOURCE_TYPE_IMAGE, 
    fileName = 'background/background_parallax.png', 
    width = 1500, height = 197,
  },

  farAway = {
    type = RESOURCE_TYPE_IMAGE, 
    fileName = 'background/far_away_parallax.png', 
    width = 625, height = 205,
  },

  main = {
    type = RESOURCE_TYPE_IMAGE, 
    fileName = 'background/main_parallax.png', 
    width = 975, height = 171,
  },

}
  1. 我们需要这三个资源定义:每个层一个。这些图像可以在本章的源代码中找到。

    我们还需要一些其他信息,但不是将其添加到resource_definitions表中,我们将创建另一个表。即使对象和定义看起来相同,它们之间在概念上存在细微的区别,所以最好将它们分开。

  2. resource_definitions表下方写下以下代码:

    local background_objects = {
    
      background = {
        position = { 0, 70 },
        parallax = { 0.05, 0.05 }
      },
    
      farAway = {
        position = { 0, 50 },
        parallax = { 0.1, 0.1 }
      },
    
      main = {
        position = { 0, -75 },
        parallax = { 1, 1 }
      },
    
    }
    

    我们在这里所做的是为创建的每个对象定义位置和视差信息。严格来说,视差是为包含对象的层。

  3. 现在,我们将重写Game:loadBackground (),使其看起来如下:

    function Game:loadBackground ()
    
      self.background = {}
    
      for name, attributes in pairs(background_objects) do
    
    1. 我们遍历在background_objects中定义的所有内容:

          local b = {}
          b.deck = ResourceManager:get ( name )
      
    2. 我们使用background_objects表中的键来加载牌组。

          b.prop = MOAIProp2D.new ()
          b.prop:setDeck ( b.deck )
          b.prop:setLoc ( unpack(attributes.position) )
      
    3. 在这里,我们使用在background_objects中定义的位置属性:

          self.layers[name]:insertProp ( b.prop )
      self.layers[name]:setParallax ( unpack(attributes.parallax) )
      
    4. 在这里,我们使用setParallax并使用我们在背景对象中定义的视差属性:

          self.background[name] = b
        end
      
      end
      

就这样,我们现在可以开始了。运行游戏并移动鼠标。你现在应该能看到三个不同的层以不同的速度移动。

现在,我们准备添加主要角色,让我们直接跳到那里!

主要角色

为了创建我们的角色,我们需要一个包含行走、跑步和跳跃动画的精灵表。

它应该看起来像以下图像:

主要角色

  1. 首先,我们将创建一个资源定义,如下所示(将其放在背景资源定义下方):

      character = {
        type = RESOURCE_TYPE_TILED_IMAGE,
        fileName = 'character/character.png',
        tileMapSize = {20, 6},
        width = 64, height = 64,
      }
    

    这与我们在Concentration游戏中使用的是一样的,但请注意,我们添加了widthheight。这是瓦片的尺寸。在Concentration游戏中,我们在网格中定义了瓦片的尺寸,但现在没有网格,所以我们需要在这里定义它。

  2. 为了处理这个定义,我们需要在resource_manager.lua中的loadTiledImage函数内添加一些行,就在调用setSize()之后:

      if definition.width and definition.height then
        local half_width = definition.width / 2
        local half_height = definition.height / 2
        tiledImage:setRect ( -half_width, -half_height, half_width, half_height )
      end
    

    前面的代码与loadImage中的代码类似;区别在于我们将widthheight除以二,并使用得到的结果作为矩形的尺寸。

  3. 让我们创建一个名为character.lua的文件来开始创建我们的角色(别忘了在game.luarequire该文件),并向其中添加以下代码:

    module ( "Character", package.seeall )
    
    local character_object = {
      position = { 0, 0 },
    }
    This table will be used to setup some initialization parameters.
    function Character:initialize (layer)
      self.deck = ResourceManager:get ( 'character' )
      self.prop = MOAIProp2D.new ()
      self.prop:setDeck ( self.deck )
      self.prop:setLoc ( unpack(character_object.position) )
      layer:insertProp ( self.prop )
    end
    

这基本上与我们在屏幕上加载图像时所做的是一样的;这里的重要部分是 initialize 接收一个层作为参数,并将 Prop 添加到该层。你能猜到为什么吗?

原因如下:在game.lua中的Game:initialize ()的底部添加以下行(就在调用loadBackground之后):

  Character:initialize ( self.layers.main )

哇,我们的角色现在出现在屏幕上了。喜欢吗?我觉得它相当静态。让我们添加一些动画。

动画

现在我们将开始处理动画。在本节中,我们将介绍三个新的类:MOAIDeckRemapperMOAIAnimCurveMOAIAnim。让我们看看它们是如何交互的。

  1. 首先,在 character.lua 中的 character_object 表上,让我们在以下位置添加以下内容:

    animations = {
        idle = {
          startFrame = 1,
          frameCount = 9,
          time = 0.1,
          mode = MOAITimer.LOOP
        },
    
        run = {
          startFrame = 41,
          frameCount = 16,
          time = 0.03,
          mode = MOAITimer.LOOP
        },
    
        jump = {
          startFrame = 89,
          frameCount = 3,
          time = 0.1,
          mode = MOAITimer.NORMAL
        },
      }
    

    这是我们要使用的三个动画的定义。看看它,你会发现我们定义了一个起始帧(动画开始的瓦片集索引),帧数(动画的帧数;这些帧需要按照前面的示例顺序放置在瓦片集中),每帧之间的时间,以及动画模式。

    类似于 MOAIEaseTypeMOAITimer 有许多不同的动画模式。你可以在 getmoai.com/docs/class_m_o_a_i_timer.html 查看它们。

  2. 然后,我们需要添加一个 MOAIDeckRemapperMOAIDeckRemapper 与动画曲线结合使用,以便告诉属性在每个动画步骤中应该显示哪个瓦片集索引。在 Character:initialize() 中输入以下代码:

      self.remapper = MOAIDeckRemapper.new ()
      self.remapper:reserve ( 1 )
    
    1. 我们将仅重映射一个索引(角色动画帧),因此我们只保留一个重映射器索引。

        self.prop:setRemapper ( self.remapper )
      
    2. 然后我们将重映射器分配给属性(Prop)。

    动画

    为了使动画能够运行,我们需要创建一个曲线。它将被用来告诉我们的 MOAIDeckRemapper 实例在给定的动画步骤中应该使用哪个瓦片集索引。

    重映射器将负责将当前属性(Prop)的索引更改为正确的索引。

    有趣的是,这些曲线是通过设置动画中给定点的值的键来定义的。然后,使用 MOAIEaseType 曲线对这些值进行插值。

    这真的很方便,因为我们不需要指定动画中的每个点,只需要两个点,其余的将通过插值生成。让我们看看它是如何工作的。

  3. 让我们创建一个方法来添加动画并逐步审查它。

    function Character:addAnimation ( name, startFrame, frameCount, time, mode )
      local curve = MOAIAnimCurve.new ()
    
    1. 首先,我们创建一个动画曲线。这些曲线用于生成在帧之间移动的特定行为。我们创建一系列点来生成曲线(插值值)。动画将在特定时间查询曲线并使用其值。

        curve:reserveKeys ( 2 )
      
    2. 是我们用来定义曲线的值。这里我们需要两个键(曲线的开始和结束),但你可能需要更多(例如,如果动画的某些部分需要比其他部分运行得更快或类似的情况)。

        curve:setKey ( 1, 0, startFrame, MOAIEaseType.LINEAR )
      
    3. 使用 setKey() 我们定义一个键:

      第一个参数是键号(从一开始)。第二个参数是键应该发生的时间;在这种情况下,由于这是动画的第一个键,所以时间是零。然后,我们传递该键的值;在我们的例子中,它将是我们的 startFrame,然后是 MOAIEaseType。缓动类型是一种告诉动画使用不同曲线的方式。在这种情况下,我们使用的是线性曲线。

        curve:setKey ( 2, time * frameCount, 
          startFrame + frameCount, MOAIEaseType.LINEAR )
      
    4. 第二个键将是最后一个。对于第二个参数(time),我们将帧之间的时间乘以帧数,应该是动画的总时间。之后,我们告诉它在startFrame之后的frameCount帧处停止,并且再次使用线性插值。

      当创建我们的动画时,这将创建一个从时间 0 的startFrame到曲线末尾的startFrame + frameCount的线性函数。这正是我们动画所需要的。

      
        local anim = MOAIAnim:new ()
        anim:reserveLinks (1)
        anim:setLink ( 1, curve, self.remapper, 1 )
      
    5. 接下来,我们创建我们的动画,预留一个将用于将曲线连接到重映射器的链接,然后我们将它们连接起来。

      这里会发生的事情是,当我们开始动画时,它将遍历曲线,并将曲线的结果传递给重映射器。当这种情况发生时,重映射器将改变 Prop 显示的图像。调用末尾的是我们将使用的重映射器的索引;因为我们只预留了一个,所以我们在这里传递一个。

        anim:setMode ( mode )
      
    6. 在这里,我们设置了定义的动画模式。

        self.animations[name] = anim
      
      end
      
    7. 完成此操作后,我们将动画存储为我们传递的参数名称下。

      这种工作方式并不简单,因此你可能需要一段时间来处理它。你应该尝试调整定义和参数,以了解MOAIAnim + MOAIAnimCurve + MOAIDeckRemapper工作流程的交互方式。

      我们将定义一些辅助方法来处理这些动画:

      function Character:getAnimation ( name )
        return self.animations[name]
      end
      
  4. getAnimation返回按名称索引的动画。

    function Character:stopCurrentAnimation ()
    
      if self.currentAnimation then
        self.currentAnimation:stop ()
      end
    
    end
    
  5. 在这个方法中,如果存在,我们将停止正在运行的动画。

  6. 以下方法用于启动动画。让我们看看它。

    function Character:startAnimation ( name )
    
      self:stopCurrentAnimation ()
    
    1. 首先,我们停止当前动画。

    2. 停止动画非常重要,因为正在运行的动画会消耗一些内存,如果我们不停止它,我们最终会有大量的动画一直在运行,消耗大量内存。我们在开发The Insulines时也发现了这一点。

        self.currentAnimation = self:getAnimation ( name )
      
    3. 然后我们通过名称索引获取动画。

        self.currentAnimation:start ()
      
    4. 然后我们启动它。

        return self.currentAnimation
      end
      
    5. 然后,我们返回它,以防我们稍后想对它做些什么(例如,阻塞协程以链接多个动画,记得吗?)。

  7. 我们需要做的最后一件事是解析在character_object内部定义的所有动画,并创建必要的动画。为此,转到Character:initialize,并在底部包含以下代码:

      self.animations = {}
    
      for name, def in pairs ( character_object.animations ) do
        self:addAnimation ( name, def.startFrame, def.frameCount,
          def.time, def.mode )
      end
    

在这里,我们正在遍历所有定义的动画,并使用属性作为addAnimation()方法的参数。

结果将是所有动画都将包含在Character.animations中,我们将能够通过之前创建的方法(startAnimationstopAnimation)访问它们。为了检查一切是否正常工作,请添加以下行:

  self:startAnimation ( 'run' )

这应该会启动正在运行的动画。你可能需要通过更改定义中的参数并调用不同的动画来稍微实验一下。

摘要

在本章中,我们开始为我们的平台游戏原型实现初始结构。我们创建了一个摄像头并学习了如何移动它。我们还尝试了层和视差效果。然后我们添加了我们的角色,并了解了如何从瓦片图像创建动画。

在下一章中,我们将使用已经随 Moai SDK 一起发布的 Box2D 引擎,将一些物理知识引入原型。

第九章. 使用 Box2D 的现实世界物理

如果你之前已经参与过游戏开发,你应该熟悉 Box2D。它是一个跨平台的物理库,在游戏社区中得到广泛使用。你会发现它与它一起工作非常容易,并且可以快速创建出看起来很棒的物理效果,但这并不意味着你必须创建另一个 愤怒的小鸟 克隆,提示提示。

创建世界

第一步是按照给定的步骤创建负责物理模拟的世界:

  1. 创建一个名为 physics_manager.lua 的文件,并从 game.luarequire 它。

  2. 现在让我们构建 PhysicsManager 模块。

    module ( "PhysicsManager", package.seeall )
    function PhysicsManager:initialize ( layer )
    
    1. 第一步是创建 MOAIBox2DWorld 对象。这将负责从 Moai SDK 与 Box2D 通信。

              self.world = MOAIBox2DWorld.new ()
      
    2. 然后,我们需要设置我们的比例。我们在这里做的是说,模拟中的 1 米相当于我们世界坐标中的 38 个点。由于我们使用的是一个高 64 个点的角色精灵,我们可以说我们的角色大约有 1.67 米高。所以,65 / 1.67 大约是 38。这就是找到这个比例所需的计算。

              self.world:setUnitsToMeters ( 1/38 )
      

      提示

      Box2D 在包含从 0.1 到 10 米的物体的模拟上工作得更好,所以在设置这个比例时要考虑这一点。

    3. 这里我们设置重力。参数是每秒在 xy 方向上我们的对象受到影响的米数。所以,在这个例子中,我们使用与地球相似的重力(地球的重力是 -9.8 mts / (s * s),所以我们可以安全地使用 -10)。

              self.world:setGravity ( 0, -10 )
      
    4. 最后,我们开始模拟:

              self.world:start ()
      
    5. 通常,我们不想在屏幕上看到 Box2D 创建的对象;我们只想让它们连接到我们自己的渲染对象,并使它们根据模拟行为。但是,出于调试目的,在屏幕上显示物理对象可以非常方便。如果你将一个层传递给 initialize 方法,它将使用它作为调试层来显示你在 Box2D 中创建的对象。你会看到物理对象的实际形状,并且当它们相互碰撞时,它们的颜色会改变。

                  if layer then
                      layer:setBox2DWorld ( self.world )
                  end
              end
      

就这样。我们的世界正在运行!现在该谈谈物体了。

Box2D 物体类型

使用物理引擎的全部目的是创建一个世界,你可以在这里放置(物体)根据物理定律行为的对象。术语 body 在物理学文献中有着很高的基础,其中所有相互作用的物体都被称为 body

Box2D 中有三种类型的物体:静态、运动学和动态。它们如下所述:

  • 静态物体:你可以把它们想象成固定在世界上。它们不会自己移动,也不会受到重力或碰撞的影响。记住,静态物体可以与其他对象碰撞,但它们不会因为碰撞而移动或改变,就像其他对象(如果它们不是静态的)一样。我们将使用静态物体作为游戏中的平台,让玩家站立。

  • 运动学体: 这些体不受碰撞或重力的影响,但它们可以自行移动。它们在碰撞期间可以影响其他对象,就像静力学体一样。一个简单的移动平台危险可能应该使用运动学体来实现。

  • 动态体: 这些体受重力、碰撞的影响,并且可以自行移动。我们将使用这些体来让我们的角色和敌人移动和碰撞。你总是可以将这些体设置为忽略某些力效果。

现在我们对体有了更多了解,让我们将地板添加到我们的游戏中。

  1. 为了做到这一点,请将以下代码追加到World:initialize()

  2. 在这里,我们使用MOAIBox2DWorldaddBody创建一个静态体。然后我们使用setTransform方法来放置它。我们在X轴上居中,并在Y轴上将它在屏幕底部放置。

        self.floor = {}
        self.floor.body = self.world:addBody ( MOAIBox2DBody.STATIC )
        self.floor.body:setTransform ( 0, -WORLD_RESOLUTION_Y/2 )
    
  3. 在此之后,我们需要定义物体的形状(在 Box2D 的上下文中称为夹具)。我们可以使用多种形状,甚至将它们组合起来创建更复杂的物体。在这个例子中,我们创建了一个矩形物体,其宽度等于屏幕大小,高度等于 100。我们定义这些矩形的方式遵循图像中矩形的惯例,以模型坐标中的(0,0)为中心。

        self.floor.fixture = self.floor.body:addRect (
        -WORLD_RESOLUTION_X/2, -50, 
        WORLD_RESOLUTION_X/2, 50 )
    
  4. 然后我们定义我们不想在地板上有摩擦。这将阻止我们的玩家在跑步时减速。

        self.floor.fixture:setFriction ( 0 )
    

现在,我们需要在game.lua中初始化我们的世界,以便在屏幕上渲染一些内容。只需将以下代码复制到loadBackground调用下方:

    PhysicsManager:initialize ( self.layers.walkBehind )

我们在这里做的是使用walkBehind层作为调试参数。如果你不想看到创建的对象,只需避免传递层,你就不会看到它们。

注意

现在运行游戏,你应该在底部看到一个盒子。那就是我们的地板。

角色与世界对比

现在,我们想要让主要角色与我们的世界进行交互。

我们将向Character模块添加一些新方法,如下所示:

  1. 首先,我们创建一个对象来存储我们的身体和形状,如下所示:

    function Character:initializePhysics ()
        self.physics = {}
    
  2. 然后我们向物理模拟添加一个动态体。这将是我们角色的主体,如下所示:

        self.physics.body = 
        PhysicsManager.world:addBody ( MOAIBox2DBody.DYNAMIC )
    
  3. 现在我们使用与我们的渲染角色相同的初始位置来定位身体,如下所示:

        self.physics.body:setTransform (
        unpack(character_object.position) )
    
  4. 现在我们添加一个形状。我们将使用与精灵大小相同的正方形(64 x 64)。这显然远非理想,因为在某些情况下碰撞可能看起来很奇怪(它将感觉像他周围有一个透明的盒子),但为了保持简单,我们可以忍受这一点。

        self.physics.fixture = self.physics.body:addRect( -32, -32, 32, 32 )
    

    角色与世界对比

  5. 在前面的图像中,我们看到角色和一个带有 Box2D 调试的平台。我们可以看到玩家应该会掉落,但不会掉落,因为它的盒子太大。

    提示

    你可能想在最终游戏中创建一个更好的形状,以便有更好的碰撞检测,但请记住,如果碰撞形状更复杂,则在碰撞时需要更多的计算。始终尝试最小化你的固定装置的顶点和边数。

  6. 最后一步是将我们的prop与我们的body绑定。为此,我们将使用setParent方法,它告诉 Action Tree 当物理体移动时会影响prop。这是一个展示 Action Tree 强大功能的绝佳例子。

        self.prop:setParent ( self.physics.body )
    end
    

如果你现在按播放,由于重力作用,角色应该会下落。

移动

现在,让我们直接进入重要的事情。首先,我们将在Character上定义几个方法。然后我们将重新构建InputManager来处理按键事件而不是鼠标事件,并将其与我们的游戏集成。

  1. 我们将要使用的第一个方法将使我们的角色跑步。它将在我们按下AD键时被调用,分别向左或向右移动。我们将传递两个参数,direction(左为-1,右为1)和keyDown,这是一个布尔值,告诉我们玩家是否在按键。

  2. 我们首先使用direction参数与setScl(用于缩放对象)结合。想法是,如果你通过一个负因子缩放精灵,它将在缩放组件的方向上翻转,所以我们在做的是告诉我们的精灵根据玩家按下的哪个键来面向左或右。

    function Character:run ( direction, keyDown )
        if keyDown then
        self.prop:setScl ( direction, 1 )
    
  3. 然后,我们通过 100 在指定方向上强制线性速度,并保持垂直轴上的速度,因为我们希望在跳跃时能够跳跃和移动:

        velX, velY = self.physics.body:getLinearVelocity ()
        self.physics.body:setLinearVelocity ( direction * 100, velY )
    
  4. 然后,如果我们没有在跑步并且没有在跳跃,我们就开始以下跑步动画:

       if(self.currentAnimation ~= self:getAnimation ( 'run' ) )and not self.jumping then
            self:startAnimation('run')
        end
    
  5. 如果这个事件是keyUp并且我们没有在跳跃,那么停止移动。这是有问题的,你应该在这里有一个更好的逻辑来保持不同键的状态,但为了简单起见,我们可以忍受它。就这样,我们的Character现在可以跑了:

        else
        if not self.jumping then
            self:stopMoving ()
        end
        end
    end
    
  6. 这两种方法都非常简单。如果我们向左移动,我们用-1作为方向调用run;如果我们向右移动,我们用1作为方向调用run。在两种情况下,我们都传递接收到的keyDown。这个keyDown将稍后从InputManager获取。

    function Character:moveLeft ( keyDown )
        self:run ( -1, keyDown )
    end
    function Character:moveRight( keyDown )
        self:run ( 1, keyDown )
    end
    
  7. 我们要停止移动的操作是(在确认我们不是在跳跃之后)停止所有移动,将线性速度在两个方向上设置为 0,并开始空闲动画。这很简单:

    function Character:stopMoving ()
        if not self.jumping then
            self.physics.body:setLinearVelocity ( 0, 0 )
            self:startAnimation ( 'idle' )
        end
    end
    
  8. 对于跳跃,我们将做一些不同的事情。我们不会强制线性速度,而是应用一个力。我们可以用同样的方式做,但只是为了说明如何应用力,我们在这里使用它。然后我们设置一个名为jumping的变量为 true;这将允许我们知道我们在跳跃,最后开始跳跃动画。这也很容易。

    function Character:jump ( keyDown )
        if keyDown and not self.jumping then
            self.physics.body:applyForce ( 0, 8000 )
            self.jumping = true
            self:startAnimation ( 'jump' )
        end
    end
    
  9. 最后,我们将实现stopJumping方法。我们基本上将跳跃设置为 false 并停止移动如下:

    function Character:stopJumping ()
        self.jumping = false
        self:stopMoving ()
    end
    
  10. 我们最后的方法将用于处理与地板的碰撞。为了使用它,我们需要在initializePhysics中添加一行。

  11. 我们现在正在设置一个callback方法,当 Box2D 检测到与玩家形状的碰撞时将执行。参数是方法本身,以及它应该在碰撞的哪个阶段被调用,定义如下:

    self.physics.fixture:setCollisionHandler (
        onCollide, 
        MOAIBox2DArbiter.BEGIN )
    

    小贴士

    要检查所有碰撞阶段,请查看setCollisionHandler定义在getmoai.com/docs/class_m_o_a_i_box2_d_fixture.html#a693a608fc6645b170d067845dd1a9c20

    现在我们需要创建callback方法:

  12. 我们现在要检查我们与之碰撞的形状是否是地板,如果是,我们就停止跳跃,如下所示:

    function onCollide ( phase, fixtureA, fixtureB, arbiter )
        if fixtureB == PhysicsManager.floor.fixture then
            Character:stopJumping ()
        end
    end
    

键盘输入

为了开始使用键盘,我们需要更改一些内容:

  1. 首先,我们需要从game.lua中删除我们的processInput方法和对Game:start的调用。

  2. 然后我们需要创建一个负责管理InputManager按键输入的方法。

    function Game:keyPressed ( key, down )
        if key == 'right' then Character:moveRight(down)end
        if key == 'left' then Character:moveLeft(down)end
        if key == 'up' then Character:jump(down)end
    end
    

    此方法将一些按键与我们在Character上创建的方法关联起来。这应该足以处理我们角色的移动。

  3. 现在,我们必须从input_manager.lua中删除所有代码(除了模块定义),并使用以下代码重新编写:

    function InputManager:initialize ()
        function onKeyboardEvent ( key, down )
            if key == 119 then key = 'up' end
            if key == 97 then key = 'left' end
            if key == 100 then key = 'right' end
            Game:keyPressed(key, down)
        end
    
    1. onKeyboardEvent将是我们的键盘回调。我们在这里所做的只是使用ADW的键码,将它们替换为可读的字符串,并将它们作为参数传递给之前实现的keyPressed方法。

          MOAIInputMgr.device.keyboard:setCallback( onKeyboardEvent )
      end
      
    2. 现在只需设置回调,我们就算完成了。

  4. 现在尝试运行游戏。你应该能够使用ADW移动角色并跳跃。

创建场景

现在,我们将创建定义级别的必要代码。我们不会对精灵进行操作;相反,我们只需使用 Box2D 的默认调试形状。我们将创建整个物理世界,你现在应该能够将精灵分配给它(这与我们对玩家所做的是一样的)。

  1. 首先,我们需要删除physics_manager.lua中与地板定义相关的所有代码,因为现在我们将使用其他平台一起创建地板。

  2. 现在,在game.lua文件中,在background_objects定义下方,我们将创建场景的定义:

    local scene_objects = {
        floor = {
            type = MOAIBox2DBody.STATIC,
            position = {0, -WORLD_RESOLUTION_Y/2},
            friction = 0,
            size = {2 * WORLD_RESOLUTION_X, 10}
        }
        platform1 = {
            type = MOAIBox2DBody.STATIC,
            position = {100, -50},
            friction = 0,
            size = {100, 20}
        }
    }
    
  3. 如您在给出的代码中所见,我们定义了两个对象,即地板和平台。我们为每个对象定义其身体类型、位置和大小。请注意,地板现在只是另一个场景对象。

  4. 我们现在将遍历这个表,并使用一段代码创建所有对象:

    function Game:loadScene()
        self.objects = {}
        for key, attr in pairs(scene_objects)do
    
    1. 我们开始遍历所有的scene_objects

          local body = PhysicsManager.world:addBody(attr.type)
      
    2. 在这里,我们创建 Box2D 身体,使用定义中的类型:

          body:setTransform ( unpack(attr.position) );
          width, height = unpack ( attr.size );
          local fixture = body:addRect (
          -width/2, -height/2, width/2, height/2 )
      
    3. 然后我们设置大小和位置。

          fixture:setFriction ( attr.friction )
      
    4. 最后是摩擦力。

          self.objects[key] = { body = body, fixture = fixture }
          end
      end
      
    5. 然后将对象存储在定义的名称下,我们就完成了。

如果你希望向定义中添加更多内容(例如多边形的顶点,或者类似的东西),这仅仅是一个决定如何在定义中表示它,并在loadScene内部添加必要的调用方法来满足那个定义的问题。

小贴士

目前,只支持八个顶点。你可以修改 Moai SDK 的源代码来添加更多。

  1. Game:initialize中的PhysicsManager初始化下面,你应该添加一个对loadScene的调用。

        self:loadScene ()
    
  2. 现在我们需要做的一件事是修复我们的collision方法,它使用了硬编码的地板。

    1. 首先,我们需要在game.lua中添加另一个辅助方法:

      function Game:belongsToScene ( fixture )
          for key, object in pairs (self.objects )do
              if object.fixture == fixture then
                  return true
              end
          end
          return false
      end
      
    2. 在这个方法中,我们遍历所有场景对象,查看传递的参数是否是其中之一。

    3. 打开character.lua,将onCollide替换为新版本:

      function onCollide ( phase, fixtureA, fixtureB, arbiter )
          if Game:belongsToScene ( fixtureB ) then
              Character:stopJumping ()
          end
      end
      
  3. 我们在这里做的是,当发生碰撞时调用辅助方法,以查看我们是否与场景中的某个对象发生碰撞。在这种情况下,我们停止跳跃。这将产生一个有趣的机制;运行游戏,你就会明白为什么(超级肉男孩?有人吗?好吧,当你与一个盒子的垂直边缘碰撞时,玩家会停止,这样你就可以再次跳跃了!)。

我们创建这个场景的方式与实际加载它的代码是解耦的。在现实世界中,你将希望能够在代码外部编辑你的关卡,然后加载它们。

加载代码将主要以这种方式运行,但你可能会使用另一种语言(XML、JSON 等)来存储你的关卡。关卡编辑器是你的朋友。

小贴士

你知道Inkscape吗?它是一个使用 SVG 作为主要格式的矢量图形编辑器。SVG 是 XML!所以它非常易于解析。顺便说一句,它是开源的,免费的。如果你在处理瓦片,另一个有用的开源工具是Tiled,它将非常有用,因为它包括图层、瓦片地图支持,并导出为 Lua。

练习

到现在为止,添加敌人到这个场景应该对你来说很熟悉了。你需要考虑移动的人工智能,但除此之外,其他所有内容都已经见过。你应该将其创建为一个动态体,并修改玩家碰撞处理器以正确反应(例如杀死玩家,或者从玩家那里扣除生命)。

另一个很好的想法是实现马里奥风格的平台,你可以从平台的底部跳起,然后碰撞并停留在顶部。(你将不得不使用 Box2D 的传感器,并在碰撞处理器中检查碰撞的法线。然后,根据碰撞的法线,在适当的时间停止移动)。

试着玩一玩,因为玩耍是学习过程中的重要部分。

概述

在这一章中,你可能感觉自己像是牛顿。我们用 Box2D 做了一些实验,以构建我们的平台游戏原型的物理系统。我们看到了如何使用键盘,并通过用户的输入和碰撞使我们的角色进行动画和移动。

下一章将负责展示一些调试数据,以说明如何创建 HUD。

第十章:创建 HUD

抬头显示(HUD)是游戏设计师用来向用户提供信息的关键技术。它基本上是一组与世界本身分离的图形或文本,用于告诉玩家他们剩余的生命值或魔法值,或他们的得分是多少。

我们将创建一个简单的 HUD,它将提供非常有用的信息,例如角色移动的方向和其在世界坐标中的位置。这将足以说明如何创建一个 HUD。你可能会想在你的游戏中显示更多有用的信息。那么,让我们开始吧!

基础知识

创建一个名为hud.lua的文件,从game.lua中调用它,然后开始我们的模块。

我们的 HUD 实现将基于我们不希望将不在世界内的对象定位在世界坐标上的想法。此外,我们更习惯于在设计 UI 布局时考虑(0, 0)位于左上角,Y轴向下增长。

所以这就是我们要做的:

module ( "HUD", package.seeall )
function HUD:initialize ()
    self.viewport = MOAIViewport.new ()

可以这样做:

  1. 为了使用不同的坐标系,我们将创建一个新的视口。

        viewport:setSize ( SCREEN_RESOLUTION_X, SCREEN_RESOLUTION_Y )
    
  2. 这个视口将与屏幕大小相同:

       viewport:setScale  (SCREEN_RESOLUTION_X, -SCREEN_RESOLUTION_Y )
    
  3. 它不会使用世界坐标。相反,它将使用屏幕坐标,但我们将反转Y轴,使其向下增长。

        viewport:setOffset ( -1, 1 )
    
  4. 现在有一些新内容。setOffset方法用于使用投影空间移动视口。投影空间是一个 2 x 2 的矩形,其Y轴向上。将-1作为X值传递给setOffset将此投影空间向左移动半个屏幕,将1作为Y值传递将移动它半个屏幕到顶部,达到我们的目标,即(0, 0)坐标位于左上角。

    小贴士

    你可以看到这里非常实用;你可以将(0, 0)移动到任何你想要的位置,位置将基于此进行计算。

  5. 在此之后,我们创建一个层并将其加载到渲染表中,就像我们习惯的那样:

        self.layer = MOAILayer2D.new ()
        self.layer:setViewport ( self.viewport )
        -- Now we need to render the layer.
        local renderTable = MOAIRenderMgr.getRenderTable ()
        table.insert ( renderTable, self.layer )
        MOAIRenderMgr.setRenderTable ( renderTable )
    end
    
  6. Game:initialize的底部添加对HUD:initialize ()的调用,你应该就可以开始了。

现在,我们准备好开始创建我们的HUD元素。

左边还是右边,这是个问题

现在我们将在屏幕上显示我们的信息:

  1. 首先,我们需要在game.lua文件中的resource_definitions表中定义一个字体。

    我们为这一章提供了源代码,但你可以使用你喜欢的任何字体:

        hudFont ={
            type = RESOURCE_TYPE_FONT,
            fileName = 'fonts/tuffy.ttf',
        glyphs = 
        "abcdefghijklmnopqrstuvwxyzABCDEFGHI
        JKLMNOPQRSTUVWXYZ0123456789,.?!",
        fontSize = 26,
        dpi = 160
      }
    

    这应该很熟悉,因为我们已经在第六章中讨论过,资源管理器

    现在我们知道hudFont将引用我们的字体。

  2. 让我们回到hud.lua,创建一个名为initializeDebugHud的方法,并在HUD:initialize中调用它:

    function HUD:initializeDebugHud ()
        self.font = MOAIFont.new ()
        self.font = ResourceManager:get ( "hudFont" )
    

    我们使用我们刚刚创建的字体资源作为文本框的字体:

        self.leftRightIndicator = self:newDebugTextBox ( 30, {10, 10, 100, 50} )
        self.positionIndicator = self:newDebugTextBox ( 30, {10, 50, 200, 100} )
    end
    
  3. 然后我们调用一个辅助方法,我们将在稍后创建它。它将接收字体大小和文本将放置的矩形。矩形由盒子的左上角和右下角的坐标组成:

    1. 首先,我们创建 MOAITextBox。这是将用于显示文本的类。它继承自 MOAIProp,因此你可以移动它、将其插入到层中,以及执行所有其他可以与 MOAIProp 做的事情:

      function HUD:newDebugTextBox ( size, rectangle )
          local textBox = MOAITextBox.new ()
      
    2. 我们设置了之前加载的字体:

          textBox:setFont ( self.font )
      
    3. 然后我们使用 size 参数设置大小:

          textBox:setTextSize ( size )
      

      提示

      你可以在 MOAITextBox 中使用多种文本样式;请参阅文档中的 setStyleMOAITextStyle

    4. 我们使用 unpackrectangle 表格拆分为参数:

          textBox:setRect ( unpack(rectangle) )
      
    5. 矩形定义了一个屏幕上的框,文本被限制在这个框内。文本不会渲染在 setRect 定义的矩形之外。

    6. 我们将文本框插入到 HUD 的层中。

          layer:insertProp ( textBox )
      
    7. 最后返回文本框,以便稍后可以引用。

          return textBox
      end
      

现在我们已经将 HUD 层填充了文本框,但为了完成我们的目标,我们还需要一件事情。

更新信息

我们需要在那些文本框中写入一些内容。在这种情况下,我们将定期在 HUD 中调用一个 update 方法,以便它可以刷新数据并在屏幕上打印:

function HUD:update ()
    local x, y = Character.prop:getScl ()
  1. 我们获取用于设置角色方向的缩放比例,并将其存储在局部变量 x 中:

        if x > 0 then
            self.leftRightIndicator:setString ( "Left" )
        else
            self.leftRightIndicator:setString ( "Right" )
        end
    
  2. 如果你记得我们之前做了什么,这应该很清楚。为了向右转,我们在 x 轴上将角色的 prop 缩放为 -1,要向左转则将其缩放为 1。所以这就是我们在这里所做的事情。如果缩放的 x 值为正,那么我们面向左边。否则,如果它是 -1,那么我们面向右边。然后,根据情况,我们使用 MOAITextBox:setString 来更新带有正确方向的字符串。

        x, y = Character.physics.body:getPosition ()
        self.positionIndicator:setString ("(" .. 
        math.floor(x) .. ", " .. 
        math.floor(y) .. ")")
    end
    
  3. 最后我们更新文本框;我们从物理体中获取位置。使用 .. 操作符连接字符串,生成格式为 (x, y) 的正确字符串。然后我们使用 math.floor 向下取整位置,因为它是一个小数。

现在我们需要做的唯一一件事是在 Game:start 中的 while 循环中添加对 HUD:update () 的调用,我们应该能在屏幕上看到两个调试字符串。

提示

如果你不确定框的大小和布局,可以使用 MOAIDebugLines.setStyle(MOAIDebugLines.TEXT_BOX) 来调试它们。这将围绕每个文本框显示一条线。

摘要

在本章中,我们学习了如何使用 Moai SDK 实现一个 HUD 的基础知识。我们深入研究了 MOAIViewport 的配置,以便创建一个使用与之前不同的坐标系统的特殊视口。我们还首次使用 MOAITextBox 显示文本。

在下一章中,我们将深入探讨游戏的重要方面之一:声音和音乐。

第十一章。让正确的音乐进来!

音频和音乐总是留给最后阶段。这本书也发生了同样的事情,这真是太尴尬了。如果你对你的游戏体验认真负责,从一开始就要考虑音乐和声音。即使你的游戏不是以音频为中心,你也应该将音乐和声音视为反馈系统,并将它们与图形同等重要。

在本章中,我们将学习如何在游戏中使用内置的 Untz 声音系统播放音频文件。我们将添加背景音乐和音效。

音频管理器

为了在我们的游戏中添加一些音乐和效果,我们将创建一个名为AudioManager的模块,该模块将负责加载和播放声音。

  1. 首先,我们需要创建一个名为audio_manager.lua的文件。这个文件将负责管理我们所有的声音需求。在这种情况下,我们将使用 Untz,但你可以轻松地修改它以使用 FMOD,如下所示:

    module ( "AudioManager", package.seeall )
    local audio_definitions = {
      backgroundMusic = {
        type = RESOURCE_TYPE_SOUND, 
        fileName = 'sounds/music.mp3', 
        loop = true,
        volume = 1
      },
      jump = {
        type = RESOURCE_TYPE_SOUND, 
        fileName = 'sounds/jump.wav', 
        loop = false,
        volume = 1
      }
    }
    
  2. 与我们为游戏定义资源的方式一样,我们将使用音频文件的资源定义来定义所有声音。我们之前在第六章中看到了这一点,资源管理器。如果你不明白我们在做什么,我建议你回过头去阅读关于声音定义的内容。第六章。sounds表将作为我们声音的缓存。它将如下所示:

    sounds = {}
    
  3. 在初始化方法中,我们首先加载所有添加的定义。我们通过从ResourceDefinitions调用setDefinitions方法来完成此操作,如下所示:

    function AudioManager:initialize ()
        ResourceDefinitions:setDefinitions ( audio_definitions )
    
  4. 然后,由于我们正在使用 Untz,我们需要按照以下方式初始化它:

        MOAIUntzSystem.initialize ()
    end
    

    小贴士

    您可以使用两个可选参数调用MOAIUntzSystem.initialize:采样率(可以更改以匹配您的音频文件)和每缓冲区帧数。

  5. 为了播放我们的声音,我们将定义一个方法,将它们加载到本地的sounds表中,如下所示:

    function AudioManager:get ( name )
        local audio = self.sounds[name]
    
        if not audio then 
            audio = ResourceManager:get ( name )
            self.sounds[name] = audio
        end
    
        return audio
    end
    

    此方法检查音频是否已被预先加载;如果没有加载,则加载并返回它。我们将此与我们将要编写的play方法分开,因为我们可能希望在播放之前预加载声音。

  6. 下一步是创建play方法:

    function AudioManager:play ( name, loop )
        local audio = AudioManager:get ( name )
    
    1. 这里我们获取实际的音频。如果它已被预加载,则可以播放而无需任何加载延迟。

          if loop ~= nil then
              audio:setLooping ( loop )
          end
      
    2. 在这里,我们可以使用loop参数来覆盖默认的loop属性。

          audio:play ()
      end
      
    3. 最后,我们播放它。

      小贴士

      值得注意的是,多次播放声音会导致之前的播放中断。

  7. 现在,我们将编写一个停止音频的函数:

    function AudioManager:stop ( name )
        local audio = AudioManager:get ( name )
        audio:stop ()
    end
    
  8. 我们需要在main.lua文件中所需game.lua之上添加此文件。

背景音乐

现在,让我们按照以下步骤播放一些背景音乐:

  1. Game:initialize的底部,您应该添加以下方法调用:

        AudioManager:initialize ()
    
  2. 我们使用AudioManager:initialize(我们在本章早些时候创建的方法)初始化音频:

        AudioManager:play ( 'backgroundMusic' )
    
  3. 然后我们播放背景音乐。记住,我们在audio_manager.lua文件中的audio_definitions表中定义了它。

应该就是这样了。运行游戏,你应该正在听 Milhouse Palacios 的一首精彩歌曲。

声音效果

我们将添加一个在角色跳跃时播放的声音。

打开character.lua文件,并在Character:jump方法内部调用AudioManager:play

function Character:jump ( keyDown )
    if keyDown and not self.jumping then
        AudioManager:play ( 'jump' )
        self.physics.body:applyForce ( 0, 8000 )
        self.jumping = true
        self:startAnimation ( 'jump' )
    end
end

就这些了。运行它并听你的跳跃。

摘要

在本章中,我们创建了一个新的模块来处理声音。我们添加了一些背景音乐和一个跳跃声音效果。

我们已经覆盖了很多内容,你现在可以使用我们所学到的知识来创建一个完整游戏。下一章将指导你如何将我们的集中注意力游戏部署到 iOS 上。

第十二章:iOS 部署

现在我们已经涵盖了制作游戏所需的大部分内容,我们可以开始讨论将你的游戏部署到特定设备上了。在本章中,我们将修改默认的 iOS 主机项目,以便让我们的游戏在其上运行。为此,我们将复制随 Moai SDK 提供的示例主机项目,并逐一介绍你需要做的一切,以便让你的游戏运行起来。

Xcode 项目

首先,如果你梦想你的游戏能进入 iTunes Store,你将不得不从 Apple 获取最新的 Xcode 和 iOS SDK。你可以通过注册 Apple 的开发者网站(developer.apple.com/)来获取访问权限。

如果你没有使用 Xcode 的经验,建议你阅读一个快速教程,因为那超出了本书的范围。关于 Xcode,你应该知道的一些基本事情是如何处理源文件,如何管理目标,以及一些基本的 Objective-C,以便修改主机以适应你的游戏。

打开随 Moai SDK 下载提供的 Xcode 项目。它位于hosts/ios/moai.xcodeproject。如果你使用该位置的项目,它已经配置为使用lib/ios中的 Moai SDK 静态库,但如果你移动它,你需要修复项目配置中的相应路径。我们很快就会看到如何做这件事。

主机

所有 iOS 特定主机的代码都可以在Classes文件夹内找到。如果你仔细查看它们,你会很容易理解它们的作用。基本上,它们设置了一个 OpenGL 视图,将所有输入方法注册到 AKU,并配置其他你可能觉得有用的服务(或者如果你不想使用它们,可以移除)。

另有一个名为Resources的文件夹(其中包含图标),编译后的源文件将放在那里(在另一个名为build的文件夹内)。

你会看到,默认情况下,moai-target文件和lua文件夹都显示为红色。原因是,在构建过程中,一个脚本会运行并读取Resources/buildmoai-target的内容,以识别我们游戏的全部 Lua 源代码。找到后,脚本会将moai-target中引用的每个目录复制到lua文件夹中(文件也被标记为只读,以提醒你它们不应该被编辑,因为它们将在下一次构建时被删除)。lua文件夹内的文件包含在应用程序包中,以便在打包部署时可以访问它们。看看MoaiAppDelegate.mm,那里存放着调用main.lua以启动游戏的代码。如果你想为main.lua使用不同的入口点,你可以在那里做到。

moai-target文件中,你应该按行包含你游戏的全部代码和资源源文件。例如:

../../myGame/src
../../myGameAssets/

小贴士

如果你正在安装的模块有特殊的依赖项(以 samples/iphone/app-apsalar 为例),你可以使用一个名为 moai-target-ext 的文件来定义它们。脚本将检查你在 moai-target 中定义的每个文件夹内是否存在此文件,并将递归地复制其中的所有内容。

这对于与提供的宿主一起工作至关重要。

你还应该看看 Frameworks 文件夹,其中包含构建我们的宿主所需的全部静态库和 iOS 框架。

运行示例

在开始修改你的游戏代码之前,你应该尝试运行示例项目。它已经指向了 Moai SDK 的一个示例,所以它应该像检查模拟器中的设备并点击 构建和调试 一样简单。

如果你看到 Moai SDK 的标志在旋转,那么你可以继续了。如果没有,请检查你的 Xcode 配置是否正确,或者加入 Moai SDK 的论坛寻求帮助,详细说明你遇到的错误。

在实际设备上运行需要你理解(或者至少能够破解——谁能理解它呢?)如何处理和设置苹果的开发者配置文件。这在苹果开发者门户的文档中有详细说明,你可以在这里找到(developer.apple.com/devcenter/ios/index.action)。

设置我们自己的项目

由于 Moai SDK 附带的自定义 Xcode 项目示例包含了一些我们可能不需要的东西,建议进行清理。我们将首先将整个项目复制到我们的硬盘上的某个位置。如果你一直跟随所有章节并提取了所有代码,将我们的项目放在代码示例相同的文件夹中是个不错的选择,这样我们就可以指向它们,并在 iOS 上拥有我们的 Concentration 游戏。你可以使用 第七章 的 集中注意力游戏 的源代码。

无论哪种方式,如果你下载了本书的源代码,你将在本章的代码中找到 iOS 项目和 Concentration 游戏实现。

现在我们已经有了 Xcode 项目和 Concentration 游戏,双击 moai.xcodeproj 以打开 Xcode 并开始配置它。

这可能需要一些时间来加载。Xcode 在第一次打开项目时执行文件索引。不用担心,后续加载时不会发生这种情况(嗯,除非它决定随机索引)。

如果你现在运行它,可能会失败。学会花时间阅读和理解错误;这是解决问题的唯一方法。

现在,让我们一步一步地修复项目。

指向正确的源代码

默认项目使用相对路径指向 Moai SDK 的一个示例。由于我们移动了项目,这个路径现在已损坏。我们需要让所有内容都指向我们自己的代码。

指向正确的源代码

为了做到这一点,展开资源文件夹,然后是构建文件夹,最后点击moai-target

moai-target的内容应该是这样的:

../../../samples/anim/anim-basic
../../../include/lua-modules lua-modules

moai-target正使用相对路径指向anim-basic示例。因此,为了运行我们自己的游戏,我们需要让它指向正确的位置。

在这个例子中,我们将游戏代码移动到了一个名为concentration-code的文件夹中,所以我们将指向那里。最终的moai-target应该如下所示:

../concentration-code

指向正确的源代码

修复源路径

由于我们将 Xcode 项目从原始位置移动了,现在我们需要修复围绕它的相对路径。

  1. 收起资源文件夹并展开。您会看到ParticlePresets.cppParticlePresets.h以红色显示。它们可以在moai-sdk/hosts/src/中找到(moai-sdk是您在初始章节中安装 Moai SDK 的文件夹)。修复源路径

  2. 选择ParticlePresets.cppParticlePresets.h并删除它们。修复源路径

  3. 现在右键单击文件夹,选择将文件添加到"moai"。转到moai-sdk/hosts/src/并选择ParticlePresets.cppParticlePresets.h。接受后,它们应该在该文件夹中,并且不应以红色显示。修复源路径

修复包含路径

下一步我们需要做的是修复相对包含路径。

要这样做,单击文件夹树顶部的moai,其中显示了 Xcode 应用程序的图标。然后,在项目部分(不是目标部分)下选择moai

这里有几个问题需要修复:

  1. 搜索名为头文件搜索路径的设置;它应该读取../../include/。如果您双击它,将显示一个弹出窗口,然后您可以双击包含路径并将其更改为您自己的包含路径,该路径位于moai-sdk/include/修复包含路径

  2. 搜索其他链接器标志,将../../../lib/ios/universal/libmoai-ios-tapjoy.a更改为正确的位置(moai-sdk/lib/ios/universal/libmoai-ios-tapjoy.a)。如果 Xcode 仍然找不到此文件,请尝试使用绝对路径(在我的情况下是/Users/nictuku/moai-sdk/lib/ios/universal/libmoai-ios-tapjoy.a)。修复包含路径

  3. 搜索库搜索路径,将../../../lib/ios/universal更改为moai-sdk/lib/ios/universal修复包含路径

  4. 现在,在目标部分下单击MoaiSample。双击库搜索路径,并以相同的方式替换剩余的相对路径。修复包含路径

修复链接库

下一步我们需要修复的是所有链接库的位置。

修复链接库

展开框架文件夹,并识别所有显示为红色的库。

你可以在moai-sdk/bin/ios/universal中找到它们。删除红色中的那些,并添加该路径下的所有文件,就像我们处理ParticlePresets文件一样。

你应该最终在目录中拥有相同的文件,但它们不应再以红色显示。

现在,如果你点击运行,构建应该成功,你应该在 iOS 模拟器上看到我们的专注力游戏。

修复链接库

但等等!它没有被正确显示,触摸也没有被检测到!这是因为我们本地配置项目的方式。所以让我们尝试修复它。

跨平台开发

我们首先需要考虑的是如何在项目中处理不同的分辨率。你可以在这方面做很多复杂的事情,比如根据不同的大小更改 UI,或者在分辨率高于或等于 1024 x 768(或你选择的任何值)时加载高清图像,等等。在这个例子中,我们将保持简单;我们只需将一切拉伸,使其适合屏幕。

打开Concentration项目,并更改以下行:

SCREEN_RESOLUTION_X = 2 * WORLD_RESOLUTION_X
SCREEN_RESOLUTION_Y = 2 * WORLD_RESOLUTION_Y

到以下:

SCREEN_RESOLUTION_X = MOAIEnvironment.horizontalResolution
SCREEN_RESOLUTION_Y = MOAIEnvironment.verticalResolution

如果你现在运行项目,它应该显示游戏的拉伸版本,但它仍然不会对触摸做出反应。

跨平台开发

为了修复这个问题,编辑game.lua并修改processInput以使用InputManager:getTouch而不是InputManager:position

在显示x, y = InputManager:position ()的地方,你应该将其替换为x, y = InputManager:getTouch ()

现在如果你运行你的项目,你将能够玩我们的游戏。

跨平台开发

但看起来被拉伸了。你可以在 Moai 的维基页面上学习如何在横幅模式下使其工作(getmoai.com/wiki/index.php?title=Setup_landscape_in_Moai_Games_For_iOS_Devices)。

在设备上运行

经过我们采取的所有步骤后,游戏应该能够在设备上运行。这仅仅是一个设置正确的代码签名标识符和更改目标设备的问题。

现在尝试这个操作会很好,如果你迷路了,可以向苹果社区或 Moai 论坛寻求帮助,因为设置苹果开发者账户和证书超出了本书的范围。

摘要

在本章中,我们学习了如何修改 iOS 宿主的 Xcode 项目,以便使其与我们的游戏一起工作。我们修复了在移动项目时中断的相对路径,并正确设置了分辨率。

我们知道 iOS 不是唯一的平台。我们将在下一章讨论其他平台!

第十三章。部署到其他平台

继续讨论多平台开发,我们应该花些时间分析 Moai SDK 支持的其他平台。

Moai SDK 为不同平台提供了示例宿主。建议您阅读并理解平台宿主的代码,这样您就可以修改它们。如果您想添加自己的广告库或与下一代社交网络的集成,您需要修改宿主。

让我们来看看 Moai SDK 发布中包含的宿主。

Windows

为了为 Windows 构建,您需要使用 Visual Studio 项目中的一个。您可以在moai-sdk/hosts/foldervs2010vs2008)中找到这两个项目。

打开适用于您 Visual Studio 版本的正确解决方案。在里面,您将找到所有需要的GlutHost代码,它应该在 Windows 上运行顺畅。Moai SDK 已经将此宿主的编译版本包含在moai-sdk/bin/win32/中。

Mac OS X

Mac OS X 的宿主可以使用moai-sdk/hosts/xcode/osx中的 Xcode 项目进行编译。

该宿主的代码库基本上与 Windows 版本相同;它使用 GlutHost 程序,并作了一些调整以使其在 Mac OS X 上运行。

Android

Android 宿主是通过位于moai-sdk/hosts/android/run-host.sh(或.bat)的 shell 脚本生成的。请确保已安装 Apache Ant 以及所有 Android 相关软件。有关在 Android 设备上构建游戏的更多信息,请参阅维基页面(getmoai.com/wiki/index.php?title=Building_Moai_Games_For_Android_Devices)。

注意

Android 的编译可能需要单独的一章。除了在 Moai SDK 维基页面中提到的链接外,您还需要了解一些 Android 开发知识,因此建议在尝试构建宿主之前先阅读 Android 的文档(developer.android.com/develop/index.html)。

Google Chrome (本地客户端)

本地客户端(developers.google.com/native-client/)是一种新的方式,可以将代码引入到计算机中,并且可以从您的 Chrome 浏览器中下载。Moai SDK 附带了一个为它构建的宿主,可以在moai-sdk/hosts/chrome中找到。

您需要更改manifest.json文件,并将所有代码放在同一个文件夹中。

有关如何进行此操作的详细说明位于 Moai 的维基链接(getmoai.com/wiki/index.php?title=Building_Moai_Games_for_Google_Chrome)。

Linux

在撰写本书时,Linux 不是 Moai SDK 发布中的目标平台,但 Moai 可以从源代码构建上运行。

目前主要有两种方法来做这件事:

目前第二种方法更受欢迎,因为它支持 Untz,但官方的 Linux 支持正在路上,所以请务必检查最新的 Moai SDK 发布版本,以确认 Linux 尚未官方支持。

注意

在这里插入下一个平台

我们之前提到过,Moai SDK 是以一种易于移植到支持 C++和 OpenGL 的任何平台的方式进行开发的。如果你能够让它在新平台上运行,你可以分享你的经验并向github.com/moai/moai-dev发送 pull request。我们将非常感激!

摘要

在本章中,我们总结了目前由 Moai SDK 支持的不同的平台。我们指出了获取有关为这些平台构建主机信息的地方。

你怎么说?你准备好创造下一个大游戏了吗?我打赌你一定准备好了!继续前进,创造史上最佳游戏,别忘了告诉社区关于它的事情!

加入getmoai.com/forums/的论坛,成为这个令人惊叹的游戏开发社区的一部分。我们期待着你的加入。

感谢你阅读这本书并支持开源技术。希望不久后能再次见到你!

posted @ 2025-10-11 12:55  绝不原创的飞龙  阅读(27)  评论(0)    收藏  举报