Skip to main content

5.1 - The Core of Control

Every command your bot issues—every attack, move, or build order—is an action that must be sent to the StarCraft II game engine. The functions that handle this critical transmission are built into the BotAI class and its Unit objects.

While there is a low-level self.do() function, you will almost always use the high-level, convenient command methods like unit.attack() or self.build(). Understanding the two primary ways to execute these commands—individually or in a batch—is essential for writing an efficient bot.

The Action Pipeline: From Intent to Execution

When you want a unit to perform an action, your command goes through a simple pipeline.

+-----------------------------------+   (Stage 1)   +----------------------+   (Stage 2)   +--------------------------+
| Your Intent | ------------> | Creates UnitCommand | ------------> | self.do_actions(actions) |
| e.g., `my_marine.attack(target)` | | (Behind the scenes) | | (Sends to SC2) |
+-----------------------------------+ +----------------------+ +--------------------------+
(High-Level Method) (Intermediate Data Object) (Batch-Sending Engine)
  1. Stage 1: High-Level Helper Methods: These are the readable methods on Unit and BotAI objects, like unit.attack(), self.build(), etc. When you call one, it creates a UnitCommand data object that represents your intent.

  2. Stage 2: The do_actions Engine: The await self.do_actions() function takes a list of those UnitCommand objects and sends them all to the game engine in a single, efficient batch for execution. Awaiting a single action like await unit.attack() is effectively a shortcut for creating and sending a batch of one.

This design gives you the best of both worlds: readable, high-level methods for creating actions and an efficient, low-level function for executing them in batches.


Individual vs. Batched Actions: A Performance Choice

You have two ways to execute actions. Your choice has significant performance implications, especially when commanding large armies.

Methodawait unit.attack(...) (in a loop)await self.do_actions([...])
How It WorksEach await sends a command (or a micro-batch of one) to the game and waits for the game step to advance.Collects many commands into a list and sends them all to the game in a single, efficient batch.
AnalogyMaking a separate phone call for every single instruction.Sending one text message with a complete list of instructions.
ProsVery simple and intuitive for single actions.Highly performant. Drastically reduces overhead when issuing many commands in one step.
ConsInefficient for large groups. Can significantly slow down your on_step loop due to repeated await overhead.Requires slightly more code (creating a list and appending to it).

Best Practice: For any loop that issues commands to multiple units, always use the batched self.do_actions() method.


A Developer's Checklist for Issuing Commands

  • 1. Use High-Level Methods: Always use the convenient methods like unit.attack() or self.train() to create your actions.
  • 2. Single Action? If you are only issuing one command, await it directly: await self.build(...).
  • 3. Multiple Actions? If you are looping through a Units collection:
    • Create an empty list: actions = [].
    • In the loop, append the command objects to the list without await.
    • After the loop, await self.do_actions(actions).

Code Example: Simple vs. Batched Performance

This bot demonstrates the two methods for commanding a group of idle Marines. You can comment out one block and run the other to see the difference. The batched method is the professional standard.

# batch_action_bot.py

from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.data import Difficulty, Race
from sc2.main import run_game
from sc2.player import Bot, Computer
from sc2.ids.unit_typeid import UnitTypeId
from sc2.units import Units

class BatchActionBot(BotAI):
"""Demonstrates the performance difference between individual and batched actions."""

async def on_step(self, iteration: int):
# We need a target for our marines to attack.
target = self.enemy_start_locations[0].towards(self.start_location, 15)
# Find all idle marines.
idle_marines: Units = self.units(UnitTypeId.MARINE).idle

if not idle_marines.exists:
# If no idle marines, just build more and do nothing else this step.
await self.train_reinforcements()
return

# --- Method 1: Individual Commands (Simple but Inefficient) ---
# Each 'await' waits for the next game step.
# This is fine for 1-2 units, but very slow for 20+.
# for marine in idle_marines:
# await marine.attack(target)

# --- Method 2: Batched Commands (The Professional Standard) ---
# This is far more efficient as it sends all commands in one go.
actions = []
for marine in idle_marines:
# Create the command object and add it to our list.
actions.append(marine.attack(target))
# Send all commands at once.
await self.do_actions(actions)


async def train_reinforcements(self):
"""A simple method to build some prerequisite structures and train marines."""
if self.supply_left < 3 and self.already_pending(UnitTypeId.SUPPLYDEPOT) == 0:
if self.can_afford(UnitTypeId.SUPPLYDEPOT):
await self.build(UnitTypeId.SUPPLYDEPOT, near=self.start_location.towards(self.game_info.map_center, 5))
if self.structures(UnitTypeId.BARRACKS).amount < 2:
if self.can_afford(UnitTypeId.BARRACKS):
await self.build(UnitTypeId.BARRACKS, near=self.start_location.towards(self.game_info.map_center, 8))
if self.structures(UnitTypeId.BARRACKS).ready.idle.exists and self.can_afford(UnitTypeId.MARINE):
await self.train(UnitTypeId.MARINE)


if __name__ == "__main__":
run_game(
maps.get("BlackburnAIE"),
[
Bot(Race.Terran, BatchActionBot()),
Computer(Race.Zerg, Difficulty.Easy)
],
realtime=True,
)