Java Sokoban
最后修改于 2023 年 1 月 10 日
在本部分的 Java 2D 游戏教程中,我们创建一个 Java Sokoban 游戏克隆。源代码和图片可以在作者的 Github Java-Sokoban-Game 仓库中找到。
Sokoban
Sokoban 是另一款经典的电脑游戏。它由今林博之 (Hiroyuki Imabayashi) 于 1980 年创建。Sokoban 在日语中的意思是仓库管理员。玩家在迷宫中推箱子。目标是将所有箱子放到指定位置。
用 Java 开发 Sokoban 游戏
我们使用光标键控制 sokoban 对象。我们也可以按 R 键重新开始关卡。当所有包裹都放在目标区域时,游戏结束。我们在窗口左上角绘制“已完成”字符串。
package com.zetcode; import java.awt.Color; import java.awt.Graphics; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import javax.swing.JPanel; public class Board extends JPanel { private final int OFFSET = 30; private final int SPACE = 20; private final int LEFT_COLLISION = 1; private final int RIGHT_COLLISION = 2; private final int TOP_COLLISION = 3; private final int BOTTOM_COLLISION = 4; private ArrayList<Wall> walls; private ArrayList<Baggage> baggs; private ArrayList<Area> areas; private Player soko; private int w = 0; private int h = 0; private boolean isCompleted = false; private String level = " ######\n" + " ## #\n" + " ##$ #\n" + " #### $##\n" + " ## $ $ #\n" + "#### # ## # ######\n" + "## # ## ##### ..#\n" + "## $ $ ..#\n" + "###### ### #@## ..#\n" + " ## #########\n" + " ########\n"; public Board() { initBoard(); } private void initBoard() { addKeyListener(new TAdapter()); setFocusable(true); initWorld(); } public int getBoardWidth() { return this.w; } public int getBoardHeight() { return this.h; } private void initWorld() { walls = new ArrayList<>(); baggs = new ArrayList<>(); areas = new ArrayList<>(); int x = OFFSET; int y = OFFSET; Wall wall; Baggage b; Area a; for (int i = 0; i < level.length(); i++) { char item = level.charAt(i); switch (item) { case '\n': y += SPACE; if (this.w < x) { this.w = x; } x = OFFSET; break; case '#': wall = new Wall(x, y); walls.add(wall); x += SPACE; break; case '$': b = new Baggage(x, y); baggs.add(b); x += SPACE; break; case '.': a = new Area(x, y); areas.add(a); x += SPACE; break; case '@': soko = new Player(x, y); x += SPACE; break; case ' ': x += SPACE; break; default: break; } h = y; } } private void buildWorld(Graphics g) { g.setColor(new Color(250, 240, 170)); g.fillRect(0, 0, this.getWidth(), this.getHeight()); ArrayList<Actor> world = new ArrayList<>(); world.addAll(walls); world.addAll(areas); world.addAll(baggs); world.add(soko); for (int i = 0; i < world.size(); i++) { Actor item = world.get(i); if (item instanceof Player || item instanceof Baggage) { g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this); } else { g.drawImage(item.getImage(), item.x(), item.y(), this); } if (isCompleted) { g.setColor(new Color(0, 0, 0)); g.drawString("Completed", 25, 20); } } } @Override public void paintComponent(Graphics g) { super.paintComponent(g); buildWorld(g); } private class TAdapter extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { if (isCompleted) { return; } int key = e.getKeyCode(); switch (key) { case KeyEvent.VK_LEFT: if (checkWallCollision(soko, LEFT_COLLISION)) { return; } if (checkBagCollision(LEFT_COLLISION)) { return; } soko.move(-SPACE, 0); break; case KeyEvent.VK_RIGHT: if (checkWallCollision(soko, RIGHT_COLLISION)) { return; } if (checkBagCollision(RIGHT_COLLISION)) { return; } soko.move(SPACE, 0); break; case KeyEvent.VK_UP: if (checkWallCollision(soko, TOP_COLLISION)) { return; } if (checkBagCollision(TOP_COLLISION)) { return; } soko.move(0, -SPACE); break; case KeyEvent.VK_DOWN: if (checkWallCollision(soko, BOTTOM_COLLISION)) { return; } if (checkBagCollision(BOTTOM_COLLISION)) { return; } soko.move(0, SPACE); break; case KeyEvent.VK_R: restartLevel(); break; default: break; } repaint(); } } private boolean checkWallCollision(Actor actor, int type) { switch (type) { case LEFT_COLLISION: for (int i = 0; i < walls.size(); i++) { Wall wall = walls.get(i); if (actor.isLeftCollision(wall)) { return true; } } return false; case RIGHT_COLLISION: for (int i = 0; i < walls.size(); i++) { Wall wall = walls.get(i); if (actor.isRightCollision(wall)) { return true; } } return false; case TOP_COLLISION: for (int i = 0; i < walls.size(); i++) { Wall wall = walls.get(i); if (actor.isTopCollision(wall)) { return true; } } return false; case BOTTOM_COLLISION: for (int i = 0; i < walls.size(); i++) { Wall wall = walls.get(i); if (actor.isBottomCollision(wall)) { return true; } } return false; default: break; } return false; } private boolean checkBagCollision(int type) { switch (type) { case LEFT_COLLISION: for (int i = 0; i < baggs.size(); i++) { Baggage bag = baggs.get(i); if (soko.isLeftCollision(bag)) { for (int j = 0; j < baggs.size(); j++) { Baggage item = baggs.get(j); if (!bag.equals(item)) { if (bag.isLeftCollision(item)) { return true; } } if (checkWallCollision(bag, LEFT_COLLISION)) { return true; } } bag.move(-SPACE, 0); isCompleted(); } } return false; case RIGHT_COLLISION: for (int i = 0; i < baggs.size(); i++) { Baggage bag = baggs.get(i); if (soko.isRightCollision(bag)) { for (int j = 0; j < baggs.size(); j++) { Baggage item = baggs.get(j); if (!bag.equals(item)) { if (bag.isRightCollision(item)) { return true; } } if (checkWallCollision(bag, RIGHT_COLLISION)) { return true; } } bag.move(SPACE, 0); isCompleted(); } } return false; case TOP_COLLISION: for (int i = 0; i < baggs.size(); i++) { Baggage bag = baggs.get(i); if (soko.isTopCollision(bag)) { for (int j = 0; j < baggs.size(); j++) { Baggage item = baggs.get(j); if (!bag.equals(item)) { if (bag.isTopCollision(item)) { return true; } } if (checkWallCollision(bag, TOP_COLLISION)) { return true; } } bag.move(0, -SPACE); isCompleted(); } } return false; case BOTTOM_COLLISION: for (int i = 0; i < baggs.size(); i++) { Baggage bag = baggs.get(i); if (soko.isBottomCollision(bag)) { for (int j = 0; j < baggs.size(); j++) { Baggage item = baggs.get(j); if (!bag.equals(item)) { if (bag.isBottomCollision(item)) { return true; } } if (checkWallCollision(bag,BOTTOM_COLLISION)) { return true; } } bag.move(0, SPACE); isCompleted(); } } break; default: break; } return false; } public void isCompleted() { int nOfBags = baggs.size(); int finishedBags = 0; for (int i = 0; i < nOfBags; i++) { Baggage bag = baggs.get(i); for (int j = 0; j < nOfBags; j++) { Area area = areas.get(j); if (bag.x() == area.x() && bag.y() == area.y()) { finishedBags += 1; } } } if (finishedBags == nOfBags) { isCompleted = true; repaint(); } } public void restartLevel() { areas.clear(); baggs.clear(); walls.clear(); initWorld(); if (isCompleted) { isCompleted = false; } } }
游戏被简化了。它只提供了最基本的功能。因此代码更容易理解。游戏只有一个关卡。
private final int OFFSET = 30; private final int SPACE = 20; private final int LEFT_COLLISION = 1; private final int RIGHT_COLLISION = 2; private final int TOP_COLLISION = 3; private final int BOTTOM_COLLISION = 4;
墙的图像尺寸是 20x20 像素。这反映了 SPACE
常量。OFFSET
是窗口边框和游戏世界之间的距离。有四种类型的碰撞。每种碰撞都由一个数字常量表示。
private ArrayList<Wall> walls; private ArrayList<Baggage> baggs; private ArrayList<Area> areas;
墙、包裹和区域是特殊的容器,它们容纳了游戏中的所有墙、包裹和区域。
private String level = " ######\n" + " ## #\n" + " ##$ #\n" + " #### $##\n" + " ## $ $ #\n" + "#### # ## # ######\n" + "## # ## ##### ..#\n" + "## $ $ ..#\n" + "###### ### #@## ..#\n" + " ## #########\n" + " ########\n";
这是游戏关卡。除了空格,还有五个字符。井号(#)代表墙。美元符号($)代表要移动的箱子。点(.)字符代表我们必须移动箱子的位置。at 字符(@)是 sokoban。最后,换行符(\n)开始新的一行世界。
private void initWorld() { walls = new ArrayList<>(); baggs = new ArrayList<>(); areas = new ArrayList<>(); int x = OFFSET; int y = OFFSET; ...
initWorld()
方法初始化游戏世界。它遍历关卡字符串并填充上述列表。
case '$': b = new Baggage(x, y); baggs.add(b); x += SPACE; break;
对于美元字符,我们创建一个 Baggage
对象。该对象被附加到 baggs 列表中。x
变量相应地增加。
private void buildWorld(Graphics g) { ...
buildWorld()
方法在窗口上绘制游戏世界。
ArrayList<Actor> world = new ArrayList<>(); world.addAll(walls); world.addAll(areas); world.addAll(baggs); world.add(soko);
我们创建一个世界列表,其中包含游戏的所有对象。
for (int i = 0; i < world.size(); i++) { Actor item = world.get(i); if (item instanceof Player || item instanceof Baggage) { g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this); } else { g.drawImage(item.getImage(), item.x(), item.y(), this); } ... }
我们遍历世界容器并绘制对象。玩家和包裹的图像稍微小一些。我们将 2px 加到它们的坐标上以使它们居中。
if (isCompleted) { g.setColor(new Color(0, 0, 0)); g.drawString("Completed", 25, 20); }
如果关卡完成,我们在窗口的左上角绘制“已完成”。
case KeyEvent.VK_LEFT: if (checkWallCollision(soko, LEFT_COLLISION)) { return; } if (checkBagCollision(LEFT_COLLISION)) { return; } soko.move(-SPACE, 0); break;
在 keyPressed()
方法中,我们检查按下了什么键。我们使用光标键控制 sokoban 对象。如果我们按下左光标键,我们会检查 sokoban 是否与墙壁或包裹发生碰撞。如果没有,我们将 sokoban 向左移动。
case KeyEvent.VK_R: restartLevel(); break;
如果我们按下 R 键,我们会重新开始关卡。
case LEFT_COLLISION: for (int i = 0; i < walls.size(); i++) { Wall wall = walls.get(i); if (actor.isLeftCollision(wall)) { return true; } } return false;
创建了 checkWallCollision()
方法以确保 sokoban 或包裹不会穿过墙壁。有四种类型的碰撞。上面的几行检查左侧碰撞。
private boolean checkBagCollision(int type) { ... }
checkBagCollision()
涉及的内容更多一些。包裹可能与墙壁、sokoban 对象或其他包裹发生碰撞。只有当包裹与 sokoban 碰撞并且不与其他包裹或墙壁碰撞时,才能移动包裹。当包裹被移动时,就该通过调用 isCompleted()
方法来检查关卡是否完成。
for (int i = 0; i < nOfBags; i++) { Baggage bag = baggs.get(i); for (int j = 0; j < nOfBags; j++) { Area area = areas.get(j); if (bag.x() == area.x() && bag.y() == area.y()) { finishedBags += 1; } } }
isCompleted()
方法检查关卡是否完成。我们获取包裹的数量。我们比较所有包裹和目标区域的 x 和 y 坐标。
if (finishedBags == nOfBags) { isCompleted = true; repaint(); }
当 finishedBags
变量等于游戏中包裹的数量时,游戏就结束了。
private void restartLevel() { areas.clear(); baggs.clear(); walls.clear(); initWorld(); if (isCompleted) { isCompleted = false; } }
如果我们犯了错误,我们可以重新开始关卡。我们从列表中删除所有对象并重新初始化世界。isCompleted
变量设置为 false。
package com.zetcode; import java.awt.Image; public class Actor { private final int SPACE = 20; private int x; private int y; private Image image; public Actor(int x, int y) { this.x = x; this.y = y; } public Image getImage() { return image; } public void setImage(Image img) { image = img; } public int x() { return x; } public int y() { return y; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public boolean isLeftCollision(Actor actor) { return x() - SPACE == actor.x() && y() == actor.y(); } public boolean isRightCollision(Actor actor) { return x() + SPACE == actor.x() && y() == actor.y(); } public boolean isTopCollision(Actor actor) { return y() - SPACE == actor.y() && x() == actor.x(); } public boolean isBottomCollision(Actor actor) { return y() + SPACE == actor.y() && x() == actor.x(); } }
这是 Actor
类。该类是 Sokoban 游戏中其他 Actor 的基类。它封装了 Sokoban 游戏中对象的基本功能。
public boolean isLeftCollision(Actor actor) { return x() - SPACE == actor.x() && y() == actor.y(); }
此方法检查 Actor 是否与左侧的另一个 Actor(墙壁、包裹、sokoban)发生碰撞。
package com.zetcode; import java.awt.Image; import javax.swing.ImageIcon; public class Wall extends Actor { private Image image; public Wall(int x, int y) { super(x, y); initWall(); } private void initWall() { ImageIcon iicon = new ImageIcon("src/resources/wall.png"); image = iicon.getImage(); setImage(image); } }
这是 Wall
类。它继承自 Actor
类。在构造时,它从资源加载墙壁图像。
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()
方法将对象移动到世界中。
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()
方法。
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
类。它是我们尝试放置包裹的对象。
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); }); } }
这是主类。

这就是 Sokoban 游戏。