Utrecht University Crowd Simulation API
Getting Started 1: Using the API - Console Demo

Table of Contents


This page explains how to use the UUCS API to run a small crowd simulation experiment.

This tutorial is based on the ConsoleDemo project included in the UUCSDemos solution.

Author
Angelos Kremyzas

To compile and run the demo described in this tutorial see Compiling the ConsoleDemo.

Console Demo

In this demo project, we will set up and run a small crowd simulation example. First, we will initialize the simulation by calling all necessary API functions in the proper order. After the simulation has been initialized, we will add some test characters to the virtual environment and set their destinations. We will then demonstrate how to progress the simulation while keeping track of the positions and orientations of the characters. For demonstration purposes, we will also give an example on how to retrieve and update the parameters of characters that already exist in the simulation.

Note that all API functions have STATUS_CODE as their return type. Currently, STATUS_CODE can either resolve to SC_OK or SC_ERROR. This allows us to check whether our API call has been successfully called and has completed its intended purpose. In the future, we plan to add more options to better inform any external program that uses this API about what went wrong.

Overview

The main function of the Console API Demo gives a quick overview of the steps required for running a crowd simulation.

int main()
{
STATUS_CODE status = SC_OK;
int vMajor, vMinor, vPatch;
status = UUCS_GetMajorVersion(vMajor);
if (status != SC_OK)
{
cout << "Error reading API major version" << endl;
return -1;
}
status = UUCS_GetPatchVersion(vPatch);
if (status != SC_OK)
{
cout << "Error reading API minor version" << endl;
return -1;
}
status = UUCS_GetMinorVersion(vMinor);
if (status != SC_OK)
{
cout << "Error reading API patch version" << endl;
return -1;
}
cout << "Utrecht University Crowd Simulation (UUCS) API v" << vMajor << "." << vMinor << "." << vPatch << endl;
if (!initializeSimulation()) {
cout << "Simulation could not be initialized!" << endl;
system("PAUSE");
return -1;
}
addTestCharacters();
exampleGetSetCharacterParameters();
initializeOutputFile();
simulationTime = 0;
while (simulationTime < sessionDuration)
{
updateSimulation();
outputStepInfo(simulationTime);
}
status = UUCS_Cleanup();
if (status != SC_OK)
{
cout << "Simulation cleanup failed." << endl;
return -1;
}
finalizeOutputFile();
cout << "Application is now closing..." << endl;
return 0;
}

In this function, we start by calling initializeSimulation() to communicate with the UUCS framework and initialize the crowd simulation (see Initializing the simulation). Next, we populate the simulation environment with some test characters using addTestCharacters() (see Adding characters to the simulation). We then call initializeOutputFile() to open an output file that will store simulation info. In this example, we choose to write to the output file the positions and orientations of all characters throughout the simulation. We then run the simulation for the specified duration by repeatedly calling updateSimulation() (see Running and updating the simulation). Every call to updateSimulation() progresses the simulation by one step and is followed by a call to outputStepInfo() for writing the information of the current simulation step to the output file. After the simulation has run for the desired duration, we clean up all objects managed by the API by calling UUCS_Cleanup(). Finally, we close the output file by calling finalizeOutputFile() and terminate the application.

Initializing the simulation

Our initialization function is the following:

bool initializeSimulation() {
// Create a new modelling environment.
if (UUCS_Modelling_CreateEnvironment("Environment") != SC_OK) return false;
std::cout << "Created a new modelling environment." << std::endl;
// Add a new layer to the environment.
LayerIndex layerIdx = -1;
if (UUCS_Modelling_AddLayer(layerIdx) != SC_OK) return false;
std::cout << "Added a new layer to the modelling environment with index: " << layerIdx << "." << std::endl;
// Add the walkable areas to the layer. Four planes will be used as the walkable area. The z-axis points 'upwards'.
VertexData3D walkableAreaData[4][4] = {
{
_constructVertexData3D(-40, 0, 0),
_constructVertexData3D(-30, 0, 0),
_constructVertexData3D(-30, 30, 0),
_constructVertexData3D(-40, 30 , 0)
},
{
_constructVertexData3D(-30, 20, 0),
_constructVertexData3D(10, 20, 0),
_constructVertexData3D(10, 30, 0),
_constructVertexData3D(-30, 30, 0)
},
{
_constructVertexData3D(0, 20, 0),
_constructVertexData3D(50, -30, 0),
_constructVertexData3D(60, -20, 0),
_constructVertexData3D(10, 30, 0)
},
{
_constructVertexData3D(-40, 0, 0),
_constructVertexData3D(50, -30, 0),
_constructVertexData3D(60, -20, 0),
_constructVertexData3D(-30, 10, 0)
}
};
// Register the walkable ares to the simulation.
for (int i = 0; i < 4; i++) {
PrimitiveIndex walkableAreaIdx = -1;
if (UUCS_Modelling_AddWalkableArea(layerIdx, walkableAreaData[i], 4, walkableAreaIdx) != SC_OK) return false;
}
std::cout << "Added the walkable areas to the simulation." << std::endl;
// Add an obstacle to the simulation such that one of the two paths to the point (5, -2) (starting from (-3.5; 2.5)) is blocked.
VertexData obstacleData[4] = {
_constructVertexData(0, 0),
_constructVertexData(20, -20),
_constructVertexData(30, -10),
_constructVertexData(0, 0)
};
PrimitiveIndex obstacleIdx = -1;
if (UUCS_Modelling_AddObstacle(layerIdx, obstacleData, 4, obstacleIdx) != SC_OK) return false;
std::cout << "Added an obstacle to the simulation." << std::endl;
// Load the simulation data from memory.
EnvironmentData *envData = (EnvironmentData*)malloc(sizeof(EnvironmentData));
if (UUCS_Modelling_SaveEnvironmentToMemory(envData) != SC_OK) return false;
if (UUCS_LoadEnvironmentFromMemory(envData) != SC_OK) return false;
std::cout << "Successfully turned the modelling environment into a simulation environment." << std::endl;
// Now that we finished modelling and made a simulation environment from the modelling environment, we compute the navigation mesh.
if (UUCS_ComputeNavigationMesh() != SC_OK) return false;
std::cout << "Navigationmesh computed successfully." << std::endl;
// Prepare the simulation.
if (UUCS_PrepareSimulation(timeStep, randomSeed) != SC_OK) return false;
std::cout << "Simulation successfully prepared!" << std::endl;
// load character profiles
const string filename_char = inputFilesPath + characterProfilesName;
cout << "Loading character profiles from " << filename_char.c_str() << "..." << endl;
if (UUCS_LoadCharacterProfiles(filename_char.c_str()) != SC_OK) return false;
cout << "Loading character profiles succeeded!" << endl;
// Simulation successfully initialized; return true.
return true;
}

The initializeSimulation() function is responsible for preparing the UUCS framework to run a simulation. It does so by modelling an environment. First of all, a modelling environment is created using the UUCS_Modelling_Environment() method. This method creates a new modelling environment and sets it as currently active. The next step is adding a layer to the environment. This is done by calling UUCS_Modelling_AddLayer(). This method adds a new layer to the currently active modelling environment. Next the walkable areas and obstacles have to be defined. First the walkable areas are defined and added to the modelling environment. This is done by passing simple geometry to the engine via the UUCS_Modelling_AddWalkableArea() method. The walkable area used for this simulation consists of four rectangular planes (eight triangles). The planes are defined in the walkableAreaData array and are then passed to the engine. Next the same is done for obstacles. One of the four planes to the goal position (see Adding characters to the simulation), is blocked off by the created obstacle. Obstacles are also defined as simple geometry and are passed to the engine using the UUCS_Modelling_AddObstacle() method. Now that the modelling environment is fully prepared, it has to be transformed into a simulation environment (which the engine can use for the actual simulation). This is achieved in two simple steps:

