Brood War API – The Comprehensive Guide: Creating buildings, and everything about the creep

So, this part is very much in the middle of the everything. It is a part of a book, and I work on different chapters all the time, so the order these articles are published in is not representative of the final book structure. This part needed some thinking abouting, as the code can be very roundabout every now and then. Huge thanks to my proofreaders on this one!

Index for the Comprehensive Guide posts

(Chapter) An inside look into StarCraft

Creating buildings

Generally, creating buildings requires the tiles for the building to be placed on to be discovered (no sight required). Only workers can construct/warp/morph buildings. Refineries can only be placed on Vespene Geysers, and main buildings (Nexus, Command Center, Hatchery) must be placed some distance away from resources. Main buildings are sometimes called resource depots as well, but it would be more accurate to say that these buildings have the resource depot property. The exact distance is determined by some rectangles placed around the resource depot. The top left corner is 3 tiles up and to the left, and the bottom right corner is 4 tiles to the right and bottom – for each tile the building occupies. In effect, this just means a 3-tile radius. Here is the code:

for (size_t y = tile_pos.y; y != tile_pos.y + tile_size.y; ++y) {
	for (size_t x = tile_pos.x; x != tile_pos.x + tile_size.x; ++x) {
		auto& tile = st.tiles[y * game_st.map_tile_width + x];
		if (tile.explored & visibility_mask) return false;
		if (tile.visible & visibility_mask || (tile.flags & flags_mask) == 0 || (check_unk4 && tile.flags & tile_t::flag_unk4)) {
			if (is_resource_depot) {
				rect bb{xy(32 * ((int)x - 3), 32 * ((int)y - 3)), xy(32 * ((int)x + 4), 32 * ((int)y + 4))};
				for (unit_t* n : find_units_noexpand(bb)) {
					if (ut_resource(n)) return false;
				}
			}
		} else return false;
	}
}

And here is a picture for clarity (The orange squares are the TilePositions occupied by the Hatchery):

Creating buildings: Generally, a building under construction is not operating, and can’t perform actions until it’s 100% done. Construction times are constant values, and cannot be sped up.

Constructing buildings: For Terran, the buildings are built by an SCV, who is occupied for the duration of the process. Only one worker can construct a building at a time, and the construction can be stopped any time and be resumed, even by another worker.

Warping buildings: After warping in a Protoss building, the Probe is free to do something else. Except for Assimilators and Nexuses, all protoss buildings must be built under the area of the psionic matrix provided by the pylons. The check is performed on the unit sprite’s position, in the following way:

bool is_in_psionic_matrix_range(xy rel) const {
	unsigned x = std::abs(rel.x);
	unsigned y = std::abs(rel.y);
	if (x >= 256) return false;
	if (y >= 160) return false;
	if (rel.x < 0) --x;
	if (rel.y < 0) --y;
	return psi_field_mask[y / 32u][x / 32u];
}

bool is_in_psionic_matrix(int owner, xy pos) const {
	for (const unit_t* u : ptr(st.psionic_matrix_units)) {
		if (u->owner != owner) continue;
		if (is_in_psionic_matrix_range(pos - u->sprite->position)) return true;
	}
	return false;
}

What this means is the game loops through all the psionic matrix provider units (Protoss Pylons), and checks if the target unit is in range. The range is 160 pixels vertically and 256 pixels horizontally from the Pylon’s position. That means that the unit need not to be fully under the psionic matrix, only the center of it. Buildings cannot be warped outside the psionic matrix, but if the already warping building loses coverage, it still finishes warping in.

Morphing buildings: Except for Hatcheries, Extractors, and Infested Command Centers, all Zerg buildings must be placed on the creep. The building must be entirely on the creep (Creep properties detailed below). When placing a building, the Zerg Drone is consumed in the process, but will be not lost if the building is canceled. When canceling a morphing building, a building death animation is played.

Creep spread: Creep is generated by Creep Colonies and Hatcheries, but it does not belong to any player – any Zerg can place buildings on any creep (Players can exploit this, for example, by placing Sunken Colonies inside the enemy Zerg base, or opening a Nydus Canal exit there). The creep spreads out continuously from the creep provider, adding extra creep tiles to the edges – some tiles can’t have creep, but they are still considered for spreading, so creep can spread over those, or even through islands and high ground.

A tile being unbuildable is not exactly the same as not being able to have creep. If the tile is the lowest row of the tiles (on the map), then it can have creep. It can’t have creep, if

  • The tile is unbuildable
  • The tile below the current tile is unbuildable
  • The tile is partially walkable

In every other case, creep can spread there.

bool tile_can_have_creep(xy_t<size_t> tile_pos) {
	size_t index = tile_pos.y * game_st.map_tile_width + tile_pos.x;
	if (st.tiles[index].flags & (tile_t::flag_unbuildable | tile_t::flag_partially_walkable)) return false;
	if (tile_pos.y == game_st.map_tile_height - 1) return true;
	if (st.tiles[index + game_st.map_tile_width].flags & tile_t::flag_unbuildable) return false;
	return true;
}

The code determining the spread of the creep:

bool spread_creep(unit_type_autocast unit_type, xy pos, bool* out_any_tiles_occupied = nullptr) {
	std::array<static_vector<size_t, 240>, 8> target_tiles;
	bool spreads_creep = unit_type_spreads_creep(unit_type, true);
	auto area = get_max_creep_bb(unit_type, pos, true);
	int dy = (int)area.from.y * 32 - pos.y + 16;
	bool any_tiles_occupied = false;
	for (size_t tile_y = area.from.y; tile_y != area.to.y + 1; ++tile_y, dy += 32) {
		int dx = (int)area.from.x * 32 - pos.x + 16;
		for (size_t tile_x = area.from.x; tile_x != area.to.x + 1; ++tile_x, dx += 32) {
			size_t index = tile_y * game_st.map_tile_width + tile_x;
			auto flags = st.tiles[index].flags;
			if (flags & tile_t::flag_has_creep) continue;
			if (!tile_can_have_creep({tile_x, tile_y})) continue;
			if (flags & tile_t::flag_occupied) {
				if (!any_tiles_occupied) any_tiles_occupied = true;
				continue;
			}
			if (spreads_creep) {
				int d = dx*dx * 25 + dy*dy * 64;
				if (d > 320*320 * 25) continue;
			}
			size_t n = count_neighboring_creep_tiles({tile_x, tile_y});
			if (n == 0) continue;
			target_tiles[n - 1].push_back(index);
		}
	}
	if (out_any_tiles_occupied) *out_any_tiles_occupied = any_tiles_occupied;
	for (auto& v : reverse(target_tiles)) {
		if (v.empty()) continue;
		size_t index = v[(lcg_rand(26) >> 4) % v.size()];
		set_tile_creep({index % game_st.map_tile_width, index / game_st.map_tile_width});
		return true;
	}
	return false;
}

What this does is searches for a tile to spread the creep on, and returns true after that. If it can’t find any, it returns false. First, the get_max_creep_bb method gets a bounding box for the maximum range of the creep. If it’s not a creep spreader unit, it just returns the unit’s bounds – that means that every Zerg building has creep underneath it, and that takes a bit to disappear after the building is killed. However, if the unit is a creep provider, the bounding box is a larger rectangle – its top left corner is (x-320, y-200), and bottom right is (x+320, y+200), where x and y are the building’s position coordinates. The box is also trimmed down for the map edges, but that is irrelevant from a gameplay perspective.

rect_t<xy_t<size_t>> get_max_creep_bb(unit_type_autocast unit_type, xy pos, bool unit_is_completed) {
	rect r;
	if (unit_type_spreads_creep(unit_type, unit_is_completed)) {
		r.from = pos - xy(320, 200);
		r.to = pos + xy(320, 200);
	} else {
		r.from = (pos / 32 - unit_type->placement_size / 32 / 2) * 32;
		r.to = (r.from / 32 + unit_type->placement_size / 32 - xy(1, 1)) * 32;
	}
	rect_t<xy_t<size_t>> rt;
	if (r.from.x <= 0) rt.from.x = 0;
	else rt.from.x = r.from.x / 32u;
	if (r.from.y <= 0) rt.from.y = 0;
	else rt.from.y = r.from.y / 32u;
	if (r.to.x >= (int)game_st.map_width) rt.to.x = game_st.map_tile_width - 1;
	else rt.to.x = r.to.x / 32u;
	if (r.to.y >= (int)game_st.map_height) rt.to.y = game_st.map_tile_height - 1;
	else rt.to.y = r.to.y / 32u;
	return rt;
}

After selecting this box, the tiles inside the box are examined, from the left to the right, and top to bottom. If the tile already has creep, or can’t have creep, or is occupied by a building, it skips and examines the next tile.

Comparatively, the game uses a different bit of code to spawn the initial coverage of creep-spreading units when starting the game. This is the same as the fully spread out creep from a creep provider.

void spread_creep_completely(unit_type_autocast unit_type, xy pos) {
	rect_t<xy_t<size_t>> unit_area;
	unit_area.from.x = pos.x / 32u - unit_type->placement_size.x / 32u / 2;
	unit_area.from.y = pos.y / 32u - unit_type->placement_size.y / 32u / 2;
	unit_area.to.x = unit_area.from.x + unit_type->placement_size.x / 32u;
	unit_area.to.y = unit_area.from.y + unit_type->placement_size.y / 32u;
	st.tiles.at(unit_area.from.y * game_st.map_tile_width + unit_area.from.x);
	if (unit_area.from != unit_area.to) st.tiles.at((unit_area.to.y - 1) * game_st.map_tile_width + unit_area.to.x - 1);
	for (size_t y = unit_area.from.y; y != unit_area.to.y; ++y) {
		for (size_t x = unit_area.from.x; x != unit_area.to.x; ++x) {
			if (!tile_can_have_creep({x, y})) continue;
			set_tile_creep({x, y});
		}
	}
	while (spread_creep(unit_type, pos));
}

Spreading the creep is done every 15 frames. It is a secondary order, so the unit can do something else during that (For example, a Sunken Colony can attack).

void secondary_order_SpreadCreep(unit_t* u) {
	if (unit_is_hatchery(u)) secondary_order_SpawningLarva(u);
	if (u->building.creep_timer) {
		--u->building.creep_timer;
		return;
	}
	u->building.creep_timer = 15;
	bool any_tiles_occupied = false;
	if (!spread_creep(get_unit_type(UnitTypes::Zerg_Hive), u->sprite->position, &any_tiles_occupied) && !any_tiles_occupied) {
		if (unit_is_hatchery(u)) set_secondary_order(u, get_order_type(Orders::SpawningLarva));
		else set_secondary_order(u, get_order_type(Orders::Nothing));
	}
}

Canceling buildings: If a warping/morphing/constructing building is canceled, 75% of the resource cost required for creation is refunded.

void partially_refund_unit_costs(int owner, unit_type_autocast unit_type) {
	st.current_minerals[owner] += unit_type->mineral_cost * 3 / 4;
	st.current_gas[owner] += unit_type->gas_cost * 3 / 4;
}

And that’s all I have for this part. Unrolling some of this code was quite an effort.

Thanks for reading! If you liked the article, consider subscribe to the mailing list for updates, or following me on my social media channels, which are Facebook and Twitter!

Leave a Reply