Discuss Scratch

_nix
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

Just a bit of a fun Scratch challenge-ish thing…

If you've ever used Snap!, you might have noticed a “launch” block in the control category. It basically works like an in-place broadcast, starting a thread for that script.

launch {
repeat until <key [x v] pressed>
change x by (2)
end
} :: control
launch {
repeat until <key [y v] pressed>
change y by (2)
end
} :: control

In the script above, both “repeat until” loops run concurrently.

You can also launch the same script as many times as you want, even while it's running, for example like this:

set [var v] to [0]
repeat (2)
launch {
repeat (10)
change [var v] by (1)
end
} :: control
end

So the “repeat 10” loop runs twice, at the same time, and by the time both runs are complete, var is 20.

The first script I showed above could be pretty easily made in Scratch by using broadcasts:

broadcast [loop 1 v]
broadcast [loop 2 v]

when I receive [loop 1 v]
repeat until <key [x v] pressed>
change x by (2)
end

when I receive [loop 2 v]
repeat until <key [y v] pressed>
change y by (2)
end

But the second script is more difficult. At first glance you'd try something like this:

set [var v] to [0]
repeat (2)
broadcast [loop v]
end

when I receive [loop v]
repeat (10)
change [var v] by (1)
end

But that wouldn't work, because when you send a broadcast, any “when I receive” blocks that are already running for that broadcast are stopped (and then immediately started). So the “loop” hat would only go through to completion once.

We can use clones instead of broadcasts, since making a clone doesn't stop other clones from doing their own scripts (of course):

set [var v] to [0]
repeat (2)
create clone of [myself v]
end

when I start as a clone
repeat (10)
change [var v] by (1)
end
delete this clone

And this would work, but it's a bit of a hack and not very useful. The problem is that using a “when I start as a clone” hat means we're running the script in a different sprite context. If “var” is a sprite-local variable, or we want to use a block like “change x” instead of “change (a global variable)”, it won't work (because the blocks would affect the individual clone, not the sprite that created the clones).

So the question is, can anyone think of a less hacky way to do this? The important things are that the same script can be running twice (or more) at the same time, and is run in the same context as the sprite that launched it (i.e. no using clones).

——–

By the way, what prompted this is an idea for making a custom block that launches a thread that stores a value, which can be requested (by setting some variable):

define store (value) to be requested with [request code]
launch {
forever
if <(code to request) = (request code)> then
set [returned value v] to (value)
set [returned the value? v] to [1]
end
end
} :: control

define request (request code)
set [returned the value? v] to [0]
set [code to request v] to (request code)
wait until <(returned the value?) = [1]>

ask [Favorite fruit?] and wait
store (answer) to be requested with [favorite fruit]
ask [Favorite animal?] and wait
store (answer) to be requested with [favorite animal]
request [favorite fruit]
say (join (returned value) [ - yummy!]) for (2) secs
request [favorite animal]
say (join (returned value) [ - I like it!]) for (2) secs

I know that example isn't exactly outstanding because you could just use variables, but that's beside the point; it's a basis for making things like a “counter”, OOP-style. (For example, you could make the launched “forever if code to request…” do something else, depending on what the code to request is, like incrementing its value.)

══ trans autistic lesbian enbydoggirls // 16 17 18 19 20, she/they
sparrows one word to the paragraph // <3 // ~(quasar) nebula
_nix
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

_nix wrote:

it's a basis for making things like a “counter”, OOP-style.
Okay, just for a proof of concept, so people believe me (and for fun):

define make a counter with ID: (id)
launch {
counter helper ID: (id) value: (0)
} :: control

define counter helper ID: (id) value: (value)
wait until <<(target counter ID) = (id)> and <(handled the message?) = [0]>>
if <(message) = [destroy]> then // Do nothing, we are done.
set [handled the message? v] to [1]
end
if <(message) = [increment]> then
set [handled the message? v] to [1]
counter helper ID: (id) value: ((value) + (1))
end
if <(message) = [reset]> then
set [handled the message? v] to [1]
counter helper ID: (id) value: (0)
end
if <(message) = [get value]> then
set [returned counter value v] to (value)
set [handled the message? v] to [1]
counter helper ID: (id) value: (value) // Don't change the value when we're just getting it!
end

define request to ID: (id) message: (message)
set [target counter ID v] to (id)
set [message v] to (message)
set [handled the message v] to [0]

set [handled the message v] to [1] // So the counters don't respond before we've even sent a message!
make a counter with ID: [counter1]
make a counter with ID: [counter2]
repeat (50)
request to ID: [counter1] message: [increment]
if <(pick random (1) to (3)) = [1]> then
request to ID: [counter2] message: [increment]
end
end
request to ID: [counter1] message: [get value]
say (join [Counter 1: ] (counter value)) for (2) secs // Should be 50
say (join [Counter 2: ] (counter value)) for (2) secs // Should be anywhere from 0 to 50
request to ID: [counter1] message: [reset]
request to ID: [counter1] message: [increment]
request to ID: [counter1] message: [get value]
say (join [Counter 1: ] (counter value)) for (2) secs // Should be 1
request to ID: [counter1] message: [destroy]
request to ID: [counter2] message: [destroy]

If you want, you can test this script just by dragging out a couple “make a counter with ID” blocks and running them separately (i.e. clicking on them individually).

The basic idea is we wait for a message, and then do something based on that message. If the message is anything but to be destroyed, we run the helper script again, with the (potentially) adjusted value. And that helper script waits for a message, and so on, until eventually a destroy message is received (or the script is externally stopped, e.g. by the stop button).

Last edited by _nix (July 3, 2018 15:02:30)


══ trans autistic lesbian enbydoggirls // 16 17 18 19 20, she/they
sparrows one word to the paragraph // <3 // ~(quasar) nebula
TheMonsterOfTheDeep
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

I've tried quite a few things to get this to work (although none of them particularly hacky), and I've found thus far that only clones are at all useful.

I've been messing around with this project which defines the following block:
define new my object id: (id) x: (x) y: (y)
set [newx v] to (x)
set [newy v] to (y)
set [newid v] to (id)
set [create new object v] to [1] // only used when trying the timer method
set [ready v] to [0]
create clone of [myself v] // "launch thread" block
wait until <(ready) = [1]>

So far, for the “launch thread” block, I've tried the following:
create clone of [myself v]

reset timer

broadcast [message1 v]

switch backdrop to [new-object v]
With the following corresponding hat blocks for actually running the “launched” thread:
when I start as a clone

when [timer v] > (0)

when I receive [message1 v]

when backdrop switches to [new-object v]

Of these, only the cloning method actually behaves as desired.

The timer method does not launch a new thread the second time the reset timer block is called, while the broadcast and backdrop methods both behave as described in the OP (the new thread replaces the old one).

To test each method, I have a somewhat hacked-on extra “message” that is sent to every object at once, telling them to add their id to a list. If, when running the project, the “enumerated objects” list does not contain both “o1” and “o2”, the object creation has failed and the project will hang (as it tries to send a message to each object individually).

