This article relates to the game "Team Fortress 2." Click here for more information.

TF2 VScript Examples

From Valve Developer Community
Jump to: navigation, search
TF2 VScript Examples
EnglishРусский

Team Fortress 2 This page contains examples of vscripts for Team Fortress 2.

Iterating Through Entities

With awhileloop and a Entities.FindByClassname() function, you can iterate through all entities of a matching classname, based on your arguments.

The first parameter of Entities.FindByClassname() is named 'previous' which accepts a script handle (if an entity inherits 'CBaseEntity' specifically), which verifies if the matching entity it finds has an entity index that's higher than the current one in the 'previous' argument. If it turns out to be not, then its ignored.

local ent = null
while( ent = Entities.FindByClassname(ent, "prop_physics") )
{
   printl(ent)
}

Iterating Through Players

It can be useful to iterate through players only. However, doing this with Entities.FindByClassname() is inefficient as it needs to search every entity. A quirk can be utilised to efficiently iterate players. Each networked entity has an associated 'entity index', which ranges from 0 to MAX_EDICTS. Usually these are unpredictable, however there is 2 groups of entities that have reserved entity indexes: worldspawn and players. Worldspawn is always reserved at entity index 0, and players are reserved from entity index 1 to maxplayers + 1. Using this fact, players can be simply iterated like shown below:

for (local i = 1; i <= Constants.Server.MAX_PLAYERS; i++)
{
    local player = PlayerInstanceFromIndex(i)
    if (player == null) continue
    printl(player)
}

Spawning an Entity

The following code shows to spawn an entity, specifically a rocket. The rocket will be spawned in front of the first available player.

// By ZooL_Smith

local ply = Entities.FindByClassname(null, "player")

local rocket = SpawnEntityFromTable("tf_projectile_rocket", 
{
    // This is a table of keyvalues, which is the same way as keyvalues that are defined in Hammer
    // Key         Value
    basevelocity = ply.EyeAngles().Forward()*250,
    teamnum      = ply.GetTeam(),
    origin       = ply.EyePosition()+ply.EyeAngles().Forward()*32,
    angles       = ply.EyeAngles()
})

rocket.SetOwner(ply) // make it not collide with owner and give proper kill credits

Swapping the !activator's Team

Whenever a script's code gets executed by inputs (e.g.,RunScriptCode/CallScriptFunctionon an entity with an attached entity script), the activator and caller variables will be set to the handles of the input's !activator and !caller respectively, allowing you to access them in code.

We can use that to easily do something to players that walk in a specific trigger, for example.

As for swapping the !activator's team, we need to do both change their team based on which team they are, and then also change the their cosmetic items' team to the their's new team.

First we need to figure on which team the !activator is. For that, we can use the GetTeam() method on the !activator (activator.GetTeam()) to get the number index of their team. You can either use an if function to compare the returned value with the desired team index, or store it in a variable to use it later. The latter in this case may be better to reduce the length of the script. For reference: 0 is Unassigned (no team yet), 1 is Spectator, 2 is Red, 3 is Blu.

To change the !activator's team, we should use the ForceChangeTeam(Int : Team index, Bool : Kill player if game is in Highlander mode + remove their dominations) method on them via activator.ForceChangeTeam(...).

To change their cosmetic items' color, we need to iterate over every tf_wearable entity, and then change its team via SetTeam(Int : Team index) if they are worn by the !activator.

  • To iterate, we need to specify a null variable (e.g. local cosmetic = null), and then pass it into the following while loop:
local cosmetic = null // Assign the "cosmetic" variable to null, will be assigned new values when going over cosmetic items
while (cosmetic = Entities.FindByClassname(cosmetic, "tf_wearable"))
{
    // Do things to individual tf_wearables entities here, the "cosmetic" variable is set to the current tf_wearable entity we are iterating through
}
  • To check if the cosmetic item belongs to the !activator, we can simply compare the value returned by the GetOwner() method when we use it on the tf_wearable (e.g. cosmetic.GetOwner()) against the !activator. Example code:
