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:
But as well as that, objects provide a number of features that otherwise are a pain to achieve :
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.
property blah, wotsit, doodah
on birth me
-- do whatever initialization you like here
return me
end birth
on whatever me -- whatever end whatever
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 birthOnce you've written your parent script, you create objects from it using birth calls like this :
put birth( script "myScript", someRect, someSprite ) into someVariableWhat 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.)
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
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.
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.
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
ispossible 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 = aBlahYou 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!
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 myObjectand 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 someThingOrOtherFactories 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.
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.
Maricopa Center for Learning and Instruction (MCLI)
The Internet Connection at MCLI is
Alan Levine --}
Comments to
levine@maricopa.edu
URL: http://www.mcli.dist.maricopa.edu/director/faq/faq14.html