安卓平板游戏编程入门指南-全-
安卓平板游戏编程入门指南(全)
一、为 Java 开发设置 Android 3.0
这本书教你为 Android 3.0 平板电脑创建自己的游戏。在阅读并完成其示例后,您将对许多新型平板电脑的传感器、触摸屏、网络功能和处理能力有所了解。这听起来令人生畏吗?它不是。不用去做开发定位商店或赠送优惠券的乏味的企业应用的苦差事,你将知道如何制作有趣和吸引人的游戏。如果你过去做过一些游戏开发,你可能会惊喜地发现,与传统的 PC 和主机游戏开发相比,Android 系统让这个过程变得如此简单。
虽然没有任何一本书可以让你从新手变成游戏编程大师,但本书中介绍的基础知识可以让你将 2D 游戏中的任何想法变成现实。这本书使编程尽可能简单,以便专注于游戏开发中更具创造性的方面。
安卓是什么?
Android 非常特别,当你开始编程时,你会对它更加欣赏。许多手机制造商开发运行 Android 操作系统的平板电脑的运动为你将要制作的游戏创造了一个巨大的市场。这一节给你一个 Android 的功能和历史的纲要。
Android 的开端
2003 年,Android 作为一家硅谷的小型创业公司成立,旨在为智能手机创造一个更具互动性和更有帮助的界面。谷歌在 2005 年迅速收购了该公司,作为其进军手机市场的一部分。在谷歌收购它之后,第一个 Android 操作系统很快在 2007 年发布。在随后的几年里,Android 经历了多次修改(超过七次重大变化),使其成为智能手机的主要操作系统之一,有人说 Android 拥有近 50%的移动设备。
Android 的修订对于理解开发如何工作非常重要。谷歌努力确保其 Android 版本的向后兼容性;然而,应用通常被设计为适用于选定的几个 Android 版本,以保证最佳的性能和用户体验。名为 Froyo 的版本仍然是最受开发者欢迎的,但随着平板电脑等更现代的设备需要更强大的操作系统,后来的版本也越来越受欢迎。
下面的 Android 版本列表,以及它们当前的市场份额,说明了哪些版本仍然受欢迎,因此是开发者感兴趣的。谷歌给每个版本起的创意名就在版号旁边。开发者经常使用这些名字,而不仅仅是数字。请记住,除了 Android 3.0,所有版本的操作系统都是专为手机设计的:
- 1.5 Android cupcakes (2.3%)
- Android 1.6 donuts (3.0%)
- 是阿云 2.1 闪电(24.5%)
- Android 2.2 Froyo (65.9%)
- Android 2.3 gingerbread (1.0%)
- Android 2.3 gingerbread (3.0%)
- Android 3.0 Honeycomb (0.3%)
如果您有兴趣查看各种版本的当前市场份额,请前往[
developer.android.com/resources/dashboard/platform-versions.html](http://developer.android.com/resources/dashboard/platform-versions.html)
。
在检查了这个列表后,许多人会说你应该为 Froyo 制作游戏,因为它在市场份额上比其他版本领先很多。Froyo 流行的原因是它安装在许多更简单的旧手机上,这些手机只能通过复杂的过程获得更新版本。随着新版本占据中心舞台,这些设备将慢慢变得无关紧要。某种程度上,为广大用户做游戏是有意义的;然而,每天都有新用户购买使用最新版本的更现代的手机。此外,也许最重要的一点是,成千上万的应用可以在 Froyo 版本上玩,而且越来越难脱颖而出。
也就是说,这本书教你为最新版(蜂巢)设计游戏有两个原因。首先,Honeycomb 是唯一针对平板电脑优化的版本,比任何智能手机都更具沉浸感和乐趣。其次,随着越来越多的公司发布可以与苹果 iPad 竞争的平板电脑,Android 平板电脑正在以巨大的速度增长。随着 webOS 的失败,Android 和 iOS 是平板电脑市场唯一的竞争者。微软也推出了自己的操作系统,但还没有获得很大的市场份额。谷歌经常被引用的关于每天有 50 万台 Android 设备注册的声明让你感受到这个市场扩张的速度有多快。
安卓 3.0 的特性
Honeycomb 相对于之前的 Android 版本是一个巨大的进步。Android 3.0 旨在利用更大的屏幕和更强大的处理器,让开发人员扩展他们通常适度的智能手机游戏。许多新功能都是用户界面的变化,使用户可以通过比智能手机屏幕大几倍的屏幕访问桌面。例如,典型的手机有两到三英寸的屏幕,而平板电脑拥有令人印象深刻的九到十英寸的屏幕。这些更新很方便;然而,游戏开发商更关注更新更快的图形渲染和操作系统的新传感器和网络能力。
并非所有游戏都使用所有这些功能,但在设计独特的游戏时,考虑它们的重要性是至关重要的。更大的屏幕本身就是一个值得注意的更新。高分辨率屏幕需要可缩放且视觉上吸引人的艺术品。很多安卓平板都登陆了 1280 × 800 作为屏幕尺寸。这与许多计算机屏幕仍在使用的分辨率相当。在这种情况下,图形必须接近计算机游戏中使用的图像。
表 1-1 列出了游戏开发者特别感兴趣的 Android 3.0 的主要变化。
在整本书中,我给出了如何充分利用 Android 平板电脑新功能的建议。如果你想把自己制作游戏作为一种爱好,那么请留意我的笔记,看看哪里可以免费获得高质量的声音和图像。我为我的游戏制作音乐和图形的工具也将在第二章中详细解释。
我希望在熟悉 Android 之后,你已经准备好开始使用了。不过,请仔细阅读下一部分,以确保你拥有为 Android 开发游戏的适当技能和硬件。
制作安卓游戏需要什么
那么,如何才能成为一名 Android 游戏开发者呢?让我们来看看你需要从本书中获得最多的技能,以及你需要通过它的例子来工作的系统。
你需要知道的事情
安卓游戏编程有多难?这真的取决于你对 Java 和 Android 操作系统的经验。如果你有扎实的 Java 知识,那么这本书对你来说再合适不过了。如果你以前为 Android 写过代码,那么你可能不会被这里的任何代码所挑战,你可以自由地享受你的体验。在继续之前,请仔细阅读这一部分,这样您就能确切地知道您需要什么。
一般来说,有兴趣学习在安卓系统中为平板电脑创建游戏的人来自三种不同的背景。每一个背景都为你准备了本书中的例子,但是它们都需要一个稍微不同的方法。
如果你既懂 Java 又懂 Android,你就可以开始了。这里的代码类似于您以前看到的,但是它侧重于图形、游戏循环和对用户输入的快速响应,这些您可能没有处理过。不管你做过什么,这本书帮助你掌握平板电脑游戏的创作。
也许你对 Java 很熟悉,但是你从来没有用过 Android。这很好。阅读示例和代码不会有太大的困难。请记住,对于任何新的环境和 API,您都应该定期查找提供的函数和类。熟悉 Android 需要时间,但努力是值得的。
你可能从来没有用 Java 编写过一个if
语句,更不用说用 Android 了。如果是这样的话,你还是可以用这本书,但是你得弄一本 Java 入门。我强烈推荐杰夫·弗里森(Apress,2010)的《学习 Android 开发的 Java》。当你有了 Java 的参考资料,熟悉 Java 的工作原理,然后直接进入本文。你在学习过程中学习语言。
理解 XML 是有益的;然而,XML 相对容易理解,您应该可以轻松掌握本书中对它的基本用法。资质不在话下,是时候考虑游戏创作使用的环境了。
您需要什么样的平台
是时候动手了,看看开发 Android 游戏到底需要什么。幸运的是,你不应该买任何软件!唯一的费用是当你准备把你的游戏放到安卓市场时,要交 25 美元的注册费。首先,检查以确保您的计算机支持 Android 开发:
- Windows XP (32 bit), Vista (32 bit or 64 bit) or Windows 7 (32 bit or 64 bit)
- Mac x 10.5.8 or later (x86 only)
- Linux(在 Ubuntu Linux、Lucid Lynx 上测试)
这个列表是根据 Android 自己的系统需求编制的。查看[
developer.android.com/sdk/requirements.html](http://developer.android.com/sdk/requirements.html)
了解最低系统标准的最新变化。
虽然满足最低要求的系统可以让你创建 Android 应用,但是测试你的程序可能会相当慢。一般来说,如果你能在电脑上玩现代电子游戏,那你应该没问题。但是,如果你有更慢的机器,不要绝望;你完全有能力编写 Android 游戏,但你应该在 Android 平板电脑上测试它们,而不是在电脑上的模拟器上。
你不需要一台 Android 平板电脑来完成本书中的任何练习或程序,但在真实设备上测试你的创作是无可替代的。随着市场上平板电脑供过于求,更便宜的型号会让你花费大约 500 到 700 美元。如果你像我一样发现游戏编程令人上瘾,那么这些投资是非常值得的。摩托罗拉和三星生产一些最受欢迎的平板电脑;寻找他们的产品,以了解 Android 平板电脑方面的顶级产品。
如果你对自己的技能很有信心,并且已经决定在哪台机器上投入游戏开发,那么你已经准备好获得工具并配置你的开发环境了。
设置您的 Android 平板电脑编程环境
您就要进入有趣的部分了,但首先您必须确保您的计算机设置正确。您必须为您的工作下载并安装三个软件包:
- Java development kit (JDK)
- Eclipse, also known as Integrated Development Environment (IDE)
- Android Java SDK
如果您是 Java 开发人员,那么您可能有最新版本的 JDK,甚至可能安装了 Eclipse。在这种情况下,请跳到以下说明的 Android SDK 部分。如果您遇到问题,请仔细阅读前两节,因为您可能使用了错误的 JDK 或 Eclipse 版本。
在接下来的几节中,您将逐步安装这些软件包。完成后,您就可以创建您的第一个 Android 平板电脑程序了。在你准备好出发之前,整个过程不会超过 20 分钟。
安装 Java JDK
第一步是为您的机器下载并安装最新版本的 JDK。以下是如何:
To find the JDK required by your system, please go to
[www.oracle.com/technetwork/java/javase/downloads/index.html](http://www.oracle.com/technetwork/java/javase/downloads/index.html)
. You need JDK to allow you to use Java language on your computer. Find the big Java icon in the upper left corner of the page and select the JDK link, as shown in in Figure 1-1. This link will take you to the JDK SE download page.在 Java SE 下载页面的下载选项卡上,如图图 1-2 所示,接受许可协议,选择适合您操作系统的软件包,点击链接下载。
图 1-2。许可协议和爪哇版本选择
After the file is downloaded, run the installer. On some computers, the installer will start automatically. If this doesn't happen, please find the folder where you downloaded the files and sort the folders by the modification date. The last file is this installer. Double-click it, and you can start.
A welcome dialog box of Java wizard installation appears, as shown in figure and figure 1-3 . Click the next button and follow the wizard prompts to complete the installation.
图 1-3。 JDK 安装向导
现在您已经准备好设置 Eclipse,这是您在本书中用来构建游戏的开发环境。如果没有 Eclipse,您将被迫使用命令行编译代码。开发环境为您节省了大量时间。
安装 Eclipse IDE
安装 JDK 后,您现在可以设置您的开发人员环境。您将使用 Eclipse,这是一个免费的软件包,为 Java 和 Android 开发人员提供了很多强大的支持。请遵循以下步骤:
- To find the Eclipse package for your system, please go to
[www.eclipse.org/downloads/](http://www.eclipse.org/downloads/)
. On the Eclipse download page, as shown in in Figure 1-4, use the small drop-down menu to match your operating system. Then select Eclipse IDE for Java Developers and click the link of the required version of the operating system. You will be taken to a download page.- Download the compressed folder containing the selected version and unzip it. Click Install executable file. During the installation process, make sure that you select the check box to create Eclipse shortcuts on your desktop, so that we can access Eclipse conveniently in the future.
- After installation, you can start Eclipse by shortcut. You should see something similar to Figure 1-5 . This means that everything is working.
图 1-5。Eclipse 开始了
安装好开发平台后,您就可以添加 Android SDK 了,它为您提供了构建游戏所需的库和工具。到目前为止,您只研究了基础知识,包括 Java 语言和开发环境。
安装 Android SDK
您的平台需要的最后一个软件包是 Google 的 Android SDK:
要找到您的系统需要的软件包,进入
[
developer.android.com/sdk/index.html](http://developer.android.com/sdk/index.html)
,如图图 1-6 所示,点击链接选择为您的操作系统制作的 Android SDK 软件包。完成后,相应的文件开始下载。图 1-6。 Android SDK 下载页面
下载完文件夹或安装程序后,找到文件双击运行。出现 Android SDK 工具安装向导的欢迎页面,如图 1-7 所示。
图 1-7。 Android SDK 安装向导
注意记住你安装软件开发工具包(Software Development Kit)的位置。我更喜欢用
C:\Android\android_sdk\
.无论您使用的是哪种操作系统,请记下它的安装位置。当我们将它连接到黯然失色时,我们将在接下来的步骤中需要它的位置点击下一步按钮,按照向导的提示安装 SDK。最终,你看到了最后一页。应选中启动 SDK 管理器复选框,如图图 1-8 所示。这将导致 SDK 管理器在安装完成后立即启动。
图 1-8。 Android SDK 工具安装向导结束
当 Android SDK 和 AVD 管理器对话框打开后,如图图 1-9 所示,点击左侧导航面板中的可用软件包链接,然后点击安装选定的按钮。这一步接受并安装 Google 推荐的游戏默认 Android 包。如果不安装这些,您将无法使用一些工具和示例应用。
图 1-9。 Android SDK 管理器。请注意所选的默认包点击安装选定项时,会出现如图 1-10 所示的对话框,显示安装进度(这可能需要几分钟)。
图 1-10。包和档案的安装
现在你有了 Java 语言、开发环境和 Android 工具。剩下的唯一步骤是将所有这些部分集成在一起。
向 Eclipse 添加 Android 工具和虚拟设备
你要做的最后一项工作是让 Eclipse 与新的 Android 工具和程序相适应。这样做可以让您将代码输入到 Eclipse 中,然后从 Eclipse 本身进行测试。否则,你必须保存你的代码,并使用不同的程序来测试应用。请遵循以下步骤:
要用将要使用的 Android 工具安装你的 Eclipse,打开 Eclipse 并选择帮助安装新软件。出现 Eclipse 安装对话框,如图图 1-11 所示。每次需要向 Eclipse 添加更多功能时,都要返回到这个安装对话框。
图【月食】1-11 日的安装对话框.
你首先需要让 Eclipse 知道在哪里寻找你想要添加的工具。在安装屏幕上,单击右上角的添加按钮。添加存储库对话框打开,如图图 1-12 所示。
图 1-12。用于向黯然失色添加机器人工具的名称和位置框
Do the following:
- In the name box, type Android tools , which is the name of the tool you will use to refer to this step.
- For location, enter the URL
[
dl-ssl.google.com/android/eclipse/](https://dl-ssl.google.com/android/eclipse/)
, which is the location of the tool you are adding.完成后,点击确定按钮,返回到图 1-13 所示的安装对话框。
图 1-13。开发者工具软件
Select the developer tools check box and follow the prompts to install the update. Doing so can increase the tools needed for Android tablet development. Restart Eclipse when prompted by the dialog box.
在 Eclipse 中,选择窗口首选项。打开侧窗格上的 Android 选项卡。你的屏幕应该看起来像图 1-14 。您将把 Eclipse 指向您的 Android SDK 的安装。这允许您在 Eclipse 中编译程序。
图 1-14。月食中机器人的配置选项在 SDK 位置字段中输入你下载 Android SDK 的准确位置名称。我的例子使用了
C:\Android\Android-sdk
。
应用这些更改后,您就完成了设置过程!
从现在开始,你要专注于实际的 Android 应用的结构,以及如何实现你对游戏的愿景。这种背景使你很容易在游戏中尝试各种不同的工具和技术。能够快速地改变你的代码并看到你努力的结果,在你的努力中是无价的。
测试你的工具
到目前为止,你可能正在急切地期待一些有形的 Android 游戏。这一节讲述了如何使用你已经安装的工具来使用 Android 内置的示例程序库。它还介绍了设计应用外观的基础知识。未来的章节将对这些项目进行扩展,以制作一个全功能的游戏。
你的每一款 Android 游戏都将被开发成一个 Eclipse 项目,在一个位置保存所有的图像、声音和代码。随着您的深入,您将对 Eclipse 有更好的理解。了解资源的存储以及如何在这种环境中访问文件是您需要掌握的一项关键技能。
即使对于最高级的程序员来说,示例程序也是一个极好的资源。你编写的任何游戏所需要的大部分基本功能已经在这些程序中的一个或多个中实现了,而且很可能是免费的。网上粗略看一下,可以为你以后节省几十个小时的工作时间。可悲的是,大多数应用都是为旧版本的 Android 编写的,因此它们在大型平板电脑屏幕上显得非常小。作为补偿,您可以将他们的一些代码合并到您的项目中,但自己处理图形。
在本节的其余部分,您将逐步了解为平板电脑创建 Android 游戏的步骤。至少从头开始一次是很重要的,这样你才能看到一个游戏最基本的框架。首先用 Eclipse 创建第一个 Android 项目。
创建 Android 项目
构建任何 Android 游戏的第一步是创建一个 Eclipse 项目:
在 Eclipse 中,选择文件新建项目,选择 Android 文件夹下的 Android 项目,进入新建 Android 项目界面,如图图 1-15 所示。
图 1-15。填写好的新机器人项目表
Fill in the missing information:
Type the name firstapp or any project name you want.
Keep the default values of other parts unchanged until you reach the construction target part. Here, you can decide which version of Android you want your application to be suitable for. Choose Android 3.0 because you want your application to run on the latest tablet. When you test your game, this name becomes crucial, and you want to make sure that it works well on the analog tablet, not on the small screen of the mobile phone.
应用名称一般与项目名称相同。重新键入 FirstApp 或您为项目使用的名称。Java 开发人员对 Package Name 字段很熟悉,但是如果您不熟悉它,可能会感到困惑。这里你声明名字为
com.gameproject.firstapp
。包是爪哇组织代码的一种方式,使得使用以前编写的文件变得容易。你可以在
[
java.sun.com/docs/books/jls/third_edition/html/packages.html](http://java.sun.com/docs/books/jls/third_edition/html/packages.html)
阅读更多关于爪哇包的内容,但是这对你来说并不重要。当您准备好稍后与世界分享您的应用时,您可以再次访问此页面.将写成 Main 作为你希望项目创建的活动。
活动对于机器人程序来说是必不可少的,稍后我会更深入地讨论它们。现在,把这个活动看作是应用的主要功能。它被调用来设置游戏,然后通过处理输入和指导精灵的移动来运行游戏。活动应该根据它们的角色来命名,所以最初的活动通常被称为主要、主要活动或类似的名称.
用编号 11 填写 Min SDK 版本字段。这意味着 Android 要求设备运行 Android 版本 11 才能正常运行你的游戏。
你可能很好奇为什么我会突然跳到数字 11,当我之前谈到安卓 3.0 是最新的更新时。嗯,安卓有一个疯狂的版本命名系统。3.0 级指的是平台版本,它遵循正常的软件惯例,小的更新增加十分之一位,大的修订得到一个新的数字。为了保持一致,安卓为每个平台版本关联了一个代码安卓 3.0。被分配了 11,而安卓 2.3.3 得到了 10。因为您的项目是为最新版本的机器人设计的,所以您键入 11 作为最低软件开发工具包(Software Development Kit)版本
Figure 1-15 shows a completed new Android project form. Check your name to make sure it is the same, because the remaining code and examples use the name provided in this walkthrough. When you're done, click Finish. You will see a blank Eclipse screen with a project folder on the far left.
现在让我们看看 Eclipse 创建的文件和代码。
探索 Eclipse 中的 Android 项目
要查看项目创建了哪些文件,请展开FirstApp
文件夹。然后进一步将src
扩展到com.gameproject.firstapp
到Main.java
。双击Main.java
在 Eclipse 编辑器中显示文件(中间的大查看窗格)。这是你游戏的核心;然而,目前它是一个基本的骨架。您看到的代码应该类似于清单 1-1 中的代码。
清单 1-1。??Main.java
`package com.gameproject.firstapp;
import android.app.Activity;
import android.os.Bundle;
public class Main extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}`
清单 1-1 中的代码创建了一个新的类,然后让这个类更新用户看到的视图。前三行定义包,然后导入应用需要使用的类。注意,这两个导入都引用了属于 Android SDK 的类。当你制作更多的功能游戏时,你将导入许多其他的类,让你执行各种各样的动作。
让我们一行一行地仔细看看清单中的代码:
package com.gameproject.firstapp
这个简单的介绍指定这个文件是
firstapp
包的一部分。包是 Java 对同一程序的文件进行分组的方式。
import android.app.Activity; import android.os.Bundle;
Import 语句向项目中添加功能。实际上,这些是您想要使用的其他包。
Activity
包括处理应用运行的方法。Bundle
是为您的应用存储信息的一种特定方式。
public class Main extends Activity
这里,类
Main
被赋予了 Android 类Activity
拥有的所有函数和变量。每当一个类扩展另一个类时,新的类继承或接收对其他类的所有功能的访问。
public void onCreate(Bundle savedInstanceState)
这里定义的函数实际上来自于
Activity
类。它处理应用启动时必须完成的所有过程。Bundle
参数savedInstanceState
保存应用的先前状态。第一次启动 app 的时候是null
。
super.onCreate(savedInstanceState);
调用
Activity
类的onCreate
方法。这将导致程序启动应用。注意函数前面的关键字super
。super
关键字意味着程序正在从 Android SDK 调用原来的onCreate
方法,而不是您之前在代码行中创建的新的onCreate
方法。
SetContentView(R.layout.main);
最后,应用通过将 Android 屏幕设置为一个 XML 文件来执行第一个真正的任务。
R
是表示资源的标识符,layout
指定资源的类型,main
表示文件的名称。很快您就可以编辑main.xml
文件来改变程序的外观。
是时候运行这个程序了,看看它能做什么。不过,在此之前,您必须创建一个虚拟的 Android 设备来测试它。如果你有一台运行 Android 3.0 的 Android 平板电脑,你可以直接在上面测试程序。要了解如何做到这一点,请前往附录 A 。
创建虚拟 Android 设备
在 Eclipse 中,创建自己的虚拟设备是一个非常简单的过程:
On the Eclipse main menu bar, select windows Android SDK and AVD manager. An Android SDK and AVD manager screen similar to that shown in Figure 1-8 opens.
因为您没有列出任何设备,点击左上角的新建按钮。弹出一个创建新的 Android 虚拟设备(AVD)对话框,让你定义新的模拟器,如图图 1-16 所示。完成表格如下:
- The device name doesn't matter; I chose the original name tablet _ device .
- Your target Android version is Android 3.0.
- For most applications, you don't need to worry about the size of SD card. However, if your game requires you to store high scores or other data on the device, please specify the size of the onboard data storage.
- And the skin and hardware parts do not need to be changed. However, it is worth noting the hardware specifications. When you make graphics for games, you should definitely use the LCD density of 160 (which is quite standard) to determine the resolution of your images. Compared with many tablet computers, the device RAM size of the simulator is actually quite low. However, the simulator cannot accurately represent the capabilities of RAM or processor. In order to truly show how your game will work, you must try it on a real device.
图 1-16。创建机器人虚拟设备(AVD)
Click the Create AVD button, and you can run your application.
如果你期待一个模拟器出现,你会失望的;新的虚拟设备仅在您运行应用时启动。下一部分启动设备。
运行应用
按照以下快速步骤运行应用:
- In the center of the toolbar near the top of the Eclipse screen is a green play button. Click it, and your program will open a big black screen. This is your new simulator. After a while, when it is loaded, the word Android will be displayed on the screen. Then, with the completion of loading, the word Android with larger font scrolls upwards.
- When the loading screen is finished, move the small round knob to the right. If you wait long enough, the application may start automatically. In this case, the word hello world, Main! appears. If not, please continue to the next step. There is a Google search bar in the upper left corner of the main screen and several buttons at the bottom. Real devices use touch gestures to select applications, but the simulator lets you use the mouse cursor. To run your own program, click the application icon in the upper right corner of the screen.
- A list of all programs on the device appears. Your application uses the universal Android robot as its icon; The application name (FirstApp) appears below the icon. Click on it, and the screen will soon display Hello World, Main!
尽管可能很简单,但您已经启动了您的第一个 Android 应用。当你陶醉其中时,点击左下角指向屏幕左侧的箭头。你回到了桌面家庭。现在尝试模拟器中的其他一些应用。您可能会惊讶地发现,浏览器、电子邮件和其他程序的功能与您预期的完全一样。
AVD 与真实的东西非常相似,甚至允许你测试传感器和 GPS 数据。要测试这个模拟器的速度,你可以制作你自己的令人难以置信的应用。请看下一节,了解如何处理代码。
对应用进行首次更改
尽管从技术上来说,你确实创建了自己的应用,但除了自动创建的应用之外,你并不需要操作代码。现在是时候改变程序的文本了:
在
Res
- Open the
values
folder. You should find a file (strings.xml) there; Double-click it to display it in the viewing pane.- List two string resources. One is the application name, and the other is called
hello
. Clickhello
to change the value to any string you want.- Save your changes and rerun the program. When you open the FirstApp, you should see that you have changed the text on the screen.
要理解这是如何工作的,你需要知道资源的重要 Android 主题。您刚刚编辑的strings.xml
文件是一个资源。大的Res
文件夹中的每个文件也是如此。
如果你还记得main.java
文件,我在代码中提到了一个资源文件:main.xml
,在布局部分。您需要对此文件进行一些更改:
5.要查看文件,请展开layout
文件夹并双击main.xml
。出现一个 WYSIWYG 编辑器,在右上角有一个小屏幕和您创建的字符串。
6.不幸的是,屏幕是为手机设计的。您可以使用顶部带有 2.7 英寸 QVGA 的菜单快速更改这一点。向下滚动列表,直到到达 10.1 WXGA。这就使得屏幕布局十寸多一点,对于平板电脑来说很正常。
7 .。使用编辑器更新布局非常容易。左侧的窗格已经有几个不同的项目可以拖到应用上。试着在你写的文字下面放一个按钮。
8.尽管 WYSIWYG 编辑器很方便,但对于制作游戏来说并不是非常有用。您需要进入图像背后的实际文件。要查看这一点,点击main.xml
(靠近屏幕底部,图形布局旁边)。
清单 1-2 显示了你在布局中添加一个按钮后应该看到的代码。
清单 1-2。??Main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/Button"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> <Button android:text="Button" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content"> </Button> </LinearLayout>
如果您不熟悉 XML,那么这可能看起来像希腊语,但实际上非常容易理解。第一行声明了您正在使用的 XML 类型。下一节创建一个特殊的布局类型,称为LinearLayout
。其中,简单的指令告诉设备如何定位应用,以及它相对于整个设备屏幕的大小。接下来,一个TextView
对象被创建为fill_parent
(扩展以适合整个空间),然后被定义为wrap_content
,这将视图限制为仅需要的数量。
最后,通过调用名为hello
的字符串资源将文本插入到屏幕中。这是您已经编辑过的hello
字符串。
下一部分是您拖到应用上的Button
信息。重要的是要记住,XML 布局并不创造功能,而仅仅是程序的外观。例如,点击你的按钮不会做任何事情,除非你专门设计了一个响应程序。
总结
这一章在让你的开发环境启动和运行方面肯定涵盖了很多内容。您讲述了 Android 背后的概念以及如何创建游戏。在接下来的章节中,你将会彻底地检查布局以及如何为游戏创建一个吸引人的背景。然后你创建精灵,并开始通过在屏幕上移动玩家来给你的应用添加一些味道。后面的章节添加用户输入、声音和人工智能来完成你的创作。
二、使用精灵和动作创造简单的游戏
祝贺您,您已经成功地设置了您的开发环境,并准备好继续进行更具创造性的游戏开发活动。当你想到你最喜欢的游戏时,你可以马上想象出它的样子,无论是怪物向你跑来还是汽车在赛道上跑来跑去。在本章中,您将为平板电脑屏幕注入活力。市场上有成千上万的游戏,你的游戏的外观和感觉可以决定它有多成功。
本章介绍在平板电脑屏幕上显示图像,然后移动图像的基础知识。你了解了精灵的概念。为了本章的目的,精灵是任何在游戏中可以移动的游戏物体。游戏中的主角或敌人通常是精灵,但游戏的背景却不是。
这一章的内容进展相当快,引入了许多新概念。
处理图像
精灵是游戏的基础,在你创建游戏之前,你需要能够在屏幕上画出它的卡片、角色和其他物体。在本节中,您将学习 Android 3.0 图形显示的基本组件。我们还将计算出精灵的组成部分,并在屏幕上移动我们的图像。这将成为我们未来项目的基础。看看图 2-1 看看我们的游戏会是什么样子。这个启动精灵实际上是来回跳动的。
图 2-1。完成的图形程序。
注如果你迷路了,从与这本书相连的谷歌代码项目中复制代码。然后回到课程中,你将能够通过操作它的某些方面来理解程序是如何工作的。
创建图像显示表面
要开始,您需要打开一个新的 Eclipse 项目。在上一章中,您在 Eclipse 中创建了一个名为 FirstApp 的新项目。那个代码对你来说已经没用了。从一个全新的项目开始:
- Select File New Project Android Project from the Eclipse main menu.
图 2-2。图形测试的项目创建窗口..
- Your app name is GraphicsTest. Make sure that the completed form looks like in Figure 2-2. Getting used to creating new projects in Eclipse is very important, because if something goes wrong, this is usually the easiest way to start from scratch.
- When the form is finished, click Finish. If you need help filling in other fields, please refer to Chapter 1.
在平板电脑上显示图像之前,您需要一个画布来渲染图像。您在程序主例程中构建曲面。请遵循以下步骤:
第一个项目中的文件可能仍在您的主编辑面板中打开。通过右键单击文件选项卡旁边的并选择全部关闭来关闭它们。这不会删除代码,而是关闭显示代码的编辑屏幕。* 在 Eclipse Package Explorer(位于屏幕左侧)中打开 GraphicsTest 项目的文件树。您想检查 Java 代码,所以打开
src
文件夹,然后继续展开,直到看到MainActivity.java
。图 2-3 显示了文件的位置。
***图 2-3。**用于图形测试的包资源管理器* * Open `MainActivity.java` in the editing pane, and you will see the general code generated in the first chapter. In the first chapter, you use a Java code file and an XML file to handle the layout. Unfortunately, a game with a lot of movements and graphic changes can't be easily built in XML. Therefore, you need a Java file to run the graphics of the game.* To this end, right-click `com.gameproject.graphicstest` in GraphicsTest Package Explorer to create a new class. Select a new category. A dialog box opens , asking how you want your new class to be named. Type **game view** , and pay attention to keep the default values of all other fields. When you are finished, you will find two files (`MainActivity` and `GameView`) in your `src` directory.* Open the `GameView.java` file in the viewing pane. You should find the code in Listing 2-1 there.
清单 2-1。??GameView.java
`package com.gameproject.graphicstest;
public class GameView {
}`
您可以添加到这个原始源代码中,将图像文件绘制到屏幕上。但是,在开始之前,您必须了解 Android 中视图和显示的基础知识。
Android 视图类是如何工作的
到目前为止,您只在项目中使用了两个 Android 类:Activity
和Bundle
。活动包含处理应用的创建、运行和关闭的功能。它们是任何安卓游戏的命脉。Bundle
类仅仅是保存程序当前状态的一种方法。
然而,现在你看看View
类。应用运行时,视图处理屏幕的图形和外观。你所有的游戏都将创建一个类来扩展View
类并给你这个功能。通常,你的View
类中的代码比Activity
类中的多得多,因为游戏的大部分内容都是操纵屏幕上的对象。
所有函数类必须有两个不同的部分。第一个是构造函数方法。像任何类一样,当您创建它的实例时,您需要调用一个函数来定义对象的各个方面。在View
类中,你可以加载你的图像并决定所有精灵的起始位置。
View
类的下一个关键部分是将图像呈现到屏幕上的方法。每次移动图像时都会调用这个函数,因为图像必须在新的位置重新绘制。
尽管这是一种抽象的查看类的方式,但它有助于您理解代码。然而,在深入研究之前,让我们来看看实际获取一个文件并将其显示在屏幕上的机制。
提示如果你对
View
类或任何其他 Android 类感到好奇,访问[
developer.android.com/reference/packages.html](http://developer.android.com/reference/packages.html)
并找到你正在寻找的包。在这里,Android 提供了关于如何使用这些类以及每个类包含的各种方法的文档。
Android 如何渲染图像
View
类只是将图像呈现到屏幕上的整个方法的一部分。其他构建块包括图像、存储图像的方式、绘制图像的方法以及屏幕上的最终结果。
图像存储在项目中。下一节将介绍如何添加图像。一旦图像存储在应用中,你就可以通过将它分配给一个位图来访问它。位图是你描述图像的一种方式,并准备好将其传送到屏幕上。
在显示之前,必须通过画布进行渲染。画布包含绘制图像的方法。在视图内部,您调用画布来处理绘制过程。视图是他们控制的屏幕的指定部分。在您的例子中,视图拥有整个屏幕。然后画布将图像绘制到屏幕上。
渲染一幅图像
为了真正理解 Android 中的View
类是如何工作的,让我们用它来显示一个图像:
- 你需要一个图像文件加载到屏幕上。您可能已经准备好了一个图像文件,或者您可能需要创建一个。您电脑上任何扩展名为
.png
或.bmp
的图像都可以。- 如果你有一个现成的图像,确保它不超过 500 × 500 像素。
- 如果你想画自己的图像,我通常使用 Inkscape (
[
inkscape.org/](http://inkscape.org/)
)或 GIMP ([www.gimp.org/](http://www.gimp.org/)
)作为我的图形编辑器,因为这两个都是免费的。如果你喜欢自己的图形编辑器,那也很好。- 将文件拖到 GraphicsTest 项目的
res
drawable-mdpi
文件夹中。Eclipse 问你要不要复制;单击是,您就可以开始了。- 如果您仔细查看项目的
res
文件夹,您会看到它包含三个以单词drawable
开头的文件夹。这些都指定了设备上图形的特定分辨率。对于为平板电脑构建的游戏,您使用中等清晰度文件夹;但是如果你是为手机开发的,你会希望每张图片都有三种分辨率的不同版本,以确保大多数手机能够尽可能快地渲染它们。- 在编辑窗格中打开
GameView.java
文件,用清单 2-2 中显示的代码替换清单 2-1 中的代码。这段代码将您的图像呈现到平板电脑的屏幕上。之后我会解释每一部分的作用。清单 2-2。??
GameView.java
`package com.gameproject.graphicstest;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.View;class GameView extends View {
public GameView(Context context) {
super(context);
}@Override
public void onDraw(Canvas canvas) {
Bitmap star = BitmapFactory.decodeResource(getResources(), R.drawable.star);
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(star, 10, 10, null);
}
}* 哇,事情变得很复杂。清单 2-2 中的代码其实非常简单明了,不用多解释你大概就能理解大部分。* 第一个主要变化是增加了许多新的
import语句。其中大多数调用 Android 的图形包,而最后一个调用
View类。第一个导入涉及到您用作函数参数的
Context类。* 实际代码的开头显示了您创建的类是如何扩展
View类的功能的。这是 Java 中的一种常见做法,只是简单地继承了
View类的方法和变量供自己使用。如果您不这样做,您将无法在屏幕上绘制图像。* 第一个函数
GameView,是一个不启动任何东西的虚拟函数。你以后会用到它,但是现在,把它放在那里以满足 Java 对类的要求。* 最后,源代码的核心是处理屏幕变化的
onDraw方法。您使用
@Override符号来运行您自己版本的
onDraw函数,而不是由
View类提供的原始
onDraw()。该方法的参数包括非常重要的负责图像绘制的
Canvas。下一行简单地创建了一个新的
Bitmap对象,并将您的图像文件上传到其中。因为我使用的图像文件被命名为
star.png,我将它的位图命名为
star。在这段代码中你看到的三个地方替换你的图片的名字。或者,您可以重命名您的图像
star.png,而根本不需要更改代码。* 接下来,你让
Canvas对象将整个屏幕涂成黑色。这是多余的,因为黑色是默认的,但保持这条线是一个好习惯。如果您喜欢不同的背景颜色,请用您的颜色名称替换
black。注意,Android 接受大部分传统的颜色名称;但是如果你正在寻找一种特定的粉红色,你必须写出 RGB 值,如下面的语句所示:
canvas.drawColor(Color.argb(0, 100, 100, 100));*
argb函数将 alpha、红色、绿色和蓝色的数量作为整数形式的参数。* 清单 2-2 的最后一行调用
drawBitmap方法将图像绘制到屏幕上。注意这个函数的参数是
(Bitmap bitmap, float left, float top, Paint paint。您不使用
Paint对象,所以您传递一个
null值给它。您可以通过编辑图像从顶部和左侧的距离值来更改图像的位置
n。在这之后,你想看到你的劳动成果。虽然您有办法将图像呈现到屏幕上,但是您的应用永远不会使用它,因为程序的开始没有调用 drawing 方法。您可以通过在
MainActivity中创建一个
GameView的实例来改变这一点。要做到这一点,您必须在
MainActivity.java文件中更改一行来指向您的
GameView类。* 在 Eclipse 的顶部,打开
MainActivity.java文件。找到类似这样的行:
setContentView(R.layout.main);* You likely remember this as the line that tells the device to load the
main.xmlfile as the layout of the app. You want to replace that XML with
GameView.java`. This is readily done by adding the statement in Listing 2-3 inside the MainActivity constructor.
***清单 2-3。**使用`GameView.java`作为视图* `setContentView(new GameView(this));`* 这个语句的添加创建了一个`GameView`类的新实例,并将其作为应用的视图加载。你现在可以开始尝试你的作品了。* 单击 Eclipse 顶部的绿色 play 按钮,应用启动。当模拟器已经开始播放新的应用时,请遵循第一章中的步骤。如果一切顺利,你的图像(最初是一个`.png`文件)会在屏幕上生动地显示出来。
这个结果肯定不是很令人兴奋,所以你的下一个目标是移动屏幕上的图像。
与精灵一起工作
你可以在屏幕上移动一个图像,你必须给它起个名字。游戏不会四处移动图像或形状,而是使用精灵——屏幕上的对象由图像表示,但其方法和属性提供了您需要控制和跟踪它们的功能和状态。创建一个专用的Sprite
类有很多好处。你可以很容易地添加动画序列和旋转,甚至跟踪每个精灵的生命或弹药。在你创建一个Sprite
类之前,让我们研究一个更好的方法来显示精灵和一个更高级的游戏循环来处理它们的一致移动和更新。
渲染精灵
您需要对您创建的View
类做一些重大的修改。首先,让我们使用SurfaceView
类而不是View
类。这是一个微妙的区别,但是SurfaceView
类具有加快渲染速度的优势。当你在下一章看动画时,你会了解到SurfaceView
类的来龙去脉。清单 2-4 显示了GameView.java
的新代码。将您的当前代码更改为这个新版本。它为你更高级的图像和精灵应用奠定了基础。
清单 2-4。GameView.java
`package com.gameproject.graphicstest;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class GameView extends SurfaceView implements
SurfaceHolder.Callback {
public GameView(Context context) {
super(context);
setFocusable(true);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
}
public void update() {
}
}`
现在,GameView.java
除了使画布变黑之外,不执行任何有意义的操作。您从类中移除了绘图函数,以便以后可以在您的Sprite
和Thread
类中实现它们。新的GameView
类的第一个重要部分是它现在实现了SurfaceHolder.Callback
。这是负责控制表面,并使您能够从它被创建时开始绘制,直到它被破坏。这样,您就有了三种可以覆盖的方法:surfaceChanged
、surfaceCreated
和surfaceDestroyed
。你很快就可以用处理精灵和游戏循环的指令填充其中的一些。
当您需要初始化您的Sprite
类的实例时,您也可以使用GameView
的构造方法。在代码的最后,你有onDraw
和update
函数。在这一章的前面,你使用了onDraw()
把你的图像放到屏幕上,所以看起来应该很熟悉。update
功能新增;您可以用它来调用每个 sprite 来更新自己。有了处理图像的能力,你现在可以探索游戏是如何运行的。
构建游戏循环
要运行好游戏,你需要利用 Java 的Thread
类的能力。如果你用现代语言编程过,你可能以前遇到过线程。一个线程是设备执行的独立例程。在所谓的多线程中,线程几乎总是与其他线程一起使用。这基本上意味着线程是自主存在的,并且通常由一个程序同时运行以执行不同的功能。一个例子是在一个线程中运行游戏的图形,而在另一个线程中处理物理。显然,这两件事必须同时发生,所以你多线程的程序。
要构建 Android 游戏,您需要使用 Java Thread
类。您可以在Java.lang.Thread
中找到Thread
类的源代码。您不必导入它,因为假设它是可用的;然而,记住这是您正在使用的类是很重要的。对于您的目的来说,线程非常简单。你创建一个扩展Thread
的类,然后你覆盖run
方法,把你的游戏循环放在那里。从那里,您可以改变视图或处理碰撞或收集输入。
既然您已经看到了我们在GameView
中所做的更改,让我们创建Thread
类的所有重要扩展:
在 Eclipse 中新建一个类,命名为
GameLogic
。因为GameView.java
处理游戏的外观,所以GameLogic.java
处理幕后的计算才是合适的。
提示当你创建越来越多的源代码文件时,给类起一个非常具体的名字会很有帮助。如果你的游戏包含不同类型的精灵或物体,不要给类标上
SpriteOne
、SpriteTwo
等等。我总是试图根据一个类的确切功能来命名它,比如EnemySprite
或者FlyingSprite
.Listing 2-5 shows the entire listing of
GameLogic.java
. Similar to the implementation of yourSurfaceView
class, the current code is spartan. Copy the code in Listing 2-5 and replace the original code inGameLogic
.
清单 2-5。GameLogic.java
`package com.gameproject.graphicstest;
import android.graphics.Canvas;
import android.view.SurfaceHolder;
public class GameLogic extends Thread {
private SurfaceHolder surfaceHolder;
private GameView mGameView;
private int game_state;
public static final int PAUSE = 0;
public static final int READY = 1;
public static final int RUNNING = 2;
public GameLogic(SurfaceHolder surfaceHolder, GameView mGameView) {
super();
this.surfaceHolder = surfaceHolder;
this.mGameView = mGameView;
}
public void setGameState(int gamestate) {
this.game_state = gamestate;
}
public int getGameState(){
return game_state;
}
@Override
public void run() {
Canvas canvas;
while (game_state == RUNNING) {
canvas = null;
try {
canvas = this.surfaceHolder.lockCanvas();
synchronized (surfaceHolder) {
this.mGameView.update();
this.mGameView.onDraw(canvas);
}
}
finally {
if (canvas != null) {
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
}
}
}`
这里列出了GameLogic.java
的重要方法以及每个方法的功能:
SurfaceHolder()
: Create a means to manipulate the canvas. In the code ofrun()
function, it locks and unlocks the canvas you draw. Locking the canvas means that only this thread can write. You unlock it and allow any thread to use it.Gameview()
: Create an instance of yourGameView
class and use it to call theupdate
andonDraw
methods you saw in the previous section.setGameState()
: Create a system to store the state of the game at any given time. Later, you can use this when you have a pause screen or want to display a message when the player wins or loses the game. The game state also determines the time when you execute the game loop.run()
: When the game is running, try to lock the canvas, then perform the operation you need, release the canvas, and prepare to restart the process.
虽然GameLogic.java
可能看起来很简单,但它并没有解决游戏中的许多问题。首先,没有合适的计时系统。该循环将按照处理器允许的速度运行,因此快速的平板电脑将运行得很快,而较慢的平板电脑将具有明显较低的速度。稍后,这一章用一个非常简单的方法来解决这个问题,当你的目标是每秒 30 帧(fps)左右时,调节精灵的移动量。
GameLogic.java
也不处理任何任务,如输入或碰撞检测,这些将在以后实现。目前,GameLogic
是一个重复执行操作的工具,不会使GameView
类复杂化。
创建精灵
构建游戏的下一步是创建Sprite
类。虽然你的游戏只需要一个GameLogic
和GameView
的实例,但是你的游戏里可以有几十个小精灵;所以代码必须是通用的,但是允许你在精灵上执行所有必要的操作。
因为在任何 Android 包中都没有真正的Sprite
类的基础,所以您从头开始创建代码。基本变量是你的类的根。例如精灵的 x 和 y 坐标以及精灵的图像本身。您还想存储精灵在每个方向上的速度。最终,你的精灵的生命值和其他方面也会被保存在这里。为了保持Sprite
类的原始性,您将所有这些变量标记为private
,并使用一个函数来更改它们的值和检索它们的值。这是常见的做法,可以防止您在想要检索这些值时无意中更改它们,反之亦然。
清单 2-6 展示了你的SpriteObject
类的代码。经历在 Eclipse 中创建新类的正常过程,然后用这段代码填充它。该代码执行一些非常简单的任务,因此理解它应该不会有太大的困难。
清单 2-6。??SpriteObject.java
`package com.gameproject.graphicstest;
import android.graphics.Bitmap;
import android.graphics.Canvas;
public class SpriteObject {
private Bitmap bitmap;
private int x;
private int y;
private int x_move = 5;
private int y_move = 5;
public SpriteObject(Bitmap bitmap, int x, int y) {
this.bitmap = bitmap;
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public Bitmap getBitmap() {
return bitmap;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, x - (bitmap.getWidth() / 2), y - (bitmap.getHeight() /
2), null);
}
public void update() {
x += (x_move);
y += (y_move);
}
}`
这个类的最后两个方法——draw()
和update()
——是最有趣的。在GameLogic.java
中从游戏循环中调用draw
功能。在将图像渲染到屏幕上之前,使用update
操作来增加 x 和 y 坐标。请注意,您可以通过手动改变变量来改变移动速度,或者您可以创建函数,让您根据碰撞或用户输入等事件来改变精灵的速度。
运行游戏
通过对你的GameView
类做一些快速的修正,你可以拥有一个完整的应用,它将你的精灵投射到屏幕上。首要任务是在GameView
中创建GameLogic
和SpriteObject
类的实例,这样您就可以利用新创建的类了:
Open the
GameView
class so that you can add some code to it.将你的类的两个实例(如清单 2-7 所示)放在
GameView
构造函数之前。??清单 2-7。创建你的类的实例
private SpriteObject sprite; private GameLogic mGameLogic;
在
GameView
类里面,你调用两个类的构造函数。然而,要格外注意论点的结构。最后一行让您能够使用设备。将清单 2-8 中的代码添加到GameView
构造函数中。清单 2-8。构造新的类
sprite = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.star), 50, 50); mGameLogic = new GameLogic(getHolder(), this); getHolder().addCallback(this);
SpriteObject
Take the coordinates of bitmap and sprite. The method of obtaining bitmap resources is the same as what you did in the first example of this chapter.GameLogic
Take aSurfaceHolder
and aGameView
. The functiongetHolder()
is a part of theSurfaceView
class, which allows you to send the current holder to the method.现在你可以利用
surfaceCreated
函数中的新对象了。清单 2-9 显示了应用一创建表面就开始游戏循环的代码。??清单 2-9。开始游戏循环
@Override public void surfaceCreated(SurfaceHolder holder) { mGameLogic.setGameState(GameLogic.RUNNING); mGameLogic.start(); }
随着你游戏的肉开始,你要把你的方法放进
onDraw
和update
的套路里,如清单 2-10 所示。注意GameView
类没有调用这些函数;它们是从GameLogic
类中调用的。清单 2-10。使用游戏中的物品
`public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
sprite.draw(canvas);
}public void update() {
sprite.update();
}`The
onDraw
method gets sprite to draw itself, and then theupdate
function lets sprite execute itsupdate
function. Separating theupdate
method from theGameView
method reduces the confusion within the class. Whenever a specific task must be executed, complete it in a separate function to keep your main code clean and tidy. All your code is ready and the game can be executed now. Make sure that all Java source codes are saved, and then click the green Eclipse play button to start the simulator.
注意如果您收到找不到类的错误消息,您可能已经在不同的文件夹中创建了 Java 文件。在左窗格的文件树中,确保所有四个文件都在
src
文件夹中。
如果一切顺利,您应该会看到图像从左上角到右下角快速穿过屏幕。正如你之前注意到的,根据运行该程序的计算机或设备,精灵可能会快速或缓慢地移动。有了控制精灵移动的能力,你可以改变x_move
和y_move
的值来加速或减速。接下来的部分是清理用户界面,为一些激烈的游戏做准备。
获得专业形象
游戏意味着尽可能沉浸其中。要在平板电脑或任何设备上实现这一点,你必须移除所有提醒玩家游戏之外世界的栏和菜单。Android 有一些功能使这变得有效,但是 Android 3.0 包括了实际上要求系统栏总是可见的功能。不管怎样,您可以在MainActivity.java
文件中用一行简单的代码隐藏动作栏。清单 2-11 显示了应该紧跟在super
命令之后的语句。
清单 2-11。移除动作条
getActionBar().hide();
如果你现在运行这个项目,带有 Android 机器人图标的顶栏就会消失。图像应该正常地在屏幕上移动。为了让你的游戏看起来更专业,你可以把默认图标改成更适合你游戏的图标。当玩家想要打开你的游戏时,他们会进入他们的主页并选择应用。提供一个充满活力的图标来吸引他们的注意力是很重要的。
在创建你自己的图标之前,记下图标的尺寸。你需要有 72 × 72,48 × 48,32 × 32 的版本。在您的图形编辑器中,创建一个最大尺寸的图标,然后将其缩小以显示其他尺寸。当你有了这三个文件后,将它们命名为icon.png
,并替换res
文件夹下每个分辨率类别中的其他图标文件。
现在,唯一要做的工作就是在你的代码上加一个头文件,就像清单 2-12 中所示的那样,这样你就可以发布你的游戏,而不用担心别人在不认可你的情况下拿走你的作品。诚然,网上发布的任何东西都可能被不当使用,但是在你的作品上签名可以帮助人们向你提问,或者至少在该表扬的地方给予表扬。
清单 2-12。代码上方的示例注释头
/******************************************************************** * GraphicsTest – illustration of basic sprite principles * * * * Author: Kerfs, Jeremy * * * * Last Modified: Jan 1st, 2000 * * * * Version: 1.0 * * * ******************************************************************/
如果你真的对保护你的作品感兴趣,你可以在许可下发布代码。例如,Android 代码本身是在 Apache License Version 2.0 下发布的,这非常自由,允许用户将代码主要用于他们可以想象的任何项目。如果你在网上发布你的代码,准备好在一个许可下提供它,保持它的开源,让其他人在上面开发。
提示关于知识共享许可和开源项目如何工作的更多信息,请访问
[
creativecommons.org/](http://creativecommons.org/)
。
实现定时和复杂运动
现在,您可以继续创建一个系统,使您能够准确地设置游戏运行的速度。游戏循环将不再受制于设备的突发奇想。要做到这一点,你可以使用一个计时器,然后根据经过的时间调整移动。这意味着,如果一个周期花费很长时间,而另一个周期花费很短时间,精灵将根据该值移动一定的距离。查看代码是理解这种方法的最好方式。请遵循以下步骤:
用清单 2-13 中的代码替换
GameLogic.java
中synchronized
块中的代码。清单 2-13。测试恒定英国制药学会会员游戏
`try {
Thread.sleep(30);
}
catch (InterruptedException e1) {
}long time_interim = System.currentTimeMillis();
int adj_mov = (int)(time_interim - time_orig);
mGameView.update(adj_mov);
time_orig = time_interim;
this.mGameView.onDraw(canvas); `At first, the whole clip looked strange. In fact, it performs several simple tasks:
- [-] blocks tell the tablet to wait 30 milliseconds before continuing. This operation may result in an exception that you cannot handle.
- Previously, in the
run
function declared next toCanvas
object, you created two long variables namedtime_orig
andtime_interim
. Uselong time_orig = System.currentTimeMillis();
to setTime_orig
to the current system time. Now, you settime_interim
as time and determine how long it has passed. You store it in the integeradj_mov
, which represents the adjusted movement of . TheUpdate
function in theGameView
class was changed to accept the integer as a parameter. After completion, the original time is set to the current time, and the view is refreshed by calling theonDraw
method.将清单 2-14 中的代码添加到
GameView
中的update
方法中。清单 2-14。的
GameView.java
与修改后的update
功能
public void update(int adj_mov) { sprite.update(adj_mov); }
清单 2-15 显示了
adj_mov
变量被传递给精灵,以便它被合并到运动中。清单 2-15。的
SpriteObject.java
与修改后的update
功能
public void update(int adj_mov) {
x += (adj_mov * x_move); y += (adj_mov * y_move); }
In this case, the sprite
update
method multipliesx_move
andy_move
by the change of time. I changed the speed constant to1
in order to keep the movement at a reasonable speed. This makes sense, because if the calculation takes a long time, the movement will be multiplied by a larger number. If the processing speed is fast, the wizard won't move that far. The idea of controlling the number of frames per second in a game has many meanings, which you can use in future projects.
虽然你可能会想象大多数游戏都想要有时间元素,但是许多应用可以不用担心这个问题。想想象棋或井字游戏。在回合制游戏中,时机不是一个重要的方面。
注意示例程序可从 Android 的参考指南中获得,你可以从不同类型的游戏中寻找灵感。查看该页面的代码源:
[
developer.android.com/resources/browser.html?tag=sample](http://developer.android.com/resources/browser.html?tag=sample)
。需要注意的是,大多数程序是为早期版本的 Android 编写的,比如 2.2 或 2.3。如果您真的对示例感兴趣,您可能想创建一个特定于该版本的仿真器。将它们移植到 Android 3.0 并不困难;你可以通过放大图片和屏幕尺寸来实现。
检测碰撞
虽然你没有收集用户的输入,但你可以通过处理与平板电脑墙壁的简单碰撞来给游戏注入一定的响应能力。清单 2-16 中显示了一个快速简单的实现。
清单 2-16。碰撞代码
if (sprite.getX() >= getWidth()){ sprite.setMoveX(-2); } if (sprite.getX() <= 0){ sprite.setMoveX(2); }
将这段摘录添加到GameView.java
文件的update
方法中。在调用sprite.update()
之前做这件事很重要,因为你必须在增加精灵的位置之前处理任何方向的改变。
您可能会注意到,您引用了一个尚未创建的函数。为了让这个函数工作,SpriteObject
类需要两个函数,分别叫做setMoveX
和setMoveY
。这些的基本代码如清单 2-17 所示。
清单 2-17 。碰撞功能
public void setMoveX(int movex){ x_move = movex; } public void setMoveY(int movey){ y_move = movey; }
在你运行这个程序之前,我手动将y_move
改为 0,这样你就可以消除任何上下移动。当你播放程序时,精灵会在屏幕的左右两边来回跳动。不过,这个运动应该有些奇怪:当精灵到达屏幕的最右侧时,它几乎消失了,因为你在精灵的位置上引用了你的碰撞,这是由其中心给出的。您可以通过考虑精灵的实际大小和尺寸来消除任何消失。
如果你想试验必要的改变,继续操作if
语句来反映精灵的绝对左和绝对右。第七章深入讨论碰撞;您使用RECT
元素来精确地找到不同精灵和墙壁或地板之间的交叉点。
注意碰撞检测几乎是任何游戏的一个重要方面,有多种方法可以实现。当您稍后想要使用项目符号或其他不规则形状时,您可以使用各种多边形来寻找精灵的交集。快速搜索不规则碰撞检测会产生大量信息来继续这个话题。
总结
这一章讲述了渲染图像的方法,你制作了一个框架来管理精灵并以一致的速度在屏幕上移动它们。通过这项工作,大多数游戏应该可以在你当前的系统上运行。在接下来的几章中,添加用户输入和声音将使高质量的游戏成为可能。当您使用 OpenGL 来加速复杂游戏的图形并向您的Sprite
类添加动画框架时,您将在稍后处理高级图形主题。所有这些代码都是以可重用和适用于任何数量的游戏为目标而编写的。
作为本章的结尾,请确保所有代码和示例都能在您的系统上正常运行。如果你犯了任何错误,你可以从这本书的相关网站下载第二章的代码。在继续前进之前,你需要理解这些信息,这样你就可以轻松地处理人工智能和高级物理学,它们根据时间表扩展了一致运动的思想。
下一章涵盖了一个完全不同的主题:用户输入。你的精灵可以活过来,对用户的行为做出反应。这是游戏开发中非常令人兴奋的部分,因为它是交互性的核心。否则你会设计一部复杂的电影。
三、收集用户输入
到目前为止,你的作品缺乏游戏的互动性,主要是因为你没有办法让玩家与精灵和角色互动。在这一章中,你将解锁几种你最终用来控制游戏外观和动作的输入形式。这也是你在 Android 3.0 中解锁平板游戏编程的一些惊人特性的地方。以前,大多数这样的工作适用于旧的 Android 手机,但现在程序员严重依赖大触摸屏来收集触摸输入。
平板电脑的独特之处在于,用户可以进行大量选择;而作为开发者,你需要为此做好准备。除了这种显而易见的交互方法,您还会看到一些更不寻常的输入,比如加速度计数据和手势。除了输入,您还将介绍事件队列如何帮助您简化游戏。但让我们先来快速概述一下大多数 Android 平板电脑上的输入设备。
了解平板电脑输入选项
为了理解你的选择,你需要知道平板电脑在收集用户行为数据方面提供了什么。以下是许多平板电脑中存在的传感器的一个相当全面的列表。一些平板电脑不具备所有这些功能,而其他平板电脑则具备额外的功能:
- Touch screen: Most tablet computers have a multi-touch interface that allows you to input on the screen with several fingers at a time. Precision screens now allow you to create very small elves, and users can still drag them because of the precision of the screens. For most games, this is a user-controlled method. Almost every game needs this as its menu.
- Microphone: Tablet PCs running Android 3.0 often have built-in microphones that can be used as input. Examples include changing the height of the helicopter according to the pitch or volume of sound samples. Although there are many interesting applications, it is not used in most games.
- Accelerometer: This sensor measures the orientation change of the tablet computer. When you rotate the tablet from landscape to portrait, you may be familiar with this: the screen usually adjusts itself according to the accelerometer data. In flight games and racing games, this is an interesting way for users to control vehicles.
- Gyroscope: Similar to accelerometer, gyroscope measures the rotation rate along three axes of motion. This is for precise movement and can tell you the exact mode of rotation. Games that use accelerometers can also use gyroscopes.
- Proximity: The proximity sensor measures the distance between the object and the mobile phone. These are usually inaccurate and are mainly used to turn off the screen near your cheek (when you make a phone call with a touch-screen phone). However, few games use this.
虽然这个列表包括了游戏从玩家那里收集数据的大多数方式,但是您可以访问描述玩家所在区域和周围环境的其他传感器。这不能代替用户交互,但是它增加了游戏的真实性。以下是大多数平板电脑提供的获取这些信息的方式:
- GPS: The GPS positioning of a device can make the game map an image of the surrounding area or change the scene or characters of a game. It is not possible to consider all the different locations where a device may be located, but later you will study how to incorporate this point.
- Ambient light: This sensor is mainly used to adjust the brightness of the screen according to external light, but it does provide some advantages for game developers. If the user is in a dark place, one way to integrate it is to change the game to night scene.
- Barometer: This sensor is a joke rather than what it is. But in reality, the game may use it to estimate the altitude and adjust the game accordingly. I haven't seen a game that successfully integrates this sensor.
了解了平板电脑上可以找到的各种内部传感器,您也可以开始考虑可能想要连接到它的其他输入设备。Android 3.0 自带对任何 Android 版本蓝牙输入的最佳支持。每一个新版本都可能继续扩展这一点。虽然蓝牙输入可能令人兴奋,但为平板电脑编写游戏的目的是为用户提供独特的体验。如果他们仍然必须连接他们的游戏控制台,那么他们也可以使用电视。也就是说,Android 现在对操纵杆、键盘和游戏控制器提供了原生支持。
注意随着运行 Android 和 Chrome 混合操作系统的电视的出现,将平板电脑用作控制器成为可能。将平板电脑连接到运行 Android 的电视上,可以让电视屏幕显示游戏,同时平板电脑还可以充当地图和控制器。Android 越广泛地用于驱动设备,你在输入方面就有越多的机会。
您几乎已经准备好为您的游戏设置一些输入了。然而,首先你要回顾一下收集信息背后的一些理论。快速获得输入对游戏来说至关重要,而传统应用(例如地图或地址簿)不需要这种速度。
了解平板电脑输入
对于传统的应用,程序通常依赖于输入事件。这意味着,在用户与应用交互之前,什么都不会发生。大多数应用有许多不同的菜单和文本,用户一按下按钮,代码就负责筛选。在这些情况下,传统的游戏循环是不必要的,因为在触摸事件发生之前没有任何事情要做。同样的原则也适用于没有触摸屏的手机的旧程序。
如果你想制作这种应用,Android 可以让你非常简单地为屏幕上的各种按钮和图像添加监听器。通过在按钮被按下时执行特定的动作,您可以操纵程序的屏幕和动作。令人惊讶的是,这种用平板电脑工作的方式与游戏有一定的关联。以回合制策略游戏(如国际象棋)为例:在玩家移动精灵之前,什么都不会发生。
所有收集用户输入的方法都不可避免地涉及到非常重要的游戏过程。这是应用经历的事件循环。有多少种游戏类型,就有多少种游戏过程。这些循环都处理输入,确定物理,并给用户反馈(通常通过改变游戏的显示)。图 3-1 显示了一款回合制策略游戏的游戏流程。
图 3-1。依赖输入游戏的游戏过程
注意如果你对回合制游戏感兴趣,你可以在网上查看几个例子。一个非常简单但优雅的演示是一个井字游戏的 Android 示例。可以在这里下载源码:
[
developer.android.com/resources/samples/TicTacToeMain/index.html](http://developer.android.com/resources/samples/TicTacToeMain/index.html)
。
因为大多数现代游戏都是快节奏和输入密集型的,所以你在处理输入事件的同时还要执行图形和逻辑操作。为了做到这一点,你收集输入并处理它,以便不中断游戏的流程。你可能认为这是同时做不同的事情;但是更实际的情况是,您注意到一个输入事件,然后等待在进入下一个游戏周期时处理它。图 3-2 显示了输入事件是如何存储的,直到你准备好处理它们。
在第二章的中讨论的Thread
类在处理输入事件中起着很大的作用。然而,View
类是注册输入事件的方法的地方。事实上,View
类有几个方法,您可以覆盖它们来执行您自己的触摸事件工作。
您实质上改变了您的Sprite
类,因为您已经有了像setX(int)
和setY(int)
这样的方法,一旦输入事件发生,您就可以操纵 sprite。你创建新的事件来改变精灵的速度和方向。稍后,一个触摸事件可能会导致精灵重新装填弹药或施放特殊法术。在本章结束时,你将使用某些事件来产生一个全新的精灵。
为了清楚地了解示例游戏是如何工作的,图 3-2 展示了如果用户开始快速点击屏幕,你如何处理输入事件而不使游戏停止。请注意图 3-1 和图 3-2 之间的差异。
你使用的游戏流程版本有两个在回合制游戏中没有的附加功能,如图 3-1 所示。首先,应用路径不是线性的:输入事件在需要时被添加到游戏中。第二,游戏循环管理整个过程,而回合制游戏中的处理循环由输入控制。这是一个关键的区别,因为不管用户在做什么,你的游戏都需要继续,而图 3-1 中的游戏必须等待用户交互。
你的游戏的图形渲染也是不同的,因为它经常发生。在回合制游戏中,图形会在用户输入事件发生后改变。但是,即使什么都没有改变,您也将在循环的每个周期中更新图形。
图 3-2。连续游戏的游戏循环
图 3-2 显示了一个通过图形和用户输入循环的连续游戏过程。任何不需要用户输入而继续的游戏循环都可以被称为游戏循环。请注意,一个输入事件会导致一个输入队列,事件会存储在该队列中,直到您准备好接受并响应它。因为您的游戏以每秒高帧数运行,所以您在输入响应方面不会有任何明显的延迟。
对触摸做出反应
Android 可以在各种设备上运行,因此它有办法收集所有类型的输入。然而,对你来说,最重要的事情是与触摸屏的互动。你的首选方法如下:
Public boolean onTouchEvent(MotionEvent event){ }
这个方法是从View
类继承的,为了执行自己的操作,您可以覆盖它。这个函数的关键是它包含的参数。A MotionEvent
是 Android 中的一个对象,描述了与平板电脑的各种交互。您可以通过调用MotionEvent
类中的方法来找到关于该事件的很多信息。这个类有许多选项,但是重要的函数会在您使用它们时指出。如果你很好奇,可以在 Android 文档中查找该类:developer . Android . com/reference/Android/view/motion event . html
。
同样,这个方法和其他几个方法是从您的GameView
类中调用的,因此您可以快速地将相关的更改传递给您的Sprite
和GameLogic
进程。您可能会用到的其他输入功能包括onKeyDown()
、onClick()
和onKeyUp()
。如果您有兴趣使用它们,它们的实现方式几乎与处理一般触摸事件的方式相同。这些方法都返回一个布尔值。这意味着当您完成处理时,您返回true
,以释放程序来收集下一个输入。
你可以想象,对于平板电脑来说,一次屏幕滑动对玩家来说可能相对简单;但是你的应用可能会将其误解为几个小动作,或者完全错过这个动作。随着游戏的进行,你要练习更复杂的手势,并确保即使是新玩家也能掌握游戏的控制。
让我们仔细看看如何处理触摸屏上的用户输入。在 Eclipse 中创建一个新项目来演示用户输入:
Create a new
project
Android project
. Type the name of the new project as input test , and make sure that the title of the activity is main activity . It is common practice to put items in the same root package.
Because you will largely reuse the earlier classes, open the folder tree of GraphicsTest and copy all four
.java
files to thesrc
folder of the InputTest project. Also close the window of each class in the editing panel to make sure that you are editing the file of the new project. Open all the classes of the InputTest project, and it's almost ready to start.If you still want the same image, please move the image file from
res
![]()
drawable_mdpi
to the folder of the new project. If you like, you can create a new image for this project, as long as you reference it correctly in the code.你已经知道输入收集发生在
GameView
类中,所以把清单 3-1 中的函数添加到游戏视图中。将此部分放在surfacedestroyed()
方法的正下方。
注意许多讨论的主题都需要在代码顶部添加额外的
import
语句。每个列表的标题包括必要的import
语句。请确保将这些内容放在文件的顶部,否则将无法运行该应用清单 3-1。添加输入采集到
GameView.java
(import android.view.MotionEvent
)
@Override public boolean onTouchEvent(MotionEvent event){ return true; }
这是
onTouchEvent
的完整实现;但是,它目前不执行任何有意义的操作。将清单 3-2 中所示的代码片段添加到return
语句之前的函数代码中。清单 3-2。基于触摸事件操纵精灵
sprite.setX((int)event.getX()); sprite.setY((int)event.getY());
This code uses your
setX
andsetY
functions to move the sprite to the place where the touch event stopped or the last position of the finger.Event.getX
is a method to retrieve the position of the last motion event. It returns a floating-point number, so you can convert it into an integer to satisfy your method.因为你的精灵一直在屏幕上快速移动,所以你移除了精灵的移动。清单 3-3 停止
GameView
类中被改变的update()
方法。清单 3-3。停止精灵运动
`public void update(int adj_mov) {
if (sprite.getX() > = getWidth()){
sprite.setMoveX(0);
}
if (sprite.getX() <= 0){
sprite.setMoveX(0);
}
sprite.update(adj_mov);}`
You must also stop the movement in the
SpriteObject
class by setting the value of the sprite-movement variable to zero.Your backstage work is finished. Save all files and run the application. If you click on the screen, the wizard will appear in the position where you moved last time.
也可以试着在数位板上拖动光标。你会看到精灵疯狂地试图跟上,即使如果你使用模拟器,它会明显滞后。真实设备的运动相当流畅。图 3-3 显示了你的工作结果。
图 3-3。通过在屏幕上拖动,将star.png
精灵移动到不同的位置。
当您在屏幕上移动光标时,您可能会注意到无论您从哪里开始移动,精灵都会移动。大多数允许你移动精灵的游戏都有几个精灵,所以你必须通过触摸来选择你想要移动的精灵。这就把你带到了触摸屏输入的主要话题之一:手势。尽管拖动事件本质上很简单,但它被认为是一种手势,因为它涉及持续的交互。其他手势包括滚动、挤压、旋转等。接下来,您将学习如何在游戏环境中创建手势并对其做出响应。
回应手势
要做到这一切,你需要对 Android SDK 中的Gesture
类非常友好。在您在代码中使用它之前,让我们在 Android 开发团队创建的示例程序中体验一下手势。你可以在模拟器中访问这个应用:它被称为手势生成器。但是,在此之前,您必须对模拟器进行一些更改:
GestureBuilder writes files into SD card of tablet. If your simulator doesn't have this, you can easily add it. Select the window
Android SDK and AVD manager.
点击平板设备,并点击屏幕左侧的编辑按钮。弹出一个对话框(如图 3-4 所示),输入设备的内存大小。我一般用 1000 兆。
图 3-4。为手势生成器应用设置仿真器
Click edit AVD.
你被带回 Android SDK 和 AVD 管理器。单击开始按钮;或者,如果开始按钮不可用,首先选择仿真器名称,如图图 3-5 所示。您从这里启动模拟器,因为您希望能够选择应用,而不是让应用默认启动。
图 3-5。从安卓 SDK 和主动脉瓣疾病管理器启动模拟器
When the simulator is up and running, go to the icon labeled Apps. Then, click the Gesture Generator program. Play with this application for a while and see how it works. Make a new gesture and give it a name. Do a series of slides to create your gestures. This application gives you a feeling of what a gesture looks like.
如果您创建了一个真正伟大的手势,并希望在游戏中使用,您可以从 SD 卡中获取该手势,并在您的游戏代码中引用它。这是一个高级话题,你现在只想体验手势;要完成这个过程,请遵循 Android 文档中的说明:[
developer.android.com/resources/articles/gestures.html](http://developer.android.com/resources/articles/gestures.html)
。
图 3-6 显示了我创建的一个星形手势。尽管你可以用多种方式画一颗星星,但手势是特定的,因为笔画的顺序非常关键。平板电脑正在寻找正确的序列。
图 3-6。做出独特的明星姿态
Android 开发小组有自己的名为 GestureDemo 的程序,可以让你做出手势,让应用识别。这一章没有详细介绍这个应用是如何工作的,因为它与大多数游戏的相关性有限。不过,还是值得简单看一下,因为你可以看到手势是如何被识别的。通过改变手势的方式,你会发现平板电脑识别手势的准确度有多高:
7.从[
code.google.com/p/apps-for-android/downloads/detail?name=GesturesDemos.zip&can=2&q=](http://code.google.com/p/apps-for-android/downloads/detail?name=GesturesDemos.zip&can=2&q=)
下载项目。解压缩文件夹,并注意记住所有内容都是从哪里提取的。
8.打开 Eclipse,选择 File Import
General
将已有的项目导入工作区。
9.找到您下载的文件夹GestureDemo
,并填写表格。当您单击 Finish 时,您的工作区中就会有一个新项目。要了解这个项目的更多信息,可以访问网页developer . Android . com/resources/articles/gestures . html
。
10.运行新项目,并开始表演手势。如果你做一个闪电手势,它应该在底部显示雷霆咒语。
如果你在这个项目上玩的时间足够长,你可能会注意到它在识别一些手势方面不是很准确。这是意料之中的,也是创建自定义手势不常见的原因之一。人们已经熟悉的著名手势,如捏和拖,很容易被 Android 计算出来,导致更少的混乱。尽管如此,当你可以像在现实生活中一样用手做动作时,一些游戏还是非常令人兴奋的。你可以在 Android 文档中查找手势库来更好地理解手势,因为这是一个太复杂的主题,本章无法完全涵盖。
提示如果你创造了自己的手势,让它们简单而夸张。此外,将你的游戏限制在一两个彼此非常非常不同的新手势,以避免错误。
使用输入队列
早些时候,这一章讨论了大量的用户输入是如何冻结游戏并导致其停止的。您可以使用非常方便的InputObject
类来解决这种可能性。基本上,当输入事件发生时,您试图限制主线程上的压力。回想一下图 3-2 展示了你如何在响应输入事件之前保持它们。这正是你在这里做的。您不必等待锁定整个线程,而是在后台完成大部分工作。
这个系统最初是由罗伯特·格林介绍给我的,它非常有效和简单,从那以后我一直在使用它。(你可以在[www.rbgrn.net/](http://www.rbgrn.net/)
阅读罗伯特关于 Android 开发和其他沉思的博客。)An ArrayBlockingQueue
负责输入处理的繁重工作。这基本上是一种存储对象并在以后遍历它们的方法。要使用一个ArrayBlockingQueue
,您需要在使用它的每个 Java 文件的顶部导入它,如下所示:
import java.util.concurrent.ArrayBlockingQueue;
为了使用这种存储输入事件并在以后处理它们的方便方法,在 InputTest 项目中创建一个名为InputObject
的新类。从现在开始,你引用InputObject
s 而不是MotionEvent
s 来获取关于发生了什么类型的事件的信息。除了加快处理速度之外,创建这样一个类还有多种原因。当您开始响应输入时,您会注意到处理有时很复杂的事件变得更加容易。
让我们试试这个技巧:
- 创建
InputObject
类,并用清单 3-4 中的代码填充它。清单 3-4。??
InputObject.java
`import java.util.concurrent.ArrayBlockingQueue;
import android.view.KeyEvent;
import android.view.MotionEvent;public class InputObject {
public static final byte EVENT_TYPE_KEY = 1;
public static final byte EVENT_TYPE_TOUCH = 2;
public static final int ACTION_KEY_DOWN = 1;
public static final int ACTION_KEY_UP = 2;
public static final int ACTION_TOUCH_DOWN = 3;
public static final int ACTION_TOUCH_MOVE = 4;
public static final int ACTION_TOUCH_UP = 5;public ArrayBlockingQueue
pool;
public byte eventType;
public long time;
public int action;
public int keyCode;
public int x;
public int y;public InputObject(ArrayBlockingQueue
pool) {
this.pool = pool;
}public void useEvent(KeyEvent event) {
eventType = EVENT_TYPE_KEY;
int a = event.getAction();
switch (a) {
case KeyEvent.ACTION_DOWN:
action = ACTION_KEY_DOWN;
break;
case KeyEvent.ACTION_UP:
action = ACTION_KEY_UP;
break;
default:
action = 0;
}
time = event.getEventTime();
keyCode = event.getKeyCode();
}public void useEvent(MotionEvent event) {
eventType = EVENT_TYPE_TOUCH;
int a = event.getAction();
switch (a) {
case MotionEvent.ACTION_DOWN:
action = ACTION_TOUCH_DOWN;
break;
case MotionEvent.ACTION_MOVE:
action = ACTION_TOUCH_MOVE;
break;
case MotionEvent.ACTION_UP:
action = ACTION_TOUCH_UP;
break;
default:
action = 0;
}
time = event.getEventTime();
x = (int) event.getX();
y = (int) event.getY();
}public void useEventHistory(MotionEvent event, int historyItem) {
eventType = EVENT_TYPE_TOUCH;action = ACTION_TOUCH_MOVE;
time = event.getHistoricalEventTime(historyItem);
x = (int) event.getHistoricalX(historyItem);
y = (int) event.getHistoricalY(historyItem);
}public void returnToPool() {
pool.add(this);
}
}* 我们来解剖一下这个类。像
KeyEvent或
MotionEvent这样的输入事件由函数
useEvent()处理,以创建具有动作类型以及相关数据的对象,如触摸屏事件的 x 和 y 坐标。需要理解的关键部分是
ArrayBlockingQueue是如何工作的。* 在集成到你的其他类的上下文中更有意义;但是现在,让
InputObject按照它们加入事件池的相反顺序存储就足够了。这意味着首先处理第一个发生的事件。显然,用户输入必须按照发生的顺序来处理。* 清单中特别值得注意的是
useEventHistory()。像
getHistoricalEventTime()和
getHistoricalX()这样的方法被用来获取运动事件的原始数据。通常,屏幕上的滑动有几个相关联的坐标和时间,因此这是获取事件的原始位置与当前光标位置的方法。* 在继续之前,还要注意每个事件都有一个名为
action的变量,它存储发生的事件的类型。当您想要对输入做出响应时,您可以查找它是什么类型的事件并做出相应的响应。这使您免于进行大量的猜测。* To implement your new
InputObjectclass, you need to make some major changes to
GameView.java. Inside the
GameView` class, create the following variable shown in Listing 3-5.
***清单 3-5。**添加一个`inputObjectPool`对象* `private ArrayBlockingQueue<InputObject> inputObjectPool;`* Under the `GameView(Context context)`, add the line in Listing 3-6. ***清单 3-6。**创建`InputObject`池* `createInputObjectPool();`* You build this function in the `GameView` class with the code shown in Listing 3-7, which you place at the end of `GameView`. ***清单 3-7。**声明一个创建对象池的函数* `private void createInputObjectPool() {` ` inputObjectPool = new ArrayBlockingQueue<InputObject>(20); for (int i = 0; i < 20; i++) { inputObjectPool.add(new InputObject(inputObjectPool)); } }`* 在这里,您初始化存储输入对象的`inputObjectPool`。您将它设为 20 个单位长,因为您可能永远不会超过这个限制(输入事件只能发生得这么快)。`for`循环用所有元素填充池。* To start sending information to the input object pool, you need to modify the `onTouchEvent` that you worked with before. Type the code from Listing 3-8 into the `onTouchEvent()` method. ***清单 3-8。**??`onTouchEvent(MotionEvent event)`* `@Override public boolean onTouchEvent(MotionEvent event) { try { int hist = event.getHistorySize(); if (hist > 0) { for (int i = 0; i < hist; i++) { InputObject input = inputObjectPool.take(); input.useEventHistory(event, i); mGameLogic.feedInput(input); } } InputObject input = inputObjectPool.take(); input.useEvent(event); mGameLogic.feedInput(input); } catch (InterruptedException e) { } try { Thread.sleep(16); } catch (InterruptedException e) { } return true; }`* 请注意,根据触摸事件的位置,您会失去移动精灵的功能。你把这一块加回下一节。在`onTouchEvent`中,您使用一个`try`块来尝试将每个事件解析成一个`InputObject`,然后存储它以供以后处理。对`mGameLogic.feedInput(input)`的调用是当线程有机会时进一步访问事件的地方。最后,让主线程休眠 16 毫秒,以确保不会一次收集太多的输入。* 将`useEvent`和`useEventHistory`的调用引用到它们在`InputObject`类中的声明。您应该能够看到如何创建已经发生的输入事件的列表。* You need to add two new methods to the `GameView` class in `GameView.java`; see Listing 3-9. They’re called by the `GameLogic` to work with the input objects. You disregard `KeyEvent` for now because tablets don’t often worry about keyboard input. `MotionEvent`, however, is handled just as you did earlier by instructing the sprite to move to wherever the user last touched. ***清单 3-9。**处理动作和按键事件* `public void processMotionEvent(InputObject input){ sprite.setX(input.x); sprite.setY(input.y); } public void processKeyEvent(InputObject input){ }`* 要设置 sprite 的 x 和 y 位置,可以访问输入对象的最后一个坐标。当您抽象出`MotionEvent`时,这个简单的过程可以让您更好地可视化您的操作。* To finish your new input pipeline method, you add some code to the `GameLogic` class. Listing 3-10 declares two objects that you need to create. Place this code right beneath the variables that store the game state, such as `PAUSE`, `READY`, and `RUNNING`. ***清单 3-10。**为输入法声明新对象* `private ArrayBlockingQueue<InputObject> inputQueue = new ArrayBlockingQueue<InputObject>(20); private Object inputQueueMutex = new Object();`* You need to make only one change to the `run()` method, but it’s important that you place it in the correct location. Listing 3-11 shows the entire `run()` function with the addition highlighted. ***清单 3-11。**告诉主线程处理输入* `@Override public void run() { long time_orig = System.currentTimeMillis(); long time_interim; Canvas canvas; while (game_state == RUNNING) { canvas = null; try { canvas = this.surfaceHolder.lockCanvas(); synchronized (surfaceHolder) { try { Thread.sleep(30);` ` } catch (InterruptedException e1) { } time_interim = System.currentTimeMillis(); int adj_mov = (int)(time_interim - time_orig); mGameView.update(adj_mov); processInput(); //this is the new way to process input. time_orig = time_interim; this.mGameView.onDraw(canvas); } } finally { if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } } }`* You must now define two functions because you’ve already created methods that call them. `ProcessInput()` is where the thread issues instructions about dealing with the input. `Feedinput()` handles the operation of the `ArrayBlockingQueue`. Place these methods, whose code appears in Listing 3-12, right below the `run()` function. ***清单 3-12。**进料和加工输入* `public void feedInput(InputObject input) { synchronized(inputQueueMutex) { try { inputQueue.put(input); } catch (InterruptedException e) { } } } private void processInput() { synchronized(inputQueueMutex) { ArrayBlockingQueue<InputObject> inputQueue = this.inputQueue; while (!inputQueue.isEmpty()) { try { InputObject input = inputQueue.take(); if (input.eventType == InputObject.EVENT_TYPE_KEY) { mGameView.processKeyEvent(input); } else if (input.eventType == InputObject.EVENT_TYPE_TOUCH) { mGameView.processMotionEvent(input); } input.returnToPool(); } catch (InterruptedException e) { } } } }`
FeedInput()
非常直白。它用synchronized()
抓取线程,并将输入合并到inputQueue
中。一旦输入对象被满意地分类,这个方法就会被GameView
类调用。
ProcessInput()
在处理inputQueue
的方式上有些复杂。它还使用synchronized()
来保持线程,同时它遍历inputQueue
中的对象,并让processKeyEvent()
或processMotionEvent()
来处理它们。这两个函数都是在GameView.java
中定义的,因为您希望能够向 sprite 对象发出指令。
在代码的巨大变化之后,你的程序现在完全按照本章开始时的方式运行。然而,如果你的游戏线程在处理输入时过于费力,用户可能会认为这是一个没有响应的程序,那么这个过程将会为你省去很多麻烦。
继续启动输入测试项目。如果所有代码都编译正确,那么您应该能够在平板电脑屏幕上拖动精灵。因为几乎没有任何物理或计算在后台进行,所以应用的行为应该没有明显的差异。当你添加人工智能程序和几十个带背景的精灵时,你将充分利用这种巧妙的方式来处理输入。
随着触摸屏事件的这些基本策略的消失,现在您可以检查使 Android 平板电脑有趣的更令人兴奋的传感器。
响应传感器数据
Android 提供了一种简单的方法来获取触摸事件,但传感器是一个更复杂的问题。这并不是说获取数据是棘手或困难的,但是以有意义的方式处理输入可能是一个真正的挑战。这里,我们将重点放在加速度计数据上,因为它是最常用的传感器,其他传感器(如陀螺仪)与之类似。
平板电脑传感器提供的数据非常精确,通常是 Java 长浮点数据类型。这是一件喜忧参半的事情,因为长浮点数据错综复杂,难以计算。让事情变得更加困难的是,平板电脑可以以多种方向握持。在纵向模式下握住平板电脑会完全改变旋转轴。为了暂时解决这个问题,您可以假设平板电脑处于横向模式。稍后,您将学习一种方法来检测平板电脑的方向,并指导用户为您的特定游戏选择正确的位置。
让我们在项目中添加一些代码,看看这些传感器数据是关于什么的:
你需要再导入一个安卓库。为此,将清单 3-13 中的代码添加到
MainActivity.java
文件中。清单 3-13。获取传感器数据
import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager;
You may notice that these imports are about hardware-specific information. The device running the game may lack the correct sensor.
在你的
MainActivity
类中实现SensorEventListener
类。为此,直接在extends Activity
行后添加implements SensorEventListener
。当出现错误消息时,双击它创建两个事件,如列表 3-14 所示。清单 3-14。自动生成传感器方法
`@Override
public void onAccuracyChanged(Sensor arg0, int arg1) {
// TODO Auto-generated method stub}
@Override
public void onSensorChanged(SensorEvent arg0) {
// TODO Auto-generated method stub} `
This is quite self-evident: create a
SensorEventListener
and two methods, which register when the sensor changes its accuracy or its value. You are most concerned aboutonSensorChanged()
because you are looking for data. In addition, there are many other functions that you can use when you want to get very specific information from the sensor.将清单 3-15 中的行放在
MainActivity
中的onCreate()
方法之上。清单 3-15。创建传感器对象
private SensorManager mSensorManager; private Sensor mAccelerometer;
用
onCreate()
方法初始化这些传感器对象,如清单 3-16 所示。清单 3-16。初始化传感器对象
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
要处理传感器,您需要添加两个基本方法,这两个方法在每个活动中都已经可用:
onPause()
和onResume()
。您在这里需要它们,因为当设备已经处于某种睡眠模式时,您不想继续搜索传感器输入。清单 3-17 中的代码处理了这个问题。清单 3-17。
onPause()
和onResume()
`protected void onResume() {
super.onResume();
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
}protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}`你可以查看修改后的
onSensorChanged()
来确定俯仰、滚转和方位角的值。作为参考,方位角为绕 z 轴旋转,俯仰为绕 x 轴旋转,滚动为绕 y 轴旋转。为了表达加速度计的值,您引入了一种针对 Eclipse 和 Android 的调试技术。在顶部,导入Android.util.Log
。然后将onSensorChanged()
更改为清单 3-18 中所示的代码。??清单 3-18。??
onSensorChanged (add import android.util.Log)
`@Override
public void onSensorChanged(SensorEvent event) {float R[] = new float[9];
float orientation[] = new float[3];
SensorManager.getOrientation(R, orientation);Log.d("azimuth",Float.toString(orientation[0]));
Log.d("pitch",Float.toString(orientation[1]));
Log.d("role",Float.toString(orientation[2]));}`
Basically, you create two arrays to store numerical values. Then call the sensor manager to get the direction of the device. Finally, get the direction array and print out the value. The
Log.d
function may seem novel to you, but it is just a way to send data to the debugger. Before running the program, you can set the view for reading these values by selecting the windowDisplay View
Other
Android
Logcat.
现在,您看到的不是控制台的输出,而是模拟器启动时闪烁的大量数据点。当应用开始运行时,您会看到加速度计的值。图 3-7 显示了当你在计算机上使用模拟器时会发生什么,它在运动中没有任何变化。
图 3-7。记录设备的方位、俯仰和滚动
当我想处理传感器数据时,我总是在我的实际平板设备上进行测试,因为在不同位置持有设备非常简单。如果您还没有看过,附录 A 提供了有关设置平板电脑进行测试的信息。如果你更喜欢冒险或者没有设备,Android 有一个传感器模拟器可以在你编写代码时帮助你。
这里可以看到 Google code 项目:[
code.google.com/p/openintents/wiki/SensorSimulator](http://code.google.com/p/openintents/wiki/SensorSimulator)
。建立整个系统实际上是一个相当简单的提议,但是我不会在这里深入讨论。在某些情况下,只有真正的设备才能提供即时响应,并向您展示其处理能力。
与真实设备相比,传感器模拟器确实有一些优势。通过将准确的移动值与设备的响应方式相关联,您可以更好地感受到您的程序运行得有多好。对于大多数开发人员来说,仅仅通过手持平板电脑很难测量出完美的 37 度旋转。
使用传感器数据
要将传感器数据合并到游戏及其更新的逻辑中,您需要将数据传递到游戏的View
类中。首先将它添加到MainActivity
类中:
GameView mGameView;
您还必须将清单 3-19 中的代码添加到onCreate()
方法中:
清单 3-19 。GameView
实例
mGameView = new GameView(this); setContentView(mGameView);
您现在有了一个GameView
实例,可以从中调用各种方法。接下来,在GameView
中,您需要添加一个新的函数来传递您的方向数据。清单 3-20 显示了在onSensorChanged()
中添加的调用。
清单 3-20。发送传感器数据
`@Override
public void onSensorChanged(SensorEvent event) {
if(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){
float orientation[] = new float[3];
for(int i = 0; i < 3; i++){
orientation[i] = event.values[i];
}
mGameView.processOrientationEvent(orientation);
Log.d("azimuth",Float.toString(event.values[0]));
Log.d("pitch",Float.toString(event.values[1]));
Log.d("role",Float.toString(event.values[2]));
}
}`
代码的新部分是对 gameview 的processOrientationEvent()
调用。请注意,您正在向它发送方向数据数组。清单 3-21 包含了GameView.java
中processOrientationEvent()
的代码。
清单 3-21。处理传感器数据
`public void processOrientationEvent(float orientation[]){
float roll = orientation[2];
if (roll < -40) {
sprite.setMoveX(2);
} else if (roll > 40) {
sprite.setMoveX(-2);
}
}`
这里你只看到了设备的滚动。如果它足够低,那么你希望精灵移动到右边。如果滚动是高的,那么你有精灵移动到左边。为了让这更令人兴奋,注释掉update()
函数的行。清单 3-22 显示了该零件现在的样子。
清单 3-22。让精灵自由移动
`public void update(int adj_mov) {
if (sprite.getX() >= getWidth()){
//sprite.setMoveX(0);
}
if (sprite.getX() <= 0){
//sprite.setMoveX(0);
}
sprite.update(adj_mov);
}`
在你的安卓平板设备上测试一下,你会很难将精灵保持在屏幕内。如果您愿意,可以在平板电脑保持相对平直时将移动量设置为零。这里您使用了一个非常简单的传感器数据实现,但是在最终的游戏项目中,您添加了一个摇动事件,让用户摇动平板电脑来重新开始关卡。现在,您可以使用平板电脑数据的滚动、方位和俯仰。
您必须了解传感器数据的几个方面才能让它们发挥作用。传统上,加速度计数据是基于重力来处理的。因此,平板静止时,加速度仍在 9.8 m/s².左右许多 Android 函数会为你处理这个问题,但是如果你遇到不处理的函数,你需要减去这个重力影响。查阅 Android 文档可以对此有所帮助。有趣的是,Android 内置了包括地球在内的所有行星的重力常数。这样,你可以根据你的设备当前所在的星球来调整加速度计的读数。
最后,坐标轴是独一无二的,因为它们既考虑了磁北,也考虑了传统的维度。这意味着 x 轴大致从东向西,而 y 轴指向北,z 轴指向地球的中心。图 3-8 中的图片来自 Android 自己的文档。
图 3-8。安卓平板电脑的坐标轴
因为加速度计和陀螺仪的读数本来就是三维的,所以理解矩阵对于它们的一些数据非常重要。要解决这个问题,您应该只要求像getOrientation()
这样的函数,其中您知道值是俯仰、方位角和滚动的数组。您可以通过查看 Android 文档来试验更多的传感器数据:[
developer.android.com/reference/android/hardware/Sensor.html](http://developer.android.com/reference/android/hardware/Sensor.html)
。在本文的顶部,您可以查看 Android 支持的所有传感器类型的列表。但是,在实现它们之前,请检查您的目标设备是否包含这些。
总结
将传感器和触摸屏输入的信息与您完成的图形工作相结合,您可以制作自己的游戏。当然,在通过碰撞使精灵正确交互和创建新的精灵实例方面还有很多工作要做。你还必须把你的游戏推向市场。
在你开始任何高级精灵和开发任务之前,你需要理解游戏的音乐和声音的基础。当你想到平板电脑和手机应用时,音乐可能很少被注意到,但这并不意味着它不重要。一个没有声音的游戏很无聊,会让人们试图在后台运行他们的音乐播放器,这会减慢你的游戏速度。Android 已经创建了几个奇妙的库来创建有趣的声音效果。你可以利用它们来增加游戏的刺激。
四、添加音效、音乐和视频
有了对如何使用精灵和处理用户交互的基本理解,你就离一个完整的、可玩的游戏更近了。现在,您可以添加一些沉浸式游戏体验所必需的元素:音效、音乐和视频。
令人惊讶的是,很多手机游戏都忽略了声音和音乐。也许开发人员很快添加了一些音效或者拼凑了一段简单的旋律,但仅此而已。游戏的音频部分真的可以让你的作品脱颖而出。没有理由在这方面表现不佳,因为这是最容易实现的 Android 游戏功能之一。真正的限制是你可以创作或购买的音乐。一些网站已经注意到了这一点,为商业和非商业项目提供了数千个免费的声音文件。
手机游戏中的视频也未能兑现承诺。这很大程度上可能是由于手机上的小硬盘空间或数据计划的成本。然而,平板电脑拥有千兆字节的存储空间,以及从网站和服务器快速加载媒体的能力。您可以使用快速电影来解释游戏或娱乐玩家,同时加载游戏资源。为游戏玩家提供可靠的乐谱和多媒体展示,无疑会让你的游戏在竞争中脱颖而出。
在本章中,您将创建一个新的 Eclipse 项目,将声音和媒体整合到您的游戏中。然而,首先你要解决处理声音的框架。
注过去,游戏开发商不愿在音效上花费时间和金钱,因为他们认为手机游戏玩家希望能够安静地玩游戏。随着平板电脑的出现,游戏已经转变为一个更加多人和社交的场合,多人可以在屏幕上同时玩游戏;因此,噪音不再是一个问题。然而,游戏必须让玩家有可能关掉声音。
为声音做好准备
在你探索 Android 游戏中的声音之前,你需要找到一些可以使用的声音。Android 支持多种声音格式,最流行的有.mp3
、.wav
和.mid
。就个人而言,我更喜欢用 MP3 文件来制作小音效,比如爆炸,用 MIDI 文件来制作乐谱。这是一种常见的做法,在使用流行的文件选择的同时,将文件大小保持在最小。如果你有其他文件格式的声音,你可以在这里访问 Android 的可接受媒体格式列表:[
developer.android.com/guide/appendix/media-formats.html](http://developer.android.com/guide/appendix/media-formats.html)
。
一些热爱音频和音乐的人特别感兴趣的是 Android 提供的免费无损音频编解码器(FLAC)支持。FLAC 是一种很像 MP3 的格式,但它保持了声音的原始质量。如果您有自己的录音设备或高质量的文件收藏,这是一个很好的格式。你可以在[
flac.sourceforge.net](http://flac.sourceforge.net)
了解更多。只有 Android 3.1 及更高版本支持这种格式。
声音和媒体文件不适合您当前拥有的图像资源文件。要存储它们,您可以将它们添加到您的resources
文件夹中一个名为raw
的新文件夹中。该文件夹不是默认创建的,所以您可以自己创建。 Raw 是您为任何非版面或图像文件的媒体或杂项文件指定的名称。
现在,让我们为你的 Android 游戏定位一些音效。
寻找并添加音效
让我们测试一些声音;你可以在网上找到很多选择。可以说最好的资源是[www.freesounds.org](http://www.freesounds.org)
。你必须先在这个网站上开一个账户,然后你就可以自由地浏览它庞大的收藏并下载它的声音文件。这些声音是在知识共享取样加许可证下发布的。基本上,你可以在你的项目中自由使用它们,只要你引用了它们的许可,并对它们的创造者给予了信任。
你不会在这个网站上找到很多完整的歌曲,但有任何可能的游戏的音效。对于本章的例子,我选择了一个 spacey 机器人噪声:[www.freesound.org/samplesViewSingle.php?id=14259](http://www.freesound.org/samplesViewSingle.php?id=14259)
。是用户 Harri 上传的。
如果你检查网站上的文件格式,大多数都是.wav
。您可以像使用.mp3
一样使用它们。
要开始使用声音效果,请按照下列步骤操作:
- Download a sound file that you find interesting, and temporarily store it on your desktop or a place that you can easily access.
- Open Eclipse IDE and follow the steps to create a new project. It was named sounds test .
- Open the InputTest project you built in chapter 2 of , and copy all its files to the corresponding folders in SoundsTest, including
star.png
images and allInputTest.java
codes. Make sure thatGameView.java
andSpriteObject.java
are in the file you copied.- Close the old source code in the edit pane and open the file from the new project.
- To incorporate new sounds into the SoundsTest project construction, you need to create a new file in its
res
folder. Remember, layout data and images are stored in this folder; But now you are dealing with different file formats for storing media files, so you use a new folder for it.- Right-click the
res
folder, select New Folder, and create a new folder. Name the folderraw
.- Find your audio file, copy and paste it into the
raw
folder. As mentioned above, theraw
folder is used to store resources such as sound and video files.
现在你可以为游戏添加一些声音了。这个过程在许多方面类似于显示图形,但与图像不同,声音有一个持续时间。正因为如此,你用一种简单的方式,播放一次声音,让它继续。
播放声音效果
要访问 Android 播放声音的功能,您需要导入 Android MediaPlayer 库。它的名字准确地解释了它的作用——它被用来在你的游戏中播放声音和视频。这个类包含了很少你需要担心的方法。你在编写代码时会看到它们。
将清单 4-1 中的行添加到GameView.java
文件的顶部。
清单 4-1。获得媒体播放能力
Import Android.media.MediaPlayer
清单 4-2 展示了创建一个媒体播放器对象并发出声音的代码。游戏过程中播放音效只需要两行。不是在游戏启动时播放声音,而是在检测到运动事件时播放。因此,您将加粗的代码添加到函数processMotionEvent
中;因为您仍然拥有控制图形的所有代码,所以您在屏幕上重新定位 sprite 的代码行下面添加了新代码。这样做有一些问题,你一测试完就知道了:也就是说,只要有动作,你的声音就会播放,即使声音已经在播放了。
清单 4-2。运动事件发生时播放声音
public void processMotionEvent(InputObject input){ sprite.setX(input.x); sprite.setY(input.y); **MediaPlayer robotnoise = MediaPlayer.create(getContext(), R.raw.robot_noise);** ** robotnoise.start();** }
MediaPlayer
类与您创建的SpriteObject
类非常相似。你初始化对象,然后给它分配声音——或者,在精灵的情况下,分配图像。然后,您可以自由调用它的各种函数,在本例中,这意味着启动噪声,同时可以移动 sprite。
要看到这一点,启动项目并等待游戏加载。当您拖动屏幕时,您会听到播放的声音。如果您使用的是 Android 模拟器,注意不要连续拖动多次,否则项目会因为使用太多资源反复播放声音而崩溃。
现在让我们来看看如何管理几种声音,每种声音都与特定的活动相关联。几乎没有游戏只用一种声音;而且每当涉及到多种声音的时候,你都要处理同时播放多种声音的可能性。下一节将介绍一个使这个命题变得简单的课程。
管理多重音效
当您考虑与游戏活动(如获得健康或射击物体)相关的声音效果和噪音时,您看到的是可以同时出现或至少非常接近的声音。对于处理大量可以快速播放的声音来说,MediaPlayer
类并没有很好的配置。为了处理这个难题,您使用了一个稍微复杂一点的名为SoundPool
的类。把这个类想象成一个对象,当游戏运行时,它在后台监控声音的加载和播放。与调用MediaPlayer
对象相比,它有几个好处。
清单 4-3 包含了更新GameView
类以使用SoundPool
类所需的所有代码。它添加了大量的函数和过程,因此您需要非常小心地正确编写整个文件。当您运行这个应用时,它的功能就像《??》第三章中的 InputTest 应用一样,但是增加了三种每当运动事件发生时播放的声音效果。你可以用一个简单的计数器来循环显示所有的噪音。
清单中的新代码以粗体突出显示。特别要注意你实现的新包以及SoundPool
是如何工作的。所有这些都在GameView.java
中完成,不需要操作任何其他类。清单 4-3 展示了GameView.java
的全貌,所以你可以确信一切都很好。
清单 4-3。??GameView.java
`package com.gameproject.soundtest;
import java.util.concurrent.ArrayBlockingQueue;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.SoundPool;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class GameView extends SurfaceView implements
SurfaceHolder.Callback {
private SpriteObject sprite;
private GameLogic mGameLogic;
private ArrayBlockingQueue
** private int sound_id;**
** private Context context;**
** private SoundPool soundPool;**
** private int ID_robot_noise;**
** private int ID_alien_noise;**
** private int ID_human_noise;**
public GameView(Context con) {
super(con);
context = con;
getHolder().addCallback(this);
sprite = new SpriteObject(BitmapFactory.decodeResource(getResources(),
R.drawable.star), 50, 50);
mGameLogic = new GameLogic(getHolder(), this);
createInputObjectPool();
** soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);**
** ID_robot_noise = soundPool.load(context, R.raw.robot_noise, 1);**
** ID_alien_noise = soundPool.load(context, R.raw.alien_noise, 1);**
** ID_human_noise = soundPool.load(context, R.raw.human_noise, 1);**
** sound_id = ID_robot_noise;**
setFocusable(true);
}
private void createInputObjectPool() {
inputObjectPool = new ArrayBlockingQueue
for (int i = 0; i < 20; i++) {
inputObjectPool.add(new InputObject(inputObjectPool));
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
int hist = event.getHistorySize();
if (hist > 0) {
for (int i = 0; i < hist; i++) {
InputObject input = inputObjectPool.take();
input.useEventHistory(event, i);
mGameLogic.feedInput(input);
}
}
InputObject input = inputObjectPool.take();
input.useEvent(event);
mGameLogic.feedInput(input);
} catch (InterruptedException e) {
}
try {
Thread.sleep(16);
} catch (InterruptedException e) {
}
return true;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mGameLogic.setGameState(mGameLogic.RUNNING);
mGameLogic.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
soundPool.release();
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
sprite.draw(canvas);
}
public void update(int adj_mov) {
if (sprite.getX() >= getWidth()){
//sprite.setMoveX(0);
}
if (sprite.getX() <= 0){
//sprite.setMoveX(0);
}
sprite.update(adj_mov);
}
public void processMotionEvent(InputObject input){
** soundPool.play(sound_id,1.0f,1.0f,10,0,1f);**
** sound_id++;**
** if (sound_id == 3){**
** sound_id = 0;**
** }**
sprite.setX(input.x);
sprite.setY(input.y);
}
public void processKeyEvent(InputObject input){
}
public void processOrientationEvent(float orientation[]){
float roll = orientation[2];
if (roll < -40) {
sprite.setMoveX(2);
} else if (roll > 40) {
sprite.setMoveX(-2);
}
}
}`
它是这样工作的。您可以通过声明一系列变量来开始这个实现:
Sound_id
: Counter, which determines which sound needs to be played.Context
: The method of transferring the instance of the main activity to the sound loading function. You have dealt with this variable before.Soundpool
: The only object that controls all kinds of sounds you play.ID_robot_noise
: Integer value of robot sound file.ID_alien_noise
: Integer value of alien sound file.ID_human_noise
: Integer value of voice file.
然后在GameView
构造函数方法中初始化soundPool
对象。SoundPool
有三个参数:同步声音流的整数数量,音频流的整数类型(使用AudioManager
来提供这个值),以及一个当前没有使用的质量的整数。
流类型值得注意,因为您选择了最常见的选项。AudioManager
有其他替代,如STREAM_ALARM
和STREAM_RING
;他们处理与其活动相关的音频文件。一个游戏可能永远不需要使用除了STREAM_MUSIC
以外的任何东西。
接下来的三行加载三个不同的音频样本。当您创建这个项目时,您需要在您的res
raw
文件夹中有三个声音样本,它们对应于您传递给load()
函数的资源 id。load()
方法的参数非常简单:第一个是应用上下文,第二个是资源 id。最后一个在 Android 当前版本中没有使用。
load()
函数返回声音的id
。然后,这将用于调用您想要播放的精确音频文件。最后,你将sound_ID
分配给第一个音的id
,这样你就从列表的开头开始了。
在processMotionEvent()
内,你让soundPool
播放它的一个音频样本。参数概述如下:
Integer sounded
: Specify which sound to play.Float Left Volume
: Use the maximum volume of 1.0.Float Right Volume
: Use the maximum volume of 1.0.Integer Priority
: Use 10 at will. The higher the number, the higher the priority.Integer Loop
: Use 0 to disable the loop. -1 indicates an infinite loop, and a positive integer indicates a loop in which the value is added by 1 (for example, 5 loops for 6 times).Float Playback rate
: Normal play is 1.0. You can use 0.5 and 2.0 for half speed or double speed respectively.
下一段代码增加了sound_ID
计数器,并在它遍历完整组声音后重置它。还要注意,在onSurfaceDestroyed()
下,您为您的soundPool
对象调用release()
来解散该对象并清理它所使用的内存。
要了解这是如何工作的,请播放该应用。像以前一样沿着屏幕拖动。每次都会播放不同的声音。然后声音效果列表循环回到开始。
您可以在游戏中的各种应用中使用这种技术。例如,当不同的怪物被消灭时,它们可以发出不同的声音。下一节将介绍如何在特定事件发生时播放声音。
将音效与事件相匹配
前面的例子对于声音循环来说很好,但是在大多数情况下,每个事件都有一个特定的声音。这很容易做到,只要想播放音频,就需要传递正确的声音 id。举个例子,想象一个场景,主角遇到一个恐怖的机器人。你通过播放机器人的声音来提醒玩家新的事件。
在你担心音效之前,你需要弄清楚机器人是否在角色附近。为此,您可以创建一个机器人精灵,并测试这两个精灵是否在一定的像素范围内。不过,对于这个例子,只要说您有办法检测到这种接近就足够了。在您的GameView.java
update()
函数中,您有一个if
语句,如果为真,则调用一个新方法进行响应。下面是伪代码:
`Public void update(adj_mov){
If(near_robot){
playsound(robot_noise);
}
}`
当你开发一个完整的游戏时,你的更新函数将会加载不同的测试来决定需要处理什么。您可以创建一个类似于robot_encounter()
的独特函数来存放与该事件相关的所有操作,而不是直接播放来自update()
函数的声音。现在,您需要快速创建一个playsound()
函数。
Playsound()
实际上是使用soundPool.play()
方法的一种更快捷的方式。清单 4-4 显示了代码:将其添加到GameView.java
。
清单 4-4。??Playsound()
public void playsound(int sound_id){ soundPool.play(sound_id, 1.0f, 1.0f, 1, 0, 1.0f); }
当你在GameView
类中时,无论你想在哪里播放一段音频,你都可以调用这个函数。当其他游戏需要更多的声音时,你可以创建新的声音 id,然后传递给这个方便的函数。
不用添加太多代码,你肯定增加了游戏的功能。因为声音在游戏过程中不像图像那样旋转或移动,所以它们可以被启动,然后就不动了。你添加到游戏中的新维度将帮助用户更加沉浸在游戏体验中。
添加音乐
安卓音乐令人兴奋。一些有趣的技术提供了惊人的功能。在您研究这些选项之前,让我们在游戏过程中播放一个 MIDI 歌曲文件。你的老朋友MediaPlayer
类完美地实现了这一点,因为它被设计用来播放 Android 中的所有媒体文件。
尽管音乐通常比声音效果长,并且您使用了不同的文件格式,但它的处理方式与前面几节中的声音效果几乎相同。
为了得到免费的音效,你去了[www.freesounds.org](http://www.freesounds.org)
。对于 MIDI 音频,我用的是[www.midiworld.com](http://www.midiworld.com)
。该网站提供了一个大型的.midi
文件库,你可以在自己的作品中使用。在流行类别下,我找到了 ABBA 的《给我一个机会》。让我们将其添加到您的应用中:
Download Give me a chance (or the song of your choice) to your desktop. Note that if you have your own MIDI file, Android is very picky about using
.mid
extension instead of.midi
. In the future, Android may support both of them, but this was a common source of problems in the past.Just like a sound effect, drag or copy the
.mid
file to theres
raw
folder of your project. Before you do this, please give it a reasonable name that is easy to retype. I now rename the filebackground_music.mid
.With the resource stored correctly, you can view the simple code for running it. First, create a private
MediaPlayer
variable at the beginning of theGameView
class:**private MediaPlayer mp;**
Add the bold code below to the
surfaceCreated()
method. This is how you instruct the tablet to start playing music immediately after creating the screen image: `@Override
public void surfaceCreated(SurfaceHolder holder) {
mGameLogic.setGameState(mGameLogic.RUNNING);
mGameLogic.start();** mp = MediaPlayer.create(getContext(), R.raw.background_music);**
** mp.setLooping(true);**
** mp.start();**
}`Because you have already handled a media player, this code should be self-evident. Create the
MediaPlayer
object by loading the appropriate file and passing the application context. You are doing this to prepare the music to be played. Then tellMediaPlayer
to cycle the sample before starting.To clean up after completion, modify the
surfaceDestroyed()
function with the following code:@Override public void surfaceDestroyed(SurfaceHolder holder) { soundPool.release(); mp.stop(); mp.release(); }
That's all.
Run the SoundsTest application, and you should hear music at the beginning of the game. If you drag the cursor on the screen, the sound from
soundPool
will play along with the music. When you want to play music files in the game, you can use the method you created.
有了播放音效和音乐的能力,你已经完成了对 Android 游戏音频的探索。下一个重要的媒体对象当然是视频。以下部分介绍了如何在游戏中播放剪辑。因为电影是媒体,它们的处理方式和声音一样。
添加视频
在游戏中播放视频是不寻常的,但它们在介绍游戏或每个关卡之前有一个非常重要的目的。幸运的是,视频与音乐和其他音频的处理方式非常相似。其实测试一个视频,可以用一个.3gp
文件代替.mid
文件。然后,当创建表面时,将播放视频。
在互联网上快速搜索 3GP 视频提供了过多的选择。如果你有.mp4
格式的音乐视频,你也可以把它们添加到你的raw
资源文件夹中。清单 4-5 包含了播放这些文件的代码。
清单 4-5。播放视频
`@Override
public void surfaceCreated(SurfaceHolder holder) {
mGameLogic.setGameState(mGameLogic.RUNNING);
mGameLogic.start();
mp = MediaPlayer.create(context, R.raw.intro_video, holder);
mp.setLooping(true);
mp.start();
}`
请注意,create()
方法的粗体参数与您播放声音的方式不同。这使用了传递给surfaceCreated()
函数的SurfaceHolder
。因为一个视频需要一个表面来播放,你把视频交给你的SurfaceView
来使用。视频在平板电脑屏幕的左上角播放。
只需一个快速的改变,MediaPlayer
就可以播放视频了。在播放基本媒体类型方面,您没有其他工作要做!您现在可以播放音效、音乐和视频。下一节回到音乐,并简要介绍了动态音频。这是一个很好的功能,可以让 Android 根据游戏的变化来改变播放的音乐。你不需要理解所有的内容,但这绝对是你在游戏中需要考虑的一个独特的功能。
管理音乐
游戏中的图像可以通过旋转、变换和移动来操作。相比之下,音乐是静态的:只能播放和暂停。在 Android 中,你有办法让音乐变得可以随时改变。这是一个相当复杂的技术,你可能需要一段时间才能适应并将其融入到游戏中。在这里,您将通过对该主题的简要概述触及到这个问题的表面。之后,您可以继续探索并将其添加到您的应用中。
以这种方式管理音乐的目的是创造一种更加身临其境的体验。当你看一部高质量的电影时,音乐会根据发生的事情而变化。例如,当主角准备战斗时,激动人心的音乐让你为史诗般的遭遇做好准备。在一个敏感的场景中播放缓慢浪漫的音乐。你可以在一场比赛中取得同样的成绩。理想的结果是游戏的音乐与动作并行。例如,当玩家到达一座岌岌可危的桥时,音乐应该转变成一种不祥的音调。当播放器接近尾声时,美妙的音乐响起。当乐谱不是固定的而是流动的时,游戏变得更加身临其境。
在这个例子中,您使用了JetPlayer
类来实现这个目的。像MediaPlayer
类一样,它可以用一些额外的特性来播放 MIDI 文件。它读取解释播放 MIDI 音频各部分的程序的 JET 文件。
在您试验JetPlayer
如何工作之前,让我们看看如何创建您自己的 JET 内容。Android 的开发者为你创造了一个美好的环境。它叫做 JET Creator 要使用它,你需要在你的电脑上安装 Python。按照以下步骤进行设置:
Download the Python version suitable for your computer at
[www.python.org/download/releases/2.7.2/](http://www.python.org/download/releases/2.7.2/)
.按照下载的安装程序的说明进行操作。在安装过程中,您可以选择 Python 的安装位置。参见图 4-1 。
图 4-1。一定要记住你把你的计算机编程语言发行版放在哪里.正确安装了 Python 的
wxPython:
[www.wxpython.org/download.php](http://www.wxpython.org/download.php)
needs to be installed. Select the version suitable for your computer again and start the installation process.在安装向导中,将 wxPython 指向您的 Python 安装,如图图 4-2 所示。
图 4-2。如果简介找不到您的计算机编程语言安装位置,您可能需要将它指向您安装中的文件夹
Lib\site-packages
.??注 WxPython 是计算机编程语言编程语言中用于图形用户界面的工具。如果没有它,您将被迫在命令提示符下完成所有工作.
Start JET Creator. To do this, please go to
android-sdk\tools\Jet\JetCreator\
in the Android installation directory and double-clickJetCreator.py
. The dialog box shown in Figure ?? and Figure 4-3 ?? appears.?? ?? 】 Figure 4-3. If you don't see the folder path in the open Jet file dialog box, don't worry about clicking the import button on the far right in the open jet file dialog box.
Find the path
android-sdk\tools\Jet\demo_content
. Select the ZIP folder nameddemocontent_1
.When prompted, the folder is allowed to decompress in the default location, usually in the
Jet
folder. You will see the JET Creator program, which lists several different MIDI files.
您可以自己探索 JET Creator 程序,但现在最重要的是知道每个 MIDI 片段可以被分配各种触发它的事件。事件是促使音乐从一首曲子转向另一首曲子的力量。如果您真的对创建自己的事件驱动音乐感兴趣,那么您需要熟练使用 JET Creator。最好的参考资料是在[
developer.android.com/guide/topics/media/jet/jetcreator_manual.html](http://developer.android.com/guide/topics/media/jet/jetcreator_manual.html)
的 Android 文档。从那里,你可以编辑演示喷气机的内容,使它适合你自己的游戏中可能发生的事件。
目前,你的游戏还没有现成的事件定义,所以让我们来看看一个名为 JetBoy 的 Android 示例项目中JetPlayer
的实现。分析完代码后,您就可以在未来的项目中实现 JET Creator 了。
为了测试这个完整的游戏,进入 Eclipse,通过完成新的 Android 项目对话框创建一个新的项目,其内容如图 4-4 所示。
图 4-4。测试 Android 示例中的 JetBoy 项目
在这个项目中,您还有几个值得关注的新文件和对象。因为这是一个完整的博弈,很复杂;然而,没有必要理解整个事情。你只需要处理JetPlayer
类是如何实现的。以下是您正在处理的文件的快速分类:
JetBoy.zip
: found inJetBoy_content
folder. Contains MIDI sequences and other information for playing streaming music.Level1.jtc
: found inres
raw
folder. Generated by JET Creator, with instructions for playing audio.Asteroid.java
:Asteroid
class, which contains some variables.Explosion.java
: Class for handling explosive variables.JetBoy.java
: The main activity that pushes most of the logic processing toJetBoyView.java
.JetBoyView.java
: The largest piece of code, which handlesJetPlayer
music content and runs the game engineer.
为了充分理解这个实现,我将重要的方法从JetBoyView.java
文件复制到了清单 4-6 。下面是一个简短的解释。
清单 4-6。??JetPlayer
`private void initializeJetPlayer() {
mJet = JetPlayer.getJetPlayer();
mJetPlaying = false;
mJet.clearQueue();
mJet.setEventListener(this);
Log.d(TAG, "opening jet file");
mJet.loadJetFile(mContext.getResources().openRawResourceFd(R.raw.level1));
Log.d(TAG, "opening jet file DONE");
mCurrentBed = 0;
byte sSegmentID = 0;
Log.d(TAG, " start queuing jet file");
mJet.queueJetSegment(0, 0, 0, 0, 0, sSegmentID);
mJet.queueJetSegment(1, 0, 4, 0, 0, sSegmentID);
mJet.queueJetSegment(1, 0, 4, 1, 0, sSegmentID);
mJet.setMuteArray(muteMask[0], true);
Log.d(TAG, " start queuing jet file DONE");
}`
下面是JetPlayer
的工作方式。首先,JetPlayer
从其队列中清除任何先前的文件或序列。这为接下来的操作提供了一个干净的石板。然后,它加载包含所需信息的文件,这一点我在前面已经指出。记住,这是使用 JET Creator 应用创建的。
起始序列设置为 0。激励元素是queueJetSegment()
:该函数加载 MIDI 的序列。它有一长串用于改变音频的参数,如 Android SDK 的表 4-1 中所解释的。
当您查看实现时,添加这些段更有意义。清单 4-7 包含了JetBoyView.java
中run()
和updateGameState()
函数的代码。
清单 4-7。 JetBoy 游戏循环
`public void run() {
while (mRun) {
Canvas c = null;
if (mState == STATE_RUNNING) {
updateGameState();
if (!mJetPlaying) {
mInitialized = false;
Log.d(TAG, "------> STARTING JET PLAY");
mJet.play();
mJetPlaying = true;
}
mPassedTime = System.currentTimeMillis();
if (mTimerTask == null) {
mTimerTask = new TimerTask() {
public void run() {
doCountDown();
}
};
mTimer.schedule(mTimerTask, mTaskIntervalInMillis);
}
}
else if (mState == STATE_PLAY && !mInitialized)
{
setInitialGameState();
} else if (mState == STATE_LOSE) {
mInitialized = false;
}
try {
c = mSurfaceHolder.lockCanvas(null);
doDraw(c);
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
/**
* This method handles updating the model of the game state. No
* rendering is done here only processing of inputs and update of state.
* This includes positions of all game objects (asteroids, player,
* explosions), their state (animation frame, hit), creation of new
* objects, etc.
*/
protected void updateGameState() {
while (true) {
GameEvent event = mEventQueue.poll();
if (event == null)
break;
if (event instanceof KeyGameEvent) {
mKeyContext = processKeyEvent((KeyGameEvent)event, mKeyContext);
updateLaser(mKeyContext);
}
else if (event instanceof JetGameEvent) {
JetGameEvent jetEvent = (JetGameEvent)event;
if (jetEvent.value == TIMER_EVENT) {
mLastBeatTime = System.currentTimeMillis();
updateLaser(mKeyContext);
updateExplosions(mKeyContext);
updateAsteroids(mKeyContext);
}
processJetEvent(jetEvent.player, jetEvent.segment, jetEvent.track,
jetEvent.channel, jetEvent.controller, jetEvent.value);
}
}
}`
虽然这段代码很难,但实际上对JetPlayer
做的事情很少;一旦添加了代码,其他游戏就不需要做很大的改动。回想一下,在run()
函数中,你有mjet.play()
。这将初始化任何需要播放的音频序列。
updateGameState()
通过改变jetEvent
触发JetPlayer
的变化。在这一领域,你也与爆炸、激光和小行星打交道。更新当前事件非常简单:将事件转换成JetGameEvent
格式。最后,最后一行调用决定音乐对新事件的响应的函数。
如果你理解了JetPlayer
,那么你已经准备好通过编辑 JetBoy 游戏来实现它了。如果您不确定这段代码以及它是如何工作的,不要担心;JET audio 是 Android 媒体功能中一个很酷但并不重要的方面。
总结
在本章中,您探索了 Android 的多媒体功能,包括播放音效、音乐和视频的能力。你也看到了这些媒体是如何融入游戏的。
这是一次旋风式的旅行。随着您自己的游戏越来越先进,您将继续探索这些特性。你可以通过声音和音频以及视频的适当实现来使你的游戏更加身临其境。
有了更多令人兴奋的技术,让我们在下一章继续为游戏玩家建立一个更加身临其境的体验。
五、有障碍的单人游戏
在了解了平板电脑游戏的图形、声音和输入之后,您就拥有了一个简单游戏所必需的所有构件。在这一章中,你将它们放在一起,构建一个简单的游戏,并为一些真正令人敬畏的创作做好准备。但是,即使要构建最简单的游戏,你也需要能够跟踪精灵,让它们遵守一些基本的物理定律,并以一种吸引用户参与的方式将它们组合起来。
在这一章中,你建立了一个有一些障碍的单人游戏。结果是一个简单的游戏,吸引了玩家。所有这些都是通过使用精灵来完成的。用户和精灵之间的交互以及精灵到精灵的交互构成了本章的核心。下一节将介绍如何构思你的第一个真正的游戏。
计划一个单人游戏:AllTogether
对于您的第一个可玩游戏,您创建了一个未爆炸的炸弹场和一个角色,其目标是从一边到另一边,而不触及一个。为了让游戏更具挑战性,你要让炸弹动起来。让我们把这个游戏叫做 AllTogether,因为它包含了你到目前为止所做的一切。
在编写代码之前,您需要做一些规划。例如,这里有一些在大多数游戏中常见的元素。不是每个游戏都有这些,但在大多数情况下,你可以在一个典型的游戏中看到它们:
- A user-controlled character (protagonist) faces obstacles and challenges in the game and must overcome them.
- Terrible consequences, this is the result that the protagonist faces because he fails to overcome the obstacles in the game.
- Success reward
这些元素可能看起来太明显了,但是它们对于正确塑造你的游戏是至关重要的。请注意,第一个标准与玩家控制整个世界的策略游戏无关。你在第九章中使用了一个策略游戏,所以你可以看到它是如何完成的。
列表上的第一项是 90%的程序的来源。如果你想象你最喜欢的游戏,几乎所有的事情都是为了达到特定的目标和战胜特定的关卡。最后两项通常很快,仅仅是赋予游戏意义。失败可能意味着在水下耗尽空气,在这种情况下,你的角色会死亡。或者,你可能无法足够快地完成这一关,你必须重新开始。当你到达终点或者杀死最后的 boss 时,成功是显而易见的。
我为这个例子创建的游戏已经处理了前两个元素。你在这一章的后面添加最后一个。为了简单起见,我想创建一个游戏,其中用户必须导航三个上下滑动的物体。用户必须仔细计时炸弹,然后快速反应。
如果用户击中了其中一个炸弹,他们会被送回起点,并被允许再试一次。游戏一直继续,直到他们厌烦了,关掉游戏。在查看了原始代码之后,您添加了一些特性,比如显示胜利消息的能力,这样用户就可以认识到他们的成功。看一下图 5-1 中的成品。
图 5-1。击败游戏
有了这个游戏的快速概览,你就可以把它变成现实了。
构建单人游戏
因为你已经在前几章做了这么多工作,你不需要改变很多东西来构建你的第一个真正的游戏。对于你的单人游戏,上一章中唯一需要修改的文件是SpriteObject.java
和GameView.java
:
- Open a new Eclipse project and name it all together .
- Copy all files of SoundTest project in chapter 4 of . Don't forget to copy Java source files in
src
folder and resource files inrsc
folder.
在你开始改变之前,让我们回顾一下处理运动和碰撞的过程。
升级游戏精灵
你首先升级你的精灵,这样你就可以更好地控制它们的运动,并检测它们之间或与游戏边界的碰撞。从现在开始,这个特性将会对你的所有工作有所帮助。
增加更精细的运动控制
对于你的新游戏来说,你上一个应用的速度太快了。为了给你更大的控制,让我们增加变量的精度,控制精灵的位置和每次移动的大小。
通过将 movement 和 location 变量转换为 Java 类型double
来完成更改。现在,当你想增加或减少一个精灵的速度时,你可以用一个小数来增加这些值,而不是局限于整数值。
当你想要更慢的速度时,这种能力是至关重要的。新游戏有 0.5 的移动调整,这在以前是不可能的——在第四章中,最低移动值是 1。为此,您需要更改 sprite 类中的函数以及变量声明。
要改变游戏精灵的运动和位置变量的精度,打开SpriteObject.java
并将清单 5-1 中的代码添加到SpriteObject
类的定义中。
清单 5-1。提高游戏位置精度和速度
private double x; private double y; private double x_move = 0; private double y_move = 0;
接下来,您需要一些新代码来检测对象之间的冲突。碰撞检测几乎是每个视频游戏的一个关键方面。
检测精灵之间的碰撞
下一个大的变化需要在SpriteObject
类中有一个全新的函数来处理冲突。如果你以前做过 2D 碰撞检测,这个解决方案看起来会很熟悉。该函数测试两个矩形的冲突。回想一下,因为 Android 中的屏幕的原点在左上角,所以如果第一个 sprite 的底部小于另一个 sprite 的顶部,那么就不会发生碰撞,因为第一个 sprite 位于屏幕上第二个 sprite 的上方。
如果两个精灵之间有冲突,那么新方法返回 true。有趣的是,当您搜索碰撞时,您使用位图来收集宽度。sprite 类不直接存储宽度或高度,因为它已经包含在位图中。您可以使用这种方法在以后为与墙壁的碰撞获取精灵的尺寸。
与任何需要一系列if
语句的函数一样,您的碰撞检测在处理方面是相当昂贵的。如果可能的话,您希望消除不必要的冲突例程。然而,这比逐像素检测要好得多,逐像素检测会导致游戏几乎停止。
将清单 5-2 中的函数添加到SpriteObject
类中。
清单 5-2。sprite object 类中的碰撞检测功能
`public boolean collide(SpriteObject entity){
double left, entity_left;
double right, entity_right;
double top, entity_top;
double bottom, entity_bottom;
left = x;
entity_left = entity.getX();
right = x + bitmap.getWidth();
entity_right = entity.getX() + entity.getBitmap().getWidth();
top = y;
entity_top = entity.getY();
bottom = y + bitmap.getHeight();
entity_bottom = entity.getY() + entity.getBitmap().getHeight();
if (bottom < entity_top) {
return false;
}
if (top > entity_bottom){
return false;
}
if (right < entity_left) {
return false;
}
if (left > entity_right){
return false;
}
return true;
}`
在清单 5-2 中,你为两个精灵的每个角收集 x 和 y 坐标。请记住,一个 sprite 调用函数,并使用第二个 sprite 作为参数。哪个精灵调用这个函数并不重要。结果将是一样的:不是对就是错。一旦你有了数据,你进入四个if
陈述。它们检查第一个精灵的底部是否低于另一个精灵的顶部。如果这是真的,那么第一个精灵将在另一个精灵之上,碰撞将是不可能的。接下来的if
语句在检查两个精灵的位置时是相似的。如果没有一个if
陈述是有效的,那么事实上就有冲突。
添加多个精灵
您的修改主要发生在GameView
类中,在那里您对更新函数进行了一些重大修改。创建一个名为bomb[]
的SpriteObject
数组可能是最重要的修改。因为炸弹的行为都是一样的,这样分组比单独处理要方便得多。这样做也消除了不必要的代码重复。
每个新炸弹精灵的初始化也很有趣,因为它们在屏幕上的位置。第一个和最后一个精灵在屏幕的底部开始,而第二个精灵在顶部附近。这在游戏过程中产生了交错运动,增加了难度。当您移动到surfaceCreated
功能时,第一个和最后一个炸弹向屏幕顶部移动,中间的炸弹向底部移动。
当你定义炸弹的运动时,你使用了 sprite 类中可以处理小数的新变量。做了一些测试,发现 1 的速度移动太快了,就减半用. 5。为了把你的炸弹放在屏幕上,onDraw()
功能使用一个快速循环来循环三个炸弹。
update
功能蕴含了游戏的魔力。在这里你定义了炸弹和玩家之间的关系以及炸弹的行为。前两个for
循环防止炸弹超出游戏边界;你想让炸弹在 y 坐标 100 和 500 之间来回反弹。下一个for
循环检查你的主精灵是否与任何炸弹相撞。如果发生碰撞,精灵会在课程开始时重置。
通过将update
函数改为清单 5-3 中的代码来完成它。
清单 5-3。新的 update()函数来控制炸弹。
`//check for bombs going too low
for(int i = 0; i < 3; i++){
if(bomb[i].getY() > 500){
bomb[i].setMoveY(-.5);
}
}
//check for bombs going too high
for(int i = 0; i < 3; i++){
if(bomb[i].getY() < 100){
bomb[i].setMoveY(.5);
}
}
//check for collisions with the sprite
for(int i = 0; i < 3; i++){
if(spritecharacter.collide(bomb[i])){
charactersprite.setX(100);
}
}
//perform specific updates
for(int i = 0; i < 3; i++){
bomb[i].update(adj_mov);
}
spritecharacter.update(adj_mov);`
最后,调用炸弹和精灵的update
函数。清单 5-4 中的所示的processMotionEvent
也有一些关键的特征变化。两个if
语句寻找用户占用和脱离屏幕的信号事件。当用户触摸屏幕时,精灵向前移动。否则,精灵将停留在屏幕上的当前位置。这种移动方式类似于你试图在洞穴中导航的直升机游戏:直升机向地面移动,除非你点击屏幕让它上升。
清单 5-4。 processMotionEvent()方法处理触摸和释放
if(input.action == InputObjectinput .ACTION_TOUCH_DOWN){ spritecharacter.setMoveX(.5); } if(input.action == InputObjectinput .ACTION_TOUCH_UP){ charactersprite.setMoveX(0); }
您的工作的代码部分已经完成。现在让我们来处理游戏中的图形。
为精灵添加图像
你的努力就要开花结果了。但是在编译项目之前,必须向项目中添加两个资源:一个炸弹图像和一个表示角色(或玩家)的图形。它们都被保存为.png
文件,角色使用透明背景,所以看起来不像一个移动的斑点。炸弹尺寸为 30 × 30,字符尺寸为 70 × 120。
小贴士如果你的图片给人的印象不深刻,不要担心;关键是要有东西可以用。在普通纸上画画,然后扫描图像是提高工作质量的一个简单策略。用绘图程序润色这幅画。或者,学习使用基于矢量的程序可以给你的艺术一个巨大的提升。
像处理任何其他应用一样,在模拟器中编译并运行这个项目。如果一切顺利,按住屏幕,你应该推动你的角色前进。如果你撞上了炸弹,你要重新开始。好好享受!
下一节通过引入奖励使兴奋度更高。
增加赢得游戏的奖励
这个简单的应用有几个关键点:
- You added an obstacle in the form of a bomb. This is aggravated by eccentric control, which can't give users precise movement.
- The response of failure returns to the beginning of the game. If you are caught by the last bomb, it will be even more serious.
- Having characters similar to people can increase players' interest. You don't do this by moving stars on the screen as in previous chapters.
你可以通过提供获胜的实际好处来使这个游戏变得更好。要做到这一点,试着制作一个如图图 5-2 所示的精灵,当玩家达到某个 x 值时调用它的draw()
函数。将一个变量设置为true
,这样标志会继续呈现,让用户沉浸在他们的荣耀中。这一点在本章的最终代码中没有涉及,因为它不是核心概念之一。但是,您可以自由添加它。
图 5-2。奖励玩家
跟踪游戏精灵的状态
因为精灵或整个游戏可能处于不同的位置或状态,所以您需要开发一种方法来跟踪它们。为了概念化状态,请看图 5-3 。它显示了三种不同状态的循环。
图 5-3。循环状态
如图 5-3 所示,在游戏过程中,状态很容易改变。游戏也经历生命周期,包括启动、循环和结束阶段。在 Android 和许多其他环境中,状态被定义为可以从各种其他类中访问的整数。
注意当你试图找出什么类型的
motionevent
发生时,你已经使用了状态。if
语句决定了eventtype
是向上还是向下动作,两者都是在InputObject
类中定义的整数值。
所有这些代码都放入您的SpriteObject
类,在那里您处理每个 sprite 的状态。炸弹之类的精灵不一定有不同的状态,所以你不用为它们使用这些特性。在您自己的游戏中,您可能更喜欢创建独立的 sprite 类,这些类继承了高级类的基本特性,然后使用更具体的方法和变量来区分 sprite 子类。
请遵循以下步骤:
在
SpriteObject.java
顶部创建四个基本状态作为整数(见清单 5-5 )。清单 5-5。代表精灵状态的常数
public int DEAD = 0; public int ALIVE = 1; public int JUMPING = 2; public int CROUCHING = 3;
我个人的偏好一直是将
DEAD
赋值为 0,因为你经常会有默认状态等于 0,为了让精灵活下来而执行某种动作(比如初始化关卡)是有意义的。国家的另一个重要方面是它们应该是排他性的。这意味着角色不能同时处于一种以上的状态。在初始化之前,角色一开始总是死的。从那时起,他们默认是活着的,直到一个动作被执行,如跳跃或被杀死..你需要创建两个快速函数来处理精灵状态。将清单 5-6 中显示的函数放入SpriteObject.java
中。清单 5-6。 getstate()和 setstate()函数
`public int getstate(){
return state;
}public void setstate(int s){
state = s;
}`These functions should look familiar, because this is how you access the X and Y coordinates of the wizard.
因为你定义的状态是公共整数,你可以用清单 5-7 中的代码在
GameView.java
中测试一个 sprite 是否死了。将此代码添加到update
函数中。清单 5-7。如果角色死了就重置角色
if(character.getstate() == SpriteObject.DEAD){ character.setX(100); character.setY(400); }
Pay attention to how simple it is to deal with basic information, such as what is happening to elves at present. It becomes more and more important when you see a complicated state like jumping. The elf was the fastest when he first left the ground. Then it gradually decreases until sprite reaches its peak height, and then it gradually accelerates. The change speed of sub-picture must be controlled within
update
function. You need to find out what state the elves are in, so as to changemoveY
at the right rate. For example, a normal jump will last for a predictable period of time. But what if the jump is interrupted by hitting the platform? You use the status to quickly evaluate the new situation.In order to completely merge the states in the game, put the code lines in Listing 5-8 into
if
statement to test the collision between characters and bombs. This is an alternative method to reset the position of the character when the bomb is hit, rather than being executed immediately in the conditional collision test.
清单 5-8。开始把人物当成死人。
Character.setState(SpriteObject.DEAD);
所有这些功能都包含在清单 5-9 和清单 5-10 的代码中。如果你迷路了,在你的项目中使用这段代码,你应该会得到一个可用的游戏。
清单 5-9。??SpriteObject.java
`package com.gameproject.alltogether;
import android.graphics.Bitmap;
import android.graphics.Canvas;
public class SpriteObject {
public int DEAD = 0;
public int ALIVE = 1;
public int JUMPING = 2;
public int CROUCHING = 3;
private Bitmap bitmap;
** private double x;**
** private double y;**
** private double x_move = 0;**
** private double y_move = 0;**
public SpriteObject(Bitmap bitmap, int x, int y) {
this.bitmap = bitmap;
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public Bitmap getBitmap() {
return bitmap;
}
public void setMoveX(double speedx){
x_move = speedx;
}
public void setMoveY(double speedy){
y_move = speedy;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
** public int getstate(){**
** return state;**
** }**
** public void setstate(int s){**
** state = s;**
** }**
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, (int)x - (bitmap.getWidth() / 2), (int)y - (bitmap.getHeight() / 2), null);
}
public void update(int adj_mov) {
x += (adj_mov * x_move);
y += (adj_mov * y_move);
}
** public boolean collide(SpriteObject entity){**
** double left, entity_left;**
** double right, entity_right;**
** double top, entity_top;**
** double bottom, entity_bottom;**
** left = x;**
** entity_left = entity.getX();**
** right = x + bitmap.getWidth();**
** entity_right = entity.getX() + entity.getBitmap().getWidth();**
** top = y;**
** entity_top = entity.getY();**
** bottom = y + bitmap.getHeight();**
** entity_bottom = entity.getY() + entity.getBitmap().getHeight();**
** if (bottom < entity_top) {**
** return false;**
** }**
** if (top > entity_bottom){**
** return false;**
** }**
** if (right < entity_left) {**
** return false;**
** }**
** if (left > entity_right){**
** return false;**
** }**
** return true;**
** }**
}`
我们现在来看看GameView.java
中的代码,它将这些新赋予的精灵付诸行动。
清单 5-10。 GameView.java
package com.gameproject.alltogether;
`import java.util.concurrent.ArrayBlockingQueue;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class GameView extends SurfaceView implements
SurfaceHolder.Callback {
private SpriteObject character;
** private SpriteObject[] bomb;**
private GameLogic mGameLogic;
private ArrayBlockingQueue
private int sound_id;
private Context context;
private SoundPool soundPool;
private int ID_robot_noise;
private int ID_alien_noise;
private int ID_human_noise;
private MediaPlayer mp;
public GameView(Context con) {
super(con);
context = con;
getHolder().addCallback(this);
character = new SpriteObject(BitmapFactory.decodeResource(getResources(),
R.drawable.sprite), 100, 400);
** bomb = new SpriteObject[3];**
** bomb[0] = new SpriteObject(BitmapFactory.decodeResource(getResources(),**
R.drawable.bomb), 400, 500);
** bomb[1] = new SpriteObject(BitmapFactory.decodeResource(getResources(),**
R.drawable.bomb), 650, 100);
** bomb[2] = new SpriteObject(BitmapFactory.decodeResource(getResources(),**
R.drawable.bomb), 900, 500);
mGameLogic = new GameLogic(getHolder(), this);
createInputObjectPool();
soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
ID_robot_noise = soundPool.load(context, R.raw.robot_noise, 1);
ID_alien_noise = soundPool.load(context, R.raw.alien_noise, 2);
ID_human_noise = soundPool.load(context, R.raw.human_noise, 3);
sound_id = ID_robot_noise;
setFocusable(true);
}
private void createInputObjectPool() {
inputObjectPool = new ArrayBlockingQueue
for (int i = 0; i < 20; i++) {
inputObjectPool.add(new InputObject(inputObjectPool));
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
int hist = event.getHistorySize();
if (hist > 0) {
for (int i = 0; i < hist; i++) {
InputObject input = inputObjectPool.take();
input.useEventHistory(event, i);
mGameLogic.feedInput(input);
}
}
InputObject input = inputObjectPool.take();
input.useEvent(event);
mGameLogic.feedInput(input);
} catch (InterruptedException e) {
}
try {
Thread.sleep(16);
} catch (InterruptedException e) {
}
return true;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mGameLogic.setGameState(mGameLogic.RUNNING);
mGameLogic.start();
bomb[0].setMoveY(-.5);
bomb[1].setMoveY(.5);
bomb[2].setMoveY(-.5);
mp = MediaPlayer.create(context, R.raw.background_music);
mp.setLooping(true);
mp.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
soundPool.release();
mp.stop();
mp.release();
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.GRAY);
character.draw(canvas);
for(int i = 0; i < 3; i++){
bomb[i].draw(canvas);
}
}
** public void update(int adj_mov) {**
if(character.getstate() == SpriteObject.DEAD){
character.setX(100);
character.setY(400);
}
** //check for bombs going too low**
** for(int i = 0; i < 3; i++){**
** if(bomb[i].getY() > 500){**
** bomb[i].setMoveY(-.5);**
** }**
** }**
** //check for bombs going too high**
** for(int i = 0; i < 3; i++){**
** if(bomb[i].getY() < 100){**
** bomb[i].setMoveY(.5);**
** }**
** }**
** //check for collisions with the sprite**
** for(int i = 0; i < 3; i++){
if(character.collide(bomb[i])){**
** character.setState(SpriteObject.DEAD);**
** }**
** }**
** //perform specific updates**
** for(int i = 0; i < 3; i++){**
** bomb[i].update(adj_mov);**
** }**
character.update(adj_mov);
}
public void processMotionEvent(InputObject input){
** if(input.action == InputObject.ACTION_TOUCH_DOWN){**
** sprite.setMoveX(.5);**
** }**
** if(input.action == InputObject.ACTION_TOUCH_UP){**
** sprite.setMoveX(0);**
** }**
}
public void processKeyEvent(InputObject input){
}
public void processOrientationEvent(float orientation[]){
float roll = orientation[2];
if (roll < -40) {
character.setMoveX(2);
} else if (roll > 40) {
character.setMoveX(-2);
}
}
public void playsound(int sound_id){
soundPool.play(sound_id, 1.0f, 1.0f, 1, 0, 1.0f);
}
}`
经历了所有这些变化后,你已经掌握了状态的概念,也处理了碰撞和精确的运动。
总结
你终于完成了你的第一个游戏。恭喜你!您还创建了可以在未来游戏中使用的代码。添加精灵状态正是你需要的功能,让你的玩家更好地控制他们的角色。几乎任何 2D 游戏现在都在你的掌握之中。您未来的项目将大量使用高效的碰撞检测方法。
接下来的几章调查了几种不同的游戏类型,它们利用了平板电脑的屏幕空间、处理能力和输入功能。第六章讲述了一个更复杂的游戏,玩家可以用球拍将球击成方块:著名的突围游戏。那里的主要问题是处理物理学。
六、球和球拍的游戏
在第五章中,你构建了一个简单的游戏,玩家躲避移动的炸弹。这给了你一个借口来使用许多在 Android 平板电脑上创建游戏的核心功能和编程概念。在这一章中,你将构建一个更复杂的游戏。
你在这一章的主要任务是建立一个 pong 类型的游戏,在这个游戏中,玩家使用一个球拍来保持球的弹跳,同时他们试图用球撞击和破坏方块。我第一次体验手机游戏是在一台旧的黑莓手机上,当时唯一的产品就是这个简单的游戏。我不得不用笨拙的黑莓轨迹球来控制球拍,小屏幕和低分辨率使这种努力不太令人满意。令人惊讶的是,这个游戏是用功能强大的 Java 语言编写的,您可以用它来创建更吸引人、更有趣的游戏。
当您构建球拍游戏时,您将掌握新的技能,并将其添加到您的工具箱中。向资源文件中添加额外的图像。你用球拍和木块替换了第五章的角色和炸弹。为了让球保持运动,您需要管理精灵的交互并检测更多的碰撞。你必须在游戏中加入一些额外的物理元素,需要更多的即时计算。你也可以用声音和消失的方块更有效地奖励玩家。最后,您将学习用单个 XML 布局文件初始化多个块的技术。
我们开始吧。
开始使用
让我们从收集你将在游戏中使用的图片和其他资源开始,然后为你的工作打开一个新的项目。
收集游戏资源
因为 pong 风格的游戏使用相当普通的形状和物体,所以制作图形应该不会有太大的困难。当然,最重要的考虑是每个元素的相对比例和大小。球拍必须足够大,能够始终如一地击球,但又足够小,这对球员来说是一个挑战。你可以看到,如果你想让能量和奖金出现在屏幕上,可以添加其他图像。
图 6-1 显示了你在这个游戏中使用的图片和尺寸。注意,它们都是不同的.png
文件。对于我的实现,我自己用 GIMP 画的,GIMP 是第二章提到的一个开源工具。
除了常规的图形和声音之外,第七章将结合使用一种新的资源来存储关卡的布局。不用在每个块的位置编码,而是用 XML 布局指定它。这是这个项目的棘手部分,所以我把它留到下一章。第一次演示仅使用了三个模块,没有任何额外的放置资源。
图 6-1。方块(上图)为 30 × 50 像素,球(中图)为 30 × 30 像素,球拍(下图)为 30 × 200 像素。
如果你担心使用黑球(因为背景传统上是黑色的),不要害怕。你可以很容易地改变背景的颜色。事实上,使用更浅的颜色会让游戏对玩家更有吸引力。
提示球拍和球的图像是半透明的。您可以通过在 GIMP 程序中选择白色透明来做到这一点。我强烈建议你也这样做,因为如果你不完全处理方块,这个游戏会显得更专业。当其他语言需要代码使元素透明时,您很幸运能够使用带有透明层的图像。
如果你在玩游戏的时候有一些好听的声音,这个游戏会更加令人身临其境。因为乒乓球游戏不会发出一系列不同的声音,所以你可以随心所欲地使用任何声音。我选择只使用一种声音:每当球与砖块碰撞时都会播放的一小段 MP3“twang”。代码不包括任何其他噪音或音乐,但你可以自由添加它们。当你开始一个新游戏时,它越简单,就越容易发现你代码中的错误和 bug。
创建新项目
因为你的游戏是完整的(也就是它有用户交互,有目标,有获胜的能力),所以你应该把它当作一个专业的 app,而不是一个练习。因此,最好对元素和代码使用特定的名称。所以,我们就把这个 app 命名为 TabletPaddle 吧。虽然没有创意,但这个名字描述了你对 pong 风格游戏的新看法。
要开始,请按照下列步骤操作:
- Make an Eclipse project with your name, and copy the code in the AllTogether project to your new project. Create a new folder in
res
and name itraw
to store the new sounds you added.- Upload your assets to its specific folder. Figure 6-2 shows what the project settings look like.
?? * ?? 】 Figure 6-2. Correct setting of test pad items*
- If there is an error at the beginning of the project, it is because the graphics and sound files that the code is looking for are missing. When you use the application, you can solve this problem in the code.
- Open the
SpriteObject.java
andGameView.java
files in the edit pane. You can leave other source files alone.
现在你已经收集了资源,并为 TabletPaddle 打开了一个新项目,你可以编写你需要的游戏元素了,准备好使用它们的界面,并调整游戏循环。
准备游戏环境
在处理的游戏循环之前,您必须启动所有这些新精灵——球拍、球和方块——每个精灵都有不同的属性和特性。你还必须准备好使用精灵的环境——游戏界面。让我们从更改您在上一节中打开的源文件开始,为您的新游戏做准备。
修改 SpriteObject.java
SpriteObject.java
需要一个额外的函数来返回MoveX
和MoveY
值,它们是存储精灵水平和垂直速度的变量。通过这种方式,您可以轻松地反转它们,使球改变方向。在其他类型的游戏中,你可能想要检查精灵的速度,以确保它不会太快。
请遵循以下步骤:
- Add the following two methods in
SpriteObject.java
:public double getMoveY(){ return y_move; } public double getMoveX(){ return x_move; }
- You can make another change to
SpriteObject.java
to make your programming more convenient. Instead of worrying about theadj_mov
variable that keeps the game at a constant speed, let the game run as fast as possible. This avoids the trouble of dealing with very small moving values and adds unpredictability to normal games. To make this change, please go toupdate()
function and change the code as follows:public void update(int adj_mov) { x += x_move; y += y_move; }
有了这些小的修正,你在游戏循环中的工作将会更加轻松。在接下来的几页里,你会看到这些碎片拼凑在一起。
修改 GameView.java
一旦你在GameView.java
中制定出你的流程和更新,你的游戏就可以最终成型了。请记住,这是您存储改变游戏性能和功能的代码的地方。以下是步骤:
Because this game didn't use the noise of the previous game, so from
GameView.java
:private SoundPool soundPool; private int sound_id; private int ID_robot_noise; private int ID_alien_noise; private int ID_human_noise;
Remove these variable declarations in, carefully check your code, and remove any references to these elements, because they will generate errors. You also need to change the two elf objects used in your previous game. The bigger your game is, the more likely you are to use a group of elves. This game is no exception. Later, you will study how to fill the block array with XML documents. You can remove the elves from the last chapter, because you don't have a bomb in this game! Declare your new elf in
GameView.java
:private SpriteObject paddle; private SpriteObject[] block; private SpriteObject ball;
添加以下变量,当测试球是否接触到边缘时,你可以使用这些变量来访问屏幕大小:
private int game_width; private int game_height;
注意如果这一切删除和重新键入都很麻烦,你可以通过这本书的网站(
[
code.google.com/p/android-tablet-games/](http://code.google.com/p/android-tablet-games/)
)下载一个空白的机器人项目。从那里,你可以从头开始创建游戏.必须彻底重做
GameView
的构造函数方法,才能让你的新 app 工作。清单 6-1 展示了新的构造函数方法,并附有简要说明。确保你的代码与清单 6-1 中的代码一致。清单 6-1。游戏视图构造器
`public GameView(Context con) {
super(con);
context = con;
getHolder().addCallback(this);
paddle = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.paddle), 600, 600);block = new SpriteObject[3];
block[0] = new SpriteObject(BitmapFactory.decodeResource(getResources(),
R.drawable.block), 300, 200);block[1] = new SpriteObject(BitmapFactory.decodeResource(getResources(),
R.drawable.block), 600, 200);block[2] = new SpriteObject(BitmapFactory.decodeResource(getResources(),
R.drawable.block), 900, 200);ball = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.ball),
600, 300);mGameLogic = new GameLogic(getHolder(), this);
createInputObjectPool();
setFocusable(true);
}`If you look back at the last project, this should look familiar. The
soundPool
object was deleted from the code, and new coordinates were inserted for the wizard during the initial rendering. Sometimes this can be tricky, so I like to create a blank image in GIMP, whose size is the size of the screen (1280 × 1040). Then, you can collect the coordinates that look suitable for your game.之前的游戏涉及到三个炸弹,这里你基本上用三块来代替。显然,您希望将来有更多的块,但是这样您就可以重用所有的
for
循环来遍历这些块。因为您现在已经熟悉了 sprite 对象,所以请注意,您唯一需要更改的是 sprite 的位置和要使用的图像资源。你需要让球动起来。下一个必须更改的功能是surfaceCreated()
,只需对 ball 功能做一些更改就可以简化它。您还可以添加两行代码,将画布或屏幕的高度和宽度分配给更新函数中使用的变量。将清单 6-2 中所示的代码添加到项目中。??清单 6-2 .
surfaceCreated()
功能覆盖`@Override
public void surfaceCreated(SurfaceHolder holder) {
mGameLogic.setGameState(mGameLogic.RUNNING);
mGameLogic.start();
ball.setMoveY(-10);
ball.setMoveX(10);
Canvas c = holder.lockCanvas();
game_width = canvas.getWidth();
game_height = canvas.getHeight();
holder.unlockCanvasAndPost(c);}`
This makes the ball start to move to the upper right, which should give the player enough time to track the movement of the ball and prepare for the reaction. If the starting speed you set here seems too fast or too slow,
surfaceCreated()
is where you come back to change it, because you will get the speed from the ball object later.You also need to change the function of
onDraw()
, but again it is not a very complicated change. The cycle of drawing all bricks is the same as the cycle you used to update the bomb before. Overwrite youronDraw()
function, as shown in Listing 6-3 .
清单 6-3。 onDraw()
功能覆盖
@Override public void onDraw(Canvas canvas) { canvas.drawColor(Color.WHITE); ball.draw(canvas); paddle.draw(canvas); for(int i = 0; i < 3; i++){ block[i].draw(canvas); } }
你已经解决了基本问题。现在,您将继续为之前的碰撞和事件工作添加一些附加功能。
增加碰撞检测和事件处理
可能没有什么比在一项编码任务上极其努力地工作,然后意识到这是不必要的更糟糕的了。为了避免这个问题,我花了大量的时间绘制图表,并弄清楚程序将如何工作,它看起来会是什么样子。图 6-3 是一个图表,显示了需要做什么以及游戏循环必须如何工作。
如果你和一个团队一起开发你的应用,每个人分享一个完成项目的愿景就更加重要了。这时,您可能想要创建概念艺术,以便每个人在处理代码或素材时都有东西可看。
图 6-3。游戏循环中你必须处理的事件。每个框代表从几行代码到处理变化的整个方法。
在你之前的作品中,你测试了碰撞,然后重置了游戏。TabletPaddle 增加了一层复杂性,因为您必须以多种方式响应碰撞。最重要的是,反应必须是即时的,以避免奇怪的行为,如球穿过球拍或离开屏幕。
好消息是,在这个游戏中,与墙壁、砖块和球拍的碰撞都会导致球反向运动。例如,当你把球扔向墙壁时,球会反弹回来。如果你把同一个球扔向桌子,它也会反弹。一旦你理解了这个概念,它很容易应用到所有的游戏元素中。
然而,并不是所有的反弹都是一样的。有时你需要翻转水平速度,而其他时候你需要翻转垂直速度。球的运动的交替是指球的方向。你的MoveX
和MoveY
值实际上是向量,当它们放在一起时,代表球的速度和方向。改变其中一个值的符号(如果它是正的,就变成负的;如果它是负的,就变成正的),这与球的前进方向相反。
图 6-4 和 6-5 说明了这是如何工作的。诀窍是检测球何时需要改变其水平方向,何时需要改变其垂直运动。这就是您必须在update()
函数中使用大量代码和if
语句的原因。
图 6-4。如果球从右侧撞上了木块,那么球会偏向右侧。这里水平运动发生变化,而垂直运动保持不变。
图 6-5。在这种情况下,球从底部撞到木块,然后又弹回来。因为球还是向右运动,只有垂直运动发生了变化。
你能够计算出两个精灵碰撞的时间,但是你从来没有指定物体的哪一侧被另一个精灵击中。清单 6-4 中的代码通过测试球的 x、y、右侧和底部与球拍、墙和木块的对比来解决这个问题。请注意,您在方块被击中后将其设置为dead
,但是您没有做任何事情将它们从游戏中移除。一旦你测试了你当前的工作,这个问题就会得到解决。
清单 6-4 显示了你用来修改update()
冲突的代码。
清单 6-4。 Update()
同碰撞物理学
`public void update(int adj_mov) {
int ball_bottom = (int)(ball.getY() + ball.getBitmap().getHeight());
int ball_right = (int)(ball.getX() + ball.getBitmap().getWidth());
int ball_y = (int) ball.getY();
int ball_x = (int) ball.getX();
//Bottom Collision
if(ball_bottom > game_height){
ball.setMoveY(-ball.getMoveY());
//player loses
}
//Top collision
if(ball_y < 0){
ball.setMoveY(-ball.getMoveY());
}
//Right-side collision
if(ball_right > game_width){
ball.setMoveX(-ball.getMoveX());
}
//Left-side collision
if(ball_x < 0){
ball.setMoveX(-ball.getMoveX());
}
//paddle collision
if(paddle.collide(ball)){
if(ball_bottom > paddle.getY() && ball_bottom < paddle.getY() + 20){
ball.setMoveY(-ball.getMoveY());
}
}
//check for block collisions
for(int i = 0; i < 3; i++){
if(ball.collide(block[i])){
block[i].setstate(block[i].DEAD);
int block_bottom = (int)(block[i].getY() +
block[i].getBitmap().getHeight());
int block_right =(int)(block[i].getX() +
block[i].getBitmap().getWidth());
//hits bottom of block
if(ball_y > block_bottom - 10){
ball.setMoveY(ball.getMoveY());
}
//hits top of block
else if(ball_bottom < block[i].getY() + 10){
ball.setMoveY(-ball.getMoveY());
}
//hits from right
else if(ball_x > block_right - 10){
ball.setMoveX(ball.getMoveX());
}
//hits from left
else if(ball_right < block[i].getX() + 10){
ball.setMoveX(-ball.getMoveX());
}
}
}
//perform specific updates
for(int i = 0; i < 3; i++){
block[i].update(adj_mov);
}
paddle.update(adj_mov);
ball.update(adj_mov);
}`
在开始测试碰撞之前,您需要定义球的点。这样可以节省你每次需要使用球的宽度和位置时获取球的宽度和位置的时间。我建议你尽可能地这样做,因为这确实能清理你的代码,让其他人更容易阅读。
接下来的四个if
语句完成了相当简单的任务,检查球是否击中了屏幕的一个边缘。你在SpriteObject
中创建的方法getMoveX()
和getMoveY()
被使用了几次,因为你想反转球之前的任何运动。与侧壁的碰撞显然会改变水平运动,而顶部和底部会导致球在垂直方向上的移动。
你可能已经敏锐地注意到,你只是把球从屏幕底部弹开,而不是因此惩罚球员。这使得编辑游戏变得更容易,因为你不必一直担心重新启动它。
提示通常,当我创建一个游戏时,我会给自己留些“遗漏”或作弊的信息,这样我就不必在整个游戏中测试一个单独的部分。例如,我不想为了测试最终的挑战而战斗通过一个游戏的 10 个级别;相反,我需要跳到这一部分。
检查球与球拍碰撞的代码看起来似乎很简单,因为你只想知道球是否碰到了球拍的顶部。虽然可以想象球会撞到球拍的侧面,但这只会改变球的水平运动,仍然会导致球撞到屏幕底部,从而结束游戏。为了避免不必要的处理,我们不要担心与侧面的碰撞。没有附加方面更容易看出概念。
球拍的代码确保球在球拍顶部 20 个像素以内。因为球一次只能向任何方向移动十个单位,它永远不会越过这个窗口。始终确保该区域超过精灵的最大移动量,这样你就不必处理卡在另一个精灵里面的球或其他物品。
与积木的碰撞是一个不同的故事。要处理那些必须能够从四面八方被击中的木块,你必须做更多的工作。主要的一点是,首先分配一些变量,以便更容易地访问块的位置和尺寸。然后,首先测试最有可能发生的顶部和底部碰撞。然后你测试左右两边的打击。请注意,您查看碰撞的顺序会影响球的整体行为。
一旦其中一个条件为真,游戏将停止搜索更多可能的碰撞。图 6-6 说明了这个概念。左右碰撞框相当小,因为你不想冒险搞砸顶部或底部碰撞。
图 6-6。球在哪里会与木块相撞
添加触摸、声音和奖励
现在,您已经准备好完成应用了。你需要让用户控制游戏手柄,并添加声音和一些回报来吸引玩家。
增加拨片的触摸控制
AllTogether 项目使用平板电脑屏幕的触摸和释放来推动角色前进。在 TabletPaddle 中,根据用户在屏幕上的拖动,面板会水平移动。为了测试碰撞,你让用户在屏幕上拖动整个球拍。当你完成游戏时,你可以通过不允许用户自由拖动球拍来锁定球拍的 y 位置。
请遵循以下步骤:
- Here is the new
processMotionEvent()
, which updates the position of the paddle according to the position of the last finger touch. Modify the project code accordingly:public void processMotionEvent(InputObject input){ paddle.setX(input.x); paddle.setY(input.y); }
- The code also needs some minor cleaning. Do you remember
playsound()
function andprocessOrientationEvent
code? You can safely comment these out.- With the ability to control paddle, you are finally ready to try TabletPaddle. Run programs and play games as usual. This may not be very interesting, but it is an amazing functional game for very limited code. Figure 6-7 shows the expected results.
图 6-7。桌面板
添加声音
游戏可以玩,但远未完成。下一步是给游戏添加声音。您可以按照前面章节中的步骤来完成此操作。因为您只想要一种声音,所以您可以使用MediaPlayer
类,而不是使用SoundPool
s:
- Add this variable to the list of variables at the beginning of the program:
Private MediaPlayer mp;
- Insert this code into the constructor of
GameView.java
:mp = MediaPlayer.create(context, R.raw.bounce);
- Listing 6-5 shows the part of the
update()
function where the sound instruction is placed. Recall that no matter which side of the building block the ball touches, you will make a sound.
清单 6-5。 Update()
同声
`//check for brick collisions
for(int i = 0; i < 3; i++){
if(ball.collide(block[i])){
block[i].setstate(block[i].DEAD);
mp.start();
int block_bottom = (int)(block[i].getY() + block[i].getBitmap().getHeight());
int block_right =(int)(block[i].getX() + block[i].getBitmap().getWidth());
//hits bottom of block
if(ball_y > block_bottom - 10){
ball.setMoveY(ball.getMoveY());
}
//hits top of block
else if(ball_bottom < block[i].getY() + 10){
ball.setMoveY(-ball.getMoveY());
}
//hits from right
else if(ball_x > block_right - 10){
ball.setMoveX(ball.getMoveX());
}
//hits from left
else if(ball_right < block[i].getX() + 10){
ball.setMoveX(-ball.getMoveX());
}
}
}`
实例化块
随着一些噪音的继续,你可以找到一种方法来添加更多的块,使游戏变得有趣。您可以将 x 和 y 位置放入一个 XML 文档中,而不必经历对每个块的位置进行硬编码的艰苦过程。在将数据存储到另一个 XML 文件时,Android 非常聪明。事实上,这种做法是非常值得鼓励的,因为它使代码更具可读性和可编辑性,性能上只有轻微的滞后,这通常是不明显的。
以下是步骤:
Right-click the
values
folder in theres
folder, select New, and then select "File" to createblockposition.xml
. Type the nameblockposition.xml
. Here is the starting code for typing this new file. The goal is to keep the blocks in the same position, but allow you to add more as you think fit: `
3
- 300
- 600
- 900
- 200
- 200
- 200
`
All this code does is create an integer value of 3, which specifies how many blocks there will be. Then, the two arrays handle the X and Y positions of the block respectively. When adding more blocks, update the
blocknumber
value and add more positions for the blocks.To access the data stored in the XML file, at
GameView.java
:private Resources res; private int[] x_coords; private int[] y_coords; private int block_count;
Declare these variables at the top of the. You are using the
Resources
class, so add the following line of code to your import set:import android.content.res.Resources;
In the constructor of
GameView.java
, delete the lines of code that handle these blocks. You have to completely redo that part. The following is the improved new code, which extracts data from the XML document you created:res = getResources(); block_count = res.getInteger(R.integer.blocknumber); x_coords = res.getIntArray(R.array.x); y_coords = res.getIntArray(R.array.y); block = new SpriteObject[block_count]; for(int i = 0; i < block_count; i++){ block[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.block), x_coords[i], y_coords[i]); }
res
is basically a handler for you to call functionsgetInteger()
andgetIntArray()
from XML files. The array and integer are stored, and then afor
loop is executed to create each new block. You no longer need to specify the number of blocks in the code, so it is very easy to change the number of blocks.Unfortunately, the number of blocks you originally specified was 3. Now you need to replace the values in the
onDraw()
andupdate()
functions. Find these points and insertblock_count
in thefor
ring where you see 3. Theupdate()
method must be changed in two places, because it calls theupdate()
function of each wizard at the end and needs to check the collision between each block and the ball.
注意我喜欢在 XML 文件中存储块的布局和位置的原因之一是能够容易地比较每个块在哪里。例如,您首先使用的三个块的 y 值都是 200。这使得慢慢增加 x 值变得容易,因为您可以注意到垂直位置的趋势。因为块的高度为 30 像素,所以可以在垂直位置 y = 230 处制作下一组块。
移除死块
在认真对待这个游戏之前,必须解决一个主要问题:方块必须在被击中后消失。您已经将它们的状态设置为dead
,但是您没有以任何方式对状态做出响应。为了解决这个问题,您需要在SpriteObject.java
文件中做一些工作。
基本上,每个函数都必须有一个检查其状态的初始if
语句。如果块是活动的,那么动作继续。如果不是,函数返回null
,不担心死 sprite。
请遵循以下步骤:
Add this statement to the
SpriteObject
constructor to ensure that all created wizards are active. It is useless to initialize a dead sprite:state = ALIVE;
看清单 6-6 中的代码为
draw()
、update()
、collide()
。一个简单的if
语句只有在 sprite 活动时才继续。清单 6-6:
draw()``update()``collide()
`public void draw(Canvas canvas) {
if(state == ALIVE){
canvas.drawBitmap(bitmap, (int)x - (bitmap.getWidth() / 2), (int)y -
(bitmap.getHeight() / 2), null);
}
}public void update(int adj_mov) {
if(state == ALIVE){
x += x_move;
y += y_move;
}
}public boolean collide(SpriteObject entity){
if(state == ALIVE){
double left, entity_left;
double right, entity_right;
double top, entity_top;
double bottom, entity_bottom;left = x;
entity_left = entity.getX();
right = x + bitmap.getWidth();
entity_right = entity.getX() + entity.getBitmap().getWidth();
top = y;
entity_top = entity.getY();
bottom = y + bitmap.getHeight();
entity_bottom = entity.getY() + entity.getBitmap().getHeight();if (bottom < entity_top) {
return false;
}
else if (top > entity_bottom){
return false;
}
else if (right < entity_left) {
return false;
}
else if (left > entity_right){
return false;
}
else{
return true;
}
}
else{
return false;
}
}`
唯一真正的技巧是,collide()
函数需要在末尾有一个else
语句,因为必须从该方法返回一个值。否则,你已经集成了一个非常简单的程序,使你的块消失,只要他们被击中。您仍然可以访问块的 x、y、位图和状态,但是没有必要这样做。
总结
你在这一章完成了很多。就目前情况来看,TabletPaddle 是一款不错的游戏,有很大的发展和改进空间。困难和核心功能是存在的,物理处理碰撞流畅,游戏反应迅速和正确。我已经整理了一个列表,列出了一些可能会引起你兴趣的游戏创意。它们都不涉及 Android touch 编程,但它们确实涉及逻辑和创造力:
- Reset the game when the ball lands: At this time, the ball just keeps bouncing. What about the image with the word "Game over"?
- Keep score: You can detect when a ball is hit, so why not track the number of hits? Users can see how well they are doing.
- Add levels: This task may be quite laborious, but please remember that the only difference between levels in this game is the layout of the squares.
在brickposition.xml
文件中,您可以创建整数集和整数数组来存储每个级别中块的位置。读完这一章后,你就可以开发一些杀手级应用了。在处理球、球拍和木块之间的碰撞时,你学到了一些新的技能。您还学习了一些游戏逻辑,并开发了一种有趣的方法来处理复杂的冲突。在对用户输入做出反应这一至关重要的世界中,你提供声音并让方块消失以奖励用户的工作。
在未来,你增加了平板电脑操作的复杂性。具体来说,在下一章,处理器将有自己的思想。代码可以自己创建事件,并使玩家对不可预测的行为做出反应,而不是只处理玩家的动作。
七、构建双人游戏
你在 Android 平板电脑游戏上做了一些了不起的工作。现在,您将通过允许一个人与附近的其他人进行游戏来为工作添加另一个级别。这是制作拥有大量粉丝的游戏的关键一步。如果你看看现在众多流行的游戏,绝大多数主要是为了在用户自己的家里玩朋友和陌生人的游戏。
添加连接多个设备的功能变得相当复杂。幸运的是,Android 文档提供了一些示例,您可以对其进行修改以实现您的目标,因此您所要做的就是理解代码是如何工作的,然后将它整合到您的游戏中。
在这一章中,你将学习多人游戏的各个方面,包括不同的类型和实现。然后,你继续专注于 Android。在本章的结尾,你会明白如何创建和改编你自己的游戏来获得多人游戏的体验。在你开始之前,让我们先看看不同类型的多人游戏模式,以及它们通常是如何实现的。
注意如果你对这一章中的任何一部分代码感到困惑,请继续阅读,它们会一起出现。如果你还是不明白,可以上网查询解决方案,或者运行程序,只修改你需要的部分。Android 文档总是一个很好的起点:
[
developer.android.com/guide/index.html](http://developer.android.com/guide/index.html)
。通常,只要你理解它是如何工作的,就没有必要编写所有的代码。
了解多人游戏
你曾经通过电子游戏机或者你的个人电脑和别人玩第一人称射击游戏吗?这些游戏每年为视频游戏公司带来数亿美元的收入,因为它们能够吸引其他玩家,而不仅仅是电脑创造的角色。
涉及整个世界的在线游戏也非常受欢迎(想想魔兽世界)。平板电脑和手机也抓住了这股越来越多的连接热潮。可能最新类型的多人游戏是社交游戏。Farmville、Mafia Wars 和其他各种产品连接到社交网站(最著名的是脸书),将您的进度信息传输到您的朋友正在玩的游戏中。
通过服务器进行多人游戏
刚才提到的所有游戏都涉及到通过服务器连接玩家。这意味着设备或玩家不是直接相互连接,而是通过另一个实体连接。事实上,互联网上的网站使用同样的方法:你(客户端)从网站(服务器)获取网页材质。
图 7-1 是一个简单的图表,展示了几个人为了玩多人游戏而连接到一个服务器。
图 7-1。一群来自不同地方的玩家登录一个中央服务器,然后就可以互相对战。
在您研究服务器类型的多人游戏的优缺点之前,能够将这种方法与其他方法进行比较是有帮助的。我们来看对等方法。
点对点多人游戏
当玩家彼此直接连接时,他们使用的是点对点(P2P)网络。对手在几英尺之内玩的 P2P 游戏通常使用蓝牙实现,蓝牙是大多数 Android 平板电脑上可用的局域网协议。这意味着没有实体控制所有的通信。如果你使用过 P2P 文件共享网络(例如,使用 torrents 从其他用户而不是单一服务器下载大文件),那么你已经连接到其他像你一样的计算机来下载文件;你不需要每个人都连接的大型服务器。许多大型游戏机视频游戏不使用点对点模式,因为一次只能有几个玩家玩。
要了解服务器-客户端游戏和点对点游戏的区别,请看一下图 7-2 。
图 7-2。为了在游戏中竞争,两个玩家直接相互连接。
显然,多人游戏的这两种策略非常不同,但你可能想知道哪种更好。没有正确的答案;相反,存在一个优于另一个的情况。
选择多人游戏方式
表 7-1 和表 7-2 列出了两种多人游戏方式的主要优缺点。这不是一个官方列表(有些人可能不同意某件事是积极的还是消极的),但它给了你一个如何选择解决方案的非常重要的把握。
如果您仔细查看了这些表,您应该已经注意到服务器-客户机方法的优点列是对等方法的缺点列,而对等方法的优点是服务器-客户机的缺点。然而,将每种类型的优缺点相加并不能得出正确的选择。相反,你必须为你想要创造的东西制定一个计划,然后选择最能让你实现目标的方法。
在本章的剩余部分,您将改编您在第六章中为两个玩家构建的平板划球拍游戏,每个玩家都可以控制平板电脑上显示的两个划球拍中的一个。因为多人游戏编程可能很复杂,这一章将介绍主要概念。这里有完整的代码供您使用:[
code.google.com/p/android-tablet-games/](http://code.google.com/p/android-tablet-games/)
。
因为你一次只需要容纳两个玩家,而你又想用最高效的手段打造这样一个游戏,所以你使用了点对点的多人游戏模式。你可以使用大多数 Android 设备上的蓝牙网络直接连接玩家,而不是通过 3G 或 Wi-Fi 网络连接。通过选择这种方式,您可以节省大量的时间,而这些时间本来是用来设置服务器架构和确保设备能够正确连接的。
提示对于初学游戏编程的人来说,最好远离服务器-客户端多人游戏,因为它们几乎总是要复杂得多。不要因此而气馁;您可以通过蓝牙连接创建大量优秀的游戏。在这种情况下,玩家的额外兴奋是他们几乎总是在彼此附近,可以通过棘手的关卡互相交谈,或者进行一些有趣的垃圾交谈。
构建一个双人点对点游戏
作为一名开发人员,您可以合理地确信,大多数 Android 平板设备都支持蓝牙。几乎所有现代手机都支持蓝牙连接无线耳机,实现免提通话。这项技术在平板电脑中实现,以允许使用相同的耳机以及键盘和各种其他外围设备。
虽然有些人使用术语蓝牙来指代耳机和他们用来连接电话的设备,但实际上蓝牙是一种无线电广播系统,各种设备都用它来连接和共享照片、音乐、视频和几乎所有其他类型的数据。蓝牙最大的优点是速度快得令人难以置信。如果你可以用蓝牙耳机不间断地打电话,那么你可以确信它对大多数游戏来说足够快了。
在接下来的几节中,您将改编第六章中的平板球拍游戏,供两名玩家使用。首先添加代码,使用内置的蓝牙无线电连接两个 Android 平板电脑,然后添加第二个球拍和代码,允许球员争夺球的控制权。
我们开始吧。首先打开一个新的 Eclipse 项目,并将其命名为 TwoPlayerPaddleGame。
增加蓝牙连接
因为连接多个设备是一项复杂的任务,在 Android 平板电脑上支持这种交互的代码更难解释。本例中的片段摘自 Android samples 中一个更大的蓝牙项目:BluetoothChat。您在这里使用它们来探索主要概念。这些变量还没有全部初始化,但它们仍然传达了基本信息。在深入这个例子之前,让我们先来看看构成一个成功的蓝牙应用的大部分元素。
首先,您必须初始化与平板电脑内蓝牙连接器的链接。请遵循以下步骤:
将清单 7-1 所示的代码包含在
MainActivity.java
的onCreate()
函数中。清单 7-1。
onCreate()
`BlueAdapter = BluetoothAdapter.getDefaultAdapter();
if (BlueAdapter == null) {
Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
return;
}`
BlueAdapter
成为设备中蓝牙功能的手柄。if
语句用于确定蓝牙是否可用。然后,该函数向用户发布消息,提醒他们不能使用该程序。
2.你的启动的另一部分发生在一个你以前没有处理过的方法中:在MainActivity.java
中,紧随onCreate()
之后的onState()
函数;参见清单 7-2 。您还需要导入android.intent.Intent
,它让活动发送消息。
清单 7-2。??onStart()
` @Override
public void onStart() {
super.onStart();
if (!BlueAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}
else {
if (game_running == null) startgame();
}
}`
清单 7-2 中的代码检查蓝牙设备看它是开着还是关着。它通过调用来启动蓝牙设备。(您很快就会看到这个新活动的表现。)如果蓝牙打开,你检查游戏是否已经开始。如果没有,你调用一个新的函数来初始化游戏。请注意,您的大部分额外代码都围绕着这样一个事实,即游戏的许多方面都必须在开始之前等待正确的蓝牙连接。
3.当活动被发送消息时,使用清单 7-3 中的代码。
清单 7-3。??onActivityResult()
`public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CONNECT_DEVICE:
if (resultCode == Activity.RESULT_OK) {
String address = data.getExtras()
.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
BluetoothDevice device = BlueAdapter.getRemoteDevice(address);
mGameView.connect(device);
}
break;
case REQUEST_ENABLE_BT:
if (resultCode == Activity.RESULT_OK) {
startgame();
} else {
Toast.makeText(this, “Bluetooth failed to initiate”, Toast.LENGTH_SHORT).show();
finish();
}
}
}`
上面的代码做了两件简单的事情。首先,如果您用连接另一个设备的请求来调用它,它会完成收集另一个设备的地址并创建一个到它的蓝牙设备的链接的步骤。然后它调用mGameView
中的一个新函数将两个设备绑定在一起。
4.现在你有了一个非常简短的startgame()
函数。清单 7-4 展示了游戏是如何开始的。
清单 7-4。??startgame()
` private void startgame() {
mGameView = new GameView(this, mHandler);
setContentView(mGameView);
}`
这个函数很大程度上并不令人兴奋,但是需要注意的是,您正在向GameView
构造函数发送一个新的参数。处理程序是你从蓝牙通道向游戏发送数据的手段。理解这是如何工作的可能是蓝牙编程最重要的方面。
5.清单 7-5 中的代码围绕着处理发送和接收数据的不同任务的处理器。
清单 7-5。处理经办人
`private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_STATE_CHANGE:
switch (msg.arg1) {
case BluetoothChatService.STATE_CONNECTED:
break;
case BluetoothChatService.STATE_CONNECTING:
Toast.makeText(this, “Connecting to Bluetooth”, Toast.LENGTH_SHORT).show();
break;
case BluetoothChatService.STATE_LISTEN:
case BluetoothChatService.STATE_NONE:
Toast.makeText(this, “Not Connected to Bluetooth”, Toast.LENGTH_SHORT).show();
break;
}
break;
case SEND_DATA:
byte[] writeBuf = (byte[]) msg.obj;
String writeMessage = new String(writeBuf);
break;
case RECEIVE_DATA:
byte[] readBuf = (byte[]) msg.obj;
String readMessage = new String(readBuf, 0, msg.arg1);
break;
case MESSAGE_DEVICE_NAME:
mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
Toast.makeText(getApplicationContext(), "Connected to "
+ mConnectedDeviceName, Toast.LENGTH_SHORT).show();
break;
case MESSAGE_TOAST:
Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
Toast.LENGTH_SHORT).show();
break;
}
}
};`
因为处理程序的初始化做了很多工作,所以下面列出了各种活动供您查看。一旦你真正创建了自己的项目,你就会回到这个问题上来。基本上,处理程序被传递一个特定的消息或事件,它必须处理或忽略。它有各种各样的反应,你必须编码。请记住,这些是从GameView
类发送的:
MESSAGE_STATE_CHANGE
: The first case is the change of Bluetooth connection status. In most cases, if the status becomes disconnected, you will remind the user. For example, if the service is trying to connect, you will remind the user of this. If the connection cannot be established in an unfortunate event, you can also remind the user by explaining the problem. This also helps to debug problems.SEND_DATA
: The next event is to send data to the other device. Here, you have collected the code string and are ready to send it to another device. You didn't actually send it here; You can come back later to add this feature.RECEIVE_DATA
: Similar to you calling to write messages to the other device, you also accept data from the other device. Similarly, when you decide what you want to accomplish, there will be more code in this area.MESSAGE_DEVICE_NAME
: The penultimate message is a call, which just reminds the user that they have connected to a specific device. You remind the user through a small pop-up box.MESSAGE_TOAST
: Finally, you have a general method to send messages from theGameView
class to users.
管理蓝牙连接
随着GameView.java
的增加,你将回到更熟悉的领域。请记住,您需要将大部分代码放在这里,因为您可以在这里根据平板电脑之间来回发送的数据来更改精灵的位置。
清单 7-6 、 7-7 和 7-8 显示了三个小线程的代码,您必须将它们添加到GameView
中,以处理两个玩家交互时出现的各种蓝牙操作:AcceptThread
、ConnectThread
和ConnectedThread
。AcceptThread
处理初始连接,ConnectThread
处理复杂的设备配对,ConnectedThread
是设备在一起时的正常程序。
清单 7-6。??AcceptThread
`private class AcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
// Create a new listening server socket
try {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "listen() failed", e);
}
mmServerSocket = tmp;
}
public void run() {
if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
setName("AcceptThread");
BluetoothSocket socket = null;
// Listen to the server socket if you're not connected
while (mState != STATE_CONNECTED) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
}
// If a connection was accepted
if (socket != null) {
synchronized (BluetoothChatService.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// Situation normal. Start the connected thread.
connected(socket, socket.getRemoteDevice());
break;
case STATE_NONE:
case STATE_CONNECTED:
// Either not ready or already connected. Terminate new socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
if (D) Log.i(TAG, "END mAcceptThread");
}
public void cancel() {
if (D) Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}`
AcceptThread
是一段复杂的代码,但实际上它只是等待一个连接被接受。请注意,关键字socket
频繁出现。这是设备或实体之间任何类型连接的标准,指的是交换信息的能力。这个代码不是我的;它是从 Android 文档的一个例子中重复使用的。一些 it 方法和代码块非常高效,不需要重做。
清单 7-7。??ConnectThread
`private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "create() failed", e);
}
mmSocket = tmp;
}
public void run() {
Log.i(TAG, "BEGIN mConnectThread");
setName("ConnectThread");
// Always cancel discovery because it will slow down a connection
mAdapter.cancelDiscovery();
// Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
mmSocket.connect();
} catch (IOException e) {
connectionFailed();
// Close the socket
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
// Start the service over to restart listening mode
GameView.this.start();
return;
}
// Reset the ConnectThread because you're done
synchronized (BluetoothChatService.this) {
mConnectThread = null;
}
// Start the connected thread
connected(mmSocket, mmDevice);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}`
这个线程与前一个线程相似,它处理连接到另一个设备的尝试。Android 的例子也包括这个,所以我没有对它做任何修改。如果你好奇的话,它会尝试ping或者与另一个设备建立连接。如果失败,它可以通过try
块调用继续尝试,失败会导致重启。
幸运的是,您真的只对来回发送数据感兴趣,不需要改变连接的建立方式。
清单 7-8。??ConnectedThread
`private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// Keep listening to the InputStream while connected
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI Activity
mHandler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost();
break;
}
}
}
/**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// Share the sent message back to the UI Activity
mHandler.obtainMessage(MainActivity.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}`
类做了大量的工作。每当设备处于连接状态时,此代码都会运行。请注意,它首先收集输入和输出流,以便可以从其他设备访问数据,然后再发送自己的信息。
接下来,run()
方法进入一个循环,不断检查它可以处理的新数据。大多数数据都是以整数的形式发送的,但是发送字符串作为设备之间的交换还是有一些好处的。首先,在一个复杂的游戏中,可能有许多数字需要发送,如生命值、弹药、位置和库存。仅仅发送数字意义不大。相反,可以快速解析像“a:10”这样的字符串,以查找冒号后面的数字和冒号前面的字符,从而确定必要的更改。
在循环之外,线程有一个方法将缓冲区上的消息发送到另一个设备。它是不言自明的,并按原样发送消息。
在这些线程之前,添加一些用于发送数据和调用线程来执行某些操作的方法。请记住,您还没有以任何方式初始化或利用线程。清单 7-9 中的代码启动了它们。
清单 7-9。连接到蓝牙设备
`public synchronized void start() {
if (D) Log.d(TAG, "start");
// Cancel any thread attempting to make a connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// Start the thread to listen on a BluetoothServerSocket
if (mAcceptThread == null) {
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}
setState(STATE_LISTEN);
}
public synchronized void connect(BluetoothDevice device) {
if (D) Log.d(TAG, "connect to: " + device);
// Cancel any thread attempting to make a connection
if (mState == STATE_CONNECTING) {
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// Start the thread to connect with the given device
mConnectThread = new ConnectThread(device);
mConnectThread.start();
setState(STATE_CONNECTING);
}
public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
if (D) Log.d(TAG, "connected");
// Cancel the thread that completed the connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// Cancel the accept thread because you only want to connect to one device
if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}
// Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg);
setState(STATE_CONNECTED);
}
public synchronized void stop() {
if (D) Log.d(TAG, "stop");
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}
setState(STATE_NONE);
}
public void write(byte[] out) {
// Create temporary object
ConnectedThread r;
// Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED) return;
r = mConnectedThread;
}
// Perform the write unsynchronized
r.write(out);
}
private void connectionFailed() {
setState(STATE_LISTEN);
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "Unable to connect device");
msg.setData(bundle);
mHandler.sendMessage(msg);
}
private void connectionLost() {
setState(STATE_LISTEN);
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(MainActivity.TOAST, "Device connection was lost");
msg.setData(bundle);
mHandler.sendMessage(msg);
}`
一旦你看到了线程,你就会明白这些函数主要用来启动线程。前三个函数启动三个线程(AcceptThread
、ConnectThread
和ConnectedThread
)。当你的游戏遇到终点(也就是角色死亡)时,会调用stop()
函数来确保没有线程继续。当你想发送一些东西到另一个设备时,你也可以使用write()
方法。
最后,另外两种方法使用Handler
在连接丢失或失败时显示消息。
为两个玩家改编游戏代码
您已经完成了处理建立连接并维护它们的大部分代码。现在你需要弄清楚你的游戏将如何使用蓝牙。这个示例游戏的整个代码太大了,不适合本书的篇幅,但是你可以从 http://code.google.com/p/android-tablet-games/下载。一个完整的其他源文件处理你如何选择你想要连接的设备(这对你现在的工作并不重要)。
事不宜迟,你想在游戏过程中在屏幕上有两个球拍:一个在顶部,一个在底部。清单 7-10 包含了来自GameView
的update()
方法的重要代码。请注意,您必须在前面的函数中初始化paddle_other
精灵,并将其添加到draw()
函数中。它被放置在屏幕的顶部,与另一个球拍的图像相同。
清单 7-10。增加了球拍和碰撞检测,并更新了游戏状态
`//paddle input
int val=0;
for (int i=latest_input.length-1, j = 0; i >= 0; i--,j++)
{
val += (latest_input[i] & 0xff) << (8*j);
}
paddle_other.setX(val);
//paddle_other collision
int paddle_other_bottom = paddle_other.getBitmap().getHeight();
if(paddle_other.collide(ball)){
if(ball_y < paddle_other_bottom && ball_y < paddle_other_bottom + 20){
ball.setMoveY(-ball.getMoveY());
}
}
//paddle output
byte[] paddle_output;
ByteBuffer bb = ByteBuffer.allocate(4);
bb.putInt((int)paddle.getX());
paddle_output = bb.array();
write(paddle_output);`
清单 7-10 中的代码做了三件事。首先,它根据控制它的其他设备的输入将paddle_other
移动到该位置。第二,它检测碰撞。第三,它将你控制的球拍的位置发送到另一个设备,这样你的对手就可以看到你最近的移动。
稍微分解一下,for
循环将您作为输入获得的字节数组转换成一个整数,用于移动拨片。幸运的是,您还不需要将byte[]
解析成更复杂的值。
碰撞检测与另一个球拍的碰撞检测类似,但是您颠倒了检测,因为您只对球撞击底部感兴趣,而不是顶部。如果你愿意,当球碰到顶部时,你可以使游戏重置或结束,给玩家 2 同样的强度。
最后,您将 paddle 的位置转换为一个字节数组,并将其发送到您的write()
函数,该函数又将它发送到connectedThread
进行处理。
测试游戏
测试一个使用蓝牙的多人游戏应用可能有点棘手。如果你有两个 Android 平板电脑,那么你可以利用它们的内置功能相互连接。然后将程序加载到两台设备上。如果你没有或者不想要几个平板,你必须做不同的安排。
显然,测试这些程序的另一种可能方式是借用别人的平板电脑,并将其与自己的平板电脑配对。请注意,要在另一台平板电脑上安装软件,您需要遵循附录 A 中针对所有平板电脑的说明。在你开始实验之前,确保你的朋友或亲戚明白你在对他们的平板电脑做什么!
将蓝牙 USB 加密狗插入您的计算机并期望您的仿真器能够处理蓝牙可能很有诱惑力。可悲的是,事实并非如此;模拟器目前不具备处理蓝牙的能力。在添加此功能之前,您必须使用真实设备进行测试。
总结
再次祝贺你:你在 Android 游戏开发的一些有趣的蓝牙和多人游戏方面取得了成功。这个话题是你在游戏编程中会遇到的最困难的话题之一。现在你已经准备好在这本书结尾的大型游戏上工作了。准备好接受更多的精灵和声音,以及更多的代码。
八、单人策略游戏第一部分:构建游戏
是时候做你的最后一个游戏了,一个单人策略游戏——Harbor Defender——在这个游戏中,你使用了你在前面章节中开发的概念和代码。大多数内容都是我们已经学过的东西。你利用你已经知道的东西。一些游戏开发书籍喜欢以华丽的 3D 游戏结尾。我选择不走这条路,因为没有足够的时间来教你添加第三维度的所有细微差别。编写一个 3D 游戏并不容易:当你在智能手机或平板电脑上玩一个游戏时,你可以相当肯定它是由一个大型团队创作的。我在这本书里的目标是教你如何创建你可以自己编程的游戏。这样,您不必与任何人分享您的利润,也不必与其他开发人员争论您的设计和实现决策!
在你构建的战略游戏中,用户必须保卫一个堡垒,防止敌人从海上攻击。游戏的设计允许你通过增加新类型的防御和增加敌人的数量来增加它的难度。还可以添加布局来创建更具挑战性的游戏关卡。
在这一章,两章中的第一章,你的重点是设置游戏和它的元素,并创建一个系统,使一切顺利运行。在下一章中,你将通过实现一个点计数器和一些有趣的用户控件来使游戏更加精彩。
注意因为你在为平板电脑开发游戏,你需要记住让它的开发不同于手机或桌面游戏的方面。这些差异包括使用触摸屏、应对屏幕尺寸以及设计直观的用户控件。一些不熟悉平板电脑的开发人员很想把他们以前的项目移植到平板电脑上。这可以很好地工作,但浏览一下 app store 会让你相信,那里的大多数游戏都是为平板电脑定制的,不能在任何其他硬件上很好地工作。通常情况下,用户只是在他们的游戏系统上玩原版游戏,他们希望他们的平板电脑体验有一个特殊的游戏。
让我们先来看看战略游戏的布局,然后组装构建它所需的元素。
介绍港湾卫士
港口保卫者是我为你在本章开始的游戏选择的名字。游戏表面由一个堡垒,一个由码头定义的港口,攻击船和可以用子弹击沉攻击者的大炮组成。图 8-1 是你在本章结束时组装的游戏面的图像。在第九章中,你添加了用户控件,但是现在你需要一个玩家最终会与之交互的界面。它让你知道机械是如何工作的。
图 8-1。港湾保卫者的测试版
港口守卫者的目标是在船只入侵要塞之前,摧毁通过港口接近要塞的船只。玩家通过发射位于港口周围的码头上的大炮来击退船只。每个桥墩都可以容纳一门大炮,但是使用者必须将它瞄准正确的方向。为了让游戏更具挑战性,用户不能制造无限数量的大炮。相反,用户得到的加农炮数量是有限的,因此他们必须明智地选择加农炮的位置。如前所述,在这一章你设置游戏环境;在下一章中,您将添加用户交互。
你可以让船以更快的速度靠近,为了最大化它们的效率,用户必须快速删除和移动加农炮。现在,让我们来看看为了使这个游戏成功,你必须创建的物品和活动。
集结港湾卫士
这里是一个港口防御者需要什么的细目分类。在本节中,您将探索这些元素以及如何处理它们:
- Wharf: The stones of the wharf support your cannon and define the port through which the invading ships must pass. The pier itself doesn't do anything, but it is used for reference cannon placement. You use XML data to quickly encode the location of each item. Each part is realized as an elf; Sprite objects give you more functions than just displaying images on the screen.
- Ground: The ground is part of the background, so you don't test it or use it. However, it is important for you to use it, because when the blue background is enough, it makes you unnecessary to use larger and resource-intensive images.
- Castle: The castle will not react until it is hit by a boat. Otherwise, it is an immovable object that is relatively easy to implement. Similarly, you can choose to put the ground and the castle in an elf, but you use this method because it makes more sense in the game by limiting the size of the image.
- Ship: The ship is one of only two mobile elves in the whole game. You create them according to a random number generator, adding some unpredictability to the game. You have to set their route and speed in advance. Bullet is another moving elf that you dealt with in Chapter 9.
- Artillery: A cannon has a simple function, that is, it fires at ships. Their location is unique because players can make and destroy cannons in the game. Similarly, the function of the cannon will be realized in the next chapter.
这个游戏编码中最有趣的部分是船只和大炮没有固定的位置和数量。这意味着不是所有的精灵都像你习惯的那样在开始时被初始化。
在开始构建游戏环境之前,您需要打开一个新的 Eclipse 项目:
- Open a new Eclipse project named harbor defender .
- Copy all the files of PaddleGame (see Chapter 7 ) into your new project. This includes art, XML files and, of course, code.
建造桥墩
在您的上一个游戏中,您使用了一个 XML 页面来存储方块的位置。您可以重用这个页面来存储大量的码头坐标。因为你有这么多的码头,有些人会说一个循环可以处理快速排列的碎片。这是真的,但是桥墩的不规则形状适合这种手工编码。另外,请记住,如果您创建了另一个级别,更改这些数据是非常容易的。
请遵循以下步骤:
清单 8-1 显示了文件
blocklocation.xml
(与你用于 TabletPaddle 的文件完全相同)的内容,但是它包含了所有墩块的位置,而不是 Paddle 游戏中的方块。将该文件的内容添加到位于res
下的blocklocation.xml
中。我强烈建议从网站([
code.google.com/p/android-tablet-games/](http://code.google.com/p/android-tablet-games/)
)下载这个文件,而不是输入这些代码。清单 8-1。码头平台位置
`
<integer name="blocknumber">32<integer-array name="x">
- 180
- 280
- 380
- 480
- 580
- 680
- 780
- 880
- 980
- 1080
- 1180
- 1080
- 1180
- 380
- 480
- 580
- 680
- 780
- 1080
- 1180
- 680
- 780
- 1080
- 1180
- 680
- 780
- 1080
- 1180
- 680
- 780
- 1080
- 1180
<integer-array name="y">
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 100
- 100
- 200
- 200
- 200
- 200
- 200
- 200
- 200
- 300
- 300
- 300
- 300
- 400
- 400
- 400
- 400
- 500
- 500
- 500
- 500
`
你用和以前一样的技巧解析这个文件。第一个项目列表是 x 坐标;y 坐标在第二个列表中。通过将 x 列表中的第一个条目与 y 列表中的第一个条目配对来创建每个 sprite,然后向下移动,直到创建了每个块。请注意,您必须在 XML 文件的顶部键入总块数——在本例中,您有 32 个桥墩。这个游戏需要一些新的精灵对象、整数和数组。在实现它们之前,您需要将它们添加到
GameView
类的顶部。清单 8-2 包含了新的声明;将它们放在文件的顶部。清单 8-2。初始化项目的对象/变量
`//SpriteObjects
private SpriteObject[] pier;
private SpriteObject[] cannon;
private SpriteObject ground;
private SpriteObject castle;
private SpriteObject[] boat;//Variables
private Resources res;private int[] x_coords;
private int[] y_coords;
private int boat_count = 0;
private int cannon_count = 3;
private int pier_count;`Although these elves and integers look similar to those you created before, it should be noted that
boat_count
is set to 0. This allows you to start the game without any boats, and add boats during the game. Similarly, you setcannon_count
to 3, because initially you only handled three cannons.将清单 8-3 中的代码添加到
GameView
构造函数中。这段代码看起来应该非常像 TabletPaddle 代码;除了您正在创建的对象的名称之外,它们是相同的。然后,在onDraw()
函数中,循环遍历每个桥墩,并将其绘制到屏幕上。清单 8-3。建造码头
//pier sprites pier_count = res.getInteger(R.integer.blocknumber); x_coords = res.getIntArray(R.array.x); y_coords = res.getIntArray(R.array.y); pier = new SpriteObject[pier_count]; for(int i = 0; i < pier_count; i++){ pier[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.pier), x_coords[i], y_coords[i]); }
将清单 8-4 中的代码放到
GameView
的onDraw()
方法中。??清单 8-4。绘制桥墩
for(int i = 0; i < pier_count; i++){ pier[i].draw(canvas); }
因为 pier 块不需要做任何事情,所以您不需要在update()
函数中为它们创建代码。让我们继续到地面和城堡。
添加地面和城堡
地面和城堡是更无生命的物体。你照顾他们就像你照顾码头一样。幸运的是,每种数据只有一个,这意味着您不需要使用更多的 XML 数据:
清单 8-5 显示了您在
GameView
构造函数中为两个精灵使用的代码。现在添加。清单 8-5。创建地面和城堡
ground = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.ground), 480, 500);
castle = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.castle), 890, 500);
这两个精灵的诀窍是确保他们都在正确的地方。它们的绘制顺序也很重要。图 8-2 显示了调用
onDraw()
函数时发生的情况。您可以看到正在绘制的图像层。图 8-2。图像图层
The ground must be lower than the pier and higher than the blue background. Similarly, the castle must be on the ground. In order to get the correct sequence, Listing 8-6 contains a new
onDraw()
routine. Make sure the order is correct: if the ground appears above the castle, then you will have an underground fortress that is not easy to use in the game!
清单 8-6。绘制城堡和地面
`canvas.drawColor(Color.BLUE);
ground.draw(canvas);
castle.draw(canvas);
for(int i = 0; i < pier_count; i++){
pier[i].draw(canvas);
}`
您创建的下一个精灵对象将在绘制完桥墩后添加。这是有道理的,因为大炮必须在桥墩的顶部;船只沿着水面滑行,可能会撞到城堡。
制造船只
船是你必须对付的最复杂的精灵。用户无法控制它们,因此它们的运动必须按照特定的路线进行预编程。更复杂的是,你必须根据精灵的方向改变它的图像。这些都集中在update()
函数中。但是现在,您可以创建一个数组来保存船只,而不必实际制作它们:
将清单 8-7 中的片段放到你的
GameView
构造函数方法中。??清单 8-7。创造 12 个船精灵
//boat sprites boat = new SpriteObject[12];
清单 8-8 显示了循环绘制可用船只的程序。在绘制完其他精灵后,将这段代码放入
onDraw()
函数中。清单 8-8。画船
for(int i = 0; i < boat_count; i++){ boat[i].draw(canvas); }
Here comes the wonderful part. Before you go forward, you need to understand the variable
boat_count
. Back to theGameView
variable declaration, you initialize this integer by setting it to 0. Therefore, in the initial state, no ship spirit is drawn, becausei
is not less thanboat_count
. You can think of ?? as a collection of available ships.因为你一开始没有船,所以他们的创作方法就有点复杂了。清单 8-9 包含了你需要添加到
update()
函数中的代码。之后我把它分解成关键部分。为了让它工作,在GameView.java
的顶部导入java.util,Random
。??清单 8-9。创建船只和随机间隔
`Random random_boat = new Random();
int check_boat = random_boat.nextInt(100);if(check_boat > 97 && boat_count < 12){
int previous_boat = boat_count - 1;
if(boat_count == 0 || boat[previous_boat].getX() > 150){
boat[boat_count] = new
SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.boat), 100, 150);
boat[boat_count].setMoveX(3);
boat_count++;
}
}`首先,你做一个随机数生成器。您调用一个
nextInt()
方法,该方法在 0 和参数之间选择一个整数。测试了check_boat
变量,以便您以随机间隔创建船只。
注意创建一个随机数发生器,并得到一个介于零和你自己的值之间的整数,这是给你的游戏增加一些随机性的完美方法。你不再需要担心小数,因为整数更容易处理。如果你使用随机元素,请记住在测试中多次运行你的游戏,因为如果随机数与你预期的不同,你可能会发现意想不到的行为第一个
if
语句只有在随机数大于 97 的情况下才继续执行,这种情况不太可能发生,但可以将船只的冲击降到最低。然后你要求boat_count
小于 12。这可以防止许多船只同时出现在赛场上。如果这对玩家来说太容易了,你可以增加这个数字,让游戏更有挑战性。The second
if
statement checks whether the new ship is the first ship or whether it has a certain distance from the previous ship. Therefore, add 1 toboat_count
and test whether the X coordinate of the previous ship is greater than 150. Otherwise, these boats may appear on top of each other, which is detrimental to the appearance of the game (although it may make the game more challenging! ).If the ship passes all the
if
statements, it is initialized to the starting x position 100. You move it at a slow speed of three pixels perupdate()
function. This is another good opportunity to increase the difficulty. When the player reaches a certain score or other achievements, slowly increase the speed of the boat.最后,
boat_count
递增,让draw()
函数处理新添加的船。你的舰队扩大了。你需要改变船只的方向,让它们能够适当地转向它们的目的地:城堡。清单 8-10 中的代码就是这样做的;将其添加到update()
方法中。??清单 8-10。改变船的方向
for(int i = 0; i < boat_count; i++){ if((int)boat[i].getX() > 950){ boat[i].setMoveX(0); boat[i].setMoveY(3); boat[i].setBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.boatdown)); } }
When the ship reaches the X position of 950 pixels, it stops moving to the right and begins to descend. Notice the last line: you changed the sprite image, because ships rarely move without changing direction. Therefore, the original ship image is rotated by 90 degrees and saved as a new resource named
boatdown
.
就是这样。当你添加加农炮时,你会看到船只随机出现并驶向你的城堡。
加农炮
对船来说也是如此,大炮的数量在游戏中会发生变化。现在,你只担心证明这个概念。请遵循以下步骤:
将清单 8-11 中的代码放入
GameView
创建器中。你可以在声明中改变cannon_count
的值来创建更多的加农炮。不是响应用户输入,而是让加农炮出现在三个连续的墩上,每次快速循环移动加农炮 100 个单位。清单 8-11。改变加农炮计数的数值
//cannon sprites cannon = new SpriteObject[cannon_count]; for(int i = 0; i < cannon_count; i++){ cannon[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonup), (580 + i * 100), 200); }
In order to prepare yourself for creating additional cannon elves, name the original picture
cannonup
. When the user wants to change the direction of the cannon, it will be easier.Add the code in Listing 8-12 to the
onDraw()
function, and your cannonball will appear when the game is running.
清单 8-12。拉炮
for(int i = 0; i < cannon_count; i++){ cannon[i].draw(canvas); }
你有一些简单的遗留问题要处理。游戏的框架完成了。
添加图像
我使用的图片可以在code.google.com/p/android-tablet-games/
买到,或者你可以自己创作。图 8-3 到图 8-7 显示了我用来构建 Harbor Defender 的图片;它们的尺寸在图标题中有规定。后来,我提出了一些关于如何创建你的图像的建议。请记住,有时您需要旋转或翻转它们,以交替状态显示它们。
图 8-3。 Castle
: 200 × 100
图 8-4。 Boat
和boatdown
:分别为 50 × 30 和 30×50
图 8-5。 Ground
: 800 × 250
图 8-6。 Pier
: 100 × 100
图 8-7。 Cannon
: 100 × 100
调试海港卫士
通过一个简单的游戏外观方法,你就可以开始尝试了。像任何游戏一样加载它,你会看到船只慢慢出现并向城堡游去。如果你等得够久,他们会穿过城堡,离开屏幕。
如果事情不是这样,或者你得到一个类似于图 8-8 中显示的错误,或者游戏在启动时关闭,那么你需要做一些工作。本节致力于解决 Android 游戏开发中的常见问题。它没有深入到具体的问题中,因为没有办法预测每一个错误。如果 Eclipse 捕捉到错误,如何修复代码中的错误应该是相当明显的,但是运行时问题可能更加困难。
以下是要使用的流程:
Make sure you are using
LogCat
to get the information on the simulator. When you useLog.d
in the program to remind you that certain events have been triggered, this is very important.LogCat
A fairly detailed report on errors is also displayed.出错时不要关闭模拟器。查看图 8-2 中的问题。你可能很想立即平仓,但这样做会抹去
LogCat
的结果。相反,请等待,以便您可以诊断问题。图 8-8。海港保卫者运行时出错
向上滚动
LogCat
读数,如图图 8-9 所示,你应该可以找到红色字体的短语,表示错误发生的位置。幸运的是,该错误指出了问题所在的确切行号。图 8-9 .??
NullPointerException
??In most cases, you only need to pay attention to the previous error line. In this case, when the cannon is pulled out, the
onDraw()
function fails. The reason is that I commented out the initialization of the cannon elf. This is a common problem when you deal with a game where elves are created and destroyed. Please make sure that all the wizards you refer to for drawing or updating really exist. The last suggestion for handling errors is to make your simulator smaller. If you have a relatively small screen size, then your simulator may occupy most of the screen. This can prevent you from looking atLogCat
at work. To solve this problem, choose to run Run_configuration. Then go to the target tab and scroll down. In the command line option, type scale.8 . This reduces the simulator to 80% of its original size.
注意如果你解决问题的最大努力没有成功,尝试在 stack overflow(
stackoverflow.com/
)上搜索解决方案。不过,将来在测试之间做些小的改变。通过这种方式,您可以回到以前的工作状态。准备好总是回到你知道有效的事情上来。
下一章涉及到游戏的许多不同的修正和更新。最值得注意的是,你使用户能够移动和旋转大炮。在你以前的游戏中,玩家从来没有这么多的选择,这将是一个独特的练习。
另一个增加的是一个积分系统,玩家每摧毁一艘船就获得奖励。物理也必须更新,因为一旦船撞上城堡,你需要结束游戏,而不是让船直接通过。
你还得担心一个新的因素:不恰当的用户交互。对于用户来说,点击码头的一部分来放置大炮是有意义的,但是如果他们错过了码头而点击了海洋呢?这就要求你快速有效地评估每一个输入,并立即对用户做出反应,同时还要防止大炮出现在不该出现的地方。
为了完成你的工作,你添加输入和逻辑来润色游戏的整体外观。
总结
你已经经历了设置一个真实游戏的过程。有了这些元素,你就可以添加让游戏成为有趣的用户体验的特性了。你应该习惯于计划一个游戏和组织处理精灵和组成它的物体的方式。
当你展望未来时,你会更加关注玩家的体验,而不是你技术能力的极限。为一个游戏创作美术作品通常也是一个限制因素,但是一个有趣和有创意的游戏可以弥补许多缺点。现在,让我们为您的游戏部署做好准备。
九、单人策略游戏第二部分:游戏编程
有了框架,现在你可以编写代码来创建一个可玩的游戏。这里的诀窍是让你的代码尽可能高效。当游戏变得越来越复杂,涉及到更多的精灵时,它们可能会因为处理器难以跟上而开始变慢。你可以用一些巧妙的技术来减轻负担,从而避免这种情况。
随着你的进展,牢记最终目标也很重要,因为你必须有一个正常运行的游戏,然后才能添加使你的工作与众不同的附加功能。事实上,以我的经验来看,知道何时停止开发一款游戏并发布它总是最棘手的部分。太简单的游戏和不可玩的游戏之间有一条细微的界限,因为它的功能和附加功能太多了,普通用户没有时间去学习。
注意当你阅读本章中的代码时,回想一下,将
Log.d
语句放入代码中有助于澄清正在发生的事情和正在调用的函数。有些代码可能相当复杂,我仍然使用这种技术来帮助我逐步完成这些方法,尤其是当我没有得到想要的行为时。
以下是你必须在本章中完成的功能列表,以便拥有一个可用的游戏:
- Augmented elf object
- Shoot a bullet from a cannon
- Destroy the hit ship.
- Restart the game when the ship hits the castle.
其中一些——比如当子弹击中一艘船时降低它的健康——很容易完成,但是其他的需要一些思考和聪明的编码。为了简化你的编辑,我已经贴出了这一章的全部方法。这样可以保证你之前的作品正是最终游戏所需要的。这也有助于您了解每个函数如何调用其他函数,以及它们之间共享哪些信息。
下一节从我们对SpriteObject.java
的改进开始。您只做了很少的修改,但是您所做的更改将会简化您在GameView.java
中的工作。
增强游戏精灵
在这个游戏中,你对你的精灵要求很多。为了处理新功能,您需要一些所有精灵都使用的新方法和变量。虽然实际上只有一个精灵可以利用一个特定的特性,而不是创建额外的类,但是你可以让每个游戏精灵从SpriteObject
继承,因为精灵在很大程度上是相同的——没有必要混淆项目。
然而,如果你扩展游戏,你想让船能够开火还击,改变方向,或者产生更小的船,那么你可能想创建一个特殊的船类来体现这些能力。每当一个精灵或对象使用两个或更多独特的函数时,我通常会为它创建一个新类。
按照以下步骤修改SpriteObject.java
:
1.清单 9-1 显示了要添加的新变量以及你赋予它们的值。将这段代码添加到SpriteObject.java
的顶部。
清单 9-1。 SpriteObject
变数
private int health = 3; private int Orientation = -1; public int LEFT = 0; public int RIGHT = 1; public int UP = 2; public int DOWN = 3; private boolean stack = false;
2.清单 9-1 中变量的使用在清单 9-2 中所示的函数中显而易见。在SpriteObject
的末尾键入所有这些代码。新方法被你的精灵们自由地使用。
清单 9-2。新增功能为SpriteObject
`public boolean cursor_selection(int cursor_x, int cursor_y){
int sprite_right = (int)(getBitmap().getWidth() + getX());
int sprite_bottom = (int)(getBitmap().getHeight() + getY());
if(cursor_x > getX() && cursor_x < sprite_right && cursor_y > getY() && cursor_y <
sprite_bottom){
return true;
}
else{
return false;
}
}
public void setStacked(boolean s){
stack = s;
}
public boolean getStacked(){
return stack;
}
public void diminishHealth(int m){
health -= m;
}
public int getHealth(){
return health;
}
public void setOrientation(int o){
Orientation = o;
}
public int getOrientation(){
return Orientation;
}`
cursor_selection()
函数是一个非常强大的方法,如果用户触摸了一个 sprite,它将返回 true,如果用户没有触摸,它将保持 false。它基本上是一个简单版本的collide()
方法,但是它只关心用户给出的输入。您通过用户选择要添加的加农炮类型来实现它。
与子画面是否堆叠相关的函数用于确定一块桥墩上是否已经有大炮。如果那里有一门加农炮,你要阻止用户在它上面放置另一门。有些地方比其他地方更好,所以让玩家放置大炮是不公平的。
添加两个函数来处理精灵的健康状况。游戏中唯一健康的精灵是船。当他们被击中三次时,他们将被移出游戏。
3.你需要修改SpriteObject
update()
函数来检查一个精灵是否已经失去了所有的健康。用清单 9-3 中的代码替换现有代码。
清单 9-3。改变update()
方法
public void update(int adj_mov) { if(state == ALIVE){ x += x_move; y += y_move; if(health <= 0){ state = DEAD; } } }
最后一个加法检查精灵面向哪个方向。你用这个装大炮。例如,如果一门大炮朝下,你必须向屏幕底部发射子弹,而一门指向右边的大炮应该向屏幕右侧发射子弹。
让我们把这些功能付诸行动吧!
创建用户控件
GameView.java
的构造器方法有几个新人。本节剖析了主要用于用户交互的新精灵,并向您展示了一个新概念。不是创建四个不同的指向所有主要方向的大炮图标,而是为四个不同的精灵旋转一个图像。这可以节省机器上的空间,但也会在启动时导致一些额外的处理器工作。
为了证明这一点,主炮都是不需要旋转的独立精灵。在这种情况下,您使用的方法取决于您的资源和磁盘空间。
请遵循以下步骤:
1.在开始使用新精灵之前,必须先在构造函数之前声明对象。将清单 9-4 中的代码放到GameView.java
中。
清单 9-4。 SpriteObject
s 为海港保卫者
private SpriteObject trash; private SpriteObject dock; private SpriteObject marker; private SpriteObject cannonrightsmall; private SpriteObject cannonleftsmall; private SpriteObject cannonupsmall; private SpriteObject cannondownsmall;
2.在GameView
构造函数中,初始化trash
、dock
和marker
图标,如清单 9-5 所示。这三个精灵创建了用户控件的基础。在屏幕右下角,有一个存放选项的 dock。在码头的前面是垃圾桶,让用户摧毁他们建造的大炮。标记精灵在图标后面跳来跳去,向玩家显示当前选择的是哪一个。
清单 9-5。设置图标
trash = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.trash), 50, 650); dock = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.dock), 0, 650); marker = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.marker), 50, 650);
3.下一步是创建小炮图标。将清单 9-6 中的代码添加到GameView
构造函数中。这是你的码头的基础。
清单 9-6。制作微型大炮图标
`Bitmap bcannonupsmall = BitmapFactory.decodeResource(getResources(),
R.drawable.cannonupsmall);
int w = bcannonupsmall.getWidth();
int h = bcannonupsmall.getHeight();
Matrix mtx = new Matrix();
mtx.postRotate(90);
Bitmap bcannonrightsmall = Bitmap.createBitmap(bcannonupsmall, 0, 0, h, w, mtx, true);
Bitmap bcannondownsmall = Bitmap.createBitmap(bcannonrightsmall, 0, 0, w, h, mtx, true);
Bitmap bcannonleftsmall = Bitmap.createBitmap(bcannondownsmall, 0, 0, h, w, mtx, true);
cannonrightsmall = new SpriteObject(bcannonrightsmall, 110, 650);
cannonleftsmall = new SpriteObject(bcannonleftsmall, 180, 650);
cannondownsmall = new SpriteObject(bcannondownsmall, 240, 650);
cannonupsmall = new SpriteObject(bcannonupsmall, 300, 650);`
如果你觉得这段代码有点像希腊语,不用担心。你创建了微型加农炮精灵,并收集其高度和宽度。然后你启动一个新的矩阵,旋转 90 度。通过旋转cannondownsmall
三次创建三个新位图。然后用新图像创建精灵。位置非常具体,将所有图标放在屏幕左下方的小 dock 上。
4.要使 dock 有用,您需要用变量存储用户的选择(换句话说,如果用户选择正面朝下的大炮,您需要知道如何创建该类型的大炮)。你可以通过将清单 9-7 中的变量放在GameView
的顶部来实现。User_choice
存储用户的选择。
清单 9-7。存储用户选择的变量
Private int TRASH = 1; Private int CANNON_LEFT = 2; Private int CANNON_RIGHT = 3; Private int CANNON_UP = 4; Private int CANNON_DOWN = 5; Private int user_choice;
5.您已经创建了一个不错的 dock,有几个选项供用户选择,但是您需要跟踪用户指向的位置。您使用四个变量来引用用户的选择。将清单 9-8 中的变量添加到GameView.java
的顶部。
清单 9-8。收集关于最后一次触摸事件的位置的数据
private int cursor_x; private int cursor_y; private boolean selection_changed; private boolean addboat;
6.编辑ProcessMotionEvent()
看起来像清单 9-9 中的代码。这包含了您在步骤 5 中声明的前三个变量。
清单 9-9。存储用户的输入
`public void processMotionEvent(InputObject input){
selection_changed = true;
cursor_x = input.x;
cursor_y = input.y;
}`
有了这些代码,当平板电脑上发生触摸时,将selection_changed
设置为true
,并用变量cursor_x
和cursor_y
存储触摸的位置。
7.在update()
函数中,您使用来自步骤 6 的数据来确定是否需要处理用户输入事件以及用户在哪里交互。将清单 9-10 中的代码添加到GameView.java
的update()
方法中。这就是处理用户输入的方式。
清单 9-10。在update()
功能中处理用户输入
`if(selection_changed){
selection_changed = false;
if(trash.cursor_selection(cursor_x, cursor_y)){
user_choice = TRASH;
marker.setX(50);
addboat = false;
}
if(cannonrightsmall.cursor_selection(cursor_x, cursor_y)){
user_choice = CANNON_RIGHT;
marker.setX(110);
addboat = true;
}
if(cannonleftsmall.cursor_selection(cursor_x, cursor_y)){
user_choice = CANNON_LEFT;
marker.setX(180);
addboat = true;
}
if(cannondownsmall.cursor_selection(cursor_x, cursor_y)){
user_choice = CANNON_DOWN;
marker.setX(240);
addboat = true;
}
if(cannonupsmall.cursor_selection(cursor_x, cursor_y)){
user_choice = CANNON_UP;
marker.setX(300);
addboat = true;
}
else if(addboat){
if(cannon_count < 10){
for(int i = 0; i < pier_count; i++){
if(pier[i].cursor_selection(cursor_x, cursor_y)){
if(pier[i].getStacked() == false){
switch(user_choice){
case 2:
cannon[cannon_count] = new
SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonleft),
(int)pier[i].getX(), (int)pier[i].getY());
cannon[cannon_count].setOrientation(cannon[cannon_count].LEFT);
break;
case 3:
cannon[cannon_count] = new
SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonright),
(int)pier[i].getX(), (int)pier[i].getY());
cannon[cannon_count].setOrientation(cannon[cannon_count].RIGHT);
break;
case 4:
cannon[cannon_count] = new
SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannonup),
(int)pier[i].getX(), (int)pier[i].getY());
cannon[cannon_count].setOrientation(cannon[cannon_count].UP);
break;
case 5:
cannon[cannon_count] = new
SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.cannondown),
(int)pier[i].getX(), (int)pier[i].getY());
cannon[cannon_count].setOrientation(cannon[cannon_count].DOWN);
break;
}
cannon_count++;
pier[i].setStacked(true);
}
else if(pier[i].getStacked() == true){
if(user_choice == 1){
for(int u = 0; u < cannon_count; u++){
if(cannon[u].getX() ==
pier[i].getX() && cannon[u].getY() == pier[i].getY()){
cannon[u].setstate(cannon[u].DEAD);
}
}
}
}
}
}
}
}
}`
这段代码处理的是 dock 图标。用户交互的另一面是船在屏幕上的实际位置。当玩家选择任何一艘船或垃圾桶时,他们设置addboat
到true
。这意味着你需要寻找用户在游戏中做什么。变量user_choice
存储用户选择的最后一个停靠图标。
处理器循环通过墩件;当它发现用户触摸了墩块时就停止。然后,它会询问码头是否堆叠。你早些时候看到,在这种情况下,被堆叠意味着码头已经容纳了一门大炮。如果不是,那么用户可以自由添加一门大炮到那个码头。然后代码进入一个switch
语句。
switch
的事例编号对应于您在构造函数方法中分配的变量(例如,大炮是否指向左边)。当你找到玩家想要的加农炮的方向时,你使用码头的位置创建新的精灵。很重要的一点是,你的码头和大炮要占同一个面积(100 × 100)。这使得定位变得很简单。
然而,放置大炮并不是玩家唯一能做的事情。他们还可以选择值为 1 的垃圾桶。垃圾的表现与你之前看到的相反:它寻找一个堆放的墩块,找到放在那里的大炮,然后移走它。
就这样。用户现在可以控制你的游戏了。接下来的部分将为你的子弹和船只添加新的功能。
把一切都显示在屏幕上
现在你已经有了很多很棒的特性,比如你的用户界面控件和船只,你需要把它们添加到屏幕上。为此,onDraw()
功能需要调整。清单 9-11 包含了这个函数的全部代码。
确保你的 onDraw 函数看起来与清单 9-11 中的完全一样,否则图像不会被绘制到屏幕上。
清单 9-11。??onDraw()
`@Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLUE);
ground.draw(canvas);
//the user controls
dock.draw(canvas);
marker.draw(canvas);
trash.draw(canvas);
cannonleftsmall.draw(canvas);
cannonrightsmall.draw(canvas);
cannondownsmall.draw(canvas);
cannonupsmall.draw(canvas);
for(int i = 0; i < pier_count; i++){
pier[i].draw(canvas);
}
for(int i = 0; i < boat_count; i++){
boat[i].draw(canvas);
}
for(int i = 0; i < cannon_count; i++){
cannon[i].draw(canvas);
}
for(int i = 0; i < 50; i++){
bullets[i].draw(canvas);
}
castle.draw(canvas);
}`
查看标题为“用户控件”的精灵组。这些图标包括用户可以选择的 dock、标记以及垃圾桶和大炮图标。这里需要注意的是,dock 显然是先画的,然后是标记,然后是图标。这样就可以一直在背景中看到 dock。然后,标记可以自由地从后面高亮显示所有图标。图 9-1 显示了码头的样子。
图 9-1。包含用户控件的 dock,用户可以与之交互
在函数的最后,四个for
循环遍历精灵列表。最后画出城堡。
你总是画出每一颗子弹,即使它们可能正在移动,也可能不在移动。这是由SpriteObject
类负责的,它在绘制精灵之前检查以确保精灵是活的。随着子弹准备摧毁船只,我们必须创造和跟踪即将到来的敌人。下一节将介绍操纵船只的来龙去脉。
部署和管理攻击艇
清单 9-12 包含了处理船只的整个GameView.java
update()
方法的代码..如果你不明白它的一部分,键入它的全部并运行游戏。你可以根据游戏的行为来看它是如何工作的。
1.确保您的 update()方法包含这里的所有代码。上市后,你会发现它的解释。
清单 9-12。在update()
功能中设置船只
`public void update(int adj_mov) {
for(int i = 0; i < boat_count; i++){
if((int)boat[i].getX() > 950){
boat[i].setMoveX(0);
boat[i].setMoveY(3);
boat[i].setBitmap(BitmapFactory.decodeResource(getResources(),
R.drawable.boatdown));
}
}
Random random_boat = new Random();
int check_boat = random_boat.nextInt(100);
if(check_boat > 97 && boat_count < 12){
int previous_boat = boat_count - 1;
if(boat_count == 0 || boat[previous_boat].getX() > 150){
boat[boat_count] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.boat), 100, 150);
boat[boat_count].setMoveX(3);
boat_count++;
}
}`
来自清单 9-12 的代码在第八章中完成..第一个for
循环确定船只是否向右移动过多。如果有,那么使用一个新的精灵图像,它开始沿着屏幕向城堡移动。
下一个块处理随机船的创建。最重要的部分是使用一个if
语句来确保前一艘船与新船充分分离。同样,你增加船只的数量,并设置新船上路,如清单 9-12 所示。
现在我们将检查与城堡的碰撞,这将导致玩家的损失。
2.在 update()方法中添加清单 9-13 中的 for 循环。
清单 9-13。测试与城堡的碰撞,重置游戏
for(int i = 0; i < boat_count; i++){ if(boat[i].collide(castle)){ reset(); } }
如果用户失败,船撞上了城堡,那么你调用一个名为reset()
的新函数。你看一下这个简单的函数做了什么。(我本可以在这里包含所有的代码,但是我发现添加额外的函数来处理不同的任务在视觉上更容易。)
随着船只的航行和子弹的准备发射,我们需要研究我们的大炮。没有它们你无法打败船。看看下一节,我们如何操纵和使用加农炮。
开炮
在用户输入之后,子弹是游戏中最复杂的部分。跟踪 50 个可以向四个不同方向移动的精灵是一件棘手的事情,这些精灵目前可能活着,也可能不活着。大炮将会变得更加精彩。在本节中,您将添加子弹并编写代码来处理大炮如何以及何时发射炮弹。
请遵循以下步骤:
1.将清单 9-14 中的代码添加到GameView
构造函数中。这个代码处理大炮发射的新子弹。为了简单起见,屏幕上的项目符号数量限制为 50 个。有两个数组:一个包含子弹精灵(bullets[]
),另一个包含当前没有使用的子弹列表(available_bullet[]
)。
清单 9-14。Additions to the onCreate() method that handle the bullets.
`available_bullet = new int[50];
for(int i = 0; i < 50; i++){
available_bullet[i] = i;
}
bullets = new SpriteObject[50];
for(int i = 0; i < 50; i++){
bullets[i] = new SpriteObject(BitmapFactory.decodeResource(getResources(), R.drawable.bullet), 10, 10);
bullets[i].setState(bullets[i].DEAD);
}`
您声明了一个整数数组,其中包含所有可用的子弹,因为您知道还没有子弹射出。项目符号精灵也被初始化。您将它们的状态设置为DEAD
,因为您不希望子弹没有发射就出现。
2.将清单 9-15 中的代码添加到update()
方法中。首先,你设置available_bullet
数组等于零;这将使你进行计算时更容易。然后你创建了一个非常重要的变量:g = 0
。g
用于指定哪些项目符号可用,哪些不可用。
清单 9-15。重置可用子弹列表
`for(int f = 0; f < 50; f++){
available_bullet[f] = 0;
}
int g = 0;`
3.在清空数组后,立即将清单 9-16 中的代码放到update()
方法中。
清单 9-16。处理子弹的变化
`for(int i = 0; i < 50; i++){
if(bullets[i].getY() > 800 || bullets[i].getX() > 1280 || bullets[i].getY() < 0 || bullets[i].getX() < 0){
bullets[i].setstate(bullets[i].DEAD);
}
for(int b = 0; b < boat_count; b++){
if(bullets[i].collide(boat[b])){
boat[b].diminishHealth(1);
bullets[i].setstate(bullets[i].DEAD);
}
}
bullets[i].update(adj_mov);
if(bullets[i].getstate() == bullets[i].DEAD){
available_bullet[g] = i;
g++;
}
}`
每个子弹精灵都有一个循环。第一个if
语句检查子弹是否已经离开屏幕;如果有,就将其状态设置为DEAD
。这意味着它可以在下一次迭代中作为可用的项目符号被重用。一个for
回路处理船只碰撞。如果船被击中,那么它的健康就会下降一,你就摧毁了子弹。同样,子弹现在可以重复使用。一个简单的update()
调用根据它的moveX
和moveY
改变了子弹的位置。
如果子弹是死的,那么你把它列为可用子弹。如果仔细观察if
语句,您会注意到第一个失效的项目符号被赋予了available_bullet
数组中的第一个位置,g
被递增,下一个失效的项目符号被赋予了下一个位置。
4.子弹准备好了,该担心发射机制了。五十次迭代的update()
函数从比赛场上的每一门大炮中释放一颗子弹。清单 9-17 中的代码通过调用新函数createBullet()
来执行这些操作,这个函数有四个参数。将这段代码放在已经添加到方法中的代码之后的update()
方法中。
清单 9-17。计算何时发射一排子弹
shooting_counter++; if(shooting_counter >= 50){ shooting_counter = 0; int round = 0; for(int i = 0; i < cannon_count; i++){ if(cannon[i].getOrientation() == cannon[i].LEFT){ int x = (int)(cannon[i].getX()); int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight()/2); createBullet(x,y,cannon[i].LEFT, round); round++; } if(cannon[i].getOrientation() == cannon[i].RIGHT){ int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth()); int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight()/2); createBullet(x,y,cannon[i].RIGHT, round); round++; } if(cannon[i].getOrientation() == cannon[i].UP){ int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth()/2); int y = (int)(cannon[i].getY()); createBullet(x,y,cannon[i].UP, round); round++; } if(cannon[i].getOrientation() == cannon[i].DOWN){ int x = (int)(cannon[i].getX() + cannon[i].getBitmap().getWidth()/2); int y = (int)(cannon[i].getY() + cannon[i].getBitmap().getHeight()); createBullet(x,y,cannon[i].DOWN, round);
round++; } } }
这段代码创建了变量round
,它跟踪哪颗子弹已经发射。第一门大炮发射第一轮,第二门大炮发射第二轮,以此类推。这一系列的if
语句使用了您在SpriteObject.java
中创建的新的getOrientation()
函数。然后将每门加农炮炮管末端的 x 和 y 坐标传递给createBullet()
方法。得到坐标需要一些计算,因为你知道枪管在大炮的中心。
子弹的结构在createBullet()
中更有意义,你将在下一节中写它;清单 9-17 中的代码只是将必要的信息发送给那个方法。因为你已经初始化了所有的子弹精灵,这不会浪费处理时间,因为你只是在更新精灵。
5.要完成update()
方法,确保调用了各种精灵的update()
函数,如清单 9-18 所示。
清单 9-18。包括基本的update()
功能
`castle.update(adj_mov);
ground.update(adj_mov);
for(int i = 0; i < boat_count; i++){
boat[i].update(adj_mov);
}
}`
接下来的部分通过处理游戏重置和发射子弹来解决遗留问题。
管理游戏结果
当玩家输掉游戏,一艘船撞上城堡,你呼叫reset()
。这是一个简单快捷的功能。
请遵循以下步骤:
1.将清单 9-19 中的代码添加到GameView
中其他函数的下面。
清单 9-19。 reset()
法
`private void reset(){
for(int i = 0; i < boat_count; i++){
boat[i].setstate(boat[i].DEAD);
}
boat_count = 0;
}`
你所做的就是毁掉那些船。这实际上重新开始了游戏,因为船又一次被随机创建了。你不移除大炮,因为没有必要担心它们。如果用户愿意,他们可以删除它们。如果您想向用户显示一条消息,您可以创建一个 sprite 并在此时将其绘制在屏幕上。在update()
功能中,等待大约 30 个周期,然后删除消息。
2.createBullet()
方法有点复杂,正如你在清单 9-20 中看到的,但它绝对是可管理的。把这个方法直接放在reset()
函数下面。
清单 9-20。 createBullet()
法
`private void createBullet(int x, int y, int direction, int r){
if(r >= 0){
int index = available_bullet[r];
if(direction == bullets[index].RIGHT){
bullets[index].setMoveX(10);
bullets[index].setMoveY(0);
bullets[index].setX(x);
bullets[index].setY(y);
bullets[index].setstate(bullets[index].ALIVE);
}
if(direction == bullets[index].LEFT){
bullets[index].setMoveX(-10);
bullets[index].setMoveY(0);
bullets[index].setX(x);
bullets[index].setY(y);
bullets[index].setstate(bullets[index].ALIVE);
}
if(direction == bullets[index].UP){
bullets[index].setMoveY(-10);
bullets[index].setMoveX(0);
bullets[index].setX(x);
bullets[index].setY(y);
bullets[index].setstate(bullets[index].ALIVE);
}
if(direction == bullets[index].DOWN){
bullets[index].setMoveY(10);
bullets[index].setMoveX(0);
bullets[index].setX(x);
bullets[index].setY(y);
bullets[index].setstate(bullets[index].ALIVE);
}
}
}`
子弹精灵是对称的,所以你不用担心它们的方位,只需要担心它们移动的方向。别忘了每个if
块的最后一行,让子弹活起来。否则,它们将永远不会被绘制出来,并且您将很难找出哪里出错了。
你终于完成了游戏项目。下一节给你一些未来计划的想法。
分析游戏
如果你还没有,运行游戏。当船开始来的时候,放置你的大炮保卫城堡。祝你在战斗中好运。
以下是您用来构建 Harbor Defender 的功能和技术列表。为你在代码、错误和工作中坚持不懈的努力感到自豪:
- Game cycle
- Multiple elves
- Draw an image on the screen
- Bitmap manipulation
- User interaction
- Some AI
- Collision detection
- XML data parsing
- And more.
写完整个游戏后,你可以放松,把游戏改成你想要的样子。如果你做了足够多的改变,也许你可以在 Android 市场上赚点钱。本书的最后一章讨论了这种可能性。
拥有一款可扩展的游戏至关重要。如果游戏开发商不得不从头开始制作每一款游戏,他们永远不会发布足够的游戏来支付租金。相反,他们将框架转化为许多独特的、看似不同的创造。你所做的有潜力转化为迷宫游戏,平台游戏,回合制策略游戏,或者其他许多可能性。
SpriteObject
类是完全可重用的,并且GameView
可以很容易地调整成其他类型。如果你需要想法,我觉得浏览其他游戏开发书籍并为 Android 创建它们的样本很有趣。任何语言的任何游戏都可能在 Android 上创建。如果游戏是为电脑设计的,并且使用键盘控制,这可能是一个挑战。要有创造性,我相信你能写出一些非常不同的程序。
图 9-2 显示完成的游戏。看看你能否想象它被转化成十几个不同的项目。
图 9-2。你完成的项目
总结
你的辛苦完成了,你也学到了很多。在本章的最近部分,你看到了如何使用矩阵来旋转位图。您还了解了如何跟踪 50 个精灵并维护另一个列表,其中列出了哪些精灵已经死亡并准备再次创建。这一章也标志着你第一次尝试创建一个用户界面,它包括几个图标和一个标记来显示用户当前选择的内容。
如果你厌倦了代码,有一个好消息:下一章处理发布你的游戏,提供更新,并处理业务结束。你看看什么游戏卖得好,平板电脑如何改变计算领域。当你理解了商业方面,就轮到你创造自己的杰作了!
十、发布游戏
你的游戏已经为大众做好了准备,但是在应用被大众消费之前,你还有几个步骤。对代码有几处修改,可以用来润色您的工作。然后这一章讲述了出售或赠送游戏的步骤。最后,您将了解如何确保在竞争激烈的移动应用市场中取得成功。
制作一款高质量的游戏只是在 Android 应用市场获得畅销地位的第一步。到目前为止,你所做的一切都可以融入到你展示最终产品的过程中。这款应用的图形、声音和外观都融入了你向消费者销售的方式中。
打磨应用
虽然你的游戏可以玩,但它可能需要一些润色。欢迎屏幕将是一个很好的补充,这样玩家可以在进入游戏之前了解游戏。当谈到添加这个功能时,你有很多选择,但制作一个基本的入口屏幕很容易,你可以针对每个游戏进行微调。在本节中,您将添加一个屏幕,然后添加一个按钮来启动游戏。
添加闪屏
因为GameView.java
负责实际的游戏及其外观,所以你的启动页面由MainActivity.java
处理。不是设置屏幕显示GameView
,而是呈现一个快速布局,然后给用户进入游戏的能力。这使您的工作更专业,对用户来说也更容易。为了扩展这个概念,你可以播放一小段视频来介绍这个游戏,但是我会留给你自己去想象。
看看图 10-1 看看你的闪屏是什么样子的。如果你想要一个更完整的介绍界面,这一节将讨论添加特性和项目的方法。
图 10-1。游戏简介
为了达到图 10-1 中的效果,让我们回到第一章中的概念。应用的外观是在main.xml
中生成的,在这里你可以通过拖动按钮和文本到屏幕上来创建界面。然后编辑文本和元素。以下步骤显示了如何做到这一点:
- Navigate to the folder:
res
layout
main.xml
to find your main.xml file in the Harbor Defender project.- Open
main.xml
and select "10.1-inch WXGA" from the drop-down menu near the top. The first task is to check the code of the main.xml file.- Select
main.xml
on the small tab near the bottom of the screen. Replace the existing code with the code in Listing 10-1 .
清单 10-1 。Main.xml
`
<AbsoluteLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
`
你用一个AbsoluteLayout
替换现有的LinearLayout
。这两者都是可以添加布局元素的框架。然而,AbsoluteLayout
让您快速指定元素的确切位置,而LinearLayout
将所有项目向左对齐。当您添加欢迎屏幕的各个部分时,这一点非常重要。
5.选择屏幕底部的小图形布局选项卡,返回图形布局。
6.您可以使用左侧的项目面板来创建您的布局。图 10-2 显示了这将会是什么样子。将一个Button
和一个TextView
拖到您的屏幕上。它们现在包含填充文本,但是您很快就会编辑它。
图 10-2。使用左边的调色板拖动TextView
和Button
对象到屏幕上
7.是时候回到代码的视图了。选择屏幕底部的main.xml
选项卡。您应该观察到两个新元素(Button
和TextView
)已经出现在您的AbsoluteLayout
元素中。
8。您需要插入文本并更改按钮的id
。查看清单 10-2 中的粗体代码。您可以使用不同的词,但重要的是记住您分配给按钮的id
的名称或其他标识符。
清单 10-2 。Main.xml
`
<AbsoluteLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button
android:text="Start Game"
android:layout_width="wrap_content"
android:id="@+id/startgame"
android:layout_height="wrap_content"
android:layout_x="557dip"
android:layout_y="249dip">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="When you are ready to begin this game, please click on the button above."
android:id="@+id/textView1"
android:layout_x="310dip"
android:layout_y="361dip">
`
layout_x
和layout_y
行指定项目的位置。如果您想要精确地确定按钮和文本的位置,您可以编辑这些值。您使用id
标签来引用代码中的对象,就像您在下一节中所做的那样。
响应开始游戏按钮的按下
现在你已经有了一个很好的展示给用户,你需要让它具有交互性。让玩家快速开始游戏至关重要。这对于一个回归的玩家来说尤其重要。请记住,如果这个人回来玩你的游戏,他们希望很快开始玩,不希望看到说明或被介绍视频打扰。
为了显示你的新布局,然后让用户导航到真正的游戏,让我们回到MainActivity.java
。这里你做一个简单的输入测试,然后展示实际的游戏。然而,最初你需要把Main.xml
而不是GameView.java
作为游戏的视图。请遵循以下步骤:
- Open
MainActivity.java
in the edit pane of Eclipse.- Add the following
import
statement to the top of the file:
import android.widget.Button;
3.更改MainActivity.java
的onCreate()
方法,使其看起来像清单 10-3 中的。粗体部分是对你之前作品的修改。你必须进口Android.view.View
才能让它工作。
清单 10-3。??MainActivity.java
`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGameView = new GameView(this);
setContentView(R.layout.main);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
final Button button = (Button) findViewById(R.id.startgame);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
setContentView(mGameView);
}
});
}`
第一个setContentView()
告诉应用加载main.xml
作为布局。按钮部分监听按钮的点击。一旦发生这种情况,你调用另一个setContentView()
在屏幕上显示GameView
。这是你用来初始化游戏的简单方法。
当你给按钮赋值时,使用函数findViewById
;作为参数,您使用按钮的id
。这就是为什么你要把id
按钮设计成一个很容易识别的启动游戏的物品。
4.运行游戏,你会看到一个欢迎界面。按下开始游戏按钮继续,应用正常运行。
恭喜你:你终于完成了这本书的代码部分!下一部分处理游戏的最终编译和发行的准备工作。您离与其他用户共享您的创作越来越近了。
包装游戏
在游戏完成并准备发布之前,你必须注意几件事情。这一节讲述了如何清理代码,并最终将产品编译成一个 APK 文件,以备分发。APK 是包含所有游戏代码、图像和资源的包装。
请遵循以下步骤:
- The first thing to do is to delete any
Log.d
statements in the code. I usually perform global search and replacement to delete them. You don't want the retail version to waste processing power and send our debugging warning. You must fix the code version in the Android manifest file. Find this file by going to the root directory ofHarborDefender
folder and openingAndroidManifest.xml
. The code should be similar to the tag shown in Listing 10-4 .
清单 10-4。??AndroidManifest.xml
`
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gameproject.harbordefender"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="11" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
`
注意粗体部分。你可以设置自己的版本代码和版本名称,但这是惯例,因为这是你的第一个游戏,使用 1.0 作为版本。还要确保 SDK 的最低版本是 11。
3.在 Eclipse 中选择文件>导出。
4.选择导出 Android 应用作为您要执行的导出类型。
5.在下一页,输入你最终项目的名称: Harbor Defender 。
6.您必须创建一个密钥库,这是保护您的应用的安全性所必需的,并且被 Android 应用市场用作标识符。选择创建新的密钥库,如图图 10-3 所示,并使用浏览按钮打开一个窗口,让您将文件放入文件夹中..键入类似于 harbordefenderkey 的文件名,并接受默认位置。
图 10-3。生成密钥的提示
7.创建一个唯一且难懂的密码来保护自己,如图图 10-3 所示。
8.在图 10-4 所示的密钥创建页面中填写适用信息。(图为我是如何完成的。)密码可以与您在上一页中使用的密码相同。
图 10-4。填写开发商信息
9.下一页是最后一页。单击浏览,并输入 HarborDefender 作为 APK 目的地。关闭对话框,并完成该过程。
就这样,你完成了这个项目。下一节将讨论如何将这个项目引入应用市场并送到消费者手中。你还将讲述如何在拥挤的应用市场中做最好的营销和工作。
部署游戏
我希望你对你的游戏感到满意,并且相信其他人也会喜欢它。本节介绍如何使用 Android 应用市场。您将了解如何上传应用以及营销和定价的基本原理。有了这些信息,您就可以继续制作更多的待售应用。
首先,看一下图 10-5 ,图中显示了[
market.android.com/](https://market.android.com/)
的 Android 应用市场主页。
图 10-5。安卓应用市场
在这个页面上,Android 移动设备和平板电脑的用户可以下载和购买应用。特别值得注意的是标签上写着特色平板电脑应用。Android 正在大力吸引平板电脑的买家,因此它将专门为平板电脑设计的应用与手机应用分开。这对你来说是个好消息,因为你面临的竞争少了很多。
在如何提供课程方面有很大的自由。你可以为你的应用设定一个介于 1 美元到 200 美元之间的价格,或者免费赠送。当一个顾客买了它,你得到了销售额的 70%;剩下的就是将应用发送到设备上的费用。谷歌不收取任何收益,但设备制造商和在线分销商收取处理交易的费用,就像信用卡公司对每笔交易收取费用一样。iPhone 和 iPad 应用只给开发者 60%的收入,所以从这个意义上说,Android 比苹果应用商店还有一个优势。
安卓市场上 57%的应用是免费的。竞争应用商店的免费应用比例要低得多。对你的暗示是,你必须意识到,要求用户付费的节目必须表现出优越的质量,并提供许多小时的播放时间。
你现在知道了应用市场的基本情况。您必须创建一个 Android 应用市场帐户,才能看到您自己的作品。下一节将介绍如何创建帐户并上传您的第一个应用。
开设谷歌开发者账户
没有什么比看到自己的工作掌握在他人手中更让应用开发人员高兴的了。在这里,您可以创建您的 Android 应用市场帐户,并向全世界发布您的程序:
- Go to
[
market.android.com/](https://market.android.com/)
. At the bottom of the screen, click Developer.- Select the option to publish the application.
- Log in to your Google account, or create a new account. You should create a new account specifically for your application business, which is different from your regular email or Google+ activities.
- The next screen is shown in Figure 10-6 . Fill in with accurate and professional information. If you don't have a website, that's fine, but you might want one.
?? * ?? 】 Figure 10-6. Create your robot application market account*
- You are prompted to pay the registration fee. This is $25, which must be paid through Google checkout.
注册完成后,您的帐户就有了一个分类配置文件。你可以做很多事情,从添加一个谷歌结账账户以便获得付款,到上传一个应用。
现在你可以将你的游戏上传到谷歌市场了。
上传游戏到谷歌市场
虽然大多数开发人员都想出售他们的应用,但这一部分介绍了如何将您的应用免费上传给公众。如果你想收到付款并为你的工作收费,请访问这个关于市场的惊人指南:www . Google . com/support/androidmarket/developer/bin/topic . py?topic=15866 。
在完成上传游戏的简单过程之前,您必须准备好几个项目,包括:
- Include applications
- Two beautiful application screenshots of APK file highlight its characteristics.
- A high-resolution icon that users choose to play your game.
上传游戏是一个简单的命题。在您的在线开发人员控制台上,单击上传应用。在这里,您将看到一个向导,询问刚才列出的项目。在您存储文件的目录中找到这些文件。
关键是要有一个有吸引力的截图和描述,以及任何你想显示的附加图表;你的成功将关系到你的游戏吸引了多少用户。下一部分着眼于如何准备在市场上取得最大的成功。
营销您的游戏
营销你的应用需要将你的产品展示给尽可能多的人。如果你创造了一个像样的游戏,那么如果人们有机会看到它,他们就会购买。第一个问题是如何让你的应用脱颖而出。与 iPads 和 iPhones 的应用商店不同,Android 程序可以从任何网站下载,而不仅仅是谷歌官方市场。这意味着拥有自己网站的开发者更容易销售他们的产品,因为他们不会与市场上过多的类似应用混淆。用户可以直接来到他们的网站,看到视频、图片和程序说明,这些在 Android Market 上的简短描述中是不可能的。
利用这一事实,创建自己的网站,吸引潜在买家。创建一个脸书页面或推特账户也能增加关注度。不要把读者引向你在 Android 应用市场上的页面,而是把他们引向你自己网站上的一个页面,这样就不会那么混乱了。
如果你做过网上营销,你就会知道邮件列表有多有用。在您的网站上,为访问者提供注册更新您的应用和免费附加服务的机会。这样,即使他们没有立即购买,你也可以继续吸引他们,说服他们购买你的产品。看看 AWeber ( [www.aweber.com/](http://www.aweber.com/)
)网站,它提供了一个很棒的邮件系统,你可以用它来向你的用户分发时事通讯。它每月收费,但许多营销人员发现,客户从简讯中获得的收益超过了成本。
最后,通过将你的公司或游戏放入更传统或可信的媒体来解决营销问题。请关注技术的杂志对其进行评论,或将相关信息发送给在线新闻来源。当你这样做的时候,确保你的游戏提供一些非常独特的东西。也许输入控制是完全创新的,或者游戏发生在零重力室内。让应用有新闻价值。这也可以由你的公司整体来做。举例来说,如果你所有游戏中的艺术作品都来自一位著名的画家,那绝对是一个值得一个网站谈论的独特故事。
所有这些技术都可以追溯到广告中使用的基本漏斗方法。它在各种各样的初级营销和公共关系书籍中有所阐述,但也需要包括在这里。你吸引的用户越多,时间越长,你的销售额就越多。图 10-7 显示了这是如何工作的。
图 10-7。将你的访客引向买家
这就是营销技巧。通过反复试验,你会找到最适合你的方法。我发现在应用市场上的成功很少是在你的第一款甚至第二款游戏上实现的。你必须坚持下去,在获得金牌之前,建立对你的产品的期待和兴奋。
总结
恭喜你!你已经完成了这本书。你从发现什么是 Android 以及如何在其中编程,到编写一个完整的游戏,再到将你的工作投入应用市场。
这是一本有趣的书,我希望你也喜欢。从事一项发展如此迅速的技术工作既令人畏惧又令人振奋;理想情况下,这本书给了你一些关于如何为 Android 平板电脑创建自己的游戏的想法。
凭借 Android 过去的成功和光明的未来,我相信平板电脑对更好游戏的需求将会持续很长一段时间。确保你在那里抓住这个令人兴奋的浪潮。
十一、在真实设备上测试 Android 游戏
如果你打算为 Android 平板电脑制作游戏,你肯定需要在真实的东西上测试它们。Android 有一个内置的方法来做到这一点,消除了开发人员过去在视频游戏机和其他移动平台上测试他们的创作时遇到的许多障碍。应用市场很难容忍那些有缺陷或问题的程序,而这些缺陷或问题在产品测试中很容易被修复。
本附录指导您快速设置平板电脑进行测试。要跟进,你需要一台 Android 3.0 平板电脑。在我写这篇文章的时候,市场上有很多平板电脑,每周都会有更多的上市,所以不可能一一列出。当我选择设备时,我很少寻找最先进的平板电脑,而是寻找最受欢迎的一款。如果大多数人使用你所使用的设备,那么你的结果将与大多数用户的结果相似。
选择一款拥有广泛认可的品牌名称和众多追随者的平板电脑。如果你有平板电脑的朋友,你应该在所有的平板电脑上测试你的应用。这里描述的过程不需要很长时间,所以你应该没有问题。
因为您将进行调试,所以硬件接口要求您将应用指定为可调试的。您可以通过在 Android 清单文件AndroidManifest.xml
中设置一个参数来实现这一点。当您在 Eclipse project explorer 中查看您的项目文件夹时,您不会在其中看到清单文件。图 AppA-1 显示了在哪里可以找到这个文件。
图 AppA-1。安卓清单文件
向 XML 中添加一个非常简单的参数,将项目定义为可调试的程序。清单 AppA-1 包含整个清单的代码,必须插入的部分用粗体显示。
清单 AppA-1。安卓清单文件
`
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gameproject.firstapp"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="11" />
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:debuggable="true">
<activity android:name=".Main"
android:label="@string/app_name">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
`
接下来的步骤因设备而异。谷歌推荐去平板上的Application
文件夹,然后去Development
文件夹,选择 USB 调试。如果这对你的平板电脑不起作用,快速搜索一下,看看如何打开这种类型的调试。
现在你需要一个特定 USB 设备的驱动程序。这与您在将平板电脑连接到电脑进行常规使用时安装的不同。你需要从谷歌 Android 开发者页面的 USB 驱动列表中选择一个:[
developer.android.com/sdk/oem-usb.html](http://developer.android.com/sdk/oem-usb.html)
。安装这些的过程非常简单。
以我拥有的摩托罗拉平板电脑为例。我点击链接进入摩托罗拉的开发者驱动主页。因为我在 64 位版本的 Windows 上运行程序,所以我选择了最新的手机 USB 驱动程序。(平板电脑是否是手机尚有争议,但驱动因素是一样的。)我按照设置流程做好了准备。
注意如果你正在麦金塔电脑上开发,你不需要担心 USB 驱动程序:你已经准备好了。然而,Linux 用户手头有些工作要做。有关更多信息,请查看 Android 官方文档,了解如何设置开发设备:
[
developer.android.com/guide/developing/device.html#setting-up](http://developer.android.com/guide/developing/device.html#setting-up)
。
如果您正确地遵循这些指示,现在您可以在设备上测试您的程序。进入 Eclipse,像往常一样运行程序。您应该可以在添加的设备和模拟器之间进行选择,而不是默认使用模拟器。选择你插入的设备,你就可以像你的用户一样与你的应用互动。
请特别注意,有些应用只能在物理设备上运行。其中包括依赖加速度计数据或蓝牙连接的应用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南