Skip to content

Feature idea: Designing class-specific variables inside member functions #67

@HoneyPony

Description

@HoneyPony

One pattern that is reasonably common in Godot is to define timers and other small state machines inside a script.

Of course, async can be used for a lot of these use cases -- it is often possible to have a behavior that is awaiting a timer and then doing whatever needs to be done on the timeout. But sometimes you do need a manually implemented timer for more control.

One thing that has always stood out to me about manually implemented timers is that they are very few lines of code. For example:

var timer := 0.0

func _process(delta: float) -> void:
    if timer > 0:
        timer -= delta
        if timer <= 0:
            do_timer_action()

    # to reset the timer: timer = 2.0

This kind of timer in particular is useful if you want an action to occur exactly once after the most recent occurrence of some other action. For example, maybe some animation or particle effect occurs 2.0 seconds after, say, the player interacts with a button, but if they repeatedly interact with the button the delay needs to be refreshed. This kind of logic is not as easy to express with a simple await call.

The most annoying thing, however, about these "5 lines of code" is that we have to define one of them at the class scope, and one of them at the function scope. This takes meaningfully longer to do, in my experience, than writing lines of code in one location, and I find I often scatter the variables in somewhat meaningless locations, maybe near the _process function, or in other poorly thought out places.

This leads me to my feature idea. What if, instead of having to declare the class member variables directly inside the class block, we could also declare member variables inside of functions?

This idea is not completely novel. It is similar to static variables in C, except that those are global variables. But it would allow us to implement these timer patterns in pretty straightforward blocks in PonieScript:

class Player {
    fun _process(delta: float) {
        @member var timer = 0.0; // @member vars must have an initializer
        if timer > 0 {
            timer -= delta;
            if timer <= 0 {
                do_timer_action();
            }
        }

        // to fire: timer = 2.0
    }
}

(If we compare this to the async proposal for PonieScript:)

class Player {
    fun fire_timer() {
        timeout(2.0).await;
        do_timer_action();
    }

    fun _process() {
        // to fire: fire_timer().induce
    }
}

(To be sure, the line overhead in PonieScript is definitely quite a bit more. It's ~7 lines total to define the timer logic, as opposed to ~2 when using async.)

The main limitation is: how do we actually write to this @member var timer later, particularly from a different function?

One option would be to namespace it, like _process.timer = 2.0. Another option would be to add all of these variables to the class's own scope, which would maybe be the most straightforward option.

Finally, on the topic of timers in particular, it might behoove us to either have reference types (e.g. &float) or simply a builtin method that can act on a float, to wrap all of these if statements in a nicer piece of logic. Something like:

if timer_tick(timer, delta) {
    // do timer action
}

All that timer_tick would have to do, is, essentially:

if timer > 0 {
    timer -= delta;
    if timer <= 0 { return true; }
}
return false;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions