使用libgdx开发Android游戏(1):一天内创建工作原型

在本文中,我会间接的讲解构建一个游戏引擎和组件的模块,而且我将演示如何使用libgdx库快速开发一个游戏原型。
你将学到:

  • 创建一个非常简单的2D射击平台游戏。
  • 一个完整的游戏架构是什么样的。
  • 在不了解OpenGL的情况下如何使用OpenGL的2D图形技术。
  • 不同的实体如何组成了游戏和在游戏的世界中它们是如何联系在一起的。
  • 如何为游戏加入音效。
  • 如何在桌面构建游戏并发布到Android上,是的,就是那种魔法。

创建游戏的步骤

  1. 有一个游戏的想法
  2. 把你想象中的游戏画面以及样子在纸上简单画出一些场景。
  3. 分析思路,不断调整迭代几个版本,决定游戏在初始版本中应该有哪些东西
  4. 选择一种技术然后开始设计原型
  5. 开始进行编码,创建游戏的资源
  6. 试玩-测试,不断改进和持续小步前进进行直至完成
  7. 美化并发布

游戏理念

因为这是一个为期一天项目,时间非常有限而且我们的目标是学习制作游戏的技术,而不是实际的流程。出于这个目的,我冒昧的借鉴了其他游戏的思路,将重点放在这一过程的技术方面。
我在很大程度上借鉴了一个叫做 星际守护的游戏,它是Vacuum Flowers制作的一个小的精品游戏。它是一个很简单的射击平台游戏,风格简单并且有街机的感觉。
这个游戏的想法是带领我们的英雄杀掉敌人,躲开其他试图杀死我们的东西来冲过关卡。
操作也非常简单,方向键向左或向右移动英雄,Z是跳跃,X是发射激光。按住跳跃键的时间越长,英雄跳的越高。可以在空中改变方向和射击。稍后我们将会把这些东西移动到Android平台。
接下来的步骤(2和3)可以跳过,因为有现成的游戏,所以我们已经做完了。

启动Eclipse

这是我们的起点,我会使用libgdx库创建游戏。为什么是libgdx?它是开发游戏最好的类库(在我看来),使开发游戏很容易而不需要知道太多相关的底层技术。它允许开发者在桌面上创建自己的游戏,不需要任何更改就可以部署到Android上。它提供了游戏中使用的所有元素,并且隐藏了与特定的技术和硬件打交道的复杂度。渐渐的这一点会变得越来越明显。

建立项目的简单方法

我们将使用Aurelien RibonLibGdx Project Setup UI工具。建议按照libgdx的wiki页面上介绍的步骤来,但是我为星际守护提供一个快速的解决方法。
首先下载可执行的jar文件:http://libgdx.badlogicgames.com/nightlies/dist/gdx-setup-ui.jar
双击运行,如果不成功,尝试在jar的下载路径下执行命令 :

你将看到初始化画面。

点击 创建按钮,使用显示的值完成接下来的画面。
另外,还要确保为项目选择了合适的目标。

点击高亮的按钮自动下载最新版本的libgdx(稳定版),等待LibGDX 文字变绿。
一旦变绿,点击右下角的 Open the generation screen 按钮。
在接下来的窗口中,点击 启动按钮等待完成。成功的标志是显示”全部成功!”。

项目已经就绪,可以从被选择创建的目录导入到Eclipse中。
在Eclipse中,单击 文件->导入…->通用->工作区间的工程,并且指向项目所在目录。
单击 完成会展现出可以被导入的项目清单,将它们全部导入。

单击完成,所有都设置完毕。

困难的设置工程(手动)

首先我们需要下载类库。
访问http://libgdx.badlogicgames.com/nightlies/下载文件ibgdx-nightly-latest.zip,解压缩。
在Eclipse中创建一个简单的Java工程,我们叫它 star-assault。

创建工程的时候使用默认配置,右击它,选择 新建->文件夹,创建一个名为libs的目录。
从解压开的libgdx-nighly-latest目录中,将gdx.jar文件拷贝到新创建的libs目录。同样把gdx-sources.jar文件拷贝到libs目录。它在解压后的gdx目录的sources 子目录下。在eclipse中,你可以简单的把jar文件拖拽到你的目录中做到这点。如果你使用资源管理器、查找器或者其他方式拷贝,不要忘记按F5刷新你的Eclipse项目。

这个结构应该像下图:

