以小时候玩的贪吃蛇为例,对于Java图像界面的学习感悟

2022-12-21,,,

简介

正文

01.JFrame是啥?

02.JPanel

03. KeyListener

04.Runnable

05.游戏Running

06.游戏初始类编写

07.main

简介:

  一直以来用代码来写图形界面是我从来没有做过的事,(-。-;)额,但是已经选择软开这条路,我觉得什么都是要会一点,这样的话也许大概可能多个月后重新写东西能够得心应手很多吧。而且,以后自己要是忘记了,也可以在这里看看,顺便提高高自己文学能力。原谅我敲字比较难看懂,这些当中多多少少是存在自己情感写出来的,看正文好了。

  Read after me:本文是适合一些刚入门学习图形化界面的博友,当然要多多少少了解java之类的基础知识(类相关知识,泛型集合等),否则看看好了。工程文件将会在最后给出附录。我比较讨厌书本上一行一行羞涩难懂的文字,一点鲜活性都没有,大概和我不喜欢背书有关系o(^▽^)o。

  此次我将以一个游戏为例开始介绍我自己感悟出来的图形化界面的一些知识,其实我也只是个菜鸟

  游戏:贪吃蛇;

  开发工具:Eclipse Java Naon,jdk1.7以上;

  开发环境:Windows10或Ubuntu14.04(我在这两个平台下是编译过,效果的话,我是推荐Ubuntu,因为不知道为什么Windows下界面经常为空的?要点击run as Java application好多好多下,(-。-;)也不见得会有游戏界面出现。 如果有博友知道,希望能够指点下小弟,在此先谢谢)。

  游戏界面在此(嗯,比较简素)

——————————————————我是分割线————————————————————

正文:

01.JFrame是啥?

  既然是写图像界面,那么先要有个框架是伐!那么JFrame就是这个图像界面框架类的祖先,所以说我们要继承这个类对吧。先新建一个class,就是new>>class>>XXXX.java;我是建议不同的class放在不同的包(Package)中,这样条例清楚(这是句废话)。我先起名这个Frame的名字叫做SnakeFram.java,代码见下,看注释(敲黑板);

 package shu.hcc.ui;

 import javax.swing.JFrame;
import java.awt.Toolkit;
import java.awt.Dimension; public class SnakeFrame extends JFrame { //每个Frame都有个id
private static final long serialVersionUID = 1L;
//Frame窗口大小
private final int _windowWidth = 530;
private final int _windowHeight = 450; public SnakeFrame()
{ this.setTitle("贪吃蛇初稿1.0");
this.setSize(_windowWidth, _windowHeight); Toolkit _toolKit = Toolkit.getDefaultToolkit();//获取电脑屏幕大小
Dimension _screenSize = _toolKit.getScreenSize();
final int _screenWidth = _screenSize.width;
final int _screenHeight = _screenSize.height; this.setLocation((_screenWidth-this.getWidth())/2,(_screenHeight-this.getHeight())/2);//注意计算后居中 this.setDefaultCloseOperation(EXIT_ON_CLOSE); //注意默认无关闭操作,即后台不死 this.setResizable(false);//设为窗口不变,注意默认可拉伸啥的 this.setVisible(true);//设为可见,注意默认不可见 this.setLayout(null);//setlayout有很多中布局方式,我这暂时设为NULL,后面会做个layout,将其插入,
} }

SnakeFrame.java

这个比较简单,一般的话JFrame这样写就ok了,我也就不细讲了,看注释恩。

02.JPanel

   在01JFrame代码里有提到过,它是含有Layout = NULL,所以我们利用JPanel给它提供一个Layout,同时这个Layout使能够刷新的,说白一点,游戏一直在显示新图就是刷新界面嘛!!!

这样我们继续新建第二个class,命名为SnakePaint.java,它是继承extends JPanel的,最后在

Frame.setContentPanel(panel);

