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 游戏。