Статья предназначается для всех любителей старой-доброй Total Annihilation и ее открытой реализации в виде SpringRTS + Balanced Annihilation.

Несмотря на то, что виджет Air Screen Keeper оказался, по большому счету, бесполезной затеей, на его примере ввиду небольшого размера можно отразить основные идеи построения расширений к играм на основе движка Spring.

Итак, суть виджета (т.е. расширения) — сообщить игроку в той или иной форме о том, что т.н. воздушный экран, состоящий из множества самолетов, выполняющих команду «патруль», а��акован врагом с земли. Обычно такие атаки в разгаре битвы (8 на 8 игроков) не очень заметны и можно легко прохлопать, как противник таким образом уничтожит до 70% самолетов, если отвлечься на что-то сиюминутное.

Поэтому в качестве заметного сообщения будем использовать маркер на карте, видимый только игроку, и будем ставить его в месте, где был атакован наш самолет. Кроме того, чтобы не заполонить маркерами все на свете, будем выставлять их с задержкой в несколько секунд.

К сожалению, отсутствует видео, где это можно было бы посмотреть вживую. Вместо этого приведу пример более сложного виджета, управляющего конструкторами для автоматической застройки шахт по добыче металла в пределах периметра базы игрока. Кстати, последний вычисляется автоматически благодаря алгоритму, описанному в замечательной статье о построении минимальных выпуклых оболочек.




Для начала инициализируем виджет и отсортируем имеющиеся воздушные суда, используя арсенал функций, предоставляемых движком:

local function dispatchUnit(unitID, unitDefID)
	local udef = UnitDefs[unitDefID] -- находим описание юнита по id
	
	-- если юнит воздушный, то помещаем его в глобальный массив
	if udef.isAirUnit then
		units[unitID] = true 
	end
end

function widget:Initialize()
	local allunits = spGetTeamUnits(spGetMyTeamID())
	for _, uid in ipairs(allunits) do
		dispatchUnit(uid, spGetUnitDefID(uid))
	end
end

В таблице units у нас теперь располагаются все самолеты, имеющиеся в распоряжении игрока на момент инициализации виджета. Этого, пр��вда, недостаточно, т.к. нам нужен именно воздушный экран, т.е. самолеты, выполняющие команду патруль. Некоторые могут прохлаждаться на земле, как, например, бомбардировщики, или транспортные самолеты, но нам нужны именно патрулирующие в данный момент.

Пройдемся по имеющимся самолетам и посмотрим их очереди команд на предмет наличия «патруля», после чего поместим найденные самолеты в таблицу airscreen (вне всяких сомнений, есть более эффективный способ реализации этой процедуры, но на момент создания виджета я не придумал ничего лучше, как каждый 128-й кадр игры проделывать такую сортировку. К счастью, это происходит достаточно быстро, чтобы игрок ничего не заметил).

local function UnitHasPatrolOrder(unitID)
	local queue=spGetCommandQueue(unitID,2)
	for i,cmd in ipairs(queue) do
		if cmd.id==CMD.PATROL then
			return true
		end
	end
	return false
end

function updateAirScreenUnits()
	for id, v in pairs(units) do
		if UnitHasPatrolOrder(id) then
			airscreen[id] = true
		end
	end
end

function widget:GameFrame(frameNum)
	if (frameNum % 128 ) == 0 then
		updateAirScreenUnits()
	end
end

Возможно, более эффективная реализация заключалась бы в обработке события UnitCommand — команды, поступающей для какого-либо юнита. Если это патруль и других команд нет, то можно поместить самолет в таблицу airscreen. Ну да ладно.

Также надо иметь ввиду, что самолеты строятся, уничтожаются, кроме того, кто-то из команды может отдать нам парочку, или, мы сами можем кому-то их отдать, за этим придется следить с помощью соответствующих событий UnitFinished и UnitDestroyed:

function widget:UnitFinished(unitID, unitDefID, unitTeam)
	if (unitTeam ~= spGetMyTeamID()) then
		return
	end
	dispatchUnit(unitID, unitDefID) -- сортируем
end

function widget:UnitDestroyed(unitID, unitDefID, unitTeam, attackerID, attackerDefID, attackerTeam)
	if airscreen[unitID] then
		if attackerID then
			notify(attackerID) -- юнит уничтожен, мы ставим маркер на месте атакующего, если известен его id
		else
			notify(unitID) -- или в месте уничтожения, если id незвестен
		end
		airscreen[unitID] = nil -- удаляем самолет из воздушного экрана
	end
	units[unitID] = nil -- удаляем самолет из списка самолетов
end

При приеме/передаче юнитов между игроками с точки зрения виджета происходит примерно то же самое, что при создании/уничтожении, так что просто сошлемся на уже имеющиеся методы:

function widget:UnitTaken(unitID, unitDefID, unitTeam, newTeam)
	widget:UnitDestroyed(unitID, unitDefID)
end


function widget:UnitGiven(unitID, unitDefID, unitTeam, oldTeam)
	widget:UnitFinished(unitID, unitDefID, unitTeam)
end

Функция notify ставит маркер на карте с задержкой и достаточно проста:

function widget:Update(dt)
	lastMarkTime = lastMarkTime - dt
end

function notify(unitID)
	if (lastMarkTime < 0) then
		lastMarkTime = MARK_DELAY
		
		local msg = "AA"
		local x, y, z = Spring.GetUnitPosition(unitID)
		spMarkerAddPoint(x, y, z, msg, true)
	end
end

Полный код виджета можно посмотреть здесь: github.com/spike-spb/air-screen-keeper/blob/master/air-screen-keeper.lua
Чтобы установить этот виджет, нужно скопировать его в <ПутьУстанокиSpring>/LuaUI/Widgets. К сожалению, до сих пор многие сталкиваются с проблемой не то, чтобы установки виджетов… многим просто не удается запустить саму игру, поэтому было составлено видео-руководство по этому, в целом, нехитрому процессу: springrts.ru/howto

Кроме того, в сети существует более подробное видео-руководство по созданию виджетов, длиною более часа, созданное Александром Липатовым, которое в свое время помогло мне быстро сориентироваться в процессе разработки расширений для Spring, так что я также приведу его здесь: youtu.be/eMEEa9imx3g

Вообще, надо сказать, что с помощью подобных расширений можно создавать замечательные возможности, временами существенно влияющие на стратегию и тактику игры, благодаря чему у игроков старой школы с навыками программирования имеется замечательная возможность не только играть в старую добрую игру, но и самостоятельно создавать ее.

Ссылки:
Lua scripting (для Spring) — здесь API, где можно найти описание всех функций, упомянутых в статье.
SpringRTS.ru
Сообщество Spring вконтакте