呢就能将该panel作为layout贴到Frame中去。

  首先我们需要一个控制按钮画板对吧,还有显示我们吃了多少食物的得分的label(我是利用button来实现的,因为自带的label我不会用(尴尬)),见代码:

 private void _initButton()
{
this.setLayout(null);
_showLabel= new JLabel();
_showLabel.setFont(new java.awt.Font("Dialog",1,20));//字形,粗细,大小
_showLabel.setText("得分:");
_showLabel.setForeground(Color.BLUE);
_showLabel.setLocation(420, 100);
_showLabel.setBackground(new Color(240,240,240));
_showLabel.setSize(80,60);
this.add(_showLabel); _showScore= new JButton("0");
_showScore.setSize(50,60);
_showScore.setLocation(480, 100);
_showScore.setBackground(new Color(240,240,240));
_showScore.setFont(new java.awt.Font("Dialog",1,20));
_showScore.setBorder(null);
_showScore.setEnabled(false);
this.add(_showScore); _startButton = new JButton();
_startButton.setText("开始游戏");
_startButton.setLocation(420, 250);
_startButton.setSize(90, 30);
this.add(_startButton);
//_startButton.addActionListener(new _StartActionListener()); _aboutButton = new JButton();
_aboutButton.setText("关于游戏");
_aboutButton.setLocation(420,300);
_aboutButton.setSize(90, 30);
this.add(_aboutButton);
//_aboutButton.addActionListener(new _AboutActionListener());
this.requestFocus();
}

initButton()

  接着是绘制我们的游戏画板,用的是@Override private void paintComponent()方法,这样的话在游戏进程中只要调用panel.repaint()函数,就能执行paintComponent()函数。所以说呢我们刷新游戏界面只要更新paintComponent()函数的画图数据就ok。

 @Override
public void paintComponent(Graphics pen)//super.paintComponent(g)是父类JPanel里的方法,会把整个面板用背景色重画一遍,起到清屏的作用
{ try{
super.paintComponents(pen);
_CreatGameInit(pen);
if(_isGG)
{//游戏是否结束?
_GameOverDisplay(pen);
}
if(_isStart)
{ //游戏是否开始?
_CreateSnake(pen);
_CreateFood(pen);
//倒计时
if(_isCount > -1)
{_CreateTip(pen);}
}
}
catch(Exception e)
{
System.out.println("ERROR");
}
this.requestFocus(); }

paintComponent()

上述代码里,第一,super.paintComponents(pen);是用来更新控件的,不能少,否则控制面板就没咯。第二,我把画网格和画蛇食物的函数分开写了,我是根据画网格坐标来定(原点在左上角(敲黑板))的见下:

     private void _CreatGameInit(Graphics pen)
{
pen.setColor(Color.BLACK);
pen.drawRect(_x, _y, _panelWidth, _panelHeight); pen.setColor(Color.WHITE);
pen.fillRect(_x+1, _y+1, _panelWidth-1, _panelHeight-1); pen.setColor(Color.GRAY);
for(int i=1;i<this._panelWidth/this._tileSize;++i)
{
pen.drawLine(this._x+i*_tileSize, this._y, this._x+i*_tileSize, this._y+this._panelHeight);
}
for(int i=1;i<this._panelHeight/this._tileSize;++i)
{
pen.drawLine(this._x, this._y+i*this._tileSize, this._x+this._panelWidth, this._y+i*this._tileSize);
}
}

更新网格的

     private void _CreateSnake(Graphics pen)
{
_snakeList = _snake._GetSnakeList();
if(_snakeList == null)
{
return;
} for(int i=0;i<_snakeList.size();++i)
{
if(!_snakeList.get(i)._CrashLine()){
if(i == _snakeList.size()-1){
_SnakePaint(_bufferImageSnakeHead,pen,this._x+(_snakeList.get(i)._x)*_tileSize,this._y+(_snakeList.get(i)._y)*_tileSize,this._x+(_snakeList.get(i)._x+1)*_tileSize,this._y+(_snakeList.get(i)._y+1)*_tileSize,_snake._GetNextDirection());
}
else if(i == 0){
_SnakePaint(_bufferImageSnakeTrail,pen,this._x+(_snakeList.get(i)._x)*_tileSize,this._y+(_snakeList.get(i)._y)*_tileSize,this._x+(_snakeList.get(i)._x+1)*_tileSize,this._y+(_snakeList.get(i)._y+1)*_tileSize,TailDirection(_snakeList.get(0),_snakeList.get(1)));
// pen.fillRect(this._x+_snakeList.get(i)._x*_tileSize ,this._y+_snakeList.get(i)._y*_tileSize ,Snake.SNAKE_WIDTH/2 , Snake.SNAKE_HEIGHT/2);
}
else{
pen.drawImage(_bufferImageSnakeBody,this._x+(_snakeList.get(i)._x)*_tileSize ,this._y+(_snakeList.get(i)._y)*_tileSize , this._x+(_snakeList.get(i)._x+1)*_tileSize ,this._y+(_snakeList.get(i)._y+1)*_tileSize,10,10, 20,20,this);
}
}
}
}
final Dimension step[]={new Dimension(0,-1),new Dimension(0,1),new Dimension(-1,0),new Dimension(1,0)};
private void _SnakePaint(BufferedImage image,Graphics pen,int dx1,int dy1,int dx2,int dy2,int dir)
{
pen.drawImage(image,dx1,dy1 ,dx2 ,dy2,coo(dir,10).width,coo(dir,10).height, coo(dir,20).width,coo(dir,20).height,this);
}

