1 写在前面
前面已经写完了蛇类以及食物类,接下来我们完成图形界面类,用到的是java gui
how2j的gui教程
2 设计
我的理解:java的gui的框架大致是有JFrame容器(也就是界面窗体,然后容器里可以放多个JPanel面板(也就是显示的画面,然后画面内可以有一些按钮等组件,可以利用布局器来组织器件。
为了便捷显示贪吃蛇的游戏界面,我们可以直接继承panel类,然后实现键盘的监听
2.1 Source类
2.1.1 设计动机
实现图形界面需要依赖贴图资源
我使用画图浅画了需要的资源,蛇头向下时的效果如图
一共包括的图片资源有一下几张,分别为蛇身,食物,以及蛇头朝4个方向的图片
接下来这些资源需要读入,所以就有了Source类
2.1.2 代码实现
使用getResource 读取配置的资源路径(这里写的是相对路径),并且定义为图标类,绘制的时候直接利用paintIcon绘制
package data;
/**
* @author: huluhulu~
* @date: 2022-06-28 11:02
* @description: 本文件为Source.java 是游戏素材图片的读入
* @version: 1.0
*/
import javax.swing.*;
import java.net.URL;
public class Source {
//贪吃蛇头部
//贪吃蛇头部
public static URL upUrl = Source.class.getResource("statics/up.png");
public static ImageIcon up = new ImageIcon(upUrl);
public static URL downUrl = Source.class.getResource("statics/down.png");
public static ImageIcon down = new ImageIcon(downUrl);
public static URL leftUrl = Source.class.getResource("statics/left.png");
public static ImageIcon left = new ImageIcon(leftUrl);
public static URL rightUrl = Source.class.getResource("statics/right.png");
public static ImageIcon right = new ImageIcon(rightUrl);
//贪食蛇身体
public static URL bodyUrl = Source.class.getResource("statics/body.png");
public static ImageIcon body = new ImageIcon(bodyUrl);
//食物
public static URL foodUrl = Source.class.getResource("statics/food.png");
public static ImageIcon food = new ImageIcon(foodUrl);
}
2.2 游戏界面类
2.2.1 设计
整个游戏由上下左右控制蛇的移动方向,由space键控制游戏开始和暂停,由esc键控制游戏退出,另外按住shift可以使得蛇加速
2.2.2 部分讲解
首先这个类继承了面板Panel类,并且使用KeyListener的监听接口(ActjonListenr本来打算分两个面板,做一个控制面板的)
public class Console extends Panel implements KeyListener, ActionListener
- 继承的Panel类需要复写paint函数也就是绘图函数(我使用复写paintComponent函数绘制不出图像,具体原因不详),每次绘图之后可以使用计时器让让paint函数定时调用。
另外在绘图的时候会出现图像闪烁的情况,需要使用双缓冲方法解决 - 目前设计的功能只需要实现键盘监听,具体是监听按下和抬起(shift加速功能)
- 如何让画面定时刷新?通过定时器Timer+actionPerformed 处理,对于加速和慢速模式分别定义两个不同的计时器。(解释一下:actionPerformed 函数主要是监听一系列有意义的时间:例如键盘按下、鼠标点击、计时器到时间等;也就是说可以通过计时器的时间来定时让蛇移动并且刷新画面)
2.2.3 类属性与类方法
-
类属性
- 蛇
- 食物
- 游戏开始状态(开始,失败,结束
- 蛇的状态(快速/慢速
- 游戏界面大小
- 一节食物/蛇的大小
- 当前分数与最大分数
- 两个计时器(对应按住shift快速模式以及松开shift的慢速模式
-
类方法
- 有参、无参构造函数
- 绘图
- 初始化
- 按钮按下监听
- 按钮抬起监听
- 计时器到时间的处理函数
2.2.4 部分关键函数
2.2.4.1 paint(Graphics g) :绘图函数(双缓冲)
绘图函数主要是将当前蛇的所有身体以及食物的绘制,另外可以根据当前的游戏状态绘制一些游戏提示,例如
游戏失败时弹出得分提示。
在绘图函数里如果直接使用画笔Graphics g绘制的话会出现闪烁现象(参考资料),出现闪烁的原因是多次绘制的延迟,所以可以先将所有的要绘制的内容保存到缓冲区然后一起绘制。
先定义一个图像缓冲区iBuffer,然后取得图像缓冲区的画笔gBuffer
//类属性定义:双缓冲,避免gui闪烁
private Image iBuffer;
private Graphics gBuffer;
//获取图像缓冲区的画笔
if(iBuffer==null)
{
iBuffer=createImage(this.getSize().width,this.getSize().height);
gBuffer=iBuffer.getGraphics();
}
super.paint(gBuffer);
将要输出的内容放在缓冲区gBuffer(实际上是画在了iBuffer图片上,然后通过画笔g直接显示出来
this.setBackground(Color.white);//设置背景板为白色
//绘制游戏区域
gBuffer.setColor(Color.GRAY);
gBuffer.fillRect(0,0,this.console_x+block_x,this.console_y+block_y);
//获取蛇的位置
ArrayList<Pos> snakePos=snake.getSnakePos();
int len=snake.getLength();
//获取当前蛇头方向
Dir headDir=snake.getDir();
//获取食物位置
Pos foodPos=food.getPos();
//画贪吃蛇头部
switch (headDir){
case UP :
Source.up.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
case DOWN :
Source.down.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
case LEFT :
Source.left.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
case RIGHT :
Source.right.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
default:
System.out.println("error head dir");
break;
}
//画贪吃蛇身体
for(int i=1;i<len;i++){
Source.body.paintIcon(this,gBuffer,snakePos.get(i).x+block_x,snakePos.get(i).y+block_y);
}
//画食物
Source.food.paintIcon(this,gBuffer,foodPos.x+block_x,foodPos.y+block_y);
另外还可以根据需要绘制一些游戏的状态提示,例如(具体字体大小可能需要根据实际调节)
if(fail){//失败绘制失败提示
gBuffer.setColor(Color.GREEN);
gBuffer.setFont(new Font("幼圆",Font.BOLD,50));
gBuffer.drawString("游戏失败",this.console_x/2-100,this.console_y/2-25);
gBuffer.drawString("得分:"+score,this.console_x/2-100,this.console_y/2+25);
}
if(!start){//游戏未开始
gBuffer.setColor(Color.GREEN);
gBuffer.setFont(new Font("幼圆",Font.BOLD,20));
gBuffer.drawString("空格键开始/暂停游戏",(this.console_x-200)/2,(this.console_y-40)/2);
gBuffer.drawString("space to start/stop:",(this.console_x-200)/2,(this.console_y-40)/2+20);
}
if(stop){//游戏暂停
gBuffer.setColor(Color.GREEN);
gBuffer.setFont(new Font("幼圆",Font.BOLD,20));
gBuffer.drawString("空格键继续游戏",(this.console_x-140)/2,(this.console_y-40)/2);
gBuffer.drawString("space to continue",(this.console_x-200)/2,(this.console_y-40)/2+20);
}
最后调用画笔g一起绘制
g.drawImage(iBuffer, 0, 0, this);
2.2.4.2 actionPerformed(ActionEvent e)定时刷新函数
这个函数是按照定时器的时间触发的,每到一定时间就调用一次该函数,因此需要在这个函数中让蛇移动并且重新调用画图函数(使用repaint)
该函数中先判断游戏状态,如果不在游戏中调用paint根据游戏状态绘制提示。
如果游戏中则让蛇移动,并且处理蛇是否吃到食物是否碰撞到自己等游戏状态。
需要注意的是这个函数结尾一定要重新设置计时器启动否则无法再次进入该函数,也就无法刷新画面
另外启动计时器是得区分是快速还是慢速模式。
(代码见2.2.5)
2.2.4.3 按键按压和释放函数
依据游戏状态判断,只有在游戏状态时才可以改变游戏的行为。
需要注意的是蛇的方向不能向相反的方向改变(比如向上走的蛇不能直接掉头向下走)
另外在按space暂停时需要调用repaint绘制暂停提示
(代码见2.2.5)
2.2.5 全部代码
package console;
/**
* @author: huluhulu~
* @date: 2022-06-28 11:02
* @description: 本文件为Console.java 主要提供了游戏的界面
* @version: 1.0
*/
import data.Source;
import object.*;
import position.Pos;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import static object.Dir.*;
import static object.Dir.DOWN;
public class Console extends Panel implements KeyListener, ActionListener {
//蛇和食物
private Snake snake;
private Food food;
private boolean start;//判断游戏是否开始
private boolean fail; //判断游戏是否失败结束
private boolean stop; //判断游戏是否暂停
private boolean fast; //判断游戏是否暂停
//游戏界面的大小:800*800
private int console_x,console_y;
//一节的大小
private int block_x,block_y;
//当前分数 与最大分数
private int score;
private int maxScore;
//双缓冲:避免gui闪烁
private Image iBuffer;
private Graphics gBuffer;
//计时器
Timer timer;
Timer fastTimer;
//无参构造函数
public Console(){
this.setFocusable(true);
this.addKeyListener(this); //添加键盘监听
//初始化蛇和食物
this.snake=new Snake();
this.food=new Food();
//蛇和食物不重叠
while(snake.isInSnake(food.getPos())){
food.genFood();
}
System.out.println("food pos:"+food.getPos().x+","+food.getPos().y);
//初始化游戏状态
this.start=false;
this.fail=false;
this.stop=false;
this.fast=false;
//初始化游戏界面
this.console_x=800;
this.console_y=800;
//初始化一节的大小
this.block_x=10;
this.block_y=10;
//启动计时器
timer = new Timer(100,this);
fastTimer= new Timer(50,this);
timer.start();
}
//带参构造函数
public Console(int console_x,int console_y,int block_x,int block_y,int time){
this.setFocusable(true);
this.addKeyListener(this); //添加键盘监听
//初始化蛇和食物
this.snake=new Snake(console_x,console_y,block_x,block_y);
this.food=new Food(console_x,console_y,block_x,block_y);
//蛇和食物不重叠
while(snake.isInSnake(food.getPos())){
food.genFood();
}
System.out.println("food pos:"+food.getPos().x+","+food.getPos().y);
//初始化游戏状态
this.start=false;
this.fail=false;
this.stop=false;
this.fast=false;
//初始化游戏界面
this.console_x=console_x;
this.console_y=console_y;
//初始化一节的大小
this.block_x=block_x;
this.block_y=block_y;
//启动计时器
timer = new Timer(time,this);
fastTimer= new Timer(time/2,this);
timer.start();
}
private void init(){
snake.init();
food.genFood();
this.score=0;
//蛇和食物不重叠
while(snake.isInSnake(food.getPos())){
food.genFood();
}
//初始化游戏状态
this.start=false;
this.stop=false;
this.fast=false;
//启动计时器
timer.start();
}
//绘图函数
public void paint(Graphics g) {
if(iBuffer==null)
{
iBuffer=createImage(this.getSize().width,this.getSize().height);
gBuffer=iBuffer.getGraphics();
}
super.paint(gBuffer);
this.setBackground(Color.white);//设置背景板为白色
//绘制游戏区域
gBuffer.setColor(Color.GRAY);
gBuffer.fillRect(0,0,this.console_x+block_x,this.console_y+block_y);
//获取蛇的位置
ArrayList<Pos> snakePos=snake.getSnakePos();
int len=snake.getLength();
//获取当前蛇头方向
Dir headDir=snake.getDir();
//获取食物位置
Pos foodPos=food.getPos();
//画贪吃蛇头部
switch (headDir){
case UP :
Source.up.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
case DOWN :
Source.down.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
case LEFT :
Source.left.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
case RIGHT :
Source.right.paintIcon(this,gBuffer,snakePos.get(0).x+block_x,snakePos.get(0).y+block_y);
break;
default:
System.out.println("error head dir");
break;
}
//画贪吃蛇身体
for(int i=1;i<len;i++){
Source.body.paintIcon(this,gBuffer,snakePos.get(i).x+block_x,snakePos.get(i).y+block_y);
}
//画食物
Source.food.paintIcon(this,gBuffer,foodPos.x+block_x,foodPos.y+block_y);
if(fail){//失败绘制失败提示
gBuffer.setColor(Color.GREEN);
gBuffer.setFont(new Font("幼圆",Font.BOLD,50));
gBuffer.drawString("游戏失败",this.console_x/2-100,this.console_y/2-25);
gBuffer.drawString("得分:"+score,this.console_x/2-100,this.console_y/2+25);
}
if(!start){//游戏未开始
gBuffer.setColor(Color.GREEN);
gBuffer.setFont(new Font("幼圆",Font.BOLD,20));
gBuffer.drawString("空格键开始/暂停游戏",(this.console_x-200)/2,(this.console_y-40)/2);
gBuffer.drawString("space to start/stop:",(this.console_x-200)/2,(this.console_y-40)/2+20);
}
if(stop){//游戏暂停
gBuffer.setColor(Color.GREEN);
gBuffer.setFont(new Font("幼圆",Font.BOLD,20));
gBuffer.drawString("空格键继续游戏",(this.console_x-140)/2,(this.console_y-40)/2);
gBuffer.drawString("space to continue",(this.console_x-200)/2,(this.console_y-40)/2+20);
}
g.drawImage(iBuffer, 0, 0, this);
}
@Override
public void actionPerformed(ActionEvent e) {
if(!start || fail || stop){//不需要更新游戏界面
if(stop)
System.out.println("stoped");
repaint();//调用绘图是因为需要绘制失败界面
if(fast){//这里是保证下一次有计时器能触发该函数
fastTimer.start();
}
else{
timer.start();
}
return;
}
//蛇移动
if(!snake.Move()){//蛇撞到自己
fail=true;//游戏失败
if(fast){
fastTimer.start();
}
else{
timer.start();
}
return;//结束函数
}
//蛇吃到食物
if(Pos.isEqual(food.getPos(),snake.getPos())) {
//移动
if(!snake.Eat()) {
fail=true;//游戏失败
if(fast){
fastTimer.start();
}
else{
timer.start();
}
return; //结束函数
}
//生成新的食物
food.genFood();
//蛇和食物不重叠
while(snake.isInSnake(food.getPos())){
food.genFood();
}
System.out.println("food pos:"+food.getPos().x+","+food.getPos().y);
score++;//提高得分
}
//重新绘制
repaint();
//继续计时
if(!fast)
timer.start();
else
fastTimer.start();
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
//获取键盘行为
int keyCode = e.getKeyCode();
System.out.println("key pressed");
System.out.println(start+" "+fail+" "+stop);
if(!start || fail){//如果游戏未开始 或者上次游戏失败
if(keyCode==KeyEvent.VK_SPACE){//按space键开始游戏
if(fail){
this.init();//重新初始化游戏
}
start=true; //设置开始游戏
fail=false; //重新设置失败状态
stop=false; //重新设置停止状态
timer.start(); //开始计时
}
}
else if(start && !fail){//如果游戏已开始
//根据键盘行为改变蛇头方向
switch (keyCode) {
case KeyEvent.VK_UP://按上键 但是此时方向不能向下
if(snake.getDir()!=DOWN) {
snake.setDir(UP);
}
break;
case KeyEvent.VK_DOWN://按下键 但是此时方向不能向上
if(snake.getDir()!=UP) {
snake.setDir(DOWN);
}
break;
case KeyEvent.VK_LEFT://按左键 但是此时方向不能向右
if(snake.getDir()!=RIGHT) {
snake.setDir(LEFT);
}
break;
case KeyEvent.VK_RIGHT://按右键 但是此时方向不能向左
if(snake.getDir()!=LEFT) {
snake.setDir(RIGHT);
}
break;
case KeyEvent.VK_SPACE://按space键重新开始游戏
if(stop){//如果游戏暂停,按space键继续游戏
timer.start();
stop=false;
}
else{//游戏正在运行:暂停游戏
stop=true;
timer.stop();
repaint();
}
break;
case KeyEvent.VK_ESCAPE://按esc键退出游戏
System.exit(0);
System.out.println("exit with score:"+score);
break;
case KeyEvent.VK_SHIFT:
timer.stop();
fastTimer.start();
default:
break;
}
}
}
@Override
public void keyReleased(KeyEvent e) {
int keyCode = e.getKeyCode();
if(keyCode==KeyEvent.VK_SHIFT){
fastTimer.stop();
timer.start();
}
}
//接口:设置游戏开始
public void setStart(boolean start) {
this.start = start;
this.fail=false; //重新设置失败状态
this.stop=false; //重新设置停止状态
timer.start();
}
//接口:设置游戏暂停
public void setStop(boolean stop) {
this.stop = stop;
timer.stop();
}
}
2.3 定义主类运行贪吃蛇
2.3.1 代码
有了前面的类可以直接定义一个主类运行贪吃蛇游戏(这里的游戏面板的构造函数没有具体讲,根据代码应该可以看明白)
/**
* @author: huluhulu~
* @date: 2022-06-28 11:02
* @description: 本文件为SnakeGame.java 是游戏的入口类,里面包含了主函数
* @version: 1.0
*/
//导入包
import console.Console;
import data.Source;
import javax.swing.*;
import static data.Source.*;
public class SnakeGame {
public static void main(String args[]){
JFrame jf=new JFrame("snake game");
jf.setSize(800,800);
jf.setLocationRelativeTo(null);//窗口显示屏幕中间
jf.setResizable(false);//固定窗口大小
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.add(new Console(800,800,10,10,200));//在窗口容器中放置一个游戏面板
jf.setVisible(true);
}
}
2.3.2 运行效果
2.3.3 项目结构
(console包下的Info可以忽略,这个是我计划做的控制面板,目前没有实现)
最后附上一个项目包下载链接
3 小结
到这里整个贪吃蛇就已经初步完成了,并且边学边做已经学习了how2j中最底下两行的大部分内容了。