添加gdx.jar作为项目的依赖。具体做法是右击工程的名称,选择 属性。在这个画面上选择 Java Build Path,单击 Libraries 选项卡,单击 Add JARs…,进入libs 目录选择 gdx.jar,然后单击确定。
为了能够访问gdx 源代码并能够轻松调试游戏,添加dx.jar文件的源码是一个好主意。要做到这一点,展开gdx.jar节点,选择 Source attachment,单击 Edit…,然后 Workspace…,选择gdx-sources.jar单击确定直到关闭所有的弹出窗口。

使用libgdx建立项目的完整文档可以在官方的Wiki上找到。
这个项目是游戏的核心项目。它将包含游戏的机制,引擎,其他所有事情。我们还需要创建2个项目,我们的2个目标平台的基本启动器。一个用于Android,一个用于桌面。这些项目会极其简单,只包含在各自平台运行游戏所需的依赖关系。把他们当做包含main方法的类。
为什么我们需要将这些分为独立的项目?因为libgdx 隐藏了处理底层操作系统(图形、音频、用户输入、文件I/O等)的复杂度,每个平台都有一个具体的实现,我们只需要包含目标平台上需要的实现(绑定)。也因为应用程序生命周期,资源加载(加载图形、音频等)和应用程序的其他常见方面被大大的简化,平台的特定实现位于不同的JAR文件中,只有我们目标平台需要的文件才会被添加。

桌面版本

像前面的步骤一样创建一个简单的Java工程,命名为star-assault-desktop。同时参照创建libs目录的步骤。这次从下载的压缩文件里面所需的jar文件是:

  • gdx-natives.jar,
  • gdx-backend-lwjgl.jar,
  • gdx-backend-lwjgl-natives.jar.

像前面的项目一样,也要把这些jar文件添加到项目的依赖中去。(右击project -> Properties -> Java Build Path -> Libraries -> Add JARs,选择这3个jar文件然后单击确定)。
我们还需要把star-assault项目添加到依赖中。要做到这一点,单击 Projects选项卡,单击 添加,选中star-assault项目然后单击确定。


重要! 我们需要设定star-assault项目为传递依赖,这意味着这个项目的依赖关系被当成依赖于本项目的项目依赖关系。要执行此操作:右击主项目, Properties -> Java Build Path -> Order and Export,选择gdx.jar然后单击确定。


Android版本

为此,你需要安装Android SDK
在Eclipse中创建一个Android工程: File -> New -> Project -> Android Project
将它命名为star-assault-android。对于构建版本,选择”Android 2.3″。指定包名为”net.obviam”或者其它你喜欢的名字。在接下来的”创建Activity”中输入StarAssaultActivity,点击 完成

进到项目的路径下,创建 libs子文件夹(可以在eclipse中完成操作)。从 nightly zip文件中,拷贝gdx-backend-android.jar文件和the armeabi 和armeabi-v7a 目录到新创建的libs目录下。
在eclipse中,右击project -> Properties -> Java Build Path -> Libraries -> Add JARs,选择 gdx-backend-android.jar,单击OK。
再次单击 Add JARS,选择主工程下(star-assault)的gdx.jar,单击OK。
单击 项目标签,单击 Add,选中主工程然后单击两次OK。

下面是展示的结构:

重要!

对于ADT发布版17或者更新版本,gdx jar文件需要被显示的标记为导出。
具体做法:

  • 在Android项目上单击
  • 选择 Properties
  • 选择 Java Build Path (第一步)
  • 选择 Order and Export(第二步)
  • 选择所有的引用,例如gdx.jar, gdx-backend-android.jar,主项目。(第三步)。

下面的图像显示了最新的状态。

另外,在此处查找有关该问题的详细信息。

共享资源(图片、声音和其他数据)

因为游戏需要在Android和桌面版本上保持一致,但是每个版本又需要从独立的项目构建。我们需要将图像、声音和其他数据放到一个共享位置。理想的情况是把这些内容放到主项目中,因为Android和桌面版都包含主项目。但是因为Android对于如何存放这些文件有严格的规则,我们必须把资源放到那里。它就在Android项目自动创建的资源目录下。在eclipse中,有一种链接目录的方式,就像 linux/mac上的符号链接或者Windows下的快捷方式。若要将Android项目中的资源目录链接到桌面版本,请执行以下操作:
右击 star-assault-desktop 项目->Properties -> Java Build Path -> Source tab -> Link Source… -> Browse… ,浏览到star-assault-android项目的assets目录,然后单击完成。你也可以扩展变量列表而不必浏览到assets 目录。建议将项目设置为文件系统无关。
另外,确保assets 目录被添加到源文件目录中。具体做法:在eclipse中(桌面版本)的assets目录上右击,选择 Build Path -> Use as Source Folder
本阶段我们的设置已经准备完毕,可以继续进行游戏的工作。

