最新消息:XAMPP默认安装之后是很不安全的,我们只需要点击左方菜单的 "安全"选项,按照向导操作即可完成安全设置。

Java实战项目:用Java开发贪吃蛇游戏 so easy

XAMPP相关 admin 565浏览 0评论

游戏玩法

该游戏用上下左右控制蛇的方向,寻找吃的东西,每吃一口就能得到一定的积分,而且蛇的身子会越吃越长,身子越长玩的难度就越大,不能碰墙,不能咬到自己的身体,更不能咬自己的尾巴,等到了一定的分数,就能过关,然后继续玩下一关。

需求分析

方向控制

首先我们需要实现的是通过按键实现控制蛇的运动方向,需要注意的有两点:

1.蛇运动的时候不能向上一个状态的反方向运动,例如,原先向右,下一次改变的方向不能为左。

2.运动的时候如果按了一个方向键,再下一次按键之前将维持原先的方向运动。

3.如果蛇头和身体的图片不一样,那么蛇头要随着运动方向进行旋转。

蛇的绘制

蛇我这里分为了蛇头和蛇身两部分,当然你也可以加蛇尾。这里以蛇头和蛇身两部分为例:

蛇头游戏开始就已经存在,之后吃到一个食物都会使蛇身长度加一。蛇身的每一部分都会沿着它的前一部分的轨迹运动,而每一部分都会沿着蛇头的轨迹运动。

食物绘制

食物绘制相对比较简单,当一个食物被吃掉以后,便在地图的其他随机的一个地方产生下一个食物。

 

蛇和食物的生命周期

蛇:当蛇碰到地图边界,碰到自己的身体和尾巴的时候,即判定为死亡。

食物:当蛇头碰到食物,则食物死亡。

 

代码实现

● 项目目录

zzzzzt32

● Constant类,存储一些常量。

public class Constant {
  public static final int GAME_WIDTH = 1024;//窗体宽度
  public static final int GAME_HEIGHT = 578;//窗体高度
  
  public static final String IMG_PRE="com/zzk/snake/img/";//图片路径前缀
}

● MyFrame类,用于加载游戏窗体和不断刷新绘制窗体内容:

import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
 
import com.zzk.snake.constant.Constant;
 
public class MyFrame extends Frame{
  /**
   * 加载窗体
   */
  public void loadFrame(){
    this.setTitle("贪吃蛇");//设置窗体标题
    this.setSize(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);//设置窗体大小
    this.setBackground(Color.BLACK);//设置背景
    this.setLocationRelativeTo(null);//居中
    //设置可关闭
    this.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    //设置可见
    this.setVisible(true);
    //运行重绘线程
    new MyThread().start();
  }
  /**
   * 防止图片闪烁,使用双重缓存
   * 
   * @param g
   */
  Image backImg = null;
 
