Talk:Plan for script multitasking

From OHRRPGCE-Wiki
Jump to navigation Jump to search

James Paige: it might be a good idea to actually avoid the word "thread" when naming this feature. To a lot of people that will imply real OS threads.


S'orlok Reaves: [Introduction] Then in that case, you seriously need to consider if you'll need pre-emptive threads or not. "OS Threads" generally must be pre-emptable, because OS software is designed by a distributed, independant network of programmers. Photoshop doesn't care about your mail daemon. An RPG is designed by ONE, interdependant group of developers, and as such the number of reasons a thread may have to pre-empt is quite small. Moreover, you don't WANT threads to pre-empt any thread at any time. It is very, very difficult to design a threaded script, knowing that script may be interrupted at any time, for any reason.

S'orlok Reaves: [Body] A healthy compromise is to enforce that: 1) Threads can only force themselves to sleep. 2) Threads can only terminate themselves. By "sleep", I mean that a thread goes to sleep, waiting on an event. For instance:

  sleep(10) //will sleep for 10 ticks. 
  wait for text box //is implicitly wrapped as sleep(waitForTextBox)
  sleep() //will sleep forever (until another thread wakes it up)

...in all cases, this is non-busy waiting.


S'orlok Reaves: [Remarks] With this scheme, threading reduces to the following: 1) For all "WAIT" commands, multitasking may now occur. 2) Short threads (e.g., opening treasure chests) never really pre-empt, which reduces memory usage. 3) Long threads (e.g., tracking keystates) reduce to:

  loop:
     //Get key
     //Do something
  
     //Allow other threads the chance to execute
     sleep(1)
  end loop


S'orlok Reaves: [One more thing] Actually, you might want to allow suggestions. So requestSleep(someThread) will set someThread's "sleep" bit. And then, someThread can decide when it wants to and if it wants to to sleep, checking if this bit is set. But this is mere syntactic sugar; a global variable could just as easily be used.


Any thoughts?


Bob the Hamster: Right. we don't want threads to be pre-emptive. We just want scripts to be able to continue running while other scripts are waiting. For example, if two scripts are running in tight loops with no waits they do not need to run simultaneously. But if two scripts are running in tight loops and they both contain waits, they will run together, trading off control with their wait statements.

TMC: (My tilde key broke!) (James got in a comment before me)

OK, it seems James was right: the term "thread" has thrown you off. We definitely don't want "preemptive threading" - this is a totally different situation (in terms of splitting CPU usage) than OS threads. "Thread" here refers to shared memory, but independence of execution of different scripts.

Think of script "threads" (new name pending) as a checklist of scripts the plotscripter wants to run every tick. Their order may be important, which is the plotscripter's responsibility, but just one is run at a time until it waits or terminates. They may interact, but generally if you need two-way interaction within the same tick, you would call a script directly instead.

I think scripts should be able to terminate other threads: there will be many cases where you leave the map/a minigame ends/you want to cancel some complicated multi-script action, and provided it's safe to do so, it's easier to kill the relevant scripts instead of "broadcasting" a quit signal with global variables.

We don't need a new sleep command. Wait will work to cause a script to stop for the desired number of ticks. Putting threads to sleep (remotely) sounds just the same as pausing them. But right, you should be able to pause yourself.


Thinking about it, I think the default for new threads should be to run them last. So:

plotscript, main, begin
  new script thread (@scripta)
  new script thread (@scriptb)
  
  while (1) do ( .... foo .... wait(1) )
end

Every tick, triggered scripts, then main, scripta, scriptb will run in that order. Hmm.. maybe there should be better control over triggered scripts somehow...


Also, that blocking thread idea sounds good to me. I think that scripts with definescripts should be blocking whenever in backwards compatibility mode.


