Custom Actions/Functions Suggestions (Listener blocks, Custom Expressions and Call by name)

2 favourites
  • 12 posts
From the Asset Store
Place blocks on a board, join them together to form even bigger blocks!
  • 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.

  • Great post dude! Very cool ideas, I hope some of this can be implemented down the line. 💛

  • CUSTOM EXPRESSIONS (= Custom Actions with return value)

    I really want these! Enemy.getHealth instead of Functions.getEnemyHealth(Enemy.UID) would greatly increase code readability.

  • So I see what functionality Overboy wants with the special trigger blocks, and I agree with him that something like this would be very useful, but I also agree with what Ashley said previously in the other discussion that functions should be executed in a single place or it can become messy pretty quickly, same with actions.

    This is a little mockup to show the issue, recreating the rotate action it becomes clear that the option of creating many executions of the same action/function call can lead to undesired/unexpected outcomes (here the object would rotate 180° instead of 90°).

    You could say, well just don't do that, and it is obviously easy to spot here, but once you trigger a dozens or so actions/functions with a single call spread all over multiple event sheets it becomes hard to follow.

    None the less I think the thing Overboy is proposing should definitely be an available functionality in C3, but I think of it more like a radio station and listeners.

    The radio is broadcasting information and doesn't care if 0 are listening or 1000.

    I think the newly added signal trigger fits that roll much better, but it doesn't have parameters so the info you can broadcast with signals is very limited.

    I think the actual programming term for this is the observer pattern. https://gameprogrammingpatterns.com/observer.html

    So maybe it would be better to keep functions and custom actions more to the point and add this functionality to the signal feature instead.

    But this is only my view on the matter, could well be that I missed a crucial point or failed to address some specifics.

    independently from how this should be implemented, I would like to know if the linked observer pattern is actually what you have in mind with the special trigger blocks proposal.

  • Thanks for your input TackerTaker.

    The Listener Block feature must be seen as if they were brand new Functions/Custom Actions, but automatically called at the same time and passing the same parameters as their "parent" Function/Custom Action.

    It shouldn't be seen as the same Function/Custom Actions defined in multiple places, as your mockup show. I agree this wouldn't work and it would be confusing.

    Instead it's like if they were 2 different functions under the hood, called with a single call and the same parameters. Similarly to how works a Delegate pattern for example. (I know this isn't actually like Delegate pattern)

    In fact the implementation under the hood would be exactly the same as built-in Function, but the user wouldn't need to set-up an extra Function, to set-up exactly the same parameters, and to call that extra Function at the same time in everyplace the original function and making sure that function is called with the exact same parameter.

    Also the execution order feature solve a lot of complex race conditions issues that can happen right now for any vanilla C3 Trigger Condition (including the new "On Signal Trigger") and are sometimes painful to workaround.

    Of course the point of this isn't to call the same logic twice as your "Enemy action rotate" Mockup shows.

    The same way it wouldn't make sense to execute the same logic twice while using any other C3 Condition. You would have the same issue if you clone any event block in C3 with the exact same conditions and actions. The mistake someone can make to execute the same action twice instead of 1 isn't specific to "Listener Blocks", it can be made anywhere in a project inadvertently.

    You would be easily able to find all "Listener Blocks" and the Function Definition thanks to the "Right Click Context Menu> Go to Function Definition" and "Right Click Context Menu> Find all function references", in case you need to Debug something. While it would be really more painful to do that exact thing if you would need to set up 12 different functions with exactly the same parameters called exactly at the same time as each of them would have a different name. You would need to look for the 12 different names using the Event Search feature, on top of all that extra painful set-up work.

    I don't think that feature should be done with Signal but as the same implementation as built-in Function (as Custom Actions are), so we would still have all the UX benefits, the parameters feature, and the amazing performance of Ashley's Function implementation that in fact, under the hood are executed only in one place.

    I'm using Chadori's Self Function extensively for a year for all my projects. Like 70% of all my "functions" were in fact Chaodri's Self Function that whole time, using polymorphism and multiple trigger blocks feature before Scirra even announced they were working on Custom Actions. So i can get why the need of Multiple Triggers doesn't seem obvious as Custom Actions were just released in beta, they're still bugged and nobody build several months-long project on top of them, push them to their limits.

    But i can guarantee I would never ever switch back to built-in Function/Custom Actions until this multiple Trigger Block feature is implemented. I just couldn't make my project at all without this.

    Still i'm happy for the other users that those feature were released, but they're still inferior of what i'm doing with Self Functions right now so I wouldn't switch to the new "On Trigger signal" and "Custom Actions" because of this.

    (If someone at Scirra wants to understand why this is so much important I can show them how i'm doing my projects by mail or Private Message.)

  • The screenshot was of course made in a way to easily spot the problem, but as I wrote in the description of it this is not how it would be in an actual project.

    Here is a more realistic mockup of how it might look in a real project, though they would probably not be on the same event sheet. ( the problem is the same as in the previous screen shot )

    How often, or if at all, this would become a problem probably depends on the person using it, but this is the point I was trying to make.

    I think sending Scirra your project where you make heavy use of the self function plugin is a great idea, and probably the fastest way to make progress on this.

    Understanding the needs and having something for comparison if they decide to implement something like this makes a lot of sense.

  • I just discovered that you can create custom actions (and hopefully custom expressions soon) pretty much for any object type! This is so incredibly awesome!

    Also, it would be great if we could convert existing functions to custom actions. I posted a suggestion:

    construct23.ideas.aha.io/ideas/C23-I-146

  • I've been holding off on exploring the custom actions, but what are the general pros and cons of custom actions vs. built-in functions?

    On the surface it "seems" like built-in functions but with copy-picked enabled.

  • I've been holding off on exploring the custom actions, but what are the general pros and cons of custom actions vs. built-in functions?

    On the surface it "seems" like built-in functions but with copy-picked enabled.

    You get polymorphism with families. when different object types have the same custom action.

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • I posted a suggestion on the dedicated platform for the Listener Blocks suggestion (what i previously called "Special Triggers") which would allow us to turn any Function or Custom Action into a Better Signal (passing parameters, featuring the same performance and UX benefits as Functions, and an execution order feature).

    construct23.ideas.aha.io/ideas/C23-I-165

  • EDIT : I released an addon pack including a Custom Expressions behavior and everything else i've been advocating for in this thread !

    overboy.itch.io/construct-3-object-signals-custom-expressions

    I posted a suggestion for Custom Expressions

    Vote here: construct23.ideas.aha.io/ideas/C23-I-336

    How it works :

    • It would work the same as returned Function.
    • You can use them using an ObjectType.MyExpression(MyParams) expression.
    • The special event block would be named "MyObject expression MyExpression1 -> Number" like in the mockup.

    Benefits :

    • It would support polymorphism
    • we would be able to create getter function very easily.
    • Would help to keep complex projects clean and readable.
    • No need to use Functions and passing UID parameter anymore
    • Just "MyObject.myexpression1" instead of "Functions.MyObject_myexpression1(MyObject.UID)"
    • Just "MyObject.myexpression2(Params)" instead of "Functions.MyObject_myexpression2(MyObject.UID, Params)" (look how long and unreadable it can get currently)
  • I posted a suggestion for Custom Expressions

    Thanks, voted.

    I'm curious why this hasn't been implemented yet. Custom Expressions seem like such a logical extension of Custom Functions.

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)