Project Longsword Dev Log #2 (Originally published on Wordpress)


In this week’s log I’ll mainly be going over the state system. I’ll go over some other minor things first, so skip ahead if you just want to read about how I implemented states.

I’ve been using the name ‘Longsword’ to refer to the game but it’s not the final title. It’s just a placeholder until I get closer to a more playable state and some sort of story. I’ve got a few names lined up and I’ve settled on a central theme. It’s quite cliché as stories go. I don’t want to get caught up in the bogs of storytelling before I get all the mechanics and design elements down.

I’ve improved the custom font I’ve been using by a lot. Most of the improvements came down to removing extra pixels and making sure all letters conform to the same character heights. Serifs are very deceptive in that the core of the letter may actually conform to the standards set by the rest of the font, but the serifs break the conformity when the letter is viewed as part of a greater whole. I’ve also made sure to repeat patterns across similar letters that makes the font more pleasant to read.

State System

The state system is one of the first things I added into the game as I knew it would be necessary for responsive inputs and clean code.

Before diving into what makes states tick, I’d like to talk about why I need states in the first place. Consider a move and an attack action without a state system. You might have something like this. (Please keep in mind that this isn’t actual code you would use in a game. This is just to illustrate the potential pitfalls of having to reference state information directly from other action classes.)

public class MoveAction : MonoBehaviour
{
    public bool is_moving {get; private set;}
    private AttackAction attack_action;
    private Rigidbody2D rb;
    private Vector2 move_direction;
    void Awake()
    {
        attack_action = GetComponent<AttackAction>();
        rb = GetComponent<Rigidbody2D>();
    }
    void FixedUpdate()
    {
        if(move_direction.sqrMagnitude > 0 && !attack_action.is_attacking)
        {
            //move
        }
        is_moving = rb.velocity.sqrMagnitude > 0.1;
    }
    public onMovePress(InputAction.CallbackContext context)
    {
        if(context.performed)
            move_direction = context.ReadValue<Vector2>();
    }
}
public class AttackAction : MonoBehaviour
{
    public bool is_attacking {get; private set;}
    private AttackProperties attack;
    public onAttackPress(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            is_attacking = true;
            StartCoroutine(Period(attack.period));
            // perform attack
        }
    }
    IEnumerator Period(float cd)
    {
        // cache your yielders instead of newing every time to reduce GC calls
        yield return Yielders.WaitForSeconds(cd);
        is_attacking = false;
    }
}

Note how we have to keep a reference to actions that prevent me from performing a particular action in each class just to keep track of their state. This means that the more actions I add, the more their classes will become dependent on each other. This isn’t something I want, as adding new actions means automatically increasing complexity in my current actions. By adding a system that keeps track of my current state, I can delegate transitions to that state system, have a single reference to the same state system in each action, specify how the action affects that state, and specify what transitions are legal. The object must also be able to perform multiple actions at the same time.

A good start would be to represent these actions in an Enum with the Flags attribute. This stores the values in a bit field, which lets me perform bitwise operations to modify the current state.

Each action has a number of valid transitions from other states. This set of transitions is also the set of possible states, so we can use an Enum with the Flags attribute to represent this as well.

Now we have to wrap these two Enums in a class that will perform all the relevant operations for us, i.e. entering and leaving a state, and checking whether we are in a particular state.

public class State
{
    private StateType valid_origin_states;
    private StateType current_states;
    public StateType GetCurrentStates()
    {
        return current_states;
    }
    public bool IsIn(State state)
    {
        return (current_states & state.current_states) != 0;
    }
    public bool HasFlag(StateType state_type)
    {
        return current_states.HasFlag(state_type);
    }
    public bool Enter(State next_state, Action act)
    {
        if (GameManager.is_paused || !CanTransition(next_state))
            return false;
        AddState(next_state);
        act();
        return true;
    }
    public void Leave(State old_state)
    {
        RemoveState(old_state);
    }
    private void AddState(State state)
    {
        current_states |= state.current_states;
    }
    private void RemoveState(State state)
    {
        current_states &= ~state.current_states;
    }
    private bool CanTransition(State state)
    {
        return (current_states & state.valid_origin_states) == current_states;
    }
}

Now we need a system that tracks the object’s current state and possible transitions. Actions will call this system in order to enter, leave, or check whether the object is in a particular state. I’ve attached an event listener that propagates the event throughout a single gameobject. This is invoked whenever the state changes:

public class StateSystem : MonoBehaviour, IEntitySystem
{
    private State state;
    public bool TryEnterState(State new_state, System.Action Act)
    {
        if (!state.Enter(new_state, Act))
            return false;
        // invoke state event
        return true;
    }
    public void LeaveState(State old_state)
    {
        state.Leave(old_state); // invoke state event
    }
    public bool IsInState(State state)
    {
        return this.state.IsIn(state);
    }
    public bool HasFlag(StateType state_type)
    {
        return state.HasFlag(state_type);
    }
}

I’ve gone back and forth over whether to let the state itself execute the next action or whether I should let the action itself do so after I’ve entered the next state. So far I’ve settled on letting the state call the Act() function so that I reduce repeated code in my Action classes.

Now in order to force actions to implement the required functions, I added an abstract GameAction class that performs some common initialisation and destruction. The important functions are the two functions TryAct() and Act(). TryAct() calls the relevant state system functions and performs any additional checks. Act() is the function passed to the state system for execution:

public abstract class GameAction : MonoBehaviour
{    
    [SerializeField]
    protected State state;
    protected Entity entity;
    protected StateSystem state_system;
    protected void Awake()
    {
        entity = GetComponentInParent<Entity>();
        state_system = entity.GetSystem<StateSystem>();
    }
    protected abstract bool TryAct();
    protected abstract void Act();
    void OnDestroy()
    {
        StopAllCoroutines();
    }
}

In the MoveAction class, the TryAct() function is implemented as such:

    //...    protected override bool TryAct()
    {
        if (move_direction.sqrMagnitude > 0)
        {
            return state_system.TryEnterState(state, Act);
        }
        else if (state_system.IsInState(state))
        {
            state_system.LeaveState(state);
        }
        return true;
    }    //...

I’ve added some editor code that allows me to set an action’s legal transitions and the added state through the unity inspector. Here’s what it looks like for the MoveAction:

Hopefully this was informative enough. If there are any errors or improvements you’d like to point out I’d love to know about them.

I’ve been working on a modifier system that will allow me to buff and debuff the player without modifying the scriptable objects containing the relevant data. It’s technically working but it’s not as polished as I’d like it to be. There are some cases that I want to add support for, which may or may not fit into the same system or some other system.

If you’d like to follow the game’s progress, I post almost weekly videos to my progress log on youtube. I also share my updates on twitter @B_Bugeja.

Here’s a link to my latest video.

Get Revenge At Last

Leave a comment

Log in with itch.io to leave a comment.