更新蛇

 private void _CreateFood(Graphics pen)
{
_foodList = _food._GetFoodList();
if(_foodList == null)
{
return;
}
pen.setColor(Color.BLACK);
for(int i=0;i<_foodList.size();++i)
{
pen.drawImage(_bufferImageStrawberry,this._x+(_foodList.get(i)._x-1)*_tileSize, this._y+(_foodList.get(i)._y-1)*_tileSize,this);
}
}

更新食物

     private void _CreateTip(Graphics pen)
{
pen.setColor(Color.BLUE);
String show;
if(_isCount > 0)
{//游戏开始
//显示计数
pen.setFont(new Font("Dialog", Font.BOLD, 100));
show = new String(String.valueOf(_isCount));
}
else
{
//显示开始
pen.setFont(new Font("Dialog", Font.BOLD, 64));
show = new String("开始");
}
pen.drawString(show,150,225);
} private void _GameOverDisplay (Graphics pen)
{
Font font = new Font("宋体", Font.BOLD, 64);
pen.setFont(font);
pen.setColor(Color.RED);
pen.drawString("游戏结束",60,100);
}

更新游戏开始提示和结束提示

  最后,我们有图了,没有感觉缺少点什么麼?对,就是控制,你怎么控制游戏来更新画板panel???这里,我们安装个玩家控制器,说白点就是当有按键敲下时,我们panel能够响应按下哪个键(即执行函数对吧)!!!

     public void setGameControl(SnakeControl control){
this.addKeyListener(control);
}

panel响应键盘

SnakeControl是一个第三个类,Implements 于KeyListener类。好,接下讲!!!

03. KeyListener

  在02中提到过,我们建起第三个类,称为SnakeControl.java ,它是Implements 于KeyListener类,这时候它就可以通过重写@Override一些响应键盘的函数,比如keyPressed(KeyEvent e) 按键按下的函数。

     @Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
//System.out.println(e.getKeyCode());
switch(e.getKeyCode())
{
case KeyEvent.VK_UP:
if(_nextDirection !=Snake.Dir.DOWN)
{
_nextDirection = Snake.Dir.UP;
}break;
case KeyEvent.VK_DOWN:
if(_nextDirection != Snake.Dir.UP)
{
_nextDirection = Snake.Dir.DOWN;
}break;
case KeyEvent.VK_LEFT:
if(_nextDirection != Snake.Dir.RIGHT)
{
_nextDirection = Snake.Dir.LEFT;
}break;
case KeyEvent.VK_RIGHT:
if(_nextDirection != Snake.Dir.LEFT)
{
_nextDirection = Snake.Dir.RIGHT;
}break; }
_snake._SetNextDirection(_nextDirection); }

按键响应函数

当按下一个方向键(我是这样设的,当然可以自己设置),同时我做了个防吃到自己的,什么意思?就是比如当蛇向上走,那么按下按键肯定不能向下,要不就吃到自己了,所以蛇有三个方向可以选择_nextDirection = Snake.Dir.UP;或_nextDirection = Snake.Dir.Left;_nextDirection = Snake.Dir.Right;这就要看按键KeyEvent的值了。这里KeyEvent的键值大家可以搜下有那些。Snake的类函数我在之后会给出,本来写这样的游戏代码要先给出一些规定的蛇Snake,食物Food,坐标Coordination的类成员结构,但是我这主要是提图形界面如何写,所以就放在后面(废话)。我们在有按键按下的时候为什么不直接就写个函数来改变方向呢?只是暂时将值存在下一个方向_nextDirection里呢?这是因为我们刷新界面是有另一个线程Runnable来做的,它会定时刷新界面,只要知道我蛇的下一个方向就ok,这样的话省下一大堆麻烦(可能难理解,我也不知道该怎么表述,要自己体会的)。

04.Runnable

  接上讲,我们说过我们要刷新界面,就是要有另外一个独立于主进程之外的线程来实现,我用的是Runnable,其结构如下:

