首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >俄罗斯方块游戏演示OOP原理

俄罗斯方块游戏演示OOP原理
EN

Code Review用户
提问于 2018-07-18 10:38:53
回答 2查看 4.5K关注 0票数 5

我设计了一个基本的俄罗斯方块游戏,并用Java语言实现了它。我将用它来演示软件工程和设计。我将用许多不同的编程语言,在不同的平台上重新实现它。因此,良好的概念是非常重要的。

目标:

  • 遵循OOP的一般原则。
  • 可读性,合理的类/算法/依赖关系。
  • 视图必须是可替换的,而不改变游戏的基本逻辑(控制台,LED矩阵输出)。
  • 应易于扩展与流行的功能(显示下四分钟,分数,时间等),改变现有的功能(颜色,大小)。

穿孔(即更多的代码/抽象),KISS/YAGNI不太重要。

更新版本实现:全源代码类图,相关代码如下:

TetrisGame.java

代码语言:javascript
复制
public class TetrisGame {
    private final static Random random = new Random();
    private final int blockSize = 30;
    private final int columns = 11;
    private final int rows = 16;

    private boolean isRunning;
    private static TetrisGame instance;
    private final Board board;
    private Tetromino fallingTetromino;
    private final PeriodicTask gravity;
    private final MainWindow window;

    private TetrisGame(MainWindow window) {
        this.window = window;
        BoardView view = new BoardView(window, blockSize);
        board = new Board(rows, columns, view);
        gravity = new PeriodicTask(() -> {
            boolean moved = fallingTetromino.moveDown();
            if (!moved)
                tetrominoCantMoveFurther();
        }, 700);
    }
    public static TetrisGame createNew(MainWindow window) {
        instance = new TetrisGame(window);
        return instance;
    }
    public static TetrisGame getInstance() {
        return instance;
    }

    public void start() {
        isRunning = true;
        generateNextTetromino();
        gravity.start();
    }
    private void stop() {
        isRunning = false;
        fallingTetromino.dispose();
        fallingTetromino = null;
        gravity.stop();
    }
    public void handleCommand(UserCommand command) {
        if (!isRunning)
            return;
        switch (command) {
            case ROTATE:
                fallingTetromino.rotateRight();
                break;
            case MOVE_LEFT:
                fallingTetromino.moveLeft();
                break;
            case MOVE_DOWN:
                if (fallingTetromino.moveDown())
                    gravity.reset();
                else
                    tetrominoCantMoveFurther();
                break;
            case MOVE_RIGHT:
                fallingTetromino.moveRight();
                break;
            case DROP:
                fallingTetromino.drop();
                tetrominoCantMoveFurther();
                break;
        }
    }

    public boolean canMoveTetrominoTo(Tetromino tetromino, int x, int y) {
        return board.canAddTetromino(tetromino, x, y);
    }

    private void tetrominoCantMoveFurther() {
        board.addTetromino(fallingTetromino);
        board.removeFullRows();
        generateNextTetromino();
    }
    private void generateNextTetromino() {
        if (fallingTetromino != null)
            fallingTetromino.dispose();
        int type = random.nextInt(7);
        TetrominoView view = new TetrominoView(window, blockSize);
        Tetromino next = Tetromino.createAtCenter(type, view, columns);
        fallingTetromino = next;
        if (next != null)
            gravity.reset();
        else
            stop();
    }
}

Board.java

代码语言:javascript
复制
public class Board {
    private final int rows;
    private final int columns;
    private SquareView[][] board;
    private final BoardView view;

    public Board(int rows, int cols, BoardView view) {
        this.rows = rows;
        this.columns = cols;
        this.board = new SquareView[rows][cols];
        this.view = view;
        updateView();
    }

    public boolean canAddTetromino(Tetromino tetromino, int fromX, int fromY) {
        SquareView[][] data = tetromino.getPolyominoData();
        int height = data.length;
        int width = data[0].length;
        if (fromX < 0 || fromX + width > columns ||
            fromY < 0 || fromY + height > rows)
            return false;
        for (int i = 0; i < height; ++i) {
            for (int j = 0; j < width; ++j) {
                if (data[i][j] != null && board[fromY + i][fromX + j] != null)
                    return false;
            }
        }
        return true;
    }
    public void addTetromino(Tetromino tetromino) {
        SquareView[][] data = tetromino.getPolyominoData();
        int x = tetromino.getPosX();
        int y = tetromino.getPosY();
        int height = data.length;
        int width = data[0].length;
        for (int i = 0; i < height; ++i) {
            for (int j = 0; j < width; ++j) {
                if (data[i][j] != null)
                    board[y + i][x + j] = data[i][j];
            }
        }
        updateView();
    }
    public void removeFullRows() {
        boolean isRowFull;
        for (int i = 0; i < rows; ++i) {
            isRowFull = true;
            for (int j = 0; j < columns && isRowFull; ++j) {
                if (board[i][j] == null)
                    isRowFull = false;
            }
            if (isRowFull) {
                for (int j = i; j > 0; --j)
                    System.arraycopy(board[j - 1], 0, board[j], 0, columns);
                for (int j = 0; j < columns; ++j)
                    board[0][j] = null;
            }
        }
        updateView();
    }

    private void updateView() {
        view.update(board);
    }
}

Tetromino.java

代码语言:javascript
复制
public class Tetromino {
    private SquareView[][][] partsData;
    private TetrominoView view;
    private int currentX = 0;
    private int currentY = 0;
    private int rotation;
    private int width;
    private int height;

    private Tetromino(int type, TetrominoView view) {
        this.view = view;
        partsData = TetrominoDataSource.getData(type);
        setRotation(0);
    }
    public static Tetromino createAtCenter(int type, TetrominoView view, int boardWidth) {
        Tetromino tetromino = new Tetromino(type, view);
        int x = (int) Math.ceil((boardWidth - tetromino.width) / 2);
        boolean moved = tetromino.tryMove(x, 0);
        if (!moved) {
            tetromino.dispose();
            return null;
        }
        return tetromino;
    }
    public void dispose() {
        view.clear();
    }

    public SquareView[][] getPolyominoData() {
        return partsData[rotation];
    }
    public int getPosX() {
        return currentX;
    }
    public int getPosY() {
        return currentY;
    }

    public boolean rotateRight() {
        int nextRotation = (rotation + 1) % 4;
        boolean canRotate = false;
        int oldRotation = rotation;
        setRotation(nextRotation);
        if (canMoveTo(0, 0))
            canRotate = true;
        else {
            for (int i = 1; i < width && !canRotate; ++i) {
                if (canMoveTo(-i, 0)) {
                    currentX -= i;
                    canRotate = true;
                }
            }
        }
        if (!canRotate)
            setRotation(oldRotation);
        else {
            setRotation(nextRotation);
            updateView();
        }
        return canRotate;
    }
    public boolean moveRight() {
        return tryMove(1, 0);
    }
    public boolean moveLeft() {
        return tryMove(-1, 0);
    }
    public boolean moveDown() {
        return tryMove(0, 1);
    }
    public void drop() {
        boolean movedDown;
        do {
            movedDown = moveDown();
        } while (movedDown);
    }

    private void setRotation(int rotation) {
        this.rotation = rotation % partsData.length;
        height = partsData[this.rotation].length;
        width = partsData[this.rotation][0].length;
    }
    private void updateView() {
        view.update(partsData[rotation], currentX, currentY);
    }
    private boolean tryMove(int x, int y) {
        boolean canSlide = canMoveTo(x, y);
        if (canSlide) {
            currentX += x;
            currentY += y;
            updateView();
        }
        return canSlide;
    }
    private boolean canMoveTo(int deltaX, int deltaY) {
        return TetrisGame.getInstance().canMoveTetrominoTo(this, currentX + deltaX, currentY + deltaY);
    }
}

TetrominoDataSource.java

代码语言:javascript
复制
public class TetrominoDataSource {
    public static SquareView[][][] getData(int type) {
        String[][] masks = rawData[type];
        SquareView[][][] result = new SquareView[masks.length][][];
        for (int rotation = 0; rotation < masks.length; ++rotation) {
            int height = masks[rotation].length;
            int width = masks[rotation][0].length();
            result[rotation] = new SquareView[height][width];
            for (int i = 0; i < height; ++i) {
                for (int j = 0; j < width; ++j) {
                    if (masks[rotation][i].charAt(j) != ' ')
                        result[rotation][i][j] = new SquareView(type);
                }
            }
        }
        return result;
    }

    private static String[][][] rawData = new String[][][] {
        new String[][] {
            new String[] {
                "XX",
                "XX"
            }
        },
        new String[][] {
            new String[] {
                "X",
                "X",
                "X",
                "X"
            },
            new String[] {
                "XXXX"
            }
        },

视图/TetrominoView.java (view/BoardView.java非常类似)

代码语言:javascript
复制
public class TetrominoView extends CanvasView {
    public TetrominoView(MainWindow window, int squareSize) {
        super(window.getTetrominoCanvas(), squareSize);
    }
    public void update(SquareView[][] data, int baseX, int baseY) {
        clear();
        int height = data.length;
        int width = data[0].length;
        for (int i = 0; i < height; ++i) {
            for (int j = 0; j < width; ++j) {
                if (data[i][j] != null)
                    data[i][j].update(context, baseX + j, baseY + i, squareSize);
            }
        }
    }
}

view:绘制一个填充的sqaure。

view:使用画布的助手类。

util:java.util.Timer的包装器。

window:将javafx.scene.input.KeyEvent转换为UserCommand

JavaFX :管理JavaFX应用程序。

EN

回答 2

Code Review用户

发布于 2018-07-18 19:40:11

单身通常是个坏主意,除非你有明确的理由。(它们几乎类似于全局变量。)在这种情况下,我看不出为什么TetrisGame应该是一个单例。事实上,如果你想让两个玩家同时玩游戏的话,你可能会想要实例化其中的两个,这似乎是很有道理的。

票数 4
EN

Code Review用户

发布于 2018-07-23 09:05:36

我的朋友们也检查了我的代码,我发现了一些可以改进的地方:

  • 他们说空头支票太多了。Id需方,我在板中使用了空单元格的null,在包含其部分的tetromino的2D数组中使用了null。Null检查在OOP中是不好的,因为您处理的是变量的状态,而不是对象的状态。另外,如果您忘记检查null,您将得到NullPointerException (是的,有时我在创建原始版本时得到了它)。
  • 在板中使用空单元格假设空单元格没有任何视图。这对可重用性不好(可能基于文本的版本需要打印一个空格)。
  • 宽度、高度、x和y坐标被“硬编码”到tetromino的2D数组中。最初,我使用矩阵变换来生成四分钟的旋转。这在2D数组中很容易(只是一些转置/反向操作),但从OOP的角度来看,它们没有那么有意义。数组的尺寸是多少?宽x高,还是高x宽?怎么编制索引的?名为ij的Iterator变量没有告诉它。

为了解决这些空检查问题,我添加了两个辅助数据结构。Boad单元实现空对象模式。在Tetromino中,任何地方都不需要部件的顺序,所以(完整的)一维数组就足够了,助手结构存储偏移量(相对于tetromino位置的相对距离)。您可以查看更改这里

  • 在函数generateNextTetromino中,也有一个空检查。我用的是工厂的方法。如果四分钟不能添加到董事会(即。),它返回null。创建一个新的对象不应该属于任何条件,这不是Tetromino的S的工作去处理自己。
  • generateNextTetromino还包含与生成无关的逻辑,但包含它的结果。
  • 这些名称不能很好地描述正在发生的事情: int x= (int) Math.ceil((boardWidth - tetromino.width) / 2);布尔移动= tetromino.tryMove(x,0);if (! moved ) {
  • 游戏不安全。因为计时器和JavaFX运行在不同的线程上,如果用户在计时器过期时同时按下一个键,并发操作可能会破坏Tetromino的状态。只有一个转换应该同时进行评估。
票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/199737

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档