Left 4 Dead 2/Scripting/Expanded Mutation System/GnomeHunter tutorial 4
Allowing Survivors to pick up the gnome
Now we have our gnome in the game. Next we need to permit Survivors to pick it up! The Mutation System will look for a function called CanPickupObject() in your VScript code. If you provide this function, it will be called when a player presses their use key on an object. In this function your script may decide whether or not to allow the player to pick up the object. Type the following function into your gnomehunter.nut VScript file:
function CanPickupObject( object )
{
// Is this object the Gnome?
if ( object.GetName().find( "the_gnome" ) )
{
return true
}
return false
}
Note that the object's name ("the_gnome") must match exactly the Name you game the prop_physics_multiplayer entity in Hammer in the previous step.
Now go play Gnome Hunter in C5M2_park, and notice that you are able to pick up the Gnome by looking at it and pressing your USE key!
Attaching pickup and drop events to the gnome
To do interesting things we want to know when someone picks up or drops the gnome. We're going to use the method ConnectOutput on the gnome to wire up the hammer OnPickedUp and OnPhysgunDrop outputs from the gnome entity to functions in our script so we'll get calls when those outputs fire.
In order to do that we'll need to get a handle to the gnome object.
Each time an entity group is spawned the function OnEntityGroupRegistered(), if it exists in your VScript, is called. You can see this in operation by adding the function to your VScript:
function OnEntityGroupRegistered( name, group )
{
printl(" ** OnEntityGroupRegistered called! I'm registering: " + name )
}
If you save your VScript and go play your Mutation in C5M2_park right now, you should see your printl message in the console indicating that the group 'Gnome' is registered.
Since we're eventually going to be registering more entity groups than just the one containing our gnome, we need to check to see if the entity group that was just registered is indeed the one containing our gnome. Just check the name to make sure it is from the 'GnomehunterGnome' entity group:
if( name == "GnomehunterGnome" )
{
// This is the 'Gnome' entity group!
}
IMPORTANT: The .vmf file containing the gnome was saved as gnomehunter_gnome.vmf. When it was exported from Hammer as gnomehunter_gnome_group.nut, the name of the .vmf file was automatically converted to camel case and used as the name of the entity group. Since our .vmf is named gnomehunter_gnome.vmf, the entity group was automatically assigned the name GnomehunterGnome.
This puts us in position to write script code that will run on our gnome entity when it spawns.
We want to attach some code to the actual gnome physics object entity. Since that entity hasn't actually spawned yet - we've only registered the entity group - we need to attach a callback function to the gnome entity. The callback is a function that will be called when the object actually spawns in the map.
We'll use the second parameter that gets passed to OnEntityGroupRegistered, the group, and use it to access the SpawnTables, which contains all the entities that are in the entity group. In the case of the gnome there is only one object -- the one we saved in Hammer -- which was named "the_gnome". We'll add a post-placement callback on the gnome entity. The actual callback will be a function we'll name GnomeSpawnCB.
// found the gnome!
group.GetEntityGroup().SpawnTables[ "the_gnome" ].PostPlaceCB <- GnomeSpawnCB
So now our callback function GnomeSpawnCB() will be called when the gnome physics object entity has actually spawned. Lets add the GnomeSpawnCB() function to our VScript. This function takes two parameters. We'll be using one of them (entity) and ignoring the other (rarity) for now:
function GnomeSpawnCB( entity, rarity )
{
printl(" **** GnomeSpawnCB: I have spawned! My entity is: " + entity + " and my name is: " + entity.GetName() )
}
If you run the game at this point you should see your printl message in the callback printed to the console. The targetname of the entity gets fixed up with a prefix to make it unique, so entity.GetName() will print out a name like "_4441_gnome".
Connecting the outputs to VScript functions
Inside your callback function you have a handle to your entity so lets hook up the ConnectOutput messages
entity.ConnectOutput( "OnPlayerPickup", "PickedUp" ) // fires the function PickedUp when OnPlayerPickup fires
entity.ConnectOutput( "OnPhysGunDrop", "Dropped" ) // fires the function Dropped when OnPhysGunDrop fires
@@@TODO: Clearly define and explain the term SCRIPT SCOPE
Pickedup() and Dropped() are the functions that will be triggered when pickup and drop outputs fire on the gnome. You may be wondering, "What scope will those functions exist in?" They'll exist in the scope of the gnome! The gnome will try to call those functions on itself. They don't exist yet. In fact, the gnome doesn't even have a script scope yet!
We need to do the following:
- create a script scope on the gnome
- get a handle to the script scope on the gnome
- inject some functions into the gnome scope that will call the functions in our Mutation
In the callback function we already have the entity of the gnome, so after connecting the outputs lets create a script scope on the gnome:
// create a script scope on the gnome
entity.ValidateScriptScope()
Next, lets create a local variable to store the script scope of the gnome:
// get the gnome's script scope
local gnomeScope = entity.GetScriptScope()
Now we want to create and add our PickedUp and Dropped functions and inject them into the gnome. Function Injection is a feature of the Squirrel language. In short, we're going to create two functions and attach (or 'inject') them into the script scope of the Gnome entity at runtime:
gnomeScope.PickedUp <- function()
{
printl(" ** Gnome picked up!")
}
gnomeScope.Dropped <- function()
{
printl(" ** Gnome dropped!")
}
Run your Mutation and you should see that your gnome now prints messages to the console when picked up and dropped. Feel the power.
Adding VScript Functions for the Gnome to call
The functions attached to the gnome are being run in the gnome scope. This is fine for letting the gnome itself know when it is picked up or dropped, but we need these functions to call out and notify our Mutation's VScript code that the gnome was picked up or dropped.
Lets add some functions to our Mutation for the gnome functions to call. And for good measure, lets create a new table to hold some data about whether we're carrying the gnome or not.
MutationState is the name of the table that will hold all the state variables for our Mutation. This table is special: the Mutation System will look in your VScript code for a MutationState table on start and if it exists it will merge it into a table called SessionState.
You create your MutationState table and add your data to it but you access it through SessionState. The only time you'll use the name MutationState is when you initially create the table.
- @@@TODO There are some exceptions that we need to fix! Spawned entities need to be able to access SessionState.
The Mutation System will check to see if you want your Mutation to be active on start or not by looking for a StartActive variable. We do want our GnomeHunter code to run on map start (opposed to turning it on midway through playing the level, which is possible) so we want to set:
StartActive = true
Then add a field for custom data for the gnome carry state, and have this field default to 'false' since no player is carrying the gnome at the start of the game:
CarryingGnome = false
The final result will look like this:
MutationState <-
{
StartActive = true
CarryingGnome = false
}
Now create functions that change the CarryingGnome state:
function GnomePickedUp()
{
printl(" ** Mutation: Gnome picked up")
SessionState.CarryingGnome = true // Note that we are accessing our variable through SessionState, not MutationState!
}
function GnomeDropped()
{
printl(" ** Mutation: Gnome dropped")
SessionState.CarryingGnome = false
}
@@@TODO - Create g_SessionScript table
@@@TODO - Figure out MapScript->ModeScript relationship
Finally, lets go back to the gnome functions that we injected in the GnomeSpawnCB() function and change them call our Mutation's functions. The gnome can access the Mutation's scope through the root level table called g_ModeScript so change this pair of functions to:
gnomeScope.PickedUp <- function()
{
g_ModeScript.GnomePickedUp()
}
and
gnomeScope.Dropped <- function()
{
g_ModeScript.GnomeDropped()
}
Now when the Gnome notices that is has been picked up, it will call out to our Mutation's functions so that we can do stuff.
And for fun...
Lets play a sound each time the Gnome is picked up or dropped. First, in the GnomeSpawnCB() callback where you spawn the gnome, precache the sound using PrecacheScriptSound( <soundname> ) on the gnome entity. It is necessary to precache sounds before you can play them:
// precache some sounds - Lilpeanut sounds will do!
entity.PrecacheScriptSound( "Lilpeanut.GALLERY_HIT" )
entity.PrecacheScriptSound( "Lilpeanut.GALLERY_SPAWN" )
And then play one sound inside the PickedUp() function that we injected into the Gnome inside GnomeSpawnCB():
EmitSoundOn( "Lilpeanut.GALLERY_SPAWN", self )
And the other sound inside the Dropped() function that was injected:
EmitSoundOn( "Lilpeanut.GALLERY_HIT", self )
And finally, to make the map a little safer during the Gnome Hunt, lets modify what kind of infected are permitted to spawn. To achieve this we're going to change some of the variables that the Director uses to control the action. The Mutation system will look for these values in a table called MutationOptions. So we need to provide the table. Add these lines to gnomehunter.nut:
MutationOptions <-
{
WanderingZombieDensityModifier = 0
BoomerLimit = 0
ChargerLimit = 0
HunterLimit = 0
JockeyLimit = 0
SpitterLimit = 0
SmokerLimit = 0
MaxSpecials = 0
CommonLimit = 20
MegaMobSize = 30
TankLimit = 0
WitchLimit = 0
}
The net effect of the values in this MutationOptions table is that only common infected will appear during a game of Gnome Hunter. Tanks, Witches and all special infected are prohibited from appearing.
Fire up your Gnome Hunter Mutation and verify that your functions are firing as expected! Pick up and drop the Gnome a couple of times to verify that the sounds we set up for those events are working.
Next: A place to put the gnome.