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

初学者移动游戏编程:第一部分,共四部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (33投票s)

2009 年 4 月 30 日

CPOL

14分钟阅读

viewsIcon

131184

downloadIcon

2991

J2ME 游戏编程入门指南的四部分中的第一部分。

引言

本文是我《移动游戏编程入门》系列的第一部分。本系列将从一个非常简单的游戏开始,但会继续展示如何实现各种不同类型的游戏以及用于编写它们的编程技术。

本系列共四部分,我将介绍以下游戏:

  • 基础
  • 打砖块 (BreakOut)
  • 俯视角卷轴游戏 (Top Down Scroller)
  • 3D 太空游戏 (3D Space Game)

本部分将向您展示如何实现一个非常简单的游戏,您需要哪些工具,以及如何在您的手机上实际尝试它。

在这个游戏中,目标是将您的角色导航到一个目标点,没有障碍物,目标甚至不会移动。没有“死亡”的方式,没有“失败”的方式,但这是一个展示游戏编程基础知识的好起点。本部分我将涵盖的领域是:

  • 必备工具:所需的应用程序(或者至少是我喜欢使用的那些)。
  • 创建 MIDlet:可以在您的手机上运行的应用程序。
  • 基本游戏循环:任何游戏的基础。
  • 读取用户输入。

工具

本系列中的所有游戏都已使用三个软件包进行开发:

  • Java 6 SDK
  • NetBeans
  • Paint.NET

Java 6 SDK

Java SDK(或 JDK,Java Development Kit)是开发 Java 应用程序时使用的类库、工具和文档的集合。它不应与 JRE(Java Runtime Environment,用于运行 Java 应用程序)混淆。我从这里下载了我的 JDK。您需要安装 JDK 才能使用列表中的下一个工具 NetBeans,所以请确保这是您下载的第一项。

NetBeans

NetBeans 是一个集成开发环境 (IDE),主要用于 Java 开发。它是免费的,这很好,但它最棒的地方在于它确实很棒。在我看来,它是目前为止最好的 Java IDE。NetBeans 是您将用于实现和测试游戏的工具,因为它附带了一个漂亮的模拟器,这样您就不必在知道游戏可行之前就将其安装在手机上。

请到这里下载并安装Mobility包。Mobility包包含了J2ME JDK(Java 2 Mobile Edition),其中包含编写 MIDlet(可在移动设备上运行的 Java 应用程序)所需的类库以及所需的工具(例如模拟器,用于在不安装到实际手机上即可测试游戏)。

Paint.NET

几乎所有的游戏都需要某种图形才能使其具有趣味性。我更喜欢使用 Paint.NET 来创建我的游戏的图形(当游戏本身不生成图形时,在本系列的第二部分中将有更多介绍)。我没有使用 Windows 自带的 Paint 的原因是因为它不像 Paint.NET 那样包含所有漂亮的附加功能,例如对透明 PNG 文件的支持。

前往 Paint.NET 的下载页面下载它。

一旦您安装了这三个工具,您就基本准备好开始编码了。

基本游戏循环

几乎所有的游戏都依赖一个核心的游戏循环,它负责管理或控制游戏。当您编写的游戏变得越来越复杂时,从游戏循环中调用的方法将需要包含越来越多的逻辑,但实际的游戏循环仍然会是一个相当简单的循环。在其最简单的形式中,它可能看起来像这样:

while(gameShouldStillBeRunning) {
    // Capture input 
    readInput();
    
    // Update the game state
    updateGameState();
    
    // Present the game to the user, i. e. render or draw it to screen
    renderGameState();

    // Check for game over
    checkGameState();
}

读取输入

此方法负责读取或捕获输入的当前状态。例如,这包括检查按下了哪些键以及自上次检查以来鼠标移动了多远。该方法通常会将与游戏相关的输入存储在一个地方,以便 `updateGameState` 方法可以访问它。

存储游戏状态可以很简单,例如,如果按下了某个键,则将一个布尔值设置为 `true`,如果未按下,则设置为 `false`。检查输入并先存储它,而不是在实际需要它时(即在更新游戏状态时)检查它的原因,是因为读取输入通常在 CPU 方面非常昂贵,并且最好一次性获取所有必需的输入。否则,更新游戏状态的代码的不同部分可能会多次查询操作系统以获取相同的按键输入,这是不必要的。让游戏流畅快速地运行是游戏开发的重要组成部分,我将在本系列的后续部分中讨论这一点。

您会注意到本文包含的游戏并未在游戏循环中读取输入状态,这是因为它依赖于 `javax.microedition.lcdui.Canvas` 的内置功能;稍后将详细介绍。

更新游戏状态

更新游戏状态包括所有实际的游戏逻辑处理,例如根据捕获的输入移动玩家角色,根据其 AI 移动敌人,更新环境,检查游戏结束状态,以及许多其他事情,所有这些都取决于正在开发的游戏类型。

在本部分中,更新游戏状态仅包括移动角色并检查角色是否已到达目标。

渲染游戏状态

当渲染游戏状态时,游戏的状态(即玩家的位置、地图以及当前分数等)将被绘制到屏幕上。`javax.microedition.lcdui` 包中的 `Canvas` 类用于将内容渲染到屏幕,其方式类似于 `java.awt` 或 `javax.swing` 组件,通过重写 `Canvas.paint(Graphics graphics)`。请注意,`Graphics` 参数不是您可能从桌面开发中认识的 `java.awt.Graphics` 对象,它是一个 `javax.microedition.lcdui.Graphics` 对象,类似于 **AWT** 对象的一个精简版本。

检查游戏结束状态

这是游戏主要检测玩家是否输掉或赢得游戏的地方,但它也会检测并处理任何主要的状态更改。例如,完成一个关卡并进入一个显示关卡摘要然后开始下一关的状态(又是另一种状态)。

入门

项目设置

开始的第一件事是在 NetBeans 中设置一个项目;选择正确的项目类型很重要,因为项目类型决定了将使用的 JDK(记住,我们需要 J2ME JDK 才能使其在移动设备上运行)。

创建 MIDP 应用程序项目

在 NetBeans 中,选择 **File -> New Project...**,然后选择项目类别 **Mobility** 和项目类型 **MIDP Application**,然后单击 Next。

给您的项目命名;在本例中,它称为 **Basics.GameLoop**。然后,确保取消勾选 **Create Hello MIDlet**,因为这会创建一个旨在处理基于控件的 UI(带有按钮和列表)的模板,而对于游戏,所有渲染都是自定义的,因此不会使用标准控件。然后,单击 Next。

下一个屏幕显示配置选项。此处选择的设置必须与目标设备的功能相匹配。这意味着,例如,为 MIDP-2.1 编写的游戏将无法在仅支持 MIDP-1.0 的手机上运行。现在,保留此页面的默认设置,然后单击 Finish。

在您的新项目源包中,创建一个包(因为不鼓励将类保留在根包中)。您可以随意命名包;在示例应用程序中,它称为 `com.bornander.games.basics`。

在您刚刚创建的包中创建一个名为 `BasicsMIDlet` 的类,这将是应用程序的入口点。当用户选择您的 MIDlet 时,Java 运行时将在移动设备上实例化此类。确保 `BasicsMIDlet` 类扩展 `javax.microedition.midlet.MIDlet`,因为此类定义了运行时用于控制 MIDlet 的接口。`javax.microedition.midlet.MIDlet` 是一个抽象类,有三个方法必须被重写:

  • startApp()

    运行时调用此方法来启动 MIDlet,无论是当用户通过在菜单中选择 MIDlet 来请求启动时,还是当移动设备决定将控制权交还给之前暂停的 MIDlet 时。

  • pauseApp()

    运行时调用此方法来指示 MIDlet 它将失去焦点,可能是由于来电。MIDlet 是否实际暂停某些内容完全取决于 MIDlet。本文包含的示例游戏将忽略此方法,这意味着游戏在来电时会继续运行。

    如果 MIDlet 决定暂停自身,在采取措施以受控方式暂停自身后,请调用 `resumeRequest()` 方法来通知框架它有兴趣了解何时可以恢复处理。

  • destroyApp(boolean conditional)

    当 MIDlet 被销毁(关闭)时调用此方法,允许它以安全可控的方式清理其资源。