  @Override
  public void update(Graphics g) {
    if (backImg == null) {
      backImg = createImage(Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
    }
    Graphics backg = backImg.getGraphics();
    Color c = backg.getColor();
    backg.setColor(Color.BLACK);
    backg.fillRect(0, 0, Constant.GAME_WIDTH, Constant.GAME_HEIGHT);
    backg.setColor(c);
    paint(backg);    
    g.drawImage(backImg, 0, 0, null);
  }
  /**
   * 这里创建一个不断重绘的线程内部类
   * 
   * @param args
   */
  class MyThread extends Thread{
    @Override
    public void run() {
      while(true){
        repaint();
        try {
          sleep(30);//每30毫秒重绘一次
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

● GameUtil类,用于获取图片和处理图片旋转

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
 
import javax.imageio.ImageIO;
 
public class GameUtil {
 
  /**
   * 根据图片的相对路径获取图片
   * 
   * @param imagePath
   * @return 图片
   */
  public static Image getImage(String imagePath) {
    URL url = GameUtil.class.getClassLoader().getResource(imagePath);
    BufferedImage img = null;
    try {
      img = ImageIO.read(url);
    } catch (IOException e) {
      e.printStackTrace();
    }
    return img;
  }
  /**
   * 按指定角度旋转图片
   * @param bufferedimage
   * @param degree
   * @return 图片
   */
  public static Image rotateImage(final BufferedImage bufferedimage, final int degree) {
    int w = bufferedimage.getWidth();// 得到图片宽度。
    int h = bufferedimage.getHeight();// 得到图片高度。
    int type = bufferedimage.getColorModel().getTransparency();// 得到图片透明度。
    BufferedImage img;// 空的图片。
    Graphics2D graphics2d;// 空的画笔。
    (graphics2d = (img = new BufferedImage(w, h, type)).createGraphics())
        .setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    graphics2d.rotate(Math.toRadians(degree), w / 2, h / 2);// 旋转,degree是整型,度数,比如垂直90度。
    graphics2d.drawImage(bufferedimage, 0, 0, null);// 从bufferedimagecopy图片至img,0,0是img的坐标。
    graphics2d.dispose();
    return img;// 返回复制好的图片,原图片依然没有变,没有旋转,下次还可以使用。
  }
}

● ImageUtil类,用于存储图片,方便使用

import java.awt.Image;
import java.util.HashMap;
import java.util.Map;
 
import com.zzk.snake.constant.Constant;
 
public class ImageUtil {
  public static Map<String,Image> images = new HashMap<>();
  
  static{
    images.put("snake_body", GameUtil.getImage(Constant.IMG_PRE+"snake_body.png"));
    images.put("food", GameUtil.getImage(Constant.IMG_PRE+"food.png"));
    images.put("snake_head", GameUtil.getImage(Constant.IMG_PRE+"snake_head.png"));
    images.put("background", GameUtil.getImage(Constant.IMG_PRE+"background.jpg"));
    images.put("fail", GameUtil.getImage(Constant.IMG_PRE+"fail.png"));
  }
}

● Drawable和Moveable接口,蛇有移动和绘制的能力

import java.awt.Graphics;
 
public interface Drawable {
  void draw(Graphics g);
}
public interface Moveable {
  void move();
}

● SnakeObject类,蛇和食物的父类,由于食物和蛇都需要进行绘制,都有生命周期,所以抽取出一个父类。

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
 
public abstract class SnakeObject implements Drawable {
 
  int x;//横坐标
  int y;//纵坐标
  Image img;//图片
  int width;//图片宽度
  int height;//图片高度
  public boolean live;//死亡/存活
  
  @Override
  public abstract void draw(Graphics g);
  /**
   * 获取图片对应的矩形
   * 
   * @return
   */
  public Rectangle getRectangle() {
    return new Rectangle(x, y, width, height);
  }
}

● Food类,食物类,绘制食物

import java.awt.Graphics;
 
import com.zzk.snake.constant.Constant;
import com.zzk.snake.util.ImageUtil;
 
public class Food extends SnakeObject{
 
  public Food(){
    this.live=true;
    this.img=ImageUtil.images.get("food");
    this.width=img.getWidth(null);
    this.height=img.getHeight(null);
    this.x=(int) (Math.random()*(Constant.GAME_WIDTH-width+10));
    this.y=(int) (Math.random()*(Constant.GAME_HEIGHT-40-height)+40);
 
  }
  /**
   * 食物被吃的方法
   * @param mySnake
   */
  public void eaten(MySnake mySnake){
    if(mySnake.getRectangle().intersects(this.getRectangle())&&live&&mySnake.live){
      this.live=false;//食物死亡
      mySnake.setLength(mySnake.getLength()+1);//长度加一
      mySnake.score+=10*mySnake.getLength();//加分
    }
  }
  /**
   * 绘制食物
   */
  @Override
  public void draw(Graphics g) {
    g.drawImage(img, x, y, null);
  }
  
}

● MySnake ,蛇类,用于绘制蛇,用了一个LinkedList<Point>存储蛇的每一次移动的轨迹点,当蛇吃到东西时,从尾部的轨迹点绘制一块蛇身。每次移动后添加新的轨迹点,同时移除不必要的轨迹点。

import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.List;
 
import com.zzk.snake.constant.Constant;
import com.zzk.snake.util.GameUtil;
import com.zzk.snake.util.ImageUtil;
 
public class MySnake extends SnakeObject implements Moveable {
  //蛇头图片(未旋转)
  private static final BufferedImage IMG_SNAKE_HEAD = (BufferedImage) ImageUtil.images.get("snake_head");
 
  private int speed;//移动速度
  private int length;//长度
  private int num;//
  public static List<Point> bodyPoints = new LinkedList<>();
  public int score = 0;//分数
  private static BufferedImage newImgSnakeHead;//旋转后的蛇头图片
  boolean up, down, left, right = true;//初始态向右
  public MySnake(int x, int y) {
    this.live = true;
    this.x = x;
    this.y = y;
    this.img = ImageUtil.images.get("snake_body");
    this.width = img.getWidth(null);
    this.height = img.getHeight(null);
    this.speed = 5;
    this.length = 1;
    this.num = width / speed;
    newImgSnakeHead = IMG_SNAKE_HEAD;
  }
 
  public int getLength() {
    return length;
  }
  public void setLength(int length) {
    this.length=length;
  }
  /**
   * 接收键盘按下事件
   * @param e
   */
  public void keyPressed(KeyEvent e) {
    switch (e.getKeyCode()) {
    case KeyEvent.VK_UP:
      if (!down) {// 不能向初始方向的反方向移动
        up = true;
        down = false;
        left = false;
        right = false;
        newImgSnakeHead = (BufferedImage) GameUtil.rotateImage(IMG_SNAKE_HEAD, -90);//旋转图片
      }
      break;
    case KeyEvent.VK_DOWN:
      if (!up) {
        up = false;
        down = true;
        left = false;
        right = false;
        newImgSnakeHead = (BufferedImage) GameUtil.rotateImage(IMG_SNAKE_HEAD, 90);
      }
      break;
    case KeyEvent.VK_LEFT:
      if (!right) {
        up = false;
        down = false;
        left = true;
        right = false;
        newImgSnakeHead = (BufferedImage) GameUtil.rotateImage(IMG_SNAKE_HEAD, -180);
      }
      break;
    case KeyEvent.VK_RIGHT:
      if (!left) {
        up = false;
        down = false;
        left = false;
        right = true;
        newImgSnakeHead = IMG_SNAKE_HEAD;
      }
      break;
    }
  }
  /**
   * 移动
   */
  @Override
  public void move() {
    if (up)
      y -= speed;
    else if (down)
      y += speed;
    else if (left)
      x -= speed;
    else if (right)
      x += speed;
  }
  /**
   * 绘制
   */
  @Override
  public void draw(Graphics g) {
    outOfBounds();//处理出界问题
    eatBody();//处理是否吃到身体问题
    bodyPoints.add(new Point(x, y));//保存轨迹
    if (bodyPoints.size() == (this.length+1) * num) {//当保存的轨迹点的个数为蛇的长度+1的num倍时
      bodyPoints.remove(0);//移除第一个
    }
    g.drawImage(newImgSnakeHead, x, y, null);//绘制蛇头
    drawBody(g);//绘制蛇身
    move();//移动
  }
  /**
   * 处理是否吃到到身体问题
   */
  public void eatBody(){
    for (Point point : bodyPoints) {
      for (Point point2 : bodyPoints) {
        if(point.equals(point2)&&point!=point2){
          this.live=false;//食物死亡
        }
      }
    }
  }
  /**
   * 绘制蛇身
   * @param g
   */
  public void drawBody(Graphics g) {
    int length = bodyPoints.size() - 1-num;//前num个存储的是蛇头的当前轨迹坐标
    for (int i = length; i >= num; i -= num) {//从尾部添加
      Point p = bodyPoints.get(i);
      g.drawImage(img, p.x, p.y, null);
    }
  }
 
  /**
   * 处理出界问题
   */
  private void outOfBounds() {
    boolean xOut = (x <= 0 || x >= (Constant.GAME_WIDTH - width));
    boolean yOut = (y <= 40 || y >= (Constant.GAME_HEIGHT - height));
    if (xOut || yOut) {
      live = false;
    }
  }
}

● SnakeClient类,加载窗体,控制游戏流程,我这里没有进行关卡控制和开始界面等,读者可以自行修改。

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
 
import com.zzk.snake.core.Food;
import com.zzk.snake.core.MyFrame;
import com.zzk.snake.core.MySnake;
import com.zzk.snake.util.ImageUtil;
 
public class SnakeClient extends MyFrame{
  
  public MySnake mySnake = new MySnake(100, 100);//蛇
  public Food food = new Food();//食物
  Image background = ImageUtil.images.get("background");//背景图片
  Image fail = ImageUtil.images.get("fail");//游戏结束的文字
  @Override
  public void loadFrame() {
    super.loadFrame();
    //添加键盘监听器,处理键盘按下事件
    addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {
        mySnake.keyPressed(e);//委托给mysnake
      }
    });
  }
  /**
   * 绘制界面
   */
  @Override
  public void paint(Graphics g) {
    g.drawImage(background, 0, 0, null);//绘制背景
    if(mySnake.live){//如果蛇活着,就绘制
      mySnake.draw(g);
      if(food.live){//如果食物活着,就绘制
        food.draw(g);
        food.eaten(mySnake);
      }else{//否则,产生新食物
        food = new Food();
      }
    }else{//蛇死亡,弹出游戏结束字样
      g.drawImage(fail, (background.getWidth(null)-fail.getWidth(null))/2, (background.getHeight(null)-fail.getHeight(null))/2, null);
    }
    drawScore(g);//绘制分数
  }
  /**
   * 绘制分数
   * @param g
   */
  public void drawScore(Graphics g){
    g.setFont(new Font("Courier New", Font.BOLD, 40));
    g.setColor(Color.WHITE);
    g.drawString("SCORE:"+mySnake.score,700,100);
  }
  public static void main(String[] args) {
    new SnakeClient().loadFrame();//加载窗体
  }
}

效果展示

zzzzzt032

zzzzzt0032

以上就是贪吃蛇游戏的开发原理和代码,

感兴趣的童鞋可以实际操作一下。

转载请注明:XAMPP中文组官网 » Java实战项目:用Java开发贪吃蛇游戏 so easy

您必须 登录 才能发表评论!