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

使用 J2ME 编程 2D 游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (16投票s)

2009 年 4 月 23 日

CPOL

6分钟阅读

viewsIcon

252722

downloadIcon

17112

在自己的手机上运行自己的游戏很容易。

GameProgrammingInJ2ME/game_screen.jpg

引言

J2ME 是一个有趣的游戏开发环境。凭借 Java 基础知识、预装的 NetBeans 和 J2ME 无线工具包,您可以制作简单有趣的 2D 游戏,并能够在您自己的移动设备上运行。

本文将使用包 javax.microedition.lcdui.game 中包含的 5 个类的 Game API。

背景

本帖子假设您具备 Java 基础知识,熟悉 NetBeans,并已阅读“Java ME 编程入门”帖子。游戏制作还需要一定的物理学知识,包括牛顿运动定律、运动、碰撞等。

最近,我参加了一个由我的大学及其联盟组织的名为“移动应用开发强化项目”的项目。该项目由 Uramus 资助,该计划旨在鼓励欧洲国家之间的学生交流。我们从 J2ME 开始,这是本帖子的结果。因此,在本帖子中,我将使用该项目中的一些材料。

通过使用 GameBuilder,游戏制作过程变得更加容易。但是,我不会在本帖子中介绍它。使用 GameBuilder 制作游戏的详细信息可以在这里找到。

作为一名学生,我可能没有足够的经验来应用最佳实践。因此,我热烈欢迎任何评论或建议,以使其成为更好的指南。

Using the Code

MainMidlet

作为 MidletMainMidlet 必须扩展可在包 javax.microedition.midlet 中找到的 abstractMidletMidlet 需要重写三个方法

  • startApp() 用于启动游戏
  • pauseApp() 用于暂时停止应用程序,例如,接到电话。应用程序应停止动画并释放不需要的资源。应用程序可以通过调用 resumeMIDlet() 恢复
  • destroyApp(boolean unconditional) 在退出应用程序时调用。为了终止,MIDlet 可以调用 notifyDestroyed()

(这些方法通过在 NetBeans 中创建 Visual Midlet 自动创建。)

但是,我们只需要通过创建 GameCanvas 的实例并添加 CommandListener 来退出 Midlet 来实现 startApp() 方法。当然,这不是一个好的编程习惯,但在这一步之前,我们可能只希望应用程序运行。当前显示可以在 GameCanvas 的末尾或内部通过 setCurrent 方法设置为 GameCanvas。此方法接受任何 Displayable 对象作为参数。

public class MainMidlet extends MIDlet implements CommandListener {
    private SSGameCanvas gameCanvas ;
    private Command exitCommand ;
    public void startApp() {
        try {
           //create new game thread
            gameCanvas = new SSGameCanvas();
            gameCanvas.start(); // start game thread
            exitCommand = new Command("Exit",Command.EXIT,1);
            gameCanvas.addCommand(exitCommand);
            gameCanvas.setCommandListener(this);
            Display.getDisplay(this).setCurrent(gameCanvas);
        }
        catch (java.io.IOException e) { e.printStackTrace();}
    }
public void pauseApp() {} 
public void destroyApp(boolean unconditional) {}
public void commandAction(Command command, Displayable displayable) {
        if (command == exitCommand) { 
            destroyApp(true); 
            notifyDestroyed();
        } 
    } 
}

GameCanvas

作为低级 UI 引擎的一个元素,当与 Graphics 结合使用时,GameCanvas 为我们提供了灵活的工具来设置我们自己的游戏屏幕。使用 Graphics,您基本上可以绘制您在 Java 2D 中通常可以做的事情,包括绘制形状、字符串或图像。GameCanvas 是原始 Canvas 的扩展,对绘制和按键传递到游戏中的速率有更多控制。

使用 GameCanvas,您可以注意到它的功能,包括离屏缓冲。当您绘制东西时,您可能正在离屏绘制,通过调用 flushGraphics() 方法,缓冲区会快速写入屏幕。

