SC2API
An API for AI for StarCraft II
Tutorial 2: SCVs and Supply Depots

The goal in this tutorial will be to build scvs and supply depots.

  1. Building SCVs.
  2. Building Supply Depots.
  3. Managing idle SCVs.

As a starting point you should have the code from the end of tutorial 1 compiling and running. This tutorial will start where the last one left off.

Building SCVs

The library provides many events for your convenience, a big one we will use for this tutorial is the OnUnitIdle event. This function will get called each time a unit has been built and has no orders or the unit had orders in the previous step and currently does not.

In both the Starcraft 2 engine and library both buildings and units are considered units and are represented with a Unit object.

Add the following code to your Bot class.

1 {C++}
2 // In your bot class.
3 virtual void OnUnitIdle(const Unit* unit) final {
4  switch (unit->unit_type.ToType()) {
5  case UNIT_TYPEID::TERRAN_COMMANDCENTER: {
6  Actions()->UnitCommand(unit, ABILITY_ID::TRAIN_SCV);
7  break;
8  }
9  default: {
10  break;
11  }
12  }
13 }

As you can see we are going to try to build an SCV with the idle unit if it is a Command Center.

The UNIT_TYPEID enum holds the ids of common units you are likely to find in 1v1 matches. Feel free to look at sc2_typeenums.h to see a list of units and their corresponding id.

Building Supply Depots

Compile and run your bot now. You will see the Command Center making scvs up until the supply cap. That seems pretty good! We just need to build some supply depots now, lets replace the code in OnStep with the following.

1 {C++}
2 virtual void OnStep() {
3  TryBuildSupplyDepot();
4 }

Implement TryBuildSupplyDepot and TryBuildStructure as functions of our bot class.

1 {C++}
2 bool TryBuildStructure(ABILITY_ID ability_type_for_structure, UNIT_TYPEID unit_type = UNIT_TYPEID::TERRAN_SCV) {
3  const ObservationInterface* observation = Observation();
4 
5  // If a unit already is building a supply structure of this type, do nothing.
6  // Also get an scv to build the structure.
7  const Unit* unit_to_build = nullptr;
8  Units units = observation->GetUnits(Unit::Alliance::Self);
9  for (const auto& unit : units) {
10  for (const auto& order : unit->orders) {
11  if (order.ability_id == ability_type_for_structure) {
12  return false;
13  }
14  }
15 
16  if (unit->unit_type == unit_type) {
17  unit_to_build = unit;
18  }
19  }
20 
21  float rx = GetRandomScalar();
22  float ry = GetRandomScalar();
23 
24  Actions()->UnitCommand(unit_to_build,
25  ability_type_for_structure,
26  Point2D(unit_to_build->pos.x + rx * 15.0f, unit_to_build->pos.y + ry * 15.0f));
27 
28  return true;
29 }
30 
31 bool TryBuildSupplyDepot() {
32  const ObservationInterface* observation = Observation();
33 
34  // If we are not supply capped, don't build a supply depot.
35  if (observation->GetFoodUsed() <= observation->GetFoodCap() - 2)
36  return false;
37 
38  // Try and build a depot. Find a random SCV and give it the order.
39  return TryBuildStructure(ABILITY_ID::BUILD_SUPPLYDEPOT);
40 }

Compile and run your bot now. It's mining and building supply depots, it's almost ready for ladder! You'll notice when the SCV is done building a supply depot it sits idle, how useless. Lets fix that now.

Managing Idle SCVs

We have already hooked into the on idle event for building SCVs, we can use that same function to manage idle ones. Refactor your OnUnitIdle function with the following.

1 {C++}
2 virtual void OnUnitIdle(const Unit* unit) final {
3  switch (unit->unit_type.ToType()) {
4  case UNIT_TYPEID::TERRAN_COMMANDCENTER: {
5  Actions()->UnitCommand(unit, ABILITY_ID::TRAIN_SCV);
6  break;
7  }
8  case UNIT_TYPEID::TERRAN_SCV: {
9  const Unit* mineral_target = FindNearestMineralPatch(unit->pos);
10  if (!mineral_target) {
11  break;
12  }
13  Actions()->UnitCommand(unit, ABILITY_ID::SMART, mineral_target);
14  break;
15  }
16  default: {
17  break;
18  }
19  }
20 }

The ability type of SMART is equivalent to a right click in Starcraft 2 when you have a unit selected.

Now we just need to implement FindNearestMineralPatch and we can fix our lazy SCV.

