+1
Answered

Macros vs Methods

James 3 years ago updated by Guy Rabiller 2 years ago 10

I am still experimenting with integrating Bolt into my workflow.  I have started to pull graphs into a prototype I am working on, so forgive the crudeness of the screenshots.  Here is the issue.  I am trying to figure out the best way to combine behaviors for components of my game. 

Normally, I would just use controller scripts on each component.  This more or less works with Bolt (as indicated in my screenshot) but it means that the functionality of each method must be defined in C#.  This is fine, but the logic in each method is simple enough that a Bolt graph could easily describe each method. 

If I use a Bolt Macro, I can define each of the "methods" as a control input and use the input parameters as arguments.  This kind of works, but is really clunky and error prone.  It also does not really encapsulate the class/macro in any real way, since scene members are not hidden.


I have also experimented with using custom events and listeners to simulate the communication and coordination of methods across objects.  This works a bit better, but is very clunky and error prone.  Arguments are not strongly typed, names are plain text, and nothing is indexed or available in the fuzzy finder.


So in summary, for this use case:
1. Macros kind of work, but are convoluted to set up.  I am pretty sure I am hacking together non-intended functionality.
2. C# code works fine, but starts to defeat some of the value in using Bolt, although it still makes it easier for gameplay designers to use what we developers code.
3. Event listeners probably best simulate the functionality I am looking for, but are so error prone, they start to be more time consuming that just writing the code.

I assume that I am just doing something wrong.  I went through all your documentation and examples, but this is the best I was able to come up with.  Please shove me in the right direction if I am missing something.
Bolt Version:
Unity Version:
Platform(s):
Scripting Backend:
.NET Version (API Compatibility Level):
GOOD, I'M SATISFIED
Satisfaction mark by James 3 years ago
+2
Pending Review

I'm not sure I fully understand what you're trying to accomplish, but I'll try to give you some insight.

A macro (or a super unit) should be thought of as a function. Each function has its own parameters and its own entry point, just like in C# (your second example). 

What I think is the source of the confusion here is that you seem to treat macros as "classes". Bolt has no notion of "type" per-se. The Bolt "way-of-thinking" is to treat a game object as the root type, with each machine component as an event handler. "Fields" or "properties" on that root type, to stay in C# equivalents, would be object variables in Bolt.

I understand that from a C# programmer's perspective, strings and loosely typed arguments are "fickle". But requiring users to create events or types before using them would create a big barrier of entry that most artists and designers wouldn't understand how to cross. 

+1

Hi Lazlo,

I agree with your appraisal, treating a macro like a class doesn't make much sense.  It was just an experiment.  I guess the simplest way to sum up my goal, is to be able to create Bolt graphs that act as methods that can be "called" from other graphs. 

Having "class method" type graphs that can perform utility functions for any other graph would be great.

More importantly, letting a graph have a "public method" that could me invoked from a graph on another object would mean that there would be almost nothing that could be done in c# that Bolt could not cover.

I get your point about the "fickle" nature of loosely typed arguments.  Maybe this could be solved with some autocomplete from the UI?

Like I said, I am sure my thoughts on abstraction and encapsulation are blinding me a bit here.  I am just trying to figure out the best way to rationalize that through Bolt.  It may very well be that there is a very obvious way to deal with cross object calls, that I am just not finding in the Bolt UI.


It's a clever experiment, though ;) Never thought of using them like that.

How would those "class method graphs" interact differently than custom events?

How would they be defined differently than the current macros?

I'm open to brainstorming this for a later release. However, abstraction and encapsulation are usually notions that are beyond the understanding of most visual scripting users, so I want to make sure that we don't deviate towards a "programmer-centric" approach for such a feature.

I like the idea of an autocomplete, but because registering a custom event is as easy as adding the node, it would mean that the autocomplete would have to search through every node of every graph in every scene and every asset to gather the event options, with little possibility for caching because nodes can be added and removed at any time. Furthermore, the event node itself does not specify its argument types (again, for simplicity over rigidity), which prevents any kind of type-safety.

Hi Lazlo,

Just to be clear, I don't mean this as overly critical.  You took a feature many people wanted for collaboration and you actually did it.  I am a supporter.

So I thought about it a little more, and I think you are right.  As built, the macros already work like a "class static method" in that they can be used by any other graph to complete a function (e.g. build a string, or multiply two numbers together)

