Hello,
Although the title may be misleading, this tutorial is not about creating map chunks and managing those instances. This tutorial will be about how to create and manage areas, particularly for a single player. Although I can't say i've done a lot of production testing, a server is currently using this to manage a 'single boss instance' related piece of content and it is functioning properly. I am entirely open to any constructive criticism.
Code:
public enum InstancedType {
PSEUDO_BOSS_INSTANCE
}
Code:
public abstract class InstancedArea {
/**
* The type of instance, defining this area as unique to other different types.
*/
protected final InstancedType type;
/**
* The boundary or location for this instanced area
*/
protected final Boundary boundary;
/**
* The height of this area.
*/
protected final int height;
/**
* The state that this area is in, why by default is {@link InstancedAreaState#AVAILABLE}.
*/
private InstancedAreaState state = InstancedAreaState.AVAILABLE;
/**
* Creates a new area with a boundary for the given type.
*
* <p>If {@link InstancedAreaManager#getNextOpenHeight(InstancedType)} throws an
* {@link AreaUnavailableException} then the area should not be used. However, that
* is up to the implementor to decide.
*
* @param type
* the type of instanced area, defining this as unique.
* @param boundary
* the boundary or location.
*/
public InstancedArea(InstancedType type, Boundary boundary) throws AreaUnavailableException {
this.type = type;
this.boundary = boundary;
this.height = InstancedAreaManager.getSingleton().getNextOpenHeight(type);
}
/**
* Changes the current state of this area to a new state. If the parameter
* 'state' is null, a {@link NullPointerException} will be thrown and should
* be handled although this is not enforced. If the current state is that of
* {@link InstancedAreaState#DISPOSABLE} and we attempt to change it by
* referencing this function, an {@link IllegalStateException} will be thrown.
*
* @param state
* the new state of this area.
*
* @throws IllegalStateException
* thrown when the current state is
* {@link InstancedAreaState#DISPOSABLE}.
*/
public void setState(InstancedAreaState state) throws IllegalStateException {
Objects.requireNonNull(state);
if (this.state == InstancedAreaState.DISPOSED || this.state == InstancedAreaState.DISPOSABLE
&& state == InstancedAreaState.AVAILABLE) {
throw new IllegalStateException(
"The current state is '" + state.name() + "', the state cannot be changed.");
}
this.state = state;
}
/**
* Determines if this area is disposable. This method can, and most of the
* time, should be overriden for additional functionality. The
* {@link SingleInstancedArea} class for example that subclasses this class
* overrides the method to ensure that if the player is unavailable then the
* instance should be too. It is worth noting as a reminder that if you do
* override this method that you should refer to the superclass method.
*
* @return {@code true} if the area can be disposed of by the manager,
* otherweise {@code false}.
*/
public boolean isDisposable() {
if (state == InstancedAreaState.DISPOSABLE) {
return true;
}
return false;
}
/**
* Referenced when an area is created.
*/
public abstract void create();
/**
* Referenced when an area is to be disposed.
*/
public abstract void dispose();
/**
* Referenced every game cycle.
*/
public abstract void pulse();
/**
* A method that returns the current state of this area. By
* default the state is {@link InstancedAreaState#AVAILABLE}.
*
* @return the state of the area.
*/
public final InstancedAreaState getState() {
return state;
}
/**
* A convenience function that determines if the current state
* is that of {@link InstancedAreaState#DISPOSED}.
*
* @return {@code true} if disposed, otherwise {@code false}.
*/
public final boolean isDisposed() {
return state == InstancedAreaState.DISPOSED;
}
/**
* The type of instanced area that this is. Each instance is defined
* by a type that separates this instance from others, making it unique.
*
* @return the type of instance.
*/
public InstancedType getType() {
return type;
}
/**
* Determines the height of this area.
*
* @return the height
*/
public int getHeight() {
return height;
}
/**
* The boundary or location of this instanced area
* @return the boundary
*/
public Boundary getBoundary() {
return boundary;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof InstancedArea) {
InstancedArea other = (InstancedArea) obj;
return height == other.height && boundary.equals(other.boundary)
&& type == other.type;
}
return false;
}
@Override
public int hashCode() {
return 31 * height + boundary.hashCode() + type.hashCode();
}
}
Code:
public class InstancedAreaManager {
/**
* A single instance of this class for global usage
*/
private static final InstancedAreaManager SINGLETON = new InstancedAreaManager();
/**
* The minimum height that an instanced area can control.
*/
private static final int MINIMUM_HEIGHT = 4;
/**
* The maximum height of any one instance which is currently 1024.
*/
private static final int MAXIMUM_HEIGHT = 1024;
/**
* A mapping of all {@InstancedArea} objects that are being operated on
* and are active.
*/
private Map<InstancedType, Map<Integer, InstancedArea>> active = new HashMap<>();
/**
* The task that manages instanced areas.
*/
private final InstancedAreaTask task = new InstancedAreaTask();
/**
* A private empty {@link InstancedAreaManager} constructor exists to ensure that
* no other instance of this class can be created from outside this class.
*/
private InstancedAreaManager() {
for (InstancedType type : InstancedType.values()) {
active.put(type, new HashMap<>());
}
Server.getTaskScheduler().schedule(task);
}
/**
* Appends the {@link InstancedArea} object to the mapping for the given height.
*
* <p>It should be noted that if the given parameter reference is null, a {@link NullPointerException}
* will be thrown.</p>
*
* @param area
* the area that is being appended to the active list of instanced areas.
*/
public void append(InstancedArea area) {
Preconditions.checkNotNull(area, "Area isn't permitted to be null.");
Map<Integer, InstancedArea> areas = active.getOrDefault(area.getType(), new HashMap<>());
areas.put(area.getHeight(), area);
active.put(area.getType(), areas);
}
/**
* This method utilizes {@link #append(InstancedArea)} in conjunction with
* {@link InstancedArea#create()} to append and create a new instanced area.
*
* @param area
* the area being instanced.
*/
public void appendAndCreate(InstancedArea area) {
append(area);
area.create();
}
/**
* Retrieves an open height level by sifting through the mapping and
* attempting to retrive the lowest height level.
*
* @return the next lowest, open height level will be returned otherwise an
* exception is thrown. When an exception is thrown, it signifies that
* there are no heights open from 0 to {@link MAXIMUM_HEIGHT}.
*/
int getNextOpenHeight(InstancedType type) throws AreaUnavailableException {
Map<Integer, InstancedArea> areas = active.getOrDefault(type, new HashMap<>());
for (int height = MINIMUM_HEIGHT; height < MAXIMUM_HEIGHT; height += 4) {
if (!areas.containsKey(height)) {
return height;
}
}
throw new AreaUnavailableException();
}
/**
* Retrieves the single instance of this class.
*
* @return the single instance.
*/
public static InstancedAreaManager getSingleton() {
return SINGLETON;
}
/**
* An object that represents a task that will periodically (defined by {@link #EXECUTE_DELAY}) execute.
* The objective of the execute function is to dispose of unnecessary instances and pulse otherwise.
*/
private final class InstancedAreaTask extends Task {
/**
* The amount of game ticks that pass before {@link #execute()} is referenced.
*/
private static final int EXECUTE_DELAY = 2;
/**
* Creates a new listener for the manager.
*/
public InstancedAreaTask() {
super(EXECUTE_DELAY);
}
@Override
public void execute() {
for (Entry<InstancedType, Map<Integer, InstancedArea>> mapping : active.entrySet()) {
Map<Integer, InstancedArea> submap = mapping.getValue();
Iterator<InstancedArea> areas = submap.values().iterator();
while (areas.hasNext()) {
InstancedArea area = areas.next();
if (area.isDisposable()) {
area.setState(InstancedAreaState.DISPOSED);
area.dispose();
areas.remove();
} else {
area.pulse();
}
}
}
}
}
}
Code:
public abstract class SingleInstancedArea extends InstancedArea {
/**
* The player in this single instanced area.
*/
private Player player;
/**
* Creates a new single instanced area for a player.
*
* @param player
* the player in the instanced area.
* @param type
* the type of area.
* @param boundary
* the boundary of the instanced area.
*/
public SingleInstancedArea(Player player, InstancedType type, Boundary boundary)
throws AreaUnavailableException {
super(type, boundary);
this.player = player;
}
@Override
public abstract void create();
@Override
public abstract void dispose();
@Override
public abstract void pulse();
@Override
public boolean isDisposable() {
return super.isDisposable() || player == null || !player.isActive();
}
/**
* The player for this instanced area.
*
* @return the player.
*/
public Player getPlayer() {
return player;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SingleInstancedArea) {
SingleInstancedArea other = (SingleInstancedArea) obj;
return super.equals(obj) && other.player.equals(player);
}
return false;
}
@Override
public int hashCode() {
return 31 * super.hashCode() + player.hashCode();
}
}
Code:
public abstract class MultiPlayerInstancedArea extends InstancedArea {
/**
* A list of all players that are in the room.
*/
private final List<Player> players;
/**
* Creates a new instanced area with a given list of players.
*
* @param type
* the type of instanced area.
* @param boundary
* the boundary the area resides in.
* @param players
* the players that will occupy this instanced area.
* @throws AreaUnavailableException
* thrown when the area is unavailable, take height for example.
*/
public MultiPlayerInstancedArea(InstancedType type, Boundary boundary, List<Player> players)
throws AreaUnavailableException {
super(type, boundary);
this.players = players;
}
@Override
public abstract void create();
@Override
public abstract void dispose();
@Override
public abstract void pulse();
@Override
public boolean isDisposable() {
if (super.isDisposable()) {
return true;
}
for (Player player : players) {
if (player == null || !player.isActive()) {
return true;
}
}
return false;
}
}
Code:
public class InstancedAreaTest {
public static void main(String... args) {
/**
* An example of a player object.
*/
Player player = new Player("Jason");
/**
* The type of instance.
*/
InstancedType type = InstancedType.PSEUDO_BOSS_INSTANCE;
/**
* The boundary the instanced area should be tied to.
*/
Boundary boundary = new Boundary(3200, 3200, 3202, 3202);
try {
/**
* Create a new instance, however this instance is not registered yet.
*/
PseudoBossInstance instance = new PseudoBossInstance(player, type, boundary);
/**
* Sends the instance to the manager to start pulsing and wait for disposal.
* This method also references the create method of InstancedArea.
*/
InstancedAreaManager.getSingleton().appendAndCreate(instance);
} catch (AreaUnavailableException e) {
// send some message to the player stating the instance is unavailable.
}
}
private static final class PseudoBossInstance extends SingleInstancedArea {
public PseudoBossInstance(Player player, InstancedType type, Boundary boundary)
throws AreaUnavailableException {
super(player, type, boundary);
}
@Override
public void create() {
//TODO Create instanced area related content.
}
@Override
public void dispose() {
// TODO Dispose of instanced area related content.
}
@Override
public void pulse() {
// TODO Add content that requires constant referencing.
}
}
}