创建游戏

计算机应用程序是一个运行在机器上的软件,它启动,做一些事情(甚至什么都不做),并在一种或另一种条件下停止。电脑游戏是一种特殊的应用,它在”做一些事情”的部分做的是游戏。所有应用的开始和结束都基本上是一样的。另外,游戏有一个非常简单的基于连续循环的架构。可以在这些地方找到更多关于架构循环的东西。
由于有libgdx,我们的游戏像在剧场上演戏剧一样拼凑起来。所有你需要做的就是,把游戏想象为一个舞台剧。我们将定义舞台,演员,角色和行为,但我们把编剧委托给玩家。
为了创建游戏我们需要采用下面的步骤:
1. 启动应用
2. 载入所有的图片和声音,存储在内存里
3. 为我们的游戏创建舞台,利用演员和它的行为(它们之间的交互规则)
4. 将控制权交给玩家
5. 创建基于从控制器接收的输入来操作演员在舞台上的行为的机制
6. 决定游戏何时结束
7. 结束显示

它看起来很简单并且它确实是很这样。我将会在它们出现的时候介绍概念和元素。
为了创建游戏,我们仅仅需要1个类。
让我们在star-assault项目中创建StarAssault.java,这个项目中的每个类都有2个例外。

只是实现了gdx中的ApplicationListener接口,eclipse 会自动生成需要实现的方法的存根代码。
这些都是我们需要实现的应用程序生命周期的方法。我们简单的认为需要为Android或者桌面设置所有的代码,来初始化OpenGL上下文和所有那些枯燥(困难的)的任务。
方法create()首先被调用。发生时机是该应用程序已准备就绪,可以开始加载我们的资源、创建舞台和演员。想象成在所有的东西都被运到那里,并准备好后为演出搭建舞台。根据剧场的位置和你到达的方式,物流肯定是个噩梦。你可以通过人力、飞机、卡车进行运输…我们不知道。我们在其中所有东西都已经就绪,可以开始着手组装它们。这就是libgdx帮我们完成的事情。不区分平台的帮我们搬运并交付东西。
resize()方法在每次绘画界面改变的时候被调用。这让我们有机会再继续播放前重新安排代码。例如窗口(如果游戏有窗口)调整时会触发此事件。
每个游戏的核心都是render()方法,它无非就是一个无限的循环。这个方法被不断调用,直到游戏结束或者我们终止游戏。这是游戏的过程。
注:对于电脑来说游戏结束与其他程序是不一样的。所以它只是一个状态。程序是在游戏结束的状态,但它仍然在运行。

当然,游戏可以被暂停中断,也可以恢复。每当Android或者桌面应用程序进入到后台模式时pause()方法会被调用。当应用程序重新恢复到前台时resume()方法被调用。
当游戏完成后,应用程序被关闭时,dispose()方法被调用,这是时候做一些清理工作了。它类似于演出已经结束,观众已经离开,舞台将要被拆除。不再回来了。更多关于生命周期的内容可以在这里找到。

角色

让我们开始实际的开始做游戏。第一个里程碑是有一个我们的家伙可以移动的世界。这个世界是由水平面组成,每个平面都包含地形。地形无非是一些我们的家伙无法通过的障碍物。
目前为止在游戏中确定演员和实体很容易。
我们的家伙(让我们叫它Bob,libdgx有关于Bob的教程)和障碍物组成了这个世界。
玩过星际守护的话,就会发现Bob有几个状态。当我们不操作的时候,Bob处于空闲状态。他也可以移动(在两个方向),也能跳。此外,当他死了,就不能做任何事情了。Bob在任何时候的状态只能是4个确定的状态中的一个。也还有其他的状态,但我们现在不考虑。

Bob的状态:

  • 空闲 – 当没有移动或者跳跃并且或者
  • 移动 – 以恒定的速率向左或向右
  • 跳跃 – 也是面向左或者右并且有高有低
  • 死亡 – 它甚至不可见或者再生

障碍物是其他的演员。为简单起见,我们也只有几个障碍物。水平面由放置在一个二维空间内的障碍组成。为简单起见,我们将使用一个网格。
将Star Guard的开局变为障碍物和Bob的结构,将看起来像这样:

上面一个图片是原始的,下面的是我们的世界的表示。
我们已经想像出了世界,但我们需要工作在一个有意义的测量系统。为简单起见,我们把世界里的一个块是当成1个单位宽、1个单位高。我们可以用米来表示使其更简单,但因为Bob是半个单元,这让他才半米高。让我们认定游戏世界中的4个单位为1米,因此Bob的身高是2米。

