【问题标题】:Game loop of a turn-based game回合制游戏的游戏循环
【发布时间】:2017-08-28 01:35:11
【问题描述】:

目前,我正在为一款简单的回合制游戏开发 AI。我设置游戏的方式如下(伪代码):

players = [User, AI];
(for player : players){
    player.addEventlistener(MoveListener (moveData)->move(moveData));
}

players[game.getTurn()].startTurn();

move 函数:

move(data){
    game.doStuff(data);
    if(game.isOver())
        return;

    game.nextTurn();
    players[game.getTurn()].startTurn();
}

这导致以下递归:

  • 开始转弯
  • 玩家/AI 移动
  • 移动函数被调用
  • 下一位玩家开始他们的回合
  • ...

这会一直重复直到游戏结束 - 请注意,游戏的长度是有限的,并且不会超过约 50 步。现在,即使递归是有限的,我也会收到 stackoverflow 错误。我的问题是:有没有办法解决这个问题?毕竟递归有什么问题吗?或者我应该实现一个游戏循环吗?我知道如果AIs 要相互对抗,这将如何工作,但如果程序必须等待用户输入,这将如何工作?

编辑
以下是递归的相关类:

Connect4类:

package connect4;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Connect4 extends Application {
    Group root = new Group();
    GameSquare[][] squares;
    GameButton[] buttons;
    int currentTurn;
    int columns = 7;
    int rows = 6;
    Text gameState;
    Player[] players;
    Game game;

    @Override
    public void start(Stage primaryStage) {        
        int size = 50;
        int padding = 10;

        gameState = new Text();
        gameState.setX(padding);
        gameState.setY((rows+1)*size+(rows+3)*padding);
        root.getChildren().add(gameState);

        buttons = new GameButton[columns];
        for(int i = 0; i < buttons.length; i++){
            buttons[i] = new GameButton(i);
            buttons[i].setMaxWidth(size);
            buttons[i].setMaxHeight(size);
            buttons[i].setLayoutX(i*size+(i+1)*padding);
            buttons[i].setLayoutY(padding);

            buttons[i].setMouseTransparent(true);                
            buttons[i].setVisible(false); 

            root.getChildren().add(buttons[i]);
        }

        players = new Player[2];

        players[0] = new UserControlled(buttons);
        players[1] = new AI();

        MoveListener listener = (int i) -> {move(i);};

        for(Player player : players)
            player.addListener(listener);

        game = new Game(columns, rows, players.length);

        squares = new GameSquare[columns][rows];

        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                squares[x][y] = new GameSquare(
                        x*size+(x+1)*padding, 
                        (y+1)*size+(y+2)*padding,
                        size,
                        size,
                        size,
                        size
                );
                root.getChildren().add(squares[x][y]);
            }
        }

        players[game.getTurn()].startTurn(game);
        updateTurn();
        updateSquares();

        draw(primaryStage);
    }

    public void move(int i){
        game.move(i);
        updateSquares();

        if(game.isGameOver()){
            if(game.isTie()){
                tie();
                return;
            } else {
                win();
                return;
            }
        }

        updateTurn();
        players[game.getTurn()].startTurn(game);
    }

    private void updateSquares(){
        int[][] board = game.getBoard();
        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                squares[x][y].setOwner(board[x][y]);
            }
        }
    }

    private void updateTurn(){
        gameState.setText("Player " + game.getTurn() + "'s turn");
    }

    public static void main(String[] args) {
        launch(args);
    }

    private void draw(Stage primaryStage){
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void win(){
        gameState.setText("Player " + game.getWinner() + " has won the game!");
    }

    private void tie(){
        gameState.setText("It's a tie!");
    }
}

Game 类: 包connect4;

public class Game {
    private int turn = 0;
    private int[][] board;
    private int columns;
    private int rows;
    private int players;
    private boolean gameOver = false;
    private boolean tie = false;
    private int winner = -1;