Next the navigationmesh is computed from the active simulation envrionment using UUCS_ComputeNavigationMesh(). Then, we can setup the simulation by calling UUCS_PrepareSimulation(). The next step is to call UUCS_LoadCharacterProfiles() and load a file that includes all available character profiles. A character profile includes all parameters that define the behavior of an individual character that falls under that profile (see Character profiles page).

Once an environment has been modelled and the navigationmesh is computed, these objects can be stored in a file. The benefit of this, is that an application does not have to go through the modelling and computation steps each time it is run, instead it can load the simulation environment and the navigationmesh from file.

Note that all methods modifying the modelling envrionment are of the form UUCS_Modelling_*. Methods without this prefix alter the simulation environment.

Adding characters to the simulation

The addTestCharacters() function illustrates an example of how to add on the fly characters to the simulation:

void addTestCharacters()
{
STATUS_CODE status = SC_OK;
struct CharacterSettings
{
int id, groupId, startLayer, goalLayer;
float startX, startY, goalX, goalY, goalRadius;
string profile;
CharacterSettings(int _id, int _groupId, int _startLayer, int _goalLayer,
float _startX, float _startY, float _goalX, float _goalY, float _goalRadius,
string _profile) :
id(_id), groupId(_groupId), startLayer(_startLayer), goalLayer(_goalLayer),
startX(_startX), startY(_startY), goalX(_goalX), goalY(_goalY), goalRadius(_goalRadius),
profile(_profile) {};
};
CharacterSettings testCharacterSettings1(42, 0, 0, 0, 42, 42, -42, 42, 0.42f, "default");
status = UUCS_AddCharacter(testCharacterSettings1.startX, testCharacterSettings1.startY, testCharacterSettings1.startLayer,
testCharacterSettings1.id, testCharacterSettings1.profile.c_str());
if (status != SC_OK)
{
cout << "Character with ID " << testCharacterSettings1.id << " could not be added to the simulation." << endl;
return;
}
status = UUCS_AddCharacterToGroup(testCharacterSettings1.id, testCharacterSettings1.groupId);
if (status != SC_OK)
{
cout << "Character with ID " << testCharacterSettings1.id << " could not be added to the group with ID " << testCharacterSettings1.groupId << endl;
return;
}
status = UUCS_SetCharacterGoal(testCharacterSettings1.id, testCharacterSettings1.goalX, testCharacterSettings1.goalY,
testCharacterSettings1.goalLayer, testCharacterSettings1.goalRadius);
if (status != SC_OK)
cout << "Could not set goal to character with ID " << testCharacterSettings1.id << "." << endl;
CharacterSettings testCharacterSettings2(24, 0, 0, 0, 42.42f, 42.42f, -42.42f, -42.42f, 0.42f, "slow");
status = UUCS_AddCharacter(testCharacterSettings2.startX, testCharacterSettings2.startY, testCharacterSettings2.startLayer,
testCharacterSettings2.id, testCharacterSettings2.profile.c_str());
if (status != SC_OK)
cout << "Character with ID " << testCharacterSettings2.id << " could not be added to the simulation." << endl;
status = UUCS_AddCharacterToGroup(testCharacterSettings2.id, testCharacterSettings2.groupId);
if (status != SC_OK)
{
cout << "Character with ID " << testCharacterSettings2.id << " could not be added to the group with ID " << testCharacterSettings2.groupId << endl;
return;
}
status = UUCS_SetCharacterGoal(testCharacterSettings2.id, testCharacterSettings2.goalX, testCharacterSettings2.goalY,
testCharacterSettings2.goalLayer, testCharacterSettings2.goalRadius);
if (status != SC_OK)
cout << "Could not set goal to character with ID " << testCharacterSettings2.id << "." << endl;
}