这一点很重要,因为我们将计算鲍勃跑动的速度等等,我们需要知道我们在做什么。
让我们来创建世界。
我们的主角是Bob。

Bob.java类像这样:

#16-#21行定义了Bob的属性。这些属性值定义了任何时间Bob的状态。
position – Bob在世界中的位置。用世界坐标系来表示(后面将详细讨论)。
acceleration – 决定Bob跳跃时的加速度。
velocity – 用来进行计算并用在Bob移动上。
bounds – 游戏中的每个元素都会有一个边框。这只是一个矩形,以便知道Bob是否撞到了墙,是否被被子弹杀死或是否击中敌人。其将被用于碰撞检测。想像成在玩方体。
state – Bob的当前状态。当我们发出步行的动作,状态将变成运动,基于这个状态,我们知道如何绘制屏幕。
facingLeft – 代表Bob的朝向。作为一个简单的2D平台游戏,我们只有两个方向,左和右。

#12-#15 行定义了一些常量,我们将用它来计算在世界中的速度和位置。这些将在后面进行调整。
我们还需要一些障碍来组成世界。

Block.java 看起来像这样:

障碍只不过是摆在世界上的一堆矩形。我们将使用这些障碍来构建地形。我们有一个简单的规则。没有任何东西可以穿过它们。


libgdx提示

你可能已经注意到我们使用了libgdx的Vector2类型。因为它为我们提供了处理欧几里得向量需要的所有事情,这使得我们的生活容易了很多。我们将使用向量来定位实体,计算速度,并左右移动东西。


关于坐标系统和单位

和现实世界一样,我们的世界也有尺寸。在一个平面上想象一个房间。它具有宽度、高度和深度。我们把它当做是二维的,不考虑深度。如果房间是5米高,3米宽,我们可以说我们在一个度量系统里描述房子。很容易想象在房子中间放置1个1米宽1米高的桌子。我们无法通过该桌子、穿过它,我们需要跳到它的上面,走1米,然后跳下来。我们可以使用多个桌子来创建一个金字塔,并在房间里制作一些奇怪的设计。在我们Star Assault的世界里,世界代表了现实世界的房间、障碍物代表了现实世界的桌子、单位代表了现实世界的米。
如果我每小时跑10KM,转换一下就是2.77777778每秒(10 * 1000 / 3600)。将这些转换成Star Assault的坐标,要代表10KM/h的速度,我们将使用2.7单位/秒。
检查下面的利用世界坐标系表示的图表,里的有带着边界的盒子和Bob。

红色的方块是障碍物的边界框。绿色的方块是Bob的边界框。空的方块只是个空。网格仅供参考。这就是我们将要模拟的世界。坐标系的原点位于左下角,所以向左走10,000单位/小时意味着Bob位置的x坐标将以2.7单位每秒的速度减少。
还要注意的是成员的访问权限是包默认权限,模型在一个单独的包里。我们要想从引擎中访问,必须要创建存取方法(getter和setter)。

创建世界

作为第一步,我们将只创建世界作为一个硬编码的小房间。它有10单位宽和7单位高。我们将像下面显示的图片一样放置Bob和障碍物。

World.java看起来像这样:

它只是一个简单的世界的实体的容器类。当前实体是障碍物和Bob。在构造器中,障碍物被添加到blocks数组,并且Bob被创建。暂时这一切都是硬编码的。请记住,原点在左下角。

创建游戏并显示世界

为了把世界渲染到屏幕上,我们需要创建一个画面,并让它来渲染这个世界。在libgdx中,有一个便捷的叫做Game的类,我们将重写StarAssault类作为libgdx的Game类的子类。


关于画面

一个游戏可以包含多个画面。甚至我们的游戏将包含3个画面。 开始游戏画面, 播放游戏画面, 游戏结束画面。每个画面都只是关注自己的事情而不关注其他画面的。例如 开始游戏画面将会包含菜单选项 开始退出 。它有2个元素(按钮),它关注的是处理这些元素上的 单击/触控 。它始终显示这两个按钮,如果我们单击/触控 开始 按钮,它会通知主游戏加载 播放游戏画面 并退出当前画面。播放画面将运行我们的游戏,并处理相关的所有事情。一旦游戏满足结束状态,它告诉主游戏过渡到游戏结束画面,后者的唯一目的是显示最高得分,并监听重新开始的单击事件。


让我们重构代码并暂时只创建游戏的主画面。我们将会跳过游戏的开始和结束画面。

GameScreen.java

StarAssault.java就变得非常简单。

