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!
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.
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!
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.
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>
- Description : Reset the score to zero
- Input : None
- Output :
;; Reset the score to zero call score_reset
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
DE= Pointer to point number
- Output :
- 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
- 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
;; 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
;; 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"
- 2020-11-07 – Recycled by MrEarwig