NOTE: This is the second in a series of articles I intend to write to discuss new features to be included in the upcoming P-Machinery v2.0, a.k.a. P-Machinery AGE(Advanced Game Engine).
Tick-Tock: Introducing The System Clock
P-Machinery 1.0 relied heavily on Joe Zbiciak's excellent SDK-1600 for its core libraries. Specifically, it used the TASKQ and TIMER modules, which abstracted a processing queue for game tasks and delayed or repeating events, as the basis for its operating system. Although very useful, these modules had severe limitations not only in their functionality, but on the low-level programming model they promoted.
P-Machinery AGE aims to improve on this by implementing a brand new operating system core that supports prioritized processing tasks and timed events, and expose it all with a high-level programming model. The programmer can then concentrate on game logic without having the burden of dealing with the internal representation of data structures or other arcane details.
Moreover, this model is intended to be intuitive and efficient, with very little compromise on processing speed.
Timed events go beyond just delayed or repeating tasks such as updating display or changing enemy states; they are at the center of many of the features that a game engine should handle. These include synchronizing music and sound effects, sprite animation, moving-object velocity, applied physics, screen transitions, and many more.
If something occurs over a period of time, its life-cycle needs to be measured accurately in order to maintain the pace of the game--and it all starts with a clock: the System Clock.
In P-Machinery AGE, the System Clock is a 16-bit counter that keeps track of time. It is updated automatically by the game engine by an appropriate amount, and the entire system looks to it as the standard temporal measure.
The sprite driver needs to update animations? It just needs to calculate how many time units need to pass from the current System Clock value, based on the animation frame-rate.
The entrance of an enemy into the play-field needs to be delayed by five seconds? Just add to it the current value of the System Clock, and trigger a task on expiration.
Keeping time becomes a matter of tracking counters, and since the entire system agrees on the counting units, all pieces can synchronize with each other.
Moreover, P-Machinery AGE goes a step further. The framework encapsulates the tracking of time and triggering of events, not only allowing all sub-systems to work in synchronicity, but exposing these facilities to the programmer with an intuitive programming interface.
Vertical Blanking Madness
The only stable and practical means that the Intellivision offers to track time is the Vertical Blank interrupt request, or VBLANK IRQ. This IRQ is controlled by the video chip (STIC) and occurs on a very regular frequency, in between drawing television frames. During this time, while the television realigns its electron gun to draw a new screen frame, the STIC interrupts the CPU to read the graphics data buffer.
During this VBLANK period, the CPU, the STIC and the Graphics RAM, are all connected to the data bus, giving the programmer access to read and write to Graphics RAM and the STIC registers. In fact, this is the only way to manipulate graphics on an Intellivision.
More importantly, the VBLANK IRQ provides a regular signal that synchronizes all game events and time-dependent aspects of a program.
There is only one problem: it is directly tied to the frame-rate of the television signal.
This provides two main challenges. First, if we use the VBLANK signal as our System Clock, we can only track timed events that occur on periods that are multiples of the TV signal's frame-rate. Second, there are two variations of the Intellivision that support the most common TV standards around the world, and they both have different frame-rates: NTSC with a frame-rate of 60Hz, and PAL/SECAM with a frame-rate of 50Hz.
Jiffy Pop Time!
In order to synchronize all time-dependent aspects of the frame-work, it is important to define a standard unit to measure time. Moreover, for this unit to be equally useful regardless of the TV standard employed, it needs to be a multiple of both 50 and 60 Hertz, lest we want to be bogged down with complex fractional arithmetic.
Finally, in order to provide a more granular scale for timed events, such 10th or 100th of seconds, and still work with integers, the unit should be divisible by 10 or 100.
All these things considered, I have chosen a unit of time for P-Machinery AGE of 300. Borrowing a term from the Linux Kernel community, I'm calling this unit the Jiffy.
A Jiffy is formally defined as 1/300th of a second.
This leads automatically to the following ratios:
1 NTSC Frame = 1/60th Second 1 PAL Frame = 1/50th Second 1 Jiffy = 1/300th Second 1 NTSC Frame = (300 / 60) = 5 Jiffies 1 PAL Frame = (300 / 50) = 6 Jiffies 300 Jiffies = 1 Second 30 Jiffies = 1/10th Second 3 Jiffies = 1/100th Second
What's more, P-Machinery AGE calibrates the game engine during the cartridge boot-up sequence, automatically detecting whether the game is running on an NTSC or PAL/SECAM environment. This sets the number of Jiffies by which to increment the System Clock on each VBLANK IRQ. Since all time calculations are based on Jiffies and not on individual frame counts, this effectively makes all time measures the same irrespective of the TV-standard employed.
No longer do games need to be region-specific, or play slower on one version of the console versus another. Timed events are normalized across all platforms.
Timed Events: Delayed & Repeating Tasks
The new TIMER module in P-Machinery AGE groups timed events into two categories:
- Delayed Events: One-off tasks that need to start after a given initial delay.
- Repeating Events: Recurring tasks that repeat on a given frequency or period.
P-Machinery AGE offers a set number of timers, by default eight but this is configurable at assembly time. Setting a timed event is a matter of defining the event and assigning it to one of the available timers. An event can be configured to use any of the available timers; but once set, the timer will remain dedicated to it until reset or re-configured.
That's it. The game engine takes it from there, converting the units accordingly, keeping track of the System Clock, and automatically triggering the event when appropriate.
The Application Programming Interface (API) exposed by the framework, offers five routines to manage timed events:
- TIMER.Reset - Reset the timer sub-system.
- TIMER.StartRepeatEvent - Initiates a repeating event immediately.
- TIMER.SetRepeatEvent - Initiates a repeating event after a given delay.
- TIMER.SetDelayEvent - Initiates a non-recurring event after a given delay.
- TIMER.CancelEvent - Cancels an active timer event.
By the default, all timers are reset upon starting the program and during game state transitions, so the programmer only needs to do this manually when he wants to clear all active timers at once. The rest of the routines allow the programmer to set repeating or one-shot events, or cancel any untriggered event previously set.
Since P-Machinery AGE follows an event-driven model, timed events work by providing a pointer to an event-handler routine that will be called when the timed event is triggered. This is a program-specific routine to perform some task when the event occurs.
For instance, suppose you want to update a particular part of the screen once every second, perhaps to highlight a cave exit by making the door blink intermittently. You can use the TIMER.StartRepeatEvent routine to engage a repeating timer that will call your blinking routine at the appropriate time.
The arguments to the TIMER.StartRepeatEvent routine are as follows
TIMER.StartRepeatEvent: ----------------------- timer - The timer number to use (0..PM.TIMER_COUNT). rate - The repeating rate frequency or period. units - The units of the repeating rate frequency or period. - Available values are: Jiffy Internal clock units. Fps Frames per second. kFps Thousand frames per second. Hz Hertz (same as Fps). kHz Kilo-Hertz (same as kFps). Sec Seconds. mSec Milli-seconds. task - A pointer to a task to execute when the event triggers. data - An optional data argument to pass to the task.
The following code will engage timer #0 with a repeating event that will call procedure BLINK_DOOR every second with an NULL (unused) argument.
TIMER.StartRepeatEvent(0, 1, Sec, BLINK_DOOR, PM.NULL)
However, blinking a door may involve a two step process: first painting it with a highlighting colour, then reverting it back to normal after a short period; repeating this over and over.
We could enhance our code so that the procedure BLINK_DOOR accepts an argument telling it whether to paint the door with a highlight (1) or normal colour (0). We then set two timers, the second of which would be slightly offset after the first one, say 1/2 a second or 500 milli-seconds. For this we need to use the TIMER.SetRepeatEvent routine, which is similar to TIMER.StartRepeatEvent except that it accepts an initial delay.
The arguments to the TIMER.SetRepeatEvent routine are:
TIMER.SetRepeatEvent: --------------------- timer - The timer number to use (0..PM.TIMER_COUNT). delay - An initial delay before the first event rate - The repeating rate frequency or period. units - The units of the repeating rate frequency or period. - Available values are: Jiffy Internal clock units. Fps Frames per second. kFps Thousand frames per second. Hz Hertz (same as Fps). kHz Kilo-Hertz (same as kFps). Sec Seconds. mSec Milli-seconds. task - A pointer to a task to execute when the event triggers. data - An optional data argument to pass to the task.
Our code would then look like this:
TIMER.StartRepeatEvent(0, 1, Sec, BLINK_DOOR, 1) TIMER.SetRepeatEvent(1, 500, 1000, mSec, BLINK_DOOR, 0)
If at some point you want to stop highlighting the door, say, when the player finally notices and opens it; just call the TIMER.CancelEvent routine at the appropriate point in your program. The routine accepts the timer number to cancel as an argument:
Et voilá! The timed events are now cancelled. To restart them, you'll have to call the same routines again to define the timers as described above.
That's all there is to it. There is no need to wait, loop, or poll the System Clock. There is not even the need to convert the units and calculate timer deadlines. The framework and the game-engine conspire to abstract all the timing complexities from your program.
Managing time is an integral part of any game. With a general System Clock to drive all temporal events, flexible time measurement units that are independent of the hardware and frame-rate speeds, and a frame-work that abstracts the complexities of conversion and event handling; P-Machinery AGE encourages the game programmer to think at a higher cognitive level, and lets him concentrate on the fun part of making games.
NOTE: I would like to thank Joe Zbiciak (intvnut) and Arnauld Chevallier for their assistance and inspiration. Much of the innards of the time management sub-system in P-Machinery AGE are based on ideas provided by them in the INTV-Prog mailing list.
Also, thanks to Joe for suggesting Jiffy as the name for the unit of time. I probably would have gone with something equally cute but not as cool, like TICKlets.
Edited by DZ-Jay, Sun Aug 14, 2016 8:15 AM.