godotengine / godot

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

GDScript: a keyword that returns a method and yields the caller of that method #25511

Closed santouits closed 4 years ago

santouits commented 5 years ago

When I write imperative actions for my NPCs I use something like this:

func run_actions():
    say("How are you doing")
    yield(some_object, "some_signal")
    say("You don't seem the talking type")
    yield(some_object, "some_signal")
    move("player", 10, 1)
    yield(some_object, "some_signal")

func say(text):
    $label.text = text

But I would really like it to be more like this and to hide the implementation from my script:

func run_actions():
    say("How are you doing")
    say("You don't seem the talking type")
    move("player", 10, 1)

func say(text):
    $label.text = text
    yield_parent(some_object, "some_signal")

What are your opinions? I also would like to know if there are other ways to achieve that without making my custom scripting language.

sdfgeoff commented 5 years ago
for sentence in ["How are you doing", "You don't seem to be the talking type"]:
    say(sentence)
    yield(some_object, some_signal)

Actually, yields are good ways of accidentally hiding more complex state machines. This makes them easy to use, but you often end up with "logic errors" hidden in these state machines (eg when they span many objects, you can end up with resource lock that is extremely hard to diagnose and prevent).

So if you're hitting the limit of the "yield" keyboard, try making your state machine explicit

bojidar-bg commented 5 years ago

I think it is possible to do with something like:

func run_actions():
    yield(say(".."), "completed")

func say(sentence):
    # ...
    yield(some_object, "some_signal")
ghost commented 5 years ago

It may be somewhat related, but I did at one time desire that there would be another way to get a GDScriptFunctionState without having to call a yield first, or at least some way to have more knowledge and control over yields that may be dangling.

A failed attempt found here: https://github.com/godotengine/godot/issues/24122

santouits commented 5 years ago

Yeah thanks for the answers. I think I didn't make clear the reason for wanting gdscript having this functionality or some other functionality that can achieve the same thing.

The reason is let's say you want to make a framework or a modable game and want the users/modders to write scripts, or the artists. They will only use the API you give them and all the logic/error handling will be encapsulated in the commands, without explicitly yielding everytime you write a talk command for example, as it is now I don't see any other way than to make your own scripting language like Escoria did. It would be great for gdscript to have this, but I don't know how easy it is to implement it or if it is wanted for gdscript.

sdfgeoff commented 5 years ago

modders... They will only use the API you give them

In my experience, even level designers working in the same building as you won't use your documented API. Instead, they will find some arcane function deep within your code! Then one day when you change this function because no-one uses functions prefixed with underscores (everyone know's they're private, right?), everything will break..... True story, both for code and assets: why oh why does someone make some random asset in a level depend on the main characters clothing normal map? (today I had to sort out an issue very similar to this)

Fully separating what the modders/designers can control from the actual running code can be useful. For example, in the example I gave above, you could expose the list of things to say, and then you know exactly what the modders can and can't do. In this way you turn what would be code into ... data. And data is easier to manipulate: eg: load from a text file. But then, lo and behold, you have a simple scripting language.

For our next project, we're planning to do the main framework in C# or perhaps even GDnative so that we can limit what the level designers have access to (amongst other reasons - such as established code test frameworks, linters, etc).

pchasco commented 5 years ago

bojidar-bd posted the correct wait to yield for the completion of another coroutine:

yield(couroutine_func(my, args), “completed”)

Coroutine emits signal “completed” when it completes.

willnationsdev commented 5 years ago

Related to #21039.

muchitto commented 5 years ago

I have exactly similar use case for this kind of language/engine feature, and I made my own markup/scripting language, so that I can handle the execution state much smoother and terser, but if this would be part of the engine, I could do away my own scripting language entirely and just use this.

SolarLune commented 4 years ago

Hello, I'm also running into the same issue with writing an event system as the OP explains as an example.

I made a custom scripting language in my Godot project to plan out events (i.e. display a message box, wait a certain amount of time, trigger a choice, set a game flag, etc), but I thought it would be easier to replace that language with just GDScript, as then I wouldn't have to maintain a parser and continuously add on basic functionality to peform rudimentary tasks.

Using a function and yielding works almost exactly as I need, as that allows me to effectively pause an event sequence (a function) when a singular event (another function) processes and needs to wait a game frame. However, because it's not possible to yield multiple levels up the function stack, it's awkward to call methods and then yield depending on the result of those methods.

