Search Results for

    Show / Hide Table of Contents

    Tutorial 9: Repeater Modifiers

    In this tutorial, we're going to go through almost all of the repeater modifiers (GenCtxProperty) in the engine. Use this page as a reference!

    Bank/Bank0

    Let's say we want to summon bullets in this kind of structure:

    Unity_V9myf0XFax

    What exactly are we trying to do here? Well, we want to go around six points in a circle, and for each one summon this weird spirally shape. How might you do this?

    Your first thought might be to use the circle or rv2incr prop to summon each of the six colored groups, and then use a nested repeater to summon all of the bullets for each group. However, there's a bit of an issue here. If you use rv2incr with an angle in the nested repeater, it will shift the summon around the boss, instead of around the origin of each group.

    The bank command allows us to deal with this issue. When used on entering a repeater, it shifts the current rotational X and Y coordinates into nonrotational coordinates, and also adds a new offset. In essence, it allows us to perform "inner repeats".

    bank0 is similar, but also resets the rotational angle on entry.

    		sync("ellipse-*/w", <2;:>, gsrepeat {
    			times(6)
    			circle
    			color({ "red", "pink", "purple", "blue", "teal", "green" })
    		} gsrepeat {
    			bank <1;:>
    			times(60)
    			rv2incr <-0.02;0:6>
    		} s(none))
    

    Bind*

    See the tutorial on empty-guiding for bindArrow.

    bindLR assigns 1 to the bound variable lr and -1 to rl if the loop iteration is even-numbered, and -1 to lr and 1 to rl if the loop iteration is odd-numbered.

    bindUD is the same, but with ud and du.

    Cancel/Clip/While/Unpause

    These modifiers allow controlling the execution flow of the repeater loop.

    Cancel takes a boolean function and checks it before every iteration. If it is true, the repeater stops.

    		sync("ellipse-red/w", <2;:>, gsrepeat {
    			times(60)
    			circle
    			cancel(i > 10)
    		} s(none))
    

    Clip is the same, but is checked once at the beginning of the repeater only.

    		sync("ellipse-red/w", <2;:>, gsrepeat {
    			times(60)
    			circle
                preloop b{
                    hvar loop = i;
                }
    		} gsrepeat {
    			clip(loop < 20)
    		} s(none))
    

    WhileTrue and Unpause are time-based and cannot be used in GSRepeat.

    WhileTrue takes a condition and only steps through the repeater while the condition is true. If the condition is not true, it pauses indefinitely. Listen to the sound effects in this example.

    		move(inf, nroffset(px(sine(5, 3, t))))
    		async("ellipse-red/w", <1;:>, gcrepeat {
    			times(inf)
    			wait(4)
    			rv2incr(<4>)
    			whiletrue(x > -2)
    			sfx "x-fire-burst-2"
    		} s(rvelocity(lerpt(2, 3, zero, cx(2)))))
    

    Unpause runs a state machine whenever a While condition is unpaused after being paused. Add this modifier to the above code:

    			unpause(sync("scircle-purple/b", <>, gsrepeat { 
    				times(10)
    				circle
    			} s(rvelocity(cx(4)))))
    
    • What do you think would happen if you tried using while or unpause in a GSRepeat? What about if you tried using unpause without while?

    Circle/Spread/RV2Incr

    rv2incr is the most basic way of adjusting bullet offsets between firing. After each iteration, it adds the provided RV2 to rv2, which is the current bullet offset.

    		async("ellipse-red/w", <1;:>, gcrepeat {
    			wait(4)
    			times(30)
    			rv2incr(<7>)
    		} s(none))
    

    spread allows you to instead specify a range over which you want bullets to be summoned. Note that when using spread(X), the first bullet will summon at offset 0 and the last bullet will summon at offset X. This means that spread(<360>) is not the same as circle, as spread(<360>) would make the first and last bullets overlap.

    		async("ellipse-red/w", <1;:>, gcrepeat {
    			wait(4)
    			times(30)
    			spread(<180>)
    		} s(none))
    

    circle is similar to spread; it simply summons all the bullets evenly in a circle.

    		async("ellipse-red/w", <1;:>, gcrepeat {
    			wait(4)
    			times(30)
    			circle
    		} s(none))
    

    Note: you can use spread and circle with non-fixed repeater counts. Try using times(rand(10, 50)). Internally, they use the times variable. You could write: rv2incr(angle(360 / times)) instead of circle.

    Color/Colorf/ColorR

    These are all modifiers that allow merging colors via wildcard rules.

    Color is the most basic form.

    		async("ellipse-*/w", <1;:>, gcrepeat {
    			times(40)
    			wait(3)
    			circle
    			color({ "red", "blue", "green" })
    		} s(none))
    

    ColorR merges the colors in the reverse direction, so you can merge even if the original style doesn't have a wildcard. Normally, that would cause an override.

    		async("ellipse", <1;:>, gcrepeat {
    			times(40)
    			wait(3)
    			circle
    			colorr({ "*-red/", "*-blue/", "*-green/" })
    		} s(none))
    
    • What happens if you try this with color instead?

    Colorf is like color, but instead of looping through the array, it allows you to provide an indexing function that selects one of the colors.

    		async("ellipse-*/w", <1;:>, gcrepeat {
    			times(40)
    			wait(3)
    			circle
    			colorf({ "red", "blue", "green" }, t/5)
    		} s(none))
    

    Delay/Wait/Wait-Child

    These are not allowed on GSRepeat.

    Delay creates a delay in frames before the first invocation of the repeater.

    Wait creates a delay in frames between successive invocations of the repeater.

    Recall that you can also use waitchild for GIRepeat and GTRepeat to wait for the child to finish executing before continuing.

    		async("ellipse-pink/w", <1;:>, gcrepeat {
    			delay(120)
    			times(40)
    			wait(3)
    			circle
    		} s(none))
    

    Root/RootAdjust

    By default, bullets are summoned relative to the entity executing the state machine in question.

    Root overrides this to be a fixed position.

    RootAdjust overrides this to be a fixed position, but also adds to rv2 so the final summoning location is unchanged.

    Try running the following code with root, rootadjust, and then neither.

    		position 0 2
    		move inf nroffset(px(sine(4, 3, t)))
    		async("ellipse-pink/w", <0.5;:>, gcrepeat {
    			times(inf)
    			wait(73)
    		} gsrepeat {
    			root(cxy(-2, 0))
    			times(20)
    			circle
    		} s(rvelocity(px(lerpt(1, 2, 0, 2)))))
    

    Start/Preloop/Postloop/End

    These are commands that perform mathematical operations on bound data during specified times in the repeater.

    Start occurs when the repeater starts,

    Preloop occurs before each iteration,

    Postloop occurs after each iteration,

    and End occurs when the repeater ends. There is currently not much use for End.

    All of these functions take a code block as an argument. If you want to declare variables inside the code block, then you generally need to hoist the declaration (ie. use hvar instead of var) in order to make the declaration visible.

    Try figuring out why the first bullet summons in a different place in this example.

    		sync("ellipse-pink/w", <1;:>, gsrepeat {
    			times(60)
    			circle
    			start b{
    				hvar myVar = cxy(2, 0);
    			}
    			preloop b{
    				myVar.x += 0.04;
    			}
    			postloop b{
    				rv2.rx = myVar.x;
    			}
    		} s(none))
    

    Face

    When summoning bullets, a global rotation is applied to the offset and the bullet summon. By default, this is the original summoning angle of the executor. For bosses, this value is zero, but for subsummons, the value will often not be zero.

    Try running the following code and changing the face argument. original is the default value. Try velocity, which rotates bullets by the current movement direction, and derot, which derotates bullets. If you have a manually-set rotator function on the summon, as we provide here, you can also use the rotator option.

    		sync("icrow", <1;:>, gsrepeat {
    				times(4)
    				circle
    				preloop b{
    					hvar ic = i;
    				}
    			} summon(
    				rvelocity(lerpt(0.5, 1.5, cx(2), cy(2))),
    				async("circle-*/w", <>, gcrepeat {
    					face(rotator)
    					colorf({ "red", "blue", "yellow", "pink" }, ic)
    					times(inf)
    					wait(12)
    					whiletrue(onscreen(loc))
    				} s(rvelocity(cx(-1)))), {
    					rotate(60 * t)
    				}
    			))
    

    ForTime/Times/MaxTimes

    times sets the number of times that a repeater will execute. It may take a function as an argument. maxtimes indicates to the repeater what the maximum number of times might be. This is metadata that is used primarily for mod parametrization (see the tutorial on empty-guiding), and you usually don't need to provide it unless you get an error message telling you to.

    fortime sets the maximum number of frames that a repeater is allowed to run for. Between times and fortime, the repeater will stop when either one is not satisfied. for cannot be used with GSRepeat, which always executes in 0 frames.

    This code may finish drawing all the lasers before the 2 seconds are up, or it may not. Run it a few times and see what happens.

    		async("gdlaser-teal/", <>, gcrepeat {
    				fortime(2s)
    				times(rand(20, 40))
    				wait(8)
    				circle
    			} laser none 1 1 {})
    

    FRV2

    FRV2 is one of the most powerful modifiers. It is similar in concept to rv2incr, but instead of simply providing an increment, you provide a function that tells the repeater what the offset should be for each iteration.

    This is the standard method of implementing BoWaP in DMK.

    		async("fireball-*/", <>, gcrepeat {
    			times(inf)
    			wait(8)
    			frv2(angle(sine(142, 1200, i)))
    		} gsrepeat {
    			times(5)
    			circle
    			color { "purple", "pink", "red", "orange", "yellow" }
    		} s rvelocity(cx(4)))
    

    Here's another version of the bank example with a polar equation.

    		sync("ellipse-*/w", <3;:>, gsrepeat {
    			times(6)
    			circle
    			color({ "red", "pink", "purple", "blue", "teal", "green" })
    		} gsrepeat {
    			bank <>
    			times(60)
    			frv2(rot(1 + cosine(1 / 3, 0.5, t / times), 0, 360 * t / times))
    		} s(none))
    

    NoOp

    This modifier does nothing.

    OnLaser

    See the laser tutorial for details.

    Parametrization

    See the empty-guiding tutorial for details.

    SAOffset

    This is an advanced modifier which allows you to summon bullets along arbitrary equations. It is similar to FRV2, but it is far more generalized and requires far more complex input. Reference Patterns/examples/summonalong for examples of how to use this in more complex situations.

    The third argument to SAOffset is an offset equation to decide the location of the i'th bullet. This will be converted into rotational coordinates. The second argument is an angle offset applied to all the bullets.

    The main unique feature of SAOffset is the first parameter, SAAngle, which is an enum that decides the angle handling for summoned bullets.

    • original: The angle offset is added to all bullets directly. This means it will effectively rotate the entire summoned bullet body.
    • bankoriginal: The position is banked and then the angle offset is added.
    • bankrelative: The position is banked, then the angle is set to the angle from the root, and then the angle offset is added.
    • banktangent: The position is banked, then the angle is set to the tangent angle of the function, and the angle offset is added.

    Here's an example to get you started. Try replacing "bankoriginal" with the above values.

    		async("triangle-*/w", <>, girepeat {
    			times(4)
    			circle
    			color { "black", "red", "blue", "yellow" }
    		} gcrepeat {
    			times(180)
    			wait(3)
    			saoffset(bankoriginal, 30, pxy(0.07 * i, sine(40, 1.3, i)))
    		} s(rvelocity(lerpt(1, 2, zero, cx(2)))))
    

    Sequential

    By default, a generalized repeater will run all of its children at the same time. However, there are times where you want to run only one of the children, or when you want to run the children sequentially instead.

    Sequential makes the repeater run the children in sequence. Note that this only has an effect when used in GIRepeat or GTRepeat.

    		async("", <>, girepeat {
    			times(4)
    			wait(240)
    			circle
    			color { "purple", "red", "blue", "yellow" }
    			sequential
    		} {
    			gcrepeat {
    				colorr "gdlaser-*/b"
    				wait(20)
    				times(3)
    				rv2incr(<10>)
    			} laser(none, 1, 1, { dsfx })
    			gcrepeat {
    				colorr "gdlaser-*/w"
                    delay(60)
    				wait(20)
    				times(3)
    				rv2incr(<-10>)
    			} laser(none, 1, 1, { dsfx })
    		})
    

    SFX/SFXf/SFXfIf/SFXIf

    SFX and all its siblings create sound effects when used. They take an array of sound effects and loop through them.

    By using the f types, you can provide an indexer for the specific sound effect you want.

    By using the If types, you can provide a predicate that allows not playing a sound effect in certain conditions.

    Target/SLTarget

    These two modifiers are responsible for aiming bullets. They run once at the beginning of the repeater.

    They take two arguments: a control method, and a target (which is usually Lplayer, the location of the player). The control method is one of the following:

    • NX: Add (target - source).x to rv2.nx
    • NY: Add (target - source).x to rv2.ny
    • RX: Add (target - source).x to rv2.rx
    • RY: Add (target - source).x to rv2.ry
    • RAng: Add the angle from source to target to rv2.a
    • Ang: Rotate rv2 by the angle from source to target (including NX/NY)

    In Target, source is the position of the firing entity. In SLTarget, source is the real position of the offset rv2.

    You will almost always use either RAng or Ang as control methods. The others are useful almost exclusively for creating laser grids.

    Here is an example you can play with. Make sure to move around to see how the bullets track with time. Try the following:

    • Use target ang
    • Use sltarget rang
    • Move the command into gcrepeat instead (you'll probably want to move it back afterwards)
    • Use target rx and target ry
    • Use sltarget rx and sltarget ry
    		async("gem-*/", <>, girepeat {
    			times(3)
    			rv2incr(<-1;0.2:;:>)
    			color { "purple", "red", "blue" }
    		} gcrepeat {
    			times(inf)
    			wait(12)
    		} gsrepeat {
    			target(ang, Lplayer)
    		} s(rvelocity(lerpt(0, 1, zero, cx(4)))))
    

    Note that SLTarget + ang will give you strange results.

    Timer

    This modifier restarts a timer before each iteration.

    		exec b{
                hvar myTimer = newtimer();
            }
        	sync("sun-red/", <>, s(nroffset(px(myTimer.Seconds))))
    		async("", <>, gcrepeat {
    			timer(myTimer)
    			wait(120)
    			times(inf)
    		} noop)
    

    TimeReset

    Bullets have a variable st which is the "summoning time" of the bullet. This is the amount of time elapsed between the start of the topmost general repeater in its summon hierarchy and the actual summoning of the bullet. If you add this modifier, then the timer will be reset at the beginning of each iteration of the repeater.

    		async("arrow-pink/w", <>, girepeat {
    			wait(30)
    			times(20)
    			timereset
    			rv2incr(<16>)
    		} gcrepeat {
    			wait(5)
    			times(10)
    		} s(roffset(px(1 + 3 * st))))
    
    • Improve this Doc
    In This Article
    Back to top Generated by DocFX