Mittwoch, 6. Juli 2016

Creating a Mission System in Java


So... how do you create a Mission System?

I asked myself this question twice so far and recently I came up with the following approach. This system is relatively simple but you can do most of basic missions with it. It consists of a tree with depth 3: Mission, Objectives. Conditions. Each of the components has a mission state, consisting of PENDING, SUCCESS and FAILURE.

First, I will describe the classes needed, which is then followed by their java code at the end of this article.


The general structure is as follows:






Mission:
  • A Mission has a title, a debrief description and a short description. 
  • A Mission has multiple objectives.
  • As general rule, a mission is deemed a success if all its child objectives are deemed successful.

Objective:
  • An Objective has numerous conditions.
  • Objectives have a different behavior per type, described below:

    Default: All objectives must be set to success for a mission to return success.
    Failure: If this objective is failed, the entire mission fails.
    Optional: The objective is effectively ignored by the mission, but can be used to, say, determine a score.
     
  • By default, they are not being updated once a condition was met or failed, but you can change them to run permanent updates.
 
Conditions
  • All conditions must be met for an Objective to be successful. Behavior types (default, failure, optional) do not apply here to keep it simple. To make up for that, I've added the possibility to invert them.
  • Conditions are implemented by you via an interface and an abstract helper class. 
  • For GUI purposes, there's also methods to display the condition as string for the end user.

To use it, you setup your Mission, Objectives and Conditions and call Mission.update() each time you update your game.

Here's the code:



Mission


/**
 *
 * @author B5cully
 */
public class Mission {
  
    public String title;
    public String debrief;
    public String summary;   
    public LinkedList<Objective> objectives = new LinkedList();
    Objective.State state = Objective.State.PENDING;

    public Mission() {
    }
   
    public Mission(String title) {
        this.title = title;
    }

   
    public Objective.State getState() {
        return state;
    }
   
    public void addObjective(Objective obj) {
        objectives.add(obj);
    }
   
    public void start() {
        for( Objective objective : objectives ) {
            objective.start();
        }
    }
   
    public void stop() {
        for( Objective objective : objectives ) {
            objective.stop();
        }
    }

    /**
     * Updates this mission's state
     * @return
     */
    public void update() {
        int i=0;
        boolean success = true;
        boolean failure = false;
        for( Objective objective : objectives ) {
            switch (objective.state) {
                case SUCCESS:
                    if( objective.constant_check ) objective.checkState();
                    break;
                case FAILURE:
                    if( objective.constant_check ) objective.checkState();
                    break;
                case PENDING:
                    objective.checkState();
                    break;
            }
            switch( objective.type ) {
                case NORMAL: {
                    success = i == 0 ? objective.state.equals(Objective.State.SUCCESS) :
                                   success && objective.state.equals(Objective.State.SUCCESS);               
                } break;
                case OPTIONAL: {
                    //ignore the objective
                } break;
                case FAIL: {
                    //only consider fail state
                    failure = failure || objective.state.equals(Objective.State.FAILURE);                       
                } break;
                   
            }
            i++;
        }
        if( success ) {
            state = Objective.State.SUCCESS;
        }
        if( failure ) {
            state = Objective.State.FAILURE;
        }
    }





Objective

/**
 *
 * @author B5cully
 */
public class Objective {
  
    public enum Type{
        /**
         * The objective contributes to success of the mission.
         */
        NORMAL,
        /**
         * The objective counts as fail condition of the mission.+
         * If one objective of this type closes with failure,
         * the entire mission fails. The pending state of
         * FAIL objectives are ignored in the total outcome.
         */
        FAIL,
        /**
         * The objective is optional, it has no effect
         * on the total outcome.
         */
        OPTIONAL;
    }
    public enum State{
        PENDING, SUCCESS, FAILURE;
    }
  
    public String title;
    /**if this is true, the objective is constantly validated. If false,
       the objective is validated once and succeeds permanently once triggered.*/
    public boolean constant_check;
    /***/
    public Type type = Type.NORMAL;
    State state = State.PENDING;
    /**
     * All conditions must be met in order for na objective to
     * succeed.
     */
    LinkedList<ConditionImpl> conditions = new LinkedList<ConditionImpl>();

    public Objective() {
    }
  
    public Objective(String title) {
        this.title = title;
    }
  
    public void addCondition(ConditionImpl condition) {
        conditions.add(condition);
    }
  
    public void start() {
        for( ConditionImpl condition : conditions ) {
            condition.start();
        }
    }
  
    public void stop() {
        for( ConditionImpl condition : conditions ) {
            condition.stop();
        }
    }
  

