游戏wormgame是wtk里面自带的demo程序,虽然游戏很简单也很老,就是一般很常见的贪吃蛇游戏。但是既然作为demo程序,那么里面就有很多东西值得我们去思考,去学习。
首先看看这个游戏所采用的数据结构。如果没有看过贪吃蛇的源码,让我们自己选择数据结构,那我们如何选择一个在空间和性能上都很好的数据结构呢。可能很多
人首先想到的是链表,符合游戏中的蛇长度的动态增加和减少。但是我们讨论的是用JAVA写的,如果采用链表的话自己需要去封装很多方法。有没有JAVA本
来已经封装好的数据结构可以用呢?对,Vector!这是JAVA的一个非常常用而且重要的数据结构。
如果就让我们用Vector来做这个游戏,那Vector里面我们放什么呢?很多人会自然的想到,以组成贪吃蛇的每一个小方格子(在Demo中用的是
Cell,英文不好,不知道该用什么词)为对象,然后放到Vector里面去,这样就可以实现贪吃蛇长度的增加和减少,因为增加对应
addElement(),减少对应removeElement()。如果这样做的话,那么每一个对象该定义什么数据成员以及封装哪些方法呢?这样做是不
是浪费空间了呢?因为当贪吃蛇的长度增长以后,处于中间的小方格子似乎没有起到什么作用。那还有没有更好的方法呢?
让我们现在来看看这个demo是怎么做的吧!
关于贪吃蛇的数据结构方面的类有两个Worm和WormLink,在WormLink的开头作者就写了一段注释:
* WormLink represents one sub-section of a worm. Because the
* worm will usually contain a few straight segments, this is
* a relatively cost effective way to store the entire worm.
所以可以从这里就可以知道作者并不是将每个方格作为对象存到Vector里面的,而是将贪吃蛇的一部份存进去的,其实真正存进去的只是一个带着这个贪吃蛇的很多信息的对象。
WormLink的数据成员为 :
private int x, y;
private int len;
private byte dir;
其中封装的方法有(以下只举例我认为比较重要的几个方法):
public void increaseLength() {
public void decreaseLength()
public int getEndX()
public int getEndX()
public boolean contains(int x, int y)
increaseLength()是增加长度,被封装到这个类里面,因为每个对象都携带了从它开始的长度信息,同理decreaseLength()也是的,至于它们的具体实现,将在下文讲到画图的时候再具体讲。
getEndX(),getEndX()的作用是获得贪吃蛇尾部的坐标,从这个方法就可以看出作者的高明之处,并不需要将每个方格子都存到Vector中
去。比如初始状态时贪吃蛇的长度为10的话,就不需要向Vector里面存入10个对象,只需要将一个代表的对象存进去就可以了,然后通过
getEndX(),getEndX()来获取尾部的信息。这样做既节约空间又方便了以后的处理,特别是画图的处理。
contains是碰撞检测简单的函数,在这个游戏里面的碰撞是比较简单的。
下面看类Worm,我们主要看几个重要的函数,同时从这几个函数的处理上面可以看出作者在设计这个游戏时的高明之处。
/** * Regenerate the worm in its initial position. This is used * to restart the game after the worm is killed. */ public void regenerate() { synchronized (worm) { worm.removeAllElements(); worm.addElement(new WormLink(INIT_X, INIT_Y, INIT_LEN, INIT_DIR)); // Reset class variables currentDirection = 0; needUpdate = false; hasEaten = false; moveOnNextUpdate = false; } }
这是一个初始化的函数,当游戏开始或者从来一次时被调用的函数,这个函数的作用很明显,在此就不作过多的解释了。
下面结合贪吃蛇最简单的移动,从左到右横向移动来说明函数update,paint。
我们先看paint:
public void paint(Graphics g) { WormLink sl; int x1, x2, y1, y2; int len; for (int i = 0; i < worm.size(); i++) { sl = (WormLink) worm.elementAt(i); x1 = sl.getX(); x2 = sl.getEndX(); y1 = sl.getY(); y2 = sl.getEndY(); len = sl.getLength(); drawLink(g, x1, y1, x2, y2, len); } }
其中函数drawLink的主要作用就是从x1到x2或者y1到y2画len个小方格子,至于其具体实现就不再列出了。
从这个paint函数可以确定我们刚才的推断是正确的,就是整条贪吃蛇是一段一段被存放到vector里面去的,画图的时候又一段一段的画出来的,vector对象worm有多少个元素就存在着多少段。
下面在来看update函数,其实update也是一个画图函数,但是奇怪的是,为什么这个游戏不象别的游戏一样,将所有的画图都放在paint里面呢?
这里单独定义一个画图函数的作用又是什么呢?其实我们从这个函数的函数名就可以看出答案了,这个函数的作用是在于更新图像,但是又不是更新整个贪吃蛇,而
是根据头和尾部,来部分更新,这样就没有别要在画图的时候,每次都重画整个屏幕,只需要更新贪吃蛇的部分就可以了,这也是一种节省系统开销的方法。虽然对
于这个游戏本身来说性能不会有太明显的区别,但是对于一些复杂的游戏,画面丰富的游戏来说,这样做的效果就非常明显了。在后面的Canvas类
WormPit里面调用这两个函数的时候,我们可以更加清楚地看到这个作用。
下面讲解update函数的具体实现:
public void update(Graphics g) throws WormException { WormLink head, sl; int headX, headY; if (!moveOnNextUpdate) { return; } synchronized (worm) { head = (WormLink) worm.lastElement(); // the worm 'head' head.increaseLength(); if (!hasEaten) { WormLink tail; tail = (WormLink) worm.firstElement(); // the worm 'tail' int tailX = tail.getX(); int tailY = tail.getY(); tail.decreaseLength(); if (tail.getLength() == 0) { worm.removeElement(tail); } // Clear last block of the tail g.setColor(WormPit.ERASE_COLOUR); drawLink(g, tailX, tailY, tailX, tailY, 1); } else { hasEaten = false; } needUpdate = false; // Make sure we're still in bounds if (!WormPit.isInBounds(head.getEndX(), head.getEndY())) { throw new WormException("over the edge"); // You're dead Jim } headX = (byte) head.getEndX(); headY = (byte) head.getEndY(); // Draw the head g.setColor(WormPit.DRAW_COLOUR); drawLink(g, headX, headY, headX, headY, 1); // See if we ate ourself for (int i = 0; i < worm.size() - 1; i++) { sl = (WormLink) worm.elementAt(i); if (sl.contains(headX, headY)) { throw new WormException("you ate yourself"); } } } }
首先moveOnNextUpdate是用于判断是否将要移动,如果不移动就退出此函数。
我们结合刚才所说的例子,假设此时贪吃蛇是从左到右移动的,其实这个时候worm里面只有一个对象,tail和head是同一个对象。
head.increaseLength(),首先将整个蛇的长度加1 ,hasEaten的作用我们暂时不管。然后到tail.decreaseLength(),作用是将长度缩短1,这里我们再回头去看decreaseLength()的实现部分:
public void decreaseLength() { len--; switch (dir) { case Worm.LEFT: x--; break; case Worm.RIGHT: x++; break; case Worm.UP: y--; break; case Worm.DOWN: y++; break; } }
从中我们可以看出,这个函数的作用不仅是将长度减1,同时还包括了坐标的移动。对于我们举的这个例子来说,就是tail的x坐标加1。
然后回到函数update:
if (tail.getLength() == 0) { worm.removeElement(tail); } // Clear last block of the tail g.setColor(WormPit.ERASE_COLOUR); drawLink(g, tailX, tailY, tailX, tailY, 1);
判断语句的作用在于,如果当前尾部所带的长度已经为0了,就要把这个对象从worm中删除。然后就是这个update函数的主要部分了,清除原来的尾部所在的位置,其实就是将原来的小方格画成根背景一样的颜色。
isInBounds我们先不管,下面就是该画头了
g.setColor(WormPit.DRAW_COLOUR);
drawLink(g, headX, headY, headX, headY, 1);
其实我们有几个细节需要注意,变量tailX,
tailY的赋值是在tail做descreaseLength之前,所以它们记录的是尾部前一个状态下面的位置,然后才将其在画布上面清除。还有一点,
在画头的时候,headX,
headY的值是head.getEndX()和head.getEndY(),这样我们就知道了,其实存到vector对象worm里面的是贪吃蛇每一
段的尾部,对于这个例子来说,就是存放的贪吃蛇最后的那个小方格,当然还有它封装的一些数据信息。而这个时候headX,
headY已经是贪吃蛇更新以后头所在的位置,对此例,头的位置已经向前移动了一个单位。
所以对这个例子来说,贪吃蛇的移动过程就是先将长度加1,然后将长度减1并将尾部的x坐标加1,即右移一个单位,然后将原来位置上面的小方格清除掉,再将
头的位置右移一个单位并在此位置画头。这样就实现了贪吃蛇的右移了。其实整个过程就只重画了两个方格,尾部和头部。
刚才我们所举的例子跟方向是没有关系的,现在我们来看改变方向的函数。
public void setDirection(byte direction) { synchronized (worm) { if ((direction != currentDirection) && !needUpdate) { WormLink sl = (WormLink) worm.lastElement(); int x = sl.getEndX(); int y = sl.getEndY(); switch (direction) { case UP: if (currentDirection != DOWN) { y--; needUpdate = true; } break; case DOWN: if (currentDirection != UP) { y++; needUpdate = true; } break; case LEFT: if (currentDirection != RIGHT) { x--; needUpdate = true; } break; case RIGHT: if (currentDirection != LEFT) { x++; needUpdate = true; } break; } if (needUpdate == true) { worm.addElement(new WormLink(x, y, 0, direction)); currentDirection = direction; } } } }
我们主要看这个函数的最后,如果needUpdate ==
true我们就要向worm里面添加一个对象,即贪吃蛇的头改变了方向。例如开始蛇从左向右移动,然后再改变方向向上移动,那么就要像worm里面添加一
个新的对象,这个时候的贪吃蛇一共有两段,一段是原来的向右移动的水平方向上的一段,还有一段就是刚刚生成的向上移动的一段。如果这个时候贪吃蛇一直向上
移动的话,头这一段的长度不断增加,而尾部这段的长度不断减少,当长度减少为0的时候,就将这个尾部的对象从worm里面删除,这样刚新增加的对象就变成
了新的尾部了。如果这样不断改变方向下去,整个worm就是这样的添加元素和删除元素的过程。这里我们再回头去看为什么刚开始定义worm的时候用
Vector(5,2)了。其实worm里面元素的个数是跟贪吃蛇有多少个弯是一样的,这样开始定义为5个,因为在开始阶段整个贪吃蛇的长度还比较短的时
候生成5个新对象的几率是比较小的,所以开始才定义大小为5。
下面我们看canvas类wormPit,首先看paint函数:
public void paint(Graphics g) { if (forceRedraw) { // Redraw the entire screen forceRedraw = false; // Clear background g.setColor(WormPit.ERASE_COLOUR); g.fillRect(0, 0, getWidth(), getHeight()); // Draw pit border g.setColor(WormPit.DRAW_COLOUR); g.drawRect(1, 1, (width - START_POS), (height - START_POS)); // Display current level g.drawString("L: " + level, START_POS, height, g.TOP | g.LEFT); g.drawString("" + score, (width - (SCORE_CHAR_WIDTH * 3)), height, g.TOP | g.LEFT); // Display current score g.drawString("S: ", (width - (SCORE_CHAR_WIDTH * 4)), height, g.TOP | g.RIGHT); g.drawString("" + score, (width - (SCORE_CHAR_WIDTH * 3)), height, g.TOP | g.LEFT); // Display highest score for this level g.drawString("H: ", (width - (SCORE_CHAR_WIDTH * 4)), (height + SCORE_CHAR_HEIGHT), g.TOP | g.RIGHT); g.drawString("" + WormScore.getHighScore(level), (width - (SCORE_CHAR_WIDTH * 3)), (height + SCORE_CHAR_HEIGHT), g.TOP | g.LEFT); // Draw worm & food g.translate(START_POS, START_POS); g.setClip(0, 0, CellWidth * CELL_SIZE, CellHeight * CELL_SIZE); myWorm.paint(g); myFood.paint(g); } else { // Draw worm & food g.translate(START_POS, START_POS); } if (gamePaused) { Font pauseFont = g.getFont(); int fontH = pauseFont.getHeight(); int fontW = pauseFont.stringWidth("Paused"); g.setColor(WormPit.ERASE_COLOUR); g.fillRect((width - fontW) / 2 - 1, (height - fontH) / 2, fontW + 2, fontH); g.setColor(WormPit.TEXT_COLOUR); g.setFont(pauseFont); g.drawString("Paused", (width - fontW) / 2, (height - fontH) / 2, g.TOP | g.LEFT); } else if (gameOver) { Font overFont = g.getFont(); int fontH = overFont.getHeight(); int fontW = overFont.stringWidth("Game Over"); g.setColor(WormPit.ERASE_COLOUR); g.fillRect((width - fontW) / 2 - 1, (height - fontH) / 2, fontW + 2, fontH); g.setColor(WormPit.TEXT_COLOUR); g.setFont(overFont); g.drawString("Game Over", (width - fontW) / 2, (height - fontH) / 2, g.TOP | g.LEFT); } else { paintPitContents(g); } g.translate( -START_POS, -START_POS); }
整个贪吃蛇的活动区域是在黑色边框内,所以通过g.translate(START_POS, START_POS)将坐标原点移动到(START_POS, START_POS),这个坐标系都作了坐标变换了。
在paint函数的开始通过forceRedraw来判断是否对整个画布进行重画,如果是,(这里这样画分数,level这些东西我们就不具体讲了,直接
跳到画myWorm.paint(g))就调用worm的paint函数,对整个贪吃蛇进行画图。然后再这个paint函数里面调用
paintPitContents(g),这个函数是这个wormPit类的成员函数,也是整个游戏的主要逻辑部分所在。
private void paintPitContents(Graphics g) { try { myWorm.update(g); // update worm position if (myFood.isAt(myWorm.getX(), myWorm.getY())) { myWorm.eat(); score += level; foodEaten++; if (foodEaten > (level << 1)) { /* Increase difficulty level */ forceRedraw = true; foodEaten = 0; level++; if (tonePlayer != null) { try { tonePlayer.setMediaTime(0); tonePlayer.start(); } catch (MediaException me) {} } } else { if (audioPlayer != null) { try { Manager.playTone(69, 50, 100); // Play audio } catch (MediaException me) {} } } g.setColor(WormPit.ERASE_COLOUR); g.fillRect((width - (SCORE_CHAR_WIDTH * 3)) - START_POS, height - START_POS, (SCORE_CHAR_WIDTH * 3), SCORE_CHAR_HEIGHT); g.setColor(WormPit.DRAW_COLOUR); // Display new score g.drawString("" + score, width - (SCORE_CHAR_WIDTH * 3) - START_POS, height - START_POS, g.TOP | g.LEFT); myFood.regenerate(); int x = myFood.getX(); int y = myFood.getY(); while (myWorm.contains(x, y)) { // generate again if food placed under worm.. myFood.regenerate(); x = myFood.getX(); y = myFood.getY(); } } myFood.paint(g); } catch (WormException se) { gameOver = true; } }
从这个函数的开始我们就可以看到,worm的update函数就是在此被调用的。至此,整个贪吃蛇的数据结构以及如何画图就完成,至于如何处理food这个对象,以及如何处理分数,暂停等,在这里就不讲了。
转自: http://blog.csdn.net/pandonix