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.
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();
Now, we can implement the ScriptObject
in a class called Trigger
. This will have six steps:
Trigger
class:private boolean enabled; private float delay; private boolean triggered; private float timer; private HashMap<String, ScriptObject> targets;
enabled
and delay
fields should have getters and setters and targets
should have a addTarget
and removeTarget
method publically available.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.
update
method, we should perform the following steps:triggered
is true
and delay is more than 0, the timer should be increased by tpf.onTrigger()
.triggered
is true
, the timer should also call onTrigger()
.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.
ScriptAppState
, which extends AbstractAppState
.List<ScriptObject>
called scriptObjects
, together with methods to add and remove ScriptObjects from List
.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:
EnterableTrigger
, which extends Trigger
.Vector3f
field called position
to define its place in the physical world along with a getter and setter.BoundingVolume
field called volume
. In the setter
method for this, we should call volume.setCenter(position)
.List<Spatial>
called actors
along with add
and remove
methods.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(); } } }
SpawnTarget
, implementing ScriptObject
.EnterableTrigger
class, the SpawnTarget
class needs a position
field and also a Quaternion
field called rotation
.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.Node
field called sceneNode
to attach the target to.trigger
method, we should check whether it has been triggered already. If not, we should set triggered
to true
and call onTrigger
.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:
Pickup
extending Trigger
.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
.Pickupable
. In addition, we need to keep track of whether it's been picked up by a Boolean called pickedUp
.Spatial
called model
.update
method, we should check whether the Pickup
object is enabled and not pickedUp
.model.rotate(0, 0.05f, 0)
value.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(); } }
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.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!
18.227.102.50