Skip to main content

Bot behavior trees

A bot behavior tree is a KV3 file that describes what a Counter-Strike 2 iconCounter-Strike 2 bot does, as a tree of small nodes the bot evaluates every tick, instead of the hardcoded classic bot AI. Deathmatch, Arms Race, Rush and the new player training use them, and any server or map can point bots at its own tree.

info

This page covers how a tree is chosen, the file format, how the nodes run and every node type the game accepts. Navigation meshes, which bots need to move at all, and the classic bot AI are not covered here. How the Rush tree extends the default one is on the Rush game mode page.

Choosing a tree​

The game picks a tree when a bot spawns. When mp_bot_ai_bt is not empty, every bot uses that file, otherwise bots run the classic bot AI.

The official modes set the cvar in their game mode configs:

Configmp_bot_ai_bt
gamemode_deathmatch.cfg, gamemode_dm_freeforall.cfgscripts/ai/deathmatch/bt_default.kv3
gamemode_armsrace.cfgscripts/ai/armsrace/bt_default.kv3
gamemode_rush.cfgscripts/ai/rush/bt_default.kv3
gamemode_new_user_training.cfgscripts/ai/practice/newplayer_dust2_01.kv3

All of these trees, and the modules they share under scripts/ai/modules/, ship as plain text inside game/csgo/pak01_dir.vpk. Source2 Viewer iconSource2 Viewer can extract them, and they are the best examples of complete trees.

Loaded files are cached. After editing a tree, mp_bot_ai_bt_clear_cache clears the cache, and bots pick up the new version the next time they spawn.

Using a custom tree​

  1. Create the tree as a .kv3 file under scripts/ai/, for example scripts/ai/mymode/bt_default.kv3.
  2. Set mp_bot_ai_bt to its path, in a config or from a cs_script with Instance.ServerCommand.
  3. Run mp_bot_ai_bt_clear_cache and restart the round so the bots respawn.
  4. With developer set to 1, every bot prints [AI BT]: Loaded behavior tree '<file>' when it spawns. Problems with the file print [AI BT] warnings regardless of developer.

Tree files load through the game's GAME search path, the same one other game files use, so a workshop addon can ship its trees in its own scripts/ai/ folder.

Enum values​

KeyValues
entity_type_filterALL, PLAYERS, HUMAN_PLAYERS, NOISE, DAMAGE, AREA_DAMAGE, CLASSNAME, GRENADE
team_filter, attacker_filterANY, CT, TERRORIST, SAME, OPPOSITE, ENEMY
teamANY, CT, TERRORIST, OPPOSITE
team_restrictionANY, CT, T
shape typesensor_shape_fov, sensor_shape_sphere (with radius)
movement_typeBT_ACTION_MOVETO_WALK, BT_ACTION_MOVETO_RUN
route_typeBT_ACTION_MOVETO_FASTEST_ROUTE, BT_ACTION_MOVETO_SAFEST_ROUTE
slotRIFLE, PISTOL, KNIFE, GRENADES, C4
typesFLASH, FIRE, EXPLOSIVE, DECOY, SMOKE
operation_typeBT_DECORATOR_TAG_ENTITY_SET, BT_DECORATOR_TAG_ENTITY_CLEAR
check_typeBT_DECORATOR_TAG_THRESHOLD_AT_LEAST, BT_DECORATOR_TAG_THRESHOLD_AT_MOST
weaponBEST, or a weapon classname such as weapon_knife

Node reference​

The keys listed are all the keys each node reads.

There are 36 nodes which exist but are never used in official trees, so their exact behavior is unconfirmed.

A few keys in the official trees are not read by the game at all and do nothing: acquire_only on action_aim, ratio on action_pull_trigger and distance_threshold on decorator_picker_blocked_by_smoke.

Key types:

TypeMeaning
bool0 or 1.
int, floatA literal number.
numberA literal number, a cvar or a blackboard key, see Numbers.
positionSee Positions and entity names.
stringA blackboard key name or a literal string, see Strings.
enumOne of the names listed under Enum values.
node, node[]A child node, or an array of them.
[]Marks an array of the type before it.

