News:

Printed Amstrad Addict magazine announced, check it out here!

Main Menu
avatar_lightforce6128

Frame rate and sprite movement speed

Started by lightforce6128, 00:09, 16 July 25

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

lightforce6128

Based on the strong contrast between arcade games and average CPC games, I always wanted to examine the relation between frame rate and sprite movement speed.

The following program moves 23 sprites, each with a different frame rate. The initial movement speed is set to 1 pixel per frame, but can be changed with left and right cursor key between 1/32 pixel per frame up to 16 pixel per frame in steps of 1/32.

While the topmost sprite with a frame rate of 50 Hz stays stable even for high movement speeds, the sprites with lower frame rates get into trouble sooner or later.

NOLIST
ORG #A000
RUN program_begin



;; This program visualizes the relation between movement speed and frame rate.
;; Higher speeds combined with small frame rates lead to undesirable effects
;; like jumping movement, trails, and ragged object borders. These effects
;; cannot be avoided by double-buffering. Only a higher frame rate will
;; reduce them.
;;
;; See:
;;   - <https://en.wikipedia.org/wiki/Stroboscopic_effect>
;;   - <https://en.wikipedia.org/wiki/Wagon-wheel_effect>



ga_port EQU #7F00
ppi_port_b EQU #F500

os__key_code__escape       EQU #FC
os__key_code__cursor_left  EQU #F2
os__key_code__cursor_right EQU #F3

number_of_sprites EQU 23



program_begin:

;; Set mode 2 and clear screen.
LD A,2 : CALL os__scr_set_mode

;; Print row and column headers.
LD HL,row_and_column_headers : CALL print_string                ;; Print headers.
LD B,1                                                          ;; Sprite counter. Count upwards.
prach__loop:                                                    ;;
    LD H,4 : LD L,B : INC L : INC L : CALL os__txt_set_cursor   ;; Set cursor to (4|counter+2).
    LD A,B : CALL pad_number_with_space : CALL print_number     ;; Print counter with padding.
    LD A,10 : CALL os__txt_set_column                           ;; Set cursor to (10|*).
    LD HL,50 : LD A,B : CALL divide                             ;; Calculate 50/counter.
    PUSH AF                                                     ;;
        LD A,L : CALL pad_number_with_space : CALL print_number ;; Print quotient with padding.
        LD A,"." : CALL os__txt_output                          ;; Add a decimal point.
    POP AF                                                      ;;
    ADD A,A : LD C,A : ADD A,A : ADD A,A : ADD A,C              ;; Calculate remainder*10. Maximum remainder is 16 for 50/17.
    LD C,A : LD A,B : SRL A : ADD A,C                           ;; Calculate remainder*10+counter/2 for rounding.
    LD L,A : LD A,B : CALL divide                               ;; Calculate (remainder*10+counter/2)/counter.
    LD A,L : CALL print_number                                  ;; Print one decimal place.
INC B : LD A,B : CP A,number_of_sprites+1 : JR NZ,prach__loop   ;; Loop while counter less or equal 23.

