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)

download example files here

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:

  • the current time
  • a colour
  • or a real "thing"

  • a push button
  • a drop-down menu
  • a race car
  • 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:

  • natural to plan and conceptualize
  • reusable across projects (use one class in many programs)
  • easy to change
  • easy to expand upon
  • easy to test
  • stable and bug-free
  • easy to co-develop with two or more programmers
  • 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:

  • create one or more classes
  • make objects from those classes (i.e., "instantiate" objects)
  • tell the objects what to do
  • 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:

  • include it in a component
  • add the class file's containing folder to the classpath
  • 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:

  • makes a new Tamagotchi
  • stores it in pet
  • gives the pet some food
  • 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:

  • amount of hunger (current calories)
  • the Tamagotchi's name
  • here 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:

  • a different value than the property
  • a default value if the property is not set
  • this version of getName() returns the string "Unnamed Pet" for Tamagotchis 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:

  • the default size of a dialog box in an application
  • the maximum speed of a car in a racing game
  • 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:

  • their purpose is not self-evident
  • they are error prone because they are decentralized
  • 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:

  • give the Tamagotchi a maximum amount of calories
  • decrease calories over time
  • maximum 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:

  • gives the Tamagotchi a full stomach
  • currentCalories = Tamagotchi.MAX_CALORIES;

  • initializes isAlive to true (the Tamagotchi is not dead)
  • isAlive = true;

  • tells the Tamagotchi to digest once per second
  • digestIntervalID = 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:

  • set the state of the object to inactive
  • clearInterval(digestIntervalID); isAlive = false;

  • release any resources used by the class
  • Key.removeListener(this);

  • notify the user of the Tamagotchi's death
  • trace(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:

  • the food's name
  • the food's calorie value
  • 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:

  • warning from the compiler if a return statement is not present or returns the wrong type of value
  • 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.

  • warning from the compiler if the return value is assigned to the wrong type of variable
  • 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:

  • warning from the compiler if the wrong type of data is passed to the method
  • // 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

    Tamagotchis 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 Tamagotchis 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:

  • create a movie clip containing Tamagotchi graphics
  • add two new movie clip-related properties to our class
  • update our class's constructor, setName(), die(), and displayHealthStatus() methods
  • TamagotchiSymbol 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)