It'd be nice if there was a way to yield multiple levels up the call stack, as that would allow me to pause an event if extra game frames are necessary to proceed, while also allowing me to encapsulate commonly used events (like displaying a message) into functions. As an example, here's what I have so far (with help from @sdfgeoff's suggestion above). msg() is a function that displays a message box that returns false when a message is completely displayed and wait() is a function that waits a certain amount of time before returning false (otherwise, they return true):

var messages = [
    "Precipitation seems to be on the rise, while ambient temperatures drop...",
    "Interestingly, humans believe that cold weakens biological immunity.",
    "That is a farce."
]

for text in messages:
    while msg(text):
        yield()

while wait(1):
    yield()

while msg("... Still, I'm glad I am inside."):
    yield()

Ideally, I'd lose the yield and while statements.

As an aside, the second idea I had for this was to use function references, and store them in an array to loop over to actually execute later, but that would mean that anything an event sequence does would have to exist as a function first, which is not great, either. I wouldn't be able to just write code, I'd have to make functions out of everything first.


So for my needs for an event system, I believe GDScript needs to offer more control over function execution (things like jumping to labels within a function, yielding up several levels in a stack, etc). Another good feature would be a synchronous threading system (as then you wouldn't need to work with mutexes or semaphores as it wouldn't be for multithreading, but would still have the ability to leave the thread "idling" when it's necessary to wait for something to happen).

EDIT: Another alternative would be to add function inlining; then I could make functions that have the yield() command inline into the event sequence function, allowing an easy looping and escape process.

Jummit commented 4 years ago

I also would like to know if there are other ways to achieve that without making my custom scripting language.

This code is clean, super flexible, easy to use and works with the current yield system:

extends Node2D

class Action:
  var wait_for_object
  var wait_for_signal
  func exec():
    pass
  func _init(wait_for_object, wait_for_signal):
    self.wait_for_object = wait_for_object
    self.wait_for_signal = wait_for_signal

class SayLine extends Action:
  var text
  func exec():
    say(text)
  func _init(text, wait_for_object, wait_for_signal):
    self.text = text
    self.wait_for_object = wait_for_object
    self.wait_for_signal = wait_for_signal

class Move extends Action:
  var who
  var to
  func exec():
    who.position = to
  func _init(who, to, wait_for_object, wait_for_signal):
    self.who = who
    self.to = to
    self.wait_for_object = wait_for_object
    self.wait_for_signal = wait_for_signal

var actions = [
  SayLine.new("How are you doing", some_object, "some_signal"),
  SayLine.new("You don't seem the talking type", some_object, "some_signal"),
  Move.new("player", Vector2(10, 1), some_object, "some_signal")
]

func run_actions():
  for action in actions:
    action.exec()
    yield(action.wait_for_object, action.wait_for_signal)

Example of more complex actions:

class KickMove extends Move:
  var damage
  func exec():
    .exec()
    who.health -= damage
  func _init(damage, who, to, wait_for_object, wait_for_signal):
    self.damage = damage
    self.who = who
    self.to = to
    self.wait_for_object = wait_for_object
    self.wait_for_signal = wait_for_signal
dalexeev commented 4 years ago

@Jummit You proposed a rather interesting system as a solution. But this solution does not work well with nonlinear algorithms. Example:

func run():
    yield(act1(), "completed")
    if cond:
        yield(act2(), "completed")
    else:
        yield(act3(), "completed")
    yield(act4(), "completed")

Perhaps in the near future we will be able to write scripts like this:

func run():
    await act1()
    if cond:
        await act2()
    else:
        await act3()
    await act4()

Although ideally I would like this:

func run():
    act1()
    if cond:
        act2()
    else:
        act3()
    act4()

See also: #31494

SolarLune commented 4 years ago

Yeah, what I ended up doing for my case was basically the following:

  1. Make a class called an Event that represents a singular event to execute (displaying a message, playing an animation or a sound, etc).
  2. Make a class called an Event Sequence that holds a list of events to execute.
  3. Give the Event Sequence a function that defines the sequence of events to execute. This function runs whenever interacting with the NPC, and runs before executing any events.
  4. Make events for the commonly-used events, like message displaying, if statements, and jumping around the EventSequence array using markers. This allows me to have more complex choices and change the sequence as the game state changes, while not actually dynamically changing the length or contents of the event array.

Here's a simplified example that shows an example of an event sequence for operating an elevator:

class Elevator_Control extends ES:

    var floorChange = choices(["Press the button for the lobby.", "Press the button for the 4th floor.", "Leave the buttons alone."])

    func setSequence():

        return [
            msg("Which floor should I visit?"),
            floorChange,
            ifThen(floorChange.selected(0), jump("1st floor")),
            ifThen(floorChange.selected(1), jump("4th floor")),
            end(),

            marker("1st floor"),
            setFlag("apartment floor", 1),
            playAnim(get_node("../../../../AnimationPlayer"), "Down", true),
            end(),

            marker("4th floor"),
            setFlag("apartment floor", 4),
            playAnim(get_node("../../../../AnimationPlayer"), "Up", true),
            end(),  
        ]

If anyone's interested, I talked about this on a stream I did here (it should fast forward to the part where I'm talking about how this is set up, and how I'd like Godot to have synchronous threading as a possibility for this particular purpose). I finish talking about my approach around 1:08:30.

My method does work fine, and I do recommend it, but I do still think a synchronous thread that you can idle while the rest of the game continued would be nice for events.

clayjohn commented 4 years ago

Feature and improvement proposals for the Godot Engine are now being discussed and reviewed in a dedicated Godot Improvement Proposals (GIP) (godotengine/godot-proposals) issue tracker. The GIP tracker has a detailed issue template designed so that proposals include all the relevant information to start a productive discussion and help the community assess the validity of the proposal for the engine.

The main (godotengine/godot) tracker is now solely dedicated to bug reports and Pull Requests, enabling contributors to have a better focus on bug fixing work. Therefore, we are now closing all older feature proposals on the main issue tracker.

If you are interested in this feature proposal, please open a new proposal on the GIP tracker following the given issue template (after checking that it doesn't exist already). Be sure to reference this closed issue if it includes any relevant discussion (which you are also encouraged to summarize in the new proposal). Thanks in advance!