确保您的 `BasicsMIDlet` 实现这三个抽象方法,之后您的类应该看起来像这样:

package com.bornander.games.basics;
import javax.microedition.midlet.MIDlet;

public class BasicsMIDlet extends MIDlet {
    
    public BasicsMIDlet() {
    }
    
    public void startApp() {
    }
    
    public void pauseApp() {
    }
    
    public void destroyApp(boolean unconditional) {
    }
}

下一步是将 MIDlet 添加到 **Application Descriptor**,这是一组元数据,其中包含有关移动应用程序的信息。一个移动应用程序可以包含多个 MIDlet,但本例中只有一个。

右键单击 **Basics.GameLoop** 并单击 **Properties**,然后选择 **Application Descriptor**,**MIDlets** 选项卡,然后单击 **Add...**。这将允许您选择一个 MIDlet 类,为其命名和设置图标,并将其添加到描述符中。对话框会自动检测扩展 `javax.microedition.midlet.MIDlet` 的类,因此只需单击 **OK** 接受默认的建议。

就这样!移动应用程序已创建。单击 **Run Main Project** 在模拟器上进行尝试。这将启动一个手机模拟器,您可以在其中启动刚刚创建的 MIDlet。请注意,由于还没有实际实现,它不会执行任何操作。在 `BasicMIDlet.startApp` 方法中添加 `System.out.println("Hello, world!");`,然后再次运行应用程序以查看它是否已实际启动。文本将打印到 NetBeans IDE,因此不要期望在模拟器屏幕上看到它。

渲染到屏幕

编写 MIDlet 时,有一组用于标签、文本字段、复选框等的类,它们的用法与 Swing 或 AWT 中的相应类类似。由于本文是关于游戏编程的,因此不讨论这些控件,因为它们不太适合图形化游戏。

用于渲染到屏幕的类是 `javax.microedition.lcdui.Canvas`。通过扩展此类,可以重写其 `paint` 方法并实现自定义渲染。创建一个名为 `MainCanvas` 的新类,并使其扩展 `javax.microedition.lcdui.Canvas`。重写 `paint` 以在蓝色背景上渲染红色文本。

package com.bornander.games.basics;

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;

public class MainCanvas extends Canvas implements Runnable {
    
    public MainCanvas() {
    }

    // The Graphics object is used to render images, lines, shapes and
    // text to the screen using different draw methods.
    protected void paint(Graphics graphics) {

        // Get the width and height of the screen in pixels
        int w = getWidth();
        int h = getHeight();
        
        // Set the current color to blue (hex RGB value ) and draw a filled
        // rectangle the size of the screen
        graphics.setColor(0x00007F);
        graphics.fillRect(0, 0, w, h);

        // Set the current color to red, the font to the default font and 
        // draw a string to the center of the screen.
        graphics.setColor(0xFF0000);
        graphics.setFont(Font.getDefaultFont());
        graphics.drawString("Hello, world!", w / 2, h / 2, 
            Graphics.BASELINE | Graphics.HCENTER);
    }
}

为了让 MIDlet 使用此 `Canvas`,它必须被设置为显示器的当前画布。修改 `BasicMIDlet.startApp` 以创建一个 `MainCanvas`,并将其设置为默认 `Display`。

public void startApp() {
    MainCanvas mainCanvas = new MainCanvas();
    Display.getDisplay(this).setCurrent(mainCanvas);                   
}

再次运行 MIDlet;这次它应该看起来像这样:

现在,是时候开始实现实际的游戏了。

实现游戏

加载资源

尽管可以使用 `javax.microedition.lcdui.Graphics` 中的各种绘图方法渲染游戏的各个方面,但更常见的是使用图像编辑程序(如 Paint.NET)创建的图像。此游戏将使用两个不同的图像:一个用于玩家或角色,一个用于目标。

  • 角色
  • 目标