Composites​

NodeKeysIn official trees
parallelsucceed_after_first(bool), children(node[])Yes
selectorchildren(node[])Yes
sequencerchildren(node[])Yes

Subtree​

NodeKeysIn official trees
subtreefile(string), name(string), params({ key(string), value(int or string) }[])Yes

Decorators​

NodeKeysIn official trees
decorator_bot_servicememory_to_expire({ key(string), time(float), distance(float), domain(string) }[]), tagged_entities_to_expire(string[]), input_chatter_enemies(string), basic_chatter_enable(bool), chatter_outnumbered_threshold(int), child(node)Yes
decorator_buy_serviceoutput(string), child(node)Yes
decorator_dec_global_counterinput_name(string), child(node)No
decorator_find_utility_stratdistance_threshold(float), input_location(position), output_location(string), output_angles(string), output_weapon(string), utilities({ x(float), y(float), z(float), ang_pitch(float), ang_yaw(float), ang_roll(float), weapon(string) }[]), child(node)No
decorator_hiding_spot_servicedomain(string), output_hiding_spot(string), expiration_time(float), distance_threshold(float), child(node)Yes
decorator_invertchild(node)No
decorator_maybechance(float), child(node)Yes
decorator_memoryinput(string), output_domain(string), output(string), child(node)Yes
decorator_need_healinghealth_threshold(int), child(node)Yes
decorator_picker_blocked_by_smokeinput(string), input_domain(string), output(string), output_domain(string), negated(bool), child(node)Yes
decorator_picker_dedupinput(string), input_domain(string), output(string), output_domain(string), against(string), distance_threshold(float), negated(bool), child(node)Yes
decorator_picker_grenade_typeinput(string), input_domain(string), output(string), output_domain(string), types(enum[]), negated(bool), child(node)Yes
decorator_picker_max_scoreinput(string), input_domain(string), output(string), output_domain(string), negated(bool), child(node)Yes
decorator_picker_nearbyinput(string), input_domain(string), output(string), output_domain(string), cutoff_distance(float), negated(bool), child(node)Yes
decorator_picker_random_by_distanceinput(string), input_domain(string), output(string), output_domain(string), distance_min(float), distance_max(float), negated(bool), child(node)Yes
decorator_picker_reaction_timeinput(string), input_domain(string), output(string), output_domain(string), negated(bool), child(node)Yes
decorator_picker_visibleinput(string), input_domain(string), output(string), output_domain(string), check_fov(bool), negated(bool), child(node)Yes
decorator_picker_weight_as_distanceinput(string), input_domain(string), output(string), output_domain(string), negated(bool), child(node)Yes
decorator_random_approach_pointoutput(string), child(node)Yes
decorator_random_intmin(int), max(int), output(string), child(node)Yes
decorator_ranker_distinput(string), child(node)Yes
decorator_removeinput_domain(string), input(string), remove(string), child(node)Yes
decorator_remove_keyinput(string), child(node)Yes
decorator_repeatamount(int), child(node)Yes
decorator_route_serviceconfig({ routes(waypoint[][]), strategies(int[][]) }), output_waypoint(string), output_waypoint_name(string), output_domain(string), child(node)No
decorator_run_oncedomain(string), max_attempts(int), child(node)Yes
decorator_sensoroutput(string), class_name(string), priority(int), orphan_only(bool), append_items(bool), entity_type_filter(enum), team_filter(enum), attacker_filter(enum), shape({ type(enum), radius(float) }), child(node)Yes
decorator_set_barrierinput_domain(string), input_name(string), input_location(string), child(node)No
decorator_set_reaction_timeinput(string), child(node)Yes
decorator_succeednegated(bool), child(node)Yes
decorator_tag_entityinput(string), output(string), expiration_time(float), operation_type(enum), child(node)Yes
decorator_tag_thresholdentity_input(string), tagged_entities_input(string), amount(int), check_type(enum), child(node)Yes
decorator_token_serviceconfig({ tokens(string[]), assignments(int[][]), team_restriction(enum[]) }), output_token_name(string), output_token_domain(string), domain(string), child(node)Yes
decorator_try_lockdomain(string), child(node)Yes
decorator_wait_successtimeout(float), child(node)No

