Wait for user Input inside a call tree

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By rslepon

Suppose I have the following simplified case:

func a ():
   b()
   d()
func b ():
   c()
   e()
func c ():
   # spawn a new window with some text and two buttons (true, false)
   # wait for player to click one of the buttons
   # set some global reply variable to player's reply
   # close window and return

Also assume that all five functions are in five different modules and that the behaviour of d() and e() is dependent on the player’s reply. Therefore, d() and e() should not run until c() has completed.

I know how to do everything, except for the part that does the actual “wait for player to click one of the buttons”. I googled extensively and all the solutions depend on yield(), but yield returns to the caller (b() in this case) and results in the immediate execution of d() and e().

I can’t seem to find a solution for this very basic behaviour. Any suggestions?

Note that I don’t want a() and b() to know that somewhere underneath them there is a function that is dependent on player input, so adding extra if’s before the call to d() and e() is not really a solution.

Thanks.

Note that I don’t want a() and b() to know that somewhere underneath them there is a function that is dependent on player input

Why not? Because the most easy solution would be a global variable that is set to true after the player press the button, and, if this variable is true, run the other functions.

Saitodepaula | 2019-12-05 14:27

Because I want to encapsulate the user interaction within c().

As I mentioned above, the example I gave is rather simplified.

In reality, c() is a generic user-input window scene with variable text and buttons. It may be called from tens of different places within the game, each time with a different set of parameters and returns, and the call tree when it is called may be a lot deeper than just two functions, so now I need to pepper my code with hundreds of global variable checks.

Yuck.

Godot really needs a long jump (a yield that returns to the core without executing the code of the intervening call tree).

rslepon | 2019-12-05 19:54

:bust_in_silhouette: Reply From: adabru

In JavaScript one would use asynchronous / nonblocking code when user input is involved as you have only one thread there. As I am curious how your example would look like with asynchronous code, I converted your example to asynchronous and this is what came out.

Essentially every function that may use a value from a blocking source (user input) must be splitted at each point where the blocking happens. I created a new function for that, you could also use the same function with some param.

Then you need to store the function parameters somewhere so that they can be used after returning from user input. I used an array for that.

You can copy paste the code to a new *.gd file and just run it without any scene.
This is how it would most probably be solved with JavaScript I guess, though JavaScript provides the async-keyword to flatten the syntax.

# Run with Control+Shift+X or File→Run
tool
extends EditorScript

func _run():
  print('Your evaluation:')
  # type your evaluation below
  # ...
  a('after_a', ['a('])
func after_a(args):
  var myparam = args.pop_back()
  print('The result was: %s' % myparam)

func a (callback, args):
   var myparam = args.pop_back()
   # do sth
   args.push_back(callback)
   args.push_back(myparam + 'b(')
   b('a2', args)
func a2 (args):  
   var myparam = args.pop_back()
   var callback = args.pop_back()
   # do sth
   args.push_back(myparam + 'e(')
   args.push_back(e(args) + ')a')
   call(callback, args)
  
func b (callback, args):
   var myparam = args.pop_back()
   # do sth
   args.push_back(callback)
   args.push_back(myparam + 'c(')
   c('b2', args)
func b2 (args):
   var myparam = args.pop_back()
   var callback = args.pop_back()
   # do sth
   args.push_back(myparam + 'd(')
   args.push_back(d(args) + ')b')
   call(callback, args)
  
func d (args):
   var myparam = args.pop_back()
   return myparam + ')d'
func e (args):
   var myparam = args.pop_back()
   return myparam + ')e'

func c(callback, args):
   var myparam = args.pop_back()
   args.push_back(callback)
   args.push_back(myparam + 'wait for userinput... ')
   # here you need to store the args somewhere
   # so that it can be related to the shown UI
   var somewhere_stored = args
   # some user interaction
   myparam = somewhere_stored.pop_back()
   callback = somewhere_stored.pop_back()
   somewhere_stored.push_back(myparam + 'finished!)c')
   call(callback, somewhere_stored)

This code shows the concept of asynchronous programming, you have some flexibility where to store function parameters and how to split a function.

Thank you for your enlightening effort.

However, this is not what I would call maintainable code.

For future readers stuck with the same problem, here is my current solution, which I don’t much like:

func a () -> void:
   yield(reply = b(), "completed")
   d(reply)
func b () -> int:
   yield(reply = c(args), "completed")
   e(reply)
   return reply
func c (params) -> int:
   # spawn a new window with some text and some buttons based on params
   # wait for player to click one of the buttons
   while waiting:
      yield(get_tree(), "idle_frame")
  user_reply = whatever the user clicked, translated into an enum
   # close window
   return user_reply

Limitation:

  • b() and c() must yield on every case. You cannot have something like this in c():
   if (no need for user input)
      return default_reply

it has to be:

   if no need for user input:
      yield(get_tree(), "idle_frame")
      return default_reply
  • I’m not sure whether you can have more than one non-mutually-exclusive yields within a single function (i.e. may return two different GDFunctionState objects). Didn’t have occasion to test it.
  • Functions need to know that somewhere under their call tree there’s a yielding function and yield accordingly. To make this more maintainable, I adopted a convention whereby a yielding function’s name begins with yield_ (public) or _yield_ (private). So I my case, c(), b() and a() would become yield_c(), yield_b() and yield_a(), respectively.

rslepon | 2019-12-06 16:08

Thanks for your version, much cleaner than mine! Actually now it looks much more like JavaScript (async/await) or C#.

adabru | 2019-12-06 22:12