Director FAQ || Notes ~ Short Index ~ Full Index ~ Search || Prev ~ Next ||

[14] Objects, Parent Scripts & Factories


Director FAQ [14] Objects, Parent Scripts & Factories

[14.1] Like, this is all really complicated stuff only useful to hardened C programmers, right?

Like, this is all really complicated stuff only useful to hardened C programmers, right?

No. But IMHO it's easy to get that impression from the Director manuals.

In fact, objects are fairly simple, and the way Director handles them (though lacking some features) is quite approachable.

Essentially, objects are just little collections of data, bundled together with details of all the handlers that can operate on that data. For instance, a simple object might contain a counter variable, and a couple of handlers which increment and display the value of the counter. Multiple instances of such an object could be created, each maintaining its own counter value, but all using the same handlers to increment and display their counter.

The script which tells them how to do this is called a parent script. The process of creating an object from a parent script is called birthing. The main characteristics by which a parent script differs from any other movie script are:

Otherwise, a parent script is pretty much like any other. (Actually, even the above distinctions aren't strictly true, but they are useful conventions...)

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.2] So why would I want to use them?

Because they're glamorous and fashionable, of course!

But as well as that, objects provide a number of features that otherwise are a pain to achieve :

One common use for objects in Director is to create sprites on the fly, each of which takes care of its own behaviour independently. By using different parent scripts to give different possible behaviours, you can easily create movies which vary each time you play them.

While it is possible to do most of these things without using objects, it can be a lot harder.

For example, say you have five kinds of sprite, all of them doing different things each time the movie advances a frame (maybe moving a particular way, or changing cast, or whatever). Any given sprite may be any of the five types, since you randomly select them as you go along.

If you wanted to do this without objects, you'd have to have a handler which went through all the sprites, checked what type they were and then made them behave accordingly. It would probably involve lots of nested if..then..else statements and generally be a bit of a nightmare.

With objects, however, you just have a different parent script for each type, each with a stepMovie handler that performs the appropriate action. Then, you add all the objects you create from these parents to the actorList, and they sort it out for themselves.

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.3] Fair enough. So how do I actually go about it?

Making a parent script is dead easy. The trickier bit is working out what you want it to do and how you make it do it. Let's get the glib explanations out of the way first. Here's what you need to do to make the parent :
  1. create a new movie script in the script window, and give it a name
  2. declare any property variables you want like this:
     property blah, wotsit, doodah
    
  3. give it a "birth" handler like this:
      on birth me
        -- do whatever initialization you like here
        return me
      end birth
    
  4. add handlers for the things you want it to do:
        
    on whatever me
       -- whatever
    end whatever
     
  5. close it

Like any other handlers, those in parent scripts can take other arguments if you want, but they should always take "me" first. The me argument is used to pass a copy of the object, which Director can use to determine what messages it responds to and what the values of its property variables are. Eg, a slightly more complex birth handler might look like this :

on birth me, myRect, mySprite
  -- do whatever initialization you want
  -- including stuff using myRect and mySprite return me
end birth
Once you've written your parent script, you create objects from it using birth calls like this :
put birth( script "myScript", someRect, someSprite ) into someVariable
 
What this does is potentially confusing, so let's take it slow. Bear with me here, it's much easier to use than it is to explain!

First of all, Director creates a generic script object based on your parent script. Think of this as being a copy of the parent, with little slots for all the data the script uses. At this stage, the slots are just empty.

Then, it calls the birth handler in this copy, and passes it a value telling it where all those empty slots can be found. It also passes all the other arguments you've specified in the remainder of that birth() call (in this example, someRect and someSprite).

Your "on birth" handler then does whatever it is you've set it up to do. This usually means initializing the property variables on the basis of the parameters passed, and any other initialization stuff (eg puppeting a sprite channel, swapping castmembers etc), ready for the stuff the rest of your child object is going to do later (that is, the stuff detailed in the other handlers in the parent script: "on whatever" etc).

When the birth handler is finished, it passes back the value (me) that keeps track of where all this stuff is kept, so that you can store it away in someVariable.

At the end of this palaver, what you've got is a little variable (called someVariable in this case, though it could be anything) which points Director to that particular script object with its own set of property variables. You can now use this to call up the handlers in that script, and get at those properties. You can also make copies of it, put it in lists, and generally spread it around. All of these copies will point to this same object. If you want to create another, different, object, you have to issue another birth() statement.

