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:
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
orunpause
in a GSRepeat? What about if you tried usingunpause
withoutwhile
?
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
torv2.nx
- NY: Add
(target - source).x
torv2.ny
- RX: Add
(target - source).x
torv2.rx
- RY: Add
(target - source).x
torv2.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
andtarget ry
- Use
sltarget rx
andsltarget 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))))