Describing Bullet Hell: Declarative Danmaku Syntax
Bullet hell patterns can be incredibly complex, but as of today there isn’t really any way to succinctly describe them. The most basic element — the movement of a single object — is just a parametric equation, but everything on top of that is murky. How do you describe firing five bullets at the same time following the same parametric equation but slightly rotated for each successive bullet? How about firing five bullets over time? Or five bullets at the same time, fired five times over time? Or five bullets at the same time, fired twenty times over time but with gaps after each five fires?
As it turns out, trying to describe danmaku patterns in words is clunky. And trying to describe them purely in terms of low-level code is illegible at best. So, when writing up my engine, I tried to design it to turn complex pattern descriptions into basically pseudocode. I accidentally succeeded, and this post is the first in (hopefully) a series on how it works, and how it can be used to describe danmaku.
This post is a little bit technical — it describes architecture, but there’s no hardcore code. It should be general enough to be applied to any problem about describing compositional patterns.
0. Declarative Programming
Declarative programming is a paradigm in which programs describe what they do, and leave the actual implementation to some underlying engine. Very little code in software engineering is declarative, with one common exception: SQL. In SQL, you query a database by giving it a description, eg: “I want a list of all the cars that have red paint and were sold before 1994”. The user doesn’t define how this list should be acquired, they just describe the results they want.
What’s great about declarative programming is that a program written in a declarative language is its own description. This makes code easy to look over, and also ensures that the description has a similar structure to the code itself. The raw SQL code for the example above would be:
select * from CARS where PAINT="RED" and SALES_YEAR<1994;
This is the approach of my engine: by making the danmaku language declarative, any code for a pattern is simultaneously a human-readable semantic description. Furthermore, because it is declarative, it can be implemented in many underlying engines without modification. The most common Touhou fangame engine — Danmakufu — uses an imperative language and its concomitant deluge of for-loops and increments, so it’s next to impossible to figure out what code does except by running it, and it’s absurdly difficult to even port scripts between language versions. Declarative programming is the remedy.
1. An Example
Take a look at the small blue circles in the following pattern:
To describe it in words, we might say:
“Every few frames, spawn a blue circle slightly below the bottom of the screen at a random x-position between the left and right edges of the screen. It moves upwards at a speed that lerps (smoothly shifts) back and forth between slow and medium speeds periodically, and its starting point in this period is random.”
Let’s say that we’ll spawn every 8 frames from the position <0,-5>, the screen width goes from -6 to 6, and the two speeds are 0.4 and 0.7, which go back and forth every 4 seconds. Here’s how that’s written in my engine code (slightly modified for friendliness):
fire circle-blue <0,-5>
randomize-x -6 6
randomize-time 0 4
lerpfromtoback 0.5 1.5 2.5 3.5
Moving from the bottom up, we can see how this corresponds to the description:
- The two vectors
<0,0.4>, <0,0.7>describe the two velocity vectors we want to switch between.
lerpfromtoback T1 T2 T3 T4 V1 V2describes a function that lerps from V1 to V2, stays at V2 a while, and then lerps back to V1. In this case, V1 and V2 are the two vectors
<0,0.7>, while the numbers
0.5 1.5 2.5 3.5configure the times at which the function starts or stops shifting between the two.
periodize T Fdescribes a function that repeats the child function F every T seconds. So, after we've done one
lerpfromtoback, at t=4 we start doing it again. This handles the task of repeatedly shifting between the slow and fast speeds.
randomizetime T1 T2 Fdescribes a function that returns the child function F with a random time-shift between
T2. Every particle that uses this function will get a different random time-shift.
cartesian-velocity Findicates that we want to use the child function as a velocity descriptor in Cartesian space. Everything up to this point only returns a vector between
<0,0.7>-- we need to indicate at the top level how the particles should interpret this vector. Two similar methods might be
fire-particle Findicates that we want to fire a particle with the movement function F.
randomize-x X1 X2 Fwill randomize the initial x-position of the fired particles.
repeat-every T Fwill repeat the firing function every T frames.
Note how all of this code only describes what should happen. There’s nothing here that indicates what the machine should do to implement any of these features in any engine. This code could serve as a human-readable description, or it could be plugged into any engine that supports these functions (my engine being one of them).
Of course, there’s a lot of hand-wringing that goes into ordering these lines correctly — we’ll get to that!
2. Arbitrary Extensibility
The greatest problem with describing danmaku patterns is that there are too many ways that particles can be organized. If we make any assumptions about how something will be organized, then we need to rewrite much of our logic when the inevitable exception arises. For example, maybe we assume — as we did above with
repeat-every-- that when repeating functions over time, we wait the same number of frames between each invocation. But there could be a pattern which waits for 8 frames for the first 100 invocations, and then 6 frames for all invocations afterwards.
This contradiction between the patterns we want to create and the capabilities of our danmaku language is inevitable. Thus, the best strategy is to aggressively modularize each layer of our pattern description, so that when a new method of organization arises, we can write a tiny replacement function and extend, instead of replace, our previous architecture. For example, let’s say we wanted to do
repeat-every with a variable number of frames, as described above. We could simply add another method,
repeat-every-f, and use it as follows:
(6 if x > 100 else 8)
randomize-x -6 6
This new method would take a function instead of a single number, pass in the invocation number, and wait for the returned number of frames before the next invocation. How expensive is it to add support for this new function? Well, the danmaku language requires one new definition, and an engine would need about 10 lines of code — precisely due to the aggressive modularization of the architecture. To any other function,
repeat-every-f are just "functions that do something", so it doesn't matter to any other function if we swap one for the other.
I said previously that
repeat-every-f are just "functions that do something"-- but this isn't entirely accurate. Every word in our pseudocode description needs to say what kind of thing it is-- in other words, it needs to have a type. We said that
repeat-every would wait for some number of frames and repeat an action-- but for this to work, we need to know that
8 is a number, and not a "function that does something"!
Let’s say we have some type
AsyncFunction (for patterns that run over time) and another type
SyncFunction (for instantaneous patterns); the exact implementation of these types will depend on the underlying engine. What we want to express is that
repeat-every takes in a number (a frame counter) and a
SyncFunction, and spits out an
AsyncFunction. The type can be expressed as
(number, SyncFunction) => AsyncFunction.
repeat-every-f, our frame counter is instead a function that takes in a number and spits out a number. The type of the frame counter is
(number) => number. Then the type of
((number) => number, SyncFunction) => AsyncFunction.
We can see that
repeat-every-f have different types-- in that case, how are they interchangeable? The key is that they have the same return type
AsyncFunction. When we're parsing the danmaku language code for these two methods, we provide input arguments depending on whatever input types it requires. But what we receive from the method is always an
AsyncFunction, which can be passed up to the parent. Recall that the parent of
repeat-every in the example was
fire circle-blue <0,-5> repeat-every.... The type of
fire would be:
(string, vector, AsyncFunction) => FireConfiguration. In this case,
fire doesn't care about what input arguments are required by
repeat-every,repeat-every-f, as long as they always return an
Here are the types of each of the other layers of the example.
(number) => vector is the type of a parametric equation: we pass in a time value, and it returns an
(number, number, SyncFunction) => SyncFunction
(MovementDescriptor) => SyncFunction
((number) => vector) => MovementDescriptor
(number, number, (number) => vector) => ((number) => vector)
(number, (number) => vector) => ((number) => vector)
(number, number, number, number, (number) => vector, (number) => vector) => ((number) => vector)
There’s only one problem. For maximum modularity,
lerpfromtoback should take two parametric equations as children, as noted above. However, in the example,
<0,0.7> are vectors, not parametric equations. When coming across these type conflicts, we need to consider the semantics. The two vectors are supposed to be constants that don't change with time. In that case, we need a wrapper function that takes in a vector and treats it as a constant: let's say
constant-vector, which would have type
(vector) => ((number) => vector). Our last three lines should therefore actually be:
lerpfromtoback 0.5 1.5 2.5 3.5
3.1 What Types Do We Need?
For this modular structure to be extendable to any possible pattern, the types must be general and flexible. We’ve used the following complex types above, in addition to a few basic types (
- Patterns over time —
- Instantaneous patterns —
- Movement of a single particle —
- Parametric functions —
(number) => vector
- Number to number functions —
(number) => number
We can always add more types, but the critical path seems to be the first three complex types. At the end of the day, a danmaku pattern is always something that is done over time (
AsyncFunction) as a combination of instantaneous actions (
SyncFunction) of which the particles follow some kind of movement path (
3.2 Categories Within Types
We should also note that within a single type, we might have functions with completely different semantics. This is most notable for
SyncFunction. For example,
randomize-x are both
SyncFunction, but the first has the semantic group of "firing a single particle", whereas the second has the semantic group of "modify another SyncPattern". Here are groupings that we might make:
AsyncFunctionthat modifies another
AsyncFunctionthat repeats another
AsyncFunction, maybe over time
AsyncFunctionthat repeats a
SyncFunctionthat modifies another
SyncFunctionthat repeats a
SyncFunction(eg. calling it multiple times simultaneously)
SyncFunctionthat fires a particle or performs some other singular action (
In my unabridged engine, I have three basic “repeat” functions —
Repeat. The first is an Async-over-Async function, the second is an Async-over-Sync function, and the third is a Sync-over-Sync function. I use prefixes to mark the type-based groupings (
C, or none), while the main body of the name marks the general action. These are useful considerations for standardizing naming practices.
A declarative engine-independent language is the simplest (and probably the only practical) way to accomplish the task of describing complex danmaku patterns in a way that can be converted into code. This article sought to show what that language might look like and explain the basics of how and why its syntax works from a static verification perspective. How an engine might run this code is a more technical subject for another post.
If you’re interested in toying around with my implementation of this idea, you can check out my bullet hell engine Danmokou. It allows you write and run completely legit danmaku scripts using this structure — and because the language is declarative, you’re simultaneously writing pithy descriptions of what you’re doing. Be warned that the full implementation is a bit more powerful and a bit less friendly than the simplified example I showed here, and describes complete behavior patterns rather than only particle firing.