if (cosmetic.GetOwner() == activator)
{
    // Do things if the cosmetic's wearer is the !activator
}
  • To change the cosmetic's team, we should use the SetTeam(Int : Team index) method on the cosmetic item entity.

Full code with comments:

function ActivatorSwapTeam() // Call this in map via RunScriptCode > ActivatorSwapTeam() or CallScriptFunction > ActivatorSwapTeam on a logic_script that has an Entity Script with this function
{
    // The following snippet checks if an !activator has been specified and if they are a player
    // If either question's answer is no, then don't execute the rest of the function
    if (activator == null || activator.IsPlayer() == false)
    {
        return
    }
    
    // The following snippet compares the !activator's team number (Ranging from 0 to 3) to the ones used by Unassigned (0) and Spectator (1)
    // If they match with either Unassigned or Spectator, we don't execute the rest of the function
    // Used to ignore any potentional spectator !activators
    if (activator.GetTeam() == 0 || activator.GetTeam() == 1)
    {
        return
    }
    
    // The following snippet specifies a local newTeam variable, and then we set it to a team number based off the !activator's current team number
    local newTeam = 0
    if (activator.GetTeam() == 2) // Checks if the !activator's team number is 2 (Red), and sets the newTeam variable to 3 (Blu)
    {   
        newTeam = 3
    } else { // If the !activator's team number is not 2 (Red), sets the newTeam variable to 2 (Blu) instead
        newTeam = 2
    }

    // The following snippet calls the ForceChangeTeam method on the !activator
    // First parameter: Team number to switch to
    // Second parameter: If false, the game will reset the player's dominations and nemesises, and kill them if mp_highlander is on
    activator.ForceChangeTeam(newTeam, true)
    
    local cosmetic = null // Assign the "cosmetic" variable to null, will be assigned new values when going over cosmetic items
    // The following snippet will go over every cosmetic item currently present, and will change its colours to the appropriate team if they are the !activator's
    while (cosmetic = Entities.FindByClassname(cosmetic, "tf_wearable")) // Goes over every cosmetic item, executing code below
    {
        if (cosmetic.GetOwner() == activator) // Checks if the currently iterated cosmetic item's wearer is the !activator
        {
            cosmetic.SetTeam(newTeam) // Sets the team of the cosmetic item to the new team number that we stored in newTeam
        }
    }
}

}}

Listening for Events

Many actions in the game fire events to notify other parts of code that something has happened. For example, when a player dies or when an Engineer's building is sapped. Each event can also hold certain type of data, such as the index of an involved entity.

VScript can catch these events, parse the data and run some code when they happen (known as a 'callback'). The list of available events can be found here: https://wiki.alliedmods.net/Team_Fortress_2_Events

Warning.png Warning: Event callbacks are global and do not get cleared on round restart. Event callbacks should be cleared via ClearGameEventCallbacks() to prevent them from stacking up between rounds.

The following code shows how to listen for the post_inventory_application event, then add uber protection for 2 seconds.

// The event "post_inventory_application" is sent when a player gets a whole new set of items, aka touches a resupply locker / respawn cabinet or spawns in.
function OnGameEvent_post_inventory_application(params)
{
	local player = GetPlayerFromUserID(params.userid)

	// add uber protection to the player for 2 seconds.
	player.AddCondEx(52, 2.0, null)
}

__CollectGameEventCallbacks(this)

Setting up a Boss Health Bar

The boss bar that appears while any bosses are active (such as MONOCULUS!) is handled by the monster_resource entity, which conveniently exists in the map normally. You can do Entities.FindByClassname(null, "monster_resource") to get the monster_resource entity most of the time, but it'd be convenient to store it into a variable instead.

