godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
89.05k stars 20.19k forks source link

Idea: a method to force a condition to trigger only once #4514

Closed blurymind closed 4 years ago

blurymind commented 8 years ago

This is a very common thing in games programming. You have an if case, that you need to get checked on each frame.

So this if case, the condition get's true for more than one frames - lets say it's true for 60 -80 frames. Whats inside it will get executed 60-80 times. If you are spawning an object - gets spawned 80 times. If you are playing a sound effect - 80 times.

What if you need it to execute only one time - on the frame the condition became true. Very often you do. Please correct me if I am missing something, but you have to create a new variable - to switch it on and off, and sometimes on again - it's messy.

Now a lot of hardcore programmers out there might get annoyed I am proposing this, but not why have a common method to force the block of code in an if case to trigger only once while true? It's so common that even python programmers have been looking for it: http://stackoverflow.com/questions/4103773/efficient-way-of-having-a-function-only-execute-once-in-a-loop

more user friendly competitor game engines already have that - its called 'trigger once' condition. When you add 'trigger once' to a condition - whats in it executes only once:

construct2 has it (clickteam also): https://www.scirra.com/manual/124/system-conditions Trigger once while true Turn an ordinary event (which is tested every tick) in to a trigger. For example, if an event plays a sound when lives equals 0, normally this event runs every tick. This plays about 60 sounds a second and would sound pretty bad. Adding Trigger once while true after the other conditions makes the event run just once when it first becomes true. This makes the previous example only play a sound once the first time your lives reaches 0. It must be the last condition in an event.

This could help gdscripters use less convoluted code to get what they want, create less utility variables and switches.

But what would the syntax be? I think maybe something like:

time= time+delta if time>500 and trigger_once: get_node("sound").play("hit")

This is a very crude example. Yes I could just as easily put if time=500 instead. But sometimes you cant. sometimes you are forced to use > because it's likely that the frame gets skipped, maybe something else will force you to be very specific and not be able to limit the execution to only one frame in which it will be true. How many times have you had to make arbitrary new variables to force it to trigger once. We can get cleaner code if we had a way to force it like that

Please add to gdscript a trigger_once condition - to force the events of the met condition to execute only once - on the first frame the condition is met.

Ace-Dragon commented 8 years ago

In my opinion, there might be a few other ways to make clear that a statement is to be triggered once, one could even argue we're adding an entirely new component to GDscript (let's call it the statement flag).

trigger_once(if time > 500): if time > 500: (trigger_once) [trigger_once(if time > 500)]:

The benefits of these syntax schemes might be that you can theoretically do them on any statement in the code (though it'd be recommended to flag the if statement itself if you need a lot of logic done just once). As an example... trigger_once(time+=500)

blurymind commented 8 years ago

@Ace-Dragon I like this syntax too: trigger_once(if time> 500)

good thinking!

I suggested it to be an option that you append to an if case, because that is how other software with the feature does it. But it could just as well be a new type of a condition block that you put events in in order to force their execution only one time when it becomes true. A trigger!

for example we could nest the events under a trigger, which could be nested under an if case or other type condition check:

if time>500:
 trigger_once: 
  stuffHappens()
  otherStuffHappens()

or

if time>500:
 trigger_once: stuffHappens()

It is good to note that the trigger once will trigger the event once when a condition is met. But it will also trigger once again if the condition becomes false and then true again. Trigger once forces it to execute one time when met. It is not limited to trigger it only one time in the life cycle of the application. Instead it limits it to execute one time in the sequence of frames in which the condition is true.

We could call it 'the trigger operator' ?

In any case - whoever implements it should decide what the best syntax would be.

vnen commented 8 years ago

While this is cool and I'm not dismissing the possibility of such functionality, I can't see any implementation of this that does not includes a variable to keep track of execution. So at best (unless I'm missing something) this is a syntactic sugar for an underlying boolean flag. It is the way you don't want to do that looks clunky just under the hood, which might be good per se.

You also have to consider in the syntax something reset the trigger upon some condition. If you want to play a sound when hit, you likely want to play the sound once but also in every hit. So when the sound is finished you should reset the trigger so it can be played again. That requires an identifier for the trigger and a way to reset it (such as reset_trigger("my_trigger")).

akien-mga commented 8 years ago

You also have to consider in the syntax something reset the trigger upon some condition. If you want to play a sound when hit, you likely want to play the sound once but also in every hit. So when the sound is finished you should reset the trigger so it can be played again. That requires an identifier for the trigger and a way to reset it (such as reset_trigger("my_trigger")).

So start the trigger in some place, reset it in another place. Wait, are we talking about checking against a boolean variable? :D

blurymind commented 8 years ago

The whole point of it is not to have to start and stop it via booleans - that weight is taken away by this feature.

A trigger means that whatever is inside it triggers one time and switches itself off. But It keeps track of the if case thats you gave to the trigger - whether if is true or false and decides when to trigger once again. If it keeps track then no - you dont have to make the programmer reset their trigger. The trigger will know when to reset itself because it keeps track when whats parsed to it has become false again.

vnen commented 8 years ago

Ok, got it. It's sort of a special case for while: It runs the function once when the condition is true and reset the flag when the condition becomes false. Rinse and repeat.

In this case, none of this can work unless you make the parser backtrack:

if time>500:
    trigger_once: # Won't work
        stuffHappens()
        otherStuffHappens()

and

if time>500:
    trigger_once: stuffHappens() # Won't work

The problem with those is that trigger_once will be completely bypassed on false, since the if block won't be executed, and then it won't know when to reset.

We could add a once control structure:

once time > 500:
    do_stuff()
    do_other_stuff_too()

I also have some problem with the semantics of the word "once". It seems to me that it will happen everytime once the condition is true. I guess "trigger" might make more sense for a oneshot thing.

And, of course, never seen this in any programming language that I know. It doesn't mean we can't do it, but the impact of this (in memory, speed, etc.) should be carefully thought.

blurymind commented 8 years ago

I made a little schematic of how a trigger operator's life cycle could work: triggeroperatorlifecycle

blurymind commented 8 years ago

@vnen I like your syntax best so far:

once time > 500:
    do_stuff()
    do_other_stuff_too()

Seems to make most sense and requires much less typing. This would be super useful to have!

So it triggers the events once when the condition is met, waits for the condition to become false and then true again - and if it does - then repeats. Exactly!

Btw my schematic might not be the bestest implementation idea. The important part is that everyone gets the gist of what we can do with it and why it is very useful.

blurymind commented 8 years ago

I think the only possible initial confusion from the word might be the suggestion that this will trigger only one time in the life cycle of the application, rather than only one time when a condition is met sequentially for more than one frame.

The truth is that I would never have any use of it - if it triggers only one time in the life cycle of the application and thus will not even assume the function would work that way.

I think the word 'once' is clear as to what it does. But if we delve more into it, i guess we could also look at other possible word to use. 'on', 'at' or 'upon'

Ace-Dragon commented 8 years ago

To Vnen.

I'm not really sure if we want to go the route of using it in a way where it essentially becomes an alternative version of the if statement (because otherwise it can only be used with 'if' type scenarios and ignores the other statement types in GDscript).

With my suggestions, you can also do something like... while time > 500: (trigger_once) and for hours in timeList: (trigger_once) and

if time > 500:
    #do stuff
else: (trigger_once)
    #do this other stuff once

and even

for hours in timeList:
    if hours == 3:
        break: (trigger_once) #only break at this value on the first iteration

It could even work for more statement types that are yet to be added (should they be added in the future as mentioned in the documentation) switch(hours): (trigger_once)

blurymind commented 8 years ago

@Ace-Dragon not sure if I would ever use it in some of the cases. The for case for example I cant come up with why I would need a trigger once. We can already trigger once in a for case, even when it continues iterating. Right? The for case is not like the if case - it runs once, iterates through a list and gets the job done. Doesnt cycle every frame when I use it - that would be expensive. Perhaps I havent used it in other ways. I dont know.

The goal of the trigger is to address the need to limit the execution of events to one time - while their condition is true for more than one frame. That usually happens in simple if cases - something that is continuously checked every frame.

About the syntax, I think that in terms of actually getting this implemented, a trigger once would make more sense in @vnen 's approach. And if you want to use it the way you do, we could still nest it like this:

if time > 500:
    #do stuff
else: 
   once: #do this other stuff once

I am assuming that if the syntax worked the way you want it, more internal parts of gdscript in godot will have to be touched to get it to work.

blurymind commented 8 years ago

Actually I am not sure if nesting it would work - now that I think of it. It needs to check whether something is true or false constantly - like an if case. Otherwise How would the trigger know how to reset itself?

It probably wouldn't work like a for case. so in a lot of ways you cant apply it everywhere - or at least I am not sure why or how.

But does it have to? I think it should do one thing and do it well - act like an if case that limits to one execution the events you have given it once true - even if it's true for longer than one frame.

blurymind commented 8 years ago

An open source example of triger once condition block can be found in gdevelop: https://github.com/4ian/GD/search?utf8=%E2%9C%93&q=trigger+once

it is perhaps interesting to see how it is implemented there. Gdevelop is kind of like a clone of construct2. It has very similar approach to coding.

vnen commented 8 years ago

As I said, this is unpreceded in programming languages that I know, so I'm trying to understand what's being proposed here. Let me comment on the usage suggestions by @Ace-Dragon:

while time > 500: (trigger_once)

How's that supposed to work? Only run the loop once? If so it's not a loop. If it's a loop, it won't run only once. Same thing applies to a for loop.

if time > 500:
    #do stuff
else: (trigger_once)
    #do this other stuff once

This is possible with my suggestion, but requires a bit of repetition:

if time > 500:
    #do stuff
once not (time > 500):
    #do this other stuff once

And how's this supposed to work too:

for hours in timeList:
    if hours == 3:
        break: (trigger_once) #only break at this value on the first iteration

Do you realize that break stops the loop? If it stops always in the first iteration, it'll have only one iteration. If it's supposed to stop only if condition is true in the first iteration, it means that trigger_once has also to keep track of an iteration counter. Actually, every for will have to keep track of this and use if there's some trigger_once inside eventually. For this there are better alternative syntax, such as:

for hours in timeList:
    if loop.counter == 1 and hours == 3:
        break

This loop counter is much more useful than the OP, but also implies a higher memory allocation and usage (creating an object for every loop).


While I can see a great value in thought processes from non-programmers, you all have to understand that in the end this is all an algorithm. So if we were to add such suggestions, we do have to think of a program that solves the problem in a general fashion and uses the structures provided by the underlying platform. We need to build a basis for the higher-level language using the low-level barebones, which is not always easy, viable or even possible

Also, I want to see real use cases to the proposals, not "someone might sometime need something like this perhaps" (see this section of our FAQ).

@blurymind I can see that the code you linked uses an identifier, so it knows what trigger to check or reset. It indeed does need an identifier, as I said in my first post, even if it's internal. But I personally can't see a way for this to actually work without an explicit identifier, which makes my syntax proposal bogus. Maybe a thing like trigger my_identifier once time > 500: is more viable, but again this needs to be properly thought. And I'm pretty sure I can solve this with a GDscript singleton although in a clunkier fashion (I will try this later and show my results).

blurymind commented 8 years ago

It is unprecedented, but its commonly used and needed in terms of controlling flow in a game. All the stack overflow threads are a proof. Some of the tools in unity for state machines also have it built in. Its important to understand where it has its use, so as not to expect putting it in flow logic where it would make no sense. @vnen i appreciate that you are looking into this as i strongly believe it will make life easier and gscript code cleaner

blurymind commented 8 years ago

Another example is Unreal's blueprint system, which has a Do Once node: https://docs.unrealengine.com/latest/INT/Engine/Blueprints/UserGuide/FlowControl/#doonce doonce_example

The DoOnce node - as the name suggests - will fire off an execution pulse just once. From that point forward, it will cease all outgoing execution until a pulse is sent into its Reset input. This node is equivalent to a DoN node where N = 1.

For example, you could have the network for an opening door run through a DoOnce, and that door would open only one time. However, you could tie a trigger event into the Reset, which will cause the door to be accessible once the trigger has been activated.

I think you could look at it's source code if you wish - i believe it's written in C++

It seems to work almost exactly as we think ours should.

If godot is really planning to do a visual scripting in the future, having this operator in gdscript in advance would be useful to have under the hood. Even without a visual scripting system, it is damn useful in terms of controlling flow with less code and creation of new boolean variables all over the place.

A key point of this feature is that the programmer should not have to create a new boolean variable and keep track of it. Instead the trigger would do that for us under the hood.

blurymind commented 8 years ago

@mateusak I am trying to wrap my head around your approach. So a trigger could be a type of a new variable that is similar to the boolean - one that automatically resets itself? Please explain more what makes it different than using a boolean?

In which case - on the question of how we create it, I believe that it would be wrong to create it with the same syntax as the boolean.

var c = true #this is a bool

instead maybe:

var c = Trigger()

So you are saying that with this type of variable, it starts as false always, so we dont need to give it a true or false value. I am trying to not get too confused so far.

I think the problem is that we need to send a 'reset' signal to the trigger (unreal's approach)- or let it do that on its own (construct2,some unity state machines, gdevelop approach) . We need to feed it with conditions and also give it the actions to do one time when the conditions are true. How would that work in your syntax?

If a trigger is a variable type, do you use a method to do those things?

So far @vnen has suggested the cleanest and least confusing way to do a trigger in gdscript imo.

If we do the unreal approach and allow the programmer to manually reset triggers - then I think it shouldnt be mandatory as it will make them harder to use in a non-visual programming environment such as gdscript. In the end it's good to experiment with an implementation of a trigger - try it out in the other engines and think about it.

blurymind commented 8 years ago

@mateusak so a trigger in your case is a boolean pretty much, that toggles itself off or on when checked.How do you reset it?

The reason that I thought it should be like a new type of if case is because the reset depends on the condition that is fed to the trigger to become false again.

Without a reset, your trigger is the same as using a boolean like this:

var k = true
if (time > 400 and k):
   do stuff
   k=false

Which could leave us with situations where you can paint yourself in a corner again.

The other problem with that is of course clearly differenting the new variable type from the boolean

blurymind commented 8 years ago

@mateusak isnt that specifically designed for animations? I dont think that unity developers use it to control flow the way we want to here.

I am trying to find more documentation on them: http://answers.unity3d.com/questions/600268/mecanim-animation-parameter-types-boolean-vs-trigg.html

blurymind commented 8 years ago

@mateusak in your example - you change the boolean, but you dont really use it in the if case where it should force the events to happen once

blurymind commented 8 years ago

Wait i am starting to understand what you mean, so in your idea the boolean could have a third state 'trigger' then it should be

var mytrigger = trigger # this is a boolean, but instead of true or false, it is set to trigger

blurymind commented 8 years ago

@mateusak in your approach we at least have the user create an identifier for the trigger. If we give the boolean the ability to act like a trigger, then I think resetting it should not be done by setting it to true or false. That would make the boolean act like a normal boolean again.

Instead perhaps to reset it, we set it the same way we created it

var k = trigger #creates a boolean and sets it to be a trigger
k=trigger #resets the trigger

Or perhaps it should reset itself, whenever the conditions that it is pared with become false again. At least that is the expected behavior in gdevelop and construct2

I am still not completely sure the boolean is the best approach, but now we have more fresh ideas . Lets see what @vnen thinks too :)

What do you guys think on the resetting part? Should it do that on its own, or should the user reset it manually somehow? What would the pros and cons be?

vnen commented 8 years ago

As I thought, it is very simple to do this with a singleton. Here it is:

extends Node

var triggers = {}

func make_trigger(id, object, method):
    if not triggers.has(id):
        triggers[id] = {
            "valid": true,
            "object": object,
            "method": method
        }

func trigger_once(id, condition):
    if triggers.has(id):
        if condition:
            if triggers[id].valid:
                triggers[id].object.call(triggers[id].method)
                triggers[id].valid = false
        else:
            triggers[id].valid = true

Add that as singleton in your project settings. To use it you have to create a trigger first (with make_trigger) with an object and a method to run and then use it via the identifier you chose. Example (assuming you added as a singleton named trigger):

extends Node

func _ready():
    set_process(true)
    trigger.make_trigger("my_trigger", self, "do_stuff")
    pass

func _process(delta):
    trigger.trigger_once("my_trigger", Input.is_key_pressed(KEY_SPACE))

func do_stuff():
    print("Doing stuff...")

This prints "Doing stuff..." when you press the space bar but only once even if you hold it. It resets when you release the space bar.

It's possible to tweak this so you select the method to run on trigger_once. You would still need an identifier (unless you can infer it from the object-method pair), but then you could remove the need to make a trigger first.

blurymind commented 8 years ago

@vnen I am going to give this a try! Very nice :D Godot is so flexible!

On having trigger once as part of godot - is there interest in making it a part of gdscript?

I looked more into this - its a common problem. At some places they call it 'how to avoid a recursive trigger'. So a trigger once is a tool specifically designed to help avoid recursive triggers. http://amitsalesforce.blogspot.co.uk/2015/03/how-to-stop-recursive-trigger-in.html

vnen commented 8 years ago

On having trigger once as part of godot - is there interest in making it a part of gdscript?

I'm sorry, but I don't think so. I'm not willing to mess with the GDScript parser, at least not without @reduz approval. Though if someone wants to tackle this and make a PR, I'm not against it.

blurymind commented 8 years ago

ah fair enough. Thank you for the singleton code. :)

blurymind commented 8 years ago

@mateusak our idea - I helped shape it :p Until someone actually puts it in godot and we try it out in practice, nobody will know if its good or bad. Its up to @reduz to decide if gdscript will benefit from a trigger and how a trigger should work. Perhaps someone experienced in c++ would see this thread and laugh a little at some of our crazy syntax, but maybe they will come up with a better way to do it

Btw here the creator of construct2 explains how his trigger events work:

https://www.scirra.com/forum/trigger-once-while-true_t119456

It basically means "Is true this time and was false last time", or in other words, "is the first time this is true".

Well I don't see how you'd even accomplish it with a class or method? You basically just want a boolean for "was true last time". Also in many programming languages you'd actually explicitly fire an event when something happens and allow callers to register for that event, instead of testing a condition regularly and triggering something when the state changes. It's a case of where typical C2 style is different to traditional programming.

In C2's case, it stores a separate boolean per condition, since each condition needs to track its state independently. Actually the specific C2 implementation is based on tick counts, but if you're just learning to program, that's not really important.

blurymind commented 8 years ago

@vnen just tested your trigger on my mini rhytm game experiment. It works like a charm. It's a real shame it's not going to be built into gdscript! This is very very nice.

I guess the triggers in other engines will always have the advantage of not having to be created by the programmer at the start for each instance they would be needed. But thats not a big deal, because now at least we get this with fewer lines of code and it's clearer when read. The trigger once logic is easy to distinguish from other code. Cool stuff!

I will use it literally on everything I do in gdscript from now on :+1:

If I knew c++ and was as experienced I would make a pull request for a built in trigger_once method in gdscript that does exactly what vnen's does - but instead of creating it the ready(): process, I would like to not even have to create it manually - the engine could create it when its called for the first time and keep track of it under the hood. Thats how the others do it and its very convenient.

bojidar-bg commented 8 years ago

@mateusak ~ Here is some code that closely follows your idea:

#once.gd
var did_run = false
func run_once():
    var allow = !did_run
    did_run = true
    return allow
func reset():
    did_run = false
func set_state(state):
    did_run = !!state
func get_state():
    return did_run

Usage:

const Once = preload("res://once.gd")

var jump = Once.new()

#...

func _process():
    if Input.is_action_pressed("jump"):
        if jump.run_once(): # Inside, so that the else part works correctly
            print("JUMP!")
    else:
        jump.reset()

    jump_available_sprite.set_opacity(!jump.get_state())

Also, if you want to make it into a singleton, here is a quick idea:

extends Node

func new():
    return preload("once.gd").new()
blurymind commented 8 years ago

@bojidar-bg I dont know why but in my test your approach did not trigger a print.

When I disabled the reset - it triggered one print and then stopped triggering for all the steps after. In my example the code is supposed to trigger a print once at specific points in time - the time is taken from a json file.

When replaced at the same place with @vnen 's approach - it started working again.

I like your syntax a bit more though. get_state could be useful too!

bojidar-bg commented 8 years ago

@blurymind ~ My bad, the else was wrong, I'll edit my comment :smile: EDIT: I missed a colon in the code.... oops.

blurymind commented 8 years ago

With this change:

    if Input.is_action_pressed("jump")
        if jump.run_once(): # Inside, so that the else part works correctly
            print("JUMP!")

It still doesnt print anything for some odd reason.

blurymind commented 8 years ago

@mateusak have you managed to successfully try and use the example code provided by @bojidar-bg ? What do you think

blurymind commented 8 years ago

you know if this

const Once = preload("res://once.gd")

var jump = Once.new()

if Input.is_action_pressed("jump"):
        if jump.run_once(): 
           doThing()
  else:
       jump.reset()

could internally create a new boolean automatically, keep track of it and reset it automatically when the else happens, it will really boil down to a single line of simple code (instead of 5 scattered around) that does what we want:

if Input.is_action_pressed("jump") and run_once:
  doThing()

This is the ideal case scenario imo. It's how the others do it. Is that so impossible to achieve by extending gdscript?

Not that it worked or anything. :)
@bojidar-bg can you share a gdscript code example where your approach demonstrates itself with a print? I will try it again on a simpler project - one that tests input like yours

27thLiz commented 8 years ago

Just for the record: when testing Input, it's best to use the _input(ev) callback. Something like

if ev.is_action_pressed("jump") and !ev.is_echo:
    doThing()

Will trigger doThing()only once, as all the following pressed events will be echos.

blurymind commented 8 years ago

@vnen in your approach,

trigger.make_trigger("my_trigger", self, "do_stuff") There are two downsides to it, that are quite serious limitations:

  1. You are forced to create a new function for every event you want to happen once when triggered
  2. Most importantly that function can not take any parameters!!

How do you add parameters to it?

blurymind commented 8 years ago

@Hinsbart can you provide a more complete example of the is_echo? Is it possible to use it as a trigger for non input events?

The prime need for a trigger is within fixed_process(delta) and process(delta) events.

In this case I want something like is_echo to become available to non-input functions too! @vnen does that seem like a less extreme proposal to add to gdscript? What does @reduz think?

27thLiz commented 8 years ago

@blurymind, you can think of ev.is_echo() as an indicator if the event is happening the for first time. Example:

Frame1 - User pressed key, send event with pressed = true, echo = false
Frame2 - User is still pressing, send event with pressed = true, echo = true
Frame3 - User released key, send event pressed = false, echo = false

And yes, it's only available for objects of type InputEvent. But it doesn't make sense to make this available anywhere else. How would that even work? :P

