65.9K
CodeProject 正在变化。 阅读更多。
Home

使用libgdx创建您的第一个Android游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (51投票s)

2013年12月28日

CPOL

19分钟阅读

viewsIcon

248208

downloadIcon

20731

使用libgdx创建您的第一个Android游戏

Android libgdx games

引言

在过去的几年里,涌现了许多跨平台游戏框架。这些框架的出现填补了日益增长的移动游戏兴趣和平台多样性所造成的巨大空白。如今,采用游戏引擎并开发您的第一个游戏比以往任何时候都容易。最困难的部分是选择合适的游戏框架。然而,在所有可用的选项中,有一个明显脱颖而出:libgdx。以下是 libgdx 的一些特性,使其独一无二:

  • 跨平台:可部署到桌面、Android、HTLM5 和 iOS(借助RoboVM)。
  • 您可以在桌面环境中调试和测试您的游戏,然后几乎毫不费力地将其部署到 Android 平台。
  • 高性能
  • 良好的文档,出色的社区支持
  • 支持 2D 和 3D 游戏的丰富功能。
  • 友好的开源许可(Apache 2.0)。可免费用于商业应用。

在本文中,我将概述 libgdx。我们将以一个名为“水果捕捉器”的简单 2D 游戏为例。在这个游戏中,水果会从天而降。您需要用篮子接住它们,通过触摸屏幕或滑动手机来移动篮子。为了增加趣味性,应季水果的得分更高。您当然知道要优先选择应季水果!正如我所说,libgdx 的文档非常好,所以我不会在这里涵盖基础信息。您可以在Wiki 中阅读有关设置和运行项目的信息。您还可以阅读如何创建一个简单的游戏以及如何扩展它来使用游戏屏幕。在这里,我将介绍一些重要的游戏概念,并提供一些技巧,帮助您从示例游戏过渡到可发布到市场的产品。虽然 libgdx 是一个跨平台框架,但本文将重点介绍部署到 Android 平台。

但在开始编写代码之前,我们必须先介绍两个对游戏设计很重要的主题。

游戏资源

我猜这里的大多数读者都是开发者而不是设计师。但在我看来,创造游戏更像是一门艺术而不是开发过程。伟大的游戏通常因其精美的图形和悦耳的音效及音乐而脱颖而出。这并不意味着单个开发者就没有机会创造游戏。开发者从零开始创造所有资源是困难的,但在线查找合适的资源然后使用合适的工具来改编它们则容易得多。并且,由于这里没有人是律师,所以我列出了提供可免费用于您游戏中的资源的网址列表:

  • 图像:Openclipart。公共领域 SVG 图形。
  • 声音:freesound。知识共享许可的声音。请注意单个声音的许可。
  • 字体:Font Squirrel。也许您会感到惊讶,您无法在游戏中使用原生字体。Font Squirrel 提供免费商用的字体。libgdx 提供了一个工具,可以方便地集成这些字体。

这个列表当然不是详尽的。还有其他资源中心。搜索时,请注意许可。不要仅仅使用您在网上找到的第一张图片。您需要确保有权使用它。此外,上面的列表不包括其他类型的资源,例如 3D 模型。

工具

在您在线找到合适的资源(或请设计师为您创建)后,您仍然没有准备好开始编码。大多数情况下,资源都需要进行一些编辑。有一些基本的设计工作是可以避免的。好处是这种设计实际上很有趣,而且有免费工具可供您使用。

  • Inkscape:专业的矢量图形编辑器。最好以矢量格式设计所有图像。这将允许您轻松地为任何当前或将来的屏幕分辨率调整它们的大小。
  • Gimp:图像处理程序。它具有与 Photoshop 类似的功能。大多数游戏引擎都需要位图(通常是 PNG)图像。从 Inkscape 导出图像后,您可能需要进行一些最后的润饰才能将其集成到您的游戏中。Gimp 的用户界面不是那么友好,需要一些时间来适应。但是 Gimp 功能齐全,并且在线上有许多教程和视频。
  • Audacity:用于录制和编辑声音的跨平台软件。

以上所有工具都是免费、开源且专业的。它们足以满足您将要进行的任何任务。除了上述通用工具外,还有许多游戏专用工具。有用于创建位图字体的工具,有用于将许多小图像打包成一个大图像的工具,有用于创建瓦片地图的工具。libgdx 对这类工具提供了良好的支持,其中一些将在本文稍后介绍。

游戏主循环

游戏确实有一些自己的软件设计模式。其中最普遍的当然是游戏循环。我敢肯定您听说过它。游戏循环实际上是一个 while 循环,所有处理都在其中进行。在这个循环中,您显示所有图像、播放声音、应用物理引擎和游戏逻辑。您尝试尽可能快地完成这些操作。当循环完成时,它会立即重新开始。循环每秒运行的次数等于“每秒帧数”(FPS)。FPS 越高,游戏看起来就越流畅。

大多数游戏框架通过提供一个函数(或方法)来实现游戏循环。这个方法执行所有处理,而框架负责尽可能快地调用它。我们称之为“循环方法”。下面是用伪代码表示的“循环方法”:

void gameMain(float delta) {

    renderWorld();
    
    getUserInput();
    
    update(delta);
}

此伪代码也与著名的 MVC 模式相关,该模式在游戏开发中也很流行。 “循环方法”接受一个浮点数参数。这称为 delta,实际上是自上次调用循环方法以来经过的时间。您可以将其视为 FPS 的倒数。即:delta = 1 / FPS。

renderWorld 方法在屏幕上渲染所有图像。这是游戏中的“视图”,它在适当的位置绘制所有图像。每次循环运行时都会绘制所有图像,即使它们的位置没有改变。接下来,循环读取用户输入。这可能来自触摸屏、键盘、鼠标、加速度计、游戏手柄……尽管上面看起来是循环方法的一部分,但更常见的做法是将其实现为一个单独的类(“控制器”),该类更新“模型”。最后一部分是应用物理引擎和游戏逻辑。这就是需要 delta 信息的地方。游戏可能需要完整的物理引擎,或者只需要一些简单的对象移动和碰撞检测。无论如何,都需要计时信息来实施这一点。在这一部分,将进行更新分数和检查游戏结束条件等操作。如果您曾使用过 XNA 或 MonoGame,您可能会发现这部分与 Update 方法相似。此方法实际上更新游戏的“模型”或状态。在 MonoGame 中,会特别考虑确保 Update 方法以固定的间隔调用,而不管渲染需要多长时间。换句话说,框架始终调用 Update 方法,该方法不应花费太长时间执行,每秒调用特定次数,然后利用剩余时间尽可能频繁地渲染世界。libgdx 不支持单独的 Update 方法。因此,您需要自己实现此模式。如果 renderWorld 执行时间过长(低端设备、渲染对象过多),则不应在每次循环中调用它,以便 Update 方法能定期调用,并且游戏状态保持一致。如果未能做到这一点,您可能会发现自己陷入对象穿过障碍物以及发生各种奇怪情况的境地。

libgdx 通过 ApplicationListener 接口支持游戏主循环模式。此接口定义了以下方法:

void create() //Called when the Application is first created.
void dispose() //Called when the Application is destroyed.
void pause() //Called when the Application is paused.
void render() //Called when the Application should render itself.
void resize(int width, int height) //Called when the Application is resized.
void resume() //Called when the Application is resumed from a paused state.

以上方法为游戏中需要处理的所有事件提供了入口点。render 方法是我们上面描述的主循环方法。libgdx 提供静态构造函数来读取 delta 时间(Gdx.graphics.getDeltaTime())和处理用户输入(Gdx.input)。虽然您可以直接在 render 方法中处理输入,但 libgdx 也提供了更高级的机制,允许您实现一个单独的 Controller 类(InputProcessor)。如上所述,不提供单独的 Update 方法,您需要自己实现此模式。

