Simulations in breve can be written using a language called "steve". steve is a simple language which aims to allow rapid construction of advanced simulations while avoiding a great deal of the programming overhead associated with constructing simulations in other languages.
Writing simulations in steve is ultimately quite simple. Due to the number of features provided by the simulation engine, however, this documentation may seem at times overwhelming. Don't panic—make liberal use of the demo simulations provided with the environment and don't be put off if some of the features discussed here are difficult to understand.
"steve" is an object-oriented language. This means that programming in "steve" involves working with components called objects which contain data (variables) and behaviors (methods). In breve, objects can be either real, meaning they have a presence in the simulated world; or abstract meaning that they are used to store data or to perform computations, but do not appear in the simulated world. In order to define agents and their behaviors, one needs to construct these objects and behaviors using the "steve" language. The language is described in this chapter.
First, the classic "Hello, World" program is constructed in steve (the section called “Hello, World”). Then, a simple multiagent 3D simulation written in steve is presented (the section called “A Simple Example”). This sample should give the basic idea of what a breve simulation looks like.
As discussed above, all agents in the simulated world are represented by programming objects. These objects are defined in terms of object templates called classes. The most important object, called the Controller is described in the section The Controller Object (the section called “The Controller Object”). The section Building Classes (the section called “Building Classes”) describes how to construct basic classes in steve.
In order to define the data and behaviors of your classes, you'll need to be familiar with types (the section called “Types in "steve"”) and expressions (the section called “Expressions”). The section Program Control Structures (the section called “Program Control Structures”) discusses loops and conditional statements.
Finally, after learning how to program with steve, you'll want to explore the Chapter 17 to learn how to use the features of the breve engine in your simulations.
The traditional introduction to any programming language is a program which repeatedly prints out the text "Hello, World!". Here it is written in steve:
@include "Control.tz" Controller HelloWorld. Control : HelloWorld { + to iterate: print "Hello, world!". }
The specifics will be discussed in more detail through the rest of this
chapter. Briefly, we first declare the simulation's controller object
to be a class called HelloWorld
(line 3).
Then we construct the HelloWorld
class
(lines 5-8), and as part of that class, we define a method called
iterate
(line 6) which will print out the
text "Hello, world!". The "include" line (line 1) simply tells breve to
load in a file called "Control.tz", which is included with the breve
distribution. This built-in class file contains the class control,
which is the parent class for our
"HelloWorld" controller.
When this simulation is run, the controller object is created, and the
method named iterate
gets run
automatically at every step of the simulation. Thus, "Hello, World!".
Ad nauseum.
"Hello, World" is a very simple example which can be used for any programming language. It does not, however, give any insight into how 3D multiagent simulations are written in steve. This section shows the code for a simple 3D multiagent simulation.
In the RandomWalker demo, you can see a simple simulation containing hundreds of independent agents.
The simulation below is very heavily commented—all of the lines beginning with the # character are descriptive comments which do not effect the execution of the simulation, but clarify for the user what the simulation is doing. Comments are described in more detail below.
The simple simulation below features a number of agents which perform a "random walk" through 3D space. That is to say that at each time-step, the agents will move in a random direction.
# # RandomWalker is a simulation in which small spheres do # a random walk. This is a very simple simulation which # can be used as a skeleton for more complex simulations. # # include some required breve class files @use Control. @use Mobile. # First tell the breve engine the name of our controller class Controller myControl. # Subclass "Control" and define our "myControl" object. Control : myControl { # Here we define the class variables. + variables: walkerShape (object). # Next we define a method called init. The init method is called # automatically when our class is created. Since this is the controller # class, an instance gets created when we launch the simulation, # so this is the entry point for our simulation. + to init: print "Setting up the simulation.". self point-camera at (0, 0, 0) from (0, 60, 0). # set up a shape that all of the RandomWalker objects will use. walkerShape = (new Sphere init-with radius 1). # Create a bunch of RandomWalkers. You can create as few or # as many as you want... 200 new RandomWalker. # The get-walker-shape is a method that allows other objects # to look at the walkerShape variable. we do this so that # each RandomWalker object can reuse the same Shape object. # This is not strictly required--each RandomWalker could create # it's own copy of the same Shape, but fewer objects means # less memory used and more efficient simulations, so it's # a good programming practice. + to get-walker-shape: return walkerShape. } # The "RandomWalker" object is the physical object in the simulation # that does the random walk. It is a subclass of "Mobile". Mobile : RandomWalker { + to init: # During init, the object asks the controller for the shape # it should use. It then sets itself to a random color. self set-shape to (controller get-walker-shape). self set-color to random[(1.0, 1.0, 1.0)]. + to iterate: # Set a new random velocity at every timestep. self set-velocity to random[(60, 60, 60)] - (30, 30, 30). }
When writing code in any language, it is typically useful to include comments which clarify what the code is doing. In steve, there are two ways to include comments in code. A "single line" comment starts with a "#" is and continues to the end of the line; "multiline comments" are written starting with "#!" and ending with "!#". Some example of both kinds of comments are shown below:
# this is a simple one-line comment. print "this is not a comment...". # but this is. #! this is a multiline comment !#
The
controller object is the foundation of a
breve simulation. The controller
object is a single instance which acts as the "boss" for the
simulation. When the simulation starts, it is the controller object
which is responsible for creating and setting up the rest of the
simulation. The controller object is the
only object which is automatically instantiated when the simulation
begins. If you're used to programming in C or C++, the
controller is like the main
function.
The first step in writing a breve simulation is to create a controller class which will setup and manage the simulation. The controller class you create must be a subclass of the class Control or one of its subclasses. Details on how to do this is described in the rest of this chapter.
As shown in the previous section (the section called “A Simple Example”), the controller object is specified at the top of your simulation file using the word controller:
Controller myControllerClass
.
This tells breve to create an
instance of myControllerClass
when the simulation begins. This controller object will create and
setup the rest of the objects in the simulation. The controller object
does so by implementing the simulation setup code in the init
method. Details on constructing classes and
implementing an init
method are described
in more detail in the rest of this chapter.
In addition to setting up the simulation, the controller object is also used as a liaison between simulation objects, the breve engine and the user. While other instances may come and go, the controller object is forever. The controller class contains a great number of methods for controlling the behavior of simulations, the appearance of the graphical display and interactions with the user interface. See the class documentation on Control for more information. As with some of the other topics covered in this section, the significance of these concepts will become clearer in the rest of the documentation.
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. These data can be used both to store information about the state of an 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 methods are defined.
Two special methods are critical for an agent's behavior: one that gets
called automatically when an agent is created, init
, and another that is run automatically at every
step of a 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.
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
after the class
name.
alias_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's behavior by adding in methods and variables.
An instance variable is a variable associated with a class. Each instance of the class will have its own private copies of the class's 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 afterwards it 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
, ...,variableN
(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). }
The most elementary method call in steve is a call to a method that takes no arguments. The definition of such a method is simple. Inside the definition of instanceName, we create a line:
+ to methodName
:
The statements that 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 a method that 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
, such that a method which
takes one variable could be defined by the following line:
keyword
, variable_name
, (type
)
+ 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 effect 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 that uses local variables:
+ to find-closest-creature in creatureList (list): item (object). closestItem (object). distance (float). # we start with an 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.
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 simply 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.
Method definitions may also specify optional arguments. Optional arguments are arguments that are given default values, and therefore do not need to be provided when calling the method. The use of 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 =
after the argument name.
For example, a variable called value
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".
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 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.
An int
is a whole number, identical to the
int
type in C.
ints can be operated on using the following mathematical operators,
which operate just like their C counterparts: +, -, *, /, and %.
Additionally, the ^ operator can be used to raise an integer value to
an integer power. Divisions or mods by zero are treated as errors and
will stop the execution of a simulation. The absolute value of an
integer is given by | integer
expression
|.
A real number, also
known as "double", identical to the double
type in C. Internally, floats are represented
by 8-byte doubles.
An
object
is an instance of a steve class.
objects
cannot be used in mathematical
expressions as some of the other types can. Instead, objects
are mainly used for one type of expression:
method calling. Method calling is outlined in the section Method Calls
(the section called “Method Calls”).
Objects cannot be explicitly converted to other types. They do,
however, have meaning as "boolean" (true or false) expressions in
control structures (the section called “Program Control Structures”). If used in the
context of a boolean expression (like an if
statement, the section called “The if Statement”), the expression is true only
if the variable refers to an active object. This means that an object
variable which has not yet been associated with an instance in the
simulation is false.
A vector is used to represent a point or vector in 3D space. A vector is expressed as three floating point numbers, such as (1.0, 2.0, 5.0).
Vectors may be added (+) to and subtracted (-) from other vectors,
which yields another vector. Vectors may be multiplied (*) or divided
(/) by ints or floats, which also results in other vectors. The length
of the vector is given using the following construct: | vector expression
|.
Some examples of using vectors follow:
v = (1, 2, 3). # sets the vector to a constant vector. v = v * 4. # multiplies the vector by 4 v = v / |v|. # normalizes the vector by dividing it by it's own # length such that the new length is 1.
Individual vector components can be extracted or set using the "::" notation:
xValue = myVector::x. myVector::y = 100.
Starting in breve 2.5, the standard list index syntax may also be used with indices 0, 1, and 2:
xValue = myVector{ 0 }. myVector{ 1 } = 100.
Vectors cannot be converted to any other types. They do, however, have
meaning as "boolean" (true or false) expressions in control structures
(the section called “Program Control Structures”). If used in the
context of a boolean expression (like an if
statement, the section called “The if Statement”), the vector will be true if
it's length is greater than 0, and false if its length is equal to 0.
This is to say that only the vector (0, 0, 0) is false in a boolean
context.
A matrix in steve refers to a 3x3 matrix which describes a transformation in 3D space. Using transformation matrices is somewhat advanced and they are not generally used in most simulations. Still, they may be useful when dealing with physical simulations.
Matrices may be written in steve as three comma-separated vectors, enclosed in braces ('[' and ']'), as in this example:
# the matrix m will be initialized to: # # [ 1 2 3 ] # [ 4 5 6 ] # [ 7 8 9 ] m = [ (1, 2, 3), (4, 5, 6), (7, 8, 9) ].
Matrix components can be extracted using the list index syntax. Each row of the matrix is a vector:
myVector = myMatrix{ 0 }. myMatrix{ 1 } = ( 1, 2, 3 ).
Individual numbers may be extracted from matrices just as they are from two-dimensional lists:
myNumber = myMatrix{ 0 }{ 0 }. myMatrix{ 1 }{ 1 } = 100.
Matrices may be multiplied (*), divided (/), added (+) to and subtracted (-) from other matrices. The results of these operations are other matrices.
Matrices may be multiplied (*) or divided (/) by scalars (ints and doubles). The result of these operations are other matrices.
Matrices may be used to transform vectors by multiplying the matrix times the vector (*). The result of this operation is a vector.
Matrices cannot be converted to any other types. They do, however, have
meaning as "boolean" (true or false) expressions in control structures
(the section called “Program Control Structures”). If used in the
context of a boolean expression (like an if
statement, the section called “The if Statement”), the expression will be true
if there are any non-zero values in the matrix. This means that a
matrix of all zeros will be false, while all other matrices are true.
The
list
datatype allows you to keep a list
of other variables. lists can contain any datatype, including other
lists. lists can even contain multiple datatypes simultaneously.
lists
are formed using the syntax
{
.
item1
, item2
, ...
}
Some simple examples of constructing lists
are shown below:
myList = { 1, 2, 3.0 }. # a list of numbers (both int and float) myList = { "a", "b", "c" }. # a list of strings myList = { "a", 30, new Mobile }. # a list of mixed types myList = { 1, "a", { "dog", "cow" } }. # a list with a nested list
An important feature of lists in steve is that they are always passed by reference and are not copied. This means that if you pass a list to a function, then any modifications done to the list inside the function will modify the original list.
The following operations can be used with list
expressions:
insert
: inserts
expression
, at list
, { index
}expression
at the
specified index in the list, shifting up other list elements with
higher indices
remove
: removes the
element of list
, { index
}list
at the
specified index and returns it, shifting down other list elements
with higher indices
push
: appends expression
, onto list
expression
onto the end of list
pop
: removes the last
element of list
list
and
returns it
prepend
: prepends expression
, onto list
expression
onto the start of
list
unprepend
: removes the first
element of list
list
and
returns it
: returns the
element of the list at offset expression. The expression index is
zero based, as in C, such that 0 refers to the first element, 1
to the second, and so forth. If the offset expression is less
than zero, or greater than the length of the list minus one
(because the access is zero based), an error is triggered and the
simulation is stopped.
list
, { expression
}
: sets an element of
the list at offset expression to value. The offset index is again
zero based. If the offset expression is less than zero or greater
than the size of the list an error is triggered and the
simulation is stopped. If the offset expression is equal to the
size of the list, the list is extended by one element; the
operation has the same effect as pushing a value on to the end.
list
, { expression
, } = value
sort
: sorts
list
, with method-name
list
using the method
specified with method-name
. method-name
must be a method
which takes two list elements (the keywords are unimportant) and
compares them, returning a negative number if the first list
element belongs before the second in the sorted list, a positive
number if the second belongs before the first, and 0 if the two
entries are equal. In most cases, this confusing sounding method
returns a certain value associated with one argument minus the
same value in the other.
Unlike the perl sort operator, sort
operates on the list it is given and does not return a copy of
it. This means that the original list is modified during the sort
operation.
copylist
: copies the entire
list. Normally, assigning a list to a variable will not copy the list but instead will
yield two variables pointing to the same list.
list
|
: gives the length
of a list. Lists are automatically converted to integers when use
in mathematical expressions, but this construct can be used too
force the conversion.
list
|
lists
can be converted to ints
simply by using them in the context of integers.
They can thus also be used as floats
. In
the event that the context of the expression does not force the list to
become an integer, you can force it yourself by using it in a
mathematical context:
myInt = (myList + 0).
The string type holds a character string. A string is written in code (as in C) as a quoted string. For example, the built-in print operator is capable of printing strings:
print "this is a string".
The following operations can be done on strings:
: returns, as a
string, the letter at the specified index.
string
{ index
}
: replaces the
letter at the specified location with the string string
{ index
} = otherString
otherString
.
|
: returns the
length of the string.
string
|
In addition to the operators listed above, strings can be formatted using embedded variables. Variables can be embedded in strings so that the strings are interpreted dynamically, as in Perl. To indicate a variable embedded in a string, use the "$" character. The following, for example, will use the variable "self" in order to build a string to be printed:
print "my value is $self.".
This technique can be used to performing string concatenations as well:
$longstring = "$string1$string2".
strings can be converted to ints
and
floats
, but not to any other types.
In the context of an int or a float, the string is translated into the
appropriate type by converting the numerical component of the string.
For more information on this conversion, consult the man pages for the
ANSI C atoi()
(for ints
) or atof()
(for
doubles
) functions.
A hash is a type which works like a dictionary: it allows expressions ("values") to be stored and looked-up using other expressions as the "keys". The following example shows a steve hash being used like a dictionary:
dictionary (hash). dictionary{ "dog" } = "a four-legged pet". dictionary{ "fish" } = "a zero-legged pet". # this will print the definition we stored above. print "the definition of dog is: ", dictionary{ "dog" }.
As shown in this example, we're able to store data using strings as the keys. When data is later retrieved from the hash table using the same key, the value stored previously is returned.
hashes are not limited to using strings as keys. They can use any valid type. The most useful application is a hash table which uses objects as keys. In this way, relationships between objects can be stored in a hash. Consider an object that wants to keep track of neighbors it has encountered previously:
# let's say that this method gets called when a neighbor is seen... # if the neighbor has already been seen, print a message — otherwise, # just add it to the hash for next time! + to meet with neighbor (object): if seenHash{ neighbor }: print "I've seen this neighbor before!" else seenHash{ neighbor } = 1.
The example above also shows that the syntax of hashes is the same as the syntax for lists when storing or retrieving data. Unlike lists, however, hashes do not hold ordered data, and none of the other list operators work with hashes.
When using hashes with keys that are ints, floats, vectors, matrices or strings, steve will test the equivalence of the keyed data when looking up or storing the data. This means that two equivalent strings, even if they are stored in different variables, will refer to the same value in the hash. For the other types (objects, pointers, data, lists and hashes themselves), the hash will only return the same value for the same exact variable key. This means that two different lists, even if they contain "equal" data, will access different values in the hash.
An important feature of hashes in steve is that they are always passed by reference and are not copied. This means that if you pass a hash to a function, then any modifications done to the hash inside the function will modify the original hash.
A list of the keys in a hash may be retrieved using the built-in
function keys
. This key list is often
used to iterate through the items in a hash:
foreach key in keys( myHash ): { print myHash{ key }. }
The
pointer
type is for use by breve
and plugin developers only.
pointer variables store C-style pointers to internal data. They are not used for writing simulations in steve and are only used by breve developers and plugin authors.
pointer variables are only useful in the context of interacting with internal C-style function calls: they do not contain methods, variables or any other meaning in the context of most steve code. Like objects, however, pointers can be tested to see if they are NULL (0), but cannot be used in mathematical expressions. That is to say that pointers have a meaning in a boolean context of control structures such as if and while (as well as the logical operators && and ||).
As in C, copying a pointer (assigning it to another variable) will not copy the data it points to.
The data
type is for use by breve
and plugin developers only.
data
variables are similar to pointer variables in
that they contain a reference to internal data. Also like pointer
variables, they are not to be used in regular simulation
code—they are only to be used by breve developers and custom
plugins.
The difference between data
and
pointer
is that data
refers to a linear block of internal data of
known size. This means that data
variables can be successfully archived, while pointers cannot.
The only use for data
variables is archiving and dearchiving internal
data. For more information on using the data
type with plugins, see the section on Archiving
Plugin Data With The data
Type (the section called “Archiving Plugin Data With The data Type”).
Certain variables have special meanings in steve. Their values are managed automatically by the breve engine and should not be assigned manually.
self (object)
.
This variable always contains the value of the instantiation in which the current method is being run. The variable is used most frequently to call other methods within the same instantiation. For example, an object of class Mobile could move itself with the call:
self move to (10, 10, 10).
super (object)
.
This special value refers to the parent, or super-instance. This is used to invoke a parent class implementation of a method, instead of the current class implementation.
For example, in the Controller's iterate method, the superclass iterate method is often called: super iterate. This is because custom subclasses of Control typically preform simulation specific tasks, but must then call on the superclass implementation (Control) to actually step the physical simulation forward and update the state of the world. Anytime you wish to implement a custom object behavior in addition a parent class behavior, you should invoke the parent class method as well.
controller (object)
.
The controller variable is defined for all instances. It always refers to the simulation's controller instance.
The most simple expressions are simple assignments of literal values to variables. It may help to refer to the documentation on steve types before continuing.
Below are a few examples of this type of expression. In each case, the variable on the left will take the value of the expression on the right. If the expression on the right is not the correct type, it will be automatically converted if possible. If the conversion is not possible, an error will occur.
myInt = 4. myDouble = 12.345. myString = "Hello!". # If we assign a double to an int, it will be automatically converted by # truncating the decimal portion. In this example, myInt will take the value # 4: myInt = 4.8. # Similarly, if we assign a string to an int or a double, the variable will # take the numeric value of the string, in accordance with the atoi() or # atof() ANSI C function. Here the double will get the value 10000: myDouble = "10000 miles away.".
Mathematical operators in steve function almost exactly the same as they do in C, although there are some additions to account for vector and matrix types.
The following binary operators are valid for int
and double
types
(the descriptions refer to x and y, as though they were used in an
expression: x
): operator
y
+
, addition
-
, subtraction
*
, multiplication
/
, division
%
, modulus (the remainder of x when
divided by y)
^, power (x raised to the power of y)
Their functions should be self-explanatory, with the possible exception
of modulus, which cannot be used with double types in C. When used with
doubles, the result is calculated using the ANSI C fmod()
function.
The following operators are valid for use with two vectors:
+
, vector addition
-
, vector subtraction
The following operators are valid for a vector and a double (although
an int
is automatically promoted to a
double in this case):
*
, vector multiplied by scalar
/
, vector divided by scalar
As in many languages, parentheses are used to group expressions when the default order of operations does not compute the desired result:
# this is the default order, but we can make it explicit with parens x = x + (4 / y). # computes 4 / y first... # this is NOT the default order -- we need to force it x = (x + 4) / y. # computes x + 4 first
Mathematical expressions are often used in conjunction with assignments in order to modify the value of a variable. A few examples of using mathematical expressions in conjunction with assignments follow:
x = x + 4. y = (1, 2, 3) / x. # assumes x is an int or double z = z + x^2.
In addition to the mathematical assignment operators above, steve also support mathematical assignment operators. Mathematical assignment operators are used as shortcuts to perform a calculation and update the value of a variable simultaneously, instead of as two separate steps. These expressions are useful, but since they are only shortcuts for other expressions, understanding them is not critical.
The following mathematical assignment operators are available:
+=
-=
*=
/=
%=
^=
These operators are simply shortcuts for cases in which the left operand of the mathematical expression is also the location where the output of the expression will be stored. For example, the following expression pairs are equivalent:
a = a + b. # "a equals a plus b" can also be written as... a += b. # "a plus equals b" a = a - (2, 2, 2). a -= (2, 2, 2).
steve also has "increment" and "decrement" assignment operators:
++
--
As in languages like C and Perl, these operators increment and decrement a variable by one, respectively. Unlike C and Perl, these operators may only be placed after the variable. As an example, the following expression pairs are equivalent:
x = x + 1. # updates the variable x by adding 1 x++. x += 1. # does the same... x++. y = (x += 1). # a little confusing, but sets both x and y to (x + 1) y = x++. # as does this.
A number of internal functions (which are otherwise
typically not used in breve simulations) are available for math-related
expressions. Internal functions are called just like C functions:
.
functionName
(arguments
)
sin(
gives the sine of
the radian angle input
)input
.
cos(
gives the cosine of
the radian angle input
)input
.
tan(
gives the tangent
of the radian angle input
)input
.
asin(
gives the radian
angle arc sine of input
)input
.
acos(
gives the radian
angle arc cosine of the input
)input
.
atan(input)
gives the radian angle
arc tangent of input
.
sqrt(
gives the
input
)float
square root of input
.
angle(
gives the a
, b
)float
angle in radians between vectors
a
and b
.
max(
gives the maximum of
a
, b
)floats
a
and b
.
min(
gives the minimum of
a
, b
)floats
a
and b
.
cross(
gives the vector cross
product of vectors v1
, v2
)v1
and v2
.
dot(
gives the float dot
product of vectors v1
, v2
)v1
and v2
.
log(
gives the
input
)float
natural log of input
.
randomGauss()
gives a float
random number with a Gaussian
distribution.
transpose(
gives the transpose
of the matrix input
)input
.
The following internal functions are used for testing float variables for special values which have meaning primarily to developers and plugin authors.
isnan(
returns 1 if the
input is a "not-a-number" float value, 0 otherwise.
input
)
isinf(
returns 1 if the
input is a float value representing infinity, 0 otherwise.
input
)
Random
numbers are available in steve using the command random
. The syntax is random[
, where
expression is an expression of either expression
]int
(the section called “The int type”),
float
(the section called “The float type”) or vector
(the section called “The vector Type”). The value returned is always
the same type as the expression. In the case of int
or float
, the
returned value is a random value between 0 and the expression. In the
case of a vector
expression, the returned
value is a vector
in which each element
is between 0 and the corresponding value of the expression. For
example, a call to random[(10, 10, 20)]
returns a vector
with X and Y elements
between 0 and 10, and the Z element between 0 and 20.
Note that random[
returns a value
between 0 and intValue
]intValue
,
inclusive, as opposed to the behavior that many C programmers expect in
which the returned value is between 0 and intValue
- 1.
Because many simulations use the origin (0, 0, 0) as the "center" of
their world, it is often useful to obtain a random vector centered
around (0, 0, 0). For example, if we want to move agents somewhere
within an imaginary box surrounding the origin, we might use the
expression random[(40, 40, 40)] - (20, 20,
20)
. This convention gives us a vector
with each element between -20 and 20. This
type of expression appears frequently in simulations.
The values are produced using the standard C library rand()
routine. The library is seeded with the
current time when the breve application is launched. To explicitly set
the random seed, you may call the internal function randomSeed(
, where value is an integer.
value
)
In the event that multiple breve simulations are launched simultaneously (typically only relevant in cluster environments), it may be necessary to pick unique random seeds for each such simulation to prevent them from producing the exact same results. Refer to the Controller method set-random-seed-from-dev-random for more information on automatically picking unique random seeds on systems that support it.
As discussed in the section on defining class methods, each method is identified by a name and may have any number of input arguments. The most simple method call to a method with no arguments is simply:
instance
methodName
.
If the method takes arguments, each argument is associated with a keyword: the keyword identifies which argument will follow and what it's type is. This is somewhat different from C where the argument's type and meaning is specified by its order relative to other arguments. The steve method is more similar to Objective C and allows a more natural language structure to method calls and protects against arguments being passed in the wrong order.
To pass arguments to the method, simply precede the argument value with the keyword name. Consider a method move-object which takes a keyword to:
myObject move-object to (1, 2, 3).
If the method takes more than a single argument, the convention is the same—just add the next argument afterwards. Note that the order in which the arguments are passed does not affect the method call, though it may affect the readability of the code. For example, the Control object implements a method to point the camera at a certain vector location from a vector offset—first we'll see the method definition, then how it's called:
# if the method is defined using: + to point-camera at location (vector) from offset (vector): ... # then from another method, we can call point-camera using the code below. # these two method calls are equivalent, though the first reads more # naturally. + to do-something-else: self point-camera at (0, 0, 0) from (100, 100, 100). self point-camera from (100, 100, 100) at (0, 0, 0).
If you wish to call a method for multiple objects, you can use the method call syntax with a list of objects. Note, however, that the arguments to the list are computed separately for each item in the list. This makes a difference in the following example:
# assume that mobileList is of type list mobileList = 10 new Mobile. # The random statement is evaluated for each instance, meaning that all the # instances go to different random locations, not to a single random location. mobileList move to random[(20, 20, 20)].
You can find all instances of a given class using the all
expression. all
is
given the name of a class, and returns a list containing all objects of
that type.
# get a list of all mobile objects in the simulation myMobile = all Mobiles.
The print
and printf
statements are used to print output from a simulation to the output
log. Both of these statements accept any type of expression, as well as
multiple expressions separated by commas. Also, since strings
may contain embedded variables, you can
format the output of variables however you'd like. See the section on
strings
(the section called “The string Type”) for more information.
The only difference between print
and
printf
is that printf
does not automatically print a newline character.
This means that subsequent prints will continue on the same line (as
though the "return" key was never hit). This can be useful if you're
trying to produce output in a specific format, but is typically not
desirable. If in doubt, stick to print
Here are some examples of print
and
printf
:
# print two variables, side by side. print (self get-time), populationSize. # use a variable embedded in a string. print "the population size is $populationSize". # the following statements would produce the text: # A B C # D E F print "A B C ". print "D E F". # the following statements would produce the text: # A B C D E F printf "A B C ". printf "D E F".
As in C, of course, users can use subexpressions as part of larger expressions. For example, you can use a mathematical expression as part of a method call, or a method call as part of a mathematical expression. Because of the syntax of steve , however, subexpressions frequently need to be parenthesized in situations where it would not be required in C. The following important rules apply to using subexpressions: If a method call is not the entire statement, it must be parenthesized. If you wish to assign the result of a method call, use it in a mathematical expression or use it as an argument for another method, for example:
myInt = self get-speed. # incorrect myInt = (self get-speed). # correct myInt = self get-speed + 5. # incorrect myInt = (self get-speed) + 5. # correct self set-speed to neighbor get-speed. # incorrect self set-speed to (neighbor get-speed). # correct
All method arguments must be a single "unit"—arguments which are not simply a variable or literal value must be parenthesized.
This means that if you use mathematical expressions, instantiations or other method calls as input arguments to a method, they must be parenthesized. The first rule about method calls, of course, still applies:
self set-location to ((neighbor get-location) + (10, 10, 10)). # correct self set-location to (neighbor get-location) + (10, 10, 10). # incorrect
Internal function calls are for use by breve and plugin developers only.
The final expression type is the internal function call. Internal function calls look just like C calls:
methodName
(arg1
,arg2
,... argN
)
Although internal function calls grant the most direct access to the breve libraries and features, the included class hierarchy provides a formal interface to the internal functions such that user simulations should never use these internal functions. The only exception to this is for certain mathematical functions.
Control structures effect the flow of simulation code. Though many of these structures function the same as their counterparts in C, the syntax is slightly different in each case. The main difference is that the statement being tested is not (necessarily) surrounded by parentheses, but is followed by a colon (':') character.
Control statements evaluate test statements and then execute code
according to the result. In the case of for
(the section called “The for Loop”), foreach
(the section called “The foreach Loop”) and while
(the section called “The for Loop”), these structures are used as
loops to repeat execution of a piece of code a certain number of times
or while a certain condition is met. The if
statement is used to execute a block of code if a
certain condition is true or, optionally, a different block of code if
the statement is false. A call to return
will exit any control structure block immediately, as well as the
method to which it belongs.
The conditional statements are comprised of C-style comparison operators. The following comparison operators are available:
==
, Equals
!=
, Not equals
>=
, Greater than or Equals
<=
, Less than or Equals
>
, Greater than
<
, Less than
&&
, And (short circuit
operator)
||
, Or (short circuit operator)
!
, Negation
For all of these structures, the code to be executed may be either a single statement, or several statements enclosed in braces ('{' and '}').
The if
statement is used to execute one piece of code if
a test statement is true, or (optionally) another if the statement is
false:
iftest_statement
:true_code
[ elsefalse_code
]
Examples of the if
statement are shown
below.
# here we execute a single statement if x > 5: x = 20. else x = 0. # here we execute multiple... if x > 5: { x = 20. y = 40. } # here we execute multiple statements in the if, but only one in the else... if x > 5: { x = 20. y = 40. } else x = 200.
The
while
control structure works just like
the while statement in C. While executes a block of code repeatedly, as
long as the test condition is true:
whiletest_condition
:code
An example of the while
loop is shown
below.
# for example... while x < 10: { print "x = $x". x++. }
The
foreach
control structure is similar to
the foreach loop in Perl. The loop iterates through a list, and
executes the associated code each time. The current item in the list is
stored in a temporary variable as supplied by the user:
foreachtemporary_variable
inlist_variable
:code
.
An example of the foreach
loop is shown
below.
# so, for example, if we have a variable called agent and a list # of objects stored in agentList: foreach agent in agentList: { print (agent get-location). }
The
for
loop (similar to the for
loop in C) repeatedly executes a block of code.
Though it can function more generally like the while
loop, it is typically used to run a block of
code for each value of a "counter" variable.
The loop is separated into three statements—an initializer, a test statement, and an increment statement.
forexpression
,test_expression
,increment_expression
:code
.
The initializer is executed once
when the loop starts. It is typically used to set the iteration
variable before proceeding. The test
statement is run at every iteration to determine whether
the loop will continue to execute (similar to the while
loop). Finally, the increment statement is run at every
iteration of the loop, typically to update a counter variable. Examples
of the for
loop are shown below.
# so, for example, if we have a variable called n (int), this loop will # print the numbers from 0 to 29. for n=0, n<30, n+=1: { print n. } # we can also use a different increment statement in order to run the # loop a bit differently—let's print only even numbers between 2 and 28 for n=2, n<=28, n+=2: { print n. }
breve includes built-in garbage collection (sometimes called GC). Garbage collection is a form of memory management in which the system detects when an object is no longer referenced by any other variable. When an object is no longer referenced by any other variable, it is an indication that the memory is no longer in use and can thus be safely deallocated.
Memory management and garbage collection of basic types happens automatically and requires no user interaction. ints, floats, matrices and vectors are passed by reference and do not require garbage collection. lists, hashes and data are automatically garbage collected when appropriate.
breve's garbage collection is slightly complicated by the fact that objects do not need to be referenced in memory to be "in use". An unreferened object may, for example, "come back to life" because of an all (the section called “Finding All Objects of a Given Type”) expression. Furthermore, objects in the simulated world (members of subclasses of the class "Real") may physically interact even without referencing each other in their variables. Because of these complications, garbage collection cannot be automatically enabled for all objects in a simulation.
Garbage collection for objects is thus enabled on a per-object basis and the programmer must decide when its use is appropriate. The following guidelines should generally help to decide when garbage collection is appropriate for an object:
the object is not a member of a subclass of Real.
the object does not have it's own iterate or post-iterate methods.
the object is not a dependency of any object that does not hold a reference to it.
One important caveat applies to garbage collection of both basic types and objects. The steve garbage collection scheme does not correctly deallocate memory when there are circular references. A circular reference occurs when two (or more) objects refer to each other in a circular fashion. An example of a circular reference between three objects is shown below:
When a circular reference occurs, the objects are never recognized as "unused" and are thus never deleted as they should be. Circular references thus lead to "islands" of unused memory which do not get released. This type of circular reference is rare, but if your simulation design makes use of these types of structures, you may have to explicitly overwrite variables in order to ensure that no circular references exist.
Before getting started with the code for a simulation or class, there are often a few special lines at the top of a steve file which begin with an @-character. These lines are called @-directives ("at directives") and they specify information that can help breve to locate classes or other files, or to define some constants that will be used in your simulation. These directives are described below.
breve includes a rich hierarchy of classes that are used to construct simulations. To use a class that comes with breve (or another file which you have created yourself), you must tell breve to load in its associated class file. You need to load a class before you instantiate it or subclass it.
Classes are loaded using the @include
directive. It is used simply by specifying the name of the file to
include:
@include "Control.tz".
@use
works the same way, but with a
slightly different syntax, leaving out the quotes and the ".tz" from
the file name:
@use Control.
There is no difference between @include
and @use
in terms of how files are
actually loaded.
@include
directives are not only used to
include classes that come with the breve distribution, but also
potentially classes that you construct yourself. They are often used in
conjunction with the @path
directive
(the section called “path: Specify a Search Path”) to specify the
location of classes before loading them.
Each @path
directive specifies a directory that breve
should search within to find files. These directives apply to finding
class files, image files, sound files, archives and any other type of
resource that breve might need to search for. They should go at the top
of source files, before any other files are included or used.
Here's an example @path
directive that
allows a folder containing class files to be searched:
@path "/Users/jk/breve_classes".
You may specify as many directories with @path
directives as necessary.
A global
constant lets you associate a name with a constant value in your
simulation. The @define
directive allows
you to associate names with ints
,
floats
and strings
.
These constants can be very useful when you have the same value used
several times in a simulation—then if you wanted to change the
value, instead of making the change several times, you would make it
once in the @define
directive. They can
also be useful for assigning meaningful symbols to numbers in your
simulation. For example, if your cellular automata is arbitrarily
chosen to be 50x50, then instead of hardcoding the number 50, it is more
flexible and more descriptive to use a global constant.
Global constants are defined with the following form:
@defineconstant-name
constant-value
.
Here are some examples:
@define CA_SIZE 50. @define PI_VALUE 3.14159. @define STRING_CONSTANT "Hello".
By setting these constants at the top of the source file, you can use them later on. For example:
+ to print-pi: print "pi is approximately: ", PI_VALUE.
It is not required, but, by convention, global constants are typically written with all capital letters, to distinguish them from variables.