Difference between revisions of "Gameplay synchronization"

From MSX Game Library

Line 25: Line 25:
 
So if your gameplay loop is not synchronized, the time the CPU needs to execute it will change the movement speed. And it's where code branching (‘if’ statement) makes it even worse because code execution time becomes not constant (for example a part of the code may be executed only if a collision is detected).
 
So if your gameplay loop is not synchronized, the time the CPU needs to execute it will change the movement speed. And it's where code branching (‘if’ statement) makes it even worse because code execution time becomes not constant (for example a part of the code may be executed only if a collision is detected).
  
Synchronizing your code ensures your gameplay loop always executes at the same given speed. <sup>[#Notes|#1]</sup>
+
Synchronizing your code ensures your gameplay loop always executes at the same given speed. <sup>[[#Notes|#1]]</sup>
  
 
== How do I synchronize? ==
 
== How do I synchronize? ==
Line 181: Line 181:
 
Other solutions for managing the difference between 50 Hz and 60 Hz machines:
 
Other solutions for managing the difference between 50 Hz and 60 Hz machines:
 
* Do the opposite of the previous solution: tune your game for 60 Hz and, on a 50 Hz machine, run the gameplay loop twice every 5 frames. The big problem with this method is that the execution time of 2 frames may greatly exceed the time allocated between two v-blank signals.
 
* Do the opposite of the previous solution: tune your game for 60 Hz and, on a 50 Hz machine, run the gameplay loop twice every 5 frames. The big problem with this method is that the execution time of 2 frames may greatly exceed the time allocated between two v-blank signals.
* Have 2 sets of movement quantities, one for 50 Hz and one for 60 Hz. This is possible with fixed-point numbers (the movement quantities must be 1.2 times greater in 50 Hz to compensate for the frequency difference). You also need to provide two sets of music and sound effects, or have a format that can adapt (like VGM format). <sup>[#Notes|#2]</sup>
+
* Have 2 sets of movement quantities, one for 50 Hz and one for 60 Hz. This is possible with fixed-point numbers (the movement quantities must be 1.2 times greater in 50 Hz to compensate for the frequency difference). You also need to provide two sets of music and sound effects, or have a format that can adapt (like VGM format). <sup>[[#Notes|#2]]</sup>
  
 
== Notes ==
 
== Notes ==

Revision as of 23:28, 16 October 2025

Synchronization is the process that allows a code loop to run at a constant intended speed.

Why should I synchronize?

Let's say you want a monster to move from left to right of the screen. You create a loop where you increment a X coordinate and set a sprite position accordingly. If you test your program, the monster will move at a constant speed… So why the hell should I bother with synchronization?

‎‎
void main()
{
	/* Screen and sprites initialization... */

	u8 mPosX = 100; // Monster's position X coordinate
	while (1) // Gameplay loop
	{
		mPosX++;
		VDP_SetSpritePositionX(0, mPosX);
	}
}

Let's continue the example: Now you add a simple code to handle player movement (with some input reading and basic physics and collision). You launch your program again and the first thing you notice is the monster moves slower than before. And when you start moving your character it's even worse: Not only does the monster move even slower, but their speed is no longer constant.

Why?

The speed is a quantity of movement over time. So, for a constant quantity of movement (eg. +1 pixel to the right) the longer the time, the slower the speed.

So if your gameplay loop is not synchronized, the time the CPU needs to execute it will change the movement speed. And it's where code branching (‘if’ statement) makes it even worse because code execution time becomes not constant (for example a part of the code may be executed only if a collision is detected).

Synchronizing your code ensures your gameplay loop always executes at the same given speed. #1

How do I synchronize?

Synchronization simply consists of waiting for a signal sent at a constant frequency before executing an iteration of the gameplay loop. The MSX has a system (interruption) that allows a peripheral device to send signals to the CPU, which the program can then receive.

The most commonly used signal for gameplay synchronization is the one sent by the graphics processor (VDP) at the end of displaying an image on the screen: the "v-blank" (start of the vertical blanking interval). This signal is sent every 1/60th of a second (for NTSC machines at 60 Hz) or every 1/50th of a second for PAL/SECAM machines at 50 Hz.

If your gameplay loop waits for the v-blank signal, your code will always be executed at the same time interval regardless of its actual duration. This means that all movements you make will result in a constant speed.

There are several ways to synchronize your gameplay.

Halt

The Z80 (MSX's CPU) has a special instruction that halts the process of the program until an interruption signal occurs. As in many contexts the only interruption signal the CPU will ever receive will be the v-blank signal (set by the VDP), you can use Halt feature in your gameplay loop to synchronize with v-blank. With MSXgl, just call the Halt() function in your gameplay loop (a good practice is to call it first).

‎‎
void main()
{
	/* Screen and sprites initialization... */

	u8 mPosX = 100; // Monster's position X coordinate
	while (1) // Gameplay loop
	{
		Halt(); // Wait for an interruption signal to occurs (hopefully, the v-blank one)
		mPosX++;
		VDP_SetSpritePositionX(0, mPosX);

		/* Player handling… */
	}
}

Interruption handler

The problem with the Halt feature is that if any interruption signal other than v-blank occurs, the Halt will finish while we don't want to. This will happen if you'd like to use the h-blank feature of the MSX2 or if any device plugged to the MSX can trigger an interruption signal.

To overcome this limitation the only way is to check, when an interruption occurred, if the signal has been caused by the VDP v-blank or not.

Using BIOS

When the BIOS is active (main-ROM's slot selected in memory page #0) an interruption handler is called when an interruption signal is triggered. This handler checks the signal origin and if it is from the VDP v-blank, the BIOS calls a given address in RAM. The program can ‘hook’ this address to have its own function to be called.

The ‘hooked’ function call is thus synchronized with the v-blank.

You have 2 ways of taking advantage of this:

  • Simply put your gameplay loop in this function! It's simple but not very handy for a real game.
  • Use this function to create a blocking function, like the Halt feature but that waits for v-blank signal only. This is the recommended method.

Example of blocking function:

‎‎
// H_TIMI interrupt hook. Called when v-blank interruption occured
void VBlankHook()
{
	g_VBlank = TRUE; // Set signal flag
}

// Wait for v-blank period
void WaitVBlank()
{
	while (g_VBlank == FALSE) {} // g_VBlank become TRUE when v-blank signal 
	g_VBlank = FALSE; // Reset signal flag
}

// Program entry
void main()
{
	/* Screen and sprites initialization... */

	Bios_SetHookCallback(H_TIMI, VBlankHook); // Register our function

	u8 mPosX = 100; // Monster's position X coordinate
	while (1) // Gameplay loop
	{
		WaitVBlank(); // Wait for an v-blank signal
		mPosX++;
		VDP_SetSpritePositionX(0, mPosX);

		/* Player handling… */
	}
}

Using MSXgl ISR

When using "ROM_48K_ISR", "ROM_64K_ISR", NEO mapper targets, or any ROM with InstallRAMISR equal to "RAM0_ISR" or "RAM0_SEGMENT", the MSXgl boot program will switch memory page 0 to point to a ROM segment or RAM where an custom interruption handler (ISR) is located.

This custom ISR is optimized for games but works the same as BIOS’s ISR regarding synchronization. With MSXgl ISR, no need to ‘hook’ a function. The system ask the program to provide a given function named: VDP_InterruptHandler();

This is the function that will be called each time the v-blank signal is detected.

The previous blocking function can now be made like this:

‎‎
// H_TIMI interrupt hook. Called when v-blank interruption occured
void VDP_InterruptHandler()
{
	g_VBlank = TRUE; // Set signal flag
}

// Wait for v-blank period
void WaitVBlank()
{
	while (g_VBlank == FALSE) {} // g_VBlank become TRUE when v-blank signal 
	g_VBlank = FALSE; // Reset signal flag
}

// Program entry
void main()
{
	/* Screen and sprites initialization... */

	u8 mPosX = 100; // Monster's position X coordinate
	while (1) // Gameplay loop
	{
		WaitVBlank(); // Wait for an v-blank signal
		mPosX++;
		VDP_SetSpritePositionX(0, mPosX);

		/* Player handling… */
	}
}

How to handle 60 and 50 Hz?

V-blank signal is great to synchronize gameplay but this signal is not triggered at the same time interval on NTSC and PAL/SECAM regions (60 vs 50 Hz). So, if you synchronize with v-blank, your game will not run at the same speed on machines from both regions. This is a well known issue for European kids back in the days, who played slow Japanese games (NTSC) on their home MSX (PAL/SECAM).

As the program can detect if the MSX is a 50 or 60 Hz machine, it can adapt so the same game runs at the same speed on all machines.

There are several ways to do that.

Simple is best

The far simplest way to handle display frequency difference is to tune your game for 50 Hz and, when a 60 Hz machine is detected, just drop 1 frame out of 6. That way, you have the same number of gameplay frames per second on any MSX, and on 60 Hz, you have 1 frame out of 5 that last a little longer.

This can be achieved by changing only the WaitVBlank() function.

‎‎
// H_TIMI interrupt hook. Called when v-blank interruption occured
void VDP_InterruptHandler()
{
	g_VBlank = TRUE; // Set signal flag
	g_VBlankCount++;
}

// Wait for v-blank period
void WaitVBlank()
{
	while (g_VBlank == FALSE) {} // g_VBlank become TRUE when v-blank signal 
	g_VBlank = FALSE; // Reset signal flag
	if (g_IsNTSC && (g_VBlankCount == 4)) // 5th frame on NTSC machine
{
		while (g_VBlank == FALSE) {} // wait another frame
		g_VBlank = FALSE;
		g_VBlankCount = 0;
}	
}

Note this method works well also for music playback so the same music (made for 50 Hz) can be replayed at the same speed on 50 and 60 Hz machines.

Alternatives

Other solutions for managing the difference between 50 Hz and 60 Hz machines:

  • Do the opposite of the previous solution: tune your game for 60 Hz and, on a 50 Hz machine, run the gameplay loop twice every 5 frames. The big problem with this method is that the execution time of 2 frames may greatly exceed the time allocated between two v-blank signals.
  • Have 2 sets of movement quantities, one for 50 Hz and one for 60 Hz. This is possible with fixed-point numbers (the movement quantities must be 1.2 times greater in 50 Hz to compensate for the frequency difference). You also need to provide two sets of music and sound effects, or have a format that can adapt (like VGM format). #2

Notes