Conditions​

NodeKeysIn official trees
condition_barrierinput_domain(string), input_name(string), radius(float), negated(bool), child(node)No
condition_distance_lessinput(string), distance_threshold_min(float), distance_threshold_max(float), negated(bool), child(node)No
condition_has_parachutenegated(bool), child(node)No
condition_inactiveinput({ domain(string), key(string) }[]), round_start_threshold_seconds(float), sensor_inactivity_threshold_seconds(float), negated(bool), child(node)No
condition_is_airbornenegated(bool), child(node)No
condition_is_at_bomb_sitenegated(bool), child(node)Yes
condition_is_emptyinput(string), global(bool), negated(bool), child(node)Yes
condition_is_equalsource(string), destination(int or string), negated(bool), child(node)Yes
condition_is_greatersource(string), destination(int or string), negated(bool), child(node)No
condition_is_greater_equalsource(string), destination(int or string), negated(bool), child(node)Yes
condition_is_inv_slot_emptyslot(enum), negated(bool), child(node)Yes
condition_is_lesssource(string), destination(int or string), negated(bool), child(node)No
condition_is_less_equalsource(string), destination(int or string), negated(bool), child(node)Yes
condition_is_reloadingnegated(bool), child(node)No
condition_is_weapon_equippedweapon(string), negated(bool), child(node)Yes
condition_is_weapon_suitableinput(string), weapon(string), negated(bool), child(node)No
condition_out_of_ammonegated(bool), child(node)Yes
condition_owns_itemitem(string), items_one_of(string[]), negated(bool), child(node)Yes

Actions​

NodeKeysIn official trees
action_acquire_itemsitems(string[]), remove_all_items(bool)Yes
action_aiminput(position), ready(string)Yes
action_aim_projectileinput(position), output(string)No
action_attackinput(string), ready(string), output(string)Yes
action_buy-Yes
action_choose_bomb_site_areainput(number), output(string)Yes
action_choose_random_waypointinput(string), output(string)Yes
action_choose_random_waypoint_within_radiusorigin(position), radius(number), output(string)No
action_choose_team_spawn_areaoutput(string), team(enum)Yes
action_combat_positioninginput(string), is_attacking(string)Yes
action_commit_suicide-No
action_compare_global_counterinput_name(string), input_value(int)No
action_coordinated_buyid_no_purchase(string), purchases({ items(string[]), id(string) }[]), team_filter(enum), save_threshold(int)No
action_crouch-No
action_custom_buyitem_aliases(string[])No
action_drop_active_weapon-No
action_equip_itemitem(string), items_one_of(string[])Yes
action_equip_weaponweapon(string)Yes
action_flee_area_damageinput(string), output(string), max_search_range(float), threat_min_keep_distance(float)Yes
action_hideoutput(string), max_range(float)No
action_inspect_current_weapon-No
action_jump-No
action_look_atinput_angles(string), input_location(string)Yes
action_move_todestination(position), hiding_spot(number), threat(string), auto_look_adjust(bool), damaging_areas_penalty_cost(float), arrival_epsilon(float), additional_arrival_epsilon_2d(float), hiding_spot_check_distance_threshold(float), nearest_area_distance_threshold(float), movement_type(enum), route_type(enum)Yes
action_parachute_positioning-No
action_pull_trigger-Yes
action_reload-No
action_sayphrase(string), high_priority(bool)No
action_secondary_attack-No
action_select_areas_within_radiusinput(position), radius(number), output(string)No
action_set_global_counterinput_name(string), input_value(int)No
action_set_global_flagname(string), expiration_time_min(float), expiration_time_max(float)Yes
action_set_value_floatkey(string), value(number)No
action_set_value_vectorkey(string), value(position)No
action_standup-No
action_teleportdestination(position)No
action_use-Yes
action_waitwait_time_min(number), wait_time_max(number), wait_modulo_tick_mod(number), wait_modulo_tick_add(number)Yes

Tree files​

A tree file holds the path of a config file and the root node:

