-
Notifications
You must be signed in to change notification settings - Fork 0
The basics ‐ Phases
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
}
}