It is in some sense possible to get an arbitrarily high pre-programmed number of objects, simply be creating a large number of built-in broadcasts that all do the exact same thing (although I don't believe that this is a particularly good idea). Although it should be noted that this number could be rather easily improved by integrating it with clones, so you have 300 * whatever constant number of broadcasts.

I obviously haven't done anything particularly creative here, although I have to wonder how many more options there really are for creating threads in Scratch. Somebody more acquainted with how Scratch works internally might be able to comment on whether it is even possible to create an arbitrary number of threads (although I would be very surprised if the answer is yes).

In any case I am very fascinated by this topic as it would essentially enable creating arbitrarily many objects with their own local data.

my latest extension: 2d vector math
Jonathan50
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

You could communicate between clones with broadcasts and global variables, but why wouldn't you just use lists?

Not yet a Knight of the Mu Calculus.
TheMonsterOfTheDeep
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

Jonathan50 wrote:

You could communicate between clones with broadcasts and global variables, but why wouldn't you just use lists?
The motivation, at least from my point of view, is that this would allow you to exploit Scratch's threads to get “real” local variables – that is, each “object” would have its own data that “belonged” to the Scratch runtime, so to speak, and not to an ad-hoc system implemented on top of lists.

Not that that's a particularly good motivation, but to me it seems kinda neat.

(I'm also expressing myself really poorly here, but it's kinda hard to explain exactly what I like about this idea).

my latest extension: 2d vector math
Jonathan50
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

TheMonsterOfTheDeep wrote:

The motivation, at least from my point of view, is that this would allow you to exploit Scratch's threads to get “real” local variables – that is, each “object” would have its own data that “belonged” to the Scratch runtime, so to speak, and not to an ad-hoc system implemented on top of lists.
Okay, so if you want to avoid the need for explicit selectors and mutators, alright. But _nix is asking for the opposite. As the OP said, you can use clones and each one will have its own state, but _nix only wants one sprite, with the concurrently running scripts all sharing state. So it would probably be a lot simpler and faster (especially compared to having one script polling for messages in the background for each object) to just make data structures with lists, which is what I meant.

Last edited by Jonathan50 (July 9, 2018 03:25:43)


Not yet a Knight of the Mu Calculus.
_nix
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

Jonathan50 wrote:

So it would probably be a lot simpler and faster (especially compared to having one script polling for messages in the background for each object) to just make data structures with lists, which is what I meant.
Oh, for sure. Of course it would be far faster. But that's not really the point; this is just a fun experiment to see what's possible (and how).

Jonathan50 wrote:

Okay, so if you want to avoid the need for explicit selectors and mutators, alright. But _nix is asking for the opposite. As the OP said, you can use clones and each one will have its own state, but _nix only wants one sprite, with the concurrently running scripts all sharing state.
That's… sort of but not really right? The objective is to have concurrently running scripts with their own states, but to let the scripts interact with each other (in basically the same way you make clones interact, regarding local data). This is particularly apparent in my counter object example – we have scripts sending receiving messages to modify state data and setting variables to “publish” state data.

Of course, the scripts are all in the same one sprite, and we don't want the same script to be copied over and over again, like TheMonsterOfTheDeep noted.

TheMonsterOfTheDeep wrote:

To test each method, I have a somewhat hacked-on extra “message” that is sent to every object at once, telling them to add their id to a list. If, when running the project, the “enumerated objects” list does not contain both “o1” and “o2”, the object creation has failed and the project will hang (as it tries to send a message to each object individually).
Your investigations are really interesting! I had the same effective results when testing mine, but I didn't really have as sophisticated of a procedure as you - and I think I missed a couple of those hat blocks (like when-timer) anyhow. So thanks!

TheMonsterOfTheDeep wrote:

Although it should be noted that this number could be rather easily improved by integrating it with clones, so you have 300 * whatever constant number of broadcasts.
This is pretty clever!

TheMonsterOfTheDeep wrote:

Somebody more acquainted with how Scratch works internally might be able to comment on whether it is even possible to create an arbitrary number of threads (although I would be very surprised if the answer is yes).
Yeah… MegaApuTurkUltra and I were looking at it a bit together. We didn't find anything that would work. The only two approaches to this problem which are somewhat possible are clones, which are basically out of the question, and broadcasts. But like you said, broadcasts replace existing receive threads; they don't add new ones.

Funnily enough were looking at the code, and we saw that a script like this might work:

if <> then
when I receive [message v] :: stack // Imagine this is a hat block *inside* the if-then!
...
end

Because the code for replacing a thread only checks the top block in a script (in this case the “if-then”), not every block. But apparently the code for broadcast does check every block, so this wouldn't work. (Also it's obviously very much a hacked block and certainly wouldn't load in Scratch 3.0. )

If I recall correctly, the code for “broadcast” deciding which receive scripts to stop checked the actual text value in the dropdown. So I wonder if putting a variable in the receive hat would keep it from being stopped? But I think that might prevent it from being activated in the first place (probably broadcast also checks the text values to decide which ones to start as well.) Maybe worth a shot anyways.

══ trans autistic lesbian enbydoggirls // 16 17 18 19 20, she/they
sparrows one word to the paragraph // <3 // ~(quasar) nebula
TheMonsterOfTheDeep
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

Some brief experiments show that:
a) If the project contains the following block, all broadcasts block indefinitely:
when I receive (variable)
b) The following works as expected:
set [variable v] to [message1 v]
broadcast (variable)
set [variable v] to [message2 v]
broadcast (variable)
c) Putting the when I receive block not at the top appears to cause it to be entirely ignored.

Last edited by TheMonsterOfTheDeep (July 9, 2018 04:08:44)


my latest extension: 2d vector math
Jonathan50
Scratcher
1000+ posts

"Launch" two instances of the same script from a single script?

_nix wrote:

That's… sort of but not really right? The objective is to have concurrently running scripts with their own states, but to let the scripts interact with each other (in basically the same way you make clones interact, regarding local data). This is particularly apparent in my counter object example – we have scripts sending receiving messages to modify state data and setting variables to “publish” state data.
In your examples, the only things that aren't shared are the custom block parameters. As you said in the OP, it is run in the same context as the sprite that launched it. Whereas what TheMonsterOfTheDeep posted before had a clone for each object and used sprite-local variables.

_nix wrote:

But that's not really the point; this is just a fun experiment to see what's possible (and how).
Ok.

Last edited by Jonathan50 (July 9, 2018 06:26:29)


Not yet a Knight of the Mu Calculus.

Powered by DjangoBB