Writing and Modifying Delegates
Overview
Now you need to provide the code implementation for overriding the distance covered by the racers. Building a delegate module with the SysML v2 API has never been easier. The code required to get up is small and only needs to be setup once. The <BEE Install Directory>/samples/sysml2/tutorialTortoiseVsHare/part-2 contains the completed delegate module that you can reference at any time.
Prerequisites
Prerequisite | Description |
---|---|
Behavior Execution Engine Installation | You must have installed Behavior Execution Engine. |
Tutorial Project | You must start this section with the Behavior Execution Engine simulation project from the previous section. |
Instructions
Initial Delegate Module Setup
- Create a package called
com.agi.mbse.tutorial.tortoisevshare
package under thesrc
folder. - Create a Java Class with the name
TortoiseVsHareDelegateModuleProvider
. - Modify the class signature to implement the
com.agi.mbse.sysml2.spi.DelegateModuleProvider
interface. -
Create an override method for the interface’s
provideDelegateModule
method. The class should look like the following:public class TortoiseVsHareDelegateModuleProvider implements DelegateModuleProvider {
@Override
public DelegateModule provideDelegateModule() {
}
}The delegate module provider just does one thing. It provides the delegate module to Behavior Execution Engine when the system loads. To keep things concise, we are going to define the delegate module in the same file as the provider.
- Return a new DelegateProperty for the previously defined
provideDelegateModule
method. - Create an override method for the DelegateProperty interface’s
getIdentity
method. -
Return a new DelegateIdentity for the previously defined
getIdentity
method using the following constructor arguments:UUID.fromString("d547ca5c-3fb7-11f0-ac13-325096b39f47")
"TortoiseVsHare Part 2 Module"
"The delegate module for the Tortoise vs Hare part 2 sample."
The class should now look like the following:
public class TortoiseVsHareDelegateModuleProvider implements DelegateModuleProvider {
@Override
public DelegateModule provideDelegateModule() {
return new DelegateModule() {
@Override
public DelegateIdentity getIdentity() {
return new DelegateIdentity(
UUID.fromString("d547ca5c-3fb7-11f0-ac13-325096b39f47"),
"TortoiseVsHare Part 2 Module",
"The delegate module for the Tortoise vs Hare part 2 sample."
);
}
};
}
}
The DelegateIdentity is a class that represents the identifying information for a delegate module. The constructor we just used takes in three arguments: the UUID – universally unique identifier, the name of the module, and a short description of what the module does. Keep in mind this identifier should be a different generated UUID for every delegate module. No two modules should share the same UUID.
For the delegate module to compile, we need to add one more required method to the DelegateProperty.
- Create an override method for the interface’s
registerDelegate
method just below thegetIdentity
method.
At this point the class should look like the following:
public class TortoiseVsHareDelegateModuleProvider implements DelegateModuleProvider {
@Override
public DelegateModule provideDelegateModule() {
return new DelegateModule() {
@Override
public DelegateIdentity getIdentity() {
return new DelegateIdentity(
UUID.fromString("d547ca5c-3fb7-11f0-ac13-325096b39f47"),
"TortoiseVsHare Part 2 Module",
"The delegate module for the Tortoise vs Hare part 2 sample."
);
}
@Override
public void registerDelegate(CustomCodeRegistry codeRegistry) {
}
};
}
}
That’s all the code Behavior Execution Engine needs to load in the delegate module. The rest of the code will be using the new opt-in system where you can specify specific elements in the model that you want to delegate for.
Finally, for Behavior Execution Engine to see the delegate module, you need to make an addition to the
com.agi.mbse.sysml2.spi.DelegateModuleProvider
file.
- Go to the directory
DelegateSource/resources/META-INF/services
. - Open the file
com.agi.mbse.sysml2.spi.DelegateModuleProvider
. - Set the content to:
com.agi.mbse.tutorial.tortoisevshare.TortoiseVsHareDelegateModuleProvider
.
Now you are ready to build and test to see if Behavior Execution Engine can see your new delegate module.
- Run the
gradle install
task again. - Verify that the delegate module has been placed in the expected directory. By default, it should be <BEE Install Directory>/delegates.
- Create a new config file on your computer called
TortoiseVsHare Part 2.config
. -
Copy and paste the snippet below into the config file:
{
"configVersion" : "0.1.0",
"delegateModules" : [
{
"identifier" : "d547ca5c-3fb7-11f0-ac13-325096b39f47",
"name" : "TortoiseVsHare Part 2 Module"
}
],
"additionalDelegateModuleSearchDirectories" : [],
"simConfigs" : [
{
"name" : "Part-2 Delegate Version",
"startTime" : "2000-01-01T12:00:00.000000Z",
"stopTime" : "2000-01-02T12:00:00.000000Z",
"targetElement" : "TortoiseVsHare Part 2::infamousRaceCase"
}
]
}If you do not have
com.agi.mbse.delegateModuleDir
property set to the <BEE Install Directory>/delegates directory, then you'll need to add your custom location to theadditionalDelegateModuleSearchDirectories
list. - Open the
TortoiseVsHare Part 2
file for model execution. - Select the configuration file that you created in the previous steps.
- Execute the simulation.
Example Execution with Initial Module
If everything worked as expected, you should see a line that says,
Executing simulation with delegate modules: [TortoiseVsHare Part 2 Module]
.
This indicates that Behavior Execution Engine was able to find and load the delegate module.
You might have also noticed that there the simulation failed with an exception. Don’t worry, this is expected.
Stubbing out a Scalar Value Feature Accessor
Previously in this tutorial, we had you remove the feature value expression from the Racer
’s distanceCovered
attribute.
Feature value expressions for attribute are normally required for execution but in the case where you want to delegate for them,
only default values are allowed.
Since we don't want to use a fixed rate check-in system for keeping track of the distance covered by each racer, we are going to use delegate code to handle this.
-
Create a new java class file in your delegate module project called
DistanceCoveredAccessor
and have it implement theScalarValueFeatureAccessor
interface with the generic type set to ScalarQuantityValue. -
Create override methods for the two required methods on the
ScalarValueFeatureAccessor
interface:getCurrentValue
andsetCurrentValue
. It should look like the following:public class DistanceCoveredAccessor implements ScalarValueFeatureAccessor<ScalarQuantityValue> {
@Override
public List<ScalarQuantityValue> getCurrentValue(@NotNull InstanceContext context) {
}
@Override
public void setCurrentValue(@NotNull List<Object> value, @NotNull InstanceContext context) {
}
} -
3. Throw an IllegalStartException for the
setCurrentValue
method. In this delegate, we will not be allowing the sysml model to modify the value of thedistanceCovered
attribute. This means that any assignment actions in the model would be unexpected and should throw an error. It should look like the following:@Override
public void setCurrentValue(@NotNull List<Object> value, @NotNull InstanceContext context) {
throw new IllegalStateException("Setting is not allowed in this delegate.");
}
Before we get to determining what the current value of the racer’s distanceCovered
attribute actually is,
we need to come up with a system for keeping track of it.
Registering Delegation Data
In the system we are designing as a part of this tutorial, the racers will change their speeds in response to specific events. Recording a history of how fast the racer was going, how far the racer has traveled, and the time at which the racer’s speed changed will enable us to calculate where the racer actually is at any point in the race. Furthermore, we will be able to calculate at what time intervals the racer will be at specific points of interest in the race. To do this, let’s create a simple structure for keeping track of the racer’s movement history.
- Create a new java class file called
RacerData
. -
Set the content of the
RacerData
class to the following:public record RacerData(AbsoluteTime time, ScalarQuantityValue speed, ScalarQuantityValue distanceRan) {
}
This Java record class will act as our individual data points for keeping track of the state changes in the race. Now we will need something to keep track of those points to tie it together.
- Create a new java class file called
RacerDataHistory
. -
Set the contents of the
RacerDataHistory
class to the following:public class RacerDataHistory {
private final List<RacerData> mHistory = new ArrayList<>();
public RacerData getLastRecordedRacerData() {
if (!mHistory.isEmpty()) {
return mHistory.get(mHistory.size() - 1);
} else {
return null;
}
}
public void recordRacerData(AbsoluteTime time, ScalarQuantityValue speed, ScalarQuantityValue distanceRan) {
mHistory.add(new RacerData(time, speed, distanceRan));
}
}
Now we have class capable of recording when the racers speed changes. Behavior Execution Engine provides a way of registering a
copy of this class for every instance of an element. If we register this class for the Racer
part, then we will ensure that
every instance of Racer
(and by extension Tortoise
and Hare
)
will have a RacerDataHistory
instance created alongside.
- Open the
TortoiseVsHareDelegateModuleProvider
. -
Add the following line above the
provideDelegateModule
method:public static final String rootPackageName = "TortoiseVsHare Part 2";
If you gave the project a different name, change the string to match.
-
Add the following lines inside the
registerDelegate
method:codeRegistry.registerInstanceDataFor(rootPackageName + "::Racer", RacerDataHistory.class, context -> {
return new RacerDataHistory();
});
codeRegistry.delegateScalarValueForFeature(rootPackageName + "::Racer::distanceCovered", new DistanceCoveredAccessor());
The first call to the codeRegistry is how we ensure that the tortoise
and
hare
parts will have a RacerDataHistory
instance
that will be available for access later in our delegate code. The second call is telling Behavior Execution Engine that
instead of the default implementation, we want to delegate for the Racer::distanceCovered
attribute using the DistanceCoveredAccessor
class.
Working with units
This model uses ScalarQuantityValues to represent various attributes of the race and racers. To work with these, we need to keep in mind that there are two pieces that we need to work with – a number and a unit. Using Indriya, you have access to an abundance of standard units to use and perform simple unit conversions with to ensure all of our calculations are in the same unit of reference. See this page about working with units.
- Create a new java class file called
HelperCode
. -
Add the following static method inside the class for use across the delegate module:
public static double convertScalarQuantityUnitValue(ScalarQuantityValue value, Unit<?> unit) {
try {
UnitConverter converter = value.getRefUnit().getConverterToAny(unit);
return converter.convert(value.getValue()).doubleValue();
} catch (IncommensurableException e) {
throw new RuntimeException(e);
}
} - Open the
RacerDataHistory
class. -
Add the following method below the
recordRacerData
method:public ScalarQuantityValue calculateDistanceAtTime(AbsoluteTime time) {
RacerData lastRecordedRacerData = getLastRecordedRacerData();
double previousSpeedMetersPerSecond = HelperCode. convertScalarQuantityUnitValue(lastRecordedRacerData.speed(), Units.METRE_PER_SECOND);
double previousDistanceRanMeters = HelperCode. convertScalarQuantityUnitValue(lastRecordedRacerData.distanceRan(), Units.METRE);
long deltaTimeSeconds = Duration.between(lastRecordedRacerData.time().toJavaInstant(), time.toJavaInstant()).toSeconds();
double distanceMeters = previousDistanceRanMeters + previousSpeedMetersPerSecond * deltaTimeSeconds;
return new ScalarQuantityValue(distanceMeters, Units.METRE);
}
After making these changes, the delegate module has a reusable way of converting units and calculating where a racer
would be at a given future time. Since the accessor has the ability to call on the RacerDataHistory
,
we are now ready to integrate the two together.
- Open the
DistanceCoveredAccessor
class. -
Add the following lines to the
getCurrentValue
method:AbsoluteTime currentTime = context.getTimeService().getCurrentSimulationTime();
RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
return List.of(racerDataHistory.calculateDistanceAtTime(currentTime));
At this point the delegate module is in a compilable state again. Install the delegate module
and run the simulation again. You will see that there is now a new exception. The log will report that:
Exception encountered during implementation of delegate module.
, along with a null pointer exception because
lastRecordedRacerData
is null. This makes sense because we haven’t actually recorded any speeds yet.
Delegating for actions
In the TortoiseVsHare Part 2
model, we have effect actions on
transitions that don’t currently do anything other than show up
in the log when they occur. Let’s start by creating an organized file for the racer.
- Create a java class file called
RacerCode
. -
Create two static methods that take in an ActionContext as a parameter. One should be called
startRacing
(as it is meant to delegate for thestartRacing
action) and one forstopRacing
(forstopRacing
, naturally). TheRacerCode
class should look like the following:public class RacerCode {
public static void startRacing(ActionContext context) {
}
public static void stopRacing(ActionContext context) {
}
} - Open the
TortoiseVsHareDelegateModuleProvider
class. -
Add the following lines to the end of the
registerDelegate
method:codeRegistry.delegateExecutionOfAction(rootPackageName + "::Racer::startRacing", context -> {
RacerCode.startRacing(context);
return DelegationResult.NoAdditionalEvents.INSTANCE;
});
codeRegistry.delegateExecutionOfAction(rootPackageName + "::Racer::stopRacing", context -> {
RacerCode.stopRacing(context);
return DelegationResult.NoAdditionalEvents.INSTANCE;
});
These calls to codeRegistry will tell Behavior Execution Engine that whenever the Racer
’s startRacing
or stopRacing
actions would occur, to instead execute the delegated action.
You can verify this behavior by installing
and running again while looking for the DelegatedActionExecuted
log in the trace.
Keep in mind that delegating for actions will replace all the original behavior of the action. This means that any owned
actions will not occur. This is the reason why we removed those assignment actions from the model earlier.
The delegateExecutionOfAction
method expects a return of a DelegationResult
.
This result is used to tell Behavior Execution Engine if there are any future events that the delegated action should wait on before performing.
Dealing with future events is out of scope of this tutorial so we use the singleton NoAdditionalEvents instance to tell
Behavior Execution Engine that after performing this delegated step, the action is ready to complete.
More information can be found on the delegate module page.
Recording information during the race
With the basic stubs in place, we can now add in the logic for changing the racer’s speed over time.
- Open the
RacerCode
class. -
Add the following static helper method for when the racers start running:
public static void recordRunningStart(ActionContext context, ScalarQuantityValue distanceRan, AbsoluteTime time) {
RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
ScalarQuantityValue topSpeed = (ScalarQuantityValue) context.getAccessorService().requestFeature(rootPackageName + "::Racer::topSpeed").getValue().get(0);
racerDataHistory.recordRacerData(time, topSpeed, distanceRan);
} -
Change the
startRacing
method to the following:public static void startRacing(ActionContext context) {
AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();
recordRunningStart(context, new ScalarQuantityValue(0, Units.METRE), time);
} -
Add the following static helper method for when the racers stop running:
public static void recordRunningStop(ActionContext context, ScalarQuantityValue distanceRan, AbsoluteTime time) {
RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
racerDataHistory.recordRacerData(time, new ScalarQuantityValue(0, Units.METRE_PER_SECOND), distanceRan);
} -
Change the
stopRacing
method to the following:public static void stopRacing(ActionContext context) {
AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();
FeatureReadWriteAccessor raceFinishedAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Racer::raceFinished");
raceFinishedAccessor.setValue(List.of(true));
FeatureReadAccessor distanceCoveredAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Racer::distanceCovered");
ScalarQuantityValue distanceCovered = (ScalarQuantityValue) distanceCoveredAccessor.getValue().get(0);
recordRunningStop(context, distanceCovered, time);
} - Install and execute the simulation again.
After making these basic changes, the racers now mark when they start racing and how fast they are going.
We are going to have to add in more logic for the hare since there are additional behaviors at play for napping during the race.
However, this change brought the racers another step closer to finishing the race. You will notice that the exception that was
raised this time was about not being able to create an evaluator for the distanceCovered
attribute. This new exception occurs when
you use a delegated scalar value in a change trigger. Behavior Execution Engine needs a way to determine when some predicate
of the change event will change from false to true. More information on change events can be found
here.
Delegating for a scalar threshold comparison
Normally, the threshold system for ScalarValueFeatureAccessor
s is defaulted to have no implementation.
However, in this case, we will need an implementation for one.
- Open the
DistanceCoveredAccessor
class. - Create an override method for the interface’s
canCreateEvaluator
method. -
Have the method return true in every case. This model doesn’t require any sophisticated checking, and we therefore know we’ll
always be able to evaluate the
distanceCovered
for any given time. The method should look like the following:@Override
public boolean canCreateEvaluator(@NotNull TimeInterval analysisInterval) {
return true;
} - Create an override method for the interface’s
createEvaluator
method. -
Return a new
ScalarValueFeatureEvaluator
for the previously defined method. It should look like the following:@Override
public ScalarValueFeatureEvaluator<ScalarQuantityValue> createEvaluator(@NotNull TimeInterval analysisInterval) {
return new ScalarValueFeatureEvaluator<ScalarQuantityValue>() {
}
} -
Create an override method for the
ScalarValueFeatureEvaluator
interface’sgetValue
method with the following implementation:@Override
public List<ScalarQuantityValue> getValue(@NotNull InstanceContext context) {
AbsoluteTime currentTime = context.getTimeService().getCurrentSimulationTime();
RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
return List.of(racerDataHistory.calculateDistanceAtTime(currentTime));
}The point of the evaluator is to compare possible futures together until the earliest event is determined.
-
Create an override method for the
ScalarValueFeatureEvaluator
interface’scomputeSatisfactionIntervals
method with the following implementation :@Override
public List<TimeInterval> computeSatisfactionIntervals(
@NotNull InstanceContext context,
Object threshold,
ComparisonOperator comparisonOperator
) {
AbsoluteTime currentTime = context.getTimeService().getCurrentSimulationTime();
ScalarQuantityValue thresholdDistance = (ScalarQuantityValue) threshold;
double thresholdDistanceMeters = HelperCode.convertScalarQuantityUnitValue(thresholdDistance, Units.METRE);
RacerDataHistory racerDataHistory = context.getDataService().getDelegationData(RacerDataHistory.class);
RacerData lastRecordedRacerData = racerDataHistory.getLastRecordedRacerData();
double previousSpeedMetersPerSecond = convertScalarQuantityUnitValue(lastRecordedRacerData.speed(), Units.METRE_PER_SECOND);
double analysisStartDistanceMeters = racerDataHistory.calculateDistanceAtTime(currentTime).getValue().doubleValue();
double deltaTimeSeconds = Math.ceil((thresholdDistanceMeters - analysisStartDistanceMeters) / previousSpeedMetersPerSecond);
AbsoluteTime satisfiedTime = currentTime.addTotalSeconds(deltaTimeSeconds);
List<TimeInterval> satisfiedIntervals = null;
switch (comparisonOperator) {
case GREATER_THAN ->
satisfiedIntervals = List.of(new TimeInterval(satisfiedTime, analysisInterval.getStop(), false, true));
case GREATER_THAN_OR_EQUAL ->
satisfiedIntervals = List.of(new TimeInterval(satisfiedTime, analysisInterval.getStop()));
case LESS_THAN ->
satisfiedIntervals = List.of(new TimeInterval(analysisInterval.getStart(), satisfiedTime, true, false));
case LESS_THAN_OR_EQUAL ->
satisfiedIntervals = List.of(new TimeInterval(analysisInterval.getStart(), satisfiedTime));
case EQUAL -> satisfiedIntervals = List.of(new TimeInterval(satisfiedTime, satisfiedTime));
case NOT_EQUAL -> satisfiedIntervals = List.of(
new TimeInterval(analysisInterval.getStart(), satisfiedTime, true, false),
new TimeInterval(satisfiedTime, analysisInterval.getStop(), false, true)
);
}
return satisfiedIntervals;
}
In this method we are gathering historical information on where the racer was, how fast the racer was going, and when this information was recorded from the history. The threshold represents a ScalarQuantityValue distance that we are comparing to. We use the history information to calculate when the racer will have reached that threshold distance. After we calculate when the racer reaches the threshold, we can use the provided comparison operator to contextualize exactly when the comparison is satisfied.
Adding Logic for the Hare
If you run the simulation again now, you can look for the Winner
and Loser
StateStarted messages. You will see that the hare will win the race and the tortoise loses. This is because the hare is faster than
the tortoise, and we are still missing the custom logic for napping.
- Create a new java class file called
HareCode
. -
Create a static method called
startNapping
with the following implementation:public static void startNapping(ActionContext context) {
AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();
FeatureReadAccessor distanceCoveredAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Racer::distanceCovered");
ScalarQuantityValue distanceCovered = (ScalarQuantityValue) distanceCoveredAccessor.getValue().get(0);
FeatureReadWriteAccessor isNappingAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Hare::isNapping");
isNappingAccessor.setValue(List.of(true));
RacerCode.recordRunningStop(context, distanceCovered, time);
} -
Create a static method called
stopNapping
with the following implementation:public static void stopNapping(ActionContext context) {
AbsoluteTime time = context.getTimeService().getCurrentSimulationTime();
FeatureReadAccessor distanceCoveredAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Racer::distanceCovered");
ScalarQuantityValue distanceCovered = (ScalarQuantityValue) distanceCoveredAccessor.getValue().get(0);
FeatureReadWriteAccessor isNappingAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Hare::isNapping");
isNappingAccessor.setValue(List.of(false));
FeatureReadWriteAccessor wantsToNapAccessor = context.getAccessorService().requestMutableFeature(rootPackageName + "::Hare::hasTakenANap");
wantsToNapAccessor.setValue(List.of(true));
RacerCode.recordRunningStart(context, distanceCovered, time);
} - Open the
TortoiseVsHareDelegateModuleProvider
class. -
Add the following lines to the end of the
registerDelegate
method:codeRegistry.delegateExecutionOfAction(rootPackageName + "::Hare::startNapping", context -> {
HareCode.startNapping(context);
return DelegationResult.NoAdditionalEvents.INSTANCE;
});
codeRegistry.delegateExecutionOfAction(rootPackageName + "::Hare::stopNapping", context -> {
HareCode.stopNapping(context);
return DelegationResult.NoAdditionalEvents.INSTANCE;
});
As you can see, the logic from the previous tutorial’s assignment actions is now faithfully recreated in the delegated actions.
Now that the hare’s speed can be zero when resting, we need to add more logic to that threshold evaluator to avoid a division by zero.
- Open the
DistanceCoveredAccessor
class. -
Add the following method below the
computeSatisfactionIntervals
method:private boolean isSatisfiedWithCurrentDistance(
ComparisonOperator comparisonOperator,
double referenceDistanceMeters,
double thresholdDistanceMeters
) {
boolean satisfied = false;
switch (comparisonOperator) {
case GREATER_THAN -> satisfied = referenceDistanceMeters > thresholdDistanceMeters;
case GREATER_THAN_OR_EQUAL -> satisfied = referenceDistanceMeters >= thresholdDistanceMeters;
case LESS_THAN -> satisfied = referenceDistanceMeters < thresholdDistanceMeters;
case LESS_THAN_OR_EQUAL -> satisfied = referenceDistanceMeters <= thresholdDistanceMeters;
case EQUAL -> satisfied = referenceDistanceMeters == thresholdDistanceMeters;
case NOT_EQUAL -> satisfied = referenceDistanceMeters != thresholdDistanceMeters;
}
return satisfied;
} -
Add the following lines below the declaration of the
analysisStartDistanceMeters
variable:if (previousSpeedMetersPerSecond == 0.0) {
boolean satisfied = isSatisfiedWithCurrentDistance(comparisonOperator, analysisStartDistanceMeters, thresholdDistanceMeters);
if (satisfied) {
return List.of(analysisInterval);
} else {
return List.of();
}
}
With these last additions, the delegate module’s logic is complete. Reinstalling the delegate module and running the simulation again will reveal that the tortoise finishes the race before the hare, since the hare cannot make up the distance after the nap.
Adding Logging to the Module
You might find it difficult to sort through all the messages that show up in the trace in the log. To remedy this, you can add in your own custom log messages that will add in more useful and contextualized information for your delegate module during simulation.
- Open the
HelperCode
class. -
Add in the following method to the class:
public static void logHareAndTortoiseComparison(ActionContext context, AbsoluteTime time, ScalarQuantityValue distanceCovered, ContextLogger logger) {
FeatureReadAccessor tortoiseAccessor = context.getAccessorService().requestFeature(rootPackageName + "::Hare::tortoise");
InstanceValue tortoise = (InstanceValue) tortoiseAccessor.getValue().get(0);
RacerDataHistory tortoiseInformation = tortoise.getContext().getDataService().getDelegationData(RacerDataHistory.class);
ScalarQuantityValue tortoiseDistanceCovered = tortoiseInformation.calculateDistanceAtTime(time);
double distanceAheadMeters =
convertScalarQuantityUnitValue(distanceCovered, Units.METRE) - convertScalarQuantityUnitValue(tortoiseDistanceCovered, Units.METRE);
if (distanceAheadMeters > 0) {
logger.info("The hare is " + Math.abs(distanceAheadMeters) + " meters behind the tortoise.");
} else {
logger.info("The hare is " + distanceAheadMeters + " meters ahead of the tortoise.");
}
}
These new log messages will give us a good indication of where the hare is in comparison to the tortoise during the race. Now we can add that call and more diagnostic information to the other methods.
- Open the
RacerCode
class. -
Add the following lines to the
startRacing
method:ContextLogger logger = context.getContextLogger();
String racerName = (String) context.getAccessorService().requestFeature(rootPackageName + "::Racer::name").getValue().get(0);
logger.info("The " + racerName + " is starting to run the race at: " + time.toIso8601String());
if (racerName.equals("hare")) {
HelperCode.logHareAndTortoiseComparison(context, time, new ScalarQuantityValue(0, Units.METRE), logger);
} -
Add the following lines to the
stopRacing
method:ContextLogger logger = context.getContextLogger();
String racerName = (String) context.getAccessorService().requestFeature(rootPackageName + "::Racer::name").getValue().get(0);
logger.info("The " + racerName + " stopped running the race at: " + time.toIso8601String()); - Open the
HareCode
class. -
Add the following lines to the
startNapping
method:ContextLogger logger = context.getContextLogger();
HelperCode.logHareAndTortoiseComparison(context, time, distanceCovered, logger);
logger.info("The hare is taking a nap at: " + time.toIso8601String());
logger.info("The hare has run " + distanceCovered.getValue() + " " + distanceCovered.getRefUnit().getName() + "s."); -
Add the following lines to the
stopNapping
method:ContextLogger logger = context.getContextLogger();
logger.info("The hare woke up from the nap at: " + time.toIso8601String());
HelperCode.logHareAndTortoiseComparison(context, time, distanceCovered, logger); - Install and execute the simulation.
Now when you execute the simulation, you will see your custom log messages that Behavior Execution Engine sends to the console window, telling you the exact times the racers start and stop racing, how far they have progressed at each stoppage, and how far the hare is away from the tortoise.
You have now successfully adjusted the previous iteration of the race simulation to use delegates and then modified those delegates to provide custom functionality to meet your needs.
Next Tutorial >