EDIT : Just released an addon pack implementing everything i've been advocating for !
Grab it here if you want to create Object-based signals, passing instances and supporting multiples parameters + custom expressions
✅ overboy.itch.io/construct-3-object-signals-custom-expressions
Hi Ashley, thanks a lot for the amazing features pushed recently and for integrating the suggestion regarding Polymorphism for Custom Actions. After thinking about it longer, I must admit you were right about the implementation you chose for Custom Actions (based on the same implementation as Functions).
I have a few more feedbacks/suggestions to give as Custom Actions are still WIP and now I know better about your plans with them.
1. New “Listener Blocks" : the user could create as many extra Trigger blocks as they want for each Function and Custom Action with a boolean "Emit Signal" set to true and it would feature an execution order feature.
=> Those extra “Listener Blocks" would in fact be similar to Functions/Custom Actions but they would be called at the same time and with the same parameters as their related Function/Custom Action. This means a Function/Custom action would allow us to execute logic in multiple place in the eventsheet and to pass the parameters/Object UIDs in those places : it would work kinda like a "Better Signal" feature but allowing us to do even more (execution order) and with the top performance and ergonomy of Functions/Custom Actions.
2. Calling a Function/Custom Action by its name
3. Custom Expressions.
I. LISTENER BLOCKS
construct23.ideas.aha.io/ideas/C23-I-165
= Extra Listener Trigger Blocks for Functions and Custom Actions
= “On Custom Action called” / “On Function called“
A. Listener Blocks :
- Add a new "Emit Signal" boolean for Function/Custom Actions. (set to false by default)
- If it's set to true, the user can add as many extra “Listener Blocks” as they want related to that specific Function or Custom Actions.
Listener Blocks are a new kind of Event Block (as Custom Actions or Functions, different from regular C3’s Triggers block). There are corresponding to an existing Function (or Custom Action) and they allow to use the parameters of this corresponding Function.
Exception : The user can't create Listener Blocks for return type Function such as String/Number (they also can't create Listener Blocks potential future return type Custom Action = Custom Expression)
They could be implemented in a similar way to Function/Custom Actions in term of performance (see details below)
On the mockup above, you can see 2 “Listener Blocks” blocks corresponding to the “MyFunction1” built-in function. You can notice that :
- The Symbol of Listener Blocks is a blue arrow, this is not a mistake : Listener Blocks are different from C3’s regular Trigger block (green arrow). This is because there are some kind of “pseudo functions” but with the same parameters as their linked Function block (so they would be as performant as built-in Function).
- The Parameters are blue => it’s because as opposed to Function blocks, Listener Blocks blocks parameters can’t be edited, they’re just the same as their related Function block.
- There is only 1 actual “MyFunction1” function block, but there can be un unlimited amount of “MyFunction1” Listener Blocks (here there are 2 of them)
- The Right-Click context menu of a Listener Block is different from built-in Function block : it doesn’t allow to edit the function directly (but it allow to go to the function), it only allow to edit the Listener Block itself and it doesn’t allow to edit the parameters of the Function.
It would work exactly the same for Custom Actions : we would be allowed to create corresponding Listener Blocks blocks for them.
B. The Workflow to create Listener Blocks :
There are multiple way the user could create those Listener Blocks, but here are the 2 best alternatives I can think of. (It could be only one of them or even both of them so everyone choose their favorite workflow)
1) Creating Listener Blocks from Event sheets
1/ It would be in the same context menu as where the user can create Functions and Custom Actions.
2/ It would open a “Add Listener Blocks” pop-up with the object “Functions” selected by default. But the user can choose any actual Objects with Custom Actions instead thanks to a filtered object picker.
3/ The user chooses the corresponding Function/Custom Action for the selected Object (if it’s not just the “Functions” Object).
In the mockup, this is a string field with autocompletion because there could be issue with a dropdown list because of the huge amount of built-in Functions there could be. However it would be impossible to create a Listener Blocks for a non-existing Function/Custom Action. As you can see in all previous mockup the Listener Blocks doesn’t rely on an actual string for the Function they’re listening for. They're more like specialized Function as opposed to trigger events such as "On Tween [Tag] finished"
(It could just be a dropdown list instead if browsing a potential big number of Function isn’t an issue)
2) Creating Listener Blocks from Conditions
The user would find a new Trigger Condition with blue arrows per Function/Custom Action in the conditions of Objects/Families with associated Custom Actions, or in the conditions of the “Functions” Object (which doesn’t have any condition yet in the engine).
(I think this one is my favorite workflow among the 2 alternatives)
Of course, if they're created from Conditions Pop-up, the creation of those Listener Blocks condition would be restricted the same way Regular Trigger are restricted but with an additional exception : they can't be in a subevent. (same restriction as any other function)
C. Execution Order :
On the previous mockups you can see “‘(Order : 0)” at the end of the Listener Blocks. This is corresponding to an optional but amazing-to-have feature that would be Execution Order for Listener Blocks.
As you rightfully pointed in the last thread, the issue with Custom Triggers is annoying race conditions/issues with order of execution. This is also an issue with all existing Trigger Condition in construct such as “Object:On Created”, it’s pretty common to have to set-up a bunch of thing when an object is instanciated. Also on bigger project, for organisational purpose, it’s really important to be able to put the actions executed for Triggers in different places. But the execution order is a big and recurrent issue in C3.
There are multiple way to implement this “Listener Blocks Execution Order”, and a big spectrum of capabilities for this feature. So here i’ll just present what I think is the most powerful and intuitive/easy to understand way to handle this for the user.
The only specific parameter Listener Block would have would be a “Execution Order” number. (Default value is 0)
- If Execution Order = 0, then it means the Listener Block is triggered exactly at the same time as the related Function. Which means it would be exactly the same as calling multiple built-in function at the same time with the same parameters but with a more pleasant workflow and without the need to create a bunch of functions while in fact i just want to call and define several of them at once.
The user should not use order=0 if they absolutely need some events from other blocks to happen before.
- If Execution Order > 0, then it means the Listener Block would wait for the related actual Function block to be finished, but also all other Listener Blocks with a smaller execution order.
Example :
In this example, I have a MyAction1 Custom Action with 4 related Listener Blocks with different execution order.
1. When the Function is called, both the Custom Action and the Listener Blocks with order = 0 are executed.
2. Once the Custom Action Block and the Listener Block with order=0 are finished, the 2 Listener Blocks with order = 1 are executed.
3. Once the 2 Sepcial Triggers block with order=1 are finished, the last Listener Block with Order=3 is executed.
Note 1 : this is not an issue that the last Listener Block order isn't order=2, it could also have been Order=4552, it would work exactly the same. No need for the execution order numbers to be consecutive. The only rules is the lowest order are executed first, and the highest order Listener Blocks wait for the lowest to be finished before they're executed.
Note : Those Listener Blocks could be placed anywhere in the eventsheets, in any order. The Listener Block with Order = 3 could be placed above the rest for example, it would be the same result.
D. Execution order + Polymorphism ? :
The most simple way to handle both Custom Action Family polymorphism (Member overriding its Family Custom Action) and that execution order feature is to consider Listener Blocks from the Family custom action and the ObjectMember overriding custom action as related to 2 totally different Custom actions. Which means each of them have their corresponding Listener Blocks with their execution order, but the execution order of Family Listener Blocks doesn’t affect execution order of Object Member Trigger Events and vice versa.
Maybe the only thing that could be interesting is the ability for the “super call” (Object Member calling back its Family custom action) to be set as asynchronous, so the user could add a “Wait for previous Actions” to make sure the whole Family Custom Action and all its Listener Blocks are executed before continuing.
E. Asynchronous ? :
The Execution Order feature would require the corresponding actual Function/CustomAction block to be set as “Asynchronous”. Only then the Execution Order of corresponding Listener Blocks can be edited, otherwise, their parameter is grayed out and is only equal to 0.
That would be the only manual set-up the user would have to do : setting up the actual Function/Custom Action as asynchronous to be able to use this feature. So there would not be any overhead for Functions/Custom Actions by default.
Listener Blocks, however, would not need any Asynchronous boolean to be set-up. Construct would automatically detect under the hood if there are same Listener Blocks with highest Execution Order Number, and handle the Event Listener automatically under the hood.
II. NEW ACTIONS : Call Function / Call Custom Action by Name
(this isn't related to "Listener Blocks", it's just an other feature related to Custom Actions/Functions)
Add a new “Functions” action “Call Function by Name”
Add a new Object action “Call Custom Action by Name” (for Objects with at least 1 custom Actions)
Both actions could feature a string autocompletion with all possible Functions/Custom Actions Names.
The Function/Action parameters are set in a second String field following this syntax "Param1, Param2, Param3"
This would allow advanced users to create much more dynamic system without wasting all the UX benefits of C3 built-in Functions for the other users. For example, by playing Functions/Custom actions based on values stored in string variables at runtime.
This feature would also work really well in combination with this other feature request I made on the platform
Access hidden enums thanks to a new system Expression.
✅ construct23.ideas.aha.io/ideas/C23-I-69
This way we could have autocompletion and auto-update of Function/Custom Action Name strings everywhere thanks to those system expressions.
We could access the Functions/Custom Action names thanks to system expression such as
EnumFunctions.MyFunction1 (so we could do "Function : Call function by name [EnumFunctions.MyFunction1]")
EnumCustomActions.MyObject.MyCustomAction(so we could do "MyObject : Call custom action by name [EnumCustomActions.MyObject.MyCustomAction]")
So if later I change the “MyFunction1” name to “Enemy_Death” in the built-in Function block, then the Function : Call Function by name would now be set to EnumFunctions.Enemy_Death automatically. No need to change a bunch of “MyFunction1” strings manually.
This would allow us to create systems where we store functions names on variables easily at runtime and then just call a Function based on that variable !!
=> Endless new possibilities and capabilities for the C3 engine.
III. CUSTOM EXPRESSIONS (= Custom Actions with return value)
Here is a self-explanatory mockup :
- Basically add a Return Type dropdown list parameter to custom Actions.
- It would work the same as returned Function.
- You can use them using an ObjectType.MyExpression(MyParams) expression.
- You specify the value it returns thanks to a new Object action called "Set custom expression return value" you put in the special Custom Expression (=Custom Action) event block
- The special event block would be named "MyObject expression MyExpression1 -> Number" like in the mockup.
Custom Expressions, as return type Functions, would not support "Listener Blocks" (Suggestion number 1 above). Only Functions with no return type can have an unlimited amount of linked Listener Blocks
Thanks again for all the amazing stuff coming to C3.