One important thing here is that when you pass around or copy the value in someVariable, you aren't actually schlepping the whole object from place to place, you are just moving a copy of its address, much as you might send a copy of someone's email address around. This is why doing things like sending an object a copy of itself doesn't lead to a horrendous infinite regression. It's just a way of telling the object "you are here".

The other important thing is that anything that has this address (ie has a copy of someVariable) can access not only the data it contains, but also the handlers it uses. Eg, if you have a few routines that you want to be able to use anywhere throughout a set of movies, you can create an object with them and pass it around. (Bear in mind that this can also lead to confusion, and it uses up RAM. It isn't, for example, a sensible replacement for a shared.dir.)

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.4] What are these property variables when they're at home?

Property variables are just like the normal variables used by any handler except for two things :
  1. they persist beyond a particular handler call, just like globals do
  2. each child object maintains its own separate values for them.

What this means is that instead of having to keep track of all the data associated with each object globally and differentiate between them, you can let each object look after itself.

For example, imagine that you've got a bunch of sprites moving around onscreen, and you want to move them all at different speeds. Obviously you can't have a single global value for speed, because that would be the same for all sprites. It would be possible to keep a list of all the different speeds, but then you'd have to search through it and match the right speed with the right sprite.

By using child objects to control the sprites, and declaring a property variable for speed in the parent script, each object can maintain its own value for speed, and the handler which moves them about can treat speed as a single set value (assuming that handler is also in the parent script, which is, after all, the sensible place to put it).

Property variables are very like sprite or cast properties in this respect: each sprite has its own rect, ink, puppet etc, but they're all referred to by the same name. Just like other properties, a child object's property variables can be determined by handlers outside the object by using the keyword "the" :

put the speed of someObject
 
[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.5] So what about that "trickier bit" you mentioned up there in 14.3? You know, when, where and how to use parents and children?

Obviously, you should use them wherever you find them useful, and I'm not about to speculate on where that will be. However, here are a few general thoughts about the kinds of things objects can be useful for, and how you might approach structuring them for such uses.

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.6] Do objects always relate to sprites?

No. As discussed above, objects are just a collection of code and data. They are not sprites. Sprites are sprites.

Objects can be used for anything you want to use them for. Sprite control is one common use, but anything that needs multiple instances of some class of thing, or needs to keep data that knows how to manipulate itself (eg. abstract data types like stacks or trees) can be done using objects.

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.7] I want to put objects on the stage on the fly. How do I allocate channels?

The key thing to realize is that the objects you create, and the sprites onstage, are separate entities. The sprites will behave just like sprites always behave, whether you're controlling them with objects or ordinary lingo scripts.

In order to put an object onstage, you have to have a channel for it to go into, and in order to use that channel, it must already have something in it. So, if you want to create a bunch of object-sprites, you need to prepare the channels for them in advance when you create the movie.

Decide on a range of channels that you'll set aside to put the objects in when you create them. Put some dummy sprites into these channels: an invisible tools palette rectangle is good for this, since it doesn't take up much space (use the same cast for all these dummy sprites rather than having one each!). You may want to start the movie with all these sprites safely offstage where they won't get in the way.

The sprites you choose needn't be contiguous, but it's probably easier to keep track when you set it up if they are.

Make a list of all the channels that are available for objects to use, and put it in a global variable at the start of your movie, eg:

    global freeChannels
    
    on startMovie
      put [5,6,7,8,9,10,11,12] into freeChannels
      -- and whatever other stuff you want
    end startMovie

Since you're going to manipulate these channels using lingo (in this case child objects), you'll need to make them puppets. It doesn't matter where you do this as long as it's *before* you start using them. A convenient place might be your startMovie handler, but anywhere will do :

    repeat with pup in freeChannels
      puppetSprite pup, true
    end repeat
You've now got a list of channels that are all puppeted and ready to use. There are a number of ways in which you can allocate channels from this list, but I'll just suggest one possibility:
    -- in your parent script
    
    property myChannel
    global freeChannels
    
    on birth me
      -- check that there's a channel available
      if count(freeChannels) = 0 then return #cantAllocate
      -- otherwise, take the first channel and use it
      set myChannel = getAt(freeChannels, 1)
      -- remove it from the free list, so no-one else uses it
      deleteAt freeChannels, 1
      -- do whatever else you want
      -- and finish
      return me
    end birth
There's a couple of things to notice about this. One is that the handler returns an error code if it can't allocate a channel. Whenever you birth an object from this script, you'll need to check what you get back to make sure it's worked OK. In more complex scripts, there might be several ways the birthing could, er, miscarry, and you might want to return different error codes so that you'll know what went wrong and can deal with it appropriately.

