A memory space of one's own
August 20, 1998
by Alan Levine
What do you use to carry your most important stuff? For some of us, the answer may be a titanium briefcase from the Sharper Image, while others shuffle around pushing a grocery cart.
In the world of Lingo, you might use several kinds of "containers" to cart around important data in your code. In this article, I'll share some ones I've used, ranging from the obvious global variables, to property lists, to parent-script generated objects. I would never claim these to be the best way to write code, but maybe they'll generate some ideas when you are rummaging around the script window.
At some point the most elegant, sleek code in the world gives way to what is produced most rapidly and works from the outside. After all, a coder's work should be invisible, eh?
At the end, I'll toss in a whole pile of Lingo to try to show how a custom object can do more than just carry your possessions, think of it as a smart 007 briefcase (license to kill with lingo!).
Go Global
GLOBAL VARIABLES ARE THE MOST OBVIOUS PLACE to store data you want to use in different parts of your project or sharing data between Director files. I shouldn't have to explain how they work (if so, the rest of this article will be even more confusing), but the most obvious benefit is that the data persists and you can carry it anywhere during playback and authoring, from one Director file to another, from one shockwave file to another (if you have jumped via a goToNetMovie
command).
For the less-global aware, the easiest way to declared them is at the top of a movie, frame, or parent script. That way, all handlers in the script can access them. In D4 and D5 days, this was an undocumented feature, but has been stable in every piece of Lingo I've strung together.
global gUserName, gCreditCardNumber, gShoppingBasket on makeAnOrder if length(gCreditCardNumber) > 0 then chargeItToJohnDowdell else go to frame "get card num" end if end
Also, a good practice is the naming convention of global variables starting with the letter "g" making it easier to recognize them floating around your scripts.
More global trivia- any variable assignment you make in the Message Window while authoring in Director makes that variable a global; this can cause confusion if you use a variable by the same name in a handler that does not expect to have a global extent.
But what about globals? Well somewhere, the baggage has weight since globals live in your RAM. One benefit of global variables is that they can hold really BIG variables; I may be wrong, but I recall that there is no 32k limit on variables like there is on fields, so potentially, a global vairable could cart around a huge wad of text. In a large project or just because you are a tidy coder, it makes sense to clean up globals after you are done with them. In Director 6, there is a declared constant VOID
that makes it easy:
global gVoid on initMovie set gVoid = value("void") end
In the message window:
-- Welcome to Director -- put gVoid -- Void
So whenever cleanup was needed, like when leaving one Director 5 movie for another I would write:
on stopMovie global gMyVariable, gMyOtherVariable, gVoid set gMyVariable = gVoid set gMyOtherVariable = gVoid end
In Director 6, your cleanup would go like:
on stopMovie global gMyVariable, gMyOtherVariable set gMyVariable = VOID set gMyOtherVariable = VOID end
Of course, in most Director movies, this may not be necessary as you may never eat up much memory with your globals. How would you know? You can do tracking of memory usage via several Lingo functions, or use the Memmon application created by Alex Zavatone. My yardstick would be to cleanup if you end up creating more than 20 global variables, or if any of them will hold large chunks of text (>20k). But you cannot go wrong by cleaning up (well, unless you accidently trash some values you need later on). I used it on several projects that involved 10 to 1,250 Director movies that could be accessed within the same session and I used globals to store long text strings and deeply nested lists.
There is another reason to cleanup. For every global variable you declare, its name gets added to Director's symbol table, an internal inventory of all handlers and globals that Director has to find when it runs your application. So with a lot of differently named global variables, there have been reports of glitches and bad voodoo when this symbol table gets filled up.
Property Lists are Great Containers
Rather than creating a pile of different global variables, you can lump them into one large suitcase via a property list that is itself declared as a global variable. I think it can make code more readable when you refer to items in a property list by the syntax
set the accountBalance of MyBank = 1,000,000
The following example uses a global property list to save the conditions of the viewer's computer when my projector starts up and then restores things when they leave (I grumble when some application switches my monitor to another bitdepth, or yanks the volume to the max). I only try this on the Macintosh since it does not seem safe to dinker with the bit depth on all PC boxes.
This is a simple example, with only 2 items in the global list, but I have used rather long ones to do things like keeping track of the viewer's name, how many of my lessons they've completed, how long they've been in the program, etc. This is expanded in the last section, where I try to show you how to set up an object to manage the information. But let's take a look at a property list as a data container:
on saveStartingConditions -- store the starting sound level and bit -- depth in a 2 item property list global gSaveState -- Don't allow any quick exits, make -- them use my quit button set the exitLock = 1 if the machineType <> 256 then -- Macintosh set gSaveState = [#sound:the soundLevel, ¬ #colors:8] -- crank up the Mac volume set the soundLevel = 7 -- go to 256 colors if the colorDepth > 8 then setProp (gSaveState, #colors, the colorDepth) set the colorDepth = 8 end if end if end
And whenever they click through my "exit button", we do:
on restoreStartingConditions global gSaveState if the machineType <> 256 then -- if we are on a Mac, restore the -- colordepth and sound volume set the colorDepth = the colors of gSaveState set the soundLevel = the sound of gSaveState halt else -- if we are on a Mac, go to a frame with -- the a Windows palette colors -- so it does not cause them to see a wild palette -- shift at the desktop go to frame "windozed" end if end
The Super Dooper Object Container
NOW LET'S GO FOR THE BIG STUFF. Objects.
You can pretty much do most data tracking with a property list as a global variable, but sometimes you end up wanting to be able to manipulate or calculate things for the data you are carrying. An object created by a parent script becomes useful here because it not only contains the data, but also contains complex functionality you can build onto it. The nifty thing is that once you create the object and stick it inside a global variable, its functionality stays with it, even when you leave the movie from which you first created the object. Also, you can write cleaner code because it passes off some grunt work to the object
Now, I am so far from an OOP expert that it's not funny; most of the time I have created objects, it has been as a convenient way to organize information. This following chunk of code comes from a project where we wanted to track a user's progress in a program, which called for some fiddling with time variables, and also to carry around a text log.
In the program, but not explicitly shown here, an external file is created to store data about the viewer when they quit the program. If they return later, we prompt them to locate their data file, so we can restore them to where they left off.
So, here below, a chunk of Lingo to create an object to track information about a user. It is designed so we can pass it a list of director files that we want to track the time spent in (you can hopefully think of a way to dynamically generate this at runtime with the getNthFileNameInFolder
function). We would create the object upon entry into the program, by coding:
global gUserObject on startMovie set gUserObject = new( script "userInfo",¬ moviesInThisFolder() ) logFileEntry gUserObject end on moviesInThisFolder -- get all files in the same -- directory as this movie -- that are director files put [] into fileList repeat with i = 1 to the maxInteger put getNthFileNameInFolder(the pathName,¬ i) into nextFile if nextFile = EMPTY then exit repeat if nextFile contains ".dir" then ¬ append(fileList, nextFile) end repeat return fileList end moviesInThisFolder
For every movie in our project, we'll record in our log when we entered it:
global gUserObject on startMovie logFileEntry gUserObject end
Whenever we leave a movie file, include a stopMovie
handler to record how much time was spent in the file we are leaving:
global gUserObject on stopMovie markTrail gUserObject end
And at anytime we can write a log entry:
global gUserObject on mouseUp updateLog gUserObject "Selected item from menu 4" end
And when we exit the whole program, we can create a summary of the time spent in the different movie files and then write out the contents of the session log (use FileIO for this).
global gUserObject on leaveItAllonQuit logSummary gUserObject writetoDataFile(the sessionLog of gUserObject) end
So here is the guts, entrails, and beating heart of the object. It may not be pretty but hopefully it can give you an idea of functionality that you can embed into custom designed objects. In addition, I've created a Director 5 demo movie that lets you see how it is done:
Mac ftp://ftp.maricopa.edu/pub/director/sit/userobj.sit.hqxWin ftp://ftp.maricopa.edu/pub/director/zip/userobj.zip
property pUserName, pFirstTime property pTimeStart, pSessionLog, pTrail -- User Property Object -- progrmming by alan -- PARENT SCRIPT: userMaker -- creates the object that holds data about the user -- create an object to store -- information about the user: -- pUserName: their name -- pFirstTime: flag (1=true, 0=false) if this -- is first time in program -- pTimeStart: time (in ticks) that the user -- entered the program -- pSessionLog: log of user actions -- pTrail: -- property list for time spent in -- each movie file -- specified by the parameter mList on new me, mList -- create the user object -- mList is a linear list ofDirector -- movie file names representing -- the files we want to track time spent in -- assume they have not been in the program before set pFirstTime= 1 -- initialize properties set pTimeStart = the ticks set pUserName = "" set pSessionLog = ¬ "--------------------------------------------" ¬ & return & "started session / at / " && the long date ¬ && the long time & return & return -- set up an initial list to hold -- the the amount of time spent -- in each of the files we are tracking. set initTimeList = [] repeat with i = 1 to count(mList) append initTimeList, 0 end repeat -- Set up a property list to hold -- the "trails" information set pTrail = [ #mFiles: mList, ¬ #mTime:initTimeList, ¬ #startTime:0 ¬ , #when:the date] return me end on markTrail me -- updates the user trail, called -- by the exit of one movie -- get the time in seconds spent -- in the current file set currentTime = (the ticks - ¬ the startTime of pTrail) -- get an integer code for the current file. -- If it is not one of the files -- we are tracking, this -- function will return 0 set fileID = getPos( the mFiles ¬ of pTrail, the movieName) if fileID <> 0 then -- update the property list since we are -- in a file we are tracking -- first, dig out the total spent in the file -- from previous visits. set prevTime = getAt(the mTime of pTrail, fileID) -- now update the value by adding the time -- just spent in the file setAt(the mTime of pTrail , fileID, ¬ prevTime + currentTime) end if end on timeInFile me, fName -- returns a formatted string -- representing the time spent in -- the file specified by the -- input paramter "fName" -- get an integer code for the current file. -- If it is not one of the files -- we are tracking, this -- function will return 0 set fileID = getPos( the mFiles of pTrail, fName) if fileID <> 0 then return sessionLength( me, getAt¬ (the mTime of pTrail, fileID) ) else return 0 end if end on updateLog me, upDateString -- adds an entry to the log stamped -- by the program time -- (time relative to the time entered -- into the program) put "log>" && sessionLength(me, the ticks ¬ - pTimeStart) & ": " && upDateString & ¬ return after pSessionLog end on logFileEntry me -- record the entry into a new movie -- file, should be called -- by a startMovie handler -- reset the clock for when we entered this movie set the startTime of pTrail = the ticks updateLog me, "filename =" && the movieName end on sessionLength me, timeToCalculate -- converts input time in ticks to -- formatted output of hh:mm:ss set seconds = timeToCalculate / 60 set hours = seconds / 3600 set minutes = (seconds mod 3660) / 60 set seconds = (seconds mod 3660) mod 60 return pad(hours) & ":" & pad(minutes) ¬ & ":" & pad(seconds) end on pad x -- insert "0" chars for single digit values of x if x < 10 then return "0" & x else return x end if end on logSummary me -- called to write a summary to the log set temp ="LOG SUMMARY" & return -- step through the list of movies we are tracking -- and convert each time total to a formatted string repeat with i = 1 to count(the mFiles of pTrail) set nextMovieName = getAt(the mFiles of pTrail,i) put "time in file" && QUOTE ¬ & nextMovieName & QUOTE ¬ && " :" && timeinFile(me, ¬ nextMovieName) & return after temp end repeat updateLog me, temp end
Okay. This may be a lot of Lingo to digest. And this is but the tip of the OOP iceberg for what kinds of "intelligence" (or at least decent reflexes) you can construct with object code.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.