Building Classes

All objects in the simulated world correspond to programming objects in steve. In order to define an agent in the simulated world, you'll start by constructing a programming object, or class. This class will serve as a template which defines the agent's behaviors. This section describes how to construct and use these classes.

The section Defining Classes (the section called “Defining Classes”) describes how to define an empty class.

All classes have two major components: methods, which define a class's behavior and variables which define the data that the class can hold. This data can be used both to store information about the state of the agent, or information required for computation. The section Defining Instance Variables (the section called “Defining Instance Variables”) details how variables can be added to objects, while the section Defining Class Methods (the section called “Defining Methods”) shows how method are defined.

Two special methods are critical for an agents behavior: one that gets called automatically when the agent is created, init, and another which is run automatically at every step of the simulation, iterate. These methods, and a few other special methods are discussed in the section Special Method Names (the section called “Special Method Names”)

Even after the class is defined, it will still not be present in the simulation. This is because a class is nothing more than a "template" for an agent. In order to bring agents into the simulation, you must use the template to create instances of the class. The section on Creating and Destroying Instances (the section called “Creating and Destroying Instances”) describes how instances of classes are created and destroyed.

Defining Classes

When building a class, you typically don't start from scratch—instead, you make the new class the child of an existing class. This is called creating a subclass. By subclassing a class, the new class will inherit all of the parent's methods and variables. This approach means that most of the difficult work will already be done by an existing breve class, and we can simply override and customize the behaviors that we need to.

For example, if we're building an object which will move through the 3D world, we'd like to have an object that understands the relationship between position, velocity and acceleration. Instead of implementing such a class ourselves, we can subclass Mobile.tz which is included with breve. Our custom subclass will contain the custom behaviors we desire, while the parent class takes care of the details.

When building a class, you must first decide the class name and its parent class. The parent class is the class from which the new class will inherit its behaviors. Classes which are to be used primarily for computation and do not require any special inherited behaviors, will typically use the generic root class Object. Classes which move around the world will inherit behaviors from Mobile, while immobile objects in the world will inherit behaviors from Stationary. A full list of classes is available in the appendix (???).

An empty class is simply defined by the following steve code:

parent_class_name : class_name {

}

Because we often deal with classes in their plural form (like when creating multiple instances of an object), it can be useful to give a class an alias which will allow us to refer to the class in its plural form. This is not required but may make code easier to read. This alias is defined by adding the text (aka alias_name) after the class name.

As an example of defining a class, both with and without an alias, consider a class called myMobile which will be a child of the class Mobile:

# first without the alias...

Mobile : myMobile {

}

# now with the alias...

Mobile : myMobile (aka myMobiles) {

}

This code above defines an empty class with no variables and no methods. This means that it will behave exactly as its parent class does. The next step is to customize the class' behavior by adding in methods and variables.

Defining Instance Variables

An instance variable is a variable associated with a class. Each instance of the class will have it's own private copies of the class' instance variables.

Once the empty class declaration has been written, variables can be added using the heading + variables, followed by the list of instance variables. Variables are listed in the format variable_name, (variable_type).

The variable name must start with a letter, but afterwords may contain any alphanumeric characters, as well as the characters _ and -.

Multiple variables of the same type can also be declared on the same line:

variable1, variable2, variable3, ... (variableType). 

Variable types are covered in detail in the section Types (the section called “Types in "steve"”).

As an example, we'll add some variables to the simple class we created in the previous section:

Mobile : myMobile {
        + variables:
                myInt, myOtherInt (int).
                myObject (object).
                myFloat (float).
}

Defining Methods

The most simple method call in steve is a call to a method which takes no arguments. The definition of such an method is simple. Inside the definition of instanceName, we create a line:

+ to methodName:

The statements which follow this line will be part of the newly defined method until either the end of the object definition, or until the next method definition.

To define an method which takes arguments we will need the keyword, variable name and type of each argument. The keyword identifies the variable when it is being called, while the variable name is how the variable will be referenced from within the method. Finally, the type is simply the type of variable that will be passed in. The layout of this information is keyword, variable_name, (type), such that a method which takes one variable could be defined by the following line:

+ to set-velocity to-value theValue (float):

If the method takes two variables, we add another keyword/name/type triplet:

+ to set-rotation of-joint theJoint (Object) to-value theValue (float):

The code associated with the second method would then use the variables theJoint and theValue: "of-joint" and "to-value" are not actual variables, but instead the keywords which indicate which variables follows.

The calling convention of these methods is simple. After the instance name and method name, we give a list of keywords and values to be passed in. The order of the keyword/value pairs does not affect how the code is executed, though it may effect the readability of the code. The following lines call the set-rotation method which we defined above:

# the following lines are equivalent

myObject set-rotation of-joint myJoint to-value 200.
myObject set-rotation to-value 200 of-joint myJoint.

