Functions
So far, we have only written a single “flow” of code, but we can already spot some snippets that look redundant. Let’s use functions to “factor out” code!
For example, in three places, we are copying chunks of memory around.
Let’s write a function below the jp Main
, and let’s call it Memcpy
, like the similar C function:
; Copy bytes from one area to another.
; @param de: Source
; @param hl: Destination
; @param bc: Length
Memcopy:
ld a, [de]
ld [hli], a
inc de
dec bc
ld a, b
or a, c
jp nz, Memcopy
ret
The new ret
instruction should immediately catch our eye.
It is, unsurprisingly, what makes execution return to where the function was called from.
Importantly, many languages have a definite “end” to a function: in C or Rust, that’s the closing brace }
; in Pascal or Lua, the keyword end
, and so on; the function implicitly returns when execution reaches its end.
However, this is not the case in assembly, so you must remember to add a ret
instruction at the end of the function to return from it!
Otherwise, the results are unpredictable.
Notice the comment above the function, explaining which registers it takes as input. This comment is important so that you know how to interface with the function; assembly has no formal parameters, so comments explaining them are even more important than with other languages. We’ll see more of those as we progress.
There are three places in the initialization code where we can use the Memcpy
function.
Find each of these copy loops and replace them with a call to Memcpy
; for this, we use the call
instruction.
The registers serve as parameters to the function, so we’ll leave them as-is.
Before | After |
---|---|
|
|
|
|
|
|
In the next chapter, we’ll write another function, this time to read player input.