By Grim.
In games where every cycles count, the score is one of the many tiny things to take care of. The most cycle-consuming score-operations usually are converting the score value from binary to decimal and displaying that decimal value on screen.
To convert a binary number to its decimal string representation, using generic ITOA (Integer to ASCII) routines will be too slow and, more often than not, limited to 16-bit range. Specialized routines will hopefully perform better but still take precious CPU-cycles. Bummer!
This article presents an example of somewhat fast decimal scoring routines for arbitrary large numbers that work directly on decimal values, because the fastest conversion is no conversion at all! Technically, all the numbers will be stored in little-endian (LSB to MSB) and BCD form (2 digits per byte).
To keep the source-code simple, we will deal with only two numbers, the score and the points (to be added to or subtracted from the score), both with separately configurable sizes. Also, no checks are made to prevent under/overflows.
Why not using the same size for all numbers you may ask? Well, you can if you want to. But in some cases, for example shooter games, using very large score (10 or more digits), you almost never add to it any point values with more than 4 or 5 digits. So it would be a waste of memory and CPU-cycles to use the same size on everything, score and points. And in practice, you might even need differently sized points!
Little-Endian?
In little-Endian, the values are stored with the least significant byte first (exactly like the Z80 already does for 16-bit values). This endianness was chosen because it simplifies a bit the ADD/SUB operations on the score.
BCD?
Binary-Coded-Decimal will store 2 decimal digits in a single byte, 1 digit (0 to 9) per nibble (4-bit). The BCD values will obviously take a little bit more memory-space (since a single byte can only represent values from 0 to 99 instead of the usual 0 to 255). This trade-of allows avoiding slow conversions from binary to decimal and our high-end Z80 CPU has the necessary instruction (DAA
) to quickly handle arithmetic operations on BCD values! Yay!
Score display?
There won’t be any optimized score display presented here since optimized means tailored for a very specific context only you know. However, here are a few hints:
💡 #1: The score does not need to be displayed at full frame rate! Only once every, say, 20 video-frames will be more than enough.
💡 #2: Also, to avoid cycle-spikes when displaying very large score numbers, you can display only one or two digits per frame instead of the whole thing at once.
💡 #3: Captain Obvious, no need to display the score if it did not change.
The score
You can configure the size of the score to any length, but the larger you set it to, the more memory and CPU-cycles it will take to store, update and display.
;; Configure the size of the score in byte (2 digits per byte)
CNF_SCORE_SIZE EQU <size between 1 and 255>
API: score_reset
- Description : Reset the score to zero
- Input : None
- Output :
HL
,B
,AF
modified
;; Reset the score to zero
call score_reset
The points
Again, configurable size. Obviously it should not be greater than the score‘ size (OR ELSE! :).
;; Configure the size of the points in byte (2 digits per byte)
CNF_POINT_SIZE EQU <size between 1 and 255>
If you do not need the subtraction routine, you can disable it at compile time with:
;; Enable (1) or Disable (0) compilation of the score subtraction function
CNF_SCORE_WITH_SUB EQU <0 or 1>
API: score_add and score_sub
- Description: Add or Sub operations on the current score value
- Input:
DE
= Pointer to point number
- Output :
HL
,DE
,AF
modified - Execution Time:
- Min =
7 + CNF_POINT_SIZE * 10
- Max =
19 + CNF_POINT_SIZE * 10 + (CNF_SCORE_SIZE - CNF_POINT_SIZE) * 12
- Times given in NOPs when the score buffer does not cross a
&xx00
address boundary.
- Min =
;; Usage example of score_add and score_sub routines
;; Add points when a zombie is killed
ld de,pt_zombie_killed
call score_add
;; Sub points when a zombie bit you!
ld de,pt_zombie_bite
call score_sub
...
pt_zombie_killed DB &00,&10,&00 ; 1000 pts
pt_zombie_bite DB &50,&00,&00 ; 50 pts
Source-code
;; Micro-optimization utility
MACRO SCORE_INC_HL
LET _a = _score_data AND &FF00
LET _b = _score_data + CNF_SCORE_SIZE AND &FF00
IF _a XOR _b
inc hl
ELSE
inc l
ENDIF
MEND
;; Reset score to zero
score_reset:
ld hl,_score_data
ld b,CNF_SCORE_SIZE
xor a
_score_reset_loop
ld (hl),a
SCORE_INC_HL
djnz _score_reset_loop
ret
;; Add points
score_add:
ld hl,_score_data
; Clear carry
or a
; Add points to the score
REPEAT CNF_POINT_SIZE
ld a,(de)
adc a,(hl)
; Adjust ADC result to BCD
daa
ld (hl),a
SCORE_INC_HL
inc de
REND
; Exit immediately if there is no carry left
ret nc
; Carry propagation
REPEAT CNF_SCORE_SIZE - CNF_POINT_SIZE
ld a,(hl)
adc a,0
daa
ld (hl),a
ret nc
SCORE_INC_HL
REND
; Overwrite the useless last RET NC:INC
ORG $-2
; Exit
ret
IF CNF_SCORE_WITH_SUB
;; Substract points (optional)
score_sub:
ld hl,_score_data
; Clear carry
or a
; Sub points to the score
REPEAT CNF_POINT_SIZE
ld a,(de)
sbc a,(hl)
daa
ld (hl),a
SCORE_INC_HL
inc de
REND
; Exit immediately if there is no carry left
ret nc
; Carry propagation
REPEAT CNF_SCORE_SIZE - CNF_POINT_SIZE
ld a,(hl)
sbc a,0
daa
ld (hl),a
ret nc
SCORE_INC_HL
REND
; Overwrite the useless last RET NC:INC
ORG $-2
; Exit
ret
ENDIF
Usage example
;; Compilation options (must be defined _before_ including score.asm)
CNF_SCORE_WITH_SUB EQU 0 ; Disable subtract function (not needed)
CNF_SCORE_SIZE EQU 10 ; Score is 20 digits long
CNF_POINT_SIZE EQU 3 ; Points are 6 digits long
ORG &1000
RUN $
di
; reset score
call score_reset
; add some points
ld de,score_pt_drone
call score_add_point
ld de,score_pt_turet
call score_add_point
ld de,score_pt_zombie
call score_add_point
ld de,score_pt_boss
call score_add_point
; ...
;; Example of a Right To Left (RTL) ASCII conversion, all at once score display.
;; (for an LTR conversion, you must read the score from MSB to LSB instead)
print_score:
ld hl,_score_data
ld b,CNF_SCORE_SIZE
_print_score_loop
ld a,(hl)
SCORE_INC_HL
; first digit
ld c,a
call print_score_digit
; second digit
ld a,c
rra
rra
rra
rra
call print_score_digit
djnz _print_score_loop
ret
;; Display a single digit (0 to 9)
print_score_digit:
and &0F
add a,"0"
; A = ASCII code to display
push hl
push bc
;call to some chr out routine here
pop bc
pop hl
ret
;; Define the point values that will be added to the score
;; LSB to MSB and BCD form.
score_pt_drone DB &50,&00,&00 ; 50 pts
score_pt_turet DB &00,&01,&00 ; 100 pts
score_pt_zombie DB &00,&10,&00 ; 1000 pts
score_pt_boss DB &00,&00,&01 ; 10000 pts
;; Score buffer, ideally located within a 256-bytes address boundary so that
;; the INC L speed-optimization can be used
_score_data DS CNF_SCORE_SIZE, 0
;;Include the score routines
READ "score.asm"
Changelog
- 2020-11-07 – Recycled by MrEarwig