5

Keeping Movement Code Clean in Alakaram

Editor’s Note: We’ve officially begun development of Alakaram, and it’s coming along well. Today, we have an article from our programmer and resident origami master Nicholas Maddalena on keeping our code clean. We hope it’ll be useful for people making all sorts of games. Enjoy!

As you may have heard, Dinofarm’s newest game Alakaram recently entered development. The game takes heavily after Auro, which means that moving and bumping actors is a core pillar of the game. I quickly arrived at some simple movement code that worked well enough:

// part of the player component. Will be fired when the player clicks anywhere on their turn.
void HandleClick(DirectionalClickInfo info) {
    if (Mover.IsMoveValid(info)) {
        // remove this method from the OnClick event, so that the player can't act anymore this turn.
        // HandleClick will be re-registered to OnClick by an external TurnOrderManager.
        clickHandler.OnClick -= HandleClick;
        // endTurn is a callback given to us by the TurnOrderManager.
        // the Move method will fire this callback when it finishes its animations.
        Mover.Move(info, endTurn);  
    }
}

This code was fine, but when I began to implement bumping, I began to notice a few flaws that I wanted to clean up. For a bumping system to work, we need a way to specify if a move is “forced.” This can change what the IsMoveValid function would consider to be true (for example, moving into a non-land tile is valid when you are bumped). It may also change some of the properties of how the Move function works (a bumped actor might slide along ice, while a deliberate move won’t). I decided to create a MovementFlagsclass to help with this:

public class MovementFlags {
    public bool IsForcedMovement;

    public MovementFlags(bool IsForcedMovement = false) {
        this.IsForcedMovement = IsForcedMovement;
    }
}

It doesn’t have much data in it now, but I expect this will be a useful tool once we start to add more movement types like “airborne,” “charging,” “stack pushed,” etc…
Now we can pass instances of this class to the IsMoveValid and Move function to alter their properties:

