Coordinating Coroutines: Difference between revisions

From PMDOWiki
IDK (talk | contribs)
first post, needs work
 
IDK (talk | contribs)
formatting
 
Line 1: Line 1:
{{stub}}
{{stub}}


The flow of coroutine logic


The turn system is responsible for ensuring that all actions occur in sequence: as far as game logic goes, one action must complete before the next action begins.
 
== 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:
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.


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.
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:
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.
* 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.
* 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
* 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
* 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
* Or just the ones that are sped up.  Depends on how you want it


This means that any move schedule that goes
This means that any move schedule that goes
You move
* You move
Enemy moves
* Enemy moves
Enemy moves
* Enemy moves
Enemy moves
* Enemy moves
Enemy attacks!
* Enemy attacks!
Enemy moves
* Enemy moves
Enemy moves
* Enemy moves
Enemy moves
* Enemy moves
Round OVER.
* 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
...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:
This is what it looks like for a player, after taking some time deciding their move, inputs a movement command:
<pre>
START FRAME
START FRAME
Player idles
Player idles
Line 78: Line 83:
Theyre no longer moving.
Theyre no longer moving.
Ask for input.  The player still has the left button down?
Ask for input.  The player still has the left button down?
Game executes movement and updates the character coordinates one tile left-
Game executes movement and updates the character coordinates one tile left
etc.
</pre>


== 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.
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.
Line 85: Line 93:
The master coroutine will have this loop:
The master coroutine will have this loop:


Process Scene GameManager.ScreenMainCoroutine()
<pre>
Process Entity Inputs CurrentScene.ProcessInput()
Process Scene: GameManager.ScreenMainCoroutine()
Wait until all animations are finished yield return new WaitUntil(AnimationsOver)
Process Entity Inputs: CurrentScene.ProcessInput()
Process Player Input yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(GameManager.Instance.InputManager))
Wait until all animations are finished: yield return new WaitUntil(AnimationsOver)
Spin until player input is detected yield return CoroutineManager.Instance.StartCoroutine(ProcessPlayerInput(action))
Process Player Input: yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(GameManager.Instance.InputManager))
Perform action based on command yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(action, CurrentCharacter, result))
Spin until player input is detected: yield return CoroutineManager.Instance.StartCoroutine(ProcessPlayerInput(action))
If it's a combat action, wait until all animations are finished yield return new WaitUntil(AnimationsOver)
Perform action based on command: yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(action, CurrentCharacter, result))
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))
If it's a combat action, wait until all animations are finished: yield return new WaitUntil(AnimationsOver)
Process End Turn CoroutineManager.Instance.StartCoroutine(FinishTurn(character, true, false, false))
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))
If it's a blocking event, wait until all animations are finished yield return new WaitUntil(AnimationsOver)
Process End Turn: CoroutineManager.Instance.StartCoroutine(FinishTurn(character, true, false, false))
Process AI Input yield return CoroutineManager.Instance.StartCoroutine(ProcessAI())
If it's a blocking event, wait until all animations are finished: yield return new WaitUntil(AnimationsOver)
AI is given game state and decides on command CurrentCharacter.Tactic.GetAction(CurrentCharacter, DataManager.Instance.Save.Rand, false)
Process AI Input: yield return CoroutineManager.Instance.StartCoroutine(ProcessAI())
Perform action based on command yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(action, CurrentCharacter, result))
AI is given game state and decides on command: CurrentCharacter.Tactic.GetAction(CurrentCharacter, DataManager.Instance.Save.Rand, false)
If it's a combat action, wait until all animations are finished yield return new WaitUntil(AnimationsOver)
Perform action based on command: yield return CoroutineManager.Instance.StartCoroutine(ProcessInput(action, CurrentCharacter, result))
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))
If it's a combat action, wait until all animations are finished: yield return new WaitUntil(AnimationsOver)
Process End Turn CoroutineManager.Instance.StartCoroutine(FinishTurn(character, true, false, false))
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))
If it's a blocking event, wait until all animations are finished yield return new WaitUntil(AnimationsOver)
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)
...
...
</pre>


When a walk animation is started, it will be NON BLOCKING to the main coroutine.
When a walk animation is started, it will be NON BLOCKING to the main coroutine.
Line 108: Line 118:
The update loop performs as such:
The update loop performs as such:


<pre>
GameManager.Update()
GameManager.Update()
CoroutineManager.Instance.Update()
CoroutineManager.Instance.Update()
Line 114: Line 125:
ZoneManager.Instance.CurrentMap.IterateCharacters()
ZoneManager.Instance.CurrentMap.IterateCharacters()
character.Update(elapsedTime)
character.Update(elapsedTime)
 
</pre>


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
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
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.
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.

Latest revision as of 17:56, 8 November 2024

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.