您的代码中有一些优点以及一些可能会妨碍您将 GUI 与当前控制台程序代码混搭的主要问题,其中包括:
- 您确实将程序逻辑分离为单独的类,例如 Player、Map 和 Room 类,这是一件非常好的事情,但您仍然可以进行更多的逻辑分离,这样做可以简化让您的代码更易于调试和增强。
- Player 类包含一个 Player 实例,我不知道为什么。外部主类可能应该从
Player 重命名为 Game 类,并且 Player 类应该是一个完全独立的类,它包含 single 播放器实例的状态,并且不应该像你一样创建自己的实例。关键是努力“优化”你的代码,尤其是代码的“模型”部分,即包含非用户界面程序逻辑的代码,使其更加面向对象,因为这会使它变得更加更容易让您将 GUI 或任何其他界面连接到您的模型。
- 您的模型(再次命名为 Player)中包含用户界面 (UI) 代码:其中包含 Scanner 输入代码和 println 语句。这是有问题的,尤其是现在,因为您想增强程序并使其与 GUI 一起工作。首先,您必须做的最重要的事情是将用户界面代码(即从用户获取输入并将输出显示给用户的代码)分离到模型类之外,该类再次包含程序逻辑。
- 您的 Player 类有很多非 Player 代码使其混乱。一个基本的 OOP 原则是一个类应该有一个单一的职责(称为“单一职责规则”),因此您的 Player 类应该只包含封装 Player 状态所需的代码(例如 name、Room,也许还有其他属性,例如力量、武器、健康、智力……)和玩家行为(允许玩家与其他职业互动的方法),或多或少。
- 按照同样的思路,所有程序数据和行为似乎都被硬连接到驱动程序的单个主 Player 类中。这是非常有限的,并且会阻碍您创建一个包含许多房间、房间中的物品以及诸如此类的东西的丰富游戏环境的能力。分而治之 - 从主程序中获取这些信息并将其放入符合 OOP 的类中,包括一个 Room 类,该类具有它可能包含的项目的字段,它知道自己与其他房间的连接。
- 您的 GUI 程序扩展了 Player 对象。这是一个主要问题,因为这个程序结构没有通过基本的继承测试,即“is-a”测试:从逻辑上讲,GUI 是一种更具体的播放器类型(很像 Dog 是一种更具体的 Animal,a通过“is-a”测试的结构)?不,它不是,虽然这种区别可能看起来很悬疑,但由于这种继承,您可以尝试在 GUI 类中使用 Player 字段,但如果您尝试这样做,代码将无法正常工作。删除此继承,而是尝试通过“组合”连接类,其中一个类包含另一个类的实例,而不是扩展另一个类。因此,例如,GUI 可能会保存一个 Player 变量,而不是从 Player 扩展。
底线:
您将希望在尝试添加 GUI 之前完全“OOP-ify”您的代码
为了使程序能够与 GUI 很好地结合,我建议使您的底层逻辑代码(非用户界面逻辑代码)更加面向对象,并使用所有具有 的类单一职责,易于可测试的类(远离 GUI 或任何用户界面)。从第一原则开始,程序将更容易构建。
因此,例如,可以考虑用于冒险游戏的类包括:
- Direction 类,或者更好的Direction enum。
这将封装“方向”的概念,并将用于让游戏中的对象知道它们要去哪个方向,并能够使用常量而不是字符串将其传达给其他对象。枚举的用户将允许编译器检查方向是否正确使用,因为如果您使用字符串,例如“West”,编译器将不会自动知道您是否输入错误的字符串并使用“Best”代替。
像这样简单的东西,例如:
public enum Direction {
NORTH, EAST, SOUTH, WEST
}
这将包含信息,告诉它它在房间网格中的位置,名称和描述属性的字符串,以及游戏玩家字段或游戏玩家的List(如果允许多个),可能包含代码像这样:....
public class GameRoom {
// map with connections to other game rooms
private Map<Direction, GameRoom> connections = new HashMap<Direction, GameRoom>();
// location information
private int x;
private int y;
// identifying information
private String roomName;
private String description;
// holds any player objects that may be in the room
private List<GamePlayer> playersInRoom = new ArrayList<>();
该类将具有适当的构造函数以及 getter 和 setter 方法,并且:
允许游戏将玩家添加到房间的addPlayer(...) 方法:
public void addPlayer(GamePlayer gamePlayer) {
playersInRoom.add(gamePlayer);
gamePlayer.setCurrentRoom(this); // we'll get to this later
}
public boolean move(...) 方法将房间中包含的玩家移动到连接房间。它首先检查房间是否真的包含被移动的玩家,然后检查房间是否与请求方向的另一个房间有连接。如果其中任何一个为假,则该方法返回假以让尝试移动失败的调用代码。否则,如果允许移动,则返回 true:
public boolean move(GamePlayer gamePlayer, Direction direction) {
// if the room doesn't currently hold this player
if (!playersInRoom.contains(gamePlayer)) {
return false; // invalid move request
}
// if the room doesn't have a connecting room in the requested direction
if (!connections.containsKey(direction)) {
return false; // invalid move request
}
// otherwise, we're good
playersInRoom.remove(gamePlayer);
connections.get(direction).addPlayer(gamePlayer);
return true;
}
这个类将包含几个属性,例如名称的 String 和当前房间的 GameRoom 字段、构造函数、getter 和 setter 方法。它的声明和字段可能类似于:
public class GamePlayer {
private String name;
private GameRoom currentRoom;
//.... other properties
它也应该有一个 move 方法来调用 currentRoom GameRoom 的 move 方法并返回相同的布尔值,该结果告诉调用代码移动是否有效且成功:
public boolean move(Direction direction) {
return currentRoom.move(this, direction);
}
这将保存当前游戏的状态、玩家字段、保存所有房间的数据结构。它可以有自己的move方法.....
只有在创建了符合 OOP 的类之后,您才能进行下一步:创建 GUI 或“视图”类/类。这个类可以保存一个 GameModel 实例,并负责 1) 显示游戏的状态(房间及其持有的物品的视觉表示),以及 2) 从用户,并将该输入传递给游戏模型进行处理。
我喜欢使用 MVC 程序结构,它代表“模型-视图-控制器”,其中程序逻辑和 GUI 尽可能分开,并且可能有一个或多个控制器类帮助绑定模型和一起来看。我尝试遵循的另一个原则是使 GUI 尽可能“愚蠢”。愚蠢,我的意思是它应该从用户那里获得输入,也许做最基本的输入验证,并且应该显示模型的状态,但仅此而已。程序的几乎所有“大脑”都应该由模型本身而不是视图来控制。
概念验证示例:
我上面描述的一个不完整但正在运行的“概念验证”示例如下所示。您应该复制整个程序,将其粘贴到您的 IDE 中,并将其粘贴到名为 VideoGame.java 的单个文件中,然后应该能够运行它。它使用了一些您可能还不熟悉的概念,包括使用键绑定从 GUI 获取用户输入以及使用 PropertyChangeListeners 和 PropertyChangeSupport 对象来允许对象之间的干净通信(如果 state 通知侦听器 模型对象之一已更改)。该程序应该响应箭头键的按下:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;
public class VideoGame {
private static final int[][] ROOM_GRID_KEY = {
{ 1, 0, 0, 2, 0, 0, 3, 4 },
{ 5, 6, 7, 8, 9, 10, 11, 0 },
{ 0, 12, 0, 13, 0, 0, 0, 0 },
{ 0, 14, 0, 0, 0, 0, 15, 16 },
{ 17, 18, 0, 19, 0, 0, 20, 0 },
{ 21, 22, 23, 24, 25, 26, 27, 28 }
};
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
GameModel gameModel = new GameModel(ROOM_GRID_KEY);
GameView gameView = new GameView();
new GameController(gameModel, gameView);
JFrame frame = new JFrame("Video Game");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(gameView);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
class GameController {
private GameModel gameModel;
private GameView gameView;
public GameController(GameModel gameModel, GameView gameView) {
this.gameModel = gameModel;
this.gameView = gameView;
ModelListener modelListener = new ModelListener();
gameView.setModel(gameModel);
gameModel.addPropertyChangeListener(modelListener);
}
private class ModelListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
gameView.modelChange(evt);
}
}
}
@SuppressWarnings("serial")
class DisplayPanel extends JPanel {
private JPanel[][] panelGrid;
private int gridCellSize;
private GameModel gameModel;
private GameRoom[][] roomGrid;
public DisplayPanel(int gridCellSize) {
this.gridCellSize = gridCellSize;
}
public void setModel(GameModel gameModel) {
this.gameModel = gameModel;
this.roomGrid = gameModel.getRoomGrid();
int rows = roomGrid.length;
int cols = roomGrid[0].length;
setBackground(Color.BLACK);
setLayout(new GridLayout(rows, cols, 1, 1));
setBorder(BorderFactory.createLineBorder(Color.BLACK));
panelGrid = new JPanel[rows][cols];
for (int r = 0; r < panelGrid.length; r++) {
for (int c = 0; c < panelGrid[r].length; c++) {
JPanel panel = new JPanel(new GridBagLayout());
panelGrid[r][c] = panel;
panel.setPreferredSize(new Dimension(gridCellSize, gridCellSize));
if (roomGrid[r][c] == null) {
panel.setBackground(Color.BLACK);
} else {
panel.setBackground(Color.PINK);
if (roomGrid[r][c].getPlayersInRoom().size() > 0) {
GamePlayer gamePlayer = roomGrid[r][c].getPlayersInRoom().get(0);
String name = gamePlayer.getName();
JLabel label = new JLabel(name);
panel.add(label);
}
}
add(panel);
}
}
// key bindings code
addBindings(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), Direction.SOUTH);
addBindings(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), Direction.NORTH);
addBindings(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), Direction.WEST);
addBindings(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), Direction.EAST);
}
private void addBindings(KeyStroke keyStroke, Direction direction) {
int condition = WHEN_IN_FOCUSED_WINDOW;
InputMap inputMap = getInputMap(condition);
ActionMap actionMap = getActionMap();
Action action = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
gameModel.move(direction);
}
};
inputMap.put(keyStroke, keyStroke.toString());
actionMap.put(keyStroke.toString(), action);
}
public void modelChange(PropertyChangeEvent evt) {
for (int r = 0; r < panelGrid.length; r++) {
for (int c = 0; c < panelGrid[r].length; c++) {
JPanel panel = panelGrid[r][c];
if (roomGrid[r][c] != null) {
if (roomGrid[r][c].getPlayersInRoom().size() > 0) {
GamePlayer gamePlayer = roomGrid[r][c].getPlayersInRoom().get(0);
String name = gamePlayer.getName();
JLabel label = new JLabel(name);
panel.add(label);
} else {
panel.removeAll();
}
}
}
}
revalidate();
repaint();
}
public GameModel getGameModel() {
return gameModel;
}
}
class GameView extends JPanel {
private static final int CELL_SIZE = 80;
private DisplayPanel displayPanel = new DisplayPanel(CELL_SIZE);
private GameModel gameModel;
// private JTextField textField = new JTextField();
public GameView() {
setLayout(new BorderLayout());
add(displayPanel);
// add(textField, BorderLayout.PAGE_END);
}
public void setModel(GameModel gameModel) {
this.gameModel = gameModel;
displayPanel.setModel(gameModel);
}
public void modelChange(PropertyChangeEvent evt) {
displayPanel.modelChange(evt);
}
public GameModel getGameModel() {
return gameModel;
}
}
class GameModel {
public static final String GAME_MODEL = "game model";
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
private GameRoom[][] roomGrid;
private GamePlayer player = new GamePlayer("Fred");
public GameModel(int[][] roomGridKey) {
roomGrid = new GameRoom[roomGridKey.length][roomGridKey[0].length];
// fill room grid with rooms if 1 in grid key array, with null if 0
for (int y = 0; y < roomGridKey.length; y++) {
for (int x = 0; x < roomGridKey[0].length; x++) {
roomGrid[y][x] = roomGridKey[y][x] != 0 ? new GameRoom("Some Room", "Some Description", x, y) : null;
}
}
// make room connections:
for (int y = 0; y < roomGrid.length; y++) {
for (int x = 0; x < roomGrid[0].length; x++) {
GameRoom thisRoom = roomGrid[y][x];
// if no room present, don't
if (thisRoom == null) {
continue;
}
if (x > 0) {
GameRoom otherGameRoom = roomGrid[y][x - 1];
if (otherGameRoom != null) {
thisRoom.putConnection(Direction.WEST, otherGameRoom);
}
}
if (x < roomGrid[0].length - 1) {
GameRoom otherGameRoom = roomGrid[y][x + 1];
if (otherGameRoom != null) {
thisRoom.putConnection(Direction.EAST, otherGameRoom);
}
}
if (y > 0) {
GameRoom otherGameRoom = roomGrid[y - 1][x];
if (otherGameRoom != null) {
thisRoom.putConnection(Direction.NORTH, otherGameRoom);
}
}
if (y < roomGrid.length - 1) {
GameRoom otherGameRoom = roomGrid[y + 1][x];
if (otherGameRoom != null) {
thisRoom.putConnection(Direction.SOUTH, otherGameRoom);
}
}
}
}
// put player in top left room
GameRoom currentRoom = roomGrid[0][0];
if (currentRoom == null) {
// some big error occurred
System.err.println("Current room at 0, 0 is null. Exiting");
System.exit(-1);
}
player.setCurrentRoom(currentRoom);
currentRoom.addPlayer(player);
player.addPropertyChangeListener(pce -> pcSupport.firePropertyChange(GAME_MODEL, null, player));
}
public boolean move(Direction direction) {
boolean success = player.move(direction);
return success;
}
public GamePlayer getPlayer() {
return player;
}
public GameRoom[][] getRoomGrid() {
return roomGrid;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(GAME_MODEL, listener);
}
}
class GamePlayer {
public static final String GAME_PLAYER = "game player";
private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);
private String name;
private GameRoom currentRoom;
public GamePlayer(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public boolean move(Direction direction) {
boolean success = currentRoom.move(this, direction);
return success;
}
public void setCurrentRoom(GameRoom currentRoom) {
GameRoom oldValue = this.currentRoom;
GameRoom newValue = currentRoom;
this.currentRoom = currentRoom;
pcSupport.firePropertyChange(GAME_PLAYER, oldValue, newValue);
}
public GameRoom getCurrentRoom() {
return currentRoom;
}
@Override
public String toString() {
return "GamePlayer [name=" + name + ", currentRoom=" + currentRoom + "]";
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcSupport.addPropertyChangeListener(GAME_PLAYER, listener);
}
}
class GameRoom {
// map with connections to other game rooms
private Map<Direction, GameRoom> connections = new HashMap<Direction, GameRoom>();
// location information
private int x;
private int y;
// identifying information
private String roomName;
private String description;
// holds any player objects that may be in the room
private List<GamePlayer> playersInRoom = new ArrayList<>();
public GameRoom(String roomName, String description, int x, int y) {
this.roomName = roomName;
this.description = description;
this.x = x;
this.y = y;
}
public boolean move(GamePlayer gamePlayer, Direction direction) {
// if the room doesn't currently hold this player
if (!playersInRoom.contains(gamePlayer)) {
return false; // invalid move request
}
// if the room doesn't have a connecting room in the requested direction
if (!connections.containsKey(direction)) {
return false; // invalid move request
}
// otherwise, we're good
playersInRoom.remove(gamePlayer);
connections.get(direction).addPlayer(gamePlayer);
return true;
}
public void addPlayer(GamePlayer gamePlayer) {
playersInRoom.add(gamePlayer);
gamePlayer.setCurrentRoom(this);
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public String getRoomName() {
return roomName;
}
public String getDescription() {
return description;
}
public List<GamePlayer> getPlayersInRoom() {
return playersInRoom;
}
public Map<Direction, GameRoom> getConnections() {
return connections;
}
public void putConnection(Direction direction, GameRoom otherGameRoom) {
connections.put(direction, otherGameRoom);
}
@Override
public String toString() {
return "GameRoom [x=" + x + ", y=" + y + "]";
}
}
enum Direction {
NORTH, EAST, SOUTH, WEST
}
再次,编译并运行此代码,并使用箭头键在网格中移动“Fred”。
更完整的程序会将数据完全从代码中分离出来,并允许文件 I/O 将房间网格中的数据读入程序,可能通过创建一个或多个 CSV 文件来保存房间信息,或者如果预计数据会增长并变得更复杂,然后是关系数据库,例如使用一种 SQL 风格的数据库。