blurymind commented 8 years ago

@Hinsbart we are trying to figure that out. You tell me. Me and some other users supplied free and proprietary game engine examples of the feature functioning - without need to write many lines of code at different locations of your script, their source code, made a diagram... What else do you need :p Now we have a couple of prototypes - one of which works - but is limited.

@vnen suggested the one that works - but does not support function parameters.

@bojidar-bg suggested another - but that does not work at all. It doesnt trigger the event at all.

Today I tried it on a clean scene with a single node2d node and a 'jump' event added to the project's inputmap. in my case the name of the trigger script is once.gd instead of 0nce.gd the code is as follows:

extends Node2D

const once = preload("res://once.gd")
var jump = once.new()

func _ready():
    set_process(true)

func _process(delta):
    if Input.is_action_pressed("jump"):
        if jump.run_once(): # Inside, so that the else part works correctly
            print("JUMP!")
    else: jump.reset()
    #jump_available_sprite.set_opacity(!jump.get_state()) 

nothing happens.

27thLiz commented 8 years ago

Well, that seems like a communications issue. I was just talking about ev.is_echo, which really doesn't any make sense outside of _input(ev) :P

blurymind commented 8 years ago

@Hinsbart xD point taken. Thank you for explaining how it works.

bojidar-bg commented 8 years ago

