接上篇
V0.4:从这个版本开始考虑吃的问题,补充:由于边界被窗口上面那一行挡住了,所以我给程序动了一个小手术,加上了一个panel,但是随之带来了一个问题,用pack()设置大小的话窗口只是一条线,没有办法让窗口刚刚好将Panel包住,如果有大牛知道怎么弄麻烦说一声,谢谢
突然发现在贪吃蛇这种小格子的游戏中,碰撞检测不需要弄一个getRectangle方法,直接判断蛇头和豆的行列数是否相等就可以了,所以我取消了getRectangle方法,直接用row和column来比较是否吃到豆。这时候需要在蛇类中调用豆的坐标,所以需要添加一个主窗口的引用。
细心观察,可以发现吃豆eat 和移动move 两个动作的区别只在于移动move 去掉尾巴。移动是加一个头,去一个尾,而吃则是只加一个头,不去尾,所以通过观察,可以将eat 方法简化为判断下一步是否为吃,如果为吃,则在move 方法中,不执行去尾的操作。代码如下:
SnakeWindow.java
import java.awt.*;
import java.awt.event.*;
/**
* 窗口类
* @author hzp
*/
public class SnakeWindow extends Frame {
public static void main(String[] args){
new SnakeWindow("贪吃蛇").launch();
}
/**
* 初始化窗口
*/
private void launch() {
setLocation(400,130);
setSize(520,540);
setLayout(null);
setBackground(Color.WHITE);
addWindowListener(new WindowAdapter(){
public void windowClosing (WindowEvent e){
System.exit(0);
}
});
SnakePanel sp = new SnakePanel();
sp.launch();
add(sp);
setVisible(true);
}
/**
* 构造方法
* @param s 标题栏名称
*/
public SnakeWindow(String s){
super(s);
}
}
/**
* 界面类
* @author hzp
*/
class SnakePanel extends Panel {
/**
* 窗口宽度
*/
public static final int WIDTH = 500;
/**
* 窗口高度
*/
public static final int HEIGHT = 500;
/**
* 单元格边长
*/
public static final int UNIT = 20;
/**
* 行数
*/
public static final int ROWS = HEIGHT/UNIT;
/**
* 列数
*/
public static final int COLUMNS = WIDTH/UNIT;
/**
* 刷新速度
*/
public static final int UPDATES = 100;
/**
* 蛇对象
*/
private Snake snake = new Snake(this);
/**
* 豆对象
*/
public Bean bean = Bean.newBean();
/**
* 界面初始化
*/
public void launch(){
setLocation(10,30);
setSize(WIDTH,HEIGHT);
setBackground(Color.GRAY);
this.setFocusable(true);
addKeyListener(new KeyMonitor());
new Thread(new PaintThread()).start();
}
/**
* 绘制界面
*/
public void paint(Graphics g){
Color c = g.getColor();
g.setColor(Color.DARK_GRAY);
for(int i = 1; i < HEIGHT/UNIT ; i++) {
g.drawLine(0,UNIT * i,WIDTH ,UNIT * i);
}
for (int i = 1; i < WIDTH/UNIT ; i++ )
{
g.drawLine(UNIT * i, 0, UNIT * i, HEIGHT);
}
snake.draw(g);
bean.draw(g);
g.setColor(c);
}
/**
* 键盘监听器
* @author hzp
*/
class KeyMonitor extends KeyAdapter
{
/**
* 键盘按下时调用该方法
* @param e:键盘事件
*/
public void keyPressed (KeyEvent e){
snake.keyPressed(e);
}
}
/**
* 界面刷新线程
* @author hzp
*/
class PaintThread implements Runnable
{
public void run() {
while(true){
repaint();
try{
Thread.sleep(UPDATES);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
Snake.java
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Snake
{
/**
* 盛放组成蛇的各个节点
*/
public LinkedList<Node> nodeList = new LinkedList<Node>();
/**
* 蛇头行进的方向
*/
private Direction direction = Direction.Left;
private SnakePanel sw = null;
/**
* 方向枚举类型
*/
public enum Direction {Up,Down,Left,Right}
/**
* 构造方法
*/
public Snake(SnakePanel sw){
nodeList.addFirst(new Node(19,14));
this.sw = sw;
}
/**
* 自身绘制
*/
public void draw(Graphics g){
move();
Color c = g.getColor();
g.setColor(Color.GREEN);
for(int i = 0; i < nodeList.size(); i++){
Node n = nodeList.get(i);
n.draw(g);
}
g.setColor(c);
}
/**
* 移动,如果不为吃则去尾
*/
public void move() {
addNode();
if(!isEat()) nodeList.removeLast();
else sw.bean = Bean.newBean();
}
/**
* 根据蛇头和豆的位置判断是否为吃
*/
public boolean isEat() {
if(nodeList.getFirst().getColumn() == sw.bean.getColumn() && nodeList.getFirst().getRow() == sw.bean.getRow()) return true;
return false;
}
/**
* 根据方向新增一个节点
*/
public void addNode() {
switch(direction){
case Up :
nodeList.addFirst(new Node(nodeList.getFirst().getColumn(), nodeList.getFirst().getRow() - 1));
break;
case Down :
nodeList.addFirst(new Node(nodeList.getFirst().getColumn(), nodeList.getFirst().getRow() + 1));
break;
case Left :
nodeList.addFirst(new Node(nodeList.getFirst().getColumn() - 1, nodeList.getFirst().getRow()));
break;
case Right :
nodeList.addFirst(new Node(nodeList.getFirst().getColumn() + 1, nodeList.getFirst().getRow()));
break;
}
}
public void collide () {
//
}
/**
* 方向键按键事件的处理,改变移动方向
*/
public void keyPressed (KeyEvent e) {
int keyCode = e.getKeyCode();
switch(keyCode){
case KeyEvent.VK_UP :
direction = Direction.Up;
break;
case KeyEvent.VK_DOWN :
direction = Direction.Down;
break;
case KeyEvent.VK_LEFT :
direction = Direction.Left;
break;
case KeyEvent.VK_RIGHT :
direction = Direction.Right;
break;
}
}
/**
* 节点类
*/
class Node
{
/**
* 豆子列坐标
*/
private int row = 0;
/**
* 豆子行坐标
*/
private int column = 0;
/**
* 豆子行坐标
*/
public static final int SIZE = SnakePanel.UNIT;
/**
* 私有的构造方法,不许别的类调用
*/
public Node(int col, int row){
this.row = row;
this.column = col;
}
public int getRow(){
return row;
}
public int getColumn() {
return column;
}
/**
* 节点的绘制方法
*/
public void draw(Graphics g){
Color c = g.getColor();
g.setColor(Color.BLACK);
g.fillRect(column * SnakePanel.UNIT, row * SnakePanel.UNIT, SIZE, SIZE);
g.setColor(c);
}
}
}
Bean.java
import java.awt.*;
import java.util.*;
/**
* 豆类
*/
public class Bean
{
/**
* 豆子的直径
*/
public static final int SIZE = SnakePanel.UNIT;
/**
* 豆子行坐标
*/
private int row = 0;
/**
* 豆子列坐标
*/
private int column = 0;
/**
* 随机数产生器
*/
static Random r = new Random();
/**
* 私有的构造方法,不许别的类调用
*/
private Bean(int col, int row){
this.row = row;
this.column = col;
}
/**
* 豆的绘制方法
*/
public void draw(Graphics g){
Color c = g.getColor();
g.setColor(Color.GREEN);
g.fillOval(column * SnakePanel.UNIT, row * SnakePanel.UNIT, SIZE, SIZE);
g.setColor(c);
}
public int getRow(){
return row;
}
public int getColumn() {
return column;
}
/**
* 静态方法,用来产生一个新豆
*/
public static Bean newBean(){
Bean b = new Bean(r.nextInt(SnakePanel.COLUMNS), r.nextInt(SnakePanel.ROWS));
return b;
}
}
V0.5:在这一个版本里我们重点解决各种边界问题,碰撞问题
首先是豆类的newBean方法,在创建之前,需要判断新位置是否已经被占据了,若被占据,则需要重新选取坐标创建。因为要获取贪吃蛇每个节点的坐标,所以要传一个LinkedList的引用。
然后是贪吃蛇的出界问题和追尾问题,出界只需要判断坐标即可,追尾可以分析一下,只要头没有撞上任何一个身体节点,其他节点必然不可能互相撞上。所以只要判断头节点有没有和其他节点重合即可。
V0.6:在这个版本中解决剩余的一些杂七杂八的问题,比如碰撞之后的界面问题,我是让界面消失,然后输出两行提示“GAME OVER!”“按F2重新开始。”具体做法就是添加一个布尔型变量live表示活着,在paint方法开始时进行判断,若是为true才进行正常绘制,否则就输出这两行字符。
然后我又添加了一个对F2键的响应和一个reset方法,按F2调用reset重新初始化snake、bean和live变量,实现重新开始的功能。
还有双缓冲机制用来消除闪烁。
还加入了暂停功能,暂停时就让PaintThread 线程不调用repaint 方法,一直空转。
我现在正在研究关于道具和关卡的问题。欢迎有问题的童鞋提问。关于贪吃蛇暂时就先写到这。