The other thing is that each time an object gets birthed, freeChannels gets smaller, until there's nothing left. Once the list is empty, that's it: when you run out of channels, you can't create any more.

But let's say the sprites in question don't only get created, they also get destroyed -- maybe they're little space invaders or something. You need to be able to reclaim the channels once they're no longer in use, so that you can use them again (on the next level, with the bigger, meaner and more bloodthirsty invaders...).

You might just have a total reset handler that you call each time you start afresh:

    on startNewLevel
      put [5,6,7,8,9,10,11,12] into freeChannels
    end startNewLevel
but this may be too restrictive. If you want to be able to create and destroy sprites on the fly, you'll need to have the object give back the channel it's using once it's finished with it. This ability becomes one of the object's own handlers (in the parent script) :
    on die me
      doHugeExplosion(myChannel)
      add freeChannels, myChannel
      set score = score + 100
    end die
Or whatever. These examples are pretty banal, but you get the idea.

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.8] How about ancestor scripts? What are they about?

Ancestor scripts are, loosely speaking, a way to implement an object-oriented programming concept known as "inheritance". This allows you to create a hierarchy of objects, each layer of which inherits a set of generic handlers and properties from its parent, and in turn bequeaths these, usually along with other, more specialized ones, to its offspring.

In Director, an ancestor is just another birthed object that you link to your own. If your object receives a message that it doesn't have a handler for, it passes the message along to its ancestor object to see if that has a handler for it, much as a mouseClick that doesn't get handled by a sprite script will be passed along to the relevant cast script.

In order to attach an ancestor to an object, you must birth an object from the ancestor script (exactly as you would from any parent script), and then put that object into a property variable called "ancestor". For example :

    -- this script is called "ma"
    
    property ancestor
  
    on birth me
      set the ancestor of me = birth( script "grandma" )
      return me
    end birth

What does this do when it receives a "birth" message?

First of all, it births an object from another script called "grandma". This works just the same as any birthing process. Then, it looks for the property variable "ancestor" in "me", and assigns the created object to that. Having satisfactorily initialized itself, it then passes "me" back to the script that originally made the birth() call. (Note that, since it isn't explicitly returned, the "grandma" object would normally get vaped as soon as the handler came to an end. However, because we've put it into a property variable, it persists for the life of the "ma" object.)

All fine and dandy, but what's the point? Well, this parent script doesn't really do anything: it has no handlers and no user-defined properties, and in fact it's pretty useless all round. But imagine that "grandma" creates a fantastically useful object with dozens of handlers. The object birthed by "ma", because it includes this object, immediately inherits all of those handlers.

So, if one of "grandma"'s handlers is:

    on bleep me
      beep 3
    end bleep
And you do the following:
    put birth(script "ma") into mommy
    bleep mommy
there'll be three beeps, even though the script for "ma" doesn't contain any such handler.

Okay, you're saying to yourself, why bother with "ma" at all? Why not just go straight to "grandma"? In this case, there's no reason at all: "ma" doesn't add anything to grandma, so there's no point. But suppose you wanted to create a bunch of objects exactly like "grandma", with all her bells and whistles, but which beep *four* times in response to a "bleep" message, rather than the usual three. If you add this to "ma" :

    on bleep me
      beep 4
    end bleep
then objects birthed from "ma" will do just that: they inherit all the rest of their behaviour from the fabulous "grandma", but conveniently intercept "bleep" messages to make four beeps rather than three.

What all this means is that you can start off with a single parent script containing all the basic handlers that all your objects are going to use, and then create various customized versions of it, all sharing the same basic structure but adding or changing their behaviour as necessary. Each parent script only needs to handle the stuff that differs from the ancestral defaults, and can just inherit all the rest.

There's a couple of other things worth noting about ancestors. The first, which is a bit obvious, is that it's not a good idea to do this :

    -- in script "blah"
    
    on birth me
      put birth(script "blah") into ancestor
    end birth
Each time you birth an object from this script, it births another, which births another and so on until there are blahs all over the place like tribbles, and Director falls over.

It

is
possible to make an object's ancestor another object birthed from the same script, but I don't think you'll find a good reason for doing so. The only use that springs to mind is to implement some kind of list or tree structure, and since Director already gives you much more efficient list handling tools, this is a pointless thing to do.

The second thing, not as immediately obvious, is that since the properties of a child object in Director can be tested and set from outside the object, you can reassign the ancestry of an existing object on the fly :

  put birth(script "blah") into aBlah
  put birth(script "wotsit") into aWotsit
  set the ancestor of aWotsit = aBlah
