this blog post is about ΔV, a rhythm game i'm working on. you can find all posts about deltav here
what's it made out of?
Many decisions on this thing were made by 2021 me, and current me is often not very happy with them. Many of those 2021 decisions have already been fixed. However, 2021 me decided to use raylib, because he was tired of the minor annoyances of sdl2. Current me says she thinks raylib was a pretty good choice. Raylib is really nice and overall just very convenient, and feels more modern than sdl2 (because it is). I've made extensive use of sdl2 in both c and rust, and while the rust version is pretty nice, raylib still beats it. (haven't tried raylib in rust yet, though.) Especially the audio facilities are what makes this whole thing tick (literally, since the time used for timing enemy hits is the time coming from the sound playing...)
we take a slight diversion to talk about beatmaps
obviously, a rhythm game like this rests on the quality of beatmaps for different songs that exist for it. idk what unbeatable uses (wasn't going to go spelunking to find out) and the osu file format a) is horrifically illegible and b) only exists in c# so I just decided to go with a human-readable json deal. It's alright, but it sucks to create manually since you need to know the times at which enemies should be hit. I don't have a beatmap editor set up yet at the time of writing, but it's the next priority. The beatmap parser is written using cJSON at the moment. It was originally json.h but 2021 me made a poor decision with that one, and modern me has duly fixed it.
how the gameplay works
(i wrote this section and realized it's incredibly dense, sorry)
in this style of game, enemies move towards you along a line, and you hit them when they reach a certain point by pressing a key. in the beatmap, enemy hit timing is specified in milliseconds since the beginning of the song. since the window could be a lot of different sizes, instead of moving a set distance each frame, the enemies move a set percentage of the current length of the track they are on each frame. this percentage is arbitrarily chosen by me because it looks good. as i write this, i'm realizing this system is not very robust, because the music could easily become desynced with the gameplay, causing the enemies to arrive at the correct time relative to one another, but the wrong time relative to the song. whatever, later me problem. based on the percentage traveled each frame (arbitrarily chosen, remember) the amount of time that an enemy should last from spawn to perfectly-timed hit is calculated (resolution-independent, since it's based on percentage not pixels) every frame, the game checks the list of enemies to find the next X enemies that are supposed to be on screen right now, and then stops when they are outside of that time period (so we don't traverse possibly hundreds of enemies every frame). then, when the enemies reach a certain time margin around their specified hit time (which also coincides by chance with the position on the track that they can be hit) you can hit them and they disappear. Otherwise they hit you and they... disappear, because there is no current system for the player to take damage. There are multiple types of enemies: normal ones which follow this pattern exactly, disappearing enemies, which lose opacity based on the percentage that they have traveled, 'swap' enemies, which take 1 hit on one track at a certain time, and reset to a position x% of the way along another track to be hit again at a second time, and 'hold' enemies, which have 2 consecutive hits on the same track, one on the press of the track key and one on the release of said key. It must be held the entire time to avoid missing the enemy. If you are particularly sharp, you may have noticed that all of these enemies were stolen from unbeatable.
how the code is set up
there's not much of a game engine, just some rickety scaffolding with a game glued to it. It's separated into several engine-y parts, but still way too tied to the rest of the code.
- graphics (gfx.c) handles mostly ingame graphics, drawing enemies and the background as well as just initializing the draw stuff
- input (input.c) is just a wrapper to allow me to configure the inputs and easily tell what's going on since i've named them
- settings?? (settings.c) mostly concerned with inputs, lets me set each game input to a physical key input as well as tune the enemy velocity
everything else is less engine-y
- beatmaps (beatmap.c) is one function to parse beatmaps and one to deallocate them. wowza.
- enemies (enemy.c) deals with updating, spawning, and removing enemies. all the logic of them deciding when they can be hit and whatnot is in here.
- gameplay?? (gameplay.c) is where i separated the gameplay out into to make it cleaner. function for gameplay loop update and draw, as well as to start up gameplay (after a menu) and stop gameplay (to enter a menu)
- menu???? (menu.c) astonishingly large mess of code to handle switching between astonishingly few menus and make them actually do things
right now, the code creates a 4x3 space in the middle of the window to do primary drawing in. I went 4x3 because it makes the tracks pleasantly diagonal but not too perfect. i wanted to make the aspect ratio constant, so i use a scissor to make sure everything is drawn in only the 4x3 drawing area. everything else is just circles and lines, because i felt like it.
the menus only use the raylib default font, and honestly have some horrific code behind them. they do all work, though. there's the title screen, a select screen that
lets you pick a beatmap from the
beatmaps/ directory (made this one first because it was such a pain to have to manually change the path to the beatmap to load), and a settings
screen to change the framerate and the enemy speed. you can also (!!!) pause the game, which produces a semi-transparent menu drawn over the gameplay to let you either
resume, restart, or exit the level.