    @Override
    public String toString() {
        String s = "";
        int i =0;
        for( ConditionImpl condition : conditions ) {
            s += condition.getName() + ": " + condition.getDisplayedText();
            if( i > 0 && i < conditions.size()) s += "\n";
            i++;
        }
        return s;
    }

    public State getState() {
        return state;
    }
  
    public void checkState() {
        boolean success = false;
        int i =0;

        //check the conditions
        for( ConditionImpl condition : conditions ) {
          
            state = condition.getState();
          
            if( !state.equals(State.PENDING) && constant_check) {
                //still update the condition if constant check enabled
                condition.checkState();
            } else
            if( state.equals(State.PENDING) ) {
                //pending: simply update. No updates if failed or succeeded.
                condition.checkState();
            }
            success = i == 0 ? state.equals(Objective.State.SUCCESS) :
                               success && state.equals(Objective.State.SUCCESS);
            switch( state ) {
                case FAILURE: {
                    this.state = State.FAILURE;
                    return;
                }
                default: break;
            }
            i++;
        }
        if( success) this.state = State.SUCCESS;
        else this.state = State.PENDING;
    }
}



Condition 


/**
 *
 * @author B5cully
 */
public interface ConditionImpl {
  

    /**
     * Invoked on condition start.
     */
    public void start();
   
    /**
     * Gets the name of this condition
     * @return
     */
    public String getName();
    /**
     * A Localized, properly formatted display
     * text for this condition.
     * @return
     */
    public String getDisplayedText();
   
    /**
     * @return the state of the condition
     */
    public Objective.State getState();
   
    /**
     * Evaluates the state of this condition. This normally involves
     * calculations.
     */
    public void checkState();
   
    /**
     * Invoked on condition stop (e.g. when
     * it has failed or is being reset)
     */
    public void stop();

}

Here's the helper class for Condition, followed by an example implementation. 


/**
 *
 * @author B5cully
 */
public abstract class Condition implements ConditionImpl {
  
    protected Objective.State state = Objective.State.PENDING;
    protected boolean inverted = false;
    /**
     * The name of this condition.
     */
    protected String name;
    /**
     * A format string to display the condition.
     */
    protected String format_string;
    /**
     * The string returned for display. This is usually
     * name + String.format(format_string, args), where
     * args is relevant info about the condition.
     */
    protected String displayed_text;

    public Condition(String name, String format_string) {
        this.name = name;
        this.format_string = format_string;
    }
   
    @Override
    public String getName() {
        return name;
    }
   
    /**
     * Inverts the result. E.g. Instead of delivering
     * SUCCESS by default, FAILED is being returned.
     * @param inverted
     */
    public void setInverted(boolean inverted) {
        this.inverted = inverted;
    }
   
    @Override
    public void start() {
    }

    @Override
    public Objective.State getState() {
        return state;
    }

    @Override
    public void stop() {
    }
}


/**
 *
 * @author B5cully
 */
public class ConditionKilledEnemies extends Condition{

    LinkedList<Entity> enemies = new LinkedList<Entity>();
    LinkedList<ListenerEntityImpl> listeners = new LinkedList<ListenerEntityImpl>();
    int size = 0;
   
    String current_displayed_text = "";
   
    {
        format_string = "%d/%d";
    }

    public ConditionKilledEnemies() {
        super("Killed", "%d/%d");
    }
   
    public void registerEnemy(Entity enemy) {
        //add enemy to list
        //add a entity listener that tracks the enemy death
        //and adds to the counter here
        ListenerEntityImpl listener = getListener(enemy);
        enemy.addEntityListener( listener);
        enemies.add(enemy);
        listeners.add(listener);
        size++;
        current_displayed_text = String.format(format_string, enemies.size(), size);
    }
   
    public ListenerEntityImpl getListener(Entity enemy) {
        return new ListenerEntityImpl() {

            @Override
            public void onDeath(EntityMobile object) {
                super.onDeath(object);
                int index = enemies.indexOf(object);
                if( index >= 0 ) {
                    enemies.remove(index);
                    listeners.remove(index);
                    current_displayed_text = String.format(format_string, enemies.size(), size);
                }
            }
        };
    }
   
    @Override
    public String getDisplayedText() {
        return current_displayed_text;
    }

    @Override
    public void checkState() {
        //success if the list is empty
        if( enemies.isEmpty() ) state = Objective.State.SUCCESS;
        else state = Objective.State.PENDING;
    }
   

Keine Kommentare:

Kommentar posten