GameCanvas 还通过允许我们使用 getKeyState() 方法查询按键状态来简化获取输入的过程。然而,按键状态的处理留给 GameManager,以便于管理。

游戏坐标系的原点位于屏幕的左上角,如图所示。

GameProgrammingInJ2ME/gamescreen.jpg

render 方法中,清除离屏,并通过调用 GameManagerpaint 方法渲染图形。

public void render(Graphics g) {

        ……..
       // Clear the Canvas.
        g.setColor(0, 0, 50);
        g.fillRect(0,0,WIDTH-1,HEIGHT-1);
        ….….
        gameManager.paint(g);
    }

在这个例子中,SSGameCanvas 实现了 Runnable 接口,这导致创建了 run() 方法,在该方法中我们创建了一个游戏循环,直到达到某个结束条件。

GameProgrammingInJ2ME/game_states.jpg

游戏循环

  public void run() {

 
        while (running) {
            // draw graphics 
            render(getGraphics());
            // advance to the next tick 
            advance(tick++); 
            // display 
            flushGraphics();
            try { Thread.sleep(mDelay); } 
            catch (InterruptedException ie) {} 
        }
    }

游戏的计时由一个名为 tick 的整数控制。tick 简化了游戏中的计时问题,例如飞船的射击速率,或者星星会闪烁多长时间,精灵会进入下一帧多长时间。如果计时是在 GameCanvas 中通过实现 Runnable 完成的,则一个 tick 意味着 mDelay + 完成一个游戏周期的时间(毫秒)。如果我们创建一个负责 tick 的线程,我们可能会有 tick = mDelay。我们可能只需要 24 - 30 帧/秒,因此我们相应地限制 mDelay,以实现所需的动画效果并降低功耗。在游戏的每个周期中,我们调用 GameManager 的 advance 方法(它扩展了 LayerManager)来检查用户输入、碰撞并绘制图形。

public void advance(int ticks) {
       // advance to next game canvas
        gameManager.advance(ticks);
        this.paint(getGraphics());
    }
}

tick 可能有一个限制:它通过整数的限制限制了游戏时间,在 32 位系统中大约超过 590 小时。

Sprite

精灵扮演游戏中的角色。它可以是马里奥游戏中的马里奥角色、鸭子和子弹,或者是星球大战游戏中的宇宙飞船。作为基本的视觉元素,它可以渲染以显示连续的动作,其中有几帧存储在图像中。图像文件必须打包精灵的所有帧才能显示。所有帧必须具有相同且预定义的 widthheight

public SpaceShip(Image image, int w, int h) throws java.io.IOException {
       super(image,w ,h);
        WIDTH = w;
        HEIGHT= h;
        setFrameSequence(SEQUENCE);
        defineReferencePixel(WIDTH/2,HEIGHT/2);
        setTransform(this.TRANS_MIRROR_ROT270);
    }

为了初始化 Ship 精灵,我从超类 Sprite 调用构造函数:super(image, w, h);其中 wh 是每帧的 widthheight。图像包含 8 帧,因此我使用 setFrameSequence 方法设置帧序列 {0,1,2,3,4,5,6,7}。

接下来,我调用 defineReferencePixel 方法将参考点设置到帧的中间。此参考像素将用于在屏幕上定位飞船。最后,我通过 setTransform 方法旋转所有帧。

public void advance(int ticks) {
        if ((ticks%RATE==0))
            nextFrame();
    }

advance 方法将根据 RATE 改变飞船的帧以创建动画。通过连续调用 nextFrame(),屏幕将显示从 0 到 8 的帧序列,然后返回到 0:0,1,2…7,0,1,2…。以下方法 moveLeft()moveRight()moveUp()moveDown() 根据飞船的 speedXspeedY 改变飞船在屏幕上的位置。

public void moveLeft () {
       if (this.getRefPixelX()>0)
            this.move(-speedX, 0);
    }