In this example, we add two test characters in the simulation, insert both to the same group and assign a different goal to each one of them. First, we define a struct named CharacterSettings that stores all info that is necessary for adding a character to the simulation, inserting it to a group and assigning a goal to it. The required information includes the ID of the character and the group, the layer and position of the character's start and goal configurations, the name of the character profile and the radius of the disc that defines the goal area. Each character is added to the simulation by calling UUCS_AddCharacter(). Note that, if adding a character to the simulation is successfull, then the parameter corresponding to the character ID (i.e. testCharacterSettings1.id and testCharacterSettings2.id) now stores the ID that was assigned to the character by UUCS. Both characters are also added in the same group by calling UUCS_AddCharacterToGroup() using appropriate settings. For each character we add to the simulation, we also set a goal by calling UUCS_SetCharacterGoal() using the desired settings. Since both characters belong to the same group the second call of UUCS_SetCharacterGoal() also updates the goal of the first character and both of them move towards the common goal as a social group. This can be also verified by visualizing the output of the simulation found in "simulation_output.csv".

Running and updating the simulation

The updateSimulation() function is where the actual simulation takes place:

void updateSimulation()
{
STATUS_CODE status = SC_OK;
if (status != SC_OK)
cout << "Simulation step at " << simulationTime << "s could not be performed." << endl;
status = UUCS_GetCharacterStepData(characterStepData, numCharacters);
if (status != SC_OK)
cout << "Character step data could not be updated." << endl;
status = UUCS_GetSimulationTime(simulationTime);
if (status != SC_OK)
cout << "Simulation time could not be updated." << endl;
return;
}

This function is responsible for advancing the simulation by one step by calling UUCS_DoSimulationStep(). It also keeps track of the information of all characters in the simulation by calling UUCS_GetCharacterStepData() each time the simulation progresses. We keep the simulation time indicator up to date by calling UUCS_GetSimulationTime().

To keep track of the character information this demo application uses a global array of CharacterStepData objects:

...
CharacterStepData* characterStepData = nullptr;
int numCharacters = 0;
...

This array is managed by the DLL and not by the external program. Each successfull call to UUCS_GetCharacterStepData() updates the CharacterStepData array and its size indicator (i.e. characterStepData and numCharacters global variables respectively).

Getting and setting character parameters

The exampleGetSetCharacterParameters() function illustrates an example of how to retrieve and update some character parameters:

void exampleGetSetCharacterParameters()
{
STATUS_CODE status = SC_OK;
int characterID = 42;
CHARACTER_PARAMETER parameter = PATH_FOLLOWING_METHOD;
void* value;
status = UUCS_GetCharacterParameterAndType(characterID, parameter, type, value);
if (status == SC_OK)
{
assert(type == PT_PF_METHOD_TYPE);
PathFollowingMethodType pfType = *((PathFollowingMethodType*)value);
switch (pfType)
{
case PATH_FOLLOWING_IRM:
cout << "Character with ID " << characterID << " is using IRM as a path following method." << endl;
break;
case PATH_FOLLOWING_MIRAN:
cout << "Character with ID " << characterID << " is using MIRAN as a path following method." << endl;
break;
default:
break;
}
}
else
cout << "Could not read the path following method of character with ID " << characterID << "." << endl;
characterID = 24;
parameter = GENERAL_RADIUS;
double newRadius = 0.42;
value = &newRadius;
status = UUCS_SetCharacterParameter(characterID, parameter, value);
if (status == SC_OK)
cout << "Could not update the radius of character with ID " << characterID << "." << endl;
}

Reading the value of a character parameter is done by calling UUCS_GetCharacterParameterAndType(). In this example, we request the path following method of the character with ID 42. To do so, we use PATH_FOLLOWING_METHOD to specify the character parameter we request. After calling UUCS_GetCharacterParameterAndType() a void pointer called 'value' points to the value of the requested parameter in the memory. A PARAMETER_TYPE variable called 'type' also stores an indication of a storage type for the requested parameter. To read the parameter pointed by 'value' we need to cast the pointer to the a pointer of the correct storage type and subsequently dereference it.

On the contrary, when we update a character parameter using UUCS_SetCharacterParameter() we need to create a pointer to the new parameter value and cast this pointer to a void pointer.