一丶游戏效果
二、游戏架构
一个Java项目可以有多种设计模式,MVC,工厂,单例等等。
但在本次项目中,我们将介绍最基础的知识,在后续项目中逐步升级。
贪吃蛇项目的基本架构是窗体为主体,在面板中执行游戏操作,并在面板中绘制贪吃蛇图像和显示用户操作
三、代码设计
1.窗体架构
public class StartGame {
public static void main(String[] args) {
JFrame jFrame =new JFrame();
jFrame .setTitle("贪吃豆");
int width= Toolkit.getDefaultToolkit().getScreenSize().width;
int height=Toolkit.getDefaultToolkit().getScreenSize().height;
jFrame .setBounds((width-700)/2,(height-700)/2,700,700);
jFrame .setResizable(false);
jFrame .setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
GamePanel gamePanel= new GamePanel();
jFrame.add(gamePanel);
jFrame .setVisible(true);
}
}
窗体方面我们利用Java中的JFrame创建一个窗体,并利用Toolkit获取屏幕大小,使的窗体能够正常显示在电脑中央
同时,这个main方法也是程序的入口,可以看出,我们在StartGame中加入了gamePanel这个对象,而我们的图像显示和游戏逻辑全部封装在GamePanel这个类中
2.游戏嵌板
Panel ,中文意思为“嵌板”。这个嵌板相当于窗体上的一个区域,我们可以根据自己的需求放置嵌板,并在对应的嵌板中设计我们的功能。这个项目只有一个嵌板。
package com.Karthus.game;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
public class GamePanel extends JPanel {
int[] snakeX=new int[200];
int[] snakeY=new int[200];
int length;
String direction;
boolean isStart;
boolean isDie;
boolean isHungry;
int foodX=200;
int foodY=200;
Timer timer;
public GamePanel()
{
init();
this.setFocusable(true);
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
int keyCode=e.getKeyCode();
if(keyCode==KeyEvent.VK_SPACE)
{
if(isDie)
{
init();
repaint();
}
else
{
isStart=!isStart;
repaint();
}
}
if (keyCode==KeyEvent.VK_UP)
{
direction="u";
}
if (keyCode==KeyEvent.VK_DOWN)
{
direction="d";
}
if (keyCode==KeyEvent.VK_RIGHT)
{
direction="r";
}
if (keyCode==KeyEvent.VK_LEFT)
{
direction="l";
}
}
});
timer =new Timer(100, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(isStart)
{
for(int i=length-1;i>0;i--)
{
snakeX[i]=snakeX[i-1];
snakeY[i]=snakeY[i-1];
}
if("r".equals(direction))
{
snakeX[0]+=25;
}
if("l".equals(direction))
{
snakeX[0]-=25;
}
if("u".equals(direction))
{
snakeY[0]-=25;
}
if("d".equals(direction))
{
snakeY[0]+=25;
}
if(snakeX[0]>700)
{
snakeX[0]=0;
}
if(snakeX[0]<0)
{
snakeX[0]=675;
}
if(snakeY[0]>700)
{
snakeY[0]=0;
}
if(snakeY[0]<0)
{
snakeY[0]=675;
}
if(snakeX[0]==foodX&&snakeY[0]==foodY)
{
length++;
foodX=new Random().nextInt(27)*25;
foodY=(new Random().nextInt(26)+1)*25;
isHungry=false;
}
for(int i=length-1;i>0;i--)
{
if(snakeX[i]==snakeX[0]&&snakeY[i]==snakeY[0])
{
isDie=true;
isStart=false;
}
}
repaint();
}}
});
timer.start();
}
public void init()
{
length=3;
snakeX[0]=175;
snakeY[0]=275;
snakeX[1]=150;
snakeY[1]=275;
snakeX[2]=125;
snakeY[2]=275;
direction="r";
isStart=false;
isDie=false;
isHungry=true;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
this.setBackground(new Color(231, 214, 214));
g.setColor(new Color(252, 252, 252));
g.fillRect(0,0,700,700);
if(isDie)
{
g.setColor(new Color(65, 193, 193));
g.setFont(new Font("微软雅黑",Font.BOLD,30));
g.drawString("游戏死亡,按空格键重新开始",200,300);
}
switch (direction)
{
case "r":Images.right.paintIcon(this,g,snakeX[0],snakeY[0]);break;
case "l":Images.left.paintIcon(this,g,snakeX[0],snakeY[0]);break;
case "u":Images.up.paintIcon(this,g,snakeX[0],snakeY[0]);break;
case "d":Images.down.paintIcon(this,g,snakeX[0],snakeY[0]);break;
}
if(isHungry) {
for (int i = 1; i < length; i++) {
Images.body.paintIcon(this, g, snakeX[i], snakeY[i]);
isHungry=false;
}
}
else {
for (int i = 1; i < length-1; i++) {
Images.body.paintIcon(this, g, snakeX[i], snakeY[i]);
}
}
Images.food.paintIcon(this,g,foodX,foodY);
if(!isStart&&!isDie)
{
g.setColor(new Color(65, 193, 193));
g.setFont(new Font("微软雅黑",Font.BOLD,30));
g.drawString("点击空格开始游戏",200,300);
}
}
}
以上是GamePanel嵌板的全部内容。由于程序较简单,我们将蛇的属性放置于该嵌板中。下面会详细介绍嵌板中的内容。
3.图像绘制与显示
想要绘制图像,首先得拿到图片资源
程序中获取图片的方法有很多种,这里我们将所有图片文件放入images文件夹中
点击Build,选择reBuild project
会生成一个out文件,这些文件时JVM运行的文件,也就是JAVA虚拟机,图片的路径将保存在这个文件中
新建一个images类,将需要的图片数据放入这个类中
public class Images {
public static URL upUrl =Images.class.getResource("/images/head_up.png");
public static URL rightUrl =Images.class.getResource("/images/head_right.png");
public static URL leftUrl =Images.class.getResource("/images/head_left.png");
public static URL downUrl =Images.class.getResource("/images/head_down.png");
public static URL bodyUrl =Images.class.getResource("/images/body.png");
public static URL barUrl =Images.class.getResource("/images/topBar.png");
public static URL foodUrl=Images.class.getResource("/images/food.png");
public static ImageIcon up =new ImageIcon(upUrl);
public static ImageIcon right =new ImageIcon(rightUrl);
public static ImageIcon left =new ImageIcon(leftUrl);
public static ImageIcon down =new ImageIcon(downUrl);
public static ImageIcon body =new ImageIcon(bodyUrl);
public static ImageIcon bar=new ImageIcon(barUrl);
public static ImageIcon food=new ImageIcon(foodUrl);
}
收集到图片资源后就可以开始绘制了
在GamePanel中有一个绘制方法
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
this.setBackground(new Color(231, 214, 214));
}
g就相当于画笔,而每次生成一个嵌板对象时,都将自动调用该方法
对于整个嵌板就相当于一个坐标系,一张图片,我们想要放在嵌板中的某个地方,我们要考虑的问题,这个图片展多大空间,在嵌板的那个位置?
因此我们在GamePanel定义了蛇的二维坐标,并定义了蛇头位置朝向
下面给出蛇的画法
switch (direction)
{
case "r":Images.right.paintIcon(this,g,snakeX[0],snakeY[0]);break;
case "l":Images.left.paintIcon(this,g,snakeX[0],snakeY[0]);break;
case "u":Images.up.paintIcon(this,g,snakeX[0],snakeY[0]);break;
case "d":Images.down.paintIcon(this,g,snakeX[0],snakeY[0]);break;
}
for(int i=1;i<length;i++)
Images.body.paintIcon(this, g, snakeX[i], snakeY[i]);
4.视窗刷新与按键监听
游戏必须得要动起来才是游戏,贪吃蛇也要能够自己动起来,并且能够监听键盘的方向键
首先在GamePanel类中加入监听方法
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
int keyCode=e.getKeyCode();
if(keyCode==KeyEvent.VK_SPACE)
{
if(isDie)
{
init();
repaint();
}
else
{
isStart=!isStart;
repaint();
}
}
if (keyCode==KeyEvent.VK_UP)
{
direction="u";
}
if (keyCode==KeyEvent.VK_DOWN)
{
direction="d";
}
if (keyCode==KeyEvent.VK_RIGHT)
{
direction="r";
}
if (keyCode==KeyEvent.VK_LEFT)
{
direction="l";
}
}
}
监听按键利用适配器模式
我们知道游戏刷新的本质是视图的刷新,也就是说我们要不断重复执行嵌板中的画图方法,我们定义一个Timer计时器让他计时刷新
即让是刷新我们还要弄清楚小蛇每次移动的算法,那么小蛇每次移动都是由后一节向前一节传递,而头部的刷新是由小蛇运动的方向决定的,刷新代码如下
timer =new Timer(100, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(isStart)
{
for(int i=length-1;i>0;i--)
{
snakeX[i]=snakeX[i-1];
snakeY[i]=snakeY[i-1];
}
if("r".equals(direction))
{
snakeX[0]+=25;
}
if("l".equals(direction))
{
snakeX[0]-=25;
}
if("u".equals(direction))
{
snakeY[0]-=25;
}
if("d".equals(direction))
{
snakeY[0]+=25;
}
if(snakeX[0]>700)
{
snakeX[0]=0;
}
if(snakeX[0]<0)
{
snakeX[0]=675;
}
if(snakeY[0]>700)
{
snakeY[0]=0;
}
if(snakeY[0]<0)
{
snakeY[0]=675;
}
if(snakeX[0]==foodX&&snakeY[0]==foodY)
{
length++;
foodX=new Random().nextInt(27)*25;
foodY=(new Random().nextInt(26)+1)*25;
isHungry=false;
}
for(int i=length-1;i>0;i--)
{
if(snakeX[i]==snakeX[0]&&snakeY[i]==snakeY[0])
{
isDie=true;
isStart=false;
}
}
repaint();
}}
});
timer.start();
}
repaint将会重新执行paintComponent方法
5.食物刷新与小蛇死亡判断
每次吃完一个食物后将有新事物刷新,同时蛇身变长
if(snakeX[0]==foodX&&snakeY[0]==foodY)
{
length++;
foodX=new Random().nextInt(27)*25;
foodY=(new Random().nextInt(26)+1)*25;
isHungry=false;
}
小蛇死亡的判断则是判断蛇头与蛇身的坐标是否重合
for(int i=length-1;i>0;i--)
{
if(snakeX[i]==snakeX[0]&&snakeY[i]==snakeY[0])
{
isDie=true;
isStart=false;
}
}
6.游戏暂停与重新开始
在游戏过程中如果要暂停或者游戏死亡后要重新开始,这就需要控制嵌板的画图计时,因此加入了isStart判定
空格监听
if(keyCode==KeyEvent.VK_SPACE)
{
if(isDie)
{
init();
repaint();
}
else
{
isStart=!isStart;
repaint();
}
}