It's not uncommon to create dozens of complex objects that store rich information about everything from products in a shopping-cart system to bad guys with artificial intelligence in a video game. To expedite the creation of objects and to define object hierarchies (relationships between objects), we use object classes. A class is a template-style definition of an entire category of objects. As you learned in the introduction to this chapter, classes describe the general features of a specific breed of objects, such as "all dogs bark."
Are you ready to get your hands dirty? Let's create a custom Ball class. The first step in class creation is designing the properties and methods the class supports. Our Ball class will have the following properties:
Half the diameter of a ball
A ball's color
A ball's horizontal location
A ball's vertical location
Our Ball class will have the following methods:
Used to reposition a ball
Used to determine the amount of space an individual ball occupies
In natural language, our Ball class would be described to the interpreter as follows:
A Ball is a type of object. All ball objects have the properties radius, color, xPosition, yPosition, which are set individually for each ball. All ball objects also share the methods moveClipTo( ) and getArea( ), which are identical across all members of the Ball class.
Instances of our Ball class will have no on-screen presence; for now, they will simply represent balls conceptually in our program. Later in this chapter, under Section 12.5.4, we'll consider how to actually render the ball to screen. Then, in Chapter 14, we'll show how to implement our Ball class as a type of movie clip. Stay tuned!
There is no specific "class declaration" device in ActionScript; there is no "class" statement that creates a new class as the var statement creates new variables. Instead, we define a special type of function, called a constructor function, that generates a new instance of our class. By defining the constructor function, we are effectively creating our class template or class definition.
Syntactically, constructor functions (or simply constructors) are formed just like normal functions. For example:
function Constructor () { statements }
The name of a class's constructor function can be any valid function name, but, by convention, it is capitalized to indicate that it is a class constructor. A constructor's name should describe the class of objects it creates, as in Ball, Product, or Vector2d. The statements of a constructor function initialize the objects it creates.
|
We'll make our Ball( ) constructor function as simple as possible to start. All we want it to do for now is create an empty object. To create an empty object, our Ball( ) constructor need not include any statements.
// Make a Ball( ) constructor function Ball () { }
That didn't hurt much. Shortly, we'll see how to create properties inside our constructor and initialize new ball objects, but first let's see how to generate ball objects using our Ball( ) constructor function.
A constructor function both defines our class and is used to instantiate new instances of the class. As you learned earlier in this chapter under Section 12.2, when invoked in combination with the new operator, a constructor function creates, and then returns, an object instance.
Recall the general syntax for creating a new object based on a constructor function:
new Constructor(); // Returns an instance of the Constructor class
So, to create a ball object (an instance) using our Ball class, we write:
// Stores an instance of the Ball class in the variable bouncyBall var bouncyBall = new Ball( );
Notice that even though our Ball( ) constructor function is currently empty, and does not explicitly return an instance of the Ball class, when the constructor is invoked together with the new operator, ActionScript generates an instance automatically. Now let's make our Ball class constructor assign properties to the ball objects it creates.
To customize an object's properties during the object-creation stage, we use the special this. Within a constructor function, the this keyword is a reference to the object currently being generated. Using that reference, we can assign whatever properties we like to the embryonic object. The following syntax shows the general technique:
function Constructor () { this.propertyName = value; }
where this is the object being created, propertyName is the property we want to attach to that object, and value is the data value we're assigning to that property.
Let's apply the technique to our Ball example. Here's how the Ball class constructor might assign initial values for the radius, color, xPosition, and yPosition properties to its instances. Note the use of the this keyword, and remember that inside our Ball constructor function, this means "the new ball being created."
function Ball () { this.radius = 10; this.color = 0xFF0000; this.xPosition = 59; this.yPosition = 15; }
|
With the Ball( ) constructor thus prepared, we can create Ball instances (i.e., objects of the Ball class) bearing our predefined properties�radius, color, xPosition, and yPostion�by invoking Ball( ) with the new operator, just as we did earlier. For example:
// Make a new instance of Ball var bouncyBall = new Ball( );
Now we can access the properties of bouncyBall that were set when it was made by the Ball( ) constructor, as follows:
trace(bouncyBall.radius); // Displays: 10 trace(bouncyBall.color); // Displays: 16711680 (decimal for 0xFF0000) trace(bouncyBall.xPosition); // Displays: 59 trace(bouncyBall.yPosition); // Displays: 15
Now wasn't that fun?
Unfortunately, so far our Ball( ) constructor uses fixed values when it assigns properties to the objects it creates (e.g., the initial radius is always 10). Every object of the Ball class, therefore, will have the same initial property values, which is very inflexible. Let's add parameters to our class constructor that let us specify varying initial property values, so that each ball can be unique when it is born.
Here's the general syntax used to define parameters for a class constructor:
function Constructor (value1, value2, value3) { this.property1 = value1; this.property2 = value2; this.property3 = value3; }
Inside the Constructor function, we refer to the object being created with the this keyword, just as we did earlier. This time, however, we don't hardcode the object properties' values. Instead, we assign the values of the arguments value1, value2, and value3 to the object's properties. When we want to create a new, unique instance of our class, we pass our class constructor function the initial property values for our new instance:
someObject = new Constructor (value1, value2, value3);
Let's see how this applies to our Ball class, shown in Example 12-1.
// Make the Ball( ) constructor accept property values as arguments. function Ball (radius, color, xPosition, yPosition) { this.radius = radius; this.color = color; this.xPosition = xPosition; this.yPosition = yPosition; } // Invoke our constructor twice, passing it arguments to use as // our new objects' property values. var greenBall = new Ball(42, 0x00FF00, 59, 15); var whiteBall = new Ball(14, 0xFFFFFF, 50, 12); // Now let's see if it worked... trace(greenBall.radius); // Displays: 42, :) Pretty cool... trace(whiteBall.radius); // Displays: 14 // Each instance has unique property values.
By convention, the parameters being passed in (radius, color, etc.) use names similar to the corresponding internal properties (this.radius, this.color, etc.), but don't confuse the two. The former are transient parameter variables used simply to transmit the arguments passed to the constructor function; the latter (beginning with this) are the object properties where those values are permanently stored.
In general, a class's properties should all be declared at the top of the class constructor. Typically, a new property should not be added to a class by a method. Unlike Java, ActionScript leniently allows methods to create new object properties, but doing so leads to unmanageable class structures. Methods should only modify properties declared in the constructor. However, for the sake of reduced memory consumption, a class with, say, 100 properties can be forced to relegate property creation to methods if instantiating instances has a noticeable effect on Flash Player performance.
We're almost done building our Ball class. But you'll notice that we're still missing the moveClipTo( ) and getArea( ) methods discussed earlier. Let's take care of that now.
Methods are added to a class by assigning a function to the special prototype property of the class constructor (we'll cover the technical role of prototype later in this chapter under Section 12.7). The general syntax is:
Constructor.prototype.methodName = function;
where Constructor is the class constructor function, methodName is the name of the method, and function is the function executed when the method is used. The function can be supplied as a function reference or a function literal. For example:
// Function reference Ball.prototype.getArea = calcArea; function calcArea ( ) { // ...method body goes here... } // Function literal Ball.prototype.getArea = function () { // ...method body goes here... };
The function literal approach is generally preferred for methods because it does not leave an unneeded function reference in memory after method assignment is finished.
|
You learned earlier that a method of an object often retrieves or sets the properties of the object on which it is invoked. So, the question is, how does the method refer to that object in order to access its properties? Answer: through the magic of the this keyword. Remember that we used the this keyword inside our Ball constructor function to refer to new ball objects being created by the constructor. Inside a method, this similarly refers to the ball object currently being operated on. For example, to access an individual Ball instance's radius property inside getArea( ), we use this.radius, as shown in the following listing for our Ball.getArea( ) method:
Ball.prototype.getArea = function ( ) { // The this keyword means the ball on which getArea( ) was invoked. return Math.PI * this.radius * this.radius; };
Likewise, a method can set the value of a property via this, as shown in our Ball class's moveClipTo( ) method. The moveClipTo( ) method assigns new values to the xPosition and yPosition properties of a ball. Notice that the moveClipTo( ) method takes two parameters, x and y, which specify the ball's new position:
Ball.prototype.moveClipTo = function (x, y) { // Notice the use of this to refer to the ball being moved. this.xPosition = x; this.yPosition = y; };
We've now created the all properties and methods of our Ball class, so our work is done! The final code is shown in Example 12-2. Attach the example code to frame 1 of a new movie; then choose Control Test Movie to see it in action.
// The Ball( ) class constructor. function Ball (radius, color, xPosition, yPosition) { // Create Ball properties. this.radius = radius; this.color = color; this.xPosition = xPosition; this.yPosition = yPosition; } // The Ball.getArea( ) method. Ball.prototype.getArea = function () { // The keyword this means the ball on which getArea( ) was invoked. return Math.PI * this.radius * this.radius; }; // The Ball.moveClipTo( ) method. Ball.prototype.moveClipTo = function (x, y) { // Notice the use of this to refer to the ball being moved. this.xPosition = x; this.yPosition = y; }; // Make a large red ball, positioned at the origin (0, 0). var bigBall = new Ball(600, 0xFF0000, 0, 0); // Check the ball's area, which is based on its radius. trace(bigBall.getArea( )); // Displays: 1130973.35529233 // Reposition our ball via the moveClipTo( ) method. bigBall.moveClipTo(100, 200); // Check the ball's new xPosition. trace(bigBall.xPosition); // Displays: 100
Invoking a method and defining a method have similar syntax but very different results. It's crucial to understand the difference. When defining a method, omit the parentheses:
SomeClass.prototype.someMethod = someFunction;
When invoking a method, include the parentheses:
someObj.someMethod( );
The difference in syntax is subtle. Make sure to get it right.
As you learned earlier, strict OOP practice frowns upon accessing an object property directly. Instead, so-called getter/setter methods should be used to access the property. A getter method retrieves the value of the property, while a setter method assigns it. For example, our Ball class defines a radius property. Rather than access that property directly, it's considered good form to access it with a getRadius( ) method and a setRadius( ) method. For example, consider this simplified version of our Ball class:
// The Ball class function Ball (radius) { // Instance properties this.radius = 0; // Initialization this.setRadius(radius); } Ball.prototype.getRadius = function ( ) { return this.radius; } Ball.prototype.setRadius = function (newRadius) { this.radius = newRadius; } Ball.prototype.getArea = function () { return Math.PI * this.getRadius( ) * this.getRadius( ); }; // A ball instance var largeBall = new Ball(400); largeBall.setRadius(500); trace(largeBall.getArea( )); // Displays: 78.5398163397448
The class constructor declares the radius property and suggests its datatype by assigning it a number (0). However, all other access to radius is mediated by the getRadius( ) and setRadius( ) methods. Even the constructor itself uses setRadius( ) to assign radius its starting value. Inside the getArea( ) method, the radius property is retrieved via calls to getRadius( ). And in the outside world, we use setRadius( ) to assign the largeBall's new radius. By using getter/setter methods, we indicate that we want to change some attribute of the ball, but we let the object itself worry about how that change is implemented. Changing the internal implementation from, say, using a radius property measured in pixels to using a radius property measured in centimeters won't break any code that properly uses the getter and setter methods; only the getter/setter methods need to be modified. If code outside the object refers to the radius property directly, the programmer maintaining the object has less freedom to change it.
Getter/setter methods safely encapsulate the workings of an object, but they are admittedly more verbose than a property assignment (and often make the code harder to read). Therefore, in practice, many programmers don't bother implementing getter/setter methods, and many programmers don't use them even when they are available. To address this issue, Flash MX added the addProperty( ) method to all objects (derived from the Object class). The addProperty( ) method creates a so-called getter/setter property that, when accessed, automatically invokes the appropriate getter/setter method. This lets programmers set and retrieve object properties with somewhat more brevity, while still maintaining the benefits of pure OOP practice. The following example shows the general technique. For much more information, see Object.addProperty( ) in the ActionScript Language Reference.
// The Ball class function Ball (radius) { // Instance properties. this._radius = 0; // Initialization this.setRadius(radius); } Ball.prototype.getRadius = function ( ) { return this._radius; } Ball.prototype.setRadius = function (newRadius) { this._radius = newRadius; } Ball.prototype.getArea = function () { return Math.PI * this.radius * this.radius; }; // The getter/setter radius property, defined on prototype Ball.prototype.addProperty("radius", Ball.prototype.getRadius, Ball.prototype.setRadius); // A ball instance var largeBall = new Ball(400); largeBall.radius = 5; // Invokes the setRadius( ) method automatically. trace(largeBall.getArea( )); // Displays: 78.5398163397448
Note that the class uses an internal _radius property to store the value of the getter/setter property called radius. The internal property must not have the same name as the externally visible getter/setter property, and is therefore prefaced with an underscore by convention. Getter/setter properties can be used to create read-only properties, as discussed under Object.addProperty( ) in the ActionScript Language Reference.
Reader Exercise: Try adding getColor( ) and setColor( ) methods that get and set the color property in the Ball class. Once you get that far, try using Object.addProperty( ) to turn color into a getter/setter property. Don't forget to use an internal _color property to store the value of the getter/setter property called color!
It's possible, though not advised, to add a method to each instance by assigning a function to an instance property within the class constructor. Here's the generic syntax:
function Constructor () { this.methodName = function; }
Assigning a method on a per-instance basis in a class constructor is possible in ActionScript because an ActionScript method is technically just a function stored in an object property. The function assigned as the new method can be supplied in several ways, as shown in the following examples. The function can be a function literal, as in:
function Ball (radius) { this.radius = radius; // Add the getArea( ) method... this.getArea = function () { return Math.PI * this.radius * this.radius; }; }
Alternatively, the function can be declared inside the constructor:
function Ball (radius) { this.radius = radius; // Add the getArea( ) method... this.getArea = calcArea; function calcArea () { return Math.PI * this.radius * this.radius; } }
Finally, the function can be declared outside the constructor but assigned inside the constructor:
// Declare the getArea( ) function function calcArea () { return Math.PI * this.radius * this.radius; } function Ball (radius) { this.radius = radius; // Add the getArea( ) method... this.getArea = calcArea; }
There's no real difference between the three approaches. Function literals often prove the most convenient to use, but they are perhaps not as reusable as defining a function outside of the constructor so that it can also be used in other constructors.
Regardless, all three approaches lack efficiency and are not recommended. When we assign a method to the objects of a class using the techniques we've just seen, we are unnecessarily duplicating the method on every instance of our class. The getArea( ) formula is the same for every ball, and hence the code that performs this task should be stored in a central location�the constructor's prototype property. For better or worse, ActionScript is not a strict language; it often provides several ways of accomplishing a similar goal.
In Java, a static method (also called a class method) is a method that is invoked through a class directly, rather than through an object (i.e., an instance of a class). Similarly, a static variable in Java is akin to a property that is accessed through a class, independent of any object instance. Java's static methods and properties are used to store behavior and information that logically relates to a class but should be universally available, rather than available only through a specific object instance. For example, a Square class might define a static property, numSides, that indicates the number of sides for all square instances. By convention, the getter/setter methods for a static property�such as getNumSides( ) and setNumSides( )�would be implemented as static methods. ActionScript's built-in TextField class defines the static method getFontList( ), which returns a list of fonts on the user's system. The getFontList( ) method is logically associated with text fields in general, not a specific text field, so it is implemented as a static method.
ActionScript does not have a syntactic mechanism for explicitly declaring methods or properties as static (as Java does). Instead, static methods and properties are created by attaching properties and methods to a class's constructor function, as follows:
// Class definition function SomeClass ( ) { statements } // Static Property SomeClass.someStaticProperty = value; // Static Method SomeClass.someStaticMethod = function;
Note that, unlike Java, ActionScript's static methods have no limitation on the variables, properties, or methods they can access (in Java, a static method can refer only to static variables and methods). Furthermore, in a static ActionScript method, the this keyword refers to the class constructor function, whereas Java's static methods cannot use the this keyword.
When defining a property that will hold a constant value, it is also perfectly legitimate to use the class's prototype property, as follows:
function Square ( ) { statements } Square.prototype.numSides = 4;
However, this approach demands that numSides be accessed through an instance of Square, not through the Square class directly. If you want to use a property without first creating an instance of a class, you should make that property static.
Sometimes, static properties and methods are used to package a series of related constants and functions into a class that will never be instantiated. In ActionScript, this is normally achieved by adding properties and methods to a generic instance of the Object class. For example, the Math, Key, Mouse, and Stage objects are implemented in this manner.
For the benefit of Java programmers, Table 12-1 outlines the rough equivalencies between Java and ActionScript methods and properties.
Java name |
Java description |
ActionScript description |
ActionScript example |
---|---|---|---|
Instance variables |
Variables local to an object instance |
Properties defined in a class constructor function and copied to objects |
function Square (side) { this.side = side; } |
Methods invoked on an object instance |
Methods defined on the prototype object of a class constructor function and accessed automatically through prototype when invoked on an instance |
Square.prototype.getArea = function ( ) { ... }; mySquare.getArea(); |
|
Variables with the same value across all object instances of a class |
Properties defined on a class constructor function |
Square.numSides = 4; |
|
Class methods |
Methods invoked through a class |
Methods defined on a class constructor function |
Square.findSmaller = function (square1, square2) { ... } |
One of the crucial features of classes in advanced object-oriented programming is their ability to inherit properties and methods. That is, an entire class can adopt the properties and methods of another class and add its own specialized features. The class that borrows the properties and methods of another class is known as the subclass. The class that has the properties and methods borrowed is known as the superclass. The subclass is said to extend the superclass because it borrows (inherits) the superclass's methods and properties but also adds its own. Several subclasses can inherit from the same superclass; for example, there may be a Car superclass with Coupe and Sedan subclasses. The ability of classes to adopt methods and properties from other classes is known as inheritance.
For example, suppose we want to create two types of quiz programs: a multiple-choice quiz and a fill-in-the-blank quiz. The two programs will have much in common. They'll both load questions, display questions, accept answers, and tally the user's score. However, there will be minor variations in the ways they present questions, receive answers, and tally the score. Rather than creating two highly similar programs separately, we create a general Quiz class that defines methods and properties common to both quizzes. Then, to represent our specific quiz types, we create two Quiz subclasses, MultipleChoiceQuiz and FillInBlankQuiz. The subclasses inherit all of Quiz's abilities but add methods and properties specific to the tasks of multiple-choice or fill-in-the-blank question management. The subclasses can even replace (or override) the behavior of one or more Quiz methods.
Let's see how to implement inheritance in ActionScript with our simple Ball example class. Earlier we created a getArea( ) method on Ball. Now suppose that we want to create many different types of circular objects. Rather than creating a getArea( ) method on each circular object class, we create a general superclass, Circle, that defines a general method, getArea( ), used to calculate the area of any circular object. Then, we make Ball (and any other circular object class) a subclass of Circle, so Ball inherits the getArea( ) method from Circle. Notice the hierarchy�the simplest class, the Circle superclass, defines the most general methods and properties. Another class, the Ball subclass, builds on that simple Circle class, adding features specific to Ball-class instances but relying on Circle for the basic attributes that all circular objects share.
Earlier you learned that methods are defined on the special prototype property of a class's constructor function.
|
We'll delay our technical consideration of the prototype property until later. For now, concentrate on the general syntax used to specify a superclass:
SubclassConstructor.prototype = new SuperClassConstructor( );
where SubclassConstructor is the subclass's constructor function and SuperClassConstructor is the superclass's constructor function. Example 12-3 applies the technique to our Ball and Circle example.
// Create a Circle (superclass) constructor function Circle () { // Instance properties and initialization go here. } // Create the Circle.getArea( ) method Circle.prototype.getArea = function ( ) { return Math.PI * this.radius * this.radius; }; // Create our usual Ball class constructor function Ball (radius, color, xPosition, yPosition) { this.radius = radius; this.color = color; this.xPosition = xPosition; this.yPosition = yPosition; } // Make Ball a subclass of Circle Ball.prototype = new Circle( ); // Now let's make an instance of Ball and check its properties var bouncyBall = new Ball (16, 0x445599, 34, 5); trace(bouncyBall.xPosition); // 34 trace(bouncyBall.getArea( )); // 804.247719318987 // The getArea( ) method was inherited from Circle
|
The class hierarchy in Example 12-3 has poor structure�Ball defines radius, but radius is a property common to all circles, so it belongs in our Circle class. The same is true of xPosition and yPosition. To fix the structure, we'll move radius, xPosition, and yPosition to Circle, leaving only color in Ball. (For the sake of the example, we'll treat color as a property only balls can have.)
Conceptually, here's the setup of our revised Circle( ) and Ball( ) constructors (the Circle.getArea( ) method is not shown):
// Create the Circle (superclass) constructor function Circle (radius, xPosition, yPosition) { this.radius = radius; this.xPosition = xPosition; this.yPosition = yPosition; } // Create the Ball class constructor function Ball (color) { this.color = color; }
Having moved the properties around, we're faced with a new problem. How can we provide values for radius, xPosition, and yPosition when we're creating objects using Ball and not Circle? We have to make one more adjustment to our Ball( ) constructor code. First, we set up Ball to receive all the required properties as parameters:
function Ball (color, radius, xPosition, yPosition) {
Next, within our Ball( ) constructor, we use the super operator to invoke the Circle's constructor function on the Ball object being constructed. While we're at it, we pass Circle's constructor the arguments that were supplied to Ball:
super(radius, xPosition, yPosition);
Our completed class/superclass code is shown in Example 12-4.
// Create a Circle (superclass) constructor function Circle (radius, xPosition, yPosition) { this.radius = radius; this.xPosition = xPosition; this.yPosition = yPosition; } // Create the Circle.getArea( ) method Circle.prototype.getArea = function ( ) { return Math.PI * this.radius * this.radius; }; // Create our Ball class constructor function Ball (color, radius, xPosition, yPosition) { // Invoke the Circle( ) constructor on the ball object, passing values // supplied as arguments to the Ball( ) constructor super(radius, xPosition, yPosition); // Set the color of the ball object this.color = color; } // Set Ball's superclass to Circle Ball.prototype = new Circle( ); // Now let's make an instance of Ball and check its properties var bouncyBall = new Ball (0x445599, 16, 34, 5); trace(bouncyBall.xPosition); // 34 trace(bouncyBall.getArea()); // 804.247719318987 trace(bouncyBall.color); // 447836
In Flash 5, the super operator is not available, so we must replace this line:
super(radius, xPosition, yPosition);
with these three lines, which accomplish the same thing.
this.superClass = Circle; this.superClass(radius, xPosition, yPosition); delete this.superClass;
|
When a subclass implements a method by the same name as a method of its superclass, the subclass version overrides the superclass version. That is, instances of the subclass use the subclass's implementation of the method, not the superclass's, if it exists. However, within a subclass method, the super operator gives us access to the superclass version of any method. For example, suppose both Circle and Ball define a toString( ) method, as follows:
Circle.prototype.toString = function ( ) { return "A circular object"; } Ball.prototype.toString = function ( ) { return "A ball object"; }
By default, Ball instances will use the Ball class's toString( ) method and ignore the method of the same name in Circle. However, we can create another Ball method, named superToString( ), that specifically accesses Circle.toString( ), as follows:
Ball.prototype.superToString = function ( ) { return super.toString( ); }
See more about the super operator in Chapter 5.
Inheritance makes another key OOP concept, polymorphism, possible. Polymorphism is a fancy word meaning "many forms." It simply means that you tell an object what to do but leave the details up to the object. It is best illustrated with an example. Suppose you are creating a cops and robbers game. There are multiple cops, robbers, and innocent bystanders displayed on the screen simultaneously, all moving independently according to different rules. The cops chase the robbers, the robbers run away from the cops, and the innocent bystanders move randomly, confused and frightened. In the code for this game, suppose we create an object class to represent each category of person:
function Cop () { ... } function Robber () { ... } function Bystander () { ... }
In addition, we create a superclass, Person, from which the classes Cop, Robber, and Bystander all inherit:
function Person () { ... } Cop.prototype = new Person(); Robber.prototype = new Person(); Bystander.prototype = new Person();
On each frame of the Flash movie, every person on the screen should move according to the rules for his class. To make this happen, we define a move( ) method on every object (the move( ) method is customized for each class):
Person.prototype.move = function () { ... default move behavior ... } Cop.prototype.move = function () { ... move to chase robber ... } Robber.prototype.move = function () { ... move to run away from cop ... } Bystander.prototype.move = function () { ... confused, move randomly ... }
On each frame of the Flash movie, we want every person on the screen to move. To manage all the people, we create a master array of Person objects. Here's an example of how to populate the persons array:
// Create our cops var cop1 = new Cop(); var cop2 = new Cop(); // Create our robbers var robber1 = new Robber(); var robber2 = new Robber(); var robber3 = new Robber(); // Create our bystanders var bystander1 = new Bystander(); var bystander2 = new Bystander(); // Create an array populated with cops, robbers, and bystanders persons = [cop1, cop2, robber1, robber2, robber3, bystander1, bystander2];
In every frame of the Flash movie, we call the moveAllPersons( ) function, which is defined as follows:
function moveAllPersons () { for (var i=0; i < persons.length; i++) { // Call the move() method for each person in the array persons[i].move(); } }
When moveAllPersons( ) is invoked, each cop, robber, and bystander will move according to the individual rules associated with his class, as defined by its move( ) method. This is polymorphism in action�objects with common characteristics can be organized together, but they retain their individual identities. The cops, robbers, and bystanders have much in common, embodied by the superclass Person. They have operations in common, such as movement. However, they can implement the common operations differently, and they can support other class-specific operations and data.
|
Suppose we have a Rectangle class that is a subclass of Shape. To check whether an object is a descendant of Shape, we can use the instanceof operator, introduced in Flash MX:
rect = new Rectangle(15, 35); trace(rect instanceof Rectangle); // Yields true trace(rect instanceof Circle); // Yields false trace(rect instanceof Shape); // Yields true
We can use instanceof to operate conditionally on an object, depending on its class. The following sample, taken from the quiz at the end of this chapter, deletes all movie clips and text fields in quizClip. It cycles through quizClip's properties and invokes the appropriate removal method on each, depending on whether it is an instance of the TextField class or belongs to the MovieClip datatype.
for (var p in quizClip) { if (typeof quizClip[p] = = "movieclip") { quizClip[p].removeMovieClip( ); } else if (quizClip[p] instanceof TextField) { quizClip[p].removeTextField( ); } }
In Flash 5, the instanceof operator doesn't exist, so we must use the chain of prototype objects (called the prototype chain) to determine an object's class, as shown in Example 12-5. We'll learn more about prototype objects and _ _proto_ _ shortly.
// This function checks if theObj is a descendant of theClass function objectInClass (theObj, theClass) { while (theObj._ _proto_ _ != null) { if (theObj._ _proto_ _ = = theClass.prototype) { return true; } theObj = theObj._ _proto_ _; } return false; } // Make a new instance of Rectangle myObj = new Rectangle(); // Now check if myRect inherits from Shape trace (objectInClass(myRect, Shape)); // Displays: true
All objects descend from the top-level Object class. Hence, all objects inherit the properties defined in the Object constructor (for example, the toString( ) and valueOf( ) methods). We can, therefore, add new properties to every object in a movie by adding new properties to the Object class. Properties attached to Object.prototype will proliferate throughout the entire class hierarchy, including all built-in objects, such as Array, TextField, and even MovieClip objects. This approach can be used to make a sort of global variable or function. In the following code, for example, we hardcode the Stage width and height dimensions into the Object class prototype. We can then access that information from any movie clip (this technique was common in Flash 5 movies, although in Flash Player 6, the Stage dimensions are available as Stage.width and Stage.height):
Object.prototype.mainstageWidth = 550; Object.prototype.mainstageHeight = 400; trace(anyClip.mainstageWidth); // Displays: 550
Object.prototype properties are inherited not only by movie clips, but also by every object, so even our own custom objects and instances of built-in classes such as XML, Date, and Sound inherit properties assigned to Object.prototype. This side-effect causes unwanted properties to appear in for-in loops. Therefore, when exporting for Flash Player 6, always use the _global object to create global properties and methods. See _global in the Language Reference for details.
Reader Exercise: We can attach new properties and methods to any built-in class. Try adding the case-insensitive alphabetical sort function from Example 11-6 to the Array class as a custom method.
Throughout this chapter, we've followed the bouncing Ball class through a lot of theory. However, for all our work, we still haven't actually made a single ball appear on screen. That's because updating the display isn't necessarily the Ball class's concern. Drawing the current content of the screen is often the responsibility of a separate class that deals specifically with the screen and graphical user interface. How the screen is drawn, when it is drawn, and what draws the screen can vary greatly from application to application. For example, in our Quiz at the end of this chapter, we'll update the screen from a QuizGUI class every time a question is displayed. But, in other applications, a ball object would probably move around a lot, so it would need to be displayed repeatedly via either MovieClip.onEnterFrame( ) or setInterval( ).
Here are the major issues to consider when designing an application's display system.
First decide whether the display elements will render themselves or be rendered by a central class. For example, will the Ball class provide its own drawBall( ) method, or will there be a drawBalls( ) or drawScreen( ) method on some other class?
Decide how often the elements should be displayed�either when some event occurs (such as a mouse click) or repeatedly (as fast as frames are displayed in the Flash Player).
Decide on the rendering technique. Each visual element in a Flash movie must be displayed in a movie clip. However, movie clips can be placed manually at authoring time, attached with ActionScript at runtime, or even created from scratch using the MovieClip Drawing API.
Decide on a screen refresh strategy. Will each visual element be maintained as a single persistent movie clip that is updated regularly, or will the entire contents of the Stage be cleared and recreated each time the display is updated?
These decisions will dictate exactly how an application manages its visual assets. For example, we might have a BallSimulation class that updates the screen by drawing circles that represent a list of ball objects. Or, we might add a Ball property, mc, that holds a movie clip representing each Ball object. A Ball.drawBall( ) method could then update the display of a Ball object's corresponding movie clip. When an object and the movie clip that represents it are tied that tightly together, it's often wise to implement the class as a MovieClip subclass, as described in Chapter 14.