Skip to content

The basics ‐ Phases

ImIllusion edited this page Mar 11, 2024 · 5 revisions

Before we write any minigame code, let's break down the concept of a minigame so we can develop a simple structure that'll end up benefiting us later on. The core principle of a minigame is a "phase". Think of phases as short temporary states that have their own tasks and event handlers associated, that run for a given amount of time.

Before we can write a phase, let's start by defining a couple interfaces that will allow us to re-use shared code between the phase and the game class, by implementing an interface:

public interface GameEventController {
    
    EventBus getEventBus();

    default <T extends Event> SubscribedListener subscribe(Class<T> eventClass, EventConsumer<T> listener) {
        return getEventBus().subscribe(eventClass, listener);
    }
}

Here is the main structure of a phase:

public abstract class AbstractPhase<T extends AbstractGame> implements GameEventController {

    protected final T game;
    private final EventBus eventBus; // Whatever else you need

    protected AbstractPhase(T game) {
        this.game = game;
        this.eventBus = ...; // Initialize a single event bus per phase
    }

    protected abstract void setup(); // This will be called from the public start method
    protected abstract void teardown(); 

    protected void registerListeners() {} // Optional override
    protected void registerTasks() {}

    public final void start() {
        this.setup();
        this.registerListeners();
        this.registerTasks();
    }

    public final void nextPhase() {
        this.dispose();
        this.game.advancePhase();
    }

    public final void previousPhase() {
        this.dispose();
        this.game.previousPhase();
    }

    public final void dispose() {
        this.teardown();
        this.eventBus.dispose();
        // Whatever else needs to be disposed
    }

    public T getGame() {
        return this.game;
    }

    protected MyPlugin getPlugin() {
        return this.game.getPlugin();
    }

    public World getWorld() {
        return ...
    }
}
    
}

Phase chaining: When dealing with multiple phases, it's important to follow a sequential order. So, we'll create a sort of "Phase chain", where we can link a phase order, so that when a phase ends, the next one starts:

public interface PhaseChain {

    void addPhase(AbstractPhase<?> phase);
    
    boolean nextPhase();
    void previousPhase();
    
    void start();
    void dispose();

    Collection<AbstractPhase<?>> getPhases();
}

With this approach, we have the following rules:

  • When a phase ends, the next one starts
  • When a phase cancels, the previous one starts. If there is no previous one, the current phase cancels and restarts
  • When all phases have finished, the phase chain disposes

Given these rules, let's create a simple "linear phase chain":

public class GamePhaseChain implements PhaseChain {

    private final List<AbstractPhase<?>> phases = new LinkedList<>();
    private int currentPhase = 0;

    @Override
    public void addPhase(AbstractPhase<?> phase) {
        phases.add(phase);
    }

    private void endPhase() {
        phases.get(currentPhase).teardown();
        currentPhase++;
    }

    private void startPhase() {
        AbstractPhase<?> phase = phases.get(currentPhase);
        phase.start();
    }

    @Override
    public boolean nextPhase() {
        if (isFinished()) {
            return false; // HUH?
        }

        endPhase();

        if (isFinished()) {
            return false;
        }

        startPhase();
        return true;
    }

    @Override
    public void previousPhase() {
        if (currentPhase == 0 || isFinished()) {
            return;
        }

        int nextPhase = currentPhase - 1;
        endPhase();
        currentPhase = nextPhase;
        startPhase();
    }

    @Override
    public void start() {
        startPhase();
    }

    @Override
    public void dispose() {
        for (AbstractPhase<?> phase : phases) {
            phase.dispose();
        }
    }

    @Override
    public Collection<AbstractPhase<?>> getPhases() {
        return List.copyOf(phases);
    }

    private boolean isFinished() {
        return currentPhase >= phases.size();
    }

}

Now that this is done, we can start by creating phases, but before we do that, let's write a basic Game class. This will be your entry point

public abstract class AbstractGame implements GameEventController {

    protected final MyPlugin plugin;
    
    private final UUID gameId = UUID.randomUUID(); // This is useful if you're doing cross-server communication or anything like that
    private final PhaseChain phaseChain;
    private final EventBus globalEventBus;
    // Whatever else

    protected AbstractGame(MyPlugin plugin) {
        this.plugin = plugin;
        this.phaseChain = new GamePhaseChain();
        // ...
    }

    public final void start() {
        this.setup();
        // Add any extra data here. I usually add a "kick every player" phase 
        this.phaseChain.start();
    }

    @Override
    public final void end() {
        this.teardown();

        for (AbstractPhase<?> phase : phaseChain.getPhases()) {
            phase.getEventBus().unregister();
        }

        this.phaseChain.dispose();
        this.globalEventBus.dispose();
        this.globalEventBus.unregister();
    }

    protected void setup() {
    }

    protected void teardown() {
    }

    protected <T extends AbstractPhase<?>> T registerPhase(T phase) {
        this.phaseChain.addPhase(phase);
        return phase;
    }

    public void advancePhase() {
        if (!this.phaseChain.nextPhase()) {
            this.end();
        }
    }

    public void previousPhase() {
        this.phaseChain.previousPhase();
    }

    public MyPlugin getPlugin() {
        return this.plugin;
    }

    // Add your getter / add/remove/isPlayer methods here
}

A simple minigame to code will be TNT Tag, which when broken down into states, looks like the following:

  • Wait for players

  • Countdown

  • Main "game loop" starts:

  • Every 15 seconds all "bombers" explode, and new bombers are picked at random

  • As a bomber, if you punch a "non-bomber", they become the bomber. The timer does not reset

  • Last player standing wins

Here's a sample version of the "Waiting for players phase":

public class WaitForPlayersPhase extends AbstractPhase<AbstractGame> {

    private final int requiredPlayers;

    public WaitForPlayersPhase(AbstractGame game, int requiredPlayers) {
        super(game);
        this.requiredPlayers = requiredPlayers;
    }

    @Override
    public void setup() {
        checkPlayers();
        subscribe(GamePlayerAddEvent.class, ignored -> checkPlayers());
    }

    @Override
    public void teardown() {

    }

    private void checkPlayers() {
        int playerCount = game.getPlayerTracker().getPlayerCount();

        if (playerCount >= requiredPlayers) {
            nextPhase();
        }
    }

}

And a sample game, skipping all the map logic for now

public class TNTTagGame extends AbstractGame {

    public TNTTagGame(MyPlugin plugin) {
        super(plugin);
    }

    @Override
    public void setup() {
       addPhase(new WaitingForPlayersPhase(this, 4)) // 4 players minimum
    }
}

Clone this wiki locally