1 {C++}
2 const Unit* FindNearestMineralPatch(const Point2D& start) {
3  Units units = Observation()->GetUnits(Unit::Alliance::Neutral);
4  float distance = std::numeric_limits<float>::max();
5  const Unit* target = nullptr;
6  for (const auto& u : units) {
7  if (u->unit_type == UNIT_TYPEID::NEUTRAL_MINERALFIELD) {
8  float d = DistanceSquared2D(u->pos, start);
9  if (d < distance) {
10  distance = d;
11  target = u;
12  }
13  }
14  }
15  return target;
16 }

Exercises

These exercises are very optional, so feel free to move onto the next tutorial. Otherwise, they act as a fun way to discover more about the API.

  1. As you build more scvs you'll want to start building supply depots at a higher rate. Try modifying the code to build multiple supply depots instead of just 1 at a time.
  2. (Challenging) Build two refineries and start mining gas. You can use code similar to FindNearestMineralPatch to find a geyser. You'll then want to detect when the refinery is either created or becomes idle and begin gathering gas with 3 scvs.

Full Source Code

1 {C++}
2 #include <sc2api/sc2_api.h>
3 
4 #include <iostream>
5 
6 using namespace sc2;
7 
8 class Bot : public Agent {
9 public:
10  virtual void OnGameStart() final {
11  std::cout << "Hello, World!" << std::endl;
12  }
13 
14  virtual void OnStep() final {
15  TryBuildSupplyDepot();
16  }
17 
18  virtual void OnUnitIdle(const Unit* unit) final {
19  switch (unit->unit_type.ToType()) {
20  case UNIT_TYPEID::TERRAN_COMMANDCENTER: {
21  Actions()->UnitCommand(unit, ABILITY_ID::TRAIN_SCV);
22  break;
23  }
24  case UNIT_TYPEID::TERRAN_SCV: {
25  const Unit* mineral_target = FindNearestMineralPatch(unit->pos);
26  if (!mineral_target) {
27  break;
28  }
29  Actions()->UnitCommand(unit, ABILITY_ID::SMART, mineral_target);
30  break;
31  }
32  default: {
33  break;
34  }
35  }
36  }
37 private:
38  bool TryBuildStructure(ABILITY_ID ability_type_for_structure, UNIT_TYPEID unit_type = UNIT_TYPEID::TERRAN_SCV) {
39  const ObservationInterface* observation = Observation();
40 
41  // If a unit already is building a supply structure of this type, do nothing.
42  // Also get an scv to build the structure.
43  const Unit* unit_to_build = nullptr;
44  Units units = observation->GetUnits(Unit::Alliance::Self);
45  for (const auto& unit : units) {
46  for (const auto& order : unit->orders) {
47  if (order.ability_id == ability_type_for_structure) {
48  return false;
49  }
50  }
51 
52  if (unit->unit_type == unit_type) {
53  unit_to_build = unit;
54  }
55  }
56 
57  float rx = GetRandomScalar();
58  float ry = GetRandomScalar();
59 
60  Actions()->UnitCommand(unit_to_build,
61  ability_type_for_structure,
62  Point2D(unit_to_build->pos.x + rx * 15.0f, unit_to_build->pos.y + ry * 15.0f));
63 
64  return true;
65  }
66 
67  bool TryBuildSupplyDepot() {
68  const ObservationInterface* observation = Observation();
69 
70  // If we are not supply capped, don't build a supply depot.
71  if (observation->GetFoodUsed() <= observation->GetFoodCap() - 2)
72  return false;
73 
74  // Try and build a depot. Find a random SCV and give it the order.
75  return TryBuildStructure(ABILITY_ID::BUILD_SUPPLYDEPOT);
76  }
77 
78  const Unit* FindNearestMineralPatch(const Point2D& start) {
79  Units units = Observation()->GetUnits(Unit::Alliance::Neutral);
80  float distance = std::numeric_limits<float>::max();
81  const Unit* target = nullptr;
82  for (const auto& u : units) {
83  if (u->unit_type == UNIT_TYPEID::NEUTRAL_MINERALFIELD) {
84  float d = DistanceSquared2D(u->pos, start);
85  if (d < distance) {
86  distance = d;
87  target = u;
88  }
89  }
90  }
91  return target;
92  }
93 };
94 
95 int main(int argc, char* argv[]) {
96  Coordinator coordinator;
97  coordinator.LoadSettings(argc, argv);
98 
99  Bot bot;
100  coordinator.SetParticipants({
101  CreateParticipant(Race::Terran, &bot),
102  CreateComputer(Race::Zerg)
103  });
104 
105  coordinator.LaunchStarcraft();
106  coordinator.StartGame(sc2::kMapBelShirVestigeLE);
107 
108  while (coordinator.Update()) {
109  }
110 
111  return 0;
112 }