<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:generic:version{7412167c-06e9-4698-aff2-e63eb59037e7} -->
{
config = "scripts/ai/deathmatch/bt_config.kv3"
root =
{
type = "decorator_repeat"
child =
{
type = "sequencer"
children =
[
{
type = "action_move_to"
destination = "bot_target"
movement_type = "BT_ACTION_MOVETO_RUN"
route_type = "BT_ACTION_MOVETO_FASTEST_ROUTE"
},
{
type = "action_look_at"
input_location = "bot_target"
},
{
type = "action_wait"
wait_time_min = 2
wait_time_max = 3
}
]
}
}
}

This tree makes every bot run to the entity named bot_target, look at it and wait, over and over. Both config and root are required.

Every node is a table with a type. Decorators and conditions wrap a single child, composites hold a children array. Key names are case-insensitive, and // comments are allowed.

Config file​

The config file tunes how well bots aim and react: aim acquisition and tracking speed, look-around behavior, reaction time, crouch and dodge chances, and burst timings per weapon type in an attack array.

It has a required default block and one block per skill level, low, fair, normal, tough, hard, very_hard, expert and elite. A level missing a key falls back to default. The four official copies are identical.

Each attack row holds 5 numbers: burst duration, its deviation, cadence, cooldown and cooldown deviation. The game requires exactly one row per weapon type, 13 in the current build, in the order of the comments in the official files: knife, pistol, submachine gun, rifle, shotgun, sniper rifle, machine gun, C4, taser, grenade, equipment, stackable item and unknown. A block with a different number of rows is rejected with [AI BT]: Config node 'attack' must contain 13 entries, and the whole tree fails to load.

Subtree files​

A subtree node runs another file, so common behavior can live in one place. Both file and name are required:

{
type = "subtree"
file = "scripts/ai/modules/bt_attack.kv3"
name = "Attack"
}

A subtree file holds a single node as its top level table, without the config and root wrapper.

How a tree runs​

Each node reports one of three results: success, failure or running, the last meaning it needs more ticks to finish.

Two passes per tick​

Every tick the tree is walked twice. The first pass checks the tree from the root down, deciding which branches can run. If the root fails this check, nothing runs that tick. Otherwise a second pass runs the chosen branches, which is where actions move, aim and shoot.

Because the check pass repeats every tick, conditions are re-checked while their branch is running, and a selector can switch to a more important branch at any time.

The example tree over time​

A tree is not a script that runs once. The game evaluates it every tick, about 64 times a second, and each node remembers where it left off. The example tree plays out like this:

  1. On the first tick, the sequencer starts action_move_to, which looks up bot_target and moves the bot one tick toward it. It reports running, and so do the nodes above it.
  2. Every following tick, the game goes straight back to action_move_to, which moves the bot another tick. This is one continuous move, like a player holding a key, not a new decision every tick.
  3. Once the bot arrives, action_move_to succeeds and the sequencer moves on to action_look_at, which turns the bot toward the target and succeeds.
  4. action_wait picks an end time 2 to 3 seconds ahead when it starts, then reports running every tick until that time has passed.
  5. With all three children done, the sequencer succeeds, and decorator_repeat starts it again from the top.

Nodes do not move the bot themselves, they drive the regular bot code underneath. Button actions such as action_use or action_reload press that button for the tick, while action_move_to, action_aim and action_attack steer the bot's own pathfinding, aiming and shooting until they finish.

Sequencer​

A sequencer runs its children in order. It fails as soon as one fails, reports running while it moves on to the next child, and succeeds once all of them have succeeded.

The check pass only looks at the child the sequencer is currently on, so a condition earlier in the sequence is not checked again once it has passed.

Selector​

A selector picks the first child, in order, that passes the check, and runs it. It reports whatever that child reports.

The check repeats from the first child every tick. When an earlier child starts passing, the selector stops the running child and switches. This makes a selector of guarded branches the tree's priority list, and the official trees use one to pick between buying, reacting to damage, attacking, investigating and hunting.

Parallel​

A parallel runs all its children every tick. If any child fails, it stops the others and fails. It succeeds once every child has succeeded, or, with succeed_after_first = 1, as soon as the first one does.