;; Main loop.
main_loop:

    ;; Wait for VSYNC.
    LD B, ppi_port_b / 256                                           ;;
    vsyncIsAlreadyActive: IN A,(C) : RRA : JR C,vsyncIsAlreadyActive ;; Wait for end of VSYNC.
    waitForBeginOfVSync: IN A,(C) : RRA : JR NC,waitForBeginOfVSync  ;; Now wait for begin of VSYNC.

    ;; Wait longer until screen refresh has passed drawing area.
    HALT : HALT : HALT : HALT : HALT : HALT ;; Wait for 2+5*52 = 262 pixel lines.
    LD B,144 : DJNZ $ : NOP                 ;; Wait for 144*4/64 = 576/64 = 9 more pixel lines.

    ;; For debugging: mark begin of sprite update.
    IF 1                                 ;;
        DEFS 5                           ;; Align raster to begin of non-border region.
        LD BC, ga_port + #10 : OUT (C),C ;; Select pen of border.
        LD C,#55 : OUT (C),C             ;; Set ink "bright blue".
        LD A,8 : DEC A : JR NZ,$-1 : NOP ;; In combination with other commands: Wait 40 chars.
        LD C,#44 : OUT (C),C             ;; Set ink "dark blue".
    ENDIF                                ;;

    ;; Update sprite position.
    LD HL,(position) : LD DE,(speed) : ADD HL,DE ;; Update position.
    LD A,H : AND A,#3F : LD H,A                  ;; Restrict to a maximum byte offset of 63.
    LD (position),HL                             ;; Store position.

    ;; Prepare bit pattern for new position. This is at least used by the fastest row.
    ADD HL,HL : ADD HL,HL : ADD HL,HL : LD A,H : AND A,#07 ;; Extract bit position.
    LD BC,#FF00                                            ;; Start with set block at right.
    JR Z,pbp__skip                                         ;; Skip for offset 0.
        pbp__loop:                                         ;;
            SRL B : RR C                                   ;; Shift right.
        DEC A : JR NZ,pbp__loop                            ;;
    pbp__skip:                                             ;;
    LD IYH,B : LD IYL,C                                    ;; Store bit pattern in IY.

    ;; Update sprites.
    LD B,number_of_sprites                                          ;; Counter.
    LD DE,#0800                                                     ;; Offset from one scanline to next.
    LD HL, 80*2 + 15 + #C000                                        ;; Set sprite base address on screen to third row, character 15.
    LD IX,sprite_table                                              ;; Pointer to current sprite descriptor.
    ml__sprite_loop:                                                ;;
        LD A,(IX+sprite__counter) : DEC A : JR NZ,mlsl__not_at_zero ;; Decrement sprite counter. If not zero, skip sprite processing.
            ;;                                                      ;;
            PUSH HL                                                 ;;
                LD A,(IX+sprite__position)                          ;; Get old sprite position in bytes.
                ADD A,L : LD L,A : JR NC,$+3 : INC H                ;; Add to screen address.
                XOR A,A                                             ;; Set A to eight background pixels.
                ADD HL,DE                                           ;; Delete old sprite in region 2x4 bytes, starting at scanline 2.
                REPEAT 2                                            ;;
                    ADD HL,DE : LD (HL),A : INC HL : LD (HL),A      ;;
                    ADD HL,DE : LD (HL),A : DEC HL : LD (HL),A      ;;
                REND                                                ;;
            POP HL                                                  ;;
            ;;                                                      ;;
            LD A,(position+1) : LD (IX+sprite__position),A          ;; Get new sprite position. Store in sprite descriptor.
            ;;                                                      ;;
            PUSH BC : PUSH HL                                       ;;
                ADD A,L : LD L,A : JR NC,$+3 : INC H                ;; Add nes sprite position to screen address.
                LD B,IYH : LD C,IYL                                 ;; Set BC to bit pattern.
                ADD HL,DE                                           ;; Draw new sprite in region 2x4 bytes, starting at scanline 2.
                REPEAT 2                                            ;;
                    ADD HL,DE : LD (HL),B : INC HL : LD (HL),C      ;;
                    ADD HL,DE : LD (HL),C : DEC HL : LD (HL),B      ;;
                REND                                                ;;
            POP HL : POP BC                                         ;;
            ;;                                                      ;;
            LD A,number_of_sprites+1 : SUB A,B                      ;; Reset sprite counter. Calculate number_of_sprites + 1 - loop_counter.
            ;;                                                      ;;
        mlsl__not_at_zero:                                          ;;
        LD (IX+sprite__counter),A                                   ;; Store updated or reset sprite counter.
        LD A,L : ADD A,80 : LD L,A : JR NC,$+3 : INC H              ;; Go to next row on screen.
        INC IX : INC IX                                             ;; Go to next sprite descriptor.
    DJNZ ml__sprite_loop                                            ;;

    ;; For debugging: mark end of sprite update.
    IF 1                                  ;;
        LD BC, ga_port + #10 : OUT (C),C  ;; Select pen of border.
        LD C,#55 : OUT (C),C              ;; Set ink "bright blue".
        LD A,32 : DEC A : JR NZ,$-1 : NOP ;; Wait 128 chars.
        LD C,#44 : OUT (C),C              ;; Set ink "dark blue".
    ENDIF                                 ;;

    ;; Check keys.
    LD HL, 36 * 256 + 1 : CALL os__txt_set_cursor        ;; Set cursor to position of speed.
    LD HL,(speed)                                        ;; Load speed.
    CALL os__km_read_char : JP NC,main_loop              ;; Get last pressed key. Continue loop, if nothing was pressed.
    CP A,os__key_code__escape       : JR  Z,ml__leave    ;; Leave loop, if <Esc> has been pressed.
    CP A,os__key_code__cursor_left  : JR NZ,$+3 : DEC HL ;; Left cursor key decreases speed.
    CP A,os__key_code__cursor_right : JR NZ,$+3 : INC HL ;; Right cursor key increases speed.
    LD A,H : OR A,L : JR NZ,$+3 : INC HL                 ;; Minimum speed is  0|01 =   1.
    LD A,H : CP 2   : JR  C,$+3 : DEC HL                 ;; Maximum speed is 15|31 = 511.
    LD (speed),HL                                        ;; Store updated speed.
    LD B,L                                               ;; Store fraction in B.
        ADD HL,HL : ADD HL,HL : ADD HL,HL : LD A,H       ;; Extract integer speed.
        CALL pad_number_with_space : CALL print_number   ;; Print integer speed.
        LD A,"|" : CALL os__txt_output                   ;; Print separator.
    LD A,B : AND A,#1F                                   ;; Extract fractional speed.
    CALL pad_number_with_zero : CALL print_number        ;; Print fractional speed.