@blurymind ~ Lol, seems like I messed up my reset func. I'll fix it, but mind that I was giving example code, not a working solution :smile: .

blurymind commented 8 years ago

@bojidar-bg after the changes you made - it works now!!! You approach has the big advantage of being able to be paired with an if case, and thus its also possible to run a function with parameters once (or do anything else). I will therefore use it instead of @vnen 's

It is awesome! :+1: Why cant we have something like this built in

vnen commented 8 years ago

I'd do it a little bit differently, as @bojidar-bg's code is not far from a boolean flag.

#once.gd
var did_run = false
func run_once(condition):
    if condition:
        var allow = !did_run
        did_run = true
        return allow
    else:
        self.reset()
        return false
func reset():
    did_run = false
func set_state(state):
    did_run = !!state
func get_state():
    return did_run

So you can use it like this, without a need to manual reset:

if jump.run_once(Input.is_action_pressed("jump")):
    print("JUMP!")
# No else block needed
blurymind commented 8 years ago

@vnen This is better as a quick trigger, as we also dont have to nest two if cases in order to get it to trigger once and reset properly- yours looks cleaner and tidier. is it possible to also do it in a way where we dont even need to create a new variable for each instance we need the quick trigger to do it's thing?

having a reset in @bojidar-bg 's approach allows for the user to reset the trigger once with a logic that is detached from the trigger once condition. That is similar to how Unreal's doOnce blueprint node allows a reset input to come from anywhere, and it can be useful in some cases too:

Reset - This execution input will reset the DoOnce node so that it can be triggered again. For example, you could have the network for an opening door run through a DoOnce, and that door would open only one time. However, you could tie a trigger event into the Reset, which will cause the door to be accessible once the trigger has been activated.

I guess in an ideal world we would be able to create a trigger in two ways and use it

if run_once(Input.is_action_pressed("jump")):
    print("JUMP!")

Godot does the rest under the hood. This could be used for a quick automatic trigger, which does not give the programmer control, but also doesnt ask for more code to work.

think of it as a temporary local variable that is needed only for the event where its used - created automatically by the runtime - under the hood.

pros: no need to create a new trigger variable every single time you need to use a trigger pros: needs less lines of code to work and does work as expected for most needed cases cons: if you need a more specific user case where you need to be able to manually reset that trigger to work again, you cant address it directly, nor tell it when to reset.

var jump = run_once.new()

#...

func _process():
    if Input.is_action_pressed("jump"):
        if jump.run_once(): # Inside, so that the else part works correctly
            print("JUMP!")
    else:
        jump.reset()

    jump_available_sprite.set_opacity(!jump.get_state())

think of it as a global variable that other functions and events need access and control to.

pros: You have a bit more control over your trigger - you can reset it with some logic that doesnt have to do with the trigger's condition to trigger once. cons: You need to write and keep track of more code. Create a variable for each custom trigger once event and manually reset it. If you dont reset it - the event will automatically trigger only one time in the game and will not trigger once the second time it's conditions have become true after being false - until you reset it.

