Skip to content

Example Game

Exploring the example game is the best way to get started with BlueDragon software. This guide will break down the main class and the necessary configuration file.

Configuration File

# The game.properties file defines the game's main class (which extends com.bluedragonmc.server.Game)
# It is used by the plugin loader to determine which class to instantiate when a game is requested.
# The name is also used to populate menus and command completion.
name=ExampleGame
main-class=com.bluedragonmc.games.examplegame.ExampleGame

This file must be named game.properties and be placed in the root of the compiled JAR file. Only one game can be registered per JAR. If you wish to combine multiple games into one project, we recommend using Gradle subprojects.

Anatomy of the Main Class

There are only two requirements for a game’s main class:

  • It must inherit com.bluedragonmc.server.Game
  • It must override the initialize method

Here is an example main class from our ExampleGame project:

class ExampleGame(mapName: String) : Game(name = "ExampleGame", mapName = mapName) {
override fun initialize() {
use(VoidDeathModule(threshold = 0.0))
use(CountdownModule(threshold = 2, countdownSeconds = 5))
use(MyModule()) { myModuleInstance ->
myModuleInstance.sayHello()
}
use(WinModule())
use(AnvilFileMapProviderModule(Paths.get("worlds/$name/$mapName")))
use(SharedInstanceModule())
handleEvent<PlayerChatEvent> { event ->
event.player.exp += 10
}
}
@DependsOn(WinModule::class)
class MyModule : GameModule() {
override fun initialize(parent: Game, eventNode: EventNode<Event>) {
eventNode.addListener(PlayerSpawnEvent::class.java) { event ->
event.player.sendMessage(Component.text("Hello, world!", NamedTextColor.AQUA))
}
eventNode.addListener(PlayerStartSneakingEvent::class.java) { event ->
parent.getModule<WinModule>().declareWinner(event.player)
}
}
fun sayHello() {
logger.info("Hello, world!")
}
override fun deinitialize() { }
}
}

Let’s break this down!

The class declaration

class ExampleGame(mapName: String)
: Game(name = "ExampleGame", mapName = mapName) {

Every game needs to extend BlueDragon’s Game class. When overriding the constructor, we recommend keeping the game name (the name constructor parameter) constant.

Your constructor is called in different ways depending on the number of parameters it accepts:

# of Parameters AcceptedRoles of Constructor Parameters
0Called with no parameters.
1Map name
2Map name, Game mode

This logic can be found here.

The game mode can be any arbitrary string. The Game implementation can use the game mode string to change various game mechanics.

The initialize method

override fun initialize() {
use(VoidDeathModule(threshold = 0.0))
use(CountdownModule(threshold = 2, countdownSeconds = 5))
...
}

The initialize method is the time to use game modules. In this example, we’re using the VoidDeathModule and the CountdownModule.

Each module should have a single purpose and ideally shouldn’t contain much code. They are designed to be easy to use in many games without much adjustment.

The module registration callback

use(MyModule()) { myModuleInstance ->
myModuleInstance.sayHello()
}

If a module has dependencies, calling use may not initialize it right away. Trying to get an instance of a module immediately after usage is not considered safe, since the module may be waiting for its dependencies to load (and therefore hasn’t registered itself yet).

Instead, use the callback option in the second parameter of the use method. When the module is registered, the instance of the module will be passed to the callback.

You can register additional modules in this callback as well, and callbacks can be nested multiple times if necessary.

Loading maps

use(AnvilFileMapProviderModule(Paths.get("worlds/$name/$mapName")))
use(SharedInstanceModule())

Loading maps, just like everything else, is delegated to a module.

Creating a game module

@DependsOn(WinModule::class)
class MyModule : GameModule() {
override fun initialize(parent: Game, eventNode: EventNode<Event>) {
eventNode.addListener(PlayerSpawnEvent::class.java) { event ->
event.player.sendMessage(Component.text("Hello, world!", NamedTextColor.AQUA))
}
eventNode.addListener(PlayerStartSneakingEvent::class.java) { event ->
parent.getModule<WinModule>().declareWinner(event.player)
}
}
override fun deinitialize() { }
}

You can create your own modules by extending the GameModule class and overriding the initialize and deinitialize methods.

Module Dependencies

Dependencies are specified with the DependsOn and SoftDependsOn annotations. If a module lists a dependency, that dependency will be registered before the dependent module is registered.

However, for soft dependencies, if the dependency is not registered by the time initialize is fully invoked, the module will be registered instead of throwing an exception.

Scoped Event Nodes

Every module gets its own EventNode, which is scoped to the game. All events that pass through this EventNode must be related to a player in the game, an instance that this game owns, or the game itself.

The only exception to this is the ServerTickMonitorEvent. See the source code for details.

Unregistering Modules

When a module is unregistered (typically after the game ends), each of its modules get unregistered. At this point, the module’s deinitialize method will be invoked. This is one final time to do any cleanup, like closing file handles or database connections or reverting any non-scoped changes.

Further Reading