The VScript Book

Chapter 3.3: Hooks & Think Functions

While running code via I/O is useful, VScript offers more direct and powerful ways to make entities "come alive." These are Hooks and Think Functions, which allow your scripts to run automatically based on game events or at regular time intervals.

Input Hooks

Input Hooks are an incredibly powerful feature. If an entity's script has a function named Input<InputName>, that function will run whenever the entity receives that input. If the function return false, the original input is cancelled!

Example: A door that requires 3 button presses to unlock.

Entity: a prop_door_rotating named "secure_door". Script attached to the door:

// Create a counter in the door's scope.
unlockPresses <- 3

// This is an Input Hook for the "Unlock" input.
function InputUnlock() {
    unlockPresses-- // Decrement the counter
    printl("Unlock signal received. " + unlockPresses + " more required.")
    
    if (unlockPresses <= 0) {
        printl("Door unlocked!")
        return true // Allow the original "Unlock" input to happen.
    }
    else {
        // We still need more presses.
        return false // CANCEL the original "Unlock" input. The door will not unlock.
    }
}

The Precache Hook: Loading Resources

Some resources—like sounds, models, or particle effects—must be precached before they can be used. Precaching loads the resource into memory when the map starts, preventing stuttering or crashes when the resource is first accessed during gameplay.

The Two Precache Hooks

VScript gives you two ways to define a precache hook in your entity script:

// Option 1: Standard hook (runs if DispatchPrecache is not defined)
function Precache() {
    self.PrecacheSoundScript("Portal.room1_TickTock")
    printl("Precache() was called")
}

// Option 2: Dispatch version (has PRIORITY over Precache)
function DispatchPrecache() {
    self.PrecacheSoundScript("Portal.room1_TickTock")
    printl("DispatchPrecache() was called instead")
}

Priority Rule: If both Precache() and DispatchPrecache() are defined in the same script, only DispatchPrecache() will run. The standard Precache() is ignored. This dispatch pattern is an internal VScript system for chaining multiple precache functions, but for most use cases, simply defining Precache() is sufficient.

When Does It Run?

The precache hook is called during map load, before any entities spawn. This is the only safe time to precache resources.

// Example: Precaching multiple resources
function Precache() {
    // Precache sounds
    self.PrecacheSoundScript("Weapon_Portalgun.fire_blue")
    self.PrecacheSoundScript("Weapon_Portalgun.fire_red")
    
    // Note: Model precaching is usually automatic when entities
    // reference them, but sounds must be explicitly precached.
    
    printl("Resources precached successfully.")
}

The Post-Spawn Hook: Initialization

After an entity spawns and its script executes, you often need to perform initialization—like finding other entities, connecting outputs, or setting up variables. This is the job of the post-spawn hook.

The Two Post-Spawn Hooks

// Option 1: Standard hook (runs if DispatchOnPostSpawn is not defined)
function OnPostSpawn() {
    printl("OnPostSpawn() - Entity is fully ready")
    
    // Safe to find other entities now
    targetDoor <- Entities.FindByName(null, "exit_door")
}

// Option 2: Dispatch version (has PRIORITY over OnPostSpawn)
function DispatchOnPostSpawn() {
    printl("DispatchOnPostSpawn() - Has priority")
    
    // This will run instead if both are defined
    targetDoor <- Entities.FindByName(null, "exit_door")
}

Priority Rule: Just like with precaching, if both OnPostSpawn() and DispatchOnPostSpawn() are defined, only DispatchOnPostSpawn() will run.

OnPostSpawn vs. Top-Level Code

What's the difference between writing code at the top level of your script versus inside OnPostSpawn()?

// Top-level code: Runs IMMEDIATELY when the script loads
printl("This runs first, during script execution")
myVariable <- 100

// OnPostSpawn: Runs AFTER the entity and other entities have spawned
function OnPostSpawn() {
    printl("This runs later, after the entity is fully initialized")
    
    // It's safer to find other entities here, as they are guaranteed to exist
    local door = Entities.FindByName(null, "exit_door")
}

Best Practice: Use top-level code to define variables and functions. Use OnPostSpawn() for initialization that depends on other entities or the game state.

Hook Execution Order Summary

  1. Map Load Begins
  2. Precache Hooks Run (DispatchPrecache OR Precache)
  3. Entities Spawn
  4. Entity Scripts Execute (top-level code runs)
  5. Post-Spawn Hooks Run (DispatchOnPostSpawn OR OnPostSpawn)
  6. Map Is Fully Loaded

Think Functions

A Think Function is a function that you can make an entity run repeatedly, like a heartbeat. This is essential for any logic that needs to constantly check something—for example, a security camera constantly watching for a player, or poison goo that needs to damage a player every second they stand in it.

You assign a Think Function in Hammer by setting the entity's thinkfunction keyvalue to the name of the function in its script. By default, the function will run every 0.1 seconds.

To change the time until the next "think," you simply return a number (a float) from the function. This number is the delay in seconds. Returning a negative number will stop the think function from running again.

Example: A security camera that follows the player.

Entity: A prop_dynamic with a camera model, with its thinkfunction keyvalue set to `FollowPlayer`.

// This function will run automatically every time the camera "thinks".
function FollowPlayer() {
    local player = GetPlayer() // Get a handle to the player.
    if (!player) {
        return 1.0 // If no player, wait a full second and check again.
    }

    local selfPos = self.GetOrigin()
    local playerPos = player.GetOrigin()

    // Calculate the direction from the camera to the player.
    local directionVector = playerPos - selfPos

    // "Magic" custom function, just for example
    local newAngles = VectorToAngles(directionVector) 

    // Set the camera's angles to look at the player.
    self.SetAngles(newAngles.x, newAngles.y, newAngles.z)

    // Tell the game to run this function again in 0.05 seconds for smooth tracking.
    return 0.05
}

// Example of VectorToAngles func
function VectorToAngles(forward) {
    local yaw = atan2(forward.y, forward.x) * 180 / 3.14
    local pitch = atan2(-forward.z, sqrt(forward.x*forward.x + forward.y*forward.y)) * 180 / 3.14
    return Vector(pitch, yaw, 0)
}