List Lambda Triggers – BB – Update 3
Posted: October 24, 2024
Last Updated: December 2, 2024
List Lambda Triggers – BB – Update 3
Timeframe: October 6, 2024 – October 23, 2024
Welcome back! While it might not feel like it, this is another significant update on the path to my bullet hell maker. What I’ve got for you this week is a basic Player Builder, big behind-the-scenes changes to InGameBuilders and LambdaTriggers (list lambda triggers), and a bunch of other small improvements.
HIGHLIGHTS:
LambdaTriggers In Lists:
Please direct your attention to the graphic below:

Wasn’t that crazy? I was able to create a list of lambda triggers dynamically inside a list of arbitrary builder data! Not only that, but it properly saves/restores just like static trigger lists. Aren’t you impressed? No? Well, that’s fair. I’m impressed, though. Does this sound like a stupid technical problem? Yes. Am I glad I did it? Yes. Was I happy doing it? No.
The main takeaway from this change is that now I can have lists of GameTriggers within DynamicBuffer data more easily. Technically, this was possibly before, but required that the Builder only work on and display a single buffer element at a time (like in the FiringPointBuilder). Now, however, I can have the same kinds of triggers shown for each buffer element in a list-like Builder, such as the LevelBuilder.
This problem arose from the fact that the Binder only supports the ability to find those fields that are nested in objects or nested in objects that are in a List. In order to bind to triggers that are not in elements of a list (top-level objects), I had to search each top-level field for fields marked with the TriggerList attribute to gather up all the triggers on the subject and place them in a flat List in the binding object. This way, the Binder could edit the actual values of lambda trigger bindings and relate them back to the corresponding fields on the subject. Unfortunately, it is not so simple to do the same with TriggerLists that are in objects that are themselves elements in a List in the binding object. Because the Binder does not support nested Lists, I had to either flatten out every list in the binding object and then search each of those for TriggerList fields to put into another flattened binding list, or create a list of trigger bindings for each list of values that would grow at a rate of X bindings per new element in the value list (since each element of the value list should contain X triggers). I chose the latter option because putting all list-level trigger bindings into a single list would make it extremely difficult to determine which bindings relate to which value in any arbitrary value list.
The solution to this boiled down to making a lot of organizational and housekeeping changes to the current Binder, DynamicBufferDisplay, and InGameBuilder – which are all significant backbones for the rest of my code. You understand that I didn’t want to rock the boat too much here, but I did. With all that said, the following code shows the snippets of binding objects for two different builders: the FiringPointBuilder and the LevelBuilder.
public abstract partial class InGameBuilder : MonoBehaviour, IChangeStackSavable {
public class BaseBindingObject {
public List<BaseMountElement> mounts;
[TopLevelTriggerBinders]
public List<GameTrigger32Binder> flatTriggerBindings;
// methods omitted...
}
// ...
}
public class FiringPointBuilder : InGameBuilder {
public class BindingObject : BaseBindingObject {
// Note that since the FiringPointBuilder only operates on a single
// FiringPointElement at a time, we can basically handle the triggers
// within field 'fpe' as top-level trigger bindings.
public FiringPointElement fpe;
public FiringPointComponent fpc;
// methods omitted...
}
// ...
}
public class LevelBuilder : InGameBuilder {
public class BindingObject : BaseBindingObject {
public LevelComponent level;
public List<LevelWaveElement> waveElements;
// This list corresponds to all the TriggerLists in the waveElements list
[ListTriggerBinders("waveElements")]
public List<GameTrigger32Binder> waveElementsTriggerBinders;
// methods omitted...
}
// ...
}
public struct LevelWaveElement : IBufferElementData, IResettableStruct, IBuilderNoticeGenerator {
// ...
[TriggerList]
public SimpleList2<LambdaTrigger> OnTestTrigger;
// ...
}
Note a few different things:
- New TopLevelTriggerBinders attribute. There should only ever be 1 per binding object. In the LevelBuilder’s BindingObject, this would contain trigger bindings for all the TriggerLists inside the ‘level’ field. In the FiringPointBuilder, it would contain trigger bindings for the ‘fpe’ and ‘fpc’ fields .
- New ListTriggerBinders attribute. In the LevelBuilder’s BindingObject, this contains all the trigger bindings for each element of ‘waveElements’. While it is a little cumbersome because waveElements can grow and shrink, it is much easier than having a unified list for all the list-level trigger bindings.
- TriggerList attribute to signify which fields should be treated like triggers.
Combined with a bunch of other logic behind the scenes, this allows me to automatically map TriggerLists within list elements (ex. map each element of waveElements) to some range of trigger binders within another binding list (ex. waveElementsTriggerBinders). In this example, the LevelWaveElement struct contains a TriggerList with 2 triggers called OnTestTrigger. This means that for each element of waveElements in the binding object, we have 2 GameTrigger32Binder structs in waveElementsTriggerBinders.
I’ll stop with this stuff for now, but since this finally cleared some things up after the changes, this finally convinced me that my Binder is finished. And by that I mean it’s finished enough to talk about in more detail in a long-form post, so I’ll be working on that.
OTHER UPDATES:
PlayerBuilder:
- Set Max Health.
- Set Max Speed.
- Set Invincibility Time.
- Add/Remove Firing Points from player.
- Respawn player.
- Apply values to player.
- PlayerPresets: Player information is stored on a separate prefab when saving PlayerBuilder creations. I now have a system that can read any of this data off a preset and apply it to the actual player.
- PresetRules: Player information may change at any time (ex. on a trigger), so PresetRules define how certain values change. Ex. Should a player’s current health reset, stay the same, or stay proportional to the max health when the player’s max health changes? PresetRules help define this.
Undo/Redo:
- Changed the Undo system to use a more generalized ChangeStack.
- IChangeStackSavable now required for objects that can be saved/restored from the ChangeStack.
- Changed the way InGameBuilders are saved/restored from ChangeStack’s SaveState.
- Lambda trigger map now saved to ChangeStack, so undo/redo takes lambda trigger changes into account when storing changes.
- Redo functionality.
Firing Points:
- Bugfix: ‘Uniform in Rectangle’ bullet formation was wrong. Fixed.
- Bugfix: ‘Uniform on Circle’ did not take max angle and angle offset into account. Fixed.
- Bugfix: firing point speed/acceleration was applying per-bullet instead of per-group.
- Bugfix: applying triggers at multiple different points was overwriting old trigger activations. Fixed by generalizing add trigger functionality.
Enemies:
- New NativeCounter and NativeSum collections to use in game trigger systems for speedup. From this blog page and this documentation page.
- Waves now set spawned enemy random seeds.
- Enemies can now propagate their random seed to their mounted firing points. Combined with the prior update, enemies with randomness in their firing points are now different instead of firing in the same random way.
- Bugfix: Enemies show up in middle of screen for a single frame before moving to starting path node. This was fixed.
Misc:
- BuilderBindings now cache their behaviour and behaviourType fields. This was a significant speed improvement.
- Speed improvements by caching a bunch of reflection/field attribute information in builders.
- Support for single-hit bullets. Bullets used to be able to hit multiple things on the same frame. This is still default behavior, but the framework now supports the ability to stop bullets from hitting more than 1 thing in a frame. NO GUARANTEES ON ORDER.
- Bugfix: SimpleListDisplay and TriggerListDisplay fixes where only display elements are now counted for their binding index update (random children like text boxes are ignored now).
- Generalized functionality for adding triggers to arbitrary entities while still supporting Burst.
- Bugfix: Trigger for setting bloom value no longer throws error when field name misspelled.
- Bugfix: Dynamic dropdown menus were initializing at the wrong times. Fixed.
- Bugfix: Builders were marking themselves as finished loading too early when opening a project causing things to run at the same time when they shouldn’t. Fixed.
Video Demos

Set basic player properties like health and speed. Respawn the player.

Add many firing points to the player.

Testing undo and redo. Going back and forth between configurations with undo/redo hotkeys.

Extended demonstration of triggers within list-like builder configurations. Note how each wave element gets its own trigger list(s).

Etch A Sketch’ing orcsune through bullets at 10x speed.