原文: https://zetcode.com/tutorials/javagamestutorial/sokoban/

在 Java 2D 游戏教程的这一部分中,我们将创建 Java 推箱子游戏克隆。 源代码和图像可以在作者的 Github Java-Sokoban-Game 存储库中找到。

推箱子

推箱子是另一个经典的电脑游戏。 它由 Imabayashi Hiroyuki 于 1980 年创建。 推箱子是日语的仓库管理员。 玩家在迷宫周围推箱子。 目的是将所有盒子放置在指定的位置。

Java 推箱子游戏的开发

我们使用光标键控制推箱子对象。 我们也可以按 R 键重新启动电平。 将所有行李放在目的地区域后,游戏结束。 我们在窗口的左上角绘制"Completed"字符串。

Board.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.Graphics;
  4. import java.awt.event.KeyAdapter;
  5. import java.awt.event.KeyEvent;
  6. import java.util.ArrayList;
  7. import javax.swing.JPanel;
  8. public class Board extends JPanel {
  9. private final int OFFSET = 30;
  10. private final int SPACE = 20;
  11. private final int LEFT_COLLISION = 1;
  12. private final int RIGHT_COLLISION = 2;
  13. private final int TOP_COLLISION = 3;
  14. private final int BOTTOM_COLLISION = 4;
  15. private ArrayList<Wall> walls;
  16. private ArrayList<Baggage> baggs;
  17. private ArrayList<Area> areas;
  18. private Player soko;
  19. private int w = 0;
  20. private int h = 0;
  21. private boolean isCompleted = false;
  22. private String level
  23. = " ######\n"
  24. + " ## #\n"
  25. + " ##$ #\n"
  26. + " #### $##\n"
  27. + " ## $ $ #\n"
  28. + "#### # ## # ######\n"
  29. + "## # ## ##### ..#\n"
  30. + "## $ $ ..#\n"
  31. + "###### ### #@## ..#\n"
  32. + " ## #########\n"
  33. + " ########\n";
  34. public Board() {
  35. initBoard();
  36. }
  37. private void initBoard() {
  38. addKeyListener(new TAdapter());
  39. setFocusable(true);
  40. initWorld();
  41. }
  42. public int getBoardWidth() {
  43. return this.w;
  44. }
  45. public int getBoardHeight() {
  46. return this.h;
  47. }
  48. private void initWorld() {
  49. walls = new ArrayList<>();
  50. baggs = new ArrayList<>();
  51. areas = new ArrayList<>();
  52. int x = OFFSET;
  53. int y = OFFSET;
  54. Wall wall;
  55. Baggage b;
  56. Area a;
  57. for (int i = 0; i < level.length(); i++) {
  58. char item = level.charAt(i);
  59. switch (item) {
  60. case '\n':
  61. y += SPACE;
  62. if (this.w < x) {
  63. this.w = x;
  64. }
  65. x = OFFSET;
  66. break;
  67. case '#':
  68. wall = new Wall(x, y);
  69. walls.add(wall);
  70. x += SPACE;
  71. break;
  72. case '$':
  73. b = new Baggage(x, y);
  74. baggs.add(b);
  75. x += SPACE;
  76. break;
  77. case '.':
  78. a = new Area(x, y);
  79. areas.add(a);
  80. x += SPACE;
  81. break;
  82. case '@':
  83. soko = new Player(x, y);
  84. x += SPACE;
  85. break;
  86. case ' ':
  87. x += SPACE;
  88. break;
  89. default:
  90. break;
  91. }
  92. h = y;
  93. }
  94. }
  95. private void buildWorld(Graphics g) {
  96. g.setColor(new Color(250, 240, 170));
  97. g.fillRect(0, 0, this.getWidth(), this.getHeight());
  98. ArrayList<Actor> world = new ArrayList<>();
  99. world.addAll(walls);
  100. world.addAll(areas);
  101. world.addAll(baggs);
  102. world.add(soko);
  103. for (int i = 0; i < world.size(); i++) {
  104. Actor item = world.get(i);
  105. if (item instanceof Player || item instanceof Baggage) {
  106. g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
  107. } else {
  108. g.drawImage(item.getImage(), item.x(), item.y(), this);
  109. }
  110. if (isCompleted) {
  111. g.setColor(new Color(0, 0, 0));
  112. g.drawString("Completed", 25, 20);
  113. }
  114. }
  115. }
  116. @Override
  117. public void paintComponent(Graphics g) {
  118. super.paintComponent(g);
  119. buildWorld(g);
  120. }
  121. private class TAdapter extends KeyAdapter {
  122. @Override
  123. public void keyPressed(KeyEvent e) {
  124. if (isCompleted) {
  125. return;
  126. }
  127. int key = e.getKeyCode();
  128. switch (key) {
  129. case KeyEvent.VK_LEFT:
  130. if (checkWallCollision(soko,
  131. LEFT_COLLISION)) {
  132. return;
  133. }
  134. if (checkBagCollision(LEFT_COLLISION)) {
  135. return;
  136. }
  137. soko.move(-SPACE, 0);
  138. break;
  139. case KeyEvent.VK_RIGHT:
  140. if (checkWallCollision(soko, RIGHT_COLLISION)) {
  141. return;
  142. }
  143. if (checkBagCollision(RIGHT_COLLISION)) {
  144. return;
  145. }
  146. soko.move(SPACE, 0);
  147. break;
  148. case KeyEvent.VK_UP:
  149. if (checkWallCollision(soko, TOP_COLLISION)) {
  150. return;
  151. }
  152. if (checkBagCollision(TOP_COLLISION)) {
  153. return;
  154. }
  155. soko.move(0, -SPACE);
  156. break;
  157. case KeyEvent.VK_DOWN:
  158. if (checkWallCollision(soko, BOTTOM_COLLISION)) {
  159. return;
  160. }
  161. if (checkBagCollision(BOTTOM_COLLISION)) {
  162. return;
  163. }
  164. soko.move(0, SPACE);
  165. break;
  166. case KeyEvent.VK_R:
  167. restartLevel();
  168. break;
  169. default:
  170. break;
  171. }
  172. repaint();
  173. }
  174. }
  175. private boolean checkWallCollision(Actor actor, int type) {
  176. switch (type) {
  177. case LEFT_COLLISION:
  178. for (int i = 0; i < walls.size(); i++) {
  179. Wall wall = walls.get(i);
  180. if (actor.isLeftCollision(wall)) {
  181. return true;
  182. }
  183. }
  184. return false;
  185. case RIGHT_COLLISION:
  186. for (int i = 0; i < walls.size(); i++) {
  187. Wall wall = walls.get(i);
  188. if (actor.isRightCollision(wall)) {
  189. return true;
  190. }
  191. }
  192. return false;
  193. case TOP_COLLISION:
  194. for (int i = 0; i < walls.size(); i++) {
  195. Wall wall = walls.get(i);
  196. if (actor.isTopCollision(wall)) {
  197. return true;
  198. }
  199. }
  200. return false;
  201. case BOTTOM_COLLISION:
  202. for (int i = 0; i < walls.size(); i++) {
  203. Wall wall = walls.get(i);
  204. if (actor.isBottomCollision(wall)) {
  205. return true;
  206. }
  207. }
  208. return false;
  209. default:
  210. break;
  211. }
  212. return false;
  213. }
  214. private boolean checkBagCollision(int type) {
  215. switch (type) {
  216. case LEFT_COLLISION:
  217. for (int i = 0; i < baggs.size(); i++) {
  218. Baggage bag = baggs.get(i);
  219. if (soko.isLeftCollision(bag)) {
  220. for (int j = 0; j < baggs.size(); j++) {
  221. Baggage item = baggs.get(j);
  222. if (!bag.equals(item)) {
  223. if (bag.isLeftCollision(item)) {
  224. return true;
  225. }
  226. }
  227. if (checkWallCollision(bag, LEFT_COLLISION)) {
  228. return true;
  229. }
  230. }
  231. bag.move(-SPACE, 0);
  232. isCompleted();
  233. }
  234. }
  235. return false;
  236. case RIGHT_COLLISION:
  237. for (int i = 0; i < baggs.size(); i++) {
  238. Baggage bag = baggs.get(i);
  239. if (soko.isRightCollision(bag)) {
  240. for (int j = 0; j < baggs.size(); j++) {
  241. Baggage item = baggs.get(j);
  242. if (!bag.equals(item)) {
  243. if (bag.isRightCollision(item)) {
  244. return true;
  245. }
  246. }
  247. if (checkWallCollision(bag, RIGHT_COLLISION)) {
  248. return true;
  249. }
  250. }
  251. bag.move(SPACE, 0);
  252. isCompleted();
  253. }
  254. }
  255. return false;
  256. case TOP_COLLISION:
  257. for (int i = 0; i < baggs.size(); i++) {
  258. Baggage bag = baggs.get(i);
  259. if (soko.isTopCollision(bag)) {
  260. for (int j = 0; j < baggs.size(); j++) {
  261. Baggage item = baggs.get(j);
  262. if (!bag.equals(item)) {
  263. if (bag.isTopCollision(item)) {
  264. return true;
  265. }
  266. }
  267. if (checkWallCollision(bag, TOP_COLLISION)) {
  268. return true;
  269. }
  270. }
  271. bag.move(0, -SPACE);
  272. isCompleted();
  273. }
  274. }
  275. return false;
  276. case BOTTOM_COLLISION:
  277. for (int i = 0; i < baggs.size(); i++) {
  278. Baggage bag = baggs.get(i);
  279. if (soko.isBottomCollision(bag)) {
  280. for (int j = 0; j < baggs.size(); j++) {
  281. Baggage item = baggs.get(j);
  282. if (!bag.equals(item)) {
  283. if (bag.isBottomCollision(item)) {
  284. return true;
  285. }
  286. }
  287. if (checkWallCollision(bag,BOTTOM_COLLISION)) {
  288. return true;
  289. }
  290. }
  291. bag.move(0, SPACE);
  292. isCompleted();
  293. }
  294. }
  295. break;
  296. default:
  297. break;
  298. }
  299. return false;
  300. }
  301. public void isCompleted() {
  302. int nOfBags = baggs.size();
  303. int finishedBags = 0;
  304. for (int i = 0; i < nOfBags; i++) {
  305. Baggage bag = baggs.get(i);
  306. for (int j = 0; j < nOfBags; j++) {
  307. Area area = areas.get(j);
  308. if (bag.x() == area.x() && bag.y() == area.y()) {
  309. finishedBags += 1;
  310. }
  311. }
  312. }
  313. if (finishedBags == nOfBags) {
  314. isCompleted = true;
  315. repaint();
  316. }
  317. }
  318. public void restartLevel() {
  319. areas.clear();
  320. baggs.clear();
  321. walls.clear();
  322. initWorld();
  323. if (isCompleted) {
  324. isCompleted = false;
  325. }
  326. }
  327. }

