Posted on

Important remarks

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).

screenshot

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).

Preparing levels

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

  • unbreakable WALL

  • 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).

charpad

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 asm.bin and asm.ldr to /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.

Application initialization

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.

VDP

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 MAP TILES->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!

Stay connected!