在其他 ApplicationListener 方法中,create 和 dispose 方法对于游戏资源的处理非常重要。由于您的游戏中将大量处理 OpenGL 资源,垃圾回收将无法提供帮助,您需要手动处理它们。我们通常在 create 方法中为游戏分配资源。这只会被调用一次,即游戏启动时。在 dispose 方法中,我们有机会处理这些资源。资源处理当然通常比这更复杂。为了快速启动游戏并避免内存不足错误,需要更复杂的资源处理。您可能需要预先分配一些资源,并在内存不足的情况下尽早释放一些资源。我们将在后面进一步讨论。

游戏屏幕

下图展示了一个典型游戏的屏幕。游戏由各种屏幕组成。每次只有一个屏幕被显示。在开发游戏时,能够以这些屏幕来思考非常方便。首先实现初始菜单屏幕,然后是游戏开始屏幕,主游戏屏幕(通常是最困难的),游戏结束屏幕,等等。

Game screens

libgdx 通过 Game 抽象类Screen 接口支持此模式。

这是一个非常轻量级的结构,您需要同时为 Game 和 Screen 对象提供实现。Game 类只不过是一个 ApplicationListener,它将渲染委托给一个或多个屏幕对象。这使得以屏幕为单位思考更容易。它还使资源处理更容易。Screen 接口提供了一个 dispose 方法,但它不会自动调用。您应该自己调用它以释放屏幕的资源。这是一件好事,因为它允许更灵活的资源处理。一个屏幕可能保持预加载状态,以便可以快速重用。总的来说,此模式将允许您实现一个资源处理服务。该服务将预加载资源,并释放即将被使用的资源。例如,当从 Level 1 切换到 Level 2 时,您需要在 Level 屏幕中释放 Level 1 资源并预加载 Level 2 资源。一个单独的服务类将负责加载和卸载资源。Game Screen 模式可以方便地在适当的时候调用此服务。

如果您查看游戏屏幕,您会注意到大多数屏幕都非常简单。除了动作屏幕外,所有屏幕都只显示一条消息并为用户提供几个操作点。它们只是简单的 UI。libgdx 提供了 Scene2d 来方便地创建这些屏幕。复制自 libgdx Wiki:

scene2d 拥有强大的功能,可用于布置、绘制和处理游戏菜单、HUD 叠加层、工具和其他 UI 的输入。scene2d.ui 包提供了许多专门用于构建 UI 的 Actor 和其他实用程序。

我个人没有在“水果捕捉器”中使用 Scene2d,但我认为您应该在自己的应用程序中考虑它。

渲染图像

以下代码片段显示了渲染图像到屏幕所需的步骤(摘自 libgdx wiki 的简单游戏示例)。首先,您需要在 create 方法中创建一个 camera 和一个 SpriteBatch 对象。在那里,您还可以创建 Texture 对象来加载图像。在 render 方法中,您使用 SpriteBatch 对象绘制纹理。您必须记住在 dispose 方法中释放纹理和 SpriteBatch 对象。

Texture bucketImage;

OrthographicCamera camera;
SpriteBatch batch;

@Override
public void create() {
    camera = new OrthographicCamera();
    camera.setToOrtho(false, 800, 480);

    batch = new SpriteBatch();
    dropImage = new Texture(Gdx.files.internal("droplet.png"));
    ...
  
}

@Override
public void render() {
    Gdx.gl.glClearColor(0, 0, 0.2f, 1);
    Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

    batch.setProjectionMatrix(camera.combined);
    batch.begin();
    batch.draw(bucketImage, bucket.x, bucket.y);
    batch.end();
}
 
@Override
public void dispose() {
    bucketImage.dispose();
    batch.dispose();
    ...
}

使用 libgdx 绘制图像很容易。(如果您认为上述代码很复杂,您应该研究一下如何在没有框架的情况下使用 OpenGL 绘制图像)。然而,上述代码只适用于非常简单的游戏。在实际情况中,您需要为图像绘制代码添加一些抽象层。

首先,如果您有大量小图像,则不应逐个加载它们。相反,您应该将它们“打包”到一个大图像中,将该图像加载到 Texture 中,然后从中选择区域。下图展示了“水果捕捉器”中的所有小图像打包成了一个。

game images atlas

