|
Object-Oriented Fundamentals
Intro
Overview
What An Object Is
Accessing Properties and Methods of an Object
Passing By Reference
How Director Uses Scripts
The Objects That Make Up Director Itself
Lingo Data Types
Cloning Objects
Comparing Objects For Equality
More On Calling Object Methods
Scripts Associated With Sprites
Garbage Collection
Speed Considerations
Inheritance
Object Oriented Design
Why OOP?
Intro
Understanding how objects can be used greatly simplifies the programming
of complex animation. Investing a little time in getting the hang of it
yields big returns.
Object-oriented programming was the latest big development
in how computer programs are written. It has to do with how code
is organized and executed. Object-oriented programming must be
done in an object-oriented language, and any
program written in an object-oriented language is an object-oriented program.
The newer languages used today are object-oriented, such as C++ and Java.
Though it is not usually presented as one, Lingo is an
object-oriented language.
Overview
All code written in an object-oriented language is organized into what
are called classes. In Director, each script is a class.
A class is composed of two types of elements: properties
and methods. Properties are variables belonging to the class,
and methods are units of code belonging to the class. In Director, handlers
are methods.
Before the code in a class can be executed, the class must
be "instantiated" at run-time which creates
an object from the class. An object is a working
copy of the class, and multiple objects can be created from the
same class. In Director, the new() method is used to
create an object from a class.
This convention is relaxed in Director,
so that code can be executed without creating objects using Lingo. Just
designate a script as a "movie" script or attach it to a sprite,
etc, and Director takes care of it. However, taking advantage of the strengths
of object-oriented programming in Director requires creating and using
objects in Lingo.
What An Object Is
Make a new script called "class1" with this
code:
property var
on new(me,
arg1)
var = arg1
return me
end
on putVar()
put var
end
After making changes to a script, remember to recompile
it by clicking 
This script has one property, var, and
two methods, new() and putVar(). In
object-oriented terminology handlers are called 'methods'.
The word method will be used in place of handler
in this section.
In the message window, type this command:
objRef = script("class1").new()
The new() method is a special one that
creates a new object. This is what happens when the command
above is executed:
- a chunk of memory is designated to
hold a new object. The new object is set up with the properties and
methods defined in the "class1" script. The contents
of this chunk of memory is the object. It can be considered
a working copy of the script it is created from.
- a pointer to this memory location is
passed as the first parameter to the created object's new()
method. In this script, the first parameter variable
of the new() method is called 'me', so that the variable 'me'
becomes a pointer to the new object. A pointer is simply the
memory address of the object.
- the commands in the new() method are executed. In this
script, the value of 'me' is returned, which is a pointer to the new
object.
- since a pointer to the object is returned from the new()
method, the variable objRef becomes a pointer to the
new object, and can now be used to access the properties and methods
of the new object.
| Every time
the new() method of a script is called, this process occurs and
creates a new object at a different place in memory.
New objects can continue to be created until memory is filled up.
In object-oriented terminology, a pointer
to an object is called an "object reference",
or just a "reference". A script is a "class".
Objects are "instances" of classes, and
"instantiating" a script creates an object.
The phrase "child object"
in Director documentation means an object created from a script. |
|
Accessing Properties and Methods
of an Object
In most, if not all, object-oriented languages the properties and methods
of an object are accessed using 'dot-syntax':
objRef.property
objRef.method(arg1,arg2,...)
In the message window, type:
objRef = script("class1").new(5)
put objRef.var
-- 5
objRef.putVar()
-- 5
objRef2 = script("class1").new(10)
put objRef2.var
-- 10
put objRef.var
-- 5
The object references objRef and objRef2
point to two different objects created from the same
script. They are used to indicate which object's
properties and methods are to be accessed.
Objects created from the same script have the same methods
and properties, but may have different values for their properties.
Properties of an object may be set and retrieved,
and methods of the object may be called with 0 or more
arguments. When a method is called the statements in the method execute.
If there is a return statement in a method, it returns a value to the
statement in the program that called the method. Otherwise, the return
value is void.
When a method contains statements that use a variable defined
as a property for that object, it refers to that object's value
for that property. In order to refer to a property of a different
object, an object reference for that other object must be used (dot syntax).
Passing By Reference
When a variable var2 is set equal to another variable
var1 which is an object reference, var2 becomes a reference
to the same object as var1. This diagram represents what
is happening in memory:
This means that if var2 is used to change
properties of the object, those changes will show when accessing the object
using var1 (and vice versa). There can be any number
of pointers to the same object (until memory fills up).
Because of this, an object reference can be passed to a
method as a parameter and the method will be able to operate on that object
because its parameter variable will become a pointer
to the same object.
How Director Uses Scripts
The reason it is possible to write and use Lingo without knowing about
creating objects is that Director will create and use them for you. This
is what happens when you "attach" a script to a sprite
or designate a script a "movie" script.
Sprite Scripts
When a script is "attached" to a sprite, Director will create
an object from that script when the playback head encounters the sprite
in the score. The object reference will be placed in the sprite's scriptInstanceList,
which causes Director to call certain of the object's methods in response
to events related to that sprite, such as mouse events.
When Director calls one of the object's methods, it passes the object
reference as the first parameter. At run-time, you
may create objects and add them to a sprite's scriptInstanceList to make
Director call their sprite-related methods.
Movie Scripts
When a script is designated a "movie" script, Director will
call the script object's methods in response to events throughout the
duration of the movie. This is a little different than creating a new
object. Director seems to use the script cast member object itself rather
than create a new instance.
Other Ways Director Uses Scripts
- Frame scripts are instantiated by Director
when the script is encountered in the score. Methods are called in response
to events related to that frame.
- If you place an object reference in 'the actorlist',
Director will call that object's stepframe() method,
if it has one, once per frame.
- If you set 'the alertHook' to an object
reference, Director will call that object's alertHook() method if an
error occurs. Director will pass the object reference back to the method
along with two strings describing the error.
- I'll take a pass on cast member scripts
since I never use them.
When Director creates an object, it immediately calls its
new() method, if it has one.
Director documentation classifies scripts as movie, behavior,
and parent although script cast members are all the same type
of object. For example, you can attach a behavior script to a
sprite, then change it to a movie script, and then instantiate
it at run-time. The classification just tells Director how to use the
script.
The Objects That Make Up Director
Itself
Director itself is programmed in an object-oriented language. All the
elements in the Director environment, such as sprites and cast members,
are objects. The classes that define these objects are in the files that
are put on your hard drive when you install Director. These objects are
created when Director starts up.
Lingo phrases like sprite(1), member("foo"),
(the stage), and model("bar") are
all references to objects, and can be passed around like references to
objects you create from your own scripts.
Lingo Data Types
The types of data that you work with in Lingo are either 'primitives',
such as numbers, or objects. Primitive data types are
stored without using pointers:

There are many object data types, such
as linear list, property list, point, vector, rgb, rect, quad, image,
transform, etc. Director will create a new object when you explicitly
use an object data type in a statement. Try typing this in the message
window:
put point(350, 700).loch
-- 350
pt1 = point(100, 200)
pt2 = pt1
pt2.locv = 500
put pt1
-- point(100, 500)
In the first statement, when Director encounters the phrase
'point(350, 700)' it creates a new point object
and returns an object reference. Then it evaluates '.loch'
and uses the reference to get the object's loch property value.
Many properties of Director objects are object data types,
for example the loc property of a sprite object is a
point data type. If you set a variable equal to the loc
property of a sprite object, it will become a reference to a point
object.
You might assume that since you have a reference to the
sprite's loc point object, you can use that reference to change the loc
of the sprite. However, this isn't the case. When you access a sprite's
loc property Director creates a new point object and
returns a reference to that, so that changing the loch and locv of that
point object has no effect on the sprite:
put sprite(1).loc
-- point(100, 200)
pt = sprite(1).loc
pt.locv = 500
put sprite(1).loc
-- point(100, 200)
How Director is executing lingo behind the scenes is often
not apparent. But by testing, as above with the loc property,
you can tell if Director is giving you a reference to an object that it
is using or is creating a new one for you.
A good example of this is the transform property
of a 3D node. If you access the transform property, Director will return
a reference to the actual transform object it is using when it
renders the model. This means you can pass the transform object
reference to a method that, say, rotates a transform, and it will rotate
the 3D node. However, if you access the scale, position, or rotation of
a transform of a 3D node, it will create a new vector object
and give you a reference to that. Changing the x, y, or z of this vector
object will have no effect on the 3D node.
Cloning Objects
Sometimes it is necessary to create a copy of an object, so that the copy
can be used and changed without altering the original.
This is a standard feature in object-oriented languages.
In Lingo there isn't a standard way to clone objects. List,
cast member, image, transform, and vector objects have a duplicate()
method that creates a copy and returns a reference to the copy.
I'm not aware of a standard way of cloning objects created
from scripts. If you need to clone one of your own objects, you can write
a cloning method that creates a new object from the same script,
sets all its properties equal to the values in the original, and returns
a reference.
Script "classX"
property var1, var2
on duplicate()
dup = script("classX").new()
dup.var1 = var1
dup.var2 = var2
return dup
end
A cloning method
Comparing Objects for Equality
In object-oriented languages in general, comparing two object references
for equality will return true only if they both point to the same
object. This is the case in Lingo when comparing references to
objects created from scripts.
For other types of objects, Director uses different
comparisons. Comparing references to point objects will return
true if they have the same loch and locv. Similarly, transform, vector,
rgb, etc objects are equal if they have equal numerical values.
Comparing for inequality, such as less/greater than or
inequal, depends on the object. If you need to compare for inequality,
test to see how Director is doing the comparison. For example, if you
test whether one point is less than another, Director will return the
result of comparing their loch coordinates.
If you want to be able to compare your own objects
in some special way, you can write a method that accepts a reference to
another object, performs the comparison, and returns the result:
property var1
on equalTo(me,
objOther)
return (var1 = objOther.var1)
end
A comparison method
More On Calling Object Methods
When a method is called via an object reference, such as objRef.method(x,y,z),
it is equivalent to calling method(objRef,x,y,z) (this
is peculiar to Lingo, not object-oriented languages in general). This
means an object reference is passed as the first parameter.
The reference is to the script instance itself. Other passed arguments
like x,y,z come after. When Director itself calls a method,
like exitframe or mousedown, it always passes the reference.
This means that the first parameter variable
after the method name becomes a reference to the script instance itself
when the method is called. In examples this variable is usually called
'me', although it can be anything. There is no significance
attached to the word 'me', except that it gets colored blue
in the script editor. It can be used for a variable like other variable
names.
There are two situations where it is necessary
to include a parameter variable for the reference: 1)
you need to use the reference in the method, and 2)
the method is called via reference and parameters (other than the reference)
are passed. In case 2, the reference variable is needed as a placeholder
since a reference will be passed as the first parameter.
If the method is written without any parameter variables,
a passed reference (and any other passed parameters) get dropped. The
method will still execute and still operate on that instance's properties.
The only way to call a method without using an object reference,
for example method() as opposed to objRef.method(),
is when calling it from another method in the same object. If no object
reference is used, a reference does not get passed as the first parameter.
The methods of an object that Director calls can also
be called in lingo, so that you can call methods like exitframe(),
mouseup(), beginsprite(), etc in your code.
Scripts
Associated With Sprites
When a script instance is associated with a sprite by attaching the script
in authoring or adding the instance to the sprite's scriptInstanceList
at run-time, Director allows its properties and methods to be accessed
like sprite properties and methods. If the script instance has a property
prop, it can be accessed with sprite(x).prop.
This is equivalent to sprite(x).scriptInstanceList[1].prop
(if the instance is the first one in the list).
When a method is called via a sprite object reference,
the reference that gets passed to the method is still the script object
reference.
Garbage Collection
When there are no more references to an object, the memory
that the object occupies is marked as unused and eventually gets overwritten.
This is called "garbage collection". It is good programming
practice to remove references to an unneeded object so memory isn't used
unnecessarily.
This is done by setting all references to the unneeded
object to some other value, or void, and deleting references
from any lists.
Speed Considerations
When trying to optimize an algorithm to run as fast as possible, one thing
to do is minimize the number of objects created. Creating
an object has computational overhead associated with it.
For example, say you have a general purpose method that
rotates a vector:
on rot(vec, angleVec)
trans = transform()
trans.rotate(angleVec)
return trans * vec
end
Every time this method executes, it is creating a new transform
object. This can be avoided by creating one transform object and reusing
it:
global trans
on rot(vec, angleVec)
trans.identity()
trans.rotate(angleVec)
return trans * vec
end
This optimization makes execution of the method
two to three times as fast. The transform object would
be created sometime before rot() is called, the reference being either
global and used throughout the movie, or an object property.
Since the Lingo implementation is "black
box" (Director isn't open-source software), the efficacy of most
optimizations can only be determined by actual speed testing.
Inheritance
Inheritance allows a script (class) to build upon another script.
When a script inherits another script, it gets all the script's properties
and methods in addition to its own.
The advantage to using inheritance is that
some core functionality you want several scripts to have can be put in
one script, and then other scripts can inherit it. This way code doesn't
need to be duplicated, and any changes needed to the core functionality
only need to be made in one place.
In Lingo, inheritance is established by declaring
a property called "ancestor" and then making
it a reference to an object created from another script:
property
ancestor
property var
on new(me)
ancestor = script("ancestorScript").new()
return me
end
on putVar()
put var
end
An object created from this script will have
all the properties and methods of the script "ancestorScript"
in addition to the property var and the method putVar().
For this object to reference the properties
and methods it inherited from ancestorScript, Director requires that the
'me' reference be used (or whatever variable you use for the object reference):
me.inheritedVar
me.inheritedMethod()
If a script declares properties or methods
that are also declared in the ancestor script, they override the
ancestor's versions. In this case, the ancestor versions can
be accessed using the ancestor object reference:
ancestor.var
ancestor.method()
If a script's ancestor itself has an ancestor
(and so on), the script inherits from all of them.
An example of using inheritance can be seen
in 3D Resource Objects, where tree node
functionality is inherited to make two different types of trees.
Object Oriented
Design
So how do you write a program using objects? Organizing properties and
methods into a number of objects and deciding how the objects will work
together is an exercise in object oriented design. This
is a huge topic that is best discussed in the context of specific programming
exercises.
With that in mind, there are several sections
of this tutorial that touch on object oriented design. I recommend them
in their order of complexity:
Case Study: Space
Viewport
Case Study: Mr. Eyes
3D World Hierarchy
3D Resource Objects
3D Camera Movement
Why OOP?
Any program you can write in an OO language could be written in a non-OO
language. (Or on a Turing
Machine, for that matter). However, OOP has a number of advantages:
Organizing code into classes makes it easier
to model a program as a set of well-defined components that work
together, which is a natural way for people to think.
Classes (scripts) are highly portable
and can be easily reused to assemble new programs.
Objects are stored and passed by reference.
With one statement, all the properties and functionality (methods) of
an object can be passed for another method to use and operate on. Objects
can be assembled using references into data structures
and functional configurations (a la Mr. Eyes).
This touches on some OOP concepts most relevant
in Lingo programming and the types of programs Director is usually
used for; for more on OOP in general see any of the many books and
web pages written on the subject.
|