The official trees run perception (sensors and memory) in one branch of a parallel and decision making in another, so bots keep noticing things while they act.

Repeat​

A decorator_repeat restarts its child every time it finishes and keeps reporting running, forever. With amount = N it stops after N runs and reports the result of the last one.

Succeed and invert​

A decorator_succeed always passes the check pass, so it never blocks its parent. Its child only runs if the child passes its own check, and once the child finishes, the decorator succeeds whatever the child returned. With negated = 1 it fails instead. The official trees wrap optional steps in it.

A decorator_invert flips its child's check result: a child that fails the check makes the invert pass, and a child that passes makes it fail. While running, the child's results pass through unchanged, so invert is meant for wrapping conditions.

Condition checks​

A condition_* node is checked in every check pass. While the check holds, the node runs its child, or simply succeeds when it has none. When the check fails, the node fails, which stops its branch.

Every condition accepts negated = 1, which flips the check.

Values​

Most values in a tree name a blackboard key, the bot's own storage that nodes read from and write to. For example, a sensor writes the enemies it sees to output = "Enemy", and an attack action reads them back with input = "Enemy".

How a value is read depends on what the node expects.

Strings​

A string value in single quotes, like 'CT', is a literal, and the quotes are dropped. Any other string value names a blackboard key that must hold a string. The official trees use quoted literals to compare team names and token names.

Numbers​

A number value can be a literal number, a cvar written as @mp_plant_c4_anywhere, or a blackboard key. When that key holds a string instead of a number, the string is resolved once more, but only one level deep.

Positions and entity names​

Keys that expect a position, such as destination on action_move_to and input_location on action_look_at, accept:

  • A literal position, "-1450 2230 2".
  • A blackboard key holding a position, a position string or sensed entities, using the first one.
  • The targetname of a map entity, when no blackboard key has that name. The entity's origin is used.

Entity names make it easy to steer bots from a map. Rush bots walk to its tower button by name, and a script can move a target entity to send bots somewhere else.

Blackboard​

Domains​

Several nodes take a domain, through domain, input_domain, output_domain or output_token_domain. Domains are how the official trees share state between bots: memories under AllBots, locks such as 'PressAntennaButton' and strategy tokens under 'StratTokens'.

How a domain is read differs from node to node, so the official trees are the best reference for each one.

Sensors and memory​

A decorator_sensor fills its output key with a list of sensed entities, picked by entity_type_filter, team_filter, attacker_filter, class_name and an optional shape.

decorator_picker_* nodes narrow such a list down, for example to nearby, visible or highest scoring entries, and decorator_ranker_dist scores entries by distance. A decorator_memory copies entries into a memory key.

A decorator_bot_service at the root expires memories again: each memory_to_expire entry names a memory key with a time and a distance. Its tagged_entities_to_expire is a plain list of key names.

Global flags and counters​

action_set_global_flag sets a named flag for a random time between expiration_time_min and expiration_time_max, and the official trees check it with condition_is_empty and global = 1. action_set_global_counter, decorator_dec_global_counter and action_compare_global_counter handle named counters.

Subtree parameters​

A subtree node can write values into the blackboard before the subtree starts, so one module can serve several purposes:

{
type = "subtree"
file = "scripts/ai/mymode/bt_goto.kv3"
name = "GotoA"
params =
[
{ key = "Target" value = "site_a_target" },
{ key = "WaitTime" value = 3 }
]
}

Values must be integers or strings. Decimals and booleans are rejected with [AI BT]: unsupported type for subtree parameter.

Debugging​

Errors in a tree print [AI BT] warnings to the console, naming the missing key or the blackboard entry that could not be found, such as Blackboard entry 'bot_target' does not exist nor it is a valid entity name.

cv_bot_ai_bt_debug_target takes a bot's index and draws its tree as it runs. cv_bot_ai_bt_hiding_spot_show and cv_bot_ai_bt_moveto_show_next_hiding_spot draw the hiding spots bots consider. All three are cheat cvars without the release flag, so they may not be usable in the released game.