Pull to refresh

Пишем расширения для игры Balanced Annihilation на базе движка Spring Engine

Reading time 4 min
Views 7.1K
Статья предназначается для всех любителей старой-доброй 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 вконтакте
Tags:
Hubs:
+10
Comments 6
Comments Comments 6

Articles