Creating a trigger system

Almost all story-driven games require some kind of system to trigger some sort of event for example, dialogs, enemies, or doors opening. Unless the game is very small, you generally don't want to hardcode these. The following recipe will describe a trigger system, which can be used for almost any type of game from FPSs to RTSs and RPGs.

We'll start out by laying the ground work with an AppState controlling all the script objects and the basic functionality of a Trigger class. Then, we'll look into how to actually activate the trigger and use it for something.

Getting ready

Before we start with the actual implementation, we create a small interface that we will use for various scripting scenarios. We call it ScriptObject and it should have the following three methods:

void update(float tpf);
void trigger();
voidonTrigger();

How to do it...

Now, we can implement the ScriptObject in a class called Trigger. This will have six steps:

  1. Add the following fields to the Trigger class:
    private boolean enabled;
    private float delay;
    private boolean triggered;
    private float timer;
    private HashMap<String, ScriptObject> targets;
  2. The enabled and delay fields should have getters and setters and targets should have a addTarget and removeTarget method publically available.
  3. In the trigger method, we add the following functionality:
    If enabled is false it shouldn't do anything.
    Otherwise timer should be set to 0 and triggered to true.
  4. If the script is enabled in the update method, we should perform the following steps:
    1. If triggered is true and delay is more than 0, the timer should be increased by tpf.
    2. Then if the timer is more than or equal to delay, it should call onTrigger().
  5. If delay is 0 and triggered is true, the timer should also call onTrigger().
  6. In the onTrigger method, we should parse through all the values of targetsMap and call the trigger on them. Then triggered should be set to false.

Now, perform the following set of steps to control the Trigger class.

  1. We define a new class called ScriptAppState, which extends AbstractAppState.
  2. It should have a List<ScriptObject> called scriptObjects, together with methods to add and remove ScriptObjects from List.
  3. In the update method, if isEnabled() is true, it should parse scriptObjects and call an update on all of the ScriptObjects.

Now, we have a flexible system where one ScriptObject can trigger another. We're still lacking input and output effects though. One common way to trigger events is when a player enters an area. So let's go ahead and add that functionality by performing the following steps:

  1. Create a new class called EnterableTrigger, which extends Trigger.
  2. This trigger needs a Vector3f field called position to define its place in the physical world along with a getter and setter.
  3. Add a BoundingVolume field called volume. In the setter method for this, we should call volume.setCenter(position).
  4. Also, it needs a List<Spatial> called actors along with add and remove methods.
  5. Now, we should override the update method and then call the trigger if any item in the actors list is inside volume:
    if(isEnabled() && volume != null && actors != null){
      for(int i = 0; i<actors.size(); i++ ){
        Spatial n = actors.get(i);
        if(volume.contains(n.getWorldTranslation())){
          trigger();
        }
      }
    }
  6. We've taken care of the triggering now. Let's actually do something with that trigger by creating a new class called SpawnTarget, implementing ScriptObject.
  7. Like the EnterableTrigger class, the SpawnTarget class needs a position field and also a Quaternion field called rotation.
  8. The SpawnTarget class also requires a Spatial field called target and a Boolean field called triggered to know whether it's been triggered yet or not.
  9. We should also add a Node field called sceneNode to attach the target to.
  10. In the trigger method, we should check whether it has been triggered already. If not, we should set triggered to true and call onTrigger.
  11. The onTrigger method should apply the position and rotation to the target and attach it to the sceneNode. Depending on the implementation, we might want to subtract the worldTranslation and worldRotation values from the values we apply:
    target.setLocalTranslation(position);
    target.setLocalRotation(rotation);
    sceneNode.attachChild(target);

Let's have a look at another common game object that can be picked up. In many games, characters can pick up various power-up weapons or other items simply by walking over them. This section will have the following eight steps:

  1. We create a new class called Pickup extending Trigger.
  2. Like EnterableTrigger, the Pickup class needs a position and a List<Spatial> called actors. We also need to add a Spatial field called triggeringActor and a float called triggeringDistance.
  3. For this class, we also need something to pick up, represented here by an interface called Pickupable. In addition, we need to keep track of whether it's been picked up by a Boolean called pickedUp.
  4. The difference between the previous ScriptObjects we've worked with and the current ScriptObjects is that the one in this recipe should be visible in the world, represented by a Spatial called model.
  5. In the update method, we should check whether the Pickup object is enabled and not pickedUp.
  6. To make it stand out a bit in the game world, we rotate the model a little bit by applying the model.rotate(0, 0.05f, 0) value.
  7. Still inside the if clause, we check that actors is not null and parse through the list. If any of the actors is inside the radius of the triggerDistance, we set it to be triggeringActor and call the trigger method:
    for(int i = 0; i<actors.size(); i++ ){
    
      Spatial actor = actors.get(i);
      if((actor.getWorldTranslation().distance(position) <triggerDistance)){
        triggeringActor = actor;
        trigger();
      }
    }
  8. Finally, in the onTrigger method, we should set pickedUp to true, detach model from scene graph and call pickupObject.apply(triggeringActor) to have it apply whatever the Pickupable object is supposed to do.

How it works...

The Trigger class has a fairly simple functionality. It will wait for something to call its trigger method.

When this happens, it will either trigger all the connected ScriptObjects immediately or if a delay is set, it will start counting until the time has passed and then execute the trigger. Once this is done, it will be set up so it can be triggered again.

The ScriptAppState state is a convenient way to control the scripts. Since the AppState is either disabled or not attached to the stateManager, no call to update in ScripObjects is made. This way, we can easily disable all the scripting if we want to.

To create a working example with Trigger, we extended it into a class called EnterableTrigger. The idea with the EnterableTrigger class was that if any of the supplied actor spatials enter its BoundingVolume instance, then it should trigger whatever is connected to it.

The basic Trigger method doesn't have the need for a position as it is a purely logical object. The EnterableTrigger object, however, has to have a relation to the physical space as it needs to know when one of the actors has entered its BoundingVolume instance.

This is true for SpawnTarget as well, which in addition to a location should have a rotation, to rotate a potential enemy in a certain direction. Spawning characters or items in games is commonly used to control the gameplay flow and save some performance. The SpawnTarget option allows this kind of control by adding new spatials only when triggered.

The strategy for how to perform spawning might differ depending on the implementation but the way described here assumes it involves attaching the target Spatial to the main node tree, which would generally activate its update method and controls.

Likewise, the rootNode of the scene graph is not necessarily the best choice to attach the target to and depends a lot on the game architecture. It could be any Spatial.

Lastly, in this recipe, we created a Pickup object, which is very common in many games. These can be anything from items that increase health instantly or weapons or other equipment that are added to an inventory. In many cases, it's similar to the EnterableTrigger except it only requires a radius to see whether someone is within the pickup range or not. We keep track of the actor that enters it so that we know who to apply the pickup to. In this recipe, the pickup is represented by an object called Pickupable.

Once it's picked up, we set pickedUp to true so that it can't be picked up again and detach the model from the node tree to make it disappear. If it is a recurring power up, a delay can be used here to make it available again after some time.

Pickups in games usually stand out from other objects in the game world to draw attention to them. How this is done depends on the game style, but here we apply a small rotation to it in each call to the update method.

Since Pickup also extends Trigger, it's possible to use it to trigger other things as well!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.102.50