libgdx 非常容易创建打包的图像。您可以直接从桌面项目执行此操作。您所要做的就是将 gdx-tools.jar 添加到类路径中,并编写几行代码。我在“水果捕捉器”中是这样做的。我定义最大高度和宽度为 1024。如果图像不适合在一个 1024x1024 的图像中,则会创建更多图像。这对您来说是完全透明的,因为选择区域的代码不会改变。TexturePacker2.process 获取来自特定文件夹的所有图像,对它们进行打包,并将打包后的图像复制到 Android 项目的 assets 文件夹。它还会创建一个“atlas”文件。这是一个简单的文本文件,其中包含单个图像在打包图像中的位置。在“水果捕捉器”中,创建了三个“atlas”:一个用于小图像,一个用于英文文本图像,一个用于德文文本图像。

Settings settings = new Settings();
settings.maxWidth = 1024;
settings.maxHeight = 1024;
TexturePacker2.process(settings, "images", "../FruitCatcher-android/assets", "game");
TexturePacker2.process(settings, "text-images", "../FruitCatcher-android/assets", "text_images");
TexturePacker2.process(settings, "text-images-de", "../FruitCatcher-android/assets", "text_images_de");
 

在调用上述代码后,您需要刷新 Android 项目才能识别更改。此外,您应该注意 TexturePacker2 工具使用的命名约定,以便支持动画。您可以将一系列图像命名为:image_n.png,其中 n 是一个整数。数字 n 实际上表示帧索引。如果您有一个由多个帧组成的精灵,此约定将使加载这些帧变得容易。

 // Create an atlas object
TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("game.atlas"));

// Load Star frame 1
TextureRegion startTexture1 =  atlas.findRegion("star", 1);

// Load Star frame 2
TextureRegion startTexture2 =  atlas.findRegion("star", 2);

// Load Image button
TextureRegion buttonTexture = atlas.findRegion("button");

在复杂的游戏中,图像很多时,加载所有图像可能需要 considerable time。游戏通常的做法是在开始时显示一个“加载”屏幕,并开始加载图像和其他必需的资产。当一切加载完成后,游戏就可以开始。开始时有一个延迟,用户可以忍受,然后游戏就可以流畅运行。libgdx 在 AssetManager 的帮助下支持此模式。

正如您所见,在实际应用程序中加载图像并不那么简单。您可能需要使用 AssetManager、TexturePacker 或两者都使用。您可能还需要实现一个自定义的加载和卸载方案,该方案在游戏的屏幕之间运行。因此,我认为最好创建一个单独的服务来为其他代码层提供所有图像。我在“水果捕捉器”中称此服务为 ImageProvider。拥有此服务将使您能够尝试不同的加载和卸载技术,而不会影响整个源代码库。如果您从一开始就以这种方式设计游戏,将会非常有益。

渲染文本

如果您以前从未开发过游戏,您可能会惊讶地发现,显示文本需要特别考虑。但是,您应该理解,出于性能原因,游戏框架使用 OpenGL,并且无法将其与可以使用系统字体的原生代码结合使用。幸运的是,libgdx 可以非常轻松地创建位图字体,然后在您的应用程序中使用它。libgdx 采用了 Hiero 位图字体工具。这是一个开源工具。以前很难找到一个可用的版本。现在 libgdx 维护它作为一个工具,您可以直接从 libgdx 二进制文件中运行它。Hiero 可以加载任何系统字体并将其转换为 libgdx 可识别格式的位图字体。它还可以直接加载 TTF 字体,而无需安装它们。使用 Hiero 时,请注意许可。仅使用您有权使用的字体。创建位图字体后,就可以轻松加载它并在应用程序中使用它来显示文本。

// Loading a bitmap font 
BitmapFont font = new BitmapFont(Gdx.files.internal("fonts/poetsen.fnt"),
                                 Gdx.files.internal("fonts/poetsen.png"), false);
                                 
// Draw a string with a SpriteBatch object
font.draw(spriteBatch, line_string, lineX, lineY);

状态

您一开始可能没有意识到的一点是,您的游戏必须能够保存其状态然后从中恢复。这在可能会因电话呼叫而中断的移动设备上尤其重要。当玩家在中断后返回游戏时,他们希望游戏停留在他们离开时的状态。如果他们即将获得高分,却发现必须从头开始,他们会生气,您应该会收到一些非常负面的评论。

