-
Notifications
You must be signed in to change notification settings - Fork 1
#006 Actions
What may be evident at this stage of development is that the code of a game can become very messy as features are added. In order to keep the code of our Map, Entity, and other classes from becoming multi-thousand line classes with dozens of functions, we're going to implement an Action System.
Imagine that we have actions, where each action has a cause and effect. If the user presses an arrow key, then we create an action to move the player entity in the appropriate direction. If, when the move action is performed, the player entity collides with an enemy entity, then an attack action is created and performed.
Each map will have a set of entities present on the map and each entity will have a set of actions to perform. When the map is updated, then all of the entities are updated and all of the actions are performed in the order that they have been added to their entity.
You may now be thinking of a situation similar to the following, which would break the action system. The player has an action to attack an enemy and the enemy has an action to move away from the player. There are two possible ways to run this situation, either the player attacks the enemy before it moves or the enemy moves before the player can attack. We prevent this issue by first performing the player's actions, then we iterate over each entity and create/perform their actions as they are being updated. By doing this, there will never be a situation where the actions of two different entities are run in an incorrect order.
With this slightly long explanation in-mind, we can define the Action class as follows:
- List of runnable functions.
- Each of these functions is run when the action is performed.
- E.g. A lambda function such as () -> System.out.println("Hello World!") is a lambda function that can be run when the action is performed.
- A function to perform the action.
- A function to add a runnable function to the list.
- A function to remove a runnable function from the list.
- A function to remove all runnable functions from the list.
Because the Action System is meant to handle movement, we'll also need to define a MoveAction class:
- The original position of the entity.
- The new position of the entity.
- A function to perform the action.
- The MoveAction will have to run code unique to its purpose, so we have to override the peform function of the Action class in order to run our new code.
To the Map class, we need to add a function to check if a position is free as well as a list of entities present on the map. In the Entity class, we'll add a list of actions to perform, a function to add actions to the list, and we will update the functions that change the entity's position to use the MoveAction class.
package com.valkryst.VTerminal_Tutorial.action;
import com.valkryst.VTerminal_Tutorial.Map;
import com.valkryst.VTerminal_Tutorial.entity.Entity;
import java.util.ArrayList;
import java.util.List;
public class Action {
/** The runnable functions to run when the action is performed. */
private final List<Runnable> runnables = new ArrayList<>();
/**
* Performs the action and runs all of the runnable functions.
*
* Does nothing if the map or entity are null.
*
* @param map
* The map.
*
* @param entity
* The entity performing the action.
*/
public void perform(final Map map, final Entity entity) {
if (map == null || entity == null) {
return;
}
runnables.forEach(Runnable::run);
}
/**
* Adds a runnable function to the action.
*
* @param runnable
* The runnable function.
*/
public void addRunnable(final Runnable runnable) {
if (runnable == null) {
return;
}
runnables.add(runnable);
}
/**
* Removes a runnable function from the action.
*
* @param runnable
* The runnable function.
*/
public void removeRunnable(final Runnable runnable) {
if (runnable == null) {
return;
}
runnables.remove(runnable);
}
/** Removes all runnable functions from the action. */
public void removeAllRunnables() {
runnables.clear();
}
}
package com.valkryst.VTerminal_Tutorial.action;
import com.valkryst.VTerminal_Tutorial.Map;
import com.valkryst.VTerminal_Tutorial.entity.Entity;
import java.awt.*;
public class MoveAction extends Action {
/** The original position of the entity being moved. */
private final Point originalPosition;
/** The position being moved to. */
private final Point newPosition;
/**
* Constructs a new MoveAction.
*
* @param position
* The current position of the entity to move.
*
* @param dx
* The change to apply to the x-axis position.
*
* @param dy
* The change to apply to the y-axis position.
*/
public MoveAction(final Point position, final int dx, final int dy) {
originalPosition = position;
newPosition = new Point(dx + position.x, dy + position.y);
}
@Override
public void perform(final Map map, final Entity entity) {
if (map == null || entity == null) {
return;
}
if (map.isPositionFree(newPosition)) {
super.perform(map, entity);
entity.setPosition(newPosition);
}
}
}
You can view the entire updated class here. The following code sections are only the new, or altered, portions of the code.
/** The entities. */
@Getter private List<Entity> entities = new ArrayList<>();
/**
* Determines if a position on the map is free.
*
* A position is free if there is no entity at the position and if the tile at the position is not solid.
*
* @param position
* The position.
*
* @return
* Whether the position is free.
*/
public boolean isPositionFree(final Point position) {
if (position == null) {
return false;
}
// Ensure position isn't outside the bounds of the map.
if (position.x < 0 || position.y < 0) {
return false;
}
if (position.x >= getMapWidth() || position.y >= getMapHeight()) {
return false;
}
// Check for entities at the position.
for (final Entity entity : entities) {
if (entity.getPosition().equals(position)) {
return false;
}
}
// Check if the tile at the position is solid.
if (mapTiles[position.y][position.x].isSolid()) {
return false;
}
return true;
}
You can view the entire updated class here. The following code sections are only the new, or altered, portions of the code.
/** The actions to perform. */
private final Queue<Action> actions = new ConcurrentLinkedQueue<>();
/**
* Adds an action to the entity.
*
* @param action
* The action.
*/
public void addAction(final Action action) {
if (action == null) {
return;
}
actions.add(action);
}
/**
* Adds a move action to the entity, to move it to a new position relative to it's current position.
*
* @param dx
* The change in x-axis position.
*
* @param dy
* The change in y-axis position.
*/
public void move(final int dx, final int dy) {
actions.add(new MoveAction(this.getPosition(), dx, dy));
}
/**
* Adds a move action to the entity, to move it to a new position.
*
* Ignores null and negative positions.
*
* @param position
* The new position.
*/
public void setPosition(final Point position) {
if (position == null || position.x < 0 || position.y < 0) {
return;
}
final Point currentPosition = this.getPosition();
final int xDifference = position.x - currentPosition.x;
final int yDifference = position.y - currentPosition.y;
actions.add(new MoveAction(currentPosition, xDifference, yDifference));
}