If I have to be honest - I will most probably use the Quick trigger most of the time.

bojidar-bg commented 8 years ago

@blurymind ~ The problems with having this in the very core are two:

var did_jump = false
func _process():
    if Input.is_action_pressed("jump"):
        if not did_jump:
            did_jump = true # The only additional line needed.
            print("JUMP!")
    else:
        did_jump = false

Finally, when the asset store / addon library comes in 2.1/2.2, I guess such small-and-nifty libraries would be made every day for whoever wants to use them.

blurymind commented 8 years ago

@bojidar-bg I see. Thank you for providing the awesome utility function. Also much thanks to @vnen who came up with an implementation first.

It just saves a lot of typing and extra code. :) you know, it all adds up when you repeat this many times. You may not see much value or universal use of it, but I do and I appreciate that you made that script.

I dont think it's a specific bloat that only few can make use of. It is not a request to implement a bitcoin class or something like that. There is a good reason unreal engine and unity have a trigger once function. it's a fundamental tool for easier control of flow - to avoid recursive triggers. Recursive triggers are a common problem to game development that is obvious to see in search results.

None of the implementations so far are as convenient as whats in other engines - as they still require you to create a variable for each time a trigger is used. But they are nice and they do save time and save repeating same blocks of code. it also helps make code more readable.

