Using devkitSMS, it is possible to write game code using the C language rather than pure Z80 assembly. Therefore, we would now like to extend this knowledge and write more detailed programming sample.
Let's check it out!
Software
Follow all instructions from the previous post: this documents how to setup the pre-requisite software.
Note: ensure you have downloaded and installed the devkitSMS and Small Device C Compiler [SDCC].
Also download vgm2psg.exe required to convert VGM files to the Programmable Sound Generator [PSG].
# Clone PSGlib cd C:\GitHub\sverx git clone https://github.com/sverx/PSGlib.git
Demo
The devkitSMS programming sample is based on the Candy Kid Demo coding competition entry for 2017. All graphics, sprites, music, sound fx, coding logic etc. here is based on the devkitSMS tutorial by @sverx.
Create folder C:\CandyKidDemoSMS. Create the following sub-folders: asm, crt0, dev, gfx, lib, psg, utl
Folder | File | Location |
asm | Opcodes.dat smsexamine.exe |
C:\smsexamine1_2a C:\smsexamine1_2a |
crt0 | crt0_sms.rel | C:\Github\sverx\devkitSMS\crt0 |
lib | SMSlib.h SMSlib.lib PSGlib.h PSGlib.rel |
C:\GitHub\sverx\devkitSMS\SMSlib\src C:\GitHub\sverx\devkitSMS\SMSlib C:\GitHub\sverx\devkitSMS\PSGlib\src C:\GitHub\sverx\devkitSMS\PSGlib |
utl | folder2c.exe ihx2sms.exe vgm2psg.exe |
C:\GitHub\sverx\devkitSMS\folder2c C:\GitHub\sverx\devkitSMS\ihx2sms C:\GitHub\sverx\PSGlib\tools |
Development
Launch Visual Studio 2008. File | New | Project... | Visual C++ | Win32 | Win32 Project
Name: | Game |
Location: | C:\CandyKidDemoSMS\dev |
Create directory for solution | UNCHECKED |
Application type: | Console application |
Additional options: | Empty project CHECKED |
Graphics
Follow all instructions from the previous post to download and install BMP2Tile utility to convert graphics into Sega Master System format. Download gfxcomp_stm.dll STM compressed format to output directory.
Splash
Here is an example of background tiles that exhaust the full 256 x 192 resolution e.g. the Splash screen:
Choose "Tiles" tab. Ensure that "Remove duplicates" and "Planar tile output" are both checked.
Click Save button | File name splash (tiles).psgcompr | File type PS Gaiden (*.psgcompr)
Choose "Tilemap" tab. Leave "Use sprite palette" and "In front of sprites" options both unchecked.
Click Save button | File name splash (tilemap).stmcompr | File type Sverx's TileMap (*.stmcompr)
Choose "Palette" tab. Leave the "Output hex (SMS)" option checked for Sega Master System.
Click Save button | File name splash (palette).bin | File type Binary files (*.bin)
Here is basic code sample to render the splash screen in main.c with corresponding updated build.bat
main.c
#include "..\lib\SMSlib.h" #include "gfx.h" #define SPLASH_TILES 144 void engine_content_manager_splash() { SMS_loadPSGaidencompressedTiles(splash__tiles__psgcompr, SPLASH_TILES); SMS_loadSTMcompressedTileMap(0, 0, splash__tilemap__stmcompr); SMS_loadBGPalette(splash__palette__bin); } void main (void) { engine_content_manager_splash(); SMS_displayOn(); for (;;) { SMS_waitForVBlank(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
build.bat
@echo off ..\utl\folder2c ..\gfx gfx sdcc -c -mz80 gfx.c sdcc -c -mz80 main.c sdcc -o output.ihx -mz80 --data-loc 0xC000 --no-std-crt0 ..\crt0\crt0_sms.rel main.rel ..\lib\SMSlib.lib gfx.rel ..\utl\ihx2sms output.ihx output.sms output.sms
Background
Repeat the process for background tiles e.g. Font however save tilemap as uncompressed binary format [bin] to access data randomly. Save tiles in PS Gaiden compressed format + use same palette as above.
Tiles | font (tiles).psgcompr | PS Gaiden (*.psgcompr) |
Tilemap | font (tilemap).bin | Raw (uncompressed) binary (*.bin) |
Palette | font (palette).bin | Raw (uncompressed) binary (*.bin) |
main.c
#include "..\lib\SMSlib.h" #include "gfx.h" #define FONT_TILES 16 #define TEXT_ROOT 33 // 33 is "!" in ASCII. void engine_content_manager_load() { SMS_loadPSGaidencompressedTiles(font__tiles__psgcompr, FONT_TILES); SMS_loadBGPalette(font__palette__bin); } void engine_font_manager_draw_text(unsigned char* text, unsigned char x, unsigned char y) { const unsigned int *pnt = font__tilemap__bin; unsigned char idx=0; while ('\0' != text[idx]) { signed char tile = text[idx] - TEXT_ROOT; SMS_setNextTileatXY(x++, y); SMS_setTile(*pnt + tile); idx++; } } void main (void) { char *text = "HELLO"; engine_content_manager_load(); engine_font_manager_draw_text(text, 0, 0); SMS_displayOn(); for (;;) { SMS_waitForVBlank(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Sprites
Repeat the process for sprite tiles however no need to save the tilemap file. Also, introduce sprite code:
Tiles | sprites (tiles).psgcompr | PS Gaiden (*.psgcompr) |
Palette | sprites (palette).bin | Raw (uncompressed) binary (*.bin) |
main.c
#include <stdbool.h> #include "..\lib\SMSlib.h" #include "gfx.h" #define SPRITE_TILES 80 #define KID_BASE_TILE SPRITE_TILES + 0 void engine_content_manager_load() { SMS_loadPSGaidencompressedTiles(sprites__tiles__psgcompr, SPRITE_TILES); SMS_loadSpritePalette(sprites__palette__bin); } void engine_sprite_manager_draw(unsigned char x, unsigned char y, unsigned char tile) { SMS_addSprite(x+0, y+0, tile+0); SMS_addSprite(x+8, y+0, tile+1); SMS_addSprite(x+0, y+8, tile+8); SMS_addSprite(x+8, y+8, tile+9); } void main (void) { unsigned char kidX = 32; unsigned char kidY = 32; unsigned char kidColor = 0; unsigned char kidFrame = 0; unsigned char kidTile = KID_BASE_TILE + ((kidColor * 2) + kidFrame) * 2; SMS_setSpriteMode(SPRITEMODE_NORMAL); SMS_useFirstHalfTilesforSprites(true); engine_content_manager_load(); SMS_displayOn(); for (;;) { SMS_initSprites(); engine_sprite_manager_draw(kidX, kidY, kidTile); SMS_finalizeSprites(); SMS_waitForVBlank(); SMS_copySpritestoSAT(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Music
Download Mod2PSG2 music tracker to record music [FX] and convert output VGM files to PSG file format. File | Export module | VGM... | C:\CandyKidDemoSMS\psg\raw\music.vgm | Save
Now convert to PSG file format using vgm2psg.exe utility downloaded previously:
Start | run | cmd | cd C:\CandyKidDemoSMS
utl\vgm2psg.exe psg\raw\music.vgm psg\music.psg
Here is basic code sample to play the background music in main.c with corresponding updated build.bat
main.c
#include "..\lib\SMSlib.h" #include "..\lib\PSGlib.h" #include "gfx.h" #include "psg.h" #define MUSIC_PSG music_psg void main (void) { PSGPlayNoRepeat(MUSIC_PSG); SMS_displayOn(); for (;;) { SMS_waitForVBlank(); PSGFrame(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
build.bat
@echo off ..\utl\folder2c ..\gfx gfx ..\utl\folder2c ..\psg psg sdcc -c -mz80 gfx.c sdcc -c -mz80 psg.c sdcc -c -mz80 main.c sdcc -o output.ihx -mz80 --data-loc 0xC000 --no-std-crt0 ..\crt0\crt0_sms.rel main.rel ..\lib\SMSlib.lib ..\lib\PSGlib.rel gfx.rel psg.rel ..\utl\ihx2sms output.ihx output.sms output.sms
Sound
Repeat the process for sound effects however export VGM file to channel 2 and play on SFX_CHANNEL2:
Start | run | cmd | cd C:\CandyKidDemoSMS
utl\vgm2psg.exe psg\raw\sound.vgm psg\sound.psg 2 main.c
#include "..\lib\SMSlib.h" #include "..\lib\PSGlib.h" #include "gfx.h" #include "psg.h" #define SOUND_PSG sound_psg void main (void) { PSGSFXPlay(SOUND_PSG, SFX_CHANNEL2); SMS_displayOn(); for (;;) { SMS_waitForVBlank(); PSGSFXFrame(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Noise
Repeat the process for noise effects however export VGM file to channel 3 and play on SFX_CHANNEL3:
Start | run | cmd | cd C:\CandyKidDemoSMS
utl\vgm2psg.exe psg\raw\noise.vgm psg\noise.psg 3 main.c
#include "..\lib\SMSlib.h" #include "..\lib\PSGlib.h" #include "gfx.h" #include "psg.h" #define NOISE_PSG noise_psg void main (void) { PSGSFXPlay(NOISE_PSG, SFX_CHANNEL3); SMS_displayOn(); for (;;) { SMS_waitForVBlank(); PSGSFXFrame(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Input
Input detection is straightforward although it seems joypad variables must be static so to persist values:
main.c
#include "..\lib\SMSlib.h" void update(const unsigned int curr_joypad1, const unsigned int prev_joypad1) { if (curr_joypad1 & PORT_A_KEY_1 && !(prev_joypad1 & PORT_A_KEY_1)) { SMS_setSpritePaletteColor(0, RGB(2,2,2)); } } void main (void) { // Must be static to persist values! static unsigned int curr_joypad1 = 0; static unsigned int prev_joypad1 = 0; SMS_setSpritePaletteColor(0, RGB(3,3,3)); SMS_displayOn(); for (;;) { curr_joypad1 = SMS_getKeysStatus(); update(curr_joypad1, prev_joypad1); SMS_waitForVBlank(); prev_joypad1 = curr_joypad1; } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Pause
Many homebrew games do not query pause button although I believe this is good programming practice!
main.c
#include <stdbool.h> #include "..\lib\SMSlib.h" #include "..\lib\PSGlib.h" bool global_pause; void main (void) { global_pause = false; SMS_setSpritePaletteColor(0, RGB(3,3,3)); SMS_displayOn(); for (;;) { if (SMS_queryPauseRequested()) { SMS_resetPauseRequest(); global_pause = !global_pause; if (global_pause) { SMS_setSpritePaletteColor(0, RGB(1,1,1)); PSGSilenceChannels(); } else { SMS_setSpritePaletteColor(0, RGB(3,3,3)); PSGRestoreVolumes(); } } if (global_pause) { continue; } SMS_waitForVBlank(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Assembler
Ultimately the goal would be to write game code in Z80 assembly! However, one useful way to slowly transition from C language to Z80 assembly would be to wrap inline assembly blocks via C functions.
Let's integrate the Z80 assembly code inline to clear VRAM from Maxim Hello World example directly:
main.c
#include "..\lib\SMSlib.h" __sfr __at 0xBF VDPControlPort; __sfr __at 0xBE VDPDataPort; void engine_asm_manager_clear_VRAM() { __asm ld a,#0x00 out (_VDPControlPort),a ld a,#0x40 out (_VDPControlPort),a ld bc, #0x4000 ClearVRAMLoop: ld a,#0x00 out (_VDPDataPort),a dec bc ld a,b or c jp nz,ClearVRAMLoop __endasm; } void main (void) { engine_asm_manager_clear_VRAM(); SMS_displayOn(); for (;;) { SMS_waitForVBlank(); } } SMS_EMBED_SEGA_ROM_HEADER(9999, 0); SMS_EMBED_SDSC_HEADER(1, 0, 2017, 3, 17, "StevePro Studios", "Candy Kid Demo", "DESC");
Summary
Armed with all this knowledge, we are now in an excellent position to build complete video games for the Sega Master System that will be able to run on real hardware but now without any VDP graphics glitches!