I see your point about encapsulation.  Maybe that is my issue.  I am trying to figure out a way to not have too much functionality in a single graph.  I guess the event listeners can do that where needed.  I have started to use Macros that take a GameObject as an argument, allowing me to "reach across" to other objects in the scene to access their Bolt variables, and C# scripts. (see screenshot below)


I am going to keep playing around with it.  Once I get things setup, I think that Bolt is something my team will be able to use to be productive.

On a side note, if I were implementing the caching for variables and custom events, I would probably update a cache after the user makes an edit to the event node.  No need to crawl the graphs.  This way you could create a custom event, maybe even rename arguments.  Then when they use the Custom Event Trigger, you could make the user select an even that has already been created, rather than type one in. (I guess also accepting a string input node for the event name might allow for some fancy lambda type stuff).  This would also have the advantage of the user knowing what/how many arguments the custom event needs.  In isolation, the custom even trigger is not very "artist friendly".  Almost by definition, they would need to reference back to the custom event to try to figure out what it is called and what it takes.  

Answered

Indeed, using a game object as an argument is I think the right way of making an "instance" macro at the moment.

I see your point about events,and I believe you're right. The arguments could be defined kind of like how you define value inputs and outputs at the graph level. At this point though, I think it would make more sense to create an event "type" as an asset, just like you define macros as assets. Then that event type could be named, have documentation for each port, and show up in the fuzzy finder under a special sub category.

I'll keep that in mind for a future release and start a separate Idea thread for it. 

Edit: Here's the idea thread: http://support.ludiq.io/topics/251-/

+1

So far I too feel Methods are missing from Bolt. Everything else is awesome, really, and it works like a charm.

I believe this is more a conceptual issue than an engineering one but here is my 2 cents:

Conceptually, an event is something that happens outside of a game object, the very nature of an event has nothing to do with the game object. For instance "On Keyboard Input" is an event that has nothing to do with the nature of the current game object, it does not even require a Target. On the contrary, methods are intrinsic to the game object ("CloseDoor" for a car for instance, "SwitchEngineOn", etc..).

If you "send a message" to a car game object to "close the doors", you are ordering that very game object to do something very specific for which it has been designed for.

If you now use custom events for this, it becomes odd, because you're telling the game object that an event has been triggered somewhere else, called "CloseDoors", and you are asking the game object *would you like to react and do something about that external event ?". Yet that custom event has been defined by the car itself. Conceptually, this is very confusing.

In Unreal BluePrints you can create methods. This makes things clearer.

In fact, Bolt CustomEvents are reversed in their Targets designation. A game object triggers a CustomEvent through the "Trigger Custom Event" node, no need to define a Target Listener here. The event just "happens". Then another game object use the "Custom Event Listener" node, and instead of having to set the Target being the "Listener" (which is always Self anyway) it should be the "Emitter" of the Custom Event (or perhaps nothing to listen for an Event of the same type from all game objects).

Currently, Bolt CustomEvents are indeed treated like Messages and Methods, because the Target designation of the Listener/Emitters nodes are reversed.

In fact I even believe that current Bolt CustomEvents nodes should be renamed with "Method", for the listeners and "Message", for the triggers.

That would be much more intuitive and closer to the conceptual design of those nodes.

Hi Guy, thanks for chiming in, it really helps to have these conceptual discussions.

With Custom Event Definitions coming in the future, there's a potential to overhaul all of this, but it's not going to be actual "methods"; because as mentioned before, these require the notion of "class", and that's a whole other endeavour.

Conceptually, what exactly are you suggesting here, besides renaming "Custom Event" to "Method" and "Trigger Custom Event" to "Message"? Unless I'm misunderstanding?

I feel like perhaps another confusion is in the way people use and design Bolt graphs. Often (if not always?), a method can be represented as a macro which is then used as a super unit (to "call" the method). As long as this method graph has a "target" input parameter, it can effectively work as an instance method. How you categorize and organize these graphs in your projects afterwards is, in the end, up to you (but adding a "Category" to graphs could help, for example, to group all "methods" of a given "class" under the same folder).

Yes indeed, Macro with a "Target* parameter can act as "orphan* Methods. This even allows to use the same feature on different kind of objects so I believe this is efficient in terms of coding. Still a paradigm shift because at the same time those "Methods" are not part of the Object per say, but I think those can do the job, yes. I would say this is one more way of extending Objects with new "Methods".