创建图像时,使用透明像素很重要;否则,角色图像看起来就不像圆的而是方形的。

通过将资源添加到 Java 包,可以将它们加载到 `javax.microedition.lcdui.Image` 对象中,然后可以将这些对象绘制到 `javax.microedition.lcdui.Graphics` 上。加载资源的便捷方法是使用 `Class.getResourceAsStream` 方法,因为它允许使用 Java 包名(但用斜杠代替点)来引用资源。此外,由于获取 `Class` 实例的最简单方法是调用 `Object.getClass`,因此总有一种简单的方法来引用与当前类在同一包中的资源。

因此,可以将图像资源放置在普通的 Java 包结构中:

通过将两个 `javax.microedition.lcdui.Image` 作为 `MainCanvas` 的成员添加,并在构造函数中加载资源,可以修改 `paint` 方法以在左上角绘制角色,在右下角绘制目标。请注意,不使用绝对坐标(如 100, 100)是良好的做法,而是根据当前屏幕宽度和高度计算坐标。显然,0, 0 始终是左上角,但右下角的坐标在不同的移动设备上会有所不同。通过找到屏幕的宽度和高度,然后减去图像的宽度和高度,可以保证目标图像始终出现在右下角,而与屏幕分辨率无关。

package com.bornander.games.basics;

import java.io.IOException;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

public class MainCanvas extends Canvas implements Runnable {
    
    // Declare members for the images
    private Image avatar;
    private Image target;
    
    public MainCanvas() throws IOException {
        // Load the image resources
        avatar = Image.createImage(getClass().getResourceAsStream(
          "/com/bornander/games/basics/resources/avatar.png"));
        target = Image.createImage(getClass().getResourceAsStream(
          "/com/bornander/games/basics/resources/target.png"));
    }

    
    protected void paint(Graphics graphics) {

        int w = getWidth();
        int h = getHeight();
        
        graphics.setColor(0x00007F);
        graphics.fillRect(0, 0, w, h);

        // Draw the images
        graphics.drawImage(avatar, 0, 0, 0);
        graphics.drawImage(target, w - target.getWidth(), 
                           h - target.getHeight();

        graphics.setColor(0xFF0000);
        graphics.setFont(Font.getDefaultFont());
        graphics.drawString("Hello, world!", w / 2, h / 2, 
                            Graphics.BASELINE | Graphics.HCENTER);

    }
}

使角色可控

始终在左上角绘制角色不会让游戏更有趣。通过声明位置的成员以及移动方向,可以使用键盘来控制角色。通常,我更喜欢将 2D 空间中角色的位置存储为某种 `Point` 对象,但由于 J2ME 库是 J2SE 的精简版本,所以没有这样的类。我可以自己编写(就像我为本系列中的其他游戏所做的那样),但决定将位置存储为两个单独的整数。我将方向存储为四个独立的 boolean,分别表示上、下、左、右。

public class MainCanvas extends Canvas implements Runnable {
    
    // The two images
    private Image avatar;
    private Image target;
    
    // The coordinates of the player.
    private int x = 0;
    private int y = 0;
    
    // Flags indicating which buttons are pressed.
    private boolean up = false;
    private boolean down = false;
    private boolean left = false;
    private boolean right = false;

    ...
}

移动角色(即根据方向标志更新其 X 和 Y 坐标)被委托给一个也执行约束检查的方法。通过约束检查,我指的是将位置约束到有效位置的过程。在此示例中,角色超出屏幕边界是不允许的。

public class MainCanvas extends Canvas implements Runnable {

    ...