You could, for example, have two objects sharing a single other object as their common ancestor (ie, both modifying the same set of property variables). This sort of thing should be done with great care. Remember that sending a message to an object that doesn't have a handler for it will cause your movie to halt with a script error alert. Always make sure that when your objects *expect* to have particular handlers in their ancestry, those handlers are actually there!

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.9] How are factories different from parents, and is there any reason to use them instead?

Factories are the way objects were implemented in previous versions of Director. They continue to be available in Director 4 to maintain backwards compatibility.

If you use Director 3.1.3 or earlier you obviously have no alternative but to use factories, but for users of Director 4 there is no reason to use them, and parents offer the advantage of automatic disposal. Also, some people have reported that factories' memory management can be unreliable. In short, ditch those factories now!

The syntax for factories is somewhat different, but their functionality is very similar to parent scripts, so it may be useful to read the previous sections for some background. I'm afraid this is gonna be pretty cursory.

A factory is declared in your movie script as follows :

    factory someFactoryName
    
    method mNew
      instance blah, whatever, etc
      -- initialize the object as you wish
    end mNew
    
    method mSomeOtherMethod someArgument
      -- do whatever it is you want to do
    end mSomeOtherMethod
The factory declaration identifies the subsequent list of methods as belonging to the factory called someFactoryName. This is equivalent to naming a parent script.

The mNew method tells the factory how to initialize a newly created object. This is equivalent to a parent script's birth handler. Note that mNew neither takes "me" as a parameter nor returns it. Factories do, however, support a slightly different usage of a special keyword "me", described below.

Any further methods equate to the other handlers in a parent script. They implement whatever functions you wish your objects to perform.

The "instance" statement declares blah, whatever and etc as instance variables, which are the equivalent of properties: variables which have independent values for each object created.

A method may call other methods within the same object by using the keyword "me" to refer to itself :

    method mYetAnotherMethod
      -- do a few things first
      me(mSomeOtherMethod)
      -- etc
    end mYetAnotherMethod
Objects are created from a factory by using a statement like :
   put someFactoryName(mNew) into myObject
and their methods are called by statements like
    myObject(mSomeOtherMethod, "this value is passed in someArgument")
The most important difference between factories and parents is that objects spawned from factories must be explicitly disposed of using the built-in mDispose method:
   myObject(mDispose)
Factories also provide two other built-in methods, mPut and mGet, which support arrays, the precursor to Director 4 lists. All factory objects automatically have associated with them an array of unspecified size which can contain values of any type, including other objects. Values are placed into the array by using :
    myObject(mPut, index, value)
and retrieved using :
    put myObject(mGet, index) into someThingOrOther
Factories and arrays provide a useful way to implement data structures like stacks and tables in Director 3.1.3 and earlier, but with Director 4 it is invariably more convenient to use lists and parent scripts.

[top]


Director FAQ [14] Objects, Parent Scripts & Factories

[14.10] Do I need to dispose of these objects when I'm done?

In general, child objects birthed from parent scripts dispose of themselves as soon as there aren't any variables around the place referring to them: since you can't use them if you don't have a reference to them, Director takes the opportunity to trash them and reclaim the memory they used.

However, there may be cases where references are kept not in variables, but in lists or the properties of other objects. As discussed in section 12.8, if a circular reference arises (if, for example, your object keeps a reference to itself -- not as outlandish as it may seem, honest guv -- or keeps a reference to another object which in turn keeps a reference to it, etc), the memory occupied by an object may be lost to your movie permanently.

If your objects are simple, and don't cross-reference one another, it should usually be sufficient to clear out any object variables that you're no longer using. If they're kept as globals, they'll stay around until you set them to something else, or issue a clearGlobals command. If they're kept in a local variable, then they'll vanish in a puff of unsmoke as soon as the particular handler they're in exits.