// part of a monster component.
// returns whether or not the bump was successful.
public bool GetBumped(DirectionalClickInfo info) {
    MovementFlags forcedMovement = new MovementFlags(IsForcedMovement: true);
    if (Mover.isMoveValid(info, forcedMovement) {
        Mover.Move(info, forcedMovement);
        return true;
    } else {
        return false;
    }
}

The code still works fine, but I notice two things I don’t love. The first is that this if/else structure feels extremely similar to the one in the player’s normal movement code. The second is that the GetBumped method needs to be responsible about passing its MovementFlags instance to both the IsMoveValid and Move method. Both of these scared me, because Alakaram will be a game with a lot of movement code in it. I don’t want to manually type out the same if/else statement every time if I don’t have to. I also don’t want to worry about remembering to pass my MovementFlags to both of those functions every time.

I took some inspiration from javascript’s promise syntax to refactor the movement code and eliminate those two problems. The most complex part of the new system is this MoveAttempt class:

public class MoveAttempt {
    System.Action onCompleteCallback;
    bool isSuccess;
    public bool IsSuccess { get { return isSuccess; } }

    // constructor
    public MoveAttempt(bool isSuccess) {
        this.isSuccess = isSuccess;
    }

    // note that each of the next three methods all return this instance.
    // this allows us to do some stylish function chaining, as we'll see in a moment.
    public MoveAttempt OnInvalid(System.Action callback) {
        if (isSuccess == false) {
            callback();
        }
        return this;
    }

    public MoveAttempt OnValid(System.Action callback) {
        if (isSuccess == true) {
            callback();
        }
        return this;
    }

    public MoveAttempt OnComplete(System.Action callback) {
        onCompleteCallback = callback;
        return this;
    }

    // this method will be called by the UnitMover when it finishes playing movement animations.
    public void Complete() {
        if (onCompleteCallback != null) {
            onCompleteCallback();
        }
    }

}

With that class in place, I was able to refactor the UnitMover component to offer a cleaner movement interface to other classes:

// note that this function is private now.
// other components shouldn't need to worry about this anymore, so we've hidden it from them.
private bool IsMoveValid(DirectionalClickInfo info, MovementFlags flags) { /* ... */ }

// Move has been replaced with TryMove, 
// which replicates the if/else statements from the previous version of this code.
public MoveAttempt TryMove(DirectionalClickInfo info, MovementFlags flags = null) {
    if (flags == null) {
        flags = new MovementFlags(); // just use default flags if none were specified
    }

    bool isMoveSuccessful = IsMoveValid(info, flags);
    MoveAttempt attempt = new MoveAttempt(isMoveSuccessful);
    
    if (isMoveSuccessful) {
        Vector3 destination = GetDestination(info, flags);
        // animate the character to its position using the excellent DOTween package
        // note that we call the MoveAttempt "complete" function once the animation is over.
        transform.DOMove(destination, .2f).OnComplete(() => {
            attempt.Complete();
        });
    }

    return attempt;
}

the TryMove function now serves as the only function other components really need to be worried about. It takes care of checking the validity of a move, and makes sure that any MovementFlags are properly given to anyone who needs them. Let’s see how this changes our GetBumped method from earlier:

public MoveAttempt GetBumped(DirectionalClickInfo info) {
MovementFlags forcedMovement = new MovementFlags(IsForcedMovement: true);
    return Mover.TryMove(info, forcedMovement);
}

Wow! The whole function is now just two simple lines of code! GetBumped returns a MoveAttempt back to whoever called it, so that that class can act on the information. Let’s have a look at how the Player component might use that information, as well as seeing how the Player component’s standard movement has changed:

void TryBump(DirectionalClickInfo info) {
    // note how we can call each function one right after the next.
    // this is because each method returns the same MoveAttempt instance,
    // allowing for even cleaner code 
    bumper.TryBump(Mover.Coordinates, info)
        .OnValid(() => {
            clickHandler.OnClick -= HandleClick;
        })
        .OnComplete(endTurn)
        .OnInvalid(InvalidShake);
}

void HandleClick(DirectionalClickInfo info) {
    // onValid will be called immediately if the move attempt was a success
    // onComplete will also be called if the move was a success,
    // but not until the end of the movement animation.
    // onInvalid will be called immediately if the move attempt was a failure.
    Mover.TryMove(info)
        .OnValid(() => {
            clickHandler.OnClick -= HandleClick;
        })
        .OnComplete(endTurn)
        .OnInvalid(() => {
            TryBump(info);
        });
}

Overall, I’m extremely pleased with how his implementation of movement turned out. I think it offers a clean solution for moving actors around the playing field, and an elegant set of tools for following up on those movements.

alakaramaurocodingdinofarmgamedevprogrammingunity

Nick Maddalena • 03/03/2019


Previous Post

Comments

  1. a 03/04/2019 - 5:27 am Reply

    Deregistering the click handler to deal with turn logic…

    How to spot a novice programmer.

    • Nick Maddalena 03/04/2019 - 8:13 am Reply

      Help me learn something new, then!
      Why is this a novice move, and what’s something you would do to improve it?

  2. KoryB 04/30/2019 - 7:03 am Reply

    I love it! Although I must say the name MoveAttempt seems a bit too specific given how configurable it is with the callback functions.

    Also re: deregistering the click handler, I feel like if they didn’t want that to be done often they wouldn’t have provided a succinct syntax for doing so.

  3. dinofarmgames.com 06/13/2019 - 5:45 pm Reply

    Look at nice cash prize for you. dinofarmgames.com
    http://bit.ly/2KwbwSy

  4. dinofarmgames.com 07/18/2019 - 12:38 am Reply

    Look at enjoyably mortgage up as a substitute for of you. dinofarmgames.com
    http://bit.ly/2NLAjWc

Leave a Reply

Your email address will not be published / Required fields are marked *