A bouncing ball

A 4 by 4 cell ball moves across a 32 by 32 Screen and bounces when it reaches the sides of its play area. Row 31 is reserved for a bar that shows elapsed program time, so the ball occupies only rows 0–30. Select Open in editor, then Build and Run to watch it. Press Stop when you are done; the program keeps running until then.

The # @screen line connects the Screen to the memory labelled display. Each word there is one coloured cell. With 8 display pixels per cell and a 256 by 256 display, the grid has 32 columns and 32 rows. A rectangle at (x, y) begins at byte address display + (y * 32 + x) * 4.

fill_rect takes x, y, width and height in $a0–$a3. It reads the colour from $s0 and the address of display from $s1. It returns immediately for a nonpositive width or height; for a positive size, the caller must keep the whole rectangle inside the grid.

# @screen unit=8 width=256 height=256 base=display
.eqv SIDE 32                # words across and down
.eqv BALL 4                 # the ball, in words
.eqv LIMIT_X 28             # rightmost ball cell is column 31
.eqv LIMIT_Y 27             # bottom ball cell is row 30; row 31 is the bar
.eqv FRAME 60               # milliseconds per frame
.eqv BACKGROUND 0x00101820
.eqv BALLCOLOUR 0x00FFCC33
.eqv BAR 0x00808080

.data
display: .space 4096        # SIDE * SIDE words, four bytes each

.text
.globl main

# fill_rect(x, y, w, h): $a0-$a3 hold the rectangle;
# $s0 holds the colour and $s1 holds the grid base
fill_rect:
    blez $a2, rect_done     # no cells for a nonpositive width or height
    blez $a3, rect_done
    move $t0, $a1           # row = y
    add $t1, $a1, $a3       # one past the last row
rect_rows:
    sll $t2, $t0, 5         # row * SIDE
    add $t2, $t2, $a0       # + x
    sll $t2, $t2, 2         # four bytes per word
    add $t2, $t2, $s1
    move $t3, $a2           # how many still to draw across
rect_cols:
    sw $s0, 0($t2)
    addi $t2, $t2, 4
    addi $t3, $t3, -1
    bnez $t3, rect_cols
    addi $t0, $t0, 1
    blt $t0, $t1, rect_rows
rect_done:
    jr $ra

main:
    la $s1, display
    li $s0, BACKGROUND      # paint the whole grid once, and once only
    li $a0, 0
    li $a1, 0
    li $a2, SIDE
    li $a3, SIDE
    jal fill_rect

    li $s2, 6               # x
    li $s3, 9               # y
    li $s4, 1               # dx
    li $s5, 1               # dy

frame:
    li $s0, BACKGROUND      # erase the ball where it was
    move $a0, $s2
    move $a1, $s3
    li $a2, BALL
    li $a3, BALL
    jal fill_rect

    add $s2, $s2, $s4       # move it
    bltz $s2, flip_x
    ble $s2, LIMIT_X, x_done
flip_x:
    sub $s4, $zero, $s4     # turn it round at the edge
    add $s2, $s2, $s4
    add $s2, $s2, $s4
x_done:
    add $s3, $s3, $s5
    bltz $s3, flip_y
    ble $s3, LIMIT_Y, y_done
flip_y:
    sub $s5, $zero, $s5
    add $s3, $s3, $s5
    add $s3, $s3, $s5
y_done:

    li $s0, BALLCOLOUR      # and draw it where it is now
    move $a0, $s2
    move $a1, $s3
    li $a2, BALL
    li $a3, BALL
    jal fill_rect

    li $v0, 30              # service 30: elapsed milliseconds in $a0
    syscall
    li $t4, 100
    div $a0, $t4
    mflo $t5                # tenths of a second
    li $t4, SIDE
    div $t5, $t4
    mfhi $s6                # wrapped at the width of the grid

    li $s0, BACKGROUND      # the bar along the bottom row
    li $a0, 0
    li $a1, 31
    li $a2, SIDE
    li $a3, 1
    jal fill_rect
    beqz $s6, no_bar
    li $s0, BAR
    li $a0, 0
    li $a1, 31
    move $a2, $s6
    li $a3, 1
    jal fill_rect
no_bar:

    li $v0, 32              # service 32: wait $a0 milliseconds
    li $a0, FRAME
    syscall
    j frame

The loop erases the old ball, moves its upper-left corner, draws it again, updates the bar, and waits. The background is painted once before the loop. Later frames change the moving ball and the bottom row, leaving the rest of the grid alone. Stores to display appear on the Screen as they happen, so the ball can briefly be partly erased or drawn. Touching fewer cells makes that interval shorter.

$s2 and $s3 hold the ball's upper-left column and row. $s4 and $s5 hold its steps, each either 1 or -1. The ball is four cells wide, so its left edge can reach column 28 and still cover only columns 28–31. Its top edge can reach row 27 and cover rows 27–30 without covering the bar. These are the reasons for LIMIT_X and LIMIT_Y. After a step crosses either limit, sub reverses that step; the two following add instructions undo the crossing and take one step back into the play area. For example, moving right from x = 28 first gives 29, then reflects to 27.

The bar uses two clock services. Put 30 in $v0 and call syscall to receive elapsed program milliseconds in $a0. div $a0, $t4 divides by 100; mflo copies the quotient from lo, giving tenths of a second. A second div divides that count by 32, and mfhi copies the remainder from hi into $s6. That remainder is a width from 0 to 31, so the bar grows and then starts again. When it is zero, beqz skips drawing the coloured bar. fill_rect would also do nothing with a zero width, but skipping the call avoids needless setup. Put 32 in $v0 and the frame delay in $a0 to wait that many milliseconds of program time before the next frame.

Try changing .eqv FRAME 60 to .eqv FRAME 200 in the editor. Build and Run again. The ball still moves one cell each frame, but the frames are farther apart, so it moves more slowly. The bar uses elapsed time, so it advances farther between frames. Restore FRAME to 60 afterward.

Then try a bounce near both limits. Change the initial li $s2, 6 to li $s2, 27 and li $s3, 9 to li $s3, 26; leave both steps at 1. Before running, predict the next two positions of the upper-left corner. Build and Run to see the ball reach the right and bottom edges, then turn back. The positions are (28, 27) and (27, 26): the first step reaches both limits, and the second tries to cross them, so both steps reverse. You can change FRAME to 200 again if you want more time to watch each position.