Methods may also have local variables associated with them. These variable definitions look just like class variable definitions, except that they follow after the method definition line, and not after the variable definition line. Method variables are automatically initialized to zero every time the method is called. Variable declarations in a method must precede all statements in the method.

For example, here is a simple method which uses local variables:

+ to find-closest-creature in creatureList (list):
        item (object).
        closestItem (object).
        distance (float).

        # we start with a unreasonably high "closestDistance" so that 
        # we are sure to find something closer.

        closestDistance = 1000000.

        foreach item in creatureList: {
                distance = |(self get-location) - (item get-location)|.

                if distance < closestDistance: {
                        closestItem = item.
                        closestDistance = distance.
                }
        }

        return closestItem.
[Note] For developer use only

When examining the internal classes included with the breve distribution, you might notice some methods defined using a minus sign instead of a plus sign:

- to methodName:

This syntax simple means that the method should be treated as a non-public method and that the method should not be documented. Though these methods function as all other methods, their use in user simulations is discouraged.

Optional Arguments in Methods

Methods definitions may also specify optional arguments. Optional arguments are arguments which are given default values, and therefore do not need to be provided when calling the method. Optional arguments can greatly simplify writing code in steve.

To make an argument optional, you need to provide it with a default value. To do so, you'll need to modify the argument definition to include the text = value after the argument name. For example, a variable called theHeight with keyword with-height could be given a default value like this: with-height theHeight = 100 (int). Default values for optional arguments must be literal values (and not expressions or variables).

Below is an example of a method defined with a set of optional arguments.

        # Create a new agent, with some default values.

        + to create-new-agent with-color color = (1, 0, 0) (vector) 
                with-energy energy = 100 (int)
                with-radius radius = 10 (int) 
                with-name name = "agent" (string):

The method above could be called in a number of ways, optionally including or excluding each of the arguments:

        # no arguments 
        self create-new-agent.

        # some of the arguments 
        self create-new-agent with-energy 10 with-name "Becky".

        # all of the arguments
        self create-new-agent with-color (1, 1, 1) 
                with-energy 100
                with-radius 20
                with-name "Robert".

Special Method Names

Certain method names have special meaning in steve, in that they are called automatically by the simulation at special times. These methods, in particular init and iterate are critical, as they are the entry-point into how your agents are initialized and how they will behave. These special method names are outlined below:

  • init, if it exists, is called automatically when a class is instantiated. The method is called not only for the class being instantiated, but also for its superclass and all other ancestors up to the root object. Though you should implement an init method for your class which will set up the instance when the class is instantiated, the init method should never be called manually.

  • iterate, if it exists, is called automatically during every iteration of the breve engine. If your class must perform a task during each iteration, then you may choose to implement an iterate method. The order in which the objects in the simulation are iterated cannot be controlled—if you need to control the order in which actions are performed, consider using iterate in conjunction with the post-iterate method described below.

    Unlike the init and destroy methods, iterate is not automatically called for the superclasses of an instance. This means that your iterate method must call super iterate if you wish to incorporate the parent's iterate method. This is absolutely necessary for subclasses of Control.

  • post-iterate, if it exists, is called automatically during every iteration of the breve engine after the iterate methods for all objects have been called. It is occasionally desirable to perform an action during each iteration, which requires data produced during that very same iteration from other objects. If this action is to be performed in the iterate method, than object A cannot be certain that object B has been iterated yet (and vice-versa). To solve this problem, objects may implement a post-iterate method which is automatically called after all objects have been iterated. The PatchLife demo uses this technique.

  • destroy, if it exists, is called automatically when a class is being freed. However, unlike init, and like iterate, you must explicitly call the super class destroy method if you need it to be called as well. If your class needs to perform certain tasks before being destroyed, you should implement this method. Be warned the you need to be carefull not to free an object referenced in the base class if it is needed for the base class destroy method.

Creating and Destroying Instances

Creating a new instance of an object is called instantiating the object. Instantiating in steve is done using the new command. Instantiation creates a single new instance if no number is given, or as many objects as you want by preceding the command with a number. The syntax follows:

new object_type.
number new object_type.

If a single object is created, it is returned as an object type. If several are created, they are returned as a list. For example:

myBird (object).
myBugs (list).

myBird = new Bird.
myBugs = 100 new Bugs.

The method init is called automatically for a class and all of its superclasses when the class is instantiated.

Destroying instances is simply accomplished using the command free:

free instance.

free accepts both single instances and lists of instances.

If an instance frees itself, then execution of the code is stopped immediately, as though the free command was followed by a return.

When an object is freed, the destroy method is automatically called for the instance. Prior to version 1.9, destroy would automatically be called for all superclasses. This is no longer the case—you must call "super destroy" if you wish for the superclass destroy method to be run.