private Runnable tdpaint  = new Runnable()
{
public void run()
{
//TODO:当开启此线程将执行该函数 }
};

我们只要在控制按键StartButton中添加上

  private Thread _tdpaint = new Thread(tdpaint);//新建个线程

  _tdpaint.start();//线程跑起来

便可以跑起我们的游戏进程线程。

  还有另外一种方式来写线程,就是extends Thread

 private abstract class SnakeTd extends Thread
{
private boolean suspend = false;
private String control = "";
//暂停
public void SetSuspend(boolean suspend){
if(!suspend){
synchronized (control){
control.notifyAll();
}
}
this.suspend = suspend;
}
//游戏running
public void run()
{
while(!_snakeControl._IsEndGame())
{
synchronized (control){
if(suspend){
try {
control.wait();
}catch(Exception e){
e.printStackTrace();
}
}
_snakeControl._GameRunning();
} }
_startButton.setText("再来一局");
}
} }

Snake Runnig


我们只要在游戏进程Runnable中添加上

    private SnakeTd _GameTd;
_GameTd =new SnakeTd(){};
_GameTd.start();//跑起来

便能跑起蛇运动的线程。

  之所以写这两个线程方式,一个方面是自己想要练习如何编程线程,另外,游戏不仅仅只有蛇运动,还有开场倒计时(虽然我没做很炫的倒计时)之类的,所以我做了两个进程,第一个Runnable是游戏的主进程,当开场倒计时后,会调用Thread线程来run snake。还有是如果要暂时暂停线程,我只会第二种方式,为什么要暂停进程,是这样的,我做了个关于button的功能,如果我点击button,会弹出程序的相关信息,但是不会暂停游戏,这就尴尬了,所以我特意学习了如何暂停进程这种功能。

05.游戏Running

  接上讲,我提到过,我们在一个新线程thread来跑游戏GameRunning,那么游戏数据要更新后就应该repaint界面了,

     public void _GameRunning()
{
//开始游戏蛇开始跑动
_Move(_nextDirection);
try {
Thread.sleep(200); //暂停200
} catch (InterruptedException e) {
e.printStackTrace();
}
}

GameRunning

 private void _Move(int dir)
{
if(!_IsEndGame())//游戏结束判断?
{
this._snakeList.add(new Coordination(this._snakeList.get(_snakeList.size()-1)._x+step[dir].width,this._snakeList.get(_snakeList.size()-1)._y+step[dir].height));
if(_IsEat())//是否吃到食物判断
{
_AfterEat();//吃到食物该怎么办?
}
else{
_snakeList.remove(0);//没有吃到食物又该怎么办?
}
this._panel.repaint();//游戏界面刷新
}
}

_Move

    public boolean _IsEndGame()
{
if( _IsCrashBody() || _IsCrashWall())
{
_AfterCrashWall();
return true;//如果撞到墙或吃到自己就会执行AfterCrashWall,并返回true
}
return false ;//否则false,并没有撞墙
}

游戏结束判断

    //判断是调用了编写在Coordination类中的函数来判断,我接下去会讲
private boolean _IsCrashWall()
{
return this._snakeList.get(_snakeList.size()-1)._CrashLine();
}
private boolean _IsCrashBody()
{
return this._snakeList.get(_snakeList.size()-1)._CrashBool(this._snakeList.get(i));
}

碰撞返回

    private void _AfterCrashWall()
{
//碰撞后就应该将数据什么的归0对法
_panel._SetStart(false);
_panel._SetGG(true);
_snakeList.remove((_snakeList.size()-1));
_panel.repaint();
}

碰撞后

    private boolean _IsEat()
{
//吃到食物,也是利用编写在Coordination中get()函数来判断蛇头与食物的坐标
return this._snakeList.get(this._snakeList.size()-1)._CrashBool(this._foodList.get(this._foodList.size()-1));
}

判断是否吃到食物

private void _AfterEat()
{
this._food._InitFoodLocation(this._snake);//食物被吃到了,就应该更新下一个食物点
jb.setText(String.valueOf( _snake._GetSnakeList().size() - 5));//吃到食物,成绩+1
this._foodList.remove(0);//要被这个食物去掉
}

吃到食物后

  游戏跑起来,都是在判断。我这里说下,首先不管如何,下一帧蛇必须要在_snakeList(类型为ArrayList或Vector之类的容器)尾部add上新一点,然后如果吃到食物,就不需要将头部remove(0),否则remove(0)。那么如果判断吃到食物、或者撞到墙了呢?我是利用下一个类Coordination,来判断坐标的的。

  如果游戏结束了的话,需要将数据进行初始化,然后点击开始下一局就可以重写开始。