游戏简化了。 它仅提供非常基本的功能。 该代码比容易理解。 游戏只有一个关卡。

  1. private final int OFFSET = 30;
  2. private final int SPACE = 20;
  3. private final int LEFT_COLLISION = 1;
  4. private final int RIGHT_COLLISION = 2;
  5. private final int TOP_COLLISION = 3;
  6. private final int BOTTOM_COLLISION = 4;

墙图片大小为20x20像素。 这反映了SPACE常数。 OFFSET是窗口边界和游戏世界之间的距离。 有四种类型的碰撞。 每个数字都由一个数字常数表示。

  1. private ArrayList<Wall> walls;
  2. private ArrayList<Baggage> baggs;
  3. private ArrayList<Area> areas;

墙壁,行李和区域是特殊的容器,可容纳游戏的所有墙壁,行李和区域。

  1. private String level =
  2. " ######\n"
  3. + " ## #\n"
  4. + " ##$ #\n"
  5. + " #### $##\n"
  6. + " ## $ $ #\n"
  7. + "#### # ## # ######\n"
  8. + "## # ## ##### ..#\n"
  9. + "## $ $ ..#\n"
  10. + "###### ### #@## ..#\n"
  11. + " ## #########\n"
  12. + " ########\n";

这是游戏的水平。 除空格外,还有五个字符。 井号(#)代表墙。 美元($)表示要移动的框。 点(.)字符表示我们必须移动框的位置。 at 字符(@)是推箱子。 最后,换行符(\n)开始了世界的新行。

  1. private void initWorld() {
  2. walls = new ArrayList<>();
  3. baggs = new ArrayList<>();
  4. areas = new ArrayList<>();
  5. int x = OFFSET;
  6. int y = OFFSET;
  7. ...

initWorld()方法启动游戏世界。 它遍历级别字符串并填充上述列表。

  1. case '$':
  2. b = new Baggage(x, y);
  3. baggs.add(b);
  4. x += SPACE;
  5. break;

对于美元字符,我们创建一个Baggage对象。 该对象将附加到行李列表。 x 变量相应增加。

  1. private void buildWorld(Graphics g) {
  2. ...

buildWorld()方法在窗口上绘制游戏世界。

  1. ArrayList<Actor> world = new ArrayList<>();
  2. world.addAll(walls);
  3. world.addAll(areas);
  4. world.addAll(baggs);
  5. world.add(soko);

我们创建一个包含游戏所有对象的世界列表。

  1. for (int i = 0; i < world.size(); i++) {
  2. Actor item = world.get(i);
  3. if (item instanceof Player || item instanceof Baggage) {
  4. g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
  5. } else {
  6. g.drawImage(item.getImage(), item.x(), item.y(), this);
  7. }
  8. ...
  9. }

我们遍历世界容器并绘制对象。 播放器和行李图像稍小。 我们在其坐标上添加 2px 以使其居中。

  1. if (isCompleted) {
  2. g.setColor(new Color(0, 0, 0));
  3. g.drawString("Completed", 25, 20);
  4. }

如果完成该级别,则在窗口的左上角绘制"Completed"

  1. case KeyEvent.VK_LEFT:
  2. if (checkWallCollision(soko,
  3. LEFT_COLLISION)) {
  4. return;
  5. }
  6. if (checkBagCollision(LEFT_COLLISION)) {
  7. return;
  8. }
  9. soko.move(-SPACE, 0);
  10. break;

keyPressed()方法内部,我们检查了按下了哪些键。 我们用光标键控制推箱子对象。 如果按左光标键,我们将检查推箱子是否与墙壁或行李相撞。 如果没有,我们将推箱子向左移动。

  1. case KeyEvent.VK_R:
  2. restartLevel();
  3. break;

如果按R键,我们将重新启动该级别。

  1. case LEFT_COLLISION:
  2. for (int i = 0; i < walls.size(); i++) {
  3. Wall wall = walls.get(i);
  4. if (actor.isLeftCollision(wall)) {
  5. return true;
  6. }
  7. }
  8. return false;

创建checkWallCollision()方法以确保推箱子或行李不会通过墙壁。 有四种类型的碰撞。 上面几行检查是否有左碰撞。

  1. private boolean checkBagCollision(int type) {
  2. ...
  3. }

checkBagCollision()涉及更多。 行李可能会与墙壁,推箱子或其他行李发生碰撞。 仅当行李与推箱子碰撞且不与其他行李或墙壁碰撞时,才可以移动行李。 搬运行李时,该通过调用isCompleted()方法检查水平是否已完成。

  1. for (int i = 0; i < nOfBags; i++) {
  2. Baggage bag = baggs.get(i);
  3. for (int j = 0; j < nOfBags; j++) {
  4. Area area = areas.get(j);
  5. if (bag.x() == area.x() && bag.y() == area.y()) {
  6. finishedBags += 1;
  7. }
  8. }
  9. }

isCompleted()方法检查级别是否完成。 我们得到行李数。 我们比较所有行李和目的地区域的 x 和 y 坐标。

  1. if (finishedBags == nOfBags) {
  2. isCompleted = true;
  3. repaint();
  4. }

finishedBags变量等于游戏中的行李数时,游戏结束。

  1. private void restartLevel() {
  2. areas.clear();
  3. baggs.clear();
  4. walls.clear();
  5. initWorld();
  6. if (isCompleted) {
  7. isCompleted = false;
  8. }
  9. }

如果我们做了一些不好的动作,我们可以重新启动关卡。 我们从列表中删除所有对象,然后再次启动世界。 isCompleted变量设置为false

Actor.java

  1. package com.zetcode;
  2. import java.awt.Image;
  3. public class Actor {
  4. private final int SPACE = 20;
  5. private int x;
  6. private int y;
  7. private Image image;
  8. public Actor(int x, int y) {
  9. this.x = x;
  10. this.y = y;
  11. }
  12. public Image getImage() {
  13. return image;
  14. }
  15. public void setImage(Image img) {
  16. image = img;
  17. }
  18. public int x() {
  19. return x;
  20. }
  21. public int y() {
  22. return y;
  23. }
  24. public void setX(int x) {
  25. this.x = x;
  26. }
  27. public void setY(int y) {
  28. this.y = y;
  29. }
  30. public boolean isLeftCollision(Actor actor) {
  31. return x() - SPACE == actor.x() && y() == actor.y();
  32. }
  33. public boolean isRightCollision(Actor actor) {
  34. return x() + SPACE == actor.x() && y() == actor.y();
  35. }
  36. public boolean isTopCollision(Actor actor) {
  37. return y() - SPACE == actor.y() && x() == actor.x();
  38. }
  39. public boolean isBottomCollision(Actor actor) {
  40. return y() + SPACE == actor.y() && x() == actor.x();
  41. }
  42. }

这是Actor类。 该类是游戏中其他演员的基础类。 它封装了推箱子游戏中对象的基本功能。

  1. public boolean isLeftCollision(Actor actor) {
  2. return x() - SPACE == actor.x() && y() == actor.y();
  3. }

此方法检查演员是否与左侧的另一个演员(墙壁,行李,推箱子)相撞。

Wall.java

  1. package com.zetcode;
  2. import java.awt.Image;
  3. import javax.swing.ImageIcon;
  4. public class Wall extends Actor {
  5. private Image image;
  6. public Wall(int x, int y) {
  7. super(x, y);
  8. initWall();
  9. }
  10. private void initWall() {
  11. ImageIcon iicon = new ImageIcon("src/resources/wall.png");
  12. image = iicon.getImage();
  13. setImage(image);
  14. }
  15. }

这是Wall类。 它继承自Actor类。 构建后,它将从资源中加载墙图像。

Player.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Player extends Actor {

    public Player(int x, int y) {
        super(x, y);

        initPlayer();
    }

    private void initPlayer() {

        ImageIcon iicon = new ImageIcon("src/resources/sokoban.png");
        Image image = iicon.getImage();
        setImage(image);
    }

    public void move(int x, int y) {

        int dx = x() + x;
        int dy = y() + y;

        setX(dx);
        setY(dy);
    }
}

这是Player类。

public void move(int x, int y) {

    int dx = x() + x;
    int dy = y() + y;

    setX(dx);
    setY(dy);
}

move()方法将对象移动到世界内部。

Baggage.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Baggage extends Actor {

    public Baggage(int x, int y) {
        super(x, y);

        initBaggage();
    }

    private void initBaggage() {

        ImageIcon iicon = new ImageIcon("src/resources/baggage.png");
        Image image = iicon.getImage();
        setImage(image);
    }

    public void move(int x, int y) {

        int dx = x() + x;
        int dy = y() + y;

        setX(dx);
        setY(dy);
    }
}

这是Baggage对象的类。 该对象是可移动的,因此也具有move()方法。

Area.java

package com.zetcode;

import java.awt.Image;
import javax.swing.ImageIcon;

public class Area extends Actor {

    public Area(int x, int y) {
        super(x, y);

        initArea();
    }

    private void initArea() {

        ImageIcon iicon = new ImageIcon("src/resources/area.png");
        Image image = iicon.getImage();
        setImage(image);
    }
}

这是Area类。 这是我们尝试放置行李的对象。

Sokoban.java

package com.zetcode;

import java.awt.EventQueue;
import javax.swing.JFrame;

public class Sokoban extends JFrame {

    private final int OFFSET = 30;

    public Sokoban() {

        initUI();
    }

    private void initUI() {

        Board board = new Board();
        add(board);

        setTitle("Sokoban");

        setSize(board.getBoardWidth() + OFFSET,
                board.getBoardHeight() + 2 * OFFSET);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            Sokoban game = new Sokoban();
            game.setVisible(true);
        });
    }
}

这是主要的类。

Java 推箱子 - 图1

图:推箱子

这是推箱子游戏。