Tutorial:Coordinating Coroutines: Difference between revisions

From PMDOWiki
IDK (talk | contribs)
first post, needs work
 
Imbion (talk | contribs)
No edit summary
 
(3 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{stub}}
{{Cleanup}}
In typical game logic, all functions occur in a sequence. In some cases, however, multiple actions must occur at the same time. Coroutines may be used for the purpose of this.


The flow of coroutine logic
== Flow of 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 logic in combat is as follows:
This is evident in combat:
* Player input is provided
 
* Player character makes action
Player input is provided
* Effects of action resolve one at a time
Player character makes action
* Player character's turn ends
Effects of action resolve one at a time
* Enemy AI decides its move
Player character's turn ends
* Enemy character makes action
Enemy AI decides its move
* Effects of action resolve one at a time
Enemy character makes action
* Enemy character's turn ends
Effects of action resolve one at a time
Enemy character's turn ends
etc.
etc.


However, movement must be handled such that all entities *appear* to move at the same time.
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 is accomplished by setting entities to do the following:
This means that any move schedule that 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.
* You move
Then, before anyone does an attack, or anyone moves again, the game waits for all animations to resolve.
* Enemy moves
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
* Enemy moves
You have to speed up all other entities to scale with you
* Enemy moves
Or just the ones that are sped up. Depends on how you want it
* Enemy attacks!
* Enemy moves
* Enemy moves
* Enemy moves
* Round OVER.


This means that any move schedule that goes
...will have you and first half of entities move all at once, have an enemy attack, then have the remaining enemies move all at once.
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:
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 76:
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 86:
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 111:
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 118:
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. You must do all of the former first, before doing any of the latter.


So, the procedure is to:
* Kick off all animations first
* Process all those animations in the next part of the code
* Complete the ones that have animated to completion
* On the next frame, the check for no animations has completed and another input can be taken


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
[[Category:Tutorial]]
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.

Latest revision as of 14:01, 26 October 2025

This article is in need of cleanup to increase its quality. You can help PMDO Wiki by improving it.

In typical game logic, all functions occur in a sequence. In some cases, however, multiple actions must occur at the same time. Coroutines may be used for the purpose of this.

Flow of Logic

The flow of logic in combat is as follows:

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

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 you and first half of entities move all at once, have an enemy attack, then have the remaining enemies move all 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.

So, the procedure is to:

  • Kick off all animations first
  • Process all those animations in the next part of the code
  • Complete the ones that have animated to completion
  • On the next frame, the check for no animations has completed and another input can be taken