Warlaan commented 8 years ago

I disagree that a feature like that makes code more readable. Imho it makes the code literally unreadable in that it vanishes behind a keyword. You can't read what the code does anymore, you have to know it. I would really appreciate it if we tried to keep out features that aren't really necessary and are just making something a little more convenient, otherwise we end up in a mess like the one C# is in. Just take this example:

Car b = a; b.size = 3;

If this is C++-code then I can tell that the first line allocates memory on the stack and fills it with a copy of the data stored in a. The second line changes one value in that block of memory. If this is C# code I can't tell you whether b is a copy of a or a reference to a. If Car is a struct it's a copy, if it's a class it's a reference. I even can't tell you whether the second line is an assignment or a function call. "size" might be a property and the get-method of that property could actually be quite expensive, it might update the whole Car-object, it might store the new value in a database... You can use getters and setters in C++, you can create references and copies, the only thing you are "missing" are C#'s features that allow you to save a few characters while sacrificing a lot of information.

Of course if you intend to you can mess up C++ code just as much by overwriting operator= in a weird way, but while that is considered bad style using properties in C# is completely normal.

I have no issue with adding a helper function or object like the ones suggested above, because that way you can still look up the code and read up what happens (be it in GDScript or C++), but please don't use language features in a case like this.

And yes, there is a reason why other engines have functions like that (btw. I couldn't find anything about Unity's trigger once function, does it really have one?), but there's also a reason why Unity or Unreal take Gigabytes of space and a lot longer to install than Godot, why their code base is not as readable and clean etc.. Different engines follow different philosophies, that's the main reason why it makes sense to have more than one engine, so imho asking for a feature because another engine has it doesn't really make sense.

EDIT: I hope I don't come across as too negative and offensive, but as a teacher I regularly see people using unnecessary language features just because they are there without understanding what they actually do. That's why I have a very strong opinion on this matter and why I may come across as more aggressive than I intend to.

RFEphemeration commented 8 years ago

Do you have access to the function call stack in GDScript? Or some other identifier of the context? That would allow you to use a singleton that distinguishes each call to run_once(). Removing the need to declare your triggers.

Accessing the stack trace is do-able from c++ on windows , linux, and macOS.

This is a rather dramatic solution to an incredibly simple problem, but it seems more broadly useful/powerful.

EDIT: Here is a stack trace function available only while debugging. I'm not sure why it's only available while debugging. Perhaps because of the storage of names in the stack? The stack itself is information that the program has (needs to have) even when not debugging.

vnen commented 8 years ago

Since GDScript is just for scripting and completely integrated with the engine, likely there's no need for the runtime to keep a call stack of the script. It can just call the functions and have the C++ stack as it is, which likely would make little sense for scripting. In runtime this would be unneeded overhead.

Also, for a topic surrounding beginners, analyzing the call stack seems much harder (both to write and to read) than simply using a boolean flag.