您的游戏必须能够从头开始,并且从一个已知状态开始。游戏必须不断更新其状态,并在适当的时候将其持久化到非易失性位置。libgdx 提供了 JSON 工具,可用于以跨平台的方式序列化和反序列化类。在“水果捕捉器”中,有两个独立的状态类:GameScreenState 用于存储当前游戏的状态(收集到的分数、剩余时间、空中的水果),GameState 则包含游戏会话的信息。两者都在游戏的 pause 方法中保存。如果您有更复杂的状态对象,需要花费大量时间进行序列化,则应定期间隔序列化它们,而不是等到 pause 方法。

调用 Android 原生代码

在某些情况下,调用 Android 原生代码可能很有必要:

  • 显示广告
  • 应用内购买
  • 使用原生用户输入
  • 连接排行榜

Wiki 中,您可以阅读有关如何将 libgdx 游戏与 AdMob 集成的信息。我使用了描述的技术在“水果捕捉器”中显示 AdMob 广告。但是,我还需要原生代码来获取用户的姓名,以防获得高分,并显示前 5 名的分数。与原生代码的集成实际上发生在 AndroidApplication 类内部。这是 libgdx 提供的一个扩展了 Android Activity 的类。为了从 libgdx 调用原生代码,您应该执行以下操作:

  • 创建一个接口,其中定义了需要调用原生代码的操作的入口点方法。您的 AndroidApplication 实现将实现此接口。我称此接口为 GameEventListener。此接口属于 libgdx,而不是 Android 项目。它也可以从桌面项目访问。
  • 您的 com.badlogic.gdx.Game 实现将接受一个 GameEventListener 作为构造函数参数。对于您不想实现原生功能的平台,可以将其传递为 null。我需要“水果捕捉器”的桌面版本进行快速调试和测试。但我不需要为它显示广告和高分。我只需为其传递一个 null GameEventListener。在我的 Game 类中,我在调用 GameEventListener 方法之前检查 null。
  • 完成上述操作后,您将在 AndroidApplication 类中获得入口点方法。libgdx 引擎将调用这些方法。由于 libgdx 不在 UI 线程中运行,因此您无法从它们访问 UI。相反,您需要使用消息处理程序来发布将触发相应 UI 操作的消息。

编译说明

文章顶部的 zip 文件不包含 libgdx 二进制文件(这会使 zip 文件过大)。但是,添加它们非常容易。下载 libgdx 并运行 gdx-setup-ui.jar。这允许您创建一个新项目或更新现有项目。点击“Update”,选择解压 zip 文件的位置,然后将添加所需的​​文件。有一件事不会被添加,那就是 gdx-tools.jar 文件。您需要手动将其添加到桌面项目的 libs 文件夹中。

我还将代码放在 GitHub 上。您可以从那里克隆存储库。所有必需的二进制文件都包含在内。最后,您可以从 Google Play 下载游戏。请注意,此版本的游戏会显示广告。页面顶部的 APK 是无广告版本。

摘要

如果这篇文章对您来说太长,并且您直接滚动到底部,以下是总结。

  • libgdx 是一个跨平台的开源游戏框架,可免费用于商业应用。
  • libgdx 通过 ApplicationListener 接口实现游戏主循环模式。它还实现了游戏屏幕模式。
  • libgdx 可以轻松地在屏幕上渲染图像。它还提供了将小图像打包成大图像的工具,以及在显示启动屏幕的同时在后台加载图像的工具。
  • 使用 libgdx,您只需一行代码即可嵌入位图字体。并且,您可以使用提供的实用程序从系统中安装的任何字体创建位图字体。
  • 调用原生代码通常是必需的(显示广告、应用内购买、原生输入文本、与排行榜服务集成)。
  • 定期保存游戏状态是必要的。libgdx 通过跨平台 JSON 序列化器使此操作变得容易。

历史

  • 2013/12/28:首次提交
  • 2014/01/04:添加了编译说明
© . All rights reserved.