Writing
GBA Assembly — 7. Moving a square
Combine input, a position in RAM and drawing to move a square, respect screen boundaries and erase the previous image without leaving trails.
We already know how to read buttons and draw at a regular pace. Let's combine them: a white eight-by-eight square will move over a black background using the D-pad.
The goal is still not to create a complete character. We will study position in RAM, per-frame updates, screen boundaries, and erasing the previous image. Use movimento.s, included in the example package.
Your route through this chapter
Before starting: you should be able to make A change the square's color in chapter 6. This chapter uses the D-pad instead.
- Save the complete
movimento.swith the supporting files, build it using the four commands below, and openmovimento.gba. - Check the emulator's mappings for all four directions. The initial white square is eight by eight pixels at
(16,16). - Hold Right, release it, then hold Down. Check that the square moves only while a direction is held and leaves a black background behind.
- Test each screen edge separately. Match the visible stop with the X or Y limit explained below, then trace the erase, update, and draw sequence.
Checkpoint: the square stops at all four edges and leaves no trail. If it leaves a trail, investigate erasing before changing input. If it never moves, repeat the input check from chapter 6. A larger speed or different square size is an optional experiment, not a requirement for the next chapter.
The object's position belongs to the program
In Mode 3, hardware does not maintain an object named “white square” with X and Y properties. It stores pixel colors. Our program must therefore maintain the position and decide which pixels to write.
We reserve two words in RAM: hero_x and hero_y. Startup puts 16 in each. They describe the upper-left corner of the square, whose width and height are eight pixels.
It helps to separate two questions:
| Question | Who answers |
|---|---|
| Where should the square be? | The program's variables and rules |
| What image is visible now? | VRAM contents interpreted by the video circuitry |
Changing hero_x alone does not move a pixel. We must use the new position when drawing. Likewise, drawing somewhere else does not automatically change hero_x.
Turning buttons into a position change
The commands are described under loads and stores, ADD and SUB, and conditions.
Here is the routine that updates the two coordinates:
<!-- source: en/movimento.s#movement -->update_position:
ldr r3, =hero_x
ldr r0, [r3]
ldr r3, =hero_y
ldr r1, [r3]
ldr r3, =keys
ldr r2, [r3]
tst r2, #16
addne r0, r0, #1
tst r2, #32
subne r0, r0, #1
tst r2, #64
subne r1, r1, #1
tst r2, #128
addne r1, r1, #1
cmp r0, #0
movlt r0, #0
cmp r0, #232
movgt r0, #232
cmp r1, #0
movlt r1, #0
cmp r1, #152
movgt r1, #152
ldr r3, =hero_x
str r0, [r3]
ldr r3, =hero_y
str r1, [r3]
bx lr
We load X, Y, and the button snapshot. We test each direction and perform a simple operation: Right adds one to X, Left subtracts one, Up subtracts one from Y, and Down adds one.
Conditional instructions use the flags from the preceding tst. addne means “add if the test was nonzero.” Without s, that addition does not update flags.
Finally, we store both coordinates back in RAM. Without those stores, the next update would begin from the old position again.
The example accepts two simultaneous directions. Right and Down change both axes. Right and Left cancel each other before applying boundaries. A larger game could choose a different rule, but that rule would have to be explicit in the program.
Why is the rightmost position 232?
The distinction between signed and unsigned comparisons is explained in the LT, GT, LO, and HS condition table.
The highest pixel X is 239. We are positioning the corner of an object eight pixels wide, however. To fit the whole object:
X maximum = 240 - 8 = 232
Y maximum = 160 - 8 = 152
With its corner at X 232, the final pixel is at 239. A corner at 239 would place the remaining pixels beyond the intended row.
The comparisons after the button checks clamp X to 0 through 232 and Y to 0 through 152. We use signed conditions because subtracting at a boundary can produce -1. These small, controlled values fit comfortably within that interpretation.
This protects fill_rect, which does not clip off-screen regions. Boundaries are rules enforced by the caller, not an automatic hardware safeguard.
Moving means erasing and drawing again
The difference between calling with bl and forwarding with b is explained in BL and BX.
The complete loop joining these steps is:
<!-- source: en/movimento.s#moving-loop -->main_loop:
bl wait_vblank
bl tick
bl read_keys
mov r4, #0
bl paint_hero
bl update_position
ldr r4, =0x7fff
bl paint_hero
b main_loop
First we draw black at the old position with paint_hero. Then we change the position and draw white at the destination. paint_hero reads both variables, sets width and height to eight, and forwards execution to fill_rect.
It ends with b fill_rect, rather than bl. This is a final forwarding step: it retains the lr received from its caller, and fill_rect returns directly to that caller. This form is optional; it avoids an intermediate call and return.
Removing the black drawing leaves a trail. VRAM retains old colors until something overwrites them. There is no automatic clearing between frames.
Erasing with black works on a black background. On a road or detailed scene, it would leave a hole. We would need to restore the background underneath or choose another graphics organization. In the Tower Defense article, the road is restored before drawing the current enemy.
Speed now has a unit
Each update changes position by one pixel on each pressed axis. Since every iteration waits for a new display frame, speed is approximately 59.73 pixels per second on each axis when the program keeps up with every update.
Changing the increment from one to two doubles that speed. It also changes which positions the object visits: it moves in two-pixel steps. For movement slower than one pixel per frame, a future improvement is to store positions with fractional parts using fixed-point numbers.
Moving along both axes travels farther diagonally than moving along only one. We accept that in this exercise. Correcting diagonal speed would be another movement rule, not something the D-pad does automatically.
Running and testing the boundaries
arm-none-eabi-as -mcpu=arm7tdmi -g -o movimento.o movimento.s
arm-none-eabi-ld -T gba.ld -o movimento.elf movimento.o
arm-none-eabi-objcopy -O binary movimento.elf movimento.gba
python3 rom.py movimento.gba
Open movimento.gba in mGBA. The square should appear at (16,16). Hold each direction and check that it stops at the edge without disappearing, wrapping around, or leaving a white trail.
For an exercise, make the object a 16-by-eight rectangle. Change its width in paint_hero and the maximum X to 224. Its height and maximum Y stay the same. Changing only the drawing leaves the boundary rule incorrect.
Then temporarily remove the call that erases the old position and observe the trail. Restore it before continuing. A small change with a predictable effect helps identify which part of the program controls each behavior.
Previous: reading the buttons. Next: adding a target and a win condition. Series index.