06.游戏初始类编写

  蛇或食物之类的有大小,还应该都是有一个结构体的来存放它的坐标点,另外蛇有它下一步的方向。这个结构体我是用Vector容器来装的见下:

    Vector<Coordination> _snakeList = new Vector<Coordination>();

Coordiantion.java是一个类,结构如下:

 package shu.hcc.snake;

 public class Coordination {
/*
* 蛇或食物的坐标类
*/
public int _x;
public int _y; public Coordination(int x,int y)
{
this._x = x;
this._y = y;
} //碰撞判断--方法
public boolean _CrashBool(Coordination other)
{
return (this._x == other._x && this._y == other._y)?true:false
}
//出界碰撞判断(碰墙)--方法
public boolean _CrashLine()
{
return (this._x<0)||(this._x>39)||(this._y<0)||(this._y>39)? true:false;
}
//尾巴方向判断
public int _TailDirection(Coordination other)
{
if(this._x-other._x ==0 && this._y -other._y <0)
return 0;
else if(this._x-other._x ==0 && this._y -other._y >0)
return 1;
else if(this._x-other._x < 0 && this._y -other._y ==0)
return 2;
else if(this._x-other._x > 0 && this._y -other._y == 0)
return 3;
return 0;
}
}

Coordination

其中_x,_y分别是坐标的横纵坐标轴值,_snakeList每次add,都将下一点的横纵坐标存入,同时每个_snakeList.get(i)都是Coordination的对象,所以可以调用_crashLine(),_crashBool()函数来判断是否碰撞。

Snake.java

  初始化时,我们随机给定坐标:

public Snake()
{
_InitSnake();
}
public void _InitSnake()
{
Random _random = new Random();
_nextDirection = _random.nextInt(3);
Dimension step[]={new Dimension(0,-1),new Dimension(0,1),new Dimension(-1,0),new Dimension(1,0)};
Dimension aa;
do{
aa = new Dimension(_random.nextInt(40),_random.nextInt(40));
}while(aa.height<8 ||aa.height >32 || aa.width<8 || aa.width >32);
for (int i= 0;i<5; ++i)
{
_snakeList.add(new Coordination(aa.width+step[_nextDirection].width * i,aa.height +step[_nextDirection].height * i));
}
}

Snake()

Food.java

  初始化,有个判断要保证随机生成的食物坐标不能和蛇的坐标重复,否则的话,这就会有bug。没重复的坐标的话,Flag_For=true,此时就不需要在重新生成食物坐标,Flag_while=false,结束掉该生成循环。

    public Food(Snake snake)
{
_foodList = new Vector<Coordination>() ;
_InitFoodLocation(snake);
}
public void _InitFoodLocation(Snake snake)
{
_snakeList = snake._GetSnakeList();
boolean Flag_While = true;
boolean Flag_For = true;
Random _random = new Random(); while(Flag_While){
Flag_For = true;
_foodList.add(new Coordination(_random.nextInt(40),_random.nextInt(40)));
for(int i=0;i<_snakeList.size();++i)
{
if(_foodList.get(0)._CrashBool(_snakeList.get(i)))
{
Flag_For = false;
_foodList.remove(0);
break;
}
}
if(Flag_For)
Flag_While = false;
}
}

Food()

07.main

  该写的基本上写完了,限于本人文学写作能力,有些可能不够详细,各位观众博友请看工程文件好了好了。

如报错的话,请修改加载图片的路径,我改了好久也没把相对路径搞出来,蛋疼。。。

项目是在ubuntu下做的,如果在windows下,可能需要修改下其他的地方。。。

以下是游戏截图:

    

  

  总结:其实图形界面编程不是很复杂,恩,JFrame,JPanel类,paintComponent(),repaint()函数,还有响应KeyListener()键盘函数,线程Thread(),Runnable(),泛型容器Vector<>,ArrayList<>。

  核心部分,都是计算的方法,我不敢称为算法,因为我觉得太low点,以后我还会贴上一些算法。

  图形界面编程比较直观能够很好的显现出具体的结果,但是没有算法核心,只是一团low的小游戏。

<注:未完待续>

以小时候玩的贪吃蛇为例,对于Java图像界面的学习感悟的相关教程结束。

《以小时候玩的贪吃蛇为例,对于Java图像界面的学习感悟.doc》

下载本文的Word格式文档,以方便收藏与打印。