Since I plan on the next few articles to be about plane scrolling and sprite animation, I’d like to experiment with a few of the prerequisites first – basic pad input, timing, and the main game loop. I already have a sprite on screen, plus some subroutines for setting its X and Y coords, so I’ll aim to move it around the screen using the D-pad at various speeds. Hopefully this won’t take long.
Timing seems pretty awkward; in modern games programming I’m used to recording a frame’s delta time and using that to determine how far a character should move in one frame. This would require some extra maths which could bog the 68k down, and since the VDP has a fixed refresh rate the common technique seems to be to use hard-coded speed values, but wait for vsync at the end of the game loop. It sounds very hacky to me, since I was taught to use time deltas to achieve FPS independence all through my programming career, but let’s see how it goes.
Polling gamepad input
Gamepads are interacted with through the port control and port data addresses. These are generic 9-pin serial ports, used to connect pads, joysticks, light guns, even modems, but for simplicity I’ll assume we only have gamepads connected for now. I’ll also assume we’re not interested in port C, which is the EXT port on the back of the American Genesis model 1.
To read a pad’s state, we need to read from its data port – there’s one per port, 0x00A10003 and 0x00A10005. Only a byte at a time can be read from these addresses, and to tell the port whether we want the upper or lower byte returned we have to write bit 7 to it first. A typical read for all of the buttons goes something like this:
- Read one byte from data port to a register (contains 00SA0000)
- Shift the data to the upper byte
- Write bit 7 ON to the port
- Read one byte again to the register (contains 00CBRLDU)
- Write bit 7 OFF to the port to put it back to normal
ReadPad1: ; d0 - Return result move.b pad_data_a, d0 ; Read upper byte from data port rol.w #0x8, d0 ; Move to upper byte of d0 move.b #0x40, pad_data_a ; Write bit 7 to data port move.b pad_data_a, d0 ; Read lower byte from data port move.b #0x00, pad_data_a ; Put data port back to normal rts
pad_button_up equ 0x0 pad_button_down equ 0x1 pad_button_left equ 0x2 pad_button_right equ 0x3 pad_button_a equ 0xC pad_button_b equ 0x4 pad_button_c equ 0x5 pad_button_start equ 0xD
Getting the state of a 6 button pad is a little more complex – it requires bit 7 to be set ON, and then OFF, and then ON again to retrieve the 3rd byte of data. For the moment I’ll leave it out, I don’t own any 6 button controllers for testing anyway.
Waiting for vertical blanking
In order to update a sprite’s position without causing any tearing or flickering, it’s best to modify them during vertical blanking. This is the period during which the electron beam has reached the bottom-right hand side of the screen and is in the process of moving back up to the top-left. To test for this state, we need to poll the VDP’s status register. This is as simple as reading a word from the VDP control port. The word’s bits represent the following:
- 0: Region mode: OFF=NTSC, ON=PAL
- 1: ON during a DMA operation
- 2: ON during horizontal blanking
- 3: ON during vertical blanking
- 4: ON during odd frame in interlaced mode
- 5: ON whilst two sprites have non-transparent pixels colliding
- 6: ON whilst too many sprites are on a single scanline
- 7: ON during a vertical interrupt
- 8: ON if FIFO is full
- 9: ON if FIFO is empty
- 10-15: Unused
We’re interested in bit 4, which will get turned ON whilst the screen is being blanked to perform a vertical retrace, and OFF whilst the screen is active and drawing:
WaitVBlankStart: move.w vdp_control, d0 ; Move VDP status word to d0 andi.w #0x0008, d0 ; AND with bit 4 (vblank), result in status register bne WaitVBlankStart ; Branch if not equal (to zero) rts WaitVBlankEnd: move.w vdp_control, d0 ; Move VDP status word to d0 andi.w #0x0008, d0 ; AND with bit 4 (vblank), result in status register beq WaitVBlankEnd ; Branch if equal (to zero) rts
Waiting for the vertical blanking has a second advantage – it happens once every 50th (PAL) or 60th (NTSC) of a second, so it forces our game loop to run at a maximum of 50 or 60 FPS.
Putting it all together
I’m assuming the whole idea is to ensure that the game code is fast enough to execute inside one whole frame, before the VBlank occurs, to keep it running at 24 frames per second. If it oversteps the mark, it’ll have to wait until the next VBlank which will reduce the framerate to 12. This all sounds awkward to me, but let’s see how it pans out when I make a start on the actual game.
Building on the code from the last article, I can now write a game loop which will check the gamepad data, and set the sprite’s X and Y coordinates during vertical blanking, and whilst maintaining 24 frames per second. I’ve also added a check for the A button, which will increase the speed of the sprite’s movement:
move.l #0x80, d4 ; Store X pos in d4 move.l #0x80, d5 ; Store Y pos in d5 ; ************************************ ; Main game loop ; ************************************ GameLoop: ; ************************************ ; Read gamepad input ; ************************************ jsr ReadPad1 ; Read pad 1 state, result in d0 move.l #0x1, d6 ; Default sprite move speed in d6 btst #pad_button_a, d0 ; Check A button bne @NoA ; Branch if button off move.l #0x2, d6 ; Double sprite move speed @NoA: btst #pad_button_right, d0 ; Check right button bne @NoRight ; Branch if button off add.w d6, d4 ; Increment sprite X pos by move speed @NoRight: btst #pad_button_left, d0 ; Check left button bne @NoLeft ; Branch if button off sub.w d6, d4 ; Decrement sprite X pos by move speed @NoLeft: btst #pad_button_down, d0 ; Check down button bne @NoDown ; Branch if button off add.w d6, d5 ; Increment sprite Y pos by move speed @NoDown: btst #pad_button_up, d0 ; Check up button bne @NoUp ; Branch if button off sub.w d6, d5 ; Decrement sprite Y pos by move speed @NoUp: ; ************************************ ; Update sprites during vblank ; ************************************ jsr WaitVBlankStart ; Wait for start of vblank move.w #0x0, d0 ; Sprite ID move.w d4, d1 ; X coord jsr SetSpritePosX ; Set X coord move.w d5, d1 ; Y coord jsr SetSpritePosY ; Set Y coord jsr WaitVBlankEnd ; Wait for end of vblank jmp GameLoop ; Back to the top
I’ve been experimenting with some code which delays for a set number of frames, it’s currently of no use to me but along the way it’s forced me to take a more detailed look at the h/v-sync interrupts and the 68000 status register, so I’ll share my findings.
First, a correction. My original code for VDP registers 1 and 2 make the following claims:
dc.b 0x20 ; 0: Horiz. interrupt on, plus bit 2 (unknown, but docs say it needs to be on) dc.b 0x74 ; 1: Vert. interrupt on, display on, DMA on, V28 mode (28 cells vertically), + bit 2
The values aren’t very useful, and the comments aren’t strictly true. I’ve had a good read of a document (in references) laying out each bit of each register and the following makes better sense:
dc.b 0x14 ; 0: Horiz. interrupt on, display on dc.b 0x74 ; 1: Vert. interrupt on, screen blank off, DMA on, V28 mode (40 cells vertically), Genesis mode on
The mysterious “bit 2” of the first two registers are actually compatibility modes for the SEGA Master System. Bit 2 of register 1 OFF sets 8 colours per palette, and bit 2 of register 2 OFF puts the VDP in SMS display mode. The first register needed fixing up to turn on horizontal sync interrupts.
The horizontal and vertical sync interrupts are jumped to each time the proton beam reaches the right-hand side of the screen, and when it reaches the bottom-right hand corner of the screen. As far as I can find, the horizontal interrupt is the most frequently occuring event we can monitor. By reserving an integer’s worth of RAM, we can increment a counter every time it fires, and use that as a system tick count. Surprisingly, this is the first time I’ve even used main memory – everything I’ve done so far transfers data straight from cartridge ROM into the VDP’s arena. I’ll need a memory map:
hblank_counter equ 0x00FF0000 ; Start of main RAM vblank_counter equ 0x00FF0004
I’ll start off simple, but perhaps later I could come up with some sort of macro to allow me to specify how much to allocate, and the address could be incremented automatically – like a simplified version of the art asset size/address defines.
The interrupts have already been defined in the init code, so we just need to put them to good use:
HBlankInterrupt: addi.l #0x1, hblank_counter ; Increment hinterrupt counter rte VBlankInterrupt: addi.l #0x1, vblank_counter ; Increment vinterrupt counter rte
ADDI is one of the rare opcodes that can operate directly on a memory address, without having to load the value into a register first. This is a good thing, since the work done inside the interrupts must be absolutely minimal, we have very few clock cycles available before the proton beam has finished resetting. Next, interrupts must be enabled via the status register. In my original init code, I initialised this register to 0x2700 as per some sample code, with little thought as to what it was up to. I’ve found some information about its bits:
- 0 – Trace exception
- 1 – Unused
- 2 – Supervisor mode (always enable)
- 3 – Unused
- 4 – Unused
- 5 – Interrupt level (zero for all interrupts enabled)
- 6 – Interrupt level
- 7 – Interrupt level
- 8 – Unused
- 9 – Unused
- 10 – Unused
- 11 – CCR Extend
- 12 – CCR Negative
- 13 – CCR Zero
- 14 – CCR Overflow
- 15 – CCR Carry
I’ve encountered Supervisor Mode before, on the Atari ST. It allows non-user-mode operations to be called (OS traps and such), but I’m not sure what functionality is prohibited if it were turned of on the Megadrive. Bottom line, it needs to be ON. I also need to ensure that the three interrupt level bits are OFF – these determine the lowest interrupt level that is allowed to fire, and at the moment I don’t know which interrupts qualify for what level so I’ve enabled them all. With this in mind, I’ve corrected the init code to read:
; Init status register (no trace, supervisor mode, all interrupt levels enabled, clear condition code bits) move #0x2000, sr
Now the h/v-sync interrupts should fire periodically, and we can fashion some sort of time delay out of them:
WaitFrames: ; d0 - Number of frames to wait move.l vblank_counter, d1 ; Get start vblank count @Wait: move.l vblank_counter, d2 ; Get end vblank count subx.l d1, d2 ; Calc delta, result in d2 cmp.l d0, d2 ; Compare with num frames bge @End ; Branch to end if greater or equal to num frames jmp @Wait ; Try again @End: rts
I’ll probably use it to add delays between the startup game states (startup logo to main menu to first level) since it’s pretty useless for anything in the main game loop. Even if I don’t use it, I learned something along the way.
asm68k.exe /p spritetest.asm,spritetest.bin