In order to modify the health bar's percentage, it'd be useful to first know about NetProps:

  • NetProps are network properties of an entity, which are server-side only.
  • They can be accessed and changed by the NetProps class methods.

The monster_resource entity has a m_iBossHealthPercentageByte NetProp, which determines the percentage state of the health bar based on a byte value - its value must be between 0 and 255, where 0 is 0%, and 255 is 100%

The following code will add a health bar with 25% health after executed.

local healthBar = Entities.FindByClassname(null, "monster_resource") // Get the health bar entity.
if (healthBar && healthBar.IsValid) { // Check if the health bar entity exists and if it is valid, just in case to prevent errors or potential crashes.
    // The following line will update the health bar's percentage to 25% by changing its NetProp.
    // Do note that because it is a byte (0 - 255), we need to multiply our percentage by 255.
    NetProps.SetPropInt(healthBar, "m_iBossHealthPercentageByte", 0.25 * 255)
}

Disabling HUD Elements

Some HUD elements can be hidden away by placing a bit value into one of these netprops:

NetProps.SetPropInt(player, "m_Local.m_iHideHUD", value )
NetProps.SetPropInt(player, "localdata.m_Local.m_iHideHUD", value )
Note.png Note: Both function the same.
Note.png Note: The values start at 0.

The bit's functions are found on the TF2 Script Functions.

For example:

local HideHudValue = NetProps.GetPropInt(player, "m_Local.m_iHideHUD")
NetProps.SetPropInt(player, "m_Local.m_iHideHUD", HideHudValue | (
                                                  Constants.HideHUD.HIDEHUD_CROSSHAIR |
                                                  Constants.HideHUD.HIDEHUD_HEALTH |
                                                  Constants.HideHUD.HIDEHUD_WEAPONSELECTION)
                    ) // adds bits
NetProps.SetPropInt(player, "m_Local.m_iHideHUD", HideHudValue & ~(
                                                  Constants.HideHUD.HIDEHUD_CROSSHAIR |
                                                  Constants.HideHUD.HIDEHUD_HEALTH |
                                                  Constants.HideHUD.HIDEHUD_WEAPONSELECTION)
                    ) // removes bits

Line 2 would add bits to hide the crosshair, health, and disabling weapon switching from the player.

Line 3 would remove bits to show the crosshair, health, and re-enable weapon switching for the player.

Creating Bots That Use the Navmesh

The following code show an example of creating a primitive bot that navigates using the navmesh. When this script is executed, a bot will be spawned at the host player's crosshair. This bot will simply follow the player and path-find along the map layout. The code is heavily commented, and debug visualizations are available to see the path-finding algorithm in action.

Warning.png Warning: Making bots is not an easy task. This code shows how much work is required just to setup a simple bot!
// Constrains an angle into [-180, 180] range
function NormalizeAngle(target)
{
	target %= 360.0;
	if (target > 180.0)
		target -= 360.0;
	else if (target < -180.0)
		target += 360.0;
	return target;
}

// Approaches an angle at a given speed
function ApproachAngle(target, value, speed)
{
	target = NormalizeAngle(target);
	value = NormalizeAngle(value);
	local delta = NormalizeAngle(target - value);
	if (delta > speed)
		return value + speed;
	else if (delta < -speed)
		return value - speed;
	return value;
}

// Converts a vector direction into angles
function VectorAngles(forward)
{
	local yaw, pitch;
	if ( forward.y == 0.0 && forward.x == 0.0 )
	{
		yaw = 0.0;
		if (forward.z > 0.0)
			pitch = 270.0;
		else
			pitch = 90.0;
	}
	else
	{
		yaw = (atan2(forward.y, forward.x) * 180.0 / Constants.Math.Pi);
		if (yaw < 0.0)
			yaw += 360.0;
		pitch = (atan2(-forward.z, forward.Length2D()) * 180.0 / Constants.Math.Pi);
		if (pitch < 0.0)
			pitch += 360.0;
	}

	return QAngle(pitch, yaw, 0.0);
}

