Search Results for

    Show / Hide Table of Contents

    Tutorial 2: Bullet Controls

    Unity_SCkxEkxGVQ

    Part 1: Flipping

    Go back to DMK Tutorial Setup 00. We'll use the same file.

    Drop this code in at the bottom and run the script: (Note: I have removed some parentheses. Recall that parentheses and argument separators are generally not required.)

    		bulletcontrol(persist, { { "circle-*" } }, flipygt(4, _))
    		async("circle-red/w", <1;:>, gcrepeat {
    			wait(60)
    			times(inf)
    		} gsrepeat {
    			times(50)
    			circle
    		} s(rvelocity(cx(3))))
    		_ 4
    			poolcontrol({ { "circle-*" } }, reset)
    

    You can view all bullet controls here: BulletManager.SimpleBulletControls, or in Danmaku/BulletManagement/BulletControls.cs:SimpleBulletControls.

    The bulletcontrol command creates a bullet control. It takes three arguments:

    • A predicate that runs every frame to check if the control should continue to exist. Persist is an alias for True and Once is an alias for False. If you set this to Once/False, the control will run exactly once.
    • A style selector. We'll discuss this in a later part, but for now, just think of {{ "circle-*" }} as "select all circle bullet types".
    • A control. We are using the FlipYGT control, which takes two arguments:
      • A y-value "ceiling" against which to check flipping,
      • A predicate to filter which bullets get controlled. _ is an alias for True, which means all bullets get controlled.

    Therefore, we have created a control that "flips all circle bullets when they go higher than y=4".

    There are several ways to destroy controls. If you want to clear all controls on a bullet style, you can use the reset pool control.

    Before continuing, let's establish the difference between a bullet control and a pool control.

    • A bullet control is an object that persists over time and is applied to every bullet of a given style.
    • A pool control is a function that is run once on the metadata for a bullet style (ie. it does not affect individual bullets).

    The reset pool control simply destroys all current bullet controls on a bullet style.

    To use pool controls, we invoke the poolcontrol command, which takes a style selector and a pool control as arguments.

    However, we don't want to destroy the control immediately. Instead, we use the delay command _ delaySeconds to destroy it after four seconds.

    Another way to destroy controls is to use timers.

    		exec b{ 
                hvar tm = newtimer(); 
                tm.Restart();
            } 
    		bulletcontrol(tm.Seconds < 4, { { "circle-*" } }, flipygt(4, _))
    		async "circle-red/w" <1;:> gcrepeat {
    			wait(60)
    			times(inf)
    		} gsrepeat {
    			times(50)
    			circle
    		} s(rvelocity(cx(3)))
    

    The exec StateMachine converts a block of code into a StateMachine, SyncPattern, or AsyncPattern. Every time this exec is run, it will create a new timer and restart it (setting the elapsed time to zero and starting recording).

    Since the control should be destroyed after four seconds, we use the persistence predicate of tm.Seconds < 4.

    Part 2: Style Selector

    The style selector is an array of arrays. If we take one element from each array and combine them according to wildcard rules, then we get a target style. The engine takes every possible combination and applies the control to all the applicable styles.

    For example, let's say we have the selector { { "circle-*", "ellipse-*" }, { "red/w", "blue/w" } }.

    The engine generates:

    ("circle-*", "red/w")
    ("circle-*", "blue/w")
    ("ellipse-*", "red/w")
    ("ellipse-*", "blue/w")
    

    and then merges them using wildcard rules into

    "circle-red/w"
    "circle-blue/w"
    "ellipse-red/w"
    "ellipse-blue/w"
    

    To test your understanding, try this question:

    1. Let's say we wanted to select circle and ellipses; in colors red, green, and blue; and in colorization methods /w and /b. What style selector should we use?

    If you remember the previous example, we used a style selector {{ circle-* }}. There's one unresolved wildcard! If you try to summon a bullet with style circle-*, the engine will throw an error telling you that it can't find a simple bullet style with that name. However, in a style selector, you can leave one wildcard in the final style string, and it will match all existing styles that fit the format.

    Knowing this, answer this question:

    1. Let's say we wanted to select circle and ellipses; in all colors; and in colorization methods /w and /b. What style selector should we use?

    Now look at this code and try to figure out what it will do. Hint: The cull control deletes bullets if the condition is satisfied.

    		bulletcontrol(persist, { { "circle-*", "ellipse-*" }, { "red/w", "blue/w" } }, cull(y < -2))
    		async "*/w" <1;:5> girepeat {
    			wait(60)
    			times(inf)
    			color({ "*-red", "*-blue", "*-green" })
    		} gcrepeat {
    			wait(10)
    			times(3)
    			color({ "circle", "ellipse", "triangle" })
    			rv2incr(<2>)
    		} gsrepeat {
    			times(24)
    			circle
    		} s(rvelocity(cx(2)))
    

    Part 3: Fun With GTRepeat

    Take a look at this code:

    	gtrepeat {
    		wait(40)
    		times(inf)
    		fortime(3s)
    	} bulletcontrol(once, "circle-red/w", batch(t > 1, {
    		sfx("x-transform-1", _)
    		restyleeffect("arrow-red/b", "cwheel-red/", _)
    	}))
    	async "circle-red/w" <1;:2> gcrepeat {
    		wait(10)
    		times(inf)
    		fortime(4s)
    		rv2incr(<6>)
    	} gsrepeat {
    		times(30)
    		circle
    	} s(rvelocity(cx(2)))
    

    There are a few new features here, so let's go through them.

    This time, we're actually using once (False) as the persistence predicate to bulletcontrol. This means that the control executes for exactly one frame.

    We also introduce the batch command. This allows us to apply multiple controls with a single predicate, which is highly useful for complex effects. In this case, we want to change the style of any circle bullet older than 1 second, and also play a sound effect if this happens.

    The sfx control takes two arguments: a sound effect and a predicate. You can see the list of usable sound effects in GameManagement/SFXService. Click on any of the "SFX Config" objects and look at the "Default Name" property. Sound effects have a cooldown (see the "Timeout" property) which prevents them from getting invoked too many times at once. Even if this control is applied to a hundred bullets in one frame, it will only play the sound once. This is for ear safety. For the second argument, since the predicate t is greater than 1 is already checked by the batch command, we don't need to put anything here, so we just use _ (True).

    The restyleeffect control takes three arguments: a target style, an effect style, and a predicate. In this case, we are transforming the bullets into arrows, so we provide the target style arrow-red/b. Effect styles are a special type of bullet style that you should not try to create directly. They are used for simple frame-animated particle effects, but because they are bullets and not actual particle effects, we can use tens of thousands of them simultaneously. For now, the only effect style is cwheel, so we'll use that. You could also set this to null, which creates an effect bullet that simulates the original bullet fading out. As with sfx, we use _ (True) for the predicate.

    Note that the order of controls inside batch is significant. If we put the restyleeffect control first, then the sfx control would not take effect. The reason is that controls stop running when a bullet is destroyed, and restyleeffect destroys the bullet (by transferring it to another style).

    Now, we can describe the control as follows: "Every 40 frames for 3 seconds, for every circle-red/w bullet that is older than 1 second, transform it into an arrow-red/b while playing the sound effect x-transform-1."

    Part 4: The SM Control

    All bullet controls are equal, but the SM control is the most equal of them all.

    The SM control allows us to summon an invisible node (inode) at the location of a bullet and run an arbitrary state machine on it. This is generally how you "fire bullets from bullets", or perform single-to-many bullet transformations.

    Let's start by summoning some spinning stars.

    		sync "lstar-blue/w" <> gsr {
    			times(8)
    			circle
                preloop b{
                    hvar nStar = i;
                }
    		} simple(rvelocity(lerp(0.2, 1, t, cx 3, zero)), { 
    			dir(200 * t)
    		})
    

    We're now using simple instead of s. This command allows us to provide an array of simple bullet options (see SBOption or Danmaku/BulletManagement/SimpleBulletOptions.cs), which are used for fancy effects. In our case, we want the stars to rotate independently of their movement, so we provide a custom direction function in degrees. The function 200 * t makes the bullets rotate 200 degrees counterclockwise per second.

    Also, our velocity function is now using some fancy math. The Lerp function allows us to linearly interpolate between two values. The signature of Lerp is Lerp<T>(float zeroBound, float oneBound, float controller, T val1, T val2). When the controller is between zeroBound and oneBound, the return value is weighted between val1 and val2 depending on the proximity to each of the bounds.

    In other words, lerp(0.2, 1, t, cx 3, zero) means "go from speed (3, 0) to speed (0, 0) when time is between 0.2 and 1".

    Now, below the code you already wrote, let's add an SM control.

    To make this fancier, let's make the stars burst one-by-one instead of all at the same time. We can use nStar in the control predicate to achieve this. Let's say the first star explodes when it is 1.2 seconds old, and the second at 1.5 seconds, and so on. We could write the condition as: t > (1.2 + 0.3 * nStar).

    ...Well, not exactly. There's a minor issue with this, by design. In BDSL, as in many programming languages, accessing variables like nStar can occur in one of two contexts: lexical or dynamic. Lexical access occurs when a parent declares the variable and then a child tries to read it. For example, this example from the first tutorial is lexical, because gcrepeat binds the variable loop and its child, s, tries to read it. The s function is able to determine where the loop variable is declared by looking upwards in the script code.

    	async("arrow-red/w", <0;0:1;0:0>, gcrepeat({
    			wait(10)
    			times(10)
    			rv2incr(<4>)
    			preloop b{
                    hvar loop = i;
                }
    		}, s(rvelocity(pxy(1 + 0.4 * loop, 0))))
    	)
    

    Dynamic access occurs when a third party tried to read the variable. In this case, the predicate in a bullet control function is dynamic. Bullet controls can execute on any bullets, including bullets fired from other scripts, so the predicate cannot uniquely determine where a variable is declared, or even if it is declared at all. When accessing bound variables from an dynamic context, you must write it as &nStar instead (prefix &). Dynamic variable access goes through a different (and slower) logical pathway. It is also possible for dynamic variable access to fail, so if you misspell the variable name, you will get a runtime error stating the following:

    The variable nStarr<float> was referenced in a dynamic context (eg. a bullet control), but some bullets do not have this variable defined

    With those caveats stated, drop this control function under the star code:

    		bulletcontrol(persist, "lstar-blue/w", batch(t > (1.2 + 0.3 * &nStar), {
    			sm(_, sync "star-*/w" <> gsrepeat {
    					circle
    					times(22)
    					sfx("x-fire-burst-1")
    					colorf({ "red", "pink", "purple", "blue", "teal", "green", "yellow", "orange" }, &nStar)
    				} simple(rvelocity(lerp(0.2, 1, t, cx 2, cx 4)), {
    					dir(starrotb3)
    				}))
    			softcull("cwheel-blue/w", _)
    		}))
    

    The SM control takes two arguments: a predicate, and a StateMachine to execute. The predicate comes first due to my formatting preferences, but feel free to change it. It's as easy as changing the argument order of the SM function in the file Danmaku/BulletManagement/BulletControls.cs (though you'll break many of the example scripts if you do that).

    The StateMachine here is nothing too new. We use the &nStar variable, which is passed from the bullet data to the inode, to determine the color of the small stars. We also use the starrot3 rotation function. The starrotX and starrotbX functions in BPYRepo give slightly randomized rotation functions in one or two directions respectively.

    We also use the softcull control. This is almost the same as the cull control, but in addition to deleting the bullet, it also spawns an effect bullet at the position.

    There's only one control that's as interesting as SM, which is Exec. However, Exec is too complex for the second tutorial, so we'll come back to it another time. That's all for this tutorial!

    Problem Solutions

    1. { "circle-**", "ellipse-**" }, { "red", "blue", "green" }, { "/w" "/b" }
    2. { "circle-*", "ellipse-*" }, { "*/w", "*/b" }
    • Improve this Doc
    In This Article
    Back to top Generated by DocFX