Entity-Based Scripting: The Repair Bots
Introduction
Repair Bots opened up some fun options for scripted behavior in Quake 4. They serve more as an ambient set piece than an actual enemy and can add a lot of life to a scene. Rather than scripting this ambience for each scene, we created a common entity-based script function that we can call on any Repair Bot, allowing it to randomly move from one point to the next, ‘repairing’ all the way.
In this guide we won’t cover the use of the Repair Bot script (though you can find implementation info in the .script file comments and some game levels and probably will still know how to use it by the time we’re done), but rather use it as a detailed example of how we used a common script to create a flexible system that can be added to a map with no (additional) scripting required.
This guide assumes you are familiar with the basics of the Scripting system and the level editor.
The Function
Before we cover anything, let’s touch on the order of events within the Repair Bot function:
Spawn Repair Bot.
- Select a random target from an available list.
Check if another Repair Bot is using that position.
- If the position is occupied, select a new random target until finding one that is unoccupied.
Flag the target as occupied so no other Repair Bots can select it.
- Move to the target.
- Look at a specific point of interest
- Perform the ‘repair’ action
- Mark the current target as unoccupied
- Select a new target and repeat
If at any point the Repair Bot dies, set the current target as unoccupied and break the function
This ends up looking more complicated than it really is, so even if you’re not that familiar with the script system, don’t worry – if it makes you feel any better, this was my first script for the game!
Through the next two sections (Entities and Script), we’ll look at how we achieve each of these events and how the function was designed with a drop-anywhere mentality.
The Entities
Let’s start by examining the Entities used in one use of this system: the Tetranode in the Nexus Hub. I’ve moved some points around to get some clearer lines, but otherwise this is an accurate top-down view of all the available points in that scene. We can actually use this as a great outline of what will occur in our function.
All of these points are target_nulls used to control the Repair Bot positions during the sequence. If you recall, target_nulls don’t technically do anything. But in scripting systems such as this, they’ll be your best friend.
You’ll notice I highlighted a specific target null that targets about half of the remaining points. This serves as a “list” from which the Repair Bots will select their points. Let’s take a look at the entity info box:
Each target shown here is a valid target the Repair Bot may choose. We’ll examine how to pull values from this list in the Script section.
Now, our available positions:
Each highlighted entity is another target_null, this time representing one of the points that the Repair Bot may select during the function. We’ll refer to these as ‘repairTargets.’ In looking at the entity info, we find a few differences.
You’ll notice we’ve added some keys. “action” and “duration” are used by special scriptedAction behavior that supports the Repair Bot. We’ve also added a completely made up key, “occupied.”
Even though the target_null doesn’t officially support keyvalues, we’re able to use them to store information. By using this, we know that when the Repair Bot spawns, this point will be available (as it starts unoccupied) and that if selected, the Repair Bot will move here and perform the “repair” action for 3 seconds.
The target key then tells us where this Repair Bot is going to look when it arrives (remember, that’s a key step we want it to perform).
Now to our targets:
These will be our ‘repairAction’ entities. You’ll notice not all the repairTargets have repairActions – we’ll cover this when we step through the script.
As you can see, these entities have no special settings on them – we just need their positions.
This entity layout ends up giving us a nice visual representation of how our function will flow. We see our list targeting each repairTarget, which in turn targets the repairAction point at which the Repair Bot will look during the operation.
There’s one final entity in the mix, a func_spawner to feed Repair Bots for an endless supply:
The two important keys to note are:
call_spawned: Whenever a Repair Bot spawns, this function will be called, passing in the Repair Bot into the script.
spawn_list: This sets the “list” key on any Repair Bot spawned from this spawner to “tetraList,” which in this case is the list of available positions we examined above.
These transition us nicely into the fun part: the script!
The Script
Now let’s break down the script and see how our entities fit into the mix. We’ve seen how to store the information, but how do we access and modify it from the script?
The script file can be found in scripts/common/repair_bot.script for your own reference, but we’ll cover the entire function here (with comments on why each section is important, or why you can ignore some chunks).
void randomBots( entity repairBot ) { thread botActions( repairBot ); }
You’ll recognize this as the script function set on the func_spawner. It might seem redundant to just thread another function, but this is actually extremely important – by threading the second function, we ensure that all the Repair Bots running this function simultaneously can run parallel with no issues.
void botActions( entity botName ) {
When the function starts, the only entity it knows is our Repair Bot, threaded from randomBots.
// Define the entities that will be used for random target selection entity repairTarget = $null_entity; entity oldTarget = $null_entity;
Our next two lines declare a pair of local variables. We’ll set their values later in the function.
// Pull the name of the correct list passed to this Repair Bot from the spawner. entity botList = botName.getEntityKey("list");
This is the single most important line in this script. With this one line, we’ve already made the difference between a static script and one that can be used anywhere. By pulling a keyvalue from an entity that is already a local variable, we know that any Repair Bot can gain access to any list of entities we choose.
We’ve also opened our script wide open for gaining information from other entities. Now that the list target_null is stored, we have access to every entity that may be specified on the list as well as any keys or targets those entities may store. By keeping our entities as local variables to the function rather than relying on scripting them by name, we’ve already ensured that this function can be used anywhere with any entity list provided it follows the structure we established originally.
Let’s keep looking at ways we can access and fiddle with this information.
// Perform these actions as long as the Repair Bot lives. while ( isValidEntity ( botName ) ) {
We’ll drop this in a while loop – Repair Bots are somewhat flimsy, so this will help us break out if one should come across an untimely end, and also end the function if the Repair Bot is removed from the map by other means. Quick Aside: If you’re using entities that don’t explode (particularly any that don’t burn out such as the marines), you’ll want to use isLivingEntity instead – that one includes an extra check for health, as dead/ragdoll creatures may still count as ‘valid’ and cause problems.
// Select a new move target at random from the list of target_nulls repairTarget = qListRandom( botList );
Now we’re ready to set our repairTarget keyvalue. Remember that previously we referred to our available move points as repairTargets – same applies here. Using the qListRandom function, we’re able to select an entity at random from our list of available entities.
We could also get away with repairTarget.randomTarget(), but qListRandom (a handy ScriptUtility) does some extra checks for us and adds handling in the event that one of our list entities is removed.
// Check against the previous target, and select a new one if the old target repeats itself. while ( repairTarget.getKey( "occupied" ) == "1" ) { repairTarget = qListRandom( botList ); sys.waitFrame(); }
Now it’s time to bring the “occupied” key into the mix. We know that we don’t want two Repair Bots to overlap at the same point, so before we do anything we check that key. As long as the Repair Bot finds repairTargets that are occupied, it will select a new target until it finds one with the “occupied” key set to 0.
// We found a valid target! Pull the action target from the move target. entity repairAction = repairTarget.getTarget(0);
Once we have a target, we can use the getTarget function to pull the associated entity into the script. Using targets instead of keys can be quicker in setting up your entities (thanks to a handy Editor Shortcut), but remember that getTarget takes a float parameter to specify which target to select. getTarget(0) will select the “target” entity, but getTarget(4) will select the “target4” entity. Be very careful with this if storing more than one target per entity or if you’ve changed targets when it might be easy to accidentally get a “target1” key.
// Set target as occupied to prevent other repair bots from using it thread setOccupied( repairTarget );
In the case of the Repair Bot function, I used a threaded function to toggle occupied status – this was a result of some debugging for an alternate setup and it worked, so it was never changed. In most cases, though, a simple entity.setKey should be appropriate.
Now that we have all our entities set up properly, let’s see how it all comes together for the meat of the function:
// Identify action type (repair target or kill target). if( isValidEntity ( repairAction ) ) { // Move to the target. aiScriptedMoveWait( botName, repairTarget , 32 , 0 ); sys.waitFrame(); // Run check to ensure bot is still valid. if( isValidEntity ( botName ) ) { // Look at the target. aiScriptedFaceWait( botName, repairAction, 0 ); sys.waitFrame(); // Run another check to ensure bot is still valid. if( isValidEntity ( botName ) ) { // Perform the scripted action. aiScriptedActionWait( botName, repairTarget , 0 ); sys.waitFrame(); } } // Set target as unoccupied once action is completed. thread setOccupied( repairTarget ); }
You’ll notice first off that this entire chunk of script is an if conditional checking to see if repairAction is a valid entity. By not including targets for two of the repairTargets, I was able to include two very different behaviors at certain points.
Also note the abundance of isValidEntity checks. These are very important when using any version of a ‘wait’ function on entities that can be killed, and at the very least spare you a lot of console warnings getting dumped (which also would prevent the
Otherwise the behavior is exactly as we outlined. The Repair Bot moves to the repairTarget point, looks at the repairAction entity, performs the action specified on repairTarget, and then sets its repairTarget position back to unoccupied status. By some careful entity and script setup, we can get away with writing this chunk of script once. Imagine, in contrast, writing this for each Repair Bot and each repairTarget in sequence.
// Move targets with no target are kill zones which should remove the repair bot. Handle those here. else { // Move to the target. aiScriptedMoveWait( botName, repairTarget , 64 , 0 ); sys.waitFrame(); // Run check to ensure bot is still valid. if( isValidEntity ( botName ) ) { // Set target as unoccupied once action is completed. thread setOccupied( repairTarget ); //Kill repair bot. FIX LATER IF THIS MAKES THINGS EXPLODE. botName.removeUpdateSpawner(); break; } }
Now we see the alternate behavior I specified for the two repairTarget positions that did not include repairAction targets. These points were hidden out of view and designed to keep fresh Repair Bots cycling into the scene – also allowing the spawner to be turned off and eventually leave the zone empty.
It’s another straightforward function here, but instead of performing an action, the Repair Bot moves to the repairTarget, sets the repairTarget as unoccupied, and removes itself. The removeUpdateSpawner function in this case was very important, as simply removing an entity caused the func_spawner to ‘forget’ to spawn more and killing them resulted in (as I’m sure you can imagine by my humorous commentary) an awkward explosion.
// Define the old target for comparison against the new target. oldTarget = repairTarget; } }
This last line was from a previous version that also checked to make sure the Repair Bot selected a new target and did not repeat the same target twice. In practice, it ended up not being a large concern, but you could run similar checks and comparisons by storing your variables in a similar fashion.
That concludes our examination of the common Repair Bot script! Hopefully you’ve seen not only how to pull information into the script for flexible use, but also why much of this function was set up as it was. While this is more of a commentary on an existing example rather than a tutorial, hopefully you can take the information demonstrated here and put it to good use in your own sequences. Good luck!