overview
learn the basics of object oriented programming (oop)
study object-oriented concepts and code
create an example object-oriented program (a Tamagotchi-style virtual pet)
object oriented programming
oop is a programming methodolgy that breaks a program into separate modules
each module is independent, with its own private responsibilities
but each module must also cooperate with other modules
the "modules" of an object-oriented program are called "classes"
classes
each class is physically a separate part of the program
separate code block
separate file
typically, each class represents either an abstract concept:
or a real "thing"
what's in a class?
you already know what variables and functions are
variables store data
var shoppingListPrices = [100, 15, 340];
functions perform tasks
function calculateCost (priceList) { var cost = 0; for (var i = 0; i < items.length; i++) { cost += priceList[i]; } return cost; }
classes are simply groups of functions and variables
a higher level of organization
oop adds a level of program organization on top of functions
functions add only a basic level of organization to a program (tasks are grouped)
but in oop, functions themselves are grouped together with variables into classes
in a sense, each class is like a miniature program
writing an object-oriented program is like writing many small programs that work together to form a larger program
why OOP?
object-oriented programming helps make a program:
from classes come objects
on its own, a class can't do anything
a classes is only a blueprint, used to create objects
much like a factory is used to create cars
each class creates its own type of object, much like a honda civic factory creates civics but a porche boxter factory creates boxters
a single class can create many objects (again, just like a factory can create many cars)
another word for "object" is "instance", meaning a single specific incarnation of the class
three steps to oop
to create an object-oriented program:
what the objects do determines the behaviour of the program
a simple class: Tamagotchi
let's look at some code
the Tamagotchi
class will represent a simple version of the child's toy by the same name
our Tamagotchi is a virtual pet that must be fed or it will die
the class file
first, create a text file named Tamagotchi.as
the text file will contain our class
the class statement
to create a new class, use the class statement:
class Tamagotchi { }
the class name must match the file name (Tamagotchi.as
) exactly, including case-sensitivty!!
methods: the class's behavior
our Tamagotchi
can be fed, digest food, and die
accordingly, the Tamagotchi
will have a feed()
, digest()
, and die()
function
functions defined in a class determine the class's behaviours
terminology: a function defined in a class is called a method
the digest() method
let's add the digest()
method to Tamagotchi
class
class Tamagotchi { private function digest () { trace("The Tamagotchi is digesting..."); } }
logic first, display later
for now, the digest()
method only print a message in the Output window
but conceptually, the Tamagotchi
already has the ability to digest food
later, we'll define how digesting should look on screen
display is an implementation detail
public versus private
notice the keyword "private" in our method definition:
private function digest () { trace("The Tamagotchi is digesting..."); }
methods defined with the private
keyword can only be invoked with in the class in which they are defined
private methods prevent external code from misusing the class
for example, no external code can make our Tamagotchi
digest too often
in contrast, methods defined with the public
keyword can be invoked from anywhere in a program
creating an object
now that we have a simple class, we can already use it to create objects
our object will be an individual Tamagotchi
, based on the blueprint provided by the Tamagotchi
class
to create an object, we use this code:
new ClassName()
for example, this code creates an object from our Tamagotchi
class:
new Tamagotchi()
storing objects
the code new Tamagotchi()
returns a new Tamagotchi
object
normally we want to store the new object somewhere, such as in a variable
for example, this code stores the new Tamagotchi
object in a variable named pet
var pet = new Tamagotchi();
the technical name for "creating an object" is "instantiation"
accessing a class from a .fla file
we'll instantiate our Tamagotchi
object in a flash document called tamagotchi.fla
to use a class such as Tamagotchi
in a .fla
file, flash must know the location of the class file (.as
file)
flash automatically finds all classes whose .as
files reside in the same directory as the .fla
to make our Tamagotchi
class available to tamagotchi.fla
, we'll put tamagotchi.fla
in the same directory as Tamagotchi.as
then on frame 1 of tamagotchi.fla's timeline, we'll enter this code:
var pet = new Tamagotchi();
other class access options
other ways to make a class accessible to a .fla
file:
our same-directory approach is the easiest, so we'll stick to it for now
no #include required!
note that a class does not have to be #included
like raw code in an .as
file does
access to the class is provided automatically by flash
the feed() method
let's add a method, feed()
, to give the Tamagotchi
food
for now, use trace()
as a placeholder, fill in real code later
public function feed (foodItem) { trace("You fed the Tamagotchi."); }
feed()
is public because external code might need to feed the Tamagotchi
invoking methods on an object
we now have a Tamagotchi
object, stored in the variable pet
we can make the Tamagotchi
do things by invoking its methods
to invoke a method on an object, use this code:
theObject.theMethod()
for example, this code invokes the feed()
method on our Tamagotchi
object
pet.feed()
when we export tamagotchi.swf
from tamagotchi.fla
, the Output panel displays:
You fed the Tamagotchi.
what we've learned so far...
to review, the following code:
Tamagotchi
var pet = new Tamagotchi(); pet.feed();
eventually, we'll create a user interface for controlling the pet
calling the wrong method
what happens if we invoke a method on pet that doesn't exist
for example, suppose we miskenly assume that our Tamagotchi
can sing:
pet.sing()
when we export the movie, nothing happens
or, suppose we make a typo when calling a method:
pet.fede()
the Tamagotchi
class does not define methods named sing()
or fede()
, so the method invocations fail
datatypes and calling the right method
if flash knows what type of object is stored in a variable, it can tell us when we invoke a non-existent method
but flash can't guess the type on its own, we must tell flash the type manually
telling flash the type of a variable is called "declaring the datatype" of the variable
to declare a datatype we use this code:
var someVariable:ClassName;
Tamagotchis can't sing
now let's declare pet
's datatype so flash can warn us when we invoke a non-existent method
var pet:Tamagotchi = new Tamagotchi();
now when we invoke pet.sing(), flash tells us Tamagotchi
objects can't sing:
pet.sing(); **Error** Scene=Scene 1, layer=Layer 1, frame=1:Line 4: There is no method with the name 'sing'. pet.sing();
maintaining state
so far, our Tamagotchi
will live forever
it never gets hungry
we need to monitor the Tamagotchi
's hunger
in other words, we need to maintain the Tamagotchi
's state (i.e., current condition)
we need variables
variables defined in a class describe an object's current state
when defined in a class, variables are called properties
Tamagotchi's properties
our Tamagotchi
class needs to track:
Tamagotchi
's namehere are the corresponding properties:
private var currentCalories:Number; private var name:String;
storing the right type of data
take another look at the currentCalories
property definition:
private var currentCalories:Number;
notice that it includes includes a datatype declaration (:Number
)
a property can only store data matching its declared datatype
for example, the property currentCalories
can only store a number, while name
can only store a string
type mismatch errors
when the wrong type of data is assigned to a property, flash displays a "type mismatch error"
for example, this code:
currentCalories = "just a few";
causes this error:
Type mismatch in assignment statement: found String where Number is required. currentCalories = "just a few";
the code so far
here's our Tamagotchi
class so far, with the three properties added:
class Tamagotchi { private var currentCalories:Number; private var name:String; private function digest () { trace("The Tamagotchi is digesting..."); } public function feed () { trace("You fed the Tamagtochi..."); } }
notice that, by convention, properties should be defined before methods
private properties
take a closer look at the name
property definition
private var name:String;
that property definition uses the keyword private
private properties can only be assigned and retrieved from within the class in which they are defined
my properties are none of your business
suppose we want to know the name of our Tamagotchi
if we try to retrieve the name
property directly, like this
trace(pet.name);
flash displays this error:
The member is private and cannot be accessed.
(terminology: "member" means method or property)
outside of the Tamagotchi
class, Flash will not let us retrieve the name
property
accessor methods
so how do you retrieve or modify the state of an object externally without setting properties?
using accessor methods
accessor methods set and retieve properties
a property-assignment accessor
this accessor sets the name of a Tamagotchi
public function setName (newName) { name = newName; }
notice that the property name
is referred to directly within the method
using an assignment accessor
to give a Tamagotchi
a new name, we call the setName()
method instead of assigning the name
property a value
for example, we use:
pet.setName("yamada");
not:
pet.name = "yamada";
a property-retrieval accessor
this accessor returns the current name of a Tamagotchi
public function getName () { return name; }
to retrieve a Tamagotchi
's name, we call the getName()
method instead of accessing the name property
for example, we use:
trace(pet.getName());
not:
trace(pet.name);
encapsulation
properties that are not externally accessible are encapsulated by their class
similarly, the source code in methods is encapsulated by its class
conceptually, encapsulation means hiding the internal details of a class's operation
goal of encapsulation: use an object without understanding or depending on its internal details
public methods are the tools you use to control the object
build classes like cars are built for drivers...
...drivers don't need to know how a car engine works to use a car--they just press the acceleration pedal
benefits of encapsulation (assignment filtering)
prevents external code from assigning values to properties that would cause an object to malfunction
for example, a user might give a Tamagotchi
a name that won't fit into a database
filtering the assignment through setName()
gives the class a chance to restrict a property's value to a certain range
for example, this version of setName()
checks displays an error for names that are too long or too short
public function setName (newName) { if (newName.length > 20) { trace("Warning: specified name is too long."); newName = newName.substr(0, 20); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; }
benefits of encapsulation (retrieval filtering)
encapsulation allows a class to adjust a property's value before returning it
the accessor method might return:
this version of getName()
returns the string "Unnamed Pet" for Tamagotchi
s whose names have not yet been set
public function getName () { if (name == null) { return "Unnamed Pet"; } else { return name; } }
benefits of encapsulation (easy refactoring)
encapsulation prevents external code from becoming dependent on specific implementation details
lets you change a class internally ("refactor") without breaking external code that uses the class
for example, if we change the property name
to firstName
, any code that uses the name
property directly will break
but code that uses setName()
will not break
if a car's engine changes from gasoline to solar power or hydrogen, the driver's use of the car should not be affected
the driver should still use the acceleration pedal to speed the car up
this code is not encapsulated, and should be avoided:
car.gasolinePerSecond = 100;
this code is preferred because it will still work even if the car's energy source changes:
car.accelerate();
constructor functions
when we create a new Tamagotchi
, we want to be able to name it
in other words, we want to be able to set the state of the object when we create it
to initialize an object at creation time, we use a constructor function
a constructor function runs automatically when an object is created
the code in a constructor function can initialize the object by setting its properties and invoking its methods
Tamagotchi's constructor
here's a constructor function for our Tamagotchi
class
public function Tamagotchi (petName) { setName(petName); }
all constructors must have the exact same name as the class (case-sensitivity matters)
our constructor accepts a parameter used to set the Tamagotchi
's name
notice that the name property is set via setName()
even within the class
initializing an object
we can now name our Tamagotchi
when we create it, as follows:
var pet:Tamagotchi = new Tamagotchi("yamada");
the string "yamada" is passed to the constructor function
the constructor function uses the string to set the Tamagotchi
's name
class properties
to store information that applies to an entire class of objects, not just to a specific object, use class properties (also called static properties)
class properties contain information that is globally required by all objects of a class, and does not vary between objects
for example:
class property syntax
class properties are defined with the static
attribute
for example, this code creates a class property named x
that stores a numeric value
private static var x:Number;
when referring to class properties, include the class name (e.g., SomeClass.someProp
, not just someProp
)
magic values
our setName()
and getName()
methods both use literal values
setName()
uses the number 20:
public function setName (newName) { if (newName.length > 20) { ...
getName()
uses the string "Unnamed Pet"
public function getName () { if (name == null) { return "Unnamed Pet"; ...
unexplained literal values such as 20 and "Unnamed Pet" used in methods are known as magic values (magic strings, magic numbers, etc)
magic values are bad
magic values are discouraged because:
for example, to change the maximum name length we must change "20" in two places--easy to miss one
storing magic values in class properties
class properties are ideal for storing magic values
for example, every Tamagotchi
object has the same maximum name length
therefore, create a class property to store the maximum name length:
private static var MAX_NAME_LENGTH:Number = 20;
then update the setName()
method to use the class property:
public function setName (newName) { if (newName.length > Tamagotchi.MAX_NAME_LENGTH) { trace("Warning: specified name is too long."); newName = newName.substr(0, Tamagotchi.MAX_NAME_LENGTH); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; }
now the code is much easier to read and change!
storing default values in class properties
any Tamagotchi
without a name is called "Unnamed Pet"
therefore, create a class property to store the default name
private static var DEFAULT_NAME:String = "Unnamed Pet";
then update getName()
to use the class property:
public function getName () { if (name == null) { return Tamagotchi.DEFAULT_NAME; } else { return name; } }
use CAPITAL LETTERS when naming class properties with fixed values
class properties don't change on a per-instance basis
therefore, class property values are often set at declaration time, not in the class constructor
the code so far
here's our Tamagotchi
class so far:
class Tamagotchi { private var currentCalories:Number; private var name:String; private static var MAX_NAME_LENGTH:Number = 20; private static var DEFAULT_NAME:String = "Unnamed Pet"; public function Tamagotchi (petName) { setName(petName); } private function digest () { trace("The Tamagotchi is digesting..."); } public function feed (foodItem) { trace("You fed the Tamagotchi."); } public function setName (newName) { if (newName.length > Tamagotchi.MAX_NAME_LENGTH) { trace("Warning: specified name is too long."); newName = newName.substr(0, Tamagotchi.MAX_NAME_LENGTH); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; } public function getName () { if (name == null) { return Tamagotchi.DEFAULT_NAME; } else { return name; } } }
bringing the Tamagotchi to life
to simulate the Tamagotchi
's life, we need to:
Tamagotchi
a maximum amount of caloriesmaximum calories
every Tamagotchi
object has the same maximum number of calories
hence, implement as a class property
private static var MAX_CALORIES:Number = 2000;
calorie consumption rate
every Tamagotchi
consumes calories at the same rate: 100 per second
store the calorie consumption rate in a class property:
private static var CALORIES_PER_SECOND:Number = 100;
alive or dead?
at any point in time, we need a quick way to check if the Tamagotchi is alive
hence, create a Boolean property to store life/death status:
private var isAlive:Boolean;
Tamagotchi birth
when a new Tamagotchi
object is created, the constructor starts its life-cycle
previously, the constructor looked like this:
public function Tamagotchi (petName) { setName(petName); }
now the constructor also:
Tamagotchi
a full stomachcurrentCalories = Tamagotchi.MAX_CALORIES;
isAlive
to true
(the Tamagotchi
is not dead)isAlive = true;
Tamagotchi
to digest once per seconddigestIntervalID = setInterval(this, "digest", 1000);
you stop digesting when you die
this code calls digest()
every 1000 milliseconds:
setInterval(this, "digest", 1000);
setInterval()
returns an ID number used to stop calling the method later
we store the interval ID for the digest()
method call in a new property:
private var digestIntervalID:Number;
when the Tamagotchi
dies, we'll use digestIntervalID
to stop calling digest()
updated Tamagotchi constructor
here's the full constructor code listing so far:
public function Tamagotchi (petName) { setName(petName); currentCalories = Tamagotchi.MAX_CALORIES; isAlive = true; digestIntervalID = setInterval(this, "digest", 1000); }
displaying health status
the displayHealthStatus()
method outputs the Tamagotchi
's current health
for now, our Tamagotchi
displays its health status in the Flash Output window
private function displayHealthStatus () { var caloriePercentage:Number = Math.floor((currentCalories /Tamagotchi.MAX_CALORIES)*100); trace(getName() + " has " + currentCalories + " calories" + " (" + caloriePercentage + "% of its food) remaining."); }
remember, display is an implementation detail! we'll add graphics later
more important to get the logic working
Tamagotchi death
when a Tamagotchi
dies, we call the die()
method
die()
has three reponsibilities:
clearInterval(digestIntervalID); isAlive = false;
Key.removeListener(this);
Tamagotchi
's deathtrace(getName() + " has died.");
die() code listing
here's the full code listing for die()
:
private function die () { clearInterval(digestIntervalID); isAlive = false; Key.removeListener(this); trace(getName() + " has died."); }
consuming calories
every second, the digest()
method is called
digest()
reduces the current calories
if the calories run out, digest()
calls die()
otherwise, digest()
requests a display update
private function digest () { trace("The Tamagotchi is digesting..."); currentCalories -= Tamagotchi.CALORIES_PER_SECOND; if (currentCalories <= 0) { die(); } else { displayHealthStatus(); } }
the code so far
our Tamagotchi now consumes calories and can die
here's the code for the Tamagotchi
class so far:
class Tamagotchi { private var currentCalories:Number; private var name:String; private var isAlive:Boolean; private var digestIntervalID:Number; private static var MAX_NAME_LENGTH:Number = 20; private static var DEFAULT_NAME:String = "Unnamed Pet"; private static var MAX_CALORIES:Number = 2000; private static var CALORIES_PER_SECOND:Number = 100; public function Tamagotchi (petName) { setName(petName); currentCalories = Tamagotchi.MAX_CALORIES; isAlive = true; digestIntervalID = setInterval(this, "digest", 1000); } private function digest () { trace("The Tamagotchi is digesting..."); currentCalories -= Tamagotchi.CALORIES_PER_SECOND; if (currentCalories <= 0) { die(); } else { displayHealthStatus(); } } public function feed (foodItem) { trace("You fed the Tamagotchi."); } private function displayHealthStatus () { var caloriePercentage:Number = Math.floor((currentCalories /Tamagotchi.MAX_CALORIES)*100); trace(getName() + " has " + currentCalories + " calories" + " (" + caloriePercentage + "% of its food) remaining."); } private function die () { clearInterval(digestIntervalID); isAlive = false; Key.removeListener(this); trace(getName() + " has died."); } public function setName (newName) { if (newName.length > Tamagotchi.MAX_NAME_LENGTH) { trace("Warning: specified name is too long."); newName = newName.substr(0, Tamagotchi.MAX_NAME_LENGTH); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; } public function getName () { if (name == null) { return Tamagotchi.DEFAULT_NAME; } else { return name; } } }
Tamagotchi food: sushi and apples
our Tamagotchi
eats two types of food: sushi and apples
each type of food will be represented by a class: Sushi
and Apple
factoring out the similarities
our Sushi
and Apple
class are nearly identical
each class must manage:
Sushi
and Apple
are similar because both are types of food
inheritance
in OOP we model "types of things" using inheritance
inheritance is used to establish hierarchical class relationships
in inheritance, one class (the subclass) adopts (inherits) the features of another class (the superclass)
many subclasses can inherit the features of a single superclass
for example, we'll create a general class called Food
Sushi
and Apple
will both inherit the features of Food
the Food class (the superclass)
here's the source code for Food
:
class Food { private var calories; private var name; public function Food (initialCalories:Number) { setCalories(initialCalories); } public function getCalories () { return calories; } public function setCalories (newCalories) { calories = newCalories; } public function getName () { return name; } public function setName (newName) { name = newName; } }
the Sushi class (a subclass)
we use the keyword extends
to make one class inherit from another class
class Sushi extends Food { }
now Sushi
inherits the methods and properties of Food
Sushi
also adds one property of its own, indicating its default calorie value
class Sushi extends Food { private static var DEFAULT_CALORIES = 500; }
calling the superclass constructor
every subclass must invoke its superclass's constructor function
within the subclass constructor, use super()
to invoke the superclass constructor
Sushi
provides the Food
constructor with a specific calorie value (500)
Sushi
's constructor also sets the name of the food
class Sushi extends Food { private static var DEFAULT_CALORIES = 500; public function Sushi () { super(Sushi.DEFAULT_CALORIES); setName("Sushi"); } }
the Apple class
Apple
is identical to Sushi
, but has its own calorie value and name
class Apple extends Food { private static var DEFAULT_CALORIES = 100; public function Apple () { super(Apple.DEFAULT_CALORIES); setName("Apple"); } }
both Apple
and Sushi
borrow the methods and properties of Food
hence, if the Food
class changes, Apple
and Sushi
automatically change too
method return types
as with properties, datatypes can be declared for method return values
for example, our getName()
method can indicate that it returns a String
value as follows:
public function getName ():String { // ...code not shown }
a method that does not return a value has a return type of "Void" (capital "V"!)
private function displayHealthStatus ():Void { // ...code not shown }
why declare a method return type?
benefits of declaring a method's return type:
public function getName ():String { return calorieCount; // Recall that calorieCount is a number. }
**Error** Line 89: The expression returned must match the function's return type.
var petCalories:Number = pet.getName();
**Error** Line 2: Type mismatch in assignment statement: found String where Number is required.
the programmer probably wants getCalories()
not getName()
(assuming getCalories()
exists)
method parameter types
datatypes can also be declared for method parameters
public function setName (newName:String):Void { // ...code not shown }
benefit of declaring parameter types:
// ERROR! setName() requires a String, not a Number pet.setName(25);
// ERROR! setName() requires a String, not a Name object pet.setName(new Name("yamada"));
what can you feed a Tamagotchi?
earlier we defined a basic feed() method:
public function feed (foodItem) { trace("You fed the Tamagotchi."); }
now we can limit what a Tamagotchi
can eat by setting foodItem
's datatype
Tamagotchi
s can only eat instances of Food
or of Food
subclasses
public function feed (foodItem:Food) { trace("You fed the Tamagotchi."); }
adding calories at feeding time
now that our food classes are ready, we can give the Tamagotchi
calories when feed()
is called
as a precaution, we also prevent dead Tamagotchi
s from being fed
public function feed (foodItem:Food):Void { if (isAlive == false) { trace(getName() + " is dead. You can't feed it."); return; } trace(getName() + " ate the " + foodItem.getName() + " (" + foodItem.getCalories() + " calories)."); if (foodItem.getCalories() + currentCalories > Tamagotchi.MAX_CALORIES) { currentCalories = Tamagotchi.MAX_CALORIES; } else { currentCalories += foodItem.getCalories(); } }
the code so far
here are the classes in our Tamagotchi application, complete with method parameter and return types:
class Tamagotchi { private var currentCalories:Number; private var name:String; private var isAlive:Boolean; private var digestIntervalID:Number; private static var MAX_NAME_LENGTH:Number = 20; private static var DEFAULT_NAME:String = "Unnamed Pet"; private static var MAX_CALORIES:Number = 2000; private static var CALORIES_PER_SECOND:Number = 100; public function Tamagotchi (petName:String) { setName(petName); currentCalories = Tamagotchi.MAX_CALORIES; isAlive = true; digestIntervalID = setInterval(this, "digest", 1000); } private function digest ():Void { trace("The Tamagotchi is digesting..."); currentCalories -= Tamagotchi.CALORIES_PER_SECOND; if (currentCalories <= 0) { die(); } else { displayHealthStatus(); } } public function feed (foodItem:Food):Void { if (isAlive == false) { trace(getName() + " is dead. You can't feed it."); return; } trace(getName() + " ate the " + foodItem.getName() + " (" + foodItem.getCalories() + " calories)."); if (foodItem.getCalories() + currentCalories > Tamagotchi.MAX_CALORIES) { currentCalories = Tamagotchi.MAX_CALORIES; } else { currentCalories += foodItem.getCalories(); } } private function displayHealthStatus ():Void { var caloriePercentage:Number = Math.floor((currentCalories /Tamagotchi.MAX_CALORIES)*100); trace(getName() + " has " + currentCalories + " calories" + " (" + caloriePercentage + "% of its food) remaining."); } private function die ():Void { clearInterval(digestIntervalID); isAlive = false; Key.removeListener(this); trace(getName() + " has died."); } public function setName (newName:String):Void { if (newName.length > Tamagotchi.MAX_NAME_LENGTH) { trace("Warning: specified name is too long."); newName = newName.substr(0, Tamagotchi.MAX_NAME_LENGTH); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; } public function getName ():String { if (name == null) { return Tamagotchi.DEFAULT_NAME; } else { return name; } } }
class Food { private var calories:Number; private var name:String; public function Food (initialCalories:Number) { setCalories(initialCalories); } public function getCalories ():Number { return calories; } public function setCalories (newCalories:Number):Void { calories = newCalories; } public function getName ():String { return name; } public function setName (newName:String):Void { name = newName; } }
class Sushi extends Food { private static var DEFAULT_CALORIES:Number = 500; public function Sushi () { super(Sushi.DEFAULT_CALORIES); setName("Sushi"); } }
class Apple extends Food { private static var DEFAULT_CALORIES:Number = 100; public function Apple () { super(Apple.DEFAULT_CALORIES); setName("Apple"); } }
handling user input
our Tamagotchi
now works, but needs code to handle user input
for simplicity, we'll make the Tamagotchi
keyboard controlled
the 'a' key will feed the Tamagotchi
an apple
the 's' key will feed the Tamagotchi
sushi
responding to the keyboard
a new method, onKeyDown()
, will handle keyboard events
private function onKeyDown ():Void { if (Key.getCode() == 65) { feed(new Apple()); } else if (Key.getCode() == 83) { feed(new Sushi()); } }
registering for keyboard events
in the constructor, we'll tell our Tamagotchi
to listen for keystrokes:
Key.addListener(this);
"this" is the current Tamagotchi
object
so the constructor now looks like this:
public function Tamagotchi (petName:String) { setName(petName); currentCalories = Tamagotchi.MAX_CALORIES; isAlive = true; digestIntervalID = setInterval(this, "digest", 1000); Key.addListener(this); }
adding graphical display
now that the Tamagotchi
's logic is fully functional, we can add graphics
to display the Tamagotchi
graphically we must:
setName()
, die()
, and displayHealthStatus()
methodsTamagotchiSymbol movie clip
movie clip that contains Tamagotchi
graphics is called TamagotchiSymbol
TamagotchiSymbol
is exported as "TamagotchiSymbol" so it can be attached by the Tamagotchi
class (via attachMovie()
)
TamagotchiSymbol
contains *no code*!, only graphics
the contents of TamagotchiSymbol
TamagotchiSymbol
contains a text field for the Tamagotchi
's name
TamagotchiSymbol
contains one nested clips, hungerIcon
labeled frames on the timeline provide different graphical states
TamagotchiSymbol
has two states: "alive" and "dead"
hungerIcon
has three states: "full", "hungry", and "starving"
movie clip-management properties
two new properties in the Tamagotchi
class
clip
stores an instance of the TamagotchiSymbol
movie clip
private var clip:MovieClip;
SYMBOL_ID
stores the identifier of the TamagotchiSymbol
movie clip
private static var SYMBOL_ID:String = "TamagotchiSymbol";
SYMBOL_ID
is the same for all Tamagotchi
instances, so it's a class property
TamagotchiSymbol constructor updates
the constructor now creates and stores an instance of TamagotchiSymbol
the constructor's parameters tell it where to put the instance in the Flash movie
public function Tamagotchi (petName:String, target:MovieClip, depth:Number, x:Number, y:Number) { clip = target.attachMovie(Tamagotchi.SYMBOL_ID, "tamagotchi" + depth, depth); clip._x = x; clip._y = y; setName(petName); currentCalories = Tamagotchi.MAX_CALORIES; isAlive = true; digestIntervalID = setInterval(this, "digest", 1000); Key.addListener(this); }
creating the TamagotchiSymbol instance
when creating a new Tamagotchi, we must now specify the parent, depth, and position for the Tamagotchi in the movie:
var pet:Tamagotchi = new Tamagotchi("Yamada", this, 0, 100, 100);
displaying the Tamagotchi's name
the setName()
method now must display the Tamagotchi
's name in TamagotchiSymbol
:
public function setName (newName:String):Void { if (newName.length > Tamagotchi.MAX_NAME_LENGTH) { trace("Warning: specified name is too long."); newName = newName.substr(0, Tamagotchi.MAX_NAME_LENGTH); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; clip.name_txt.text = name; }
notice the flexibility of the accessor method
details of name-setting operation can change internally without affecting external use
displaying health icons
update displayHealthStatus()
so it displays health state visually via hungerIcon
and exhaustionIcon
private function displayHealthStatus ():Void { var caloriePercentage:Number = Math.floor((currentCalories /Tamagotchi.MAX_CALORIES)*100); trace(getName() + " has " + currentCalories + " calories" + " (" + caloriePercentage + "% of its food) remaining."); if (caloriePercentage < 20) { clip.hungerIcon.gotoAndStop("starving"); } else if (caloriePercentage < 50) { clip.hungerIcon.gotoAndStop("hungry"); } else { clip.hungerIcon.gotoAndStop("full"); } }
display system is encapsulated
display system can easily change without affecting external use of Tamagotchi
objects
displaying Tamagotchi death
update die()
method so it displays death visually
private function die ():Void { clearInterval(digestIntervalID); isAlive = false; Key.removeListener(this); trace(getName() + " has died."); clip.gotoAndStop("dead"); }
code complete
here's the final Tamagotchi
class code
class Tamagotchi { private var currentCalories:Number; private var name:String; private var isAlive:Boolean; private var digestIntervalID:Number; private var clip:MovieClip; private static var MAX_NAME_LENGTH:Number = 20; private static var DEFAULT_NAME:String = "Unnamed Pet"; private static var MAX_CALORIES:Number = 2000; private static var CALORIES_PER_SECOND:Number = 100; private static var SYMBOL_ID:String = "TamagotchiSymbol"; public function Tamagotchi (petName:String, target:MovieClip, depth:Number, x:Number, y:Number) { clip = target.attachMovie(Tamagotchi.SYMBOL_ID, "tamagotchi" + depth, depth); clip._x = x; clip._y = y; setName(petName); currentCalories = Tamagotchi.MAX_CALORIES; isAlive = true; digestIntervalID = setInterval(this, "digest", 1000); Key.addListener(this); } public function feed (foodItem:Food):Void { if (isAlive == false) { trace(getName() + " is dead. You can't feed it."); return; } trace(getName() + " ate the " + foodItem.getName() + " (" + foodItem.getCalories() + " calories)."); if (foodItem.getCalories() + currentCalories > Tamagotchi.MAX_CALORIES) { currentCalories = Tamagotchi.MAX_CALORIES; } else { currentCalories += foodItem.getCalories(); } } public function setName (newName:String):Void { if (newName.length > Tamagotchi.MAX_NAME_LENGTH) { trace("Warning: specified name is too long."); newName = newName.substr(0, Tamagotchi.MAX_NAME_LENGTH); } else if (newName == "") { trace("Warning: specified name is too short."); return; } name = newName; clip.name_txt.text = name; } public function getName ():String { if (name == null) { return Tamagotchi.DEFAULT_NAME; } else { return name; } } private function die ():Void { clearInterval(digestIntervalID); isAlive = false; Key.removeListener(this); trace(getName() + " has died."); clip.gotoAndStop("dead"); } private function displayHealthStatus ():Void { var caloriePercentage:Number = Math.floor((currentCalories /Tamagotchi.MAX_CALORIES)*100); trace(getName() + " has " + currentCalories + " calories" + " (" + caloriePercentage + "% of its food) remaining."); if (caloriePercentage < 20) { clip.hungerIcon.gotoAndStop("starving"); } else if (caloriePercentage < 50) { clip.hungerIcon.gotoAndStop("hungry"); } else { clip.hungerIcon.gotoAndStop("full"); } } private function digest ():Void { trace("The Tamagotchi is digesting..."); currentCalories -= Tamagotchi.CALORIES_PER_SECOND; if (currentCalories <= 0) { die(); } else { displayHealthStatus(); } } private function onKeyDown ():Void { if (Key.getCode() == 65) { feed(new Apple()); } else if (Key.getCode() == 83) { feed(new Sushi()); } } }
known limitations
same keyboard inputs will affect multiple Tamagotchi
objects
in a multi-Tamagotchi application, need to add input focus support for interaction with a single Tamagotchi at a time
Tamagotchi
class's use of TamagotchiSymbol
movie clip could be better encapsulated
no way to "restart" a dead Tamagotchi
no way to reposition a Tamagotchi
after creating it
no mouse control (e.g., can't click hunger icon to feed Tamagotchi)