Skip to content

Reading inputs (Part 5)

Peter Helcmanovsky (Ped) edited this page Jun 6, 2020 · 2 revisions

A proper game should be interactive

It's not a computer game, if you can't control anything about it. Let's add some input readings to the current source.

For the example purposes, the game will read two different inputs. The "Kempston joystick interface" at I/O port $1F and the iconic "OPQA space" from keyboard (I/O port $**FE). The game reads both inputs all the time and mix the result together. This can cause issues on original ZX Spectrum where reading Kempston joystick without the actual interface being present leads to reading "floating bus" values and the player will appear to be randomly moving/jumping, but on Next the Kempston interface is by default active, even if no joystick port is selected to read as Kempston, so it is "safe" to assume the input readings will be zero. On classic ZX Spectrum we would have to provide some options screen to enable/disable joystick reading, actually to be 100% safe we should do that in Next game too, but for the sake of this example we will ignore this issue and assume the Kempston port reading zero when no joystick is used.

Reading the joystick and keyboard

(here is a good spot to open the SpecBong.asm file and try to match the source code lines with this text)

(you can also check the total difference between "Part 5" and "Part 4C" - there was some pruning of extensive comments but the new code is mostly grouped together)

The subroutine ReadInputDevices is doing the actual readings of HW inputs, transforming the information into simple bit-mask byte stored in memory at label Player1Controls. This is the only code which cares whether the input comes from joystick or keyboard, the rest of the game will test only the bits in the Player1Controls byte, not having to deal with the HW input details. This will allow us to easily mix both joystick and keyboard inputs together, and eventually add support for different kind of input HW, affecting only one place in the source.

First the Kempston port $1F is read, and that one contains the inputs in the same way how we will store them in the Player1Controls byte, so no conversion is required, the Kempston byte read will be used "as is" (the Player1Controls byte does intentionally copy the design of Kempston readings, as it suits our purpose well).

Then the code reading keyboard follows. This one is a bit bigger, as ZX Spectrum keyboard is read in 8x5 matrix way, we clear the bit in the upper I/O port number to signal which row of the matrix we want to read and do the in instruction to get readings of five keys. The released keys are read as value "1", pressed keys are read as value "0" (opposite to how Kempston joystick reads).

(it is possible to clear multiple bits in the port number to request reading of combined rows together, used sometimes for "wait for any key" code to simplify the reading code, but SpecBong is reading only individual keys specifying the single row of keys at one time)

Once the single row of keys is read, we do few rrca instructions to extract the particular bit (most of the keys we want to read - "P", "Q", "A" and "space" - are actually in the first bit of the result, only key "O" is in the second bit) into carry flag and then rl d to add it to intermediate result. Once all five controls are in the D register at the proper position, they are inverted by single cpl to become "0 = released, 1 = pressed" information, identical with how the joystick is read.

Then simple or of the joystick and keyboard readings is done to mix the inputs together and new value for Player1Controls byte is born and stored.

Processing the inputs

The inputs are read, but now the game should do something about them. Let's do something simple as a start, mapping the direction inputs to the player sprite position (clamping it near borders to not leave the area with background image), so we can freely roam around the screen, and adding "fire" trigger to change sprite-pattern displayed as player, just as fun exercise and debug measure (to verify all inputs work).

The new subroutine Player1MoveByControls will be responsible for this logic, so let's go there.

It starts with processing value stored in Player1FireCoolDown byte. This value is decremented every time the player logic is processed, until it does reach zero, then it stays zeroed. This will be used as "cool down" period after "fire" button is used, to trigger the fire-event only every 10th frame.

Once the fire-cool-down value is refreshed, the actual reading of inputs starts by loading memory address of Player's sprite data into IX register (using the structure S_SPRITE_4B_ATTR definition to access particular field of the attribute-data) and reading the inputs value Player1Controls into register B.

The horizontal movement left/right is somewhat more tricky, as the X-coordinate is 9bit long, spread across two sprite byte attributes, the code does first load the full 9bit coordinate into HL, then it does +-2 to it depending on the pressed left/right inputs, also clearing/setting the "mirror X" bit to make the player sprite facing the direction of the movement.

After the X coordinate and mirror-X flags are modified, the X coordinate is "sanitized" to stay within 32..271 values (this is actually bug, the coordinate 272 is still valid displaying full 16x16 sprite within 256x192 area, but I didn't notice the bug when I was finalizing "Part 5" so it is now part of this narrative).

Once the X coordinate is sanitized, it is recombined back with the mirror/rotation flags (including the newly modified mirror-X flag) and stored in the player's sprite attributes memory.

The vertical movement up/down has to deal with only 8bit value of Y, moving it again by +-2 based on the input pressed, sanitizing it to range 32..207 (again the 207 is bug, the value 208 is last valid position to keep full 16x16 sprite visible within the 256x192 pixel area).

Once the movement coordinates are updated, the "fire" input is checked and if it is pressed and cool-down counter is zero, the sprite pattern is modified (to go through all 64 of them) and cool-down counter is set to 10 to introduce delay between pattern changes.

Updating game-loop code

With the subroutines ready to be used, the only thing left to do is to connect them into the game-loop to make the code active.

After the sprite data are uploaded and snowballs "AI" ran, the HW inputs are read with call ReadInputDevices and then processed by the call Player1MoveByControls and that's all (with addition of extra border-color change to measure the performance of the new subroutines - just to reveal they hardly eat one/two video-lines worth of CPU time with this simple logic).

Building the NEX file and running it will allow us to roam around the screen over the 256x192 pixel area and pressing fire to change the player's sprite pattern, so SpecBong has become interactive.

Sadly after toying with it for a while, you may notice the "playability" of such game would probably not score well in any magazine review, so there's lot of work ahead of us to make this into an actual game...