S'orlok Reaves: Ah, I see. So the prototypical (best-example) use for scripting threads would be to break up old code like:

 loop:
   if (I'm on this map)
      do this thing
   end if
   
   if (I have this item)
      do this thing
   end if
  
   ...
 end loop

...into different scripts for each "if". Certainly, that makes much more sense. So they're kinda like... concurrent scripts.... or "multi"-scripting... Oh, also, I wasn't refering to a global kill switch. I just feel that it's unwise to remotely kill a thread, and that remote termination is only needed for threads that loop forever or sleep for a very long time. So why not just "ask" the thread to terminate?
ThreadA: (shows some splash screen on "Esc", terminates it on "Esc")
//Note: Assume that a thread which has finished is given the status "inactive"

 threadB := null //nothing
 loop:
   on keypress (Esc)
     if (isNull(threadB) || isInactive(threadB))
        ask_to_kill(threadB)
     else
        threadB := new thread(@showHUD)
     end if
   end 
 
   wait 1
 end loop

ThreadB:

 loop:
   if (my "ask to kill" bit was set)
      break
   end if
   
   //Do normal stuff
   ...
   
   wait 1
 end loop

...since this keeps control of Thread A entirely within the code for Thread A.

Sidenote: As far as new scripts running first or last, you might consider giving the user control over this at creation time. I.e., thread aThread := new thread(10, @scriptB) will make a new thread which runs in slot "10" (10th position). This encourages users to keep a low number of threads, and to know which threads are running at any time.

The Mad Cacti: (On a different computer now)

Game Maker (I poked around a couple of Rinku's games) seems to do scripting by allowing you to attach an "each step" or creation or destruction, etc, script to objects in the "room" (map). I don't think the order they are called is defined, and they must finish (no waiting). The idea is: each object can call an independent script to handle their movement, or their collision detection, complex animation, etc. This is a core use I'd like for threads ("npc move script" trigger type maybe?)

I don't see why you would want to kill a thread that has already terminated. Finished threads won't need to be kept around.

The ask to kill mechanism is better achieved through a global variable, as it's simpler, I think. Of course, then the engine can't request to kill the thread, but I don't see any situations where it would actually need to ask. All possible cases in which a thread is killed: because of an error, because it finishes, or because of game over/loading a game.

I'll update the article with a suggested (rather complex) mechanism for specifying order when I have time.

S'orlok Reaves: Certainly. Er... my idea of keeping a thread around is actually just a reflex: I use Threads in Java, and they can be expensive to create. So if you KNOW that you're going to "restart" a thread, you just keep the old one around and avoid re-creating it.

This isn't an issue in the OHR, I think, since threads are just... what? A pointer to a script, some stack/heap space, and a program counter? Yeah, no need to keep used threads.

Bob the Hamster: Regarding the idea of an NPC each-step script, the only reason I did not implement that at the same time as the hero each-step script was back then performance was *much* worse, and i didn't think it could happen at a sane speed. There has been a lot of interpreter cleanup since then, so I think it is sensible now.

Although rather than an each-step script it might make more sense to have a new sort of trigger, and "NPC AI script" which is run once for each NPC on every cycle in which it isn't already running (which means that each NPC instance needs to keep track of whether or not its own script has terminated). So you could write a script for your NPC that looks like this: and it would do the right thing:

plotscript, castle guards, npcref, begin
  variable(x,y)
  if(hero is close to(npcref, 1))
    then (got caught)
  if(hero is straight ahead)
    then (alarm := true)
  if(alarm)
    then (chase hero(npcref))
    else (walk NPC(npcref, random(0,3), random(1,2)), wait for NPC(npcref))
end

Does that make any sense? As long as script multitasking was available, and the above script was only run once-at-a-time for each instance of the "guard" NPC, it would behave like a loop, but with simpler syntax.


S'orlok Reaves: So... is NPCAI just like NPC On Step, except at a finer granularity (each tick, instead of each finished step), and threaded?


The Mad Cacti: Regarding the blockingscript trigger type idea, I thought it was a good idea, however it turns out that the way we designed script triggers, that this isn't possible: each script id field is presumed to be a certain trigger type, and you can't tell which type it is (plot or blocking, for example) from its value. Since we only have one trigger type so far, we can still easily change this, and associate each trigger id with a trigger type, script id, and script name. But we don't have to fix it right now, and I'd rather not get that sidetracked.

Besides, I think this blockingscript command is a more powerful idea.

Bob the Hamster: Yeah, that sounds like a good idea.


Bob the Hamster: I like the way this plan is shaping up. Comments:

  • I like the "spawned" terminology
  • I am glad to see you think the script debugger might be salvageable after all. I always thought you were being to hard on yourself about it :)
  • I like the whole script handles thing, and understand their distinction as compared to script objects. As for having different types of handles for a thread and a script withing a thread, I don't personally see a need. Making a thread handle be a handle to the topmost script in the thread tree seems reasonable.
  • Regarding Proposal: A "spawn script" block counts as a separate script, unless it consists of only a single script call. So, in "child script" below, "parent script" returns 0.", maybe this would be less confusing with a different syntax:
script, child script, foo ( show value (parent script), wait (10) )

plotscript, master script, begin
  spawn (child script (3)) # the "spawn" command would fail to compile with any argument other than a single script
  spawn script ( # spawn script would be an inline spawned script definition, even if it only happened to contain one command.
    child script (4)
    wait for npc (5)
  )
  child script (5)
end
  • I concur that nested "spawn scripts" seem unnecessary. The feature will be extremely powerful even with no nesting syntax.
  • Random syntax thought:
 # This syntax is just fine...
 variable (Sarah's path)
 Sarah's path := spawn script, begin

 # but what if we could express the exact same thing by writing:
 spawn script, Sarah's path, begin
  • Re: "get script id" name, I don't think this name is too confusing since it matches "run script by id"
  • Yes, I am confused by the functionalit(y)(ies) of "pause child scripts".
  • Having "wait for child scripts" wait for grandchildren sounds reasonable to me, although I cannot think of a use-case example for "wait for child scripts" off the top of my head.
  • Re: "kill script" when people ask for this they are almost always doing something wrong. However, that in itself is not a reason for "kill script" not to exist.
  • I think "exit script when parent exits" is an excellent name. Maybe it is a tad long, but it is very clear.

The Mad Cacti: Thanks, very helpful.

  • "spawned": I just realised we could rename "wait for child scripts" and "pause all child scripts" to "wait for spawned scripts" and "pause all spawned scripts"
  • Script debugger: I have a lot of uncommitted improvements to the script debugger that IMO finally bring it to a friendly and usable state. The code itself is still atrocious and needs to be rewritten, but if there's one thing I've learnt from working on the OHR, it's the success incrementally rewriting something that already works :)
  • RE script handles/script thread handles: yes, I've come to the conclusion that script thread handles are unnecessary too. I'm just slow to change my mind, and prefer to rework an idea through to completion after every revision. But the point of script thread handles is that they wouldn't be invalidated until the thread exits, so the script handle equivalent would a handle to the bottommost (root) script, not the topmost. Unless we allow an invalidated script handle to refer to that thread even after that particular script instance has returned. But that sounds a bit dubious to me.
  • RE: spawn script/spawn: That's one idea. Need to give it more thought. At first it was just an internal optimisation, but then I realised it would be visible to users. I probably overcomplicated things. A separate "spawn" doesn't really reduce complexity, just surprising behaviour.
  • spawn script, name ( ... ) syntax: I'm not sure if you're suggesting that "Sarah's path" would be an implicitly declared local variable with a preset value, or whether you would get a handle by writing @Sarah's path. That would be perfect syntax for if we add nested scripts (ie. closures) and wanted a script object by using the @ operator. But realise that you can do this:
for (i, 0, 10) do (
  handle := spawn script (foo (i))
  write global (100 + i, handle)
)
  • confusingness of "get script id": I'm not sure why I thought that was confusing, a lot of the stuff on this page is still old and unrevised. But script IDs will mostly be obsoleted by "script objects" (which I would call "script references" but that would be REALLY confusing) so I was wondering if it's the right thing to do. But I guess it is, because you can't return a script object for a spawn script block, because they aren't callable; and script id numbers aren't going anywhere.
  • "pause child scripts": Yes, had better break this into two commands (mandatory if script thread handles are removed), something like "pause my spawned scripts" and "pause thread spawned scripts"
  • use of "wait for child scripts": The "split up!" script could be an example: what if you've spawned some scripts to control different NPCs, and now want to wait for them to all stop? Also, I'm changing my mind about waiting for grandchildren: spawned scripts can explicitly wait for their children if that's desired.

The Mad Cacti: Removing script thread handles really simplified things.

So far I have two suggested phrases for replacing "script threads": "script chains" or "script stacks". Picking a term for the root/initial script of a thread also needs attention. Eg. "root of script thread" -> "first script of chain".

Thinking more about spawn/spawnscript: there's really no need to expose such an optimisation to users. And I don't want to needlessly add separate "spawn" syntax. A dummy script handle could still refer to a spawnscript block that is optimised out.

Bob the Hamster: I vote for "script chains". Any name we can give it is gonna have a little potential for confusion, but "chains" sounds so OHResque :)

Alternate proposal: "script thingies" (jk)