У каждого в жизни наступает такой момент, когда нужно совершить действия с группой юнитов. Сейчас мы рассмотрим все способы взаимодействия с группой и сравним их производительность.
Переборы групп преследуют разные цели, но их можно свести к нескольким типам:
- Многократный перебор (проверка типа ландшафта под юнитом, расположение дамми над юнитом)
- Однократный перебор с последующим очищением (практически каждое заклинание с нанесением урона)
Чтоб использовать более менее приближённый к реалиям тест, будем использовать таймер, срабатывающий тысячу раз сто раз в секунду:
local n, g, c = 0, garbage('count'), clock()
TimerStart(CreateTimer(), 0.01, true, function()
n = n + 1
if n > 1000 then
print(round((clock() - c) * 1000), round((garbage('count') - g) * 1024), n)
PauseTimer(GetExpiredTimer())
DestroyTimer(GetExpiredTimer())
return
end
local group = CreateGroup()
GroupEnumUnitsOfPlayer(group, player)
--actions
GroupClear(group)
DestroyGroup(group)
end)
Заодно можно измерить его показатели используя миллисекунды и байты.
время | память |
---|---|
10460 | 85420 |
Намаявшись перезапускать этот тест, я заметил, что время колебалось в пределах 10350-10500, а память всегда была равна 85420. Поэтому можно смело внести поправку:
print(round((clock() - c) * 1000) - 10300, round((garbage('count') - g) * 1024) - 85420, n)
время | память |
---|---|
160 | 0 |
Теперь осталось определиться с действиями над юнитами при переборе, чтоб возможный оптимизатор не прибил цикл.
if UnitAlive(target) then BlzSetUnitName(target, n) end
ForGroup
---@param whichGroup group
---@param callback code
function ForGroup(whichGroup, callback) end
Самая первая функция, с которой сталкиваются при переводе кода из GUI в JASS.
ForGroup(group, function()
local target = GetEnumUnit()
if UnitAlive(target) then BlzSetUnitName(target, n) end
end)
время | память |
---|---|
130 | 37181 |
Анонимную функцию, передаваемую в callback можно конечно вынести за пределы таймера, но тогда единственный вариант передать n в BlzSetUnitName(target, n) это сделать n глобальной. Так как мы не учим детей плоому есть более лучшие решения, можно просто запомнить и никогда не использовать ForGroup.
FirstOfGroup
---@param whichGroup group
---@return unit
function FirstOfGroup(whichGroup) end
Возвращает первый элемент группы или nil если группа пуста. В связи с этим возник хитрый способ: запускать бесконечный цикл и удалять юнита из группы. Недостаток которого очевиден - группу нельзя перебрать дважды. Как и у любого другого популярного метода, у него возникло кучу вариантов на любой вкус:
while true do
local target = FirstOfGroup(group)
if target == nil then break end
if UnitAlive(target) then BlzSetUnitName(target, n) end
GroupRemoveUnit(group, target)
end
local target = FirstOfGroup(group)
while target ~= nil do
if UnitAlive(target) then BlzSetUnitName(target, n) end
GroupRemoveUnit(group, target)
target = FirstOfGroup(group)
end
repeat
local target = FirstOfGroup(group)
if target ~=nil and UnitAlive(target) then BlzSetUnitName(target, n) end
GroupRemoveUnit(group, target)
until target == nil
время | память |
---|---|
130 | 35893 |
BlzGroupUnitAt
---@param whichGroup group
---@param index integer
---@return unit
function BlzGroupUnitAt(whichGroup, index) end
Как долго сообщество ждало этой функции. Она позволяет получить юнита по индексу. Но чтоб максимально эффективно её использовать, нам понадобится ещё одна функция, назначение которой можно понять из названия:
---@param whichGroup group
---@return integer
function BlzGroupGetSize(whichGroup) end
Соединив их вместе можно замерить производительность:
for index = BlzGroupGetSize(group) - 1, 0, -1 do
local target = BlzGroupUnitAt(group, index)
if UnitAlive(target) then BlzSetUnitName(target, n) end
end
время | память |
---|---|
130 | 35893 |
Преимущество этого способа в том, что вам не нужно очищать группу после перебора, не используются callback функции и из-за того, что группа перебирается с конца вы можете прям в цикле удалить из неё юнита не боясь смещения.
Заключение
Заключение прекрасно написал Bergi_Bear к предыдущей версии статьи:
Но можно оптимизировать код, например вынеся создание группы до создания таймера:Нет никакой разницы каким вы способом будете перебираться своих 20 юнитов
local group = CreateGroup()
local n, g, c = 0, garbage('count'), clock()
TimerStart(CreateTimer(), 0.01, true, function()
n = n + 1
if n > 1000 then
DestroyGroup(group)
print(round((clock() - c) * 1000) - 10300, round((garbage('count') - g) * 1024) - 85420, n)
PauseTimer(GetExpiredTimer())
DestroyTimer(GetExpiredTimer())
return
end
GroupEnumUnitsOfPlayer(group, player)
-- actions
GroupClear(group)
end)
метод | время | память |
---|---|---|
ForGroup | 120 | -47419 |
FirstOfGroup | 120 | -48707 |
BlzGroupUnitAt | 120 | -48707 |
Напоследок можно вспомнить слова Martin Golding:
... и перебирать группы как вам душе угодно.Всегда пишите код так, будто сопровождать его будет склонный к насилию психопат, который знает, где вы живете.
Код
--@author https://xgm.guru/p/wc3/for-group
do
local InitGlobalsOrigin = InitGlobals
function InitGlobals()
InitGlobalsOrigin()
local garbage = collectgarbage
local clock = os.clock
local player = Player(PLAYER_NEUTRAL_PASSIVE)
local function round(num)
if num >= 0 then return math.floor(num + .5)
else return math.ceil(num - .5) end
end
for i = 1, 1000 do
CreateUnit(player, FourCC('hfoo'), 0, 0, 0)
end
local n, g, c = 0, garbage('count'), clock()
TimerStart(CreateTimer(), 0.01, true, function()
n = n + 1
if n > 1000 then
print(round((clock() - c) * 1000) - 10300, round((garbage('count') - g) * 1024) - 85420, n)
PauseTimer(GetExpiredTimer())
DestroyTimer(GetExpiredTimer())
return
end
local group = CreateGroup()
GroupEnumUnitsOfPlayer(group, player)
--actions
GroupClear(group)
DestroyGroup(group)
end)
--[[
ForGroup(group, function()
local target = GetEnumUnit()
if UnitAlive(target) then BlzSetUnitName(target, n) end
end)
while true do
local target = FirstOfGroup(group)
if target == nil then break end
if UnitAlive(target) then BlzSetUnitName(target, n) end
GroupRemoveUnit(group, target)
end
local target = FirstOfGroup(group)
while target ~= nil do
if UnitAlive(target) then BlzSetUnitName(target, n) end
GroupRemoveUnit(group, target)
target = FirstOfGroup(group)
end
repeat
local target = FirstOfGroup(group)
if target ~=nil and UnitAlive(target) then BlzSetUnitName(target, n) end
GroupRemoveUnit(group, target)
until target == nil
for index = BlzGroupGetSize(group) - 1, 0, -1 do
local target = BlzGroupUnitAt(group, index)
if UnitAlive(target) then BlzSetUnitName(target, n) end
end
]]
end
end
Ред. Берги
0.86 > 0.82, чё? это то самое что имеют ввиду когда говорят:
Способ №1 быстрее способа №2 на 0.00001 наносекунд?
В реалиях варкрафта как это применимо? кто в здравом уме будет перебирать 2000 юнитов когда игра на топ железе залагает при 100 юнитах на экране...
Статья замечательная, и показывается все известные (почти) на данный момент методы, но я здесь вижу лишь 1 посыл:
Нет никакой разницы каким вы способом будете перебирать своих 20 юнитов
NazarPunk, Так что по скорости перебора массива 2000 юнитов циклом?
Ред. PT153
Также стоит проводить тесты параллельно, ПОСЛЕ старта игры, а не во время инициализации.
Ред. PT153
Про параллельно я имел ввиду, чтобы каждый тест запускался за другим, и так несколько раз подряд, например раз 1000, где потом выведется среднее значение и самое частое значение для каждого способа. Вот в этом реально будет смысл.
А пока выходит то, что я написал в предыдущем комментарии.
Ред. nazarpunk
Код не проверен на реальной луа машине, но вроде должно работать, если я все правильно помню.
Преимущество над анонимными функциями в чистом виде - контролируемый кложур, содержащий только нужные нам данные. Минус - более медленный старт за счет дополнительного вызова функции создающей каллбек.