Coordinating Coroutines
This article is a stub. You can help PMDO Wiki by expanding it.
The flow of coroutine logic
PMDO Uses coroutines to ensure that all actions occur in sequence: as far as game logic goes, one action must complete before the next action begins.
This is evident in combat:
- Player input is provided
- Player character makes action
- Effects of action resolve one at a time
- Player character's turn ends
- Enemy AI decides its move
- Enemy character makes action
- Effects of action resolve one at a time
- Enemy character's turn ends
etc.
However, movement must be handled such that all entities *appear* to move at the same time.
This is accomplished by setting entities to do the following:
- When you make a tile movement, the player and their allies and all enemies that intend to move in response are instantly moved as far as logic goes.
- Then, before anyone does an attack, or anyone moves again, the game waits for all animations to resolve.
- This, on its own, still isnt airtight, because if some enemies move at double speed, they will take twice as long to get to their destination and thus you would have to pause for them
- You have to speed up all other entities to scale with you
- Or just the ones that are sped up. Depends on how you want it
This means that any move schedule that goes
- You move
- Enemy moves
- Enemy moves
- Enemy moves
- Enemy attacks!
- Enemy moves
- Enemy moves
- Enemy moves
- Round OVER.
...will have the you +first half of entities move all at once, have an enemy attack, then have the remaining enemies move at once
This is what it looks like for a player, after taking some time deciding their move, inputs a movement command:
START FRAME Player idles Player character’s animation advances one frame END FRAME START FRAME Player idles Player character’s animation advances one frame END FRAME START FRAME Player chooses movement left Game executes movement and updates the character coordinates one tile left Game changes the player sprite animation to “move left” Control goes to the enemy Enemy decides to move right Game executes movement and updates the enemy character coordinates one tile right Game changes the player sprite animation to “move right” Control goes to enemy 2 ... Player character animation advances one frame, moving it one pixel left Enemy character animation advances one frame, moving one pixel right ... END FRAME START FRAME Player and enemy animations are checked. Theyre still moving. Do not ask for input. Player character animation advances one frame, moving it one pixel left Enemy character animation advances one frame, moving one pixel right ... END FRAME ... START FRAME Player and enemy animations are checked. Theyre still moving. Do not ask for input. Player character animation advances one frame, moving it one pixel left and reaches goal. Animation ends. Enemy character animation advances one frame, moving one pixel right, reaches goal. Animation ends. END FRAME Player and enemy animations are checked. Theyre no longer moving. Ask for input. The player still has the left button down? Game executes movement and updates the character coordinates one tile left etc.
Pseudocode
One master coroutine will be kicked off at the start of the game: this is where all game logic begins, and any subprocesses will be processed in this coroutine such that they resolve in order.
The master coroutine will have this loop:
Process Scene: GameManager.ScreenMainCoroutine() Process Entity Inputs: CurrentScene.ProcessInput() Wait until all animations are finished: yield return new WaitUntil(AnimationsOver) Process Player Input: yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(GameManager.Instance.InputManager)) Spin until player input is detected: yield return CoroutineManager.Instance.StartCoroutine(ProcessPlayerInput(action)) Perform action based on command: yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(action, CurrentCharacter, result)) If it's a combat action, wait until all animations are finished: yield return new WaitUntil(AnimationsOver) If it's a walk action, update the player coordinates immediately and start the character's walk animation: yield return CoroutineManager.Instance.StartCoroutine(character.StartAnim(walkAnim)) Process End Turn: CoroutineManager.Instance.StartCoroutine(FinishTurn(character, true, false, false)) If it's a blocking event, wait until all animations are finished: yield return new WaitUntil(AnimationsOver) Process AI Input: yield return CoroutineManager.Instance.StartCoroutine(ProcessAI()) AI is given game state and decides on command: CurrentCharacter.Tactic.GetAction(CurrentCharacter, DataManager.Instance.Save.Rand, false) Perform action based on command: yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(action, CurrentCharacter, result)) If it's a combat action, wait until all animations are finished: yield return new WaitUntil(AnimationsOver) If it's a walk action, update the player coordinates immediately and start the character's walk animation: yield return CoroutineManager.Instance.StartCoroutine(character.StartAnim(walkAnim)) Process End Turn: CoroutineManager.Instance.StartCoroutine(FinishTurn(character, true, false, false)) If it's a blocking event, wait until all animations are finished: yield return new WaitUntil(AnimationsOver) ...
When a walk animation is started, it will be NON BLOCKING to the main coroutine.
The update loop performs as such:
GameManager.Update() CoroutineManager.Instance.Update() GameManager.Update(FrameTick elapsedTime) CurrentScene.Update(elapsedTime) ZoneManager.Instance.CurrentMap.IterateCharacters() character.Update(elapsedTime)
The most important thing to prevent interruption is to have a separation of the code that decides and starts animations, against the code that processes and completes animations You must do all of the former first, before doing any of the latter Kick off all animations first, then process all those animations in the next part of the code, then complete the ones that have animated to completion, such that on the next frame you pass the check for not animating and can take in another input.