// Coordinate which is part of a path
class PathPoint
{
	constructor(_area, _pos, _how)
	{
		area = _area;
		pos = _pos;
		how = _how;
	}

	area = null;	// Which area does this point belong to?
	pos = null;		// Coordinates of the point
	how = null;		// Type of traversal. See Constants.ENavTraverseType
}

// The big boy that handles all our behavior
class Bot
{
	function constructor(bot_ent, follow_ent)
	{
		bot = bot_ent;

		move_speed = 230.0;
		turn_rate = 5.0;
		search_dist_z = 128.0;
		search_dist_nearest = 128.0;

		path = [];
		path_index = 0;
		path_reach_dist = 16.0;
		path_follow_ent = follow_ent;
		path_follow_ent_dist = 50.0;
		path_target_pos = follow_ent.GetOrigin();
		path_update_time_next = Time();
		path_update_time_delay = 0.2;
		path_update_force = true;
		area_list = {};

		seq_idle = bot_ent.LookupSequence("Stand_MELEE");
		seq_run = bot_ent.LookupSequence("Run_MELEE");
		pose_move_x = bot_ent.LookupPoseParameter("move_x");

		debug = true;

		// Add behavior that will run every tick
		AddThinkToEnt(bot_ent, "BotThink");
	}

	function UpdatePath()
	{
		// Clear out the path first
		ResetPath();

		// If there is a follow entity specified, then the bot will pathfind to the entity
		if (path_follow_ent && path_follow_ent.IsValid())
			path_target_pos = path_follow_ent.GetOrigin();

		// Pathfind from the bot's position to the target position
		local pos_start = bot.GetOrigin();
		local pos_end = path_target_pos;

		local area_start = NavMesh.GetNavArea(pos_start, search_dist_z);
		local area_end = NavMesh.GetNavArea(pos_end, search_dist_z);

		// If either area was not found, try use the closest one
		if (area_start == null)
			area_start = NavMesh.GetNearestNavArea(pos_start, search_dist_nearest, false, true);
		if (area_end == null)
			area_end = NavMesh.GetNearestNavArea(pos_end, search_dist_nearest, false, true);

		// If either area is still missing, then bot can't progress
		if (area_start == null || area_end == null)
			return false;

		// If the start and end area is the same, one path point is enough and all the expensive path building can be skipped
		if (area_start == area_end)
		{
			path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES));
			return true;
		}

		// Build list of areas required to get from the start to the end
		if (!NavMesh.GetNavAreasFromBuildPath(area_start, area_end, pos_end, 0.0, Constants.ETFTeam.TEAM_ANY, false, area_list))
			return false;

		// No areas found? Uh oh
		if (area_list.len() == 0)
			return false;

		// Now build points using the list of areas, which the bot will then follow
		local area_target = area_list["area0"];
		local area = area_target;
		local area_count = area_list.len();

		// Iterate through the list of areas in order and initialize points
		for (local i = 0; i < area_count && area != null; i++)
		{
			path.append(PathPoint(area, area.GetCenter(), area.GetParentHow()));
			area = area.GetParent(); // Advances to the next connected area
		}

		// Reverse the list of path points as the area list is connected backwards
		path.reverse();

		// Now compute accurate path points, using adjacent points + direction data from nav
		local path_first = path[0];
		local path_count = path.len();

		// First point is simply our current position
		path_first.pos = bot.GetOrigin();
		path_first.how = Constants.ENavTraverseType.NUM_TRAVERSE_TYPES; // No direction specified

		for (local i = 1; i < path_count; i++)
		{
			local path_from = path[i - 1];
			local path_to = path[i];

			// Computes closest point within the "portal" between adjacent areas
			path_to.pos = path_from.area.ComputeClosestPointInPortal(path_to.area, path_to.how, path_from.pos);
		}

		// Add a final point so the bot can precisely move towards the end point when it reaches the final area
		path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES));
	}

	function AdvancePath()
	{
		// Check for valid path first
		local path_len = path.len();
		if (path_len == 0)
			return false;

		local path_pos = path[path_index].pos;
		local bot_pos = bot.GetOrigin();

		// Are we close enough to the path point to consider it as 'reached'?
		if ((path_pos - bot_pos).Length2D() < path_reach_dist)
		{
			// Start moving to the next point
			path_index++;
			if (path_index >= path_len)
			{
				// End of the line!
				ResetPath();
				return false;
			}
		}

		return true;
	}

	function ResetPath()
	{
		area_list.clear();
		path.clear();
		path_index = 0;
	}

	function Move()
	{
		// Recompute the path if forced to do so
		if (path_update_force)
		{
			UpdatePath();
			path_update_force = false;
		}
		// Recompute path to our target if present
		else if (path_follow_ent && path_follow_ent.IsValid())
		{
			// Is it time to re-compute the path?
			local time = Time();
			if (path_update_time_next < time)
			{
				// Check if target has moved far away enough
				if ((path_target_pos - path_follow_ent.GetOrigin()).Length() > path_follow_ent_dist)
				{
					UpdatePath();
					// Don't recompute again for a moment
					path_update_time_next = time + path_update_time_delay;
				}
			}
		}

		// Check and advance up our path
		if (AdvancePath())
		{
			local path_pos = path[path_index].pos;
			local bot_pos = bot.GetOrigin();

			// Direction towards path point
			local move_dir = (path_pos - bot_pos);
			move_dir.Norm();

			// Convert direction into angle form
			local move_ang = VectorAngles(move_dir);

			// Approach new desired angle but only on the Y axis
			local bot_ang = bot.GetAbsAngles()
			move_ang.x = bot_ang.x;
			move_ang.y = ApproachAngle(move_ang.y, bot_ang.y, turn_rate);
			move_ang.z = bot_ang.z;

			// Set our new position and angles
			// Velocity is calculated from direction times speed, and converted from per-second to per-tick time
			bot.SetAbsOrigin(bot_pos + (move_dir * move_speed * FrameTime()));
			bot.SetAbsAngles(move_ang);

			return true;
		}

		return false;
	}

	function Update()
	{
		// Try moving
		if (Move())
		{
			// Moving, set the run animation
			if (bot.GetSequence() != seq_run)
			{
				bot.SetSequence(seq_run);
				bot.SetPoseParameter(pose_move_x, 1.0); // Set the move_x pose to max weight
			}
		}
		else
		{
			// Not moving, set the idle animation
			if (bot.GetSequence() != seq_idle)
			{
				bot.SetSequence(seq_idle);
				bot.SetPoseParameter(pose_move_x, 0.0); // Clear the move_x pose
			}
		}

		// Replay animation if it has finished
		if (bot.GetCycle() > 0.99)
			bot.SetCycle(0.0);

		// Run animations
		bot.StudioFrameAdvance();
		bot.DispatchAnimEvents(bot);

		// Visualize current path in debug mode
		if (debug)
		{
			// Stay around for 1 tick
			// Debugoverlays are created on 1st tick but start rendering on 2nd tick, hence this must be doubled
			local frame_time = FrameTime() * 2.0;

			// Draw connected path points
			local path_len = path.len();
			if (path_len > 0)
			{
				local path_start_index = path_index;
				if (path_start_index == 0)
					path_start_index++;

				for (local i = path_start_index; i < path_len; i++)
				{
					DebugDrawLine(path[i - 1].pos, path[i].pos, 0, 255, 0, true, frame_time);
				}
			}

			// Draw areas from built path
			foreach (name, area in area_list)
			{
				area.DebugDrawFilled(255, 0, 0, 30, frame_time, true, 0.0);
				DebugDrawText(area.GetCenter(), name, false, frame_time);
			}
		}

		return 0.0; // Think again next frame
	}

	function OnKilled()
	{
		// Change life state to "dying"
		// The bot won't take any more damage, and sentries will stop targeting it
		NetProps.SetPropInt(bot, "m_lifeState", 1);
		// Reset health, preventing the default base_boss death behavior
		bot.SetHealth(bot.GetMaxHealth() * 20);
		// Custom death behavior can be added here
		// For this example, turn into a ragdoll with the saved damage force
		bot.BecomeRagdollOnClient(damage_force);
	}

	bot = null;						// The bot entity we belong to

	move_speed = null;				// How fast to move
	turn_rate = null;				// How fast to turn
	search_dist_z = null;			// Maximum distance to look for a nav area downwards
	search_dist_nearest = null; 	// Maximum distance to look for any nearby nav area

	path = null;					// List of BotPathPoints
	path_index = null;				// Current path point bot is at, -1 if none
	path_reach_dist = null;			// Distance to a path point to be considered as 'reached'
	path_follow_ent = null;			// What entity to move towards
	path_follow_ent_dist = null;	// Maximum distance after which the path is recomputed
									// if follow entity's current position is too far from our target position
	path_target_pos = null;			// Position where bot wants to navigate to
	path_update_time_next = null;	// Timer for when to update path again
	path_update_time_delay = null;  // Seconds to wait before trying to attempt to update path again
	path_update_force = null;		// Force path recomputation on the next tick
	area_list = null;				// List of areas built in path

	seq_idle = null;				// Animation to use when idle
	seq_run = null;					// Animation to use when running
	pose_move_x = null;				// Pose parameter to set for running animation

	damage_force = null;			// Damage force from the bot's last OnTakeDamage event

	debug = null;					// When true, debug visualization is enabled

}

