I’ve recently been upgrading what I now call my scripting system (was cutscene system, but it’s not just that any more), so I thought I’d detail how it works:
My ROM File System
Although the game started out using disk, I changed it to use cartridge due to the flexibility and speed of accessing a ROM vs disk. Currently I’m using a 512K cart, but like all carts, the ROM is only accessible in 8K chunks known as banks. That means a lot of bank switching to access the data! To ease the process I added a file system to access the cart. I build all the data as files, using PUCrunch to compress them, ensuring each final file is less than the 8K limit. My build tool then packs files together into 8K banks and builds a file directory from this. ROM bank #0 holds the directory structure (stored in the last 1K), each entry holding the ROM bank to use, plus a start address for the file. I don’t need to store the length as the decompressor doesn’t need to know it.
Oh, BTW, I use my own custom 6502 assembler (SJAsm), incorporated into my build system. By clicking a few buttons, I can build code/data, build cartridge/disk image (disk image used for save-games), plus auto-run VICE using both cart and disk images. I’m a bit lazy when it comes to tools and love automation, so no typing in shell commands for me! Just add some 6502 assembly to source files, click a few buttons, then run the game.
I’ve also coded my own custom map editor, but currently I’m still using SpritePad to edit all my sprites, letting my build tool do all the format conversion I need.
So, anyway, back to Scripts. My script system uses a byte stream of commands, performing such actions as setting sprite positions, opening/closing windows, displaying text, plus various specialised functions (including triggering combat). I’ve been adding to the command list recently, giving it the ability to manipulate the inventory, change various flags, even to force a screen refresh due to the block swapping system.
Multiple scripts are stored together in one script file to reduce the number of ROM files; for now my directory has a limit of 256 files, but I could easily extend that if needed by placing ROM files into separate categories: cutscenes, level graphics, gameplay, etc. Near the top of each script file is a table of addresses to each script so scripts are easy for the code to locate.
Script files are always loaded at the same address in RAM, currently $A000 in my memory map, and cannot exceed a 4K limit. If a script exceeds this limit, it can be split into separate parts, a special SCRCOM_chain byte command performing the link.
I should add that all script files (each being nothing more than a .asm file) are assembled as part of the main file, each file being split off into a separate code overlay. This way, any piece of code can access the symbols of another, extremely useful in scripts. In the past I’ve used jump tables at a fixed location to access functions in a block of code, but my current system is so much nicer/faster.
The script files are used for both cutscenes and other functions, basically anything that can be stripped out of the main game code; things like the various interaction handlers, an example of one being the open treasure chest handler. Code RAM is a precious commodity, so any saving is worthwhile. As an example, I’ve already split most of the combat system into an overlay, plus code for the status screen, and many other triggered interactions. I’m data-driving as much of the game as possible, and this includes the title screen, save game screen… Pretty much anything can be turned into a script (as long as it isn’t time-critical).
So, to trigger a script in code, I just need to set the ID in the A and X registers, and then call the chain_script subroutine. The ID is composed of two parts, the high byte holding the ROM file ID, the bottom byte containing the segment to run. In the sc_equ.asm file, it kinda looks like the following:
SCID_visit_branwen = ROMF_sc_branwen1<8 + 0
The ROM file IDs are auto-generated by my build system and stored in a file named romf_equ.asm.
So a script file looks something like the following:
overlay sc_branwen1, $a000 ; export code as overlay @ $a000.
… more code …
sc_talk branwen, .txt_001
All my script commands are actually asm macros. here’s an example of the sc_call script command:
byte SCRCOM_call, [%0]
BTW the square brackets tell my assembler I’m storing a 16bit value, low + high.
Scripts also have conditional branching, and can test various flags/data. The main type are what I call Enables. Enables is an array of 128 bytes and holds values that are testable by various game systems, namely triggers, interaction points, plus sprite output. Using Enables allows me to switch on/off various parts of the game. Enables are testable by the scrip system with the following commands:
If the testable condition is true, script can either goto another part of the same script file, or chain to another script in another script file. Enables can also be changed by the script system, thus possibly changing the flow of the game, useful also for handling sub-quests.
The game also uses an array of 1-bit flags I call general flags, and these too are testable, also having their own script branch commands. General flags can be set/cleared by either game code or scripts. There are 256 such flags, thus consuming just 32 bytes of the save-game area.
To further extend the branching ability, I added a script variable named ret, a return parameter (zero-page location labelled SCR_ret in 6502) that can be set by various methods.
One method to set ret is a call to external code by using the sc_call script command. When such a call is executed, ret is initially cleared, allowing the code to optionally modify it. Upon returning, the ret parameter can be checked by the following branching script commands:
Another method to set ret is to use the script command sc_get_mem. The command sc_get_mem can access any value in RAM and copy it to the ret parameter.
My trigger system also uses scripts, as does the interaction system. When the player hits a trigger, a script ID is fetched from a table and called. Typically the script called is short (so it loads fast), and contains chain instructions to other scripts. Having scripts able to decide and chain to other scripts is quite powerful, and helps to keep code RAM use to a minimum; I’ve managed to remove a lot of branching 6502 code handlers as a result.
The portal and doorway trigger systems also use the script system, thus freeing up more code RAM. They also use the Enables system, so I can enable/disable pretty much any type of game map interaction. And with scripts able to set/clear Enables, it means scripts can control other scripts.
And That’s It!
I feel I have a pretty powerful system right now, one that should allow me to finish the game. Currently, my gameplay side code has just under 5K RAM left, and that’s been pretty constant since I upgraded the script system and started adding loads of new scripts.
Upgrading the script system also allowed me to free up 0.5K of engine-side code, so I’m free to add more scripting commands if needed.
Thanks for reading, I hope it was interesting!
Any questions, let me know and I’ll try to answer them.