JP main_loop
ml__leave:

;; Set mode 1 and clear screen.
LD A,1 : CALL os__scr_set_mode

;; Program end.
RST #00     ;; Option 1: Reset system.
;;DI : HALT ;; Option 2: Halt system.
;;RET       ;; Option 3: Return to caller.



divide:
    ;; Divide 16-bit by 8-bit number.
    ;; Source: <https://wikiti.brandonw.net/index.php?title=Z80_Routines:Math:Division>
    ;;     Input:
    ;;         HL - dividend / numerator
    ;;         A  - divisor / denominator
    ;;     Output:
    ;;         HL - quotient
    ;;         A  - remainder
    ;;         *  - Everting else preserved.
    PUSH BC                                  ;;
        LD C,A                               ;;
        XOR A,A                              ;; Collected bits
        LD B,16                              ;; Number of bits, loop counter.
        d__loop:                             ;;
            ADD HL,HL : RLA                  ;; Shift HL to the left, transfer topmost bit to A.
            JR C,d__found_set_bit            ;;
                CP A,C                       ;; If the current bit is not set, and also not enough bits have
                JR C,d__smaller_than_divisor ;;   been collected in A, go on with collecting more bits.
            d__found_set_bit:                ;;
                SUB A,C                      ;; Reduce collected bits.
                INC L                        ;; Build up quotient. This will not interfere with the use as dividend.
            d__smaller_than_divisor:         ;;
        DJNZ d__loop                         ;;
    POP BC                                   ;;
    RET                                      ;;



binary_to_bcd:
    ;; Use "double dabble" algorithm to convert a binary number to BCD format.
    ;; See <https://en.wikipedia.org/wiki/Double_dabble> for more details.
    ;;     Input:
    ;;         A - Binary number in range 0..99.
    ;;     Output:
    ;;         A - BCD number in range 0..99.
    ;;         * - Everything else preserved.
    PUSH BC : PUSH HL                         ;;
        LD H,0 : LD L,A : SLA L               ;; Store binary number (7 bits) on right/lower side.
        LD B,7                                ;; Number of bits to process; loop counter.
        btb__loop:                            ;;
            LD A,H : AND A,#0F                ;; Get lower BCD digit.
            CP A,5 : JR C,$+4 : ADD A,3       ;; If greater or equal to 5, add 3.
            LD C,A                            ;; Save in C.
            LD A,H : AND A,#F0                ;; Get higher BCD digit.
            CP A,5*16 : JR C,$+4 : ADD A,3*16 ;; If greater or equal to 5, add 3.
            ADD A,C : LD H,A                  ;; Store both BCD digits in H.
            ADD HL,HL                         ;; Shift bits.
        DJNZ btb__loop                        ;;
        LD A,H                                ;; Move result to A.
    POP HL : POP BC                           ;;
    RET                                       ;;