    public Game(int columns, int rows, int playerCount){
        this.columns = columns;
        this.rows = rows;
        board = new int[columns][rows];

        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                board[x][y] = -1;
            }
        }

        players = playerCount;
    }

    public int[][] getBoard(){
        return board;
    }

    public int getTurn(){
        return turn;
    }

    private void updateTurn(){
        turn++;
        if(turn >= players)
            turn = 0;
    }

    public boolean isGameOver(){
        return gameOver;
    }

    private void win(int player){
        gameOver = true;
        winner = player;
    }

    public int getWinner(){
        return winner;
    }

    private void tie(){
        gameOver = true;
        tie = true;
    }

    public boolean isTie(){
        return tie;
    }

    public void move(int i){
        if(gameOver)
            return;

        if(columnSpaceLeft(i) == 0){
            return;
        }

        board[i][columnSpaceLeft(i)-1] = turn;
        checkWin(turn);        
        checkFullBoard();        

        if(gameOver)
            return;

        updateTurn();
    }

    private void checkFullBoard(){
        for(int i = 0; i < columns; i++){
            if(columnSpaceLeft(i) != 0)
                return;
        }
        tie();
    }

    public int columnSpaceLeft(int column){
        for(int i = 0; i < board[column].length; i++){
            if(board[column][i] != -1)
                return i;
        }
        return board[column].length;
    }

    public int[] getAvailableColumns(){        
        int columnCount = 0;
        for(int i = 0; i < board.length; i++){
            if(columnSpaceLeft(i) != 0)
                columnCount++;
        }

        int[] columns = new int[columnCount];
        int i = 0;
        for(int j = 0; j < board.length; j++){
            if(columnSpaceLeft(i) != 0){
                columns[i] = j;
                i++;
            }
        }

        return columns;
    }

    private Boolean checkWin(int player){
        //vertical
        for(int x = 0; x < columns; x++){
            int count = 0;
            for(int y = 0; y < rows; y++){
                if(board[x][y] == player)
                    count++;
                else
                    count = 0;
                if(count >= 4){
                    win(player);
                    return true;
                }
            }
        }

        //horizontal
        for(int y = 0; y < rows; y++){
            int count = 0;
            for(int x = 0; x < columns; x++){
                if(board[x][y] == player)
                    count++;
                else
                    count = 0;
                if(count >= 4){
                    win(player);
                    return true;
                }
            }
        }

        //diagonal
        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                int count = 0;
                //diagonaal /
                if(!(x > columns-4 || y < 3) && board[x][y] == player){
                    count ++;
                    for(int i = 1; i <= 3; i++){
                        if(board[x+i][y-i] == player){
                            count++;
                            if(count >= 4){
                                win(player);
                                return true;
                            }
                        } else {
                            count = 0;
                            break;
                        }
                    }
                }

                //diagonal \                
                if(!(x > columns-4 || y > rows-4) && board[x][y] == player){
                    count ++;
                    for(int i = 1; i <= 3; i++){
                        if(board[x+i][y+i] == player){
                            count++;                            
                            if(count >= 4){
                                win(player);
                                return true;
                            }
                        } else {
                            count = 0;
                            break;
                        }
                    }
                }
            }
        }

        return false;
    }
}

UserControlled类:

package connect4;

import java.util.ArrayList;
import java.util.List;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;

public class UserControlled implements Player  {
    private List<MoveListener> listeners = new ArrayList<MoveListener>();
    private GameButton[] buttons;
    private boolean active = false;

    public UserControlled(GameButton[] buttons){
        this.buttons = buttons;
    }

    @Override
    public void addListener(MoveListener listener){
        listeners.add(listener);
    }

    @Override
    public void startTurn(Game game){
        System.out.println(0);
        active = true;
        for(int i = 0; i < buttons.length; i++){
            if(game.columnSpaceLeft(i) != 0){
                setButton(i, true);
                buttons[i].setOnAction(new EventHandler<ActionEvent>() {
                    @Override public void handle(ActionEvent e) {
                        move(( (GameButton) e.getTarget()).getColumn());
                    }
                });
            }
        }
    }

    private void move(int i){
        if(!active)
            return;
        active = false;
        disableButtons();
        for(MoveListener listener : listeners)
            listener.onMove(i);
    }

    private void disableButtons(){
        for(int i = 0; i < buttons.length; i++){
            setButton(i, false);  
        }
    }

    private void setButton(int i, boolean enable){
        if(enable){
            buttons[i].setMouseTransparent(false);                
            buttons[i].setVisible(true);
        } else {
            buttons[i].setMouseTransparent(true);                
            buttons[i].setVisible(false);              
        }
    }
}

AI 类与精简的UserControlled 类基本相同,除了startTurn 方法:

int[] columns = game.getAvailableColumns();
move(columns[rng.nextInt(columns.length)]);

MoveListener的接口很简单:

public interface MoveListener {
    void onMove(int i);
}

堆栈跟踪:

