Devlog: Vixx (bullet hell game)
Posted: Wed Jan 27, 2021 12:12 am
Hello Everybody !
I wanted to make a post to share a project I started several days ago. I wanted to create a "Bullet Hell" type of game (it's a sub-genre of shoot'em ups games) for the X16, following a concept idea I got to control several sprites on screen.
This post will act as a devlog for this project. For the history, I discovered the genre two weeks ago and wanted to try making one for fun.
So here we go with the first issue of the devlog ! (it'll be long since I got a lot of code done already) Note that I'm not this good at programming in assembly, My only knowledge comes from some youtube videos (Game Mechanics Explained, 8-bit Show And Tell, ...) so I may not be aware of most best practices. (I would like to hear from you if you got some ! ? )
#1 26/01/2021: Game framework and first contact
So, before I start with the thing, here's some info on my working environment :
I work with ACME Assembler, because it's the first one I learned and I like the way it works so that's what I'm using.
I have some "libraries" I wrote for myself to declare every Kernal addresses, petscii special characters, some vera addresses, masks, macros, a "basic_startup" macro, some math macros (adc16, inc16, multiply, divide, ...) etc...
I usually start a project by creating a folder and some files in it, I like to start with "main.asm" and "paper.asm". The "main" file will contain the actual program, and the "paper" will contain the variables, constants and some comments on how the game will be structured.
Once I got my working environment ready, I started to think of how I'll handle the bullets on-screen. So at first I needed to design the bullet's datas.
To keep things simple I wanted to classify every moving object on screen as an "object". Each objects has 4 properties: a type, x and y coordinates and a parameter. The type will determine which code will handle the movements. The parameter will allow the code to know how to behave and also modify it if needed. The x and y coordinates are ... used as coordinates ¯\_(ツ)_/¯.
Okay, I got a structure for my objects. that's 6 bytes per objects counting the coordinates as 16 bits values. Let's see how I'll configure the VERA ... 640x480 is quite overkill so I downgrade it to 320x240. Playing with the coordinates I see that the low byte of x and y coordinates takes around 3/4 of the screen horizontally and the entire screen vertically. So I can cut off 2 bytes per objects. VERA allows up to 128 sprites on screen, so I note a hard limit of 128 objects. 128 objects means I'll take 512 bytes if the screen is full of bullets.
I drew a small 8x8 bullet sprite and on startup I initialize all VERA's sprites as this one. Now, how to handle the movements ?
I said earlier that I started this project with an idea in mind, so here it is : I have an array of addresses, and using JMP ($ABCD, X), I can access all of them. So that mean I can only store 128 types of objects in my game. That's enough for what I want.
So I have 512 bytes for the objects, and 256 bytes for the movement table. I can place them at the end of the fixed RAM ($9C00 to $9FFF). I also use one more byte ($9BFF) to store a count of how much bullets I got in my list (so I don't need to parse it in whole each time I need to).
I came with two loops to handle the objects : one that will move them, and one that'll refresh their position on screen. I wanted to move the objects on every frame but didn't want to bloat the VSYNC interrupt with it. So I created a wait_frame variable that'll be set once the movement has been handled, and will be reset when the VSYNC occured.
Here's the pseudo-code for the "move objects" loop :
Quote
Load Object structure into a fixed RAM location
Shift the type to the right once (to align with the 16bit addresses)
Place the type into the X register
Jump (movement_table, X) ; <- we will assume the movement routine selected will always return to movement_ended
movement_ended:
Save Object structure changes from the fixed RAM location back into the Object Array
Repeat until object_count
For the "display object" loop, it's even simpler : we take the coordinates and output them directly into VERA.
I implemented some simple movement functions : the first one is a "Null" movement, nothing moves. The second one is a "reset" movement, we force the position out of the screen and set its type as "Null".
The third one is a player controlled movement. We get the joystick buttons and move accordingly.
The next ones are basic constant movements. Since the parameter is only one byte, and the bullets does not need to move like a blue hedgehog, I assumed the left hex part will be the x movement and the right hex part will be the y movement. I firstly assumed that the value is pixels per frames, but I decided later to take the lowest bit of each hexadecimal number to make half-pixel movements (we move the bullet every odd frames).
You can't imagine how much I had to debug the thing and the weird behaviors I got because of some not-properly-set flag before a critical operation (I knew the carry was used for ADC and SBC, but not for ROL and ROR). Also the hard-to-reverse crashes because you exited a loop without pulling away a value you pushed on the stack. But the best reward was the system working properly at the end, and was even very generic at the end !
The next thing I decided to work with was the collisions. Bullet Hell games are easy to make on this side because pretty much everything has small round hitboxes. So after moving the bullets the next thing I do is checking for collisions against the player (I assumed the player will always be the first object on the array). So I take the player's coordinates, and loop through every other objects.
I first subtract the player's x coordinate with the current object's, check the carry to know if I have to check < 3 or > 252. If I match, I do the same check for the y coordinates and if there is a second match, the player has been hit ! (I still need to implement lives)
I later decided to award the player some points if he takes risks and go near bullets. So I added a little scoreboard on the right (discovered by the way the wonderful decimal mode of the 6502) and added a check for larger distances in my collision code to give the player points for each frame passed near a bullet.
On the way, I started to create some basic subroutines to add objects to the array (this will check for empty spaces and place the object in it, with a limit of 128 objects (objects inserted beyond the limit will be skipped), optimize the object's array by setting the count to the lowest value possible (freeing the processor as much as possible). I also added some boundary check for the movement functions that'll reset the bullets when they get offscreen. I also imported a RNG subroutine found on the web.
While playing with the insert routine, I went through a new problem : how to handle the bullets ? I'll need to create some on the fly, but I can't just use some random code for this. I need some sort of conductor to keep things clean and ease the creation of levels.
So I started to create the "Choreography" module. It'll live on the game loop (not the vsync loop), and instead of being run once per frame (as the movements), it'll be run as much as possible, but leaving the priority to movement's and collision's code.
The Choreography code will be an interpreter, with its own Program Counter, X and Y position, two A and B registers and a "mode" variable that'll just sit here doing nothing but will eventually get a meaning later.
The program counter will start at a fixed location in the code ($9000) and will travel it reading each bytes of opcodes and their parameters. The X and Y refers to a "cursor", it's the position where objects will be inserted next. it can move to absolute values as well as relative ones. It has two registers that'll be used mainly for looping and doing conditional jumps.
I created some opcodes : SLEEP to skip some frames, INSERT to insert object into the game, SETPOS to set the position of the cursor, MOVPOS to move it in a relative way, LOADA to load some value to the "A" register, LOADB for the "B" register, INCA and INCB to increment them, JMP to jump, JAZ "Jump when A is Zero", JAN "Jump when A is Not zero", and so on ...
I wrote some of this custom code and got a very nice result. I wanted to share this with you :

And that's All for this first loooooong devlog ! I'm a bit tired by writing all of this ^^' (it was around 1am when this was finished) so I'm not sure I'll double-check now if I got mistakes on this. I also wanted to share this with you in case you can get some ideas for your own projects. Another reason is I tend to easily drop projects by not being motivated in them anymore. I'm considering making this project open-source so you'll be able to review (if you're courageous) and continue it (for the courageous++) just in case. Tell me what you're thinking of this project, I'll be happy to talk with you further about it ^^