function BotThink()
{
	// Let the bot class handle all the work
	return self.GetScriptScope().my_bot.Update();
}

function BotCreate()
{
	// Find point where player is looking
	local player = GetListenServerHost();
	local trace =
	{
		start = player.EyePosition(),
		end = player.EyePosition() + (player.EyeAngles().Forward() * 32768.0),
		ignore = player
	};

	if (!TraceLineEx(trace))
	{
		printl("Invalid bot spawn location");
		return null;
	}

	// Spawn bot at the end point
	local bot = SpawnEntityFromTable("base_boss",
	{
		targetname = "bot",
		origin = trace.pos,
		model = "models/bots/heavy/bot_heavy.mdl",
		playbackrate = 1.0, // Required for animations to be simulated
		health = 300
	});

	// Add scope to the entity
	bot.ValidateScriptScope();
	// Append custom bot class and initialize its behavior
	bot.GetScriptScope().my_bot <- Bot(bot, player);

	return bot;
}

function OnScriptHook_OnTakeDamage(params)
{
	local ent = params.const_entity;
	local inf = params.inflictor;
    if (ent.IsPlayer() && HasBotScript(inf) && params.damage_type == 1)
    {
		// Don't crush the player if a bot pushes them into a wall
        params.damage = 0;
    }
	if (ent.GetClassname() == "base_boss" && HasBotScript(ent))
	{
		// Save the damage force into the bot's data
		ent.GetScriptScope().my_bot.damage_force = params.damage_force;
	}
}

function OnGameEvent_npc_hurt(params)
{
	local ent = EntIndexToHScript(params.entindex);
	if (HasBotScript(ent))
	{
		// Check if a bot is about to die
		if ((ent.GetHealth() - params.damageamount) <= 0)
		{
			// Run the bot's OnKilled function
			ent.GetScriptScope().my_bot.OnKilled();
		}
	}
}

function HasBotScript(ent)
{
	// Return true if this entity has the my_bot script scope
	return (ent.GetScriptScope() != null && ent.GetScriptScope().my_bot != null);
}

__CollectGameEventCallbacks(this)
BotCreate();

See Also