GameScreen 实现了Screen接口,该接口非常像ApplicationListener,但是添加了2个重要的方法。
show() – 当主程序激活此画面的时候调用
hide() – 当主程序激活其他画面的时候调用

StarAssault 只实现了一个方法,create()方法只是激活了新实例化的GameScreen。换句话说,它创建了它,调用show()方法,随后每个周期将调用它的render()方法。

GameScreen变成了我们下一部分的焦点,因为它是游戏存活的地方。记住游戏的循环是render()方法。但是为了渲染某些东西我们首先需要创建一个世界。世界可以在show()方法中创建,因为没有其他的画面会打断我们的游戏。目前,游戏画面仅在游戏开始的时候显示。

我们会为类添加2个成员变量,并实现render(float delta) 方法。

world属性是World的实例,它保存了障碍物和Bob。
renderer是一个将world 绘制/渲染到画面的类(不久我将解释)。
方法 render(float delta)。
让我们创建WorldRenderer 类。

WorldRenderer.java:

该WorldRenderer只有一个目的。获取世界的当前状态,并呈现在屏幕上。它只有单独一个公共的render()方法,该方法被主循环调用(GameScreen中)。渲染器需要访问世界对象,所以我们在构造它的时候将world传入。对于第一步,我们将呈现元素(块和Bob)的边框来看看我们到现在有了什么。利用原生的OpenGL绘图是相当乏味,但libgdx自带的ShapeRenderer使这个任务变得很容易。

重要的行进行说明。

#14 – 声明world 为一个成员变量。

#15 – 声明一个OrthographicCamera,我们使用这台摄像机以垂直的角度看世界。当前世界还是比较小的,它只占了一个屏幕,但我们会有一个更广泛的空间,当Bob在里面移动的时候,摄像机会跟随着它。它类似于现实生活中的相机。更多关于正交投影的内容可以在这里找到。

#18 – 声明了ShapeRenderer。我们将用它来绘制实体的图元(矩形)。这是一个辅助渲染器,可以像画很多图元,比方说直线,矩形,圆。对于熟悉基于图形的画布的人来说,这应该很简单。

#20 – 构造器以world作为参数。

#22 – 我们创建了一个拥有10单位宽、7单位高视口的摄像机。这也就意味着在用块(宽=高=1)来填充屏幕的时候,X轴上显示10个框,Y轴上显示7个。

重要说明 这与分辨率无关。如果屏幕分辨率为480×320,这意味着480像素代表10个单位,所以一个框将是48个像素宽。这也意味着,320像素代表7个单位,所以在屏幕上盒框是45.7像素高。这不会是一个完美的正方形。这取决于纵横比。在我们这个例子里纵横比为10比7。

#23 – 这一行定位摄像机来看在房子的中间。默认情况下,它看在(0,0),这是房间的一角。相机的(0,0)在中间,就和一个普通相机一样。下图显示了世界和摄像机设置的坐标。

#24 – 相机内部的矩阵被更新。 在每次相机操作(移动,缩放,旋转等)的时候都必须调用update方法。 OpenGL被完美隐藏。

render() 方法:

#29 – 我们将相机里的矩阵应用到渲染器。这是有必要的,因为我们已经定位了相机,我们希望它们是一致的。

#30 – 告诉渲染器我们要绘制矩形。

#31 – 我们要绘制块,所以要迭代访问世界里的所有块。

#32 – #34 – 提取每个块的边界矩形的坐标。 OpenGL要使用顶点(点),所以它绘制矩形时必须知道起点坐标和宽度。需要注意,我们在使用摄像机的坐标工作,它与世界坐标重合。

#35 – 矩形的颜色设置为红色。

#36 – 在X1,Y1位置利用给定的宽度和高度绘制矩形

#39 – #44 – 对于Bob做同样的操作。但这次的矩形是绿色的。

#45 – 我们让渲染器知道我们已经绘制完矩形。

我们需要将renderer和world添加到GameScreen(主循环),然后在行动中看到它。

像这样修改GameScreen。

render(float delta)有3行代码。前2行代码用黑色清空屏幕,第3行代码简单的调用renderer的render()方法。

World 和 WorldRenderer 在画面显示的时候被调用。

为了在桌面和Android上测试,我们需要为这两个平台创建启动器。

创建桌面和Android的启动器

我们在开始的时候已经创建了2个项目,star-assault-desktop和star-assault-android,后者是一个Android项目。
桌面项目非常简单,我们需要创建一个拥有main方法的类,该方法实例化一个libgdx提供的application类。
在桌面项目中创建StarAssaultDesktop.java类。