print_string:
    ;; Input:
    ;;     HL - Address of a zero-terminated string.
    ;; Output:
    ;;     HL - Changed.
    ;;     A  - Changed.
    ;;     *  - Everyting else preserved.
    ps__loop:               ;;
        LD A,(HL) : INC HL  ;; Read next character.
        OR A,A : RET Z      ;; If zero, leave routine.
        CALL os__txt_output ;; Print character.
    JR ps__loop             ;;



print_number:
    ;; Input:
    ;;     A - Number.
    ;; Output:
    ;;     A - Changed.
    ;;     * - Everyting else preserved.
    CALL binary_to_bcd                              ;; Convert to BCD.
    PUSH AF                                         ;; Save lower digit.
        SRL A : SRL A : SRL A : SRL A               ;; Shift upper to lower digit.
        CALL NZ,pn__x                               ;; Print out upper digit, if not zero.
    POP AF                                          ;; Restore lower digit.
    pn__x:                                          ;;
        AND A,#0F : ADD A,"0" : CALL os__txt_output ;; Mask lower digit, convert to character, and print.
        RET                                         ;; Return to above call or to caller.



pad_number_with_space:
    ;; Input:
    ;;     A - Number.
    ;; Output:
    ;;     * - Everyting preserved.
    CP A,10 : RET NC
    PUSH AF
        LD A," " : CALL os__txt_output
    POP AF
    RET



pad_number_with_zero:
    ;; Input:
    ;;     A - Number.
    ;; Output:
    ;;     * - Everyting preserved.
    CP A,10 : RET NC
    PUSH AF
        LD A,"0" : CALL os__txt_output
    POP AF
    RET



os__scr_set_mode EQU #BC0E
    ;; Input:
    ;;     A - Screen mode in range [0..2].



os__txt_set_column:
    ;; Input:
    ;;     A - Column, one-based.
    ;; Output:
    ;;     * - Everyting preserved.
    PUSH AF : PUSH HL
        CALL #BB6F
    POP HL : POP AF
    RET



os__txt_set_cursor:
    ;; Input:
    ;;     H - Row, one-based.
    ;;     L - Column, one-based.
    ;; Output:
    ;;     * - Everyting preserved.
    PUSH AF : PUSH HL
        CALL #BB75
    POP HL : POP AF
    RET



os__txt_output EQU #BB5A
    ;; Input:
    ;;     A - Character to write at current cursor location.
    ;; Output:
    ;;     * - Everyting preserved.



os__km_read_char EQU #BB09
    ;; Input:
    ;;     -
    ;; Output:
    ;;     flag.C - If C, then key is returned.
    ;;     A      - If C, this is the returned key.
    ;;     *       - Everyting else preserved.



row_and_column_headers:
    ;;    12345678901234567890123456789012345678901234567890123456789012345678901234567890
    DEFB "  delay   FPS   speed [px/frame] :  1|00 (use keys LEFT and RIGHT)", 13, 10
    DEFB "[frames] [Hz]", 0



speed:
    ;; This stores the current speed in 1/256 bytes/frame.
    ;; Range is 1..511.
    DEFW 32 ;; Start value 32 corresponds to 1 pixel/frame.



position:
    ;; This stores the current sprite position in 1/256 bytes.
    ;; Range is 0..63*256.
    DEFW 0



sprite_table:
    sprite__counter EQU 0
    sprite__position EQU 1
    REPEAT number_of_sprites
        DEFB 1 ;; Current counter. Counting downwards.
        DEFB 0 ;; Sprite position in bytes. Range 0..63.
    REND

roudoudou

#1
Arcade has a very few number of sprites (even today) BUT they are moved at each frame, the same sprite may be displayed 10 times (but moved a few pixels each frames) before the new one. It's obvious when there is the slow-mo time in fighting games. I don't know if there is arcade animation better than 7 fps

Back in days, the game Flashback was astonishing because there is a LOT of sprites for each action
ref : https://www.spriters-resource.com/resources/sheets/37/40146.png?updated=1460957713

My point is "it's not all about fps" :D

lightforce6128

The game Flashback is on a whole other level. It is impressing to see the number of animation steps for the main character.

Old 8-bit systems have several limits, especially computation power and available memory. A high frame rate needs high computation power (or clever programming). A high number of animation steps needs a big bunch of memory (or clever compression strategies). Sadly, for the CPC there exist many games that combine both: low frame rate and the minimum of animation steps (two). But there are remarkable exceptions.

Powered by SMFPacks Menu Editor Mod