public void moveRight (int m) {
        if (this.getRefPixelX() < m)
            this.move(speedX, 0);
    }
public void moveUp () {
        if (this.getRefPixelY()>0)
            this.move(0, -speedY);
    } 
public void moveDown (int m) {
        if (this.getRefPixelY()<m)
            this.move(0, speedY);
    }

当飞船被命令发射子弹时,我们通过将当前时间与上次射击时间进行比较来检查冷却时间是否结束。

public Bullet fire (int ticks) {
        if (ticks- fireTick > SHOOT_RATE) {
           fireTick = ticks;
           bullet.setSpeed(BULLET_SPEED);
           bullet.shot(this.getRefPixelX(), this.getRefPixelY()+HEIGHT/2);
            return bullet;
        }
        else
           return null;
   }

为了检查精灵、图像或 TitledLayer(稍后将提及)之间的碰撞,我们使用 collidesWith 方法。我将在 GameManager 中使用此方法来检查飞船和小行星之间的碰撞以减少飞船的生命值,并检查子弹和小行星之间的碰撞以增加分数并摧毁子弹和小行星。

GameManager

作为 LayerManager 的子类,GameManager 能够管理一系列层,并自动以适当的顺序渲染每个层。调用 append 方法将特定层添加到 LayerManager

private Image shipImage;
    private static final String SHIP_IMAGE    = "/resource/blue_ship.png";
    private static final int    SHIP_WIDTH    = 40;
    private static final int    SHIP_HEIGHT   = 33;
shipImage = Image.createImage( SHIP_IMAGE );
        // create space ship
        ship = new SpaceShip(shipImage, SHIP_WIDTH, SHIP_HEIGHT);
        // set it position
       ship.setRefPixelPosition(height/2, width/2);
        this.append(ship);

为了响应用户输入,我们使用 getKeyStates() 方法查询按键状态,并使用我们 GameCanvas 的引用实例。对于每个按键状态,我们通过将飞船移动到所需方向来相应地做出反应。

int keyState = gameCanvas.getKeyStates();
 
// move right
if ((keyState & GameCanvas.RIGHT_PRESSED)!= 0){
       ship.moveRight(width);
} 

// move left
if ((keyState & GameCanvas.LEFT_PRESSED)!= 0){
       ship.moveLeft();
}

// move up
if ((keyState & GameCanvas.UP_PRESSED) != 0){
        ship.moveUp();
} 

// move down
if ((keyState & GameCanvas.DOWN_PRESSED) != 0){
        ship.moveDown(height);
}

GameManager 中,我们还检查飞船、子弹和随机创建的敌人(小行星)之间的碰撞。如果发生碰撞,我们根据碰撞改变游戏状态。

private Obstacle checkCollisionWithAsteroids(Sprite t) {

        for (int i =0; i < MAX_OBS;i++) {
            if (obs[i]!=null)
                 if (obs[i].collidesWith(t, true)) {
                 return obs[i];
            }
        }
        return null;
    }

如果我们达到游戏结束条件,我们显示高分并使用 stop 方法停止 GameCanvas 线程

protected void endGame(Graphics g) {
        GameOver=true;
        gameCanvas.stop();
        Font f = g.getFont();
        int h = f.getHeight()+40;
        int w = f.stringWidth("High score")+40;       
        g.setColor(250,250,250);
        g.drawString("Score " + score,(width-w)/2,(height-h)/2,g.TOP | g.LEFT);
}

关注点

有了这些知识,我相信您可以创建简单的游戏,例如:去钓鱼,青蛙过马路。我将在下一篇文章更新中介绍 TitledLayer 和声音。它们肯定会增加游戏的外观和感觉。

GameProgrammingInJ2ME/fishing.jpg

GameProgrammingInJ2ME/frog.jpg

历史

  • 2009 年 4 月 23 日:发布
© . All rights reserved.