Tutorial: Creating a patrol behavior with the Notify component

We’re going to look closer at two components, Notify and Timer and use them to create a simple, looping patrol behavior for a spatial. While most steps here don’t require any coding, one of them does.

Notify is a powerful component that sends customizable messages to a defined Control. We’re going to use it to send a message to a Control, telling it which patrol point to move to.

We’re going to do this in 5 steps. The first is to create a scene where it’s all played out.

I’ve created this simple scene, which has three patrol points. Each point is a box which has been scaled down and translated to the location I want.

Tip: The patrol points don’t need to be visible. Put them in a Node of their own and choose ”Cull Hint – Always” to hide them from view.

With the scene done, we can create the script. Right click in the ”Scripts” folder (or create the folder if you don’t have it already) and select ”New – Other… – Other – Macaq Script”.

Open it by right clicking on it and select ”Edit Script”.

The first thing we need to add is a Start component. The Start component always executes onStart when the script is run. Right click on the canvas and select Start.

Tip: You can move the component around by pressing and holding on the title bar and dragging it.

Next, we add a Notify component using the same process. This is the component that will tell the Control to move towards the first patrol point, moveTarget1 in our scene.

Open the properties of the node by selecting it (click somewhere where there’s no connection), we can see a number of fields that we need to set.

  • Set the Name if you have several components and wish to keep track of them
  • Enabled is default true. If this is false, no output will come from the component.
  • SpatialName. This is the spatial which you want to affect. Start typing its name and you will get suggestions based on the scene which is loaded in the SceneComposer.
  • ControlClassName. This is the custom control which we will write and that will handle the communication. The name needs to be the full qualified name. In this example we’ll call it com.jme3.macaq.control.NotifyableControl.
  • Message. This is the message that the component will send when ”message” is triggered. It’s a String. For this first Notify, the message will be ”moveTarget1”.

That’s the first component done. When message is called, it will call our yet to be created NotifyableControl with the message ”moveTarget1”. We will write the handling of this later.

For now, wire up Start:onStart to Notify:message.

When a Control is done with whatever ”message” tells it to do, it will (should) report back to the Notify component. This will in turn trigger Notify:onPerformed.

In this example, we’re doing a patrol behavior, so we want it to move to a new patrol point when done. However, we want it to wait for a bit before doing so. Enter the Timer component.

When Timer:start is triggered, it will start counting and when the ”triggerTime” (in milliseconds) is reached, onTrigger will be called. For this Timer, the default 1000ms is an OK value (but feel free to change it if you want to see what happens).

Now that the basic behaviors are done, we just need to copy it 2 times to get our actor to travel to all our patrol points in the scene. Do this and you’ll end up with something that looks like the image below.

For extra effect, we can connect onTrigger in Timer3 to the first Notify again. This will cause the behavior to loop indefinitely.

At this point, some coding is inevitable. We need to write the Control that actually does something with the information coming from the script. As mentioned before, we create a new class an call it NotifyableControl. It’s important that this class implements the Macaq interface Notifyable. In doing so, you get two methods that need to be implemented. These mirror the connections in the Notify component.

These methods need to be populated by some ”plumbing” code to generate the expected connections that will call and be called from the Notify component.

Of these two, the message method is the most interesting.

First define a LogicInConnection field called message:

private LogicInConnection message;

Then inside the message() method, check if message has been created before, if not, create a new one and define its execute method (here shown with lambdas):

if(message == null){
    message = new LogicInConnection((Map<String,Object> args) -> {
        // to be written
    }, "message");
}

When a message is received, we want it to extract a target from the message. We know that it is the name of a spatial (moveTarget1 in the case of the first Notify), so we can write some code that will look for this spatial through the root node. This is done so commonly in Macaq that there is a small util class handling it already. Each time a connection is used, there can be a payload in the form of a Map. This is usually populated with ”id” of the calling component. In our case, it also has the message.

Node root = ControlUtil.findRootNode(spatial);
currentTarget = root.getChild((String)args.get("message"));
originId = (int) args.get("id");

We store the id of the originating component, because in this case, there will be several Notify’s connected to the same control. By sending the id back in onPerformed, they can figure out which one is supposed to take action.

For the onPerformed method, we need some boilerplate code. We define a field called onPerformed and add the following code:

public LogicOutConnection onPerformed() {
    if(onPerformed == null){
        onPerformed = new LogicOutConnection("onPerformed");
    }
    return onPerformed;
}

The controlUpdate method is the most interesting part. This is where we decide what happens with the incoming data. We code the method to do two things. If there is a target and this control’s spatial is too far away, it will try to move closer. If it is close enough, it will send back this information to the Notify component. It does this by creating a new HashMap and placing the id of the Notify component inside and then call execute() on the onPerformed object.

Vector3f targetLocation = currentTarget.getLocalTranslation();
if(spatial.getLocalTranslation().distance(targetLocation) > MIN_DISTANCE){
    Vector3f moveVector = targetLocation.subtract(spatial.getLocalTranslation()).normalizeLocal();
    spatial.move(moveVector.mult(speed * tpf));
    spatial.lookAt(targetLocation, Vector3f.UNIT_Y);
} else {
    currentTarget = null;
    Map args = new HashMap<>();
    args.put("id", originId);
    onPerformed().execute(args);
}

That’s all there is to it. Here’s the complete code for the class:

For step 4, we open the SceneComposer again. We will add this control to the player spatial. You do this by selecting the ”player” object in the SceneExplorer, right clicking on it and selecting ”Add Control… Custom Control”. You should now be presented with a window and NotifyableControl will be able to be selected. Save the scene.

The last step is creating the application itself, and it’s quickly done. Add a new class that extends SimpleApplication. Inside the simpleInitApp method, we just need to load the scene

Node scene = (Node) assetManager.loadModel("Scenes/TestMultiMove.j3o");
rootNode.attachChild(scene);

and then the script:

MacaqScriptAppState macaqAppState = new MacaqScriptAppState("Scripts/MultiMove.mqs");
stateManager.attach(macaqAppState);