Articles Archive
Articles Search
Director Wiki
 

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.hqx
Win 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.

Alan Levine is an Instructional Technologist at the Maricopa Center for Learning & Instruction at the Maricopa Community Colleges. Among other random things, he tries to maintain the DirectorWeb, does side work as dommy media, and not often enough, mountainbikes in the desert.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.