Brood War API – The Comprehensive Guide – Distances, high ground, and unit behavior

Index for the Comprehensive Guide posts

First, I’d like to make a small note. My sections about the inner workings of StarCraft are meant to be comprehensive, but I will inevitably leave things out. There are aspects of the game, however, that I will intentionally talk about – one big example is custom maps, triggers, and mapmaking in general. Currently, the Brood War API only makes sense in normal game mode (and on 1v1 matches, but that’s not a technical limitation), so this feels unnecessary. With that in mind, let’s continue the series!

First, continuing with some details about high ground that I left out from the previous section.

Képtalálat a következőre: „obi wan high ground”
not you again

(An addition to the “Terrain, and the effects thereof” section)

Units on high(er) ground are not visible unless they attack the unit on the lower ground. Then the tile containing the unit is set to visible, until the next vision update. If the unit on the high ground is invisible, then the tile is not revealed.

Distances

Distances in Brood War are measured in pixels. When considering unit-to-unit distance, the calculation is done between the bounding boxes, like so:

	int units_distance(const unit_t* a, const unit_t* b) const {
		auto a_rect = unit_sprite_bounding_box(a);
		auto b_rect = unit_sprite_bounding_box(b);
		return xy_length(rect_difference(a_rect, b_rect));
	}

	int unit_distance_to(const unit_t* u, xy pos) const {
		return xy_length(pos - nearest_pos_in_rect(pos, unit_sprite_bounding_box(u)));
	}
	
	xy nearest_pos_in_rect(xy pos, rect area) const {
		if (area.from.x > pos.x) pos.x = area.from.x;
		else if (area.to.x < pos.x) pos.x = area.to.x;
		if (area.from.y > pos.y) pos.y = area.from.y;
		else if (area.to.y < pos.y) pos.y = area.to.y;
		return pos;
	}

There is an interesting GitHub discussion about this here. The most important takeaway is this image:

The original comment by Ankmairdor reads: “In descending order of draw priority: green is BWAPI edge to point distance, red is openBW edge to point distance, orange is Pythagorean center distance plus average of sides(though drone is square). 20 pixel interval.” The issue was that OpenBW and BWAPI calculated distances slightly differently – and that is adjusted now. But as you can see, the Pythagorean distance and the game distance varies greatly. For the average player, this is rarely an issue, if at all, but for bot authors, this can be significant.

Units, and their attributes

In StarCraft, basically everything you interact with is a unit, including buildings, resources, and even nukes. This means they have the same attributes that can be interacted with, but this interaction might be turned off. This is where BWAPI gives us more data than is useful for the average player. For this part, let’s examine this from the player’s perspective. As previously mentioned, unit sprites have a fixed size (often called a bounding rectangle), and that doesn’t change, no matter which way the unit appears to be facing.

Interaction flags

The first group I’ll call interaction flags (This is a term I made up for easy reference).

Visibility: Cloaked units are only visible as faint outlines, and not targetable by attacks. Area of effect spells, and attacks can still damage it. Some spells reveals invisible units, these are: Lockdown, Maelstrom, Irradiate, Defensive Matrix, Ensnare, Plague, Acid Spores, and Stasis.

Invincibility: Invincible units can’t take damage, and it determines whether you can issue an attack command against this unit or not. For example, you cannot attack resource fields. You might have noticed, that with the “power overwhelming” invincibility cheat, your units can be attacked, they are just not getting hurt. They cannot be targeted with spells, and area of effect abilities won’t affect them either.

Burrowability: Some Zerg ground units can burrow in the ground, becoming invisible without detection to the enemy. They can’t attack when burrowed, except the Lurker, which can only attack then. Burrowed units can’t be loaded into transports, and Terran buildings can’t land on them. (Fun fact: At one point, a feature was planned for landing buildings to crush units. Also, you can crush Protoss Interceptors with landing to this day). Burrowed units are unaffected by the Protoss Arbiter’s Recall ability.

