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 MovementFlags
class 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.
a 03/04/2019 - 5:27 am
Deregistering the click handler to deal with turn logic…
How to spot a novice programmer.
Nick Maddalena 03/04/2019 - 8:13 am
Help me learn something new, then!
Why is this a novice move, and what’s something you would do to improve it?
KoryB 04/30/2019 - 7:03 am
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.
dinofarmgames.com 06/13/2019 - 5:45 pm
Look at nice cash prize for you. dinofarmgames.com
http://bit.ly/2KwbwSy
dinofarmgames.com 07/18/2019 - 12:38 am
Look at enjoyably mortgage up as a substitute for of you. dinofarmgames.com
http://bit.ly/2NLAjWc
포커게임 07/03/2020 - 10:36 pm
I am now not positive where you are getting your info, however great topic. I must spend a while finding out more or working out more. Thanks for great information I used to be on the lookout for this information for my mission.
포커게임