How to create a Simulation
In this HowTo we will focus on the steps involved in creating a simulation. The truth is, we already have everything we need to know to create a simulation. We can build control aspects to implement the rules governing our simulation. We can implement agent aspects, which can interact with the world through perceptors and effectors. Now, all we have to do is bundle all this different things together into a simulation.
A simulation is comprised of two things. A bundle including all the custom elements making up the simulation (control aspects, agent aspects, custom perceptors and effectors) and a so-called assembly script, which builds the object hierarchy necessary to perform the simulation. As the first aspect, building a bundle has already been described in a previous HowTo, we will focus on the assembly script.
The assembly script is just like any other Ruby script. If we look at the Survival sample simulation, we see that a subdirectory 'survival' is created under the path of the simulator executable (app/simulator). It contains a script called 'survival.rb'. The name of the directory and the script MUST match (except for the rb-extension). The simulator is started with the command-line parameter 'survival', causing it to look for a survival.rb script in the survival subdirectory. We will now go through the survival.rb assembly script line by line:
# Import classes of this simulation description importBundle('survival/survival');
First, we begin by importing the bundle containing our custom classes. This call adds them to the class object framework of the object hierarchy, making the classes available for future calls.
# create the control aspect cd ('/usr/scene');
controlAspect = new ('SurvivalControlAspect', '_control');
Then we select the 'scene' object. The root of our hierarchical scene structure. It is always located at the path '/usr/scene'. There we create an instance of SurvivalControlAspect ... a class contained in the previously loaded bundle.
# create world and space aspects cd ('/usr/scene'); world = new ('kerosin/World', '_world'); world.setGravity(0.0, -9.81, 0.0); new ('kerosin/Space', '../_space');
Next, we create two important classes for the physics aspect: a world and a space. The world is where all dynamics objects (Bodies) live and the space is, where the geometry aspects can collide.
# setup camera trans = new ('kerosin/Transform', '../camera'); trans.setLocalPos(0.0, 0.0, 10.0); new ('kerosin/Camera', 'camera'); body = new ('kerosin/Body', '../_physics'); body.useGravity(false); new ('kerosin/FPSController', 'fps'); collider = new ('kerosin/SphereCollider', '../_geometry'); collider.setRadius(2.0); light = new ('kerosin/Light', '../../_light'); light.setRadius(50.0); light.setDiffuseColor(1.0, 1.0, 1.0);
The above calls add a camera object to the scene. This is necessary to be able to visualize the simulation. At first, we add a transform node. Under it we attach a kerosin::Camera object. It has a physics aspect (kerosin::Body), which in turn has an FPSController attached. This controller receives the key-presses and mouse movements and translates these to physical movements of the camera. Hence, the connection to the Body class. Our camera is also capable of colliding with other objects in the world, so we add a SphereCollider with radius 2. For some special FX we have also endowed the camera with a dynamic light.
# add arena cd ('/usr/scene'); trans = new ('kerosin/Transform', 'arena'); trans.setLocalPos(0.0, 0.0, 0.0); mesh = new ('kerosin/StaticMesh', '_visual'); mesh.load('model/arena.void'); # floor pc = new ('kerosin/PlaneCollider', '../pc'); pc.setParams(0.0, 1.0 ,0.0, 0.0); #lights trans = new ('kerosin/Transform', '../../light0'); trans.setLocalPos(0.0, 15.0, 0.0); light = new ('kerosin/Light', '_light'); light.setRadius(80.0); light.setDiffuseColor(1.0, 1.0, 1.0);
The above blocks load a very simple rectangular 'playing field' and add a planar collision geometry (geometry aspect), as well as another dynamic light.
# add slow
cd ('/usr/scene'); trans = new ('kerosin/Transform', 'slow'); trans.setLocalPos(-12.5, 1.0, -12.5); visual = new ('kerosin/StaticMesh', '_visual'); visual.load('model/slow.void'); physics = new ('kerosin/Body', '../_physics'); physics.setSphere(1.0, 1.0); physics.setMass(1.0); physics.setMaxSpeed(3.0); geometry = new ('kerosin/SphereCollider', '../_geometry'); geometry.setRadius(1.0); agent = new ('SurvivalAgentAspect', '../_agent');
Here we add the first agent to the world. We begin by giving
it a starting position (the transform node). Then we add the visual aspect (load
the slow.void mesh), physics aspect, geometry aspect and
agent aspect (SurvivalAgentAspect).
# add fast cd ('/usr/scene'); trans = new ('kerosin/Transform', 'fast'); trans.setLocalPos(12.5, 1.0, 12.5); visual = new ('kerosin/StaticMesh', '_visual'); visual.load('model/fast.void'); physics = new ('kerosin/Body', '../_physics'); physics.setSphere(1.0, 1.0); physics.setMass(1.0); physics.setMaxSpeed(10.0); geometry = new ('kerosin/SphereCollider', '../_geometry'); geometry.setRadius(1.0); agent = new ('SurvivalAgentAspect', '../_agent');
This adds the second agent to the world. Almost identical to
the second agent, except it has a higher maximum speed and a different visual
aspect.
# add food cd ('/usr/scene'); trans = new ('kerosin/Transform', 'food'); trans.setLocalPos(0.0, 1.0, 0.0); visual = new ('kerosin/StaticMesh', '_visual'); visual.load('model/food.void');
Last but not least, the 'food' object is added. As one can tell, it has only 'visual' character. The control aspect determines internally if one of the agents has reached the food (see SurvivalControlAspect).
As one can see, creating assembly scripts is straight-forward. One only has to keep track of what one is building up in the object hierarchy. After the script is executed, the simulation is run automatically.