    private void moveAvatar() {
        if (up)
            --y;
        if (down)
            ++y;
        if (left)
            --x;
        if (right)
            ++x;

        if (y < 0)
            y = 0;
        if (y > getHeight() - avatar.getHeight())
            y = getHeight() - avatar.getHeight();

        if (x < 0)
            x = 0;
        if (x > getWidth() - avatar.getWidth())
            x = getWidth() - avatar.getWidth();        
    }    
}

再次注意到 `getWidth`/`getHeight` 和 `avatar.getWidth`/`avatar.getHeight` 的使用,以使约束检查独立于屏幕尺寸和图像尺寸。此方法 `MainCanvas.moveAvatar` 将在每次迭代中由游戏循环调用,以更新玩家的位置。显然,这还需要对 `MainCanvas.paint` 方法进行小的更改,因为角色不再绘制在 (0, 0) 处,而是绘制在 (x, y) 处。

捕获输入

通过扩展 `Canvas`,可以通过简单地重写一些方法来轻松捕获输入。`Canvas` 公开了三个与按键输入相关的方法:

  • void keyPressed(int keyCode)
  • void keyReleased(int keyCode)
  • void keyRepeated(int keyCode)

在第一个示例中,我将仅使用 `keyPressed` 和 `keyReleased`,并且为了控制角色,这些方法需要做的就是根据按下的或释放的键来设置 `up`、`down`、`left` 和 `right`。

传递给按键处理的 `keyCode` 参数可以使用名称恰当的函数 `Canvas.getGameAction` 转换为更适合游戏编程的按键代码;这将把按键代码转换为可以检查如向上和向下等按钮的代码。

/**
 * This gets called for us whenever a key is pressed.
 * @param key The pressed key.
 */
protected void keyPressed(int key) {
    int gameKey = getGameAction(key);
    switch(gameKey) {
        case Canvas.UP: up = true; break;
        case Canvas.DOWN: down = true; break;
        case Canvas.LEFT: left = true; break;
        case Canvas.RIGHT: right = true; break;
        case Canvas.FIRE: shouldRun = false; break;
    }
}

/**
 * This gets called for us whenever a key is released.
 * @param key The released key.
 */
protected void keyReleased(int key) {
    int gameKey = getGameAction(key);
    switch(gameKey) {
        case Canvas.UP: up = false; break;
        case Canvas.DOWN: down = false; break;
        case Canvas.LEFT: left = false; break;
        case Canvas.RIGHT: right = false; break;
    }
}

检查游戏结束状态

所有游戏都需要检查游戏结束状态;这可能发生在玩家输掉游戏或赢得游戏时。无论如何,都必须进行检查,否则游戏既不能赢也不能输。

在第一个示例中,游戏结束只能发生在玩家赢得游戏时。是的,没错。这是一个你不会输掉的游戏。你可以选择不赢,但不能输。此实现调用一个名为 `isGameCompleted` 的方法来检查游戏是否结束。此方法设置一个成员变量 `completed`。要改变游戏在达到游戏结束状态时的行为,`run` 方法会查看当角色到达目标时设置的 `completed` 标志,如果为 `true`,则无论按键如何,角色都不会再移动。此外,`paint` 消息会在屏幕中央绘制一条“游戏结束”消息。

/**
 * Detects if the game has been completed
 * (i.e. the avatar has navigated to the target).
 * @return true if the game is completed.
 */
private boolean isGameCompleted() {
    return x == targetX && y == targetY;
}

此方法在 `run` 方法中调用:

protected void paint(Graphics graphics) {

    ...

    if (completed) {
        graphics.setColor(0xA0A0FF);
        graphics.setFont(Font.getDefaultFont());
        graphics.drawString("Game Over", w / 2, h / 2, 
                            Graphics.BASELINE | Graphics.HCENTER);
    }
}

public void run() {
    while(shouldRun) {
        completed = isGameCompleted();

        if (!completed) {
            moveAvatar();
        }

        repaint();

        try {
            Thread.sleep(20);
        }
        catch (InterruptedException ex) {
        }
    }
    owner.exit();
}

就这样!这就是整个游戏。它不是世界上最令人上瘾的游戏,但足以展示如何设置 NetBeans 项目和游戏的基本知识。

下一部分

在下一部分中,我将介绍菜单、简单的 AI 和基本的碰撞检测,并演示如何编写一个类似《打砖块》的游戏。

一如既往,非常欢迎对文章或代码发表评论。

© . All rights reserved.