这就是了。第 #7 行做了所有事情。它实例化了一个新的LwjglApplication应用程序,传入一个新生成的StarAssault实例,它是一个游戏的实现。第二和第三参数告诉了窗口的尺寸。我选择了480×320,因为它是众多Android手机的支持的分辨率,我想就像它在桌面上。最后一个参数告诉libgdx使用OpenGL ES2。
作为一个普通的Java程序运行该应用,应该输出以下结果:

如果有错误,追溯一下,确保设置的正确并且遵循了所有的步骤,包括检查star-guard项目properties -> Build Path->export选项卡内的gdx.jar。

Android版本

在 star-assault-android 项目里有一个单独的类,叫做StarAssaultActivity。
转到StarAssaultActivity。

注意,新的activity 继承自AndroidApplication。
#13行,一个AndroidApplicationConfiguration对象对创建。我们可以设置有关Android平台的所有类型的配置。他们都不需要解释,但要注意,如果我们想要使用Wakelock,还需要修改AndroidManifest.xml文件。这要从Android中请求权限,以保持设备运行,防止我们不触碰的时候屏幕变暗。

添加以下行到AndroidManifest.xml文件里标签内的某个地方。

另外,在 #17行,我们告诉Android使用的OpenGL ES 2.0。这意味着我们将只能在设备进行测试,因为仿真器不支持OpenGL ES 2.0。但如果是出了问题,把它改为false。

#18行初始化Android应用程序并运行。
有一个连接到eclipse的设备,应用就会被直接部署到上面,下面你可以看到一张在Nexus One上运行应用程序的照片。它看起来和桌面版本一样。

MVC 模式

在这么短的时间里我们走了这么远真是相当令人印象深刻啊。注意使用MVC模式。它非常简单和有效。模型是我们想要显示的实体。视图是渲染器。视图把模型绘制到了屏幕上。现在,我们需要和实体(尤其是Bob)进行交互,我们将介绍一些控制器了。
要了解更多的MVC模式的东西,可以看看我的其他文章,或在网络上进行搜索。这是非常有用的。

添加图片

到目前为止,一切都很好,但显然我们要使用一些适当的图形。 MVC的功能就派上用场了,我们将修改渲染,让它绘制图像,而不是矩形。
在OpenGL中显示图像是一个相当复杂的过程。首先,它需要被加载,变成纹理,然后被映射到被几何描述的表面。 libgdx使得这个过程变得非常简单。要硬盘中的图像转换成一个纹理只用1行代码。
我们将用2个图像,因此也就是2个纹理。一个纹理用于Bob,另一个用于块。我已经创建了这两个图像,块和Bob。 Bob是Star Guard里的家伙的山寨。这些都是简单的PNG文件,我将它们复制到 assets/images目录里。我有两个图像: block.png和bob_01.png 。最终,Bob将成为一个活动的形象,所以我加了数字后缀(为未来考虑)。

首先让我们稍微整理一下WorldRenderer,把矩形的绘制提取到一个单独的方法中,因为我们调试的时候将用到它。
我们需要加载这些纹理,并把它们渲染在屏幕上。
一起来看看新WorldRenderer.java。

我来讲一下重要的代码行:
#17 & #18 – 声明视口尺寸的常量,用于摄像机。
#27 & #28 – 声明2个纹理,将用于Bob和块。
#30 – 声明SpriteBatch,SpriteBatch 负责纹理的映射、显示等等。
#31 – 这是一个在构造函数中设置的属性,确定我们是否也需要渲染调试画面。记住,调试渲染只会渲染游戏元素的边框。
#32 – #35 – 这些变量都是正确显示元素所必须的。width和height保存了屏幕的像素大小,是操作系统在调整尺寸的步骤里传进来的。ppuX 和ppuY是每个单位长度的像素的数量。

因为我们将相机设定为在世界坐标中具有10×7的视口(这意味着我们横向显示10个框和垂直显示7个),最终结果是我们要处理像素,我们需要将这些值映射为实际的像素坐标。我们选择了在480 ×320分辨率下工作。这意味着,480水平像素等效为10个单位,意味着一个单位将包括屏幕上48个像素。
如果我们试图用相同的单位来表示高度( 48像素),我们得到336像素( 48 * 7 = 336)。但我们只有320像素,而我们想要显示完整的7块的高度。要垂直方向达到这种显示效果,我们得到垂直的1个单元是320 /7 = 45.71个像素。我们需要稍微扭曲一下图像以适应我们的世界。
这样做完全正常,OpenGL实现起来也非常容易。在我们改变电视的长宽比时也会发生这种情况,有时图像被拉长或压扁,以适应屏幕上,或者我们只是简单地选择削减图像保持纵横比的选项。
注意:在这里我们使用了float,即使屏幕分辨率为整数,OpenGL喜欢使用浮点数,我们就这样给它。 OpenGL的将计算出尺寸和放置像素的地方。

