Skip to content

The basics ‐ Phases

ImIllusion edited this page Oct 11, 2023 · 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.

Here is the main structure of a phase:

public interface GamePhase {

    void start(); // Start method
    
    default void tick() {} // Override if you want, this runs every tick
    
    void end(); // Called when the phase ends
    
    default void cancel() { // Called when the phase is cancelled
        end();
    }
    
}

But this is a bit too rudimentary, so let's make something that's tailored to minecraft minigames, implementation is up to you:

public abstract class AbstractGamePhase implements GamePhase {
    
    // Event logic methods
    
    public <EventType extends Event> void registerEvents(Class<EventType> eventClass, Consumer<EventType> handler) {
        // Up for you to implement
    }
    
    public void scheduleSyncTask(Consumer<BukkitTask> task, int delay) {
        
    }
    
    public void scheduleAsyncTask(Consumer<BukkitTask> task, int delay) {
    
    }
    
    public void scheduleRepeatingTask(Consumer<BukkitTask> task, int offset, int delay) {
    
    }
    
    // Event state notification methods
    
    public void onStart(Runnable task) {
    
    }
    
    public void onCancel(Runnable task) {
        // This might be called by external factors, make sure to not just set a new task, but to "add" to the old one
    }
    
    public void onEnd(Runnable task) {
        
    }
    
    // Event state methods
    
    public void dispose() {
        // This is where you cancel all the tasks and unregister your listeners
    }
    
    @Override
    public void end() {
        // Call the end task
        dispose();
    }
    
    @Override
    public void cancel() {
        // Call the cancel task
        dispose();
    }
    
}

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 class PhaseChain {

    private final List<AbstractGamePhase> chain = new LinkedList<>();
    private AbstractGamePhase currentPhase;

    public void addPhase(AbstractGamePhase phase) {
        chain.add(phase);
        phase.onEnd(() -> tryAdvance(phase)); // When the phase ends, we try advancing to the next one
        phase.onCancel(() -> tryRetreat(phase)); // When the phase is cancelled, we try going back to the previous one
    }
    
    public void tryAdvance(AbstractGamePhase previous) {
        if(currentPhase != previous) { // Prevents accidental recursion
            return;
        }
        
        GamePhase next = getNext(previous);
        
        if(next == null) { // End of the chain or the chain is broken somehow
            dispose();
        }
        
        setPhase(next);
    } 
    
    public void tryRetreat(AbstractGamePhase current) {
        if(currentPhase != current) {
            return;
        }
        
        GamePhase previous = getPrevious(current);
        
        if(previous == null) {
            current.dispose();
            current.start(); // We dispose and start again, which reboots the phase
            return;
        }
        
        setPhase(previous);
    }
    
    public void start() {
        setPhase(chain.get(0));
    }
    
    public void dispose() {
        for(AbstractGamePhase phase : chain) {
            phase.dispose();
        }
        
        chain.clear();
        currentPhase = null;
    }
    
    // internal methods
    private void setPhase(AbstractGamePhase current) {
        currentPhase = current;
        current.start();
    }
    
    private AbstractGamePhase getNext(AbstractGamePhase current) {
        if(current == null && !chain.isEmpty()) {
            return chain.get(0);
        }
    
        int index = chain.indexOf(current);
        
        if(index == -1 || index == chain.size() - 1) {
            return null; // If it's the last phase or if the phase isn't even in the chain anymore
        }
        
        return chain.get(index + 1);
    }
    
    private AbstractGamePhase getPrevious(AbstractGamePhase current) {
        int index = chain.indexOf(current);
        
        if(index < 1) { // If it's 0 or -1, meaning there's no previous or it's not even in the chain
            return null;
        }
        
        return chain.get(index - 1);
    }
}

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

Now that this is done, we can start by creating phases, but before we do that, let's write a basic Game class

public class Game {

    // It'd be nice to keep a reference of your plugin just in case
    
    private final PhaseChain phases = new PhaseChain();
    
    // Phase logic
    
    protected void addPhase(AbstractGamePhase phase) {
        phases.addPhase(phase);
    }
    
    protected void start() {
        phases.start();
    }
    
    protected void dispose() {
        phases.dispose();
    }
    
    // Player logic
    
    public Collection<? extends Player> getPlayers() {
        // Up to you
    }
    
    public void addPlayer(Player player) {
    
    }
    
    public void removePlayer(Player player) {
    
    }
    
    public boolean isPlayer(Player player) {
    
    }
    
}

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 WaitingForPlayersPhase extends AbstractGamePhase {

    private final Game game;
    private final int amount;
    
    public WaitingForPlayersPhase(Game game, int amount) {
        this.game = game;
        this.amount = amount;
    }
    
    @Override
    public void start() {
        registerEvents(PlayerJoinEvent.class, event -> {
            // It's ok to run this check on every player join event, ideally we'd have a GameAddPlayerEvent
            
            if(game.getPlayers().size() >= amount) {
                end();
            }
        });
    }
}

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

public class TNTTagGame extends Game {

    public TNTTagGame() {
        addPhase(new WaitingForPlayersPhase(this, 4)) // 4 players minimum
        // Add all the other phases
        
        start();
    }
}

Clone this wiki locally