|
© Andy Wilson, 1998
Pseudo-Multithreaded Lingo
Someone raised a familiar problem on the Director User Group list recently. They wanted to know how to stop Director buffering the mouse clicks that can build up when a lingo loop is being executed. For example, you have a little lingo routine that is executed when the user clicks on a button, the code for the routine contains something like;
on myHandler
repeat with x = 1 to 1000
doSomething
end repeat
end
What often happens is that, from the users point of view, while this loop is executing the program as a whole seems to have locked up, since no matter what the user clicks on or rolls over nothing seems to happen just so long as this loop is executing. The user then starts clicking all over the interface trying to get some response but without success. Suddenly, when the loop finishes running, all those clicks that have been building up in the operating system’s buffer are thrown at Director for it to handle, buttons start flashing in response to the clicks that built up and all hell breaks loose. At this point the author starts mailing the direct-l, shocker or dug lists to ask how they can trap the buffered clicks, flush the buffer, or anything to stop the clicks - entered by the user in frustration while they waited for your loop to complete - from raining down on your poor, helpless Director application.
There are a number of ways of getting around this (John Dowdell discusses some of the solutions in an article on flushing the mouse buffer on the macromedia site). However, it would be much better not to have got into this situation at all. The real problem here is not that mouse clicks and the like get buffered - the real problem is that you have written a piece of lingo code that hogs all the memory allocated to Director just to run your loop, with the result that as long as the loop is running Director can’t do any of the other things we normally expect of it, like responding to user interaction.
The best way to deal with this is not to write such loops in the first place. To explain what I mean I first want to outline the concept of multithreading. In a multithreaded operating system the processor is capable of handling several tasks simultaneously. This means that if your spreadsheet application is running a calculation while your 3D package is rendering out your model of the Lloyd’s building, both processes (or ‘threads’) get a fair share of the processors’ attention. In fact, in a truly multithreaded environment both processes will be running at the same time.
Now, there is no way that Director can suddenly make your OS multithreaded, but what we are talking about here is a very similar concept – we want Director to keep doing it’s stuff while the loop is running at the same time. It is not possible to achieve real multithreading on the operating systems we are normally using since none of them support true multithreading. What we can do is achieve a sort of ‘pseudo-multithreading’ by writing our lingo so that no one process ever hogs memory in a way that locks out other processes within Director. That means our loop will no longer be a loop at all. Instead of telling a repeat loop to do something 1000 times, we are going to write code that does only one of the 1000 steps, hands back control to Director, then, when it is given another shot at the problem, does the same again. Writing it down like that makes it sound as though we are going to make the process much slower. In fact, code written in this way results in much faster, more responsive applications.
The key to this approach is to split all tasks into smaller, bite sized pieces and then give each in turn a chance to carry out one of these pieces before passing control back to Director.
For example, imagine we have a button which, when clicked, makes three sprites move from one side of the stage to the other. There are two ‘loopy’ ways to write this code. Attaching the following lingo to the button will take each sprite in turn and shuffle it across the stage;
on mouseUp
repeat with x = 1 to 640
set the locH of sprite 1 = x
updateStage
end repeat
repeat with x = 1 to 640
set the locH of sprite 2 = x
updateStage
end repeat
repeat with x = 1 to 640
set the locH of sprite 3 = x
updateStage
end repeat
end
This code will make each of the sprites in turn wander from the left hand side of the stage to the right. Of course we can make all of the sprites wander across together by using;
on mouseUp
repeat with x = 1 to 640
set the locH of sprite 1 = x
set the locH of sprite 2 = x
set the locH of sprite 3 = x
updateStage
end repeat
end
In either case we are in our original situation where the loop ties up all of Director’s processor time so the user is effectively locked out of Director for the duration. Our alternative approach requires us, first, to create a little parent script which we’ll use to create objects to control our animations. We’ll call this parent script ‘sprite control’;
--parent script "sprite control"
property spriteNum, steps, stepsRequired
on new me, spNum, req
set spriteNum = spNum
puppetSprite spriteNum, TRUE
set stepsRequired = req
set steps = 1
return me
end
on nextStep me
set the locH of sprite spriteNum = step
updateStage
set step = step + 1
if step > req then return #finished
return step
end
If we were to create an object based on this script then every time we called the nextStep handler for that object the sprite associated with the object would be moved one pixel to the right starting from a horizontal offset of 1. Our new code, then, would start by creating an object corresponding to each of the sprites we want to move. We pass two setup constants when we create these objects – the sprite number of the sprite we want the object to control, and the total number of steps we want the object to make. For reasons that will become obvious, I want to put all of these objects into a global list which we’ll call gThreadObjects, and which I’m assuming has already been initialised as a list. So, our new button code will start by creating the three objects and putting them into the list;
global gThreadObjects
on mouseUp
add gThreadObjects, new (script "sprite ¬
control", 1, 640)
add gThreadObjects, new (script "sprite ¬
control", 2, 640)
add gThreadObjects, new (script "sprite ¬
control", 3, 640)
end
The next step would be to write the following ‘idle’ handler in a movie script;
global gThreadObjects
on idle
set cnt = count (gThreadObjects)
if (cnt = 0) then exit
repeat with x = cnt down to 1
set obj = getAt (gThreadObjects, x)
set theResult = nextStep (obj)
if theResult = #finished then deleteAt ¬
(gThreadObjects, x)
end repeat
end
This code is called every time Director is idle, ie., every time no other Director process or interaction is happening. It works by looking at the gThreadObjects list, taking each object in the list in turn, calling the nextStep handler for that object (which moves the associated sprite one pixel), checks whether the object has completed it’s run and, if so, deletes it from the list. This way of sharing out processor time between tasks, by prompting each object to take one more step and then handing back control, is called ‘polling’ the objects. Each object in turn is polled and it’s current status checked. If the process has completed (which we know is true in this case when the nextStep handler returns #finished) then it is deleted from the list of processes to be polled. Doing things in this way means that all the ‘loops’ and processes we need to invoke can take place without effectively shutting down the rest of Director – we are creating something like a multithreaded application.
There are ways of going even further with this technique. For example, here I’ve polled all of the live processes every time the idle handler is called. However, in many instances it is just as effective to poll only one of the live processes, choosing it at random. In this case the code would be;
global gThreadObjects
on idle
set cnt = count (gThreadObjects)
if (cnt = 0) then exit
set num = random (cnt)
set obj = getAt (gThreadObjects, num)
set theResult = nextStep (obj)
if theResult = #finished then deleteAt ¬
(gThreadObjects, x)
end
This spreads the processor time around even more thinly, increasing the application’s responsiveness yet further. If you want to build more complex applications using this method, with lots of objects being controlled in this way, then you’re going to have to brush up on your child-parent scripting skills – but I promise you that it’s worth it. To see an example of a game written in Director in this way, and to download the source code, check out my Threads in Space game. If this all sounds pretty long-winded and complicated, then all I can do is encourage you to try it – the overall feel of an application written this way is far smoother and more responsive than applications that don’t make the extra effort.
Sorry to have tackled what is a fairly advanced Lingo topic this month – we’ll try and get a bit more basic next time – but it seemed to me from the discussion on the user group list that it was a topic worth covering. As usual, if you have any ideas for future articles, let me know by mailing me, and if you want to join the UK Director Users Group and take part in the mailing list (both of which are free), check out their web page.
|