如何以更少的代码重复将关卡添加到我的游戏中?

How can I add Levels to my game with less duplication of code?

我正在设计一个多层次的游戏。我有一个设置 class,它根据接收到的参数设置电路板,指示应该设置哪个级别。这是 class:

public class BoardState {
    public BoardState(InitialState state) {
        switch (state) {
            case EMPTY:
                setupEmptyState();
                break;
            case INTEGRATIONTEST:
                setupIntegrationTestState();
                break;
            case LEVEL_1:
                setupLevelOne();
                break;
            case LEVEL_2:
                setupLevelTwo();
                break;
            default:
                throw new Error("Invalid level selection");
        }
    }

    private void setupEmptyState() { }

    private void setupIntegrationTestState() { }

    private void setupLevelOne() { }

    private void setupLevelTwo() { }
}

这很好用,但每次我添加一个新级别时,我都必须在三个地方添加代码:InitialState enum 定义接受状态列表,switch 构造函数中的语句,以及 class 的主体,我必须在其中添加一个方法来设置相关级别。

我想保留的一件好事是,我的 GUI 会根据 enum 定义级别列表,为我添加的每个级别自动填充一个新按钮。

如何重构此代码以减少与添加新关卡相关的开销?

你可以使用继承,多态性是这里的关键词。

将您的 InitialState class 设置为抽象基础 class(如果没有公共字段,则为接口)并定义一个方法 public abstract void setup();.

abstract class InitialState {
    public abstract void setup();
}

然后,对于每个原始开关案例,从基础 class 派生一个 class,例如 LevelOne,并通过覆盖 setup() 来实现其特定.

class LevelOne extends InitialState {
    @Override
    public void setup() {
        // The code from "setupLevelOne()" goes here
    }
}

您的 BoardState class 减少为:

public class BoardState {
    public BoardState(InitialState state) {
        // At runtime, the right method of the actual
        // state type will be called dynamically
        state.setup();
    }
}

但是,如果您需要设置 BoardState class 的内部状态,请考虑将设置方法定义为 public abstract void setup(BoardState boardState),这样您就可以访问它的 getter 和setter 方法。

这种方法还可以促进代码的重用,因为您可以为不同类型的关卡添加多个抽象层。

通常当您需要减少代码重复时,就会出现一个接口。这次(根据您在 OP 中的评论)看来您需要根据您所在的级别向面板添加不同的对象:

import java.util.List;

public interface LevelSettings {
    List<GameObject> startingObjects();
}

现在,BoardState 看起来像那样(不再有 setupX() 方法)

import java.util.List;

public class BoardState {
    private final List<GameObject> gameObjects;

    public BoardState(LevelSettings settings) {
        this.gameObjects = settings.startingObjects();
    }
}

由于您还指定了 enum 在 GUI 上动态创建按钮对您来说很好,因此可以通过在枚举中实现接口来结合两全其美(界面和枚举) ...

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public enum InitialState implements LevelSettings {
    EMPTY {
        @Override
        public List<GameObject> startingObjects() {
            return Collections.emptyList();
        }
    },
    INTEGRATIONTEST {
        @Override
        public List<GameObject> startingObjects() {
            GameObject g1 = new GameObject("dummy 1");
            GameObject g2 = new GameObject("dummy 2");
            return Arrays.asList(g1, g2);
        }
    },
    LEVEL_1 {
        @Override
        public List<GameObject> startingObjects() {
            //read a config file to get the starting objects informations
            //or also hardcoded (not preferred)
        }
    },
    LEVEL_2 {
        @Override
        public List<GameObject> startingObjects() {
            //read a config file to get the starting objects
            //or also hardcoded (not preferred)
        }
    };
}

基本上就是这样。如果您需要添加 LEVEL_3,请在 InitialState 中添加,一切都会随之而来。

更进一步

从这里开始超出您的要求,如果您不相信,请忽略这部分。

作为一个好习惯,我会仅将这些配置存储在配置文件中,以进一步减少代码重复并获得灵活性:

import java.util.List;

public enum InitialState implements LevelSettings {
    EMPTY {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("empty.level");
        }
    },
    INTEGRATIONTEST {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("integration_test.level");
        }
    },
    LEVEL_1 {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("1.level");
        }
    },
    LEVEL_2 {
        @Override
        public List<GameObject> startingObjects() {
            return readFromFile("2.level");
        }
    };

    private static List<GameObject> readFromFile(String filename) {
        //Open file
        //Serialize its content in GameObjects
        //return them as a list
    }
}

这样当您决定添加一个新关卡时,您实际上只需要知道存储该关卡配置的文件名。

更进一步

你会看到的东西真的很棘手,我不建议你在生产代码中使用它(但它减少了代码重复)!

import java.util.List;

public enum InitialState implements LevelSettings {
    EMPTY, INTEGRATIONTEST, LEVEL_1, LEVEL_2;

    @Override
    public List<GameObject> startingObjects() {
        return readFromFile(this.name() + ".level");
    }

    private static List<GameObject> readFromFile(String filename) {
        //Open file
        //Serialize its content in GameObjects
        //return them as a list
    }
}

这里我们依靠枚举名称本身来找到对应的正确文件。此代码之所以有效,是因为它基于 约定 ,文件根据枚举名称命名,扩展名为“.level”。当您需要添加新级别时,只需将其添加到枚举中即可...

好吧,您可以将所有工作重构为一个方法。 说它需要一个 int 作为级别的 ID,同时它加载一个包含每个级别的结构化信息的 JSON 文件,并创建给定的级别。 例如:

"levels" : [
  "level" : {
      "id" : "001",
      "size" : "200",
      "difficulty" : "2"
  },
  "level" : {
      "id" : "002",
      "size" : "300",
      "difficulty" : "3"
  }
]

然后,在您的代码中:

public void setupLevel(int id) throws levelNotFoundException{
    //somehow like this
    Document doc = parse("levels.json");
    for(element elm: doc.get("levels")){
        if(Integer.parseInt(elm.get("id")).equals(id)){
            //setup your level
        }
    }
}

然后在某个地方调用您的方法:

int levelId = getNextLevel();
try{
setupLevel(levelId);
} catch (LevelNotFoundException e){e.printStackTrace();}

或者你可以使用XML,或者简单地硬编码,并将所有级别存储在一个数组中