#36 – setSize (int w, int h) 方法在每次屏幕调整的时候被调用,它简单的计算(重新计算)单位的像素。

#43 – 构造函数稍微改变了一下,但它是非常重要的。它实例化SpriteBatch并加载纹理(#50行 ) 。

#53 – loadTextures()方法和它的名字一样:加载纹理。你看这是多么的简单。要创建一个纹理,我们需要传递一个文件处理器,它创建了一个纹理出来。在libgdx文件处理器非常有用,因为我们并不区分Android和桌面,我们只是指定要使用一个内部文件,它知道如何加载它。请注意,对于路径,我们跳过了assets,因为assets是源代码目录,这意味着该目录下的所有内容都会被拷贝到最终包的根目录下。这样assets类似于一个根目录。

#58 – 新的render()方法只包含几行。

#59 & #62 – 闭合SpriteBatch的渲染块/会话。 每次我们想通过SpriteBatch 来在OpenGL里渲染图像时,都需要调用begin(),渲染我们的东西,然后在结束的时候调用end()。必须要这样做,否则它无法工作。你可以在这里阅读更多关于SpriteBatch的东西。

#60 & #61 –简单地调用2个方法,首先呈现块,然后Bob。

#63 & #64 – 如果调试启用,调用该方法来渲染框。前面详细的介绍过drawDebug方法。

#67 – #76 – drawBlocks和drawBob方法类似。每个方法都使用纹理参数调用SpriteBatch的绘制方法。理解这一点很重要的。
第一个参数是纹理(从磁盘加载的图像)。
第二个和第三个参数告诉SpriteBatch显示图像的地方。注意,我们使用从世界坐标到屏幕坐标的转换。这里使用了ppuX和ppuY。你可以自己动手进行计算,看看图像应该显示在什么地方。SpriteBatch默认使用原点(0,0)在左下角的坐标系统。

就这些了。只是要保证你修改了GameScreen类,让resize方法调用render,同时还要把render的debug参数设置为true。

GameScreen中的修改。

运行该应用程序应该产生以下结果:
没有调试。

调试渲染。

太棒了!给它在Android上一试了,看看它的外观。

处理桌面端和Android的输入

我们已经走过了很长的路,但到目前为止,世界还是静止的,没有什么有趣的事情。要做一个游戏,我们需要添加输入处理,来拦截按键和触摸,并基于这些创建一些行动。
桌面上的控制模式非常简单。箭头键将控制Bob左右移动,Z是跳跃,X是发射武器。在Android上,我们将有不同的方法。我们将为这些功能指定一些按钮,并把它们放到屏幕上,通过按压相应区域,我们会认为某个键被按下。
遵循MVC模式,我们会将控制Bob和世界其他部分的类与模型和视图类分开。创建包net.obviam.starassault.controller,所有的控制器都放在那里。
对于一开始,我们将控制鲍勃通过按键。要发挥我们需要跟踪的4个按键的状态比赛:向左移动,向右移动,跳跃和射击。因为我们将使用2种类型的输入(键盘和触摸屏),实际事件需要被送入一个处理器可以触发动作。
每个动作都由事件触发。
向左移动的动作由左箭头键按下或屏幕的特定区域被触摸时的事件引发。
当Z键被按下时跳跃动作被触发等等。
让我们创建一个非常简单的控制器,叫做WorldController。

WorldController.java

#11 – #13 – 为Bob执行的动作定义一个枚举。每一次按键/触摸都会触发一个动作。

#15 – 定义游戏中世界。我们将控制世界里面存在的实体。

#16 – 定义Bob作为私有成员,它只是世界里面的Bob的一个引用,但是我们需要它,因为每次引用它比在需要它的时候每次都获取要容易。

#18 – #24 – 它是一个静态的储存了键和状态的哈希映射。如果键被按下,则为true,否则为false。它被静态的初始化了。这个映射会用来在控制器的update方法中计算如何处理Bob。

#26 – 这是使用World 作为参数的构造器,同时也得到了Bob的引用。

#33 – #63 – 这些方法只是简单的进行回调,在动作按钮被按下或者在指定的区域触控。这些方法在我们进行任何输入的时候都会被调用。它们简单地设置映射内对应的按键按下的值。正如你所看到的,控制器也是一个状态机,它的状态是由按键映射决定的。