Detector: If the unit is a grounded building (not lifted off, it can detect cloaked units within a constant range, which is 7*32 pixels. Every other case, the detection range is the same as its sight range. Blinded, incomplete, or disabled units cannot detect, and neither do hallucinations.

int range = u_grounded_building(detector) ? 32 * 7 : unit_sight_range(detector);
//(...)
    bool unit_can_detect(const unit_t* u) const {
        if (!ut_detector(u)) return false;
        if (!u_completed(u)) return false;
        if (unit_is_disabled(u)) return false;
        if (u->blinded_by) return false;
        return true;
    }

Unit subtypes

The second group is the subtype of the unit (again, my term). These are (mostly) distinct categories, and used for determining the effect of spells and abilities on the unit (I will detail these when describing the spells themselves). The three groups are organic, robotic, and mechanical. These are distinct with a few exceptions, most notably the Terran SCV, which is both organic and mechanical. I added a table with the unit types, and their categories to the Appendix.

One-off unit types: These are a little different from the previous: beacon, flag beacon, powerup. These have no effect on normal game modes, so I won’t describe them in detail.

Spells: Some spells are implemented as units that you can’t interact with after casting. These are the Terran Scanner Sweep, the Protoss Corsair’s Disruption Web, and the Zerg Defiler’s Dark Swarm. For human gameplay, this distinction has no effect. They expire after a certain timeout.

Spellcasters: Pretty self-explanatory, basically all units with energy.

Flyers: Flyers have their own flag, and behave different when considering movement (I’ll describe that in the movement section)

Buildings: I do not consider these as subtypes, as they are orthogonal to being organic or mechanical. There are no big surprises here, Zerg buildings are organic, every other building is mechanical, and there are no robotic buildings. Flyers have different movement characteristics. Lifted Terran buildings have their own flying building flag.

Quantitive abilities of units

Hit points: Every unit you can interact with will usually have a number of maximum, and current hit points. They are displayed as integers, but in the background, they are kept track to 1/256 precision (the previously mentioned fp8 data type). Zerg units (including buildings) regenerate hit points, at the rate of 4/256 per frame. The unit is considered dead if the hit points of it fall below zero. The hit points displayed are always rounded up – the same applies for shields.

int visible_hp_plus_shields(const unit_t* u) const {
		int r = 0;
		if (u->unit_type->has_shield) r += u->shield_points.integer_part();
		r += u->hp.ceil().integer_part();
		return r;
	}

Terran mechanical units, and Terran buildings can be repaired by an SCV. The rate of repair is not constant – it will described in detail in its own segment. Terran buildings also burn down, if they go below 33% of their maximum hit points. The rate of hp loss is 20/256 per frame.

if (u_grounded_building(u) || ut_flying_building(u)) {
	if (unit_hp_percent(u) <= 33) {
		unit_deal_damage(u, 20_fp8, nullptr, u->last_attacking_player);
	}

Shields: Protoss units have shields that regenerate faster than hit points, at a rate of 7/256 per frame. Shields take full damage from any damage source. Shield recharge from a Shield Battery is 1280/256 per frame.

Energy: Spellcaster units use energy, and the maximum value for that is 200 or 250. Other than that, energy is only used for the Feedback and EMP spells. If the unit is not actively using its energy for cloaking (in case of Terran Ghosts and Wraiths), energy is constantly regenerated, at the rate of 8/256 per frame. Here is the relevant code segment:

	void update_unit_energy(unit_t* u) {
		if (!ut_has_energy(u)) return;
		if (u_hallucination(u)) return;
		if (!u_completed(u)) return;
		if ((u_cloaked(u) || u_requires_detector(u)) && !u_passively_cloaked(u)) {
			fp8 cost = unit_cloak_energy_cost(u);
			if (u->energy < cost) {
				if (u->secondary_order_type->id == Orders::Cloak) set_secondary_order(u, get_order_type(Orders::Nothing));
			} else {
				u->energy -= cost;
			}
		} else {
			fp8 max_energy = unit_max_energy(u);
			if (unit_is(u, UnitTypes::Protoss_Dark_Archon) && u->order_type->id == Orders::CompletingArchonSummon && u->order_state == 0) {
				max_energy = fp8::integer(50);
			}
			u->energy = std::min(u->energy + 8_fp8, max_energy);
		}
	}

The cost of cloaking for Ghosts, their hero variants, and Infested Kerrigan is 10/256 per frame, while for Wraiths, and their hero versions it is 13/256 per frame. Every other cloaked unit, 0 energy gets deducted – but these units usually don’t have energy of their own. The cloaking cost is actually a total of the regeneration, and the unit cloak cost, which is therefore 18, and 21 respectively.

Sight range: This is a hidden variable, not displayed for the user. It determines the line of sight for the units. This does not equal the unit’s weapon range – the most prominent example being Terran Siege Tanks, where the weapon range is greater. Every frame each unit reveals a (roughly) circular area of 32×32 tiles around it. The radius is the unit’s sight range, plus one for ground units. These areas remain visible until the next vision update, which happens when the condition frameCount % 100 == 99 is true. But any time a Terran Medic casts an Optical Flare, that forces an update, and the value 99 changes. By default, this is the logic:

  void process_frame() {
    (...)
        update_tiles = st.update_tiles_countdown == 0;
​
        if (update_tiles) {
            for (auto& v : st.tiles) {
                v.visible = 0xff;
            }
        }
    (...)
    }

And Optical Flare changes it the following way:

    void blind_unit(unit_t* u, int source_owner) {
    (...)
        st.update_tiles_countdown = 1;
    }

Target acquisition range: Another hidden variable, this is the range (in pixels) that is used for the units to (passively) get new targets by themselves. Upgrades can change (increase) this value. Cloaked ghosts have this value set to 0, so in practice they don’t acquire new targets automatically.

	int unit_target_acquisition_range(const unit_t* u) const {
		if ((u_cloaked(u) || u_requires_detector(u)) && u->order_type->id != Orders::HoldPosition) {
			if (unit_is_ghost(u)) return 0;
		}
		int bonus = 0;
		if (unit_is(u, UnitTypes::Terran_Marine) && player_has_upgrade(u->owner, UpgradeTypes::U_238_Shells)) bonus = 1;
		if (unit_is(u, UnitTypes::Zerg_Hydralisk) && player_has_upgrade(u->owner, UpgradeTypes::Grooved_Spines)) bonus = 1;
		if (unit_is(u, UnitTypes::Protoss_Dragoon) && player_has_upgrade(u->owner, UpgradeTypes::Singularity_Charge)) bonus = 2;
		if (unit_is(u, UnitTypes::Hero_Fenix_Dragoon)) bonus = 2;
		if (unit_is(u, UnitTypes::Terran_Goliath) && player_has_upgrade(u->owner, UpgradeTypes::Charon_Boosters)) bonus = 3;
		if (unit_is(u, UnitTypes::Terran_Goliath_Turret) && player_has_upgrade(u->owner, UpgradeTypes::Charon_Boosters)) bonus = 3;
		if (unit_is(u, UnitTypes::Hero_Alan_Schezar)) bonus = 3;
		if (unit_is(u, UnitTypes::Hero_Alan_Schezar_Turret)) bonus = 3;
		return u->unit_type->target_acquisition_range + bonus;
	}

And that’s all I have for you in this installment! Thanks for reading, and please don’t hesitate to point out any mistakes I might have made – even if it’s just a typo. For this weekend only, I give out double Good Boy points for that! (Or alternatively, I can give you discounts in the shop if you ask nicely. A side note: The shop has MCDT/Undermind merch, but it is run by my friend, not me, that’s why you see a lot of unrelated stuff. I just set it up some time ago). Also, don’t miss any updates, and subscribe to the mailing list, and/or follow me on my social media channels, which are Facebook and Twitter!

Leave a Reply