However, if you (like me) sometimes find yourself using objects that contain lists of other objects which in turn contain other lists and yet more objects, any one of which may in turn refer back to the first, you'll need to institute some way of aggressively clearing them up. Here's an extravagantly over-the-top way:

    -- a generalized, recursive object/list disposal handler
    
    -- note that in most cases you'll actually want to provide
    -- a more specialized mKill (or whatever) method for each of
    -- your objects that takes into account ownership issues
    -- (ie, it only disposes of those parts of the object that
    -- the object actually controls itself, and will leave alone
    -- things that are merely references to objects under the
    -- control of some other program entity; such considerations
    -- are a matter of policy and can't be determined by a general
    -- procedure such as this)
    
    -- in addition, an mKill method would only need concern itself
    -- with the specifics whereas this goes overboard trying to
    -- handle anything that's slung at it
    
    -- the hierarchy argument is used by the recursive call, and
    -- should be left out when the handler is first called
    -- ie, usage is:
    
    --    dispose object
    
    on dispose anObject, hierarchy
      -- non-objects don't need to be disposed
      -- (lists are objects as far as objectP() is concerned)
      if not objectP(anObject) then return
      
      -- we don't need to dispose of points and rects
      if ilk(anObject, #point) or ilk(anObject, #rect) then return
      
      -- use a string to determine wayward object types
      set objStr = string(anObject)
      
      -- we don't need to dispose of casts either
      if word 1 of objStr = "(cast" then return
      
      -- windows can be disposed easily
      if word 1 of objStr = "(window" then
        close anObject
        forget anObject
        return
      end if
      
      -- xobjects likewise
      if char 2 to 7 of objStr = "Object" then
        anObject(mDispose)
        return
      end if
      
      -- I *think* that should've weeded out all stray object types
      -- but just to be on the safe side...
      if not listP(anObject) and word 1 of objStr <> "<offspring" and ¬
        word 1 of objStr <> "script" then return
      
      -- check to see if we've already disposed this object
      -- in our travels, and if not add it to the list of
      -- those we've encountered so that we don't try to dispose
      -- it again (this is mainly to avoid an infinite loop if
      -- we've got a circular structure)
      if not listP(hierarchy) then set hierarchy = []
      if getOne(hierarchy, anObject) then return
      add hierarchy, anObject
      
      -- we have to treat lists and propLists differently from
      -- objects and scripts because we can't delete
      -- properties from an object
      
      if listP(anObject) and not ilk(anObject, #propList) then
        -- recursively dispose of all the objects and then
        -- remove the reference to them from the list
        
        repeat while count(anObject)
          dispose getAt(anObject, 1), hierarchy
          deleteAt anObject, 1
        end repeat
        
        return
      end if
      
      -- with propLists, there's the possibility that the property
      -- may also be an object reference (no, really...)
      
      if listP(anObject) then
        -- recursively dispose of all the objects and
        -- remove the references to them from the list
        
        repeat while count(anObject)
          set theProp = getPropAt(anObject, 1)
          dispose getAt(anObject, 1), hierarchy
          deleteAt anObject, 1
          dispose theProp, hierarchy
        end repeat
        
        return
      end if
      
      -- with scripts and their offspring, we can only clear the
      -- properties, we can't remove them, but that's sufficient
      
      if not count(anObject) then return
      
      repeat with index = 1 to count(anObject)
        set theProp = getAProp(anObject, getPropAt(anObject, index))
        dispose theProp, hierarchy
        setProp anObject, getPropAt(anObject, index), 0
      end repeat
      
    end dispose
The above handler should be able to annihilate more or less any object submitted to it, but it is wildly excessive. Disposing of every element of every list and every property of every object is *very* slow and the high level of recursion can also make it a terrible memory hog. Even for circular references, it should usually be sufficient to use more slimline disposal procedures like these:
    -- snappier disposal
    -- this is for lists only
    
    on vaporize aList
      repeat while count(aList)
        deleteAt aList, 1
      end repeat
    end vaporize
and this is for script/child objects
    
    on exterminate anObject
      if count(anObject) = 0 then return
      repeat with index = 1 to count(anObject)
        setProp anObject, getPropAt(anObject, index), 0
      end repeat
    end exterminate

This is particularly effective if used in combination with custom mKill methods for your objects -- since you'll generally know where the dangerous bits of any object are, you can handle those more thoroughly and then just wipe the properties wholesale.

Factory objects are a little different. These have to be explicitly disposed of using the built-in mDispose method:

    someObject(mDispose)
If you forget to do this, the memory used by your object won't get reclaimed. If you reset the variable that tells you where it is, you won't be able to get the memory back without quitting Director or doing a wholesale purge. So, if you do use factory objects for some reason, take care to dispose of them properly.

[top]


Director Web: Director FAQ: [14] Objects, Parent Scripts & Factories

Maricopa Center for Learning and Instruction (MCLI)
Maricopa County Community College District
HTML by Grant Young, mediac@pobox.com

The Internet Connection at MCLI is Alan Levine --}
Comments to levine@maricopa.edu

URL: http://www.mcli.dist.maricopa.edu/director/faq/faq14.html