#66 – #69 –在主循环每次调用时都会调用的更新方法。目前它做了2件事情:1-处理输入,2-更新Bob。Bob有一个专门的更新方法,后面我们会看到。

#72 – #92 – ProcessInput方法轮询键映射的键,并由此来设置Bob的值。例如#73 – #78行,检查向左运动的按键被按下,如果是这样,设置Bob面朝左边、状态为State.WALKING但它的速度是负值。负值是因为在画面上,左为负方向(原点在左下方并且指向右侧)。

向右也是一样。有一些额外的检查,如果这两个键被按下或都没有按下,在此情况下,Bob变成State.IDLE状态,它的水平速度为0。

让我们看看Bob.java中改变的部分。

只是把SPEED常量改为4单位(块)/秒。

还添加了setState方法,因为我之前忘了。

最有意思的是新获得的update(float delta),它在WorldController中被调用。本方法简单的根据Bob的速度更新它的位置。简单起见,我们只做了这么多而没有检查它的状态,因为控制器负责根据Bob的方向和状态设定它的速度。在这里我们使用了向量数学,libgdx起了很大作用。

我们简单的把Bob的当前位置加上delta秒内移动的距离。我们使用velocity.tmp(),因为tmp()方法创建了一个与速度对象具有相同值的新对象,我们把这个对象的值与所经过的时间delta相乘。在java中我们必须小心的使用引用,因为速度和位置都是Vector2对象。关于向量的更多的信息请访问这里

我们几乎做了所有的事情,我们只需要在事件发生的时候调用正确。 libgdx有一个输入处理器,它有几个回调方法。因为我们在使用GameScreen 作为游戏界面,它自然就成为了输入处理器。要做到这一点,GameScreen中将实现InputProcessor。

新的GameScreen.java

变化如下:

#13 – 该类实现了InputProcessor。

#19 – Android触摸事件使用的宽度和高度。

#25 – 利用world实例化WorldController。

#26 – 设置当前画面作为应用程序的当前输入处理器。libgdx 把输入处理器当作全局的,所以如果不共享的话每个画面都需要单独设定。在这种情况下,画面本身处理输入。

#47 & #62 – 我们设置全局的输入处理器为null只是为了清理。

#68 – 每当物理键盘上的键被按下时都会触发keyDown(int keycode)方法。参数keycode就是按键的值,在这种方式下,我们可以查询它,如果是我们期望的键就做一些操作。这正是事情的原理。基于我们想要的键,我们把事件传递给控制器。该方法也返回true,让输入器知道输入已经被处理。

#81 – keyUp 与keyDown 方法完全相反,当按键被释放时,它只是委托给WorldController。

#111 – #118 – 这里就让它变得有趣了。它只在触摸屏上发生,坐标和按钮同时被传入。指针是多点触控,代表了它捕获的触摸事件的ID。

这些控制都非常简单,只是为了简单的演示之用。屏幕被分为4部分,如果点触左下象限,那么会被当作触发左移的操作,并且把与桌面版相同的事件传递给控制器。

对于touchUp也是完全一样的东西。


警告: -这是非常错误并且不可靠的,因为没有实现touchDragged方法,当手机划过的时候会搞乱所有的事情。这当然会被修复,其目的是为了演示多个硬件输入和如果把它们关联在一起。


在桌面版和Android上运行程序都会演示控制。在桌面版上使用方向键,在Android上触摸屏幕下部角落将移动Bob。

在桌面版上,你会发现使用鼠标来模拟触控也可以用。这是因为touchXXX 也处理桌面上的鼠标输入。为了解决这个问题,在touchDown和touchUp方法的开始处添加下面的代码。

如果应用程序不是Android,程序返回false并且不执行方法其余部分的代码。请记住,false意味着输入没有被处理。

正如我们看到的,Bob移动了。

简短回顾

到目前为止,我们介绍了不少游戏开发的东西,并且已经做出来一个可以展示的东西。

我们逐渐为我们的应用引入了工作元素,并且一步一步的做了些东西。

我们仍需要添加:
* 地形的交互(块碰撞,跳跃)
* 动画
* 跟随Bob的镜头
* 敌人和攻击敌人的武器
* 音效
* 控制的定义和微调
* 游戏结束和开始的更多画面
* libgdx更有趣东西

本项目的源码可以在这里找到:

https://github.com/obviam/star-assault

分支是 part1。

用git检出:
git clone -b part1 git@github.com:obviam/star-assault.git

你也可以下载一个压缩文件

你也可以进入这个系列的下一篇文章,在里面我为Bob加入了动画。

收藏 评论

关于作者:lum

新浪微博:@鲁猛-lum 个人主页 · 我的文章

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部