学习JAVA也有一段时间了,之前看了翁恺老师的视频,跟着做了一个细胞自动机,粗浅地了解了一点MVC框架的知识,感觉获益匪浅。但是细胞自动机毕竟是跟着视频完成的,有很大程度上都是参考了视频里的代码,没有自己实践过,因此决定自己做一个贪吃蛇小程序,把MVC的结构运用到程序中来,并且也可以练习一下之前学过的一些知识。
首先简要的说明一下MVC框架,从其名称全写就能窥见其中含义。
通过这样的框架结构将程序中的各个部分分离出来,使得整个程序的脉络变得清晰明了,能提高编程的效率,并且有效地降低了类与类之间的耦合,提升了程序的可扩展性。
作为一个贪吃蛇游戏小程序,肯定是需要一个场景一条蛇和一颗苹果的,否则无法正常完成程序。这些具体的游戏的构成元素都属于MVC框架中的Model。
Cell类:
在此之前受到细胞自动机的启发,我创建了一个Cell类作为游戏图形界面的最基本单元。通过这个Cell类的对象就可以构成这个游戏所需要的场景,蛇以及苹果了。
Cell类中包含私有成员变量x,y用以描述Cell的位置信息;其余私有成员变量isSnake、isApple和isWall用于判断Cell在这个程序中的真实属性。公有成员方法的用途通过方法名和代码注释应该不难理解,这里就不作过多赘述了,以下是Cell类的代码实现。
import java.awt.Color;
import java.awt.Graphics;
public class Cell {
private int x;
private int y;
private boolean isSnake; //是否为蛇的身体,以便打印时着色
private boolean isApple; //是否为苹果,以便打印时着色
private boolean isWall;
Cell(int x,int y) {
this.x = x;
this.y = y;
isSnake = false;
isApple = false;
isWall = false;
}
void toSnake() { //设置为蛇的身体
isSnake = true;
isApple = false;
}
void toApple() { //设置为苹果
isApple = true;
}
void toWall() { //设置为墙
isWall = true;
}
boolean isSnake() { //获取是否为蛇的身体的信息
return isSnake;
}
boolean isApple() { //获取是否为苹果的信息
return isApple;
}
boolean isWall() { //获取是否为墙的信息
return isWall;
}
void moveE() { //向东移动
x++;
}
void moveN() { //向北移动
y--;
}
void moveW() { //向西移动
x--;
}
void moveS() { //向南移动
y++;
}
int getX() {
return x;
}
int getY() {
return y;
}
void changeX(int x) { //改变Cell的X坐标
this.x = x;
}
void changeY(int y) { //改变Cell的Y坐标
this.y = y;
}
void draw(Graphics g,int CELL_SIZE){ //画出Cell
g.drawRect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE);
if(isSnake()){
g.fillRect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
if(isApple()) {
g.setColor(Color.PINK);
g.fillRect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE);
g.setColor(Color.BLACK);
}
if(isWall()) {
g.setColor(Color.lightGray);
g.fillRect(x*CELL_SIZE, y*CELL_SIZE, CELL_SIZE, CELL_SIZE);
g.setColor(Color.BLACK);
}
}
}
Wall类:
有了游戏图形界面的最基本的单元我们就可以开始构造游戏场景了。贪吃蛇游戏场景其实很简单,就是一道围墙包围住整个游戏区域。形成一道围墙需要很多Cell对象共同实现,因此Wall类的私有成员函数仅包含了一个Cell类的ArrayList容器用以存放若干形成围墙的Cell对象。以下是Wall类的代码实现。
import java.util.ArrayList;
public class Wall {
private ArrayList Wall = new ArrayList();
Wall(int Hight,int Length) { //初始化围墙,根据Field的大小建立城墙
Cell c;
for(int i=0; i | |
Snake类:
有了围墙以后,我们就可以在围墙内生成游戏主体——蛇。和墙一样Snake类也是由Cell类对象构成蛇的整个身体。Snake包含私有int类型成员变量fieldLength、fieldHight用以获取场景的大小,boolean类型成员变量alive用于表示蛇是否存活,String类型成员变量Direction用于记录蛇移动的方向,Cell类的ArrayList容器用于存放Snake的身体。蛇初始长度为3,位置在场景中央,方向向北。玩家可以用左转或者右转以调整蛇的前进方向。在吃掉Apple以后蛇身会变长,撞到墙或者咬到自身以后会死亡,然后导致游戏结束。以下是Snake类的代码实现。
import java.util.ArrayList;
public class Snake {
private int fieldLength; //获取场地大小
private int fieldHight;
private boolean alive; //蛇是否存活
private String Direction = new String("North"); //初始化蛇的前进方向
private ArrayList Snake = new ArrayList(); //蛇的身体
Snake(int hight,int length) { //初始化蛇的长度为3,位置在窗口中央
fieldLength = length;
fieldHight = hight;
alive = true;
int startX = length/2; //蛇头起始位置
int startY = hight/2;
Cell c;
c = new Cell(startX,startY); //建立3个细胞作为蛇的身体
c.toSnake();
Snake.add(c);
c = new Cell(startX,startY+1);
c.toSnake();
Snake.add(c);
c = new Cell(startX,startY+2);
c.toSnake();
Snake.add(c);
}
String getDirection() { //获取Snake的前进方向
return Direction;
}
int getsize() { //获取Snake的长度
return Snake.size();
}
Cell getCell(int i) { //获取Snake的Cell
return Snake.get(i);
}
void Run() //蛇的移动
{
for(int i=Snake.size()-1; i>0; i--) { //蛇身前移
Snake.get(i).changeX(Snake.get(i-1).getX());;
Snake.get(i).changeY(Snake.get(i-1).getY());;
}
if(Direction.equals("North")) { //蛇头根据方向移动
Snake.get(0).moveN();
}
if(Direction.equals("West")) {
Snake.get(0).moveW();
}
if(Direction.equals("East")) {
Snake.get(0).moveE();
}
if(Direction.equals("South")) {
Snake.get(0).moveS();
}
}
void toNorth() { //改变蛇的移动方向为向北
Direction = "North";
}
void toEast() { //改变蛇的移动方向为向东
Direction = "East";
}
void toWest() { //改变蛇的移动方向为向西
Direction = "West";
}
void toSouth() { //改变蛇的移动方向为向南
Direction = "South";
}
boolean isEatApple(Apple apple) { //判断是否吃到水果
boolean isEat = false;
if(Snake.get(0).getX()==apple.getXfromApple()&&Snake.get(0).getY()==apple.getYfromApple()) {
Snake.add(0, apple.Eaten());
isEat = true;
}
return isEat;
}
boolean isHitWall() { //判断是否撞到墙
boolean isHit = false;
if(Snake.get(0).getX()>=fieldHight-1||Snake.get(0).getX()<=0||Snake.get(0).getY()>=fieldLength-1||Snake.get(0).getY()<=0) {
isHit = true;
}
return isHit;
}
boolean isBiteSelf() { //判断是否咬到自己
boolean isBite = false;
for(int i=1; i | |
Apple类:
游戏的得分情况是由玩家操控蛇吃掉苹果(虽然现实中蛇其实不吃水果)来决定的,因此Apple是不可或缺的一个类。Apple包含一个私有成员变量Cell作为苹果本身,其余公有成员方法用以获取其坐标,用于打印,以及做出被吃掉的反应。当Apple被吃掉以后返回自身作为Snake的一部分,实现蛇身增长。
public class Apple {
private Cell apple;
Apple(int Hight, int Length){
apple = new Cell((int)(2+Math.random()*(Hight-2)),(int)(2+Math.random()*(Length-2)));
apple.toApple();
}
int getXfromApple() { //获取Apple的X坐标
return apple.getX();
}
int getYfromApple() { //获取Apple的Y坐标
return apple.getY();
}
Cell getCell() { //获取Cell用于打印
return apple;
}
Cell Eaten() { //被吃时返回这个Cell转化为蛇身
apple.toSnake();
return apple;
}
}
Field类:
有了Snake、Apple、Wall这几个游戏的关键要素以后就需要一片场地将这几个要素整合到一起,并且通过一定的规则形成一个完整的游戏。Field类就是用于将这几个类按照既定的规则放在一起进行游戏的。其中包含int类型私有成员变量Length、Hight用于存放场景大小,score用于保存游戏得分(吃掉的苹果数),Snake、Apple、Wall保存蛇、苹果、围墙三个关键的游戏要素。需要说明的是这里的isPlay()方法就是游戏进行的规则。如果严格按照MVC框架进行编程这部分代码应该放在Controller部分中,Field只需要提供三个对象的接口,但是由于这样实在过于麻烦(需要再增加接口),并且和我对Field作为游戏场景的设想不太相符,所以我在这里并没有严格遵守MVC框架的编程思想,做了一些“瞎搞”的事情出来。这里有必要明言以避免产生误导。
public class Field {
private int Length;
private int Hight;
private static int score = 0;
private Snake s;
private Apple a;
private Wall w;
Field(int length,int hight) {
Length = length;
Hight = hight;
a = new Apple(Hight,Length);
s = new Snake(Hight,Length);
w = new Wall(Hight,Length);
}
int getLength()
{
return Length;
}
int getHight()
{
return Hight;
}
String getDirectionfromSnake() { //从Snake获取蛇的行进方向
return s.getDirection();
}
int getScore() { //获取得分
return score;
}
int getWallsize() { //从Wall获取Cell个数以便打印
return w.getsize();
}
int getSnakesize() { //从Snake获取Cell个数以便打印
return s.getsize();
}
Cell getCellfromApple() { //从Apple获取Cell以便打印
return a.getCell();
}
Cell getCellfromWall(int i) { //从Wall获取Cell以便打印
return w.getCell(i);
}
Cell getCellfromSnake(int i) { //从Snake获取Cell以便打印
return s.getCell(i);
}
void turnLeft() { //向左转
if(getDirectionfromSnake().equals("North")) {
s.toWest();
}
else if(getDirectionfromSnake().equals("West")) {
s.toSouth();
}
else if(getDirectionfromSnake().equals("East")) {
s.toNorth();
}
else if(getDirectionfromSnake().equals("South")) {
s.toEast();
}
}
void turnRight() { //向右转
if(getDirectionfromSnake().equals("North")) {
s.toEast();
}
else if(getDirectionfromSnake().equals("West")) {
s.toNorth();
}
else if(getDirectionfromSnake().equals("East")) {
s.toSouth();
}
else if(getDirectionfromSnake().equals("South")) {
s.toWest();
}
}
String isplay() //判断游戏是否继续进行下去,当得分更新时返回得分
{
String play = "true";
s.Run();
if(s.isAlive()) {
play = "true";
}else {
play = "false";
}
if(s.isEatApple(a)) {
score++;
play = new String("得分:" + score);
a = new Apple((int)(1+Math.random()*(Hight-1)),(int)(1+Math.random()*(Length-1)));
}
return play;
}
}
以上就是Model部分的全部类了,呈现了程序所需要的所有数据内容,接下来就是View部分的内容了。
View类:
有了游戏的数据还需要将其输出为图形画面,否则就不是一个真正意义上的游戏。View继承JPanel类,用于输出游戏图像。其中包含int类型私有成员变量CELL_SIZE设定为图像基本单元的大小,field成员变量用于从Field类中获取游戏信息。重载的paint()方法用于调用Cell类中的draw()方法画出图像,重载的getPreferredSize()用于获取游戏界面组件的大小,以便于自适应调整游戏窗口大小。以下为View类的代码实现。
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;
public class View extends JPanel{
private int CELL_SIZE = 20;
private Field field;
View(Field field) {
this.field = field;
}
@Override
public void paint(Graphics g) { //打印出图像
super.paint(g);
for(int i=0; i
Model板块和View板块都已经实现,最后就需要包含整个程序的业务逻辑的Controller板块用于实现程序的运行。
HungrySnake类:
HungrySnake类包含了整个程序的全部业务逻辑,控制所有的代码实现,是这个程序main()方法的所在类,并且建立了一个可视化游戏运行窗口向其中添加了一些游戏提示相关的组件。游戏每进行一步停顿0.2秒,并且判断是否继续进行,当游戏未停止则重新打印整个游戏画面,形成具有一定帧数的游戏画面(虽然只有5帧,但是GT610也能最高画质畅玩无压力)。之前提到的isPlay()方法内的有关业务逻辑的代码部分按照MVC框架其实应该放在这里游戏进行的for循环内,但是以下是HungrySnake类的代码实现。
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
public class HungrySnake {
public static void main(String[] args) {
// TODO Auto-generated method stub
final Field field = new Field(30,30);
final View view = new View(field);
JPanel playside = new JPanel();
playside.add(view);
Font font1 = new Font("微软雅黑",0,20);
Font font2 = new Font("微软雅黑",0,16);
JLabel information1 = new JLabel("道路千万条,蛇只有一条",SwingConstants.CENTER);
JLabel information2 = new JLabel("游戏不规范,玩家两行泪",SwingConstants.CENTER);
JLabel information3 = new JLabel("帮助:A:左转;D:右转;不能撞墙不能吃自己!",SwingConstants.CENTER);
JPanel introduction = new JPanel();
introduction.setLayout(new GridLayout(3,1));
information1.setFont(font1);
information2.setFont(font1);
information3.setFont(font2);
information3.setForeground(Color.RED);
introduction.add(information1);
introduction.add(information2);
introduction.add(information3);
JLabel score = new JLabel("得分:0",SwingConstants.RIGHT);
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.setTitle("HungrySnake");
frame.add(playside,BorderLayout.CENTER);
frame.add(introduction,BorderLayout.NORTH);
frame.add(score,BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
frame.addKeyListener(new KeyAdapter() { //加入键盘监听以获取转向信息
public void keyTyped(KeyEvent e){
switch(e.getKeyChar()) {
case KeyEvent.VK_A:{
field.turnLeft();
}break;
case KeyEvent.VK_D:{
field.turnRight();
}break;
}
}
});
for(int i=0; i<1000; i++) { //循环进行游戏
try {
Thread.sleep(200); //每次刷新页面间隔0.2秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String PlayOrNot = field.isplay();
if(PlayOrNot.equals("false")) //判断是否继续进行游戏,为false时退出游戏
{
break;
}else if(PlayOrNot.equals("true")) { //返回值为true继续游戏
}else { //返回值为有意义的字符串时输出得分
score.setText(PlayOrNot);;
}
frame.repaint(); //重新打印页面
}
}
}
以上,就是整个程序的所有代码实现了。再次提醒,Field类中的isPlay()方法并没有严格按照MVC框架来写,那段代码其实应该放在HungrySnake类中,但是由于提供接口和设计思路的原因我选择了放在Field类中,除此之外都是按照MVC的思想进行设计的。
我从一开始设想各个类到完成程序总共花费了10个小时左右的时间。虽然并不算快,但是比起之前我做的其他的规模相当程序,效率高了太多太多,可能也有更熟悉JAVA和有细胞自动机启发灵感的原因,但是在开发过程中真的能够感觉到MVC框架带来的清晰思路和高效率。本人目前处在学习阶段,以上代码和文字都是本人一点一点敲出来的,仅供大家参考,如有不对的地方希望大家能够不吝赐教,谢谢!