Exception in thread "JavaFX Application Thread" java.lang.StackOverflowError
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:142)
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
    at javafx.scene.text.Text.setText(Text.java:370)
    //note that the three lines above this are different every time
    //as the application crashes at a different point
    at connect4.Connect4.updateTurn(Connect4.java:107)
    at connect4.Connect4.move(Connect4.java:93)
    at connect4.Connect4.lambda$start$0(Connect4.java:49)
    at connect4.AI.move(AI.java:13)
    at connect4.AI.startTurn(AI.java:24)
    at connect4.Connect4.move(Connect4.java:94)
    at connect4.Connect4.lambda$start$0(Connect4.java:49)
    at connect4.AI.move(AI.java:13)
    ...etc

【问题讨论】:

  • while(!gameOver){ waitForUserInput(); actAccordingToInput(); makeAIMove(); }
  • 它总是取决于你的堆栈上的内容。如果您有许多或巨大的堆栈分配的本地对象,那么即使是几十个递归级别也可能已经太多了。正如@luk2302 建议的那样,让它迭代。如果递归深度没有足够低的限制,您应该始终选择迭代解决方案。
  • @Ext3h 问题是,我什至还没有从 AI 本身开始(现在它只是随机移动),整个程序甚至没有 400 行代码一半不相关。而不是增加堆栈大小,我想以一种干净的方式解决这个问题,这样它就不会回来咬我
  • 那么您需要提供的不仅仅是伪代码。错误可能出在您用于事件处理的任何库中,因为该库可能在​​堆栈上分配了过多的数据。最坏的情况是,你甚至可以得到每个递归的完整“数据”副本!一个合适的库会在延迟上下文中处理事件(将您的递归转换为迭代)-
  • 您必须共享堆栈跟踪和相关代码,以便更好地了解可能是什么原因。

标签: java recursion stack-overflow game-loop


【解决方案1】:

一般来说,除非您非常确定自己在做什么,否则不应使用递归。

想一想,每次调用下一步时,都会保存所有上下文,以及堆栈中的所有局部变量。在游戏中,它可能是很多东西。

回合制游戏中的常见游戏循环类似于:

while(!gameFinished()){
    for(player in players){
         player.doTurn();
    }
}

还要考虑到,递归很慢,因为它必须保存所有上下文并且需要时间,所以一般来说,在尝试使用递归之前要三思而后行。

编辑

要处理输入,您可以使用以下内容:

CompletableFuture.supplyAsync(this::waitUserInput)  
             .thenAccept(this::processUserInput)

在这里你可以找到它是如何工作的:

http://www.deadcoderising.com/java8-writing-asynchronous-code-with-completablefuture/

这样,您的代码将继续运行,因此请记住,在下一行代码中您将没有输入。当它得到输入时,它会调用 proccessUserInput 方法。

另一种方法是检查每一帧是否按下了任何键,这也没关系。

您可以在这里找到一种方法:

How do I check if the user is pressing a key?

你应该做的事情取决于你的项目的大小。如果您将一直检查按键,那么为此构建某种事件系统可能是个好主意。

另一方面,我建议您使用 Unreal 或 Unity 等游戏引擎。如果您想继续使用 Java,有很多游戏库可以处理很多此类常见问题。

例如:

https://www.lwjgl.org/

您可以找到很多使用该库制作的回合制游戏教程。

祝你好运!

【讨论】:

  • 使用这样的循环,如果玩家不是AI,我将如何等待用户输入(按下按钮)?
  • 首先,你至少需要两个线程,一个会处理所有的图形,所以它一直在运行,另一个线程可以有你的实际游戏代码。因此,您可以在等待用户输入时冻结该线程,可能设置超时。在 java 中,您有一个扫描仪,它在控制台中等待用户输入,类似的工作。把它想象成一个回调。
  • 如果您创建自己的输入处理系统,您实际上不需要拥有多个线程。它只是轮询每帧所有键的状态并存储它(如果您需要实现诸如“OnButtonReleased”或“ButtonHeld”之类的东西,也可能存储以前的状态),然后您可以在游戏逻辑中访问它们。
  • 是的,没错,但现在每个人都拥有多核处理器,因此使用线程可能会更好。仅使用一个内核 100% 运行缓慢的游戏很常见。
  • @leoxs 我对 java 中的线程没有任何经验。你能写一个例子或指出正确的方向吗?
猜你喜欢
  • 2013-07-26
  • 2014-01-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多