To start the docker image environment, do
docker run -p80:80 -p8765:8765 -v $PWD:/source -it klavins/enviro:v1.1 bash
This will start a bash promt from which you can build new projects and run the enviro server.
To create a new project, use esm
as follows:
mkdir my_project
cd my_project
esm init
This will create the following directories and files
Makefile
config.json
defs/
README.md
lib/
README.md
src/
Makefile
Make the project and start the enviro sever as follows.
make
enviro
Then navigate to http://localhost
you should see a rectangular walled area.
You can press Ctrl-C
at the bash
interface to stop the enviro server.
To add a robot to your project, you have to create some new files and edit the config.json
file. First, do
esm generate MyRobot
This will create the files
defs/
my_robot.json // Defines the shape, mass, and other parameters of the robot
src/
my_robot.h // Contains classes inheriting from elma::Process that define the behavior
my_robot.cc // Contains the implementation of the classes in my_robot.h
Note that esm generate
only makes directional robots. For an omni-directional robot, you need to subsequently edit the defs/my_robot.json
file and change "shape" to "omni" and add a "radius" field (see below). ⑪ New in 1.1.
To compile the robot code, do
make
This will make the file lib/my_robot.so
, which is a shared object file containing the compiled code for your robot.
To place the robot into the simulation, change the agents
entry in config.json
to
"agents": [
{
"definition": "defs/my_robot.json",
"style": { "fill": "lightgreen", "stroke": "black" },
"position": {
"x": 0,
"y": 0,
"theta": 0
}
}
]
Now you should be able to run
enviro
and see a green square in the environment. That's your robot!
See the examples directory for more examples of agent definitions and controllers.
An agent, called MyRobot
for this example, consists of the following
defs/my_robot.json
: This file contains information about the agent's shape and physical properties such as its mass and friction coefficients.src/my_agent.h
: A header file containing at the very least a class declaration of a class called MyRobot
inheriting from enviro::Agent
and a call to the macro DECLARE_INTERFACE(MyRobot)
which allows enviro to use the class.src/my_agent.cc
: A source file with whatever code you want in it, usually the implementations of methods in the header file.lib.my_agent.so
: The shared object library for the agent. This is made when you run make
from the main directory of your project. You can have any number of agents. Note that they will not appear in the simulation unless you add them in project configuration file, config.json
(described below).
The JSON files in the defs/
directory should contain an object with the following fields:
name
A string defining the name of the agent.
type
- "dynamic": the agent will move, have mass, etc.
- "static": the agent will not move but will still be something other agents will collide with (i.e. like a wall).
- "noninteractive": The agent will not by simulated with any physics. Other agents will pass through it. ⑮ New in 1.5.
- "invisible": The agent is noninteractive and invisible. It can be used to run background processes not associated with visible elements. ⑮ New in 1.5.
description
A string describing the agent.
shape
polygon shaped: A list of pairs of the form{ "x": 10, "y": 12 }
defining the vertices of a polygon. The physics engine will use this to determine the moment of initial and collision shape of the robot, and the user interface will use it to render the agent. All points are relative to the robot's center.
circular: The string "omni", which makes a circular omni-directional agent. If you choose this option, you also need to specify a "radius". ⑪ New in 1.1. Does not work with "noninteractive"
This field should not be present in "invisible" agents.
radius
The radius of a circular, omnidirectional robot. Only used when theshape
field is "omni". ⑪ New in 1.1. This field should be present in "invisible" agents.
friction
An object with three numerical fields,collision
,linear
, androtational
defining the fricition coefficients of the robot with other robots and with the environment. Note that the latter two coefficients are only used if you apply a control in yourupdate()
methods such asdamp_movement()
ortrack_velocity()
. This field should not be present in "invisible" agents.
sensors
A list of range sensor definitions of the form (for example):{ "type": "range", "location": { "x": 12, "y": 0 }, "direction": 0 }
The location field is relative to the robot's center. The direction is an angle in radians. This field should not be present in "invisible" agents. ⑮ New in 1.5.
mass
A number defining the mass of the robot. This field should not be present in "invisible" agents. ⑮ New in 1.5.
controller
A path to the shared object library for the agent, such aslib/my_robot.so
.
To define an agent, called MyRobot
for example, you need to have a header file with at least the follow in it:
#include "enviro.h"
using namespace enviro;
class MyRobot : public Agent {
public:
MyRobot(json spec, World& world) : Agent(spec, world) {}
};
DECLARE_INTERFACE(MyRobot)
This code declares the MyRobot
class, inherits from enviro::Agent
, declares the constructor, and calls macro DECLARE_INTERFACE
defined by enviro
that sets up the shared library interface. The constructor must have exactly the type signature shown above, and must call the Agent
constructor when it is initialized.
You can add any number elma
processes to and agent using the add_process()
method in the constructor. For example,
class MyRobot : public Agent {
public:
MyRobot(json spec, World& world) : Agent(spec, world) {
add_process(p1);
add_process(p2);
add_process(sm);
}
private:
MyProcess1 p1;
MyProcess2 p2;
MyStateMachine sm;
};
Here, MyProcess1
and MyProcess2
should be classes that you define that (multiple) inherit from elma::Process
and enviro::AgentInterface
. The class MyStateMachine
should inherit from elma::StateMachine
and enviro::AgentInterface
.
Processes and state machines that are added to agents are both elma processes and agent interfaces. To learn how to use elma processes, go here.
The agent interface class methods are described below. They are available in the init()
, start()
, update()
, and stop()
methods of your process, as well as the entry()
, during()
and exit()
methods of any states in your state machines.
cpVect position()
This method returns the position of the agent. ThecpVect
structure has fieldsx
andy
that can be treated asdoubles
.
cpVect velocity()
This method returns the velocity of the agent. ThecpVect
structure has fieldsx
andy
that can be treated asdoubles
.
x()
,y()
,vx()
,vy()
The horizontal and verical positions, and the horizonal and vertical velocities -- separated out so you do not need to about the structure. ⑪ New in 1.1.
double angle()
This method returns the angle of the agent in radians and can be treated as adouble
.
double angular_velocity()
This method returns the angular velocity of the agent in radians per second and can be treated as adouble
.
int id()
This method returns a unique id of the agent.
void apply_force(double thrust, double torque)
This methied applies a force specified by thethrust
argument in the direction the agent is currently facing, and applies a torque specified by thetorque
argument around the center of the agent. The agent's mass comes in to play here using Newton's laws of motion.
void track_velocity(double linear_velocity, double angular_velocity, double kL=10, double kR=10)
This method makes the agent attempty to track the given linear and angular velocities. If other elements are in the way, or if it is experience collisions, it may not be able to exactly track these values. The optional arguments are the proportional gains on the feedback controller that implements the tracking controller.
void damp_movement()
This method slows the agent down using the linear and angular friction coefficients defined in the agent's JSON definition file.
void move_toward(double x, double y, double vF=75, double vR=20)
This method attepts to move the agent to the given (x,y) location. If something in the way, the agent will not get there. The robot simultaneously attempts to rotate so that it is pointing toward the target and also moves forward, going faster as its angular error is reduced. The optional arguments are the gains on the rotational and forward motion.
void teleport(double x, double y, double theta)
This method instantaneously moves the agent to the given position and orientation.
void omni_apply_force(double fx, double fy)
This methied applies a force specified by the arguments. The agent's mass comes in to play here using Newton's laws of motion. ⑪ New in 1.1.
void omni_track_velocity(double vx, double vy, double k=10)
This method makes the agent attempty to track the given translational velocity. If other elements are in the way, or if it is experience collisions, it may not be able to exactly track these values. The optional argument is the proportional gain on the feedback controller that implements the tracking controller. ⑪ New in 1.1.
void omni_damp_movement()
This method slows the agent down using the linear and angular friction coefficients defined in the agent's JSON definition file. ⑪ New in 1.1.
void omni_move_toward(double x, double y, double v=1)
This method attepts to move the agent to the given (x,y) location. If something in the way, the agent will not get there. The robot simultaneously attempts to rotate so that it is pointing toward the target and also moves forward, going faster as its angular error is reduced. The optional argument is the desired velocity of rotation and forward motion. ⑪ New in 1.1.
double sensor_value(int index)
This method returns the value of the specificed index. It is the distance from the location of the sensor to the nearest object in the direction the sensor is pointing. The index refers to the position in the sensor list in the agent's JSON definition.
std::vector<double> sensor_values()
This method returns a list of all the sensor values, in the same order as the sensors appear in the agent's JSON definition.
std::string sensor_reflection_type(int index)
This method returns the name of object type the sensor of the specificed index is seeing. The index refers to the position in the sensor list in the agent's JSON definition. ⑬ New in 1.3.
std::vector<std::string> sensor_values()
This method returns a list of all the sensor reflection types, in the same order as the sensors appear in the agent's JSON definition. ⑬ New in 1.3.
void notice_collisions_with(const std::string agent_type, std::function<void(Event&)> handler)
Runs the handler function upon collisions with agents of the given agent type. Theagent_type
string is the name used indefs/*.json
files. These should usually be set up in theinit
function of a process, as follows:void init() { notice_collisions_with("Robot", [&](Event &e) { int other_robot_id = e.value()["id"]; Agent& other_robot = find_agent(other_robot_id); // etc. }); }
The value associated with the event
e
is a json object with a single key,id
, which is the id of the other agent. ⑫ New in 1.2.
void ignore_collisions_with(const std::string agent_type)
Stop noticing collisions with agents of the given type. ⑫ New in 1.2.
void attach_to(Agent &other_agent)
Create a constraint that attaches the calling agent to theother_agent
. For example, after the callAgent& other_robot = find_agent(other_robot_id); attach_to(other_robot);
the two agents center's will be constrained to remain at the same distance from each other. ⑫ New in 1.2.
void prevent_rotation()
Prevents the agent from rotating. Probably best to call in the init function. Good for platformer type movement. Technically sets the moment of inertia to infinity. ⑭ New in 1.4.
void allow_rotation()
Allows the agent to rotate, setting the moment of inertia to the default number (based on the mass and shape of the agent). ⑭ New in 1.4.
Agent& find_agent(int id)
Given an agent's id, returns a reference to the agent. Note: do not assign the return value of this function to a normal variable, because when it goes out of scope, the agent's destructor will be called. Instead, assign it to a reference, as inAgent& other_agent = find_agent(other_agent_id;
⑫ New in 1.2.
bool agent_exists(int id)
Returns true or false depending on whether an agent with the given id still exists (i.e. is being managed by enviro). Agents may cease to exist ifremove_agent()
is called. ⑫ New in 1.2.
void remove_agent(int id)
Removes the agent with the given id from the simulation. Also calls it's desctructor, so think of it as remove and delete. ⑫ New in 1.2.
Agent& add_agent(const std::string name, double x, double y, double theta, const json style)
Add's a new agent to the simulation with the given name at the given position and orientation. Note that an agent with this type should have been mentioned inconfig.json
so enviro knows about it. The style argument determines the agent's style, just as in the configuration file. Any valid svg styling will work. For example:Agent& v = add_agent("Block", 0, 0, 0, {{"fill": "bllue"},{"stroke": "black"}});
⑫ New in 1.2.
void set_client_id(std::string str)
Set a string id of the agent. ⑮ New in 1.5.
std::string get_client_id()
Retrieve the string id of the agent (whatever has been set byset_client_id
). ⑮ New in 1.5.
center(double x, double y)
Move the center of the view to the pointx,y
. To keep a particular agent in the center of the view, you can putcenter(x(), y());
in its
update()
method. Seeexamples/teleporter
for an example. ⑯ New in 1.6.
zoom(double z)
Change the zoom level in the viewer. THe default is 1.0. Seeexamples/teleporter
for an example. ⑯ New in 1.6.
void set_style(json style)
Change the agent's style, just as in the configuration file. Any valid svg styling will work. ⑫ New in 1.2.
void decorate(const std::string svg)
Add an aribtrary svg element to the agent's rendering method on the client side. The svg element will be transformed and rotated to the agent's coordinate system. For example, to place a red dot in the middle of the agent do:decorate("<circle x='-5' y='5' r='5' style='fill: red'></circle>");
To clear the decoration later, simple call
decorate("");
⑬ New in 1.3.
void label(const string str, double x, double y )
Add a textual label to the agent's icon. The x and y values are relative to the origin of the agent's coordinate system. ⑬ New in 1.3.
void clear_label()
Clear the label associated with the agent. ⑬ New in 1.3.
The file config.json
should have the following fields.
name
A string stating the name of your project. This will be displayed on the brower interface.
ip
The IP address of the server. Use0.0.0.0
for now.
port
The port for the server. Use8765
for now.
buttons
A list of buttons to show in the top right of the front end user interface. These should have the form{ "name": "name", "label": "Displayed Name", "style": { "background": "white", "border-color": "black" } }
The name field is used to catch button click events (see below). The label field defines the string displayed on the button. The style field is an
css
code you want to add to the stying of the button. ⑩ New in 1.0.
agents
A list of agents to put in the simulation. For example,{ "definition": "defs/my_robot.json", "style": { "fill": "lightgreen", "stroke": "black" }, "position": { "x": 100, "y": 0, "theta": 0 } }
The definition field should point to a shared object library. The style field can be any
svg
styling code. The position and orientation are the initial position and orientation of the agent.
references
A list of agents to that may be put into the simulation by theadd_agent()
method, but that are not listed in theagents
list. The format of each entry is the same as for theagents
list.
invisibles
A list of invisible agents (must have typeinvisible
in the correspondingdefs/*.json
file).{ "definition": "defs/my_invisible_process.json" }
statics
A list of static objects to place in the environment. For example,{ "style": { "fill": "gray", "stroke": "none" }, "shape": [ { "x": -350, "y": -200 }, { "x": -350, "y": 200 }, { "x": -340, "y": 200 }, { "x": -340, "y": -200 } ] }
The style field is any
svg
styling code, and the shape is a list of vertices of a polygon in world coordinates. The above example makes a ong, skinny rectangle for example.
The brower client relays mouse click, button press, and keyboard events to the enviro server, which your process can watch for. As of ⑮ Version 1.5 all events also contain a client specific unique ID that you can use to determine which client the event came from. The event types and associated information are as follows:
Name:
connection
Event: Sent when a new client attaches to the enviro server.
Value: An object with a string valuedclient_id
field that should be unique to the client that has connected. Note that this ID will be sent with all other events as well. ⑮ New in 1.5. Note: In v1.6 the key changed from "id" to "client_id" so it would not conflict with the "id" field in the "agent_click" event.Name:
screen_click
Event: Sent when a user clicks on the screen.
Value: An object withx
andy
fields holding the location of the click in world coordinates.Name:
agent_click
Event: Sent when a user clicks on an agent.
Value: An object withid
,x
andy
fields holding the agent's id and the location of the click in agent coordinates.Name:
button_click
Sent when a user clicks on a button. The buttons need to be defined inconfig.json
.
Value: An object with the aname
field that matches the name field used inconfig.json
. ⑩ New in 1.0.Name:
keydown
Sent when a user presses a key down.
Value: An object with akey
field, which is the character pressed, as well as the following boolean fieldsctrlKey shiftKey altKey metaKey
⑩ New in 1.0.
Name:
keyup
Sent when a user release a key.
Value: Same as forkeydown
. ⑩ New in 1.0.
To respond to events in your code, you should put elma watchers into the init()
method of some process. For example, to respond to an agent click, you might do
void init() {
watch("agent_click", (Event& e) => {
if ( e.value()["id"] == id() ) {
std::cout << "ouch!\n";
}
});
}
The user interface is written in Javascript using React. If you are using Chrome, you can install this plugin. Then you can open the Chrome developer tools, click the Components tab, and then click on Enviro
which will bring up the state on the right panel. Expand data
to see information about all the agents.
You can also see the client's ID by entering
CLIENT_ID
into the Javascript console. ⑮ New in 1.5.