You should use code in repository as source of truth. Code in article don't describes all steps, don't contains all used code but it's shows you most important points and aproaches that was used.
If you don't understand Z80 mnemonics you can use this page. But it's describes only vanilla Z80 behavior. We have eZ80 CPU that have some unimplemented undocumented Z80 commands and have some extensions. But basicly code will work all the same way.
About basic difference between Z80 and eZ80 you can read here.
Also, I always used prefixed
RST.LIL command(I'm talking about
.LIL prefix) - it's important in ADL mode.
.LIS prefix valid ONLY for legacy mode and not valid for ADL(according eZ80 documentation). I know that you seen ADL code with
.LIS prefix but it works only by miracle(it's undefinded behavior). I strongly don't recommend use
LIS prefix in ADL code for MOS calls.
If you making ADL applications - it is important use
.LIL prefix for every system call.
Part that should be made on bigger computer(just cause we have no enough tools on Agon yet)
We have couple preparation steps that should be made on PC or Mac - drawing graphics for game and preparing levels.
This task CAN be solven on Agon Light but currently we have no enougth tools for it.
They should be written but game was made earlier.
How to prepare your graphics?
There couple ways for preparing graphic tiles for games.
I've used piskel editor for drawing sprites.
It's free, it's comfortable enough and available for all major platforms(also it avaialble online from browser).
I'm using 16 colours pallete for sprites and Agon specific 16 colours palette can grabbed here. It's ready to use pallete - just install it to piskel editor and you'll got safe to use colours.
I've used 16 by 16 sprite sizes but it's matter only on rendering stage(it can be almost any size if it will fits into VDP's ram).
I've drawn sprites by hand and exported every sprite as sepparate pngs. And don't forget for non transparent empty bitmap(we'll use it for erasing tiles).
You have your own artwork but you can't just include PNG file into your assembly code.
There're several ways to convert your images to acceptable RGBA format.
I'm macOS user and I've used ImageMagick for converting graphics.
For converting single PNG to sprite enough simple call of
convert your-image.png sprite.rgba and you'll got ready to use RGBA bitmap. That can be used by VDP and our code.
But we have a bit more than single image. Yes, you can write small script that convert every image by image. Or you can special tool from imagemagick package that called
mogrify. And after single call of command
mogrify -format rgba *.png we'll got bunch of
.rgba files that ready to use bitmaps.
Another way is saving raw images from GIMP(that also crossplatform).
Defining level requirements
We'll using binary level representation not only for storing where object X or Y placed but also for storing state of some object.
It's easy make with simple and good working tool CharPad. It was made for C64 but it's good enough for almost any retro computer. Only one issue for me - it have no macOS version but it works fine under Wine.
We should define what kind of tiles we need(and tiles here also describes state of some objects).
Let's try find:
EMPTY SPACE. Let it be zero tile - it's easier to check in code(AND A/OR A is enough for checking this kind of tile).
PLAYER position(or starting point) - better mark it with some character styles symbol
ROCK and it have couple states: standing in place and falling(it required for correct gravitation behavior)
GROUND that can be destroyed by player
CRYSTAL that also have couple states(standing and falling)
... And GHOST - it have 4 states(by movement direction - it can fly left, right, up and down).
You can find file that I've used in game's repository.
It was from my another attempt of making same game but for Jupiter Ace but still good enough for our tasks.
Another requirement is map size - it should be 32x23(for our engine).
Now you can create your own levels(or edit my levels) - just use binary import and export for map. Exported level should be 736 bytes. It's small count and we even can forget about compressing it(on 512K ram beast).
Part where we'll go to our Agon Light and forget about world of bigger machines and light speed computing.
We'll need latest ez80-asm on our Agon Light(just copy
/mos/ directory of Agon's SD card) and Nano text editor.
Basic prerequirements - we should have our bitmaps(converted) and at least one level in our source root.
All basic initialization(and MOS application header) let's write in our
crt.inc file - it'll content basic entry point(code window can be scrolled):
ASSUME ADL=1 ;; Say to our assembler that we working in 24 bit addressing - honestly it isn't necessary here but I've used it org $40000 ;; 0x40000 is address where user ram is starts - all applications should be loaded here jp _start ;; this is first instruction that will be executed in our binary - let's go to our startup code align 64 ;; From offset 64 should be placed header that will describe how to execute our application db "MOS" ;; Magic word - MOS finds executables by this mark db 00 ;; Version of header db 01 ;; We using ADL so this byte should be equals 1. If we'd developed legacy mode application this byte should be zeroed _start: push ix ;; I've found that we should preserve index registers or system can crash push iy ;; call _main ;; Call our game's code pop iy ;; Restoring them pop ix ;; ld hl,0 ;; HL contains error code - we'll think that we never got error ret ;; Constants mos_getkey: equ $00 mos_sysvars: equ $08 ;; Macro that helps execute macro MOSCALL func ld a, func rst.lil $08 endmacro
I've commented a lot almost all lines of init file - so I wish it don't require too many focus better use this time for more interesting part - VDP routines and couple small tricks.
And in our main source(
rokky.asm or call what you like) file we'll include it:
include "crt.inc" ;; Here starts our game's code _main: ret
After assembling it with
asm rokky.asm - we'll got application that starts and exits successfully.
Most important thing that I should say in this article is about how VDP works in Agon.
VDP is just serial terminal that works on very fast UART. If you'll sent byte that contains letter via UART - you'll got this letter on screen. If you'll send some control sequence to this UART - it will be executed as command.
Most important thing to understand:
- There aren't difference between text, control sequences(VDU commands) and sprites data - it's just bytes that we'll send over UART
So if we'll read in documentation something like "Use
VDU 12 for cleaning screen" it means only one thing - just send byte 12. Technically, there no difference between letters and VDU commands, they are just bytes. That all. And this opens door for our first optimisation - we'll init our screen mode, hide cursor, upload all sprites and clean screen by one request to MOS.
For uploading big bunches of data we shouldn't use many
RST $10(putc) calls for every byte we can prepare all data in single packet and send it via single
RST $18 call. Less call we'll make - less CPU cycles we'll got overhead.
If you'll read VDP's manual for uploading single bitmap to VDP we should select it, prepare upload command and only after it send bitmap byte by byte. It's cost around 4-5 lines of code per sprite - hard to read and not looks too clear. But our ez80 assembler is macro assembler. Macroses came to our life for simplifing similar tasks.
We have one important thing - all our bitmaps are 16 by 16 pixels, so we'll have almost the same code for uploading bitmap. Changes only number of bitmap and file name.
So we can prepare macros:
macro DEFSPRITE num, file db 23, 27, 0 db num db 23, 27, 1 dw 16, 16 incbin file endmacro
This macros prepares all dirty work for us - it selects bitmap with number
num, it sends command for uploading bitmap(important thing - bitmap size are 16 bits sized) and includes bitmap.
So our VDP initialization can look like:
BMP_NOTHING: equ $00 BMP_WALK_R0: equ $01 ;; ... ;; Skipped a bit ;; ... vdp_init: ld hl, init_cmd ;; Setting start address for our VDP commands ld bc, init_cmd_end-init_cmd ;; Setting packet lenght rst.lil $18 ;; And sending them to VDP ret init_cmd: db 22, 1 ;; Setting video mode 1(similar to "VDU 22,1" command) ;; Here goes including our sprites "BMP_...." names are constants that we'd set several lines before DEFSPRITE BMP_NOTHING, "imgs/nothing.rgba" DEFSPRITE BMP_WALK_R0, "imgs/walk_r0.rgba" ;; ... skipped a bit ... db 12 ;; Cleaning screen init_cmd_end:
Just one MOS call(
RST $18) and we setted video mode, prepared screen and uploaded all bitmaps to memory. It's a lot quicker than sending byte by byte(and a lot more faster than sending them via our local functions-wrappers).
Of cause, if you want use different sized bitmaps - you can extend macros and set width and height parameters in it.
Loading bitmaps is good but why we want load it? For drawing. For this task we need 2 routines - one selects bitmap, another draws it on screen.
Select is dead simple:
; A - number bmp_select: ld (@bmp), a ;; Labels started with "@" are local. We're storing bitmap number to our packet ld hl, @cmd ;; Setting our command start ld bc, @end-@cmd ;; Setting our command lenght rst.lil $18 ;; Sending our packet by one request ret @cmd: db 23, 27, 0 @bmp: db 0 @end:
Drawing routine a bit more complex but still easy to understand. It's uses per tile coordinates. If you want use per pixel coordinates - just drop out multiplications.
; DE - Coordinates(reg E - x, reg D - y) bmp_draw: ;; X coordinate(per tile) ld b, 16 ld c, e mlt bc ; BC = B * C ld a, c ld (@c_x), a ;; Storing BC to command ld a, b ld (@c_x+1), a ;; Y coordinate(per tile with offset - skiping first text line - for game status) ld b, 16 ld c, d mlt bc ;; BC contains Y * 16 ld hl, 8 add hl, bc ;; HL = BC+8; 8 pixels offset from top of screen ld a, l ld (@c_y), a ld a, h ld (@c_y+1), a ;; Storing HL to command ld hl, @cmd ld bc, @end-@cmd rst.lil $18 ;; Sending command to VDP ret @cmd: db 23, 27, 3 @c_x: dw 0 @c_y: dw 0 @end:
You can try draw some tiles on screen:
ld a, BMP_WALK_R0 call bmp_select ld de, $0101 call bmp_draw ld de, $0404 call bmp_draw
We in one step to drawing our map.
Drawing tile on map
First important thing that we should implement is procedure that will convert address in our level buffer to coordinates on screen.
Level have some offset - it can be removed just by subtraction of address of tile from address of buffer.
As we said before - our level have 32 pixels width. So we should implement division by 32 with reminder.
Reminder(X coordinate) part can be easily found by bitmask - just by using
AND operand. For division by 32 used optimal code that implements division correctly.
; Input: HL - addr ; In DE will be coordinates on screen ; Basicly removing level_buffer offset and division by 32 with reminder addr_to_coords: push hl push af ld de, level_buffer or a sbc hl, de ;; Now HL is offset from level_buffer ;; We have 32 columns(can be found just by and 31) ld a, l and 31 ld e, a ;; And next code just implements quick division by 32 xor a add hl, hl rla add hl, hl rla add hl, hl rla ld d, h pop af pop hl ret
We'll need replace tiles on our map(and render them), so for this task and for basic render I've let make single operation -
map_poke. Basicly it founds replacement(can be found in
render.inc file) in dictionary
BITMAPS(animations performed in same way), selects required bitmap, calculated on screen coordinates and calls
bmp_draw(that draws bitmap).
It will be useful on many stages later but it also simplifies map rendering function to simple loop:
;; Draws buffer to screen draw_buffer: ld a, 12 call putc ld hl, level_buffer ld bc, 736 @loop: push bc push hl ;; We just walking thru the map and poking their bytes where they was ld a, (hl) call map_poke pop hl inc hl pop bc dec bc ld a, b or c jr nz, @loop ret
When we'll implement our game life cycle we'll use this
map_poke routine for updating on screen only changed tiles.
But it will be in next article!