But in fact I'm more concern about "True" Events (in the conceptual sense). That is Events intrinsic to the Object and emitted by this Object without the need to set a "Target" or a receiver Object for that Event when it is emitted. If a Car emit a EngineSwitchOn Event, it can't know "whom" to send it, "whom" will need it in advance.

And I think I have found a conceptual solution for this: The Object should send its Custom Events to itself!

So a car will Trigger the "SwitchEngineOn" Event, and set Itself as the Target of the Event.

As the Custom Event listener node allows to peek a "Target" different from itself, it allows to "Spy", so to speak, what's happening into another Object. And this is exactly what's needed.

Another Object Interested in that car will then have a "SwitchEngineOn" Custom Event listener node with the Target set to that Car Object.

This may add a little overhead to the Object because all the Events defined for that Object will be triggered one way or another when the Events happen, even if "Nobody" else care - is listening, but at the same time, this allows for very modular and "pluggable" systems.

When you design the car you then set all the possible Events and you forgot about it. Then when that car is imported into a Scene, any Entity knowing about the car Events can "plug" in to that car by "spying" its Events and react accordingly.

I believe this is a good way of maintaining separation of concerns on autonomous Entities.

I'll have to experiment with this approach though, and see if there are any issues or drawbacks.

I see two right know:

1) What if the Object needs to send 50 different Events? Does it means we have to create 50 CustomEvent Trigger Nodes? Perhaps not. One will be sufficient granted it has a String argument with the Name of the "true" Event. So the Object spying on it would need only one CustomEvent Listener Node to spy on it, and dispatch its action according to the "true" Event name given as a String argument. The Event name of the Trigger could be standardized to "ObjectEvents".

2) Now what if a "ParkingLot" Entity is interested in monitoring all "SwitchEngineOn" Events from all its Cars? Will it have to set a CustomEvent node for each car? Hmm, perhaps through a Registration procedure? A big or global system will then call a "RegisterEvents" Car Method, with the global system object as argument. The Car object would have to be prepared for that. That means for each of its Event, the Car would use a CustomEvent Trigger Node to itself, as well as a second CustomEvent Trigger Node and set the Target (at first set to None) to the global system. That second CustomEvent Trigger Node would add a second argument, the Car Object so the global system would know which Object sent an Event. But that means 2 Events Triggers Nodes are used for each Car Event.

Perhaps more elegantly: The Car only use one CustomEvent Trigger Node for each Event, with Event Name "ObjectEvents", and with 2 arguments: One is the "true" Event name string, the second is the Car Object. All these events are set at first with the Car Object as the Event "Target".
Then the Car Object would have 2 Methods: RegisterObjectEvents(Target Object), and ReleaseObjectEvents().
When, for instance, the ParkingLot Object call a Car Object RegisterObjectEvents(Target Object) Method, the Car Object would switch all its Events "Target" to the Target Object given as an argument. When ReleaseObjectEvents() is called, the Car Object would restore all its Events "Target" to itself.

The restriction I see with this approach is that after having "Registered" the Car Events to the ParkingLot Object, no other Object will then be able to "Spy" on the Car Object for its Events, until the ParkingLot Object issue a ReleaseObjectEvents() to the Car.

That makes me think: Could a CustomEvent Trigger Node use a list of Target Objects instead of only one Target Object ?

This would solve that last issue. The Entity could still be spied by anyone while have its "ObjectEvents" registered to many "global systems".

+1

Or even more elegantly for the exposed case, could a CustomEvent Listener Node use a list of Target Objects ? This way no need for "RegisterObjectEvents" calls, the ParkingLot system would just add the new Car to its list of "spied" Cars.. I believe it would be very simple and efficient that way. The Car then would Trigger only one Event per Event.

To summarize the concept:

An Entity (say, a Car) is setup this way in order to emit "ObjectEvents" to itself:


An Entity that want to "Spy" another one is set this way:


Granted the CustomEvent Listener Node accept a list of Target Objects, and instead of "Self" the Target Objects would point to the Car objects - for instance.

This way there is no distinction between a "casual spy" or a "global system" listening a massive amount of Entities ObjectEvents. It just has to add or remove on the fly the Target Objects to the list of Targets in the CustomEvent Listener Node (No more need for "RegisterObjectEvents()" nor "ReleaseObjectEvents()" Method calls). This would be much more simple and elegant.

This all depend on the possibility to have a list of Targets for a CustomEvent Listener Node.