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

Какие я знаю варианты:

  1. Дефолтный скилл рексара стадо ящериц, он там проблемы с углом поворота и настройками в целом, самый примитивный но норм
  2. Система снарядов:
    • 1 таймер на движение всех снарядов
    • перебор группы в которой ищется враг вокруг снаряда в определённом радиусе
  3. Система снарядов для 1 игрока
    • тот же перебор таймером
    • но столкновение определяется через IsunitInrange (снаряд, наш герой)
  1. Система снарядов + аура жара (мой фаворит):
  • снаряды снова летят на таймере
  • в качестве столкновения используется событие получения урона 131 патча, сами же снаряды излучают жар преисподни (постоянный)
  1. Медленные волны силы/ тёмные стаи:
  • 1 дамми кастует заклинание в указанную точку
  • к сожелению период урона в таком случае странный и снаряд не будет умирать при столкновении с героем
Из требований будут скорее всего такие параметры:
  • одновременное число снарядов от 10 до 300
  • скорость снарядов от 200 до 1000
Я понимаю, что работать будет прекрасно даже если каждый из 100 снарядов посадить на отдельный таймер, но всё же... какой способ самый оптимальный для слабых пк.
Если есть другие варианты реализации - пишите в комменты

то скорость разная, то вылетают не из героя а из какой то псевдо центральной точки
в общем я полностью добился желаемого результата, никакого прерывания, ни каких лагов и странных поведений (то что снаряды врезаются в трупы так и задумано =))
вот мой код
//! beginusercode

--какие то общие функции
function MoveX (x,  Dist,  Angle)
    return x+Dist*Cos(Angle*0.0175)
end
function MoveY (x,  Dist,  Angle)
    return x+Dist*Sin(Angle*0.0175)
end
function AbilityId(id)
    return id:byte(1) * 0x1000000 + id:byte(2) * 0x10000 + id:byte(3) * 0x100 + id:byte(4)
end

function Out(x,y)
    return ( ( GetRectMinX(bj_mapInitialPlayableArea) <= x ) and ( x <= GetRectMaxX(bj_mapInitialPlayableArea) ) and ( GetRectMinY(bj_mapInitialPlayableArea) <= y ) and ( y <= GetRectMaxY(bj_mapInitialPlayableArea) ) ) or IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY) == false
end

GetTerrainZ_location = Location(0, 0)
function GetTerrainZ(x,y)
    MoveLocation(GetTerrainZ_location, x, y);
    return GetLocationZ(GetTerrainZ_location);
end

function ehandler( err )
    print( "ERROR:", err )
end
--/////// глобалки (хотя какая разница где объявить то)
perebor=CreateGroup()

--/////// триггер

    local trigger = CreateTrigger()
    for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
        TriggerRegisterPlayerUnitEvent(trigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
    end
    TriggerAddCondition(trigger, Condition(function() return
        GetOwningPlayer(GetTriggerUnit()) == Player(0)
    end))
local d=0
TriggerAddAction(trigger, function()
    local u=GetTriggerUnit()
    local z=GetTerrainZ(GetUnitX(u),GetUnitY(u))
        print("perodstart")
        TimerStart(CreateTimer(), 0.1, true, function()
        d=d+1

 --print("abiclick "..d)
 -- будущая фукция запуска снаряда
 local x=GetUnitX(u)
 local y=GetUnitY(u)
 local eff=AddSpecialEffect("Abilities\\Weapons\\DemolisherFireMissile\\DemolisherFireMissile.mdl", x, y)
 local d2=1000
 local a=GetUnitFacing(u)

 TimerStart(CreateTimer(), 0.032, true, function()
 d2=d2-10
 x=MoveX(x,25,a)
 y=MoveY(y,25,a)
 BlzSetSpecialEffectPosition(eff, x, y, GetTerrainZ(x,y)+30)
-- урон
local e=nil
GroupEnumUnitsInRange(perebor,x,y,80,null)
while true do
	e = FirstOfGroup(perebor)
	if e == nil then break end
if IsUnitEnemy(e, GetOwningPlayer(u)) then
    UnitDamageTarget( u, e, BlzGetUnitBaseDamage(u, 1), false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_FIRE, WEAPON_TYPE_WHOKNOWS )
   -- print("наносим урон")
    DestroyEffect(eff)
    eff=nill 
end

	GroupRemoveUnit(perebor,e)
end

--print(d2)

 if d2<=0 or  Out(x,y)==false or eff==nil then
  --  print("УМРИ!")
    DestroyEffect(eff)
    DestroyTimer(GetExpiredTimer())
 end

 end)

    end)
end)





//! endusercode
а вот и карта
Выражаю огромную благодарность NazarPunk, и Prog за оказанную помощь и наставления
Выводы:
Более навороченные (в техническом плане) способы не всегда самые оптимальные
Точно также можно двигать эффекты и на мемхаке, так что 126 пат так же может удостоится высокой производительностью для огромного количества снарядов
Мой комп держит на 1 экране около 700 объектов в режиме 60+ FPS (с отключенной вертикальной синхронизацией, это когда макс фпс за 200)
Загруженные файлы
`
ОЖИДАНИЕ РЕКЛАМЫ...
16
ну да, это самая быстрая конструкция при входных условиях, что нам содержимое группы нигде не нужно, а все действия над ними нужно выполнить лишь 1 раз
33
Кароче еще способ:
Движение: через создание и движение юнита-снаряда (приказ мув)
Столкновение: через детект урона жара
код луа
//! beginusercode

--какие то общие функции
function MoveX (x,  Dist,  Angle)
    return x+Dist*Cos(Angle*0.0175)
end
function MoveY (x,  Dist,  Angle)
    return x+Dist*Sin(Angle*0.0175)
end
function AbilityId(id)
    return id:byte(1) * 0x1000000 + id:byte(2) * 0x10000 + id:byte(3) * 0x100 + id:byte(4)
end
--триггер запуска снарядов (примитивный)
MissleClick = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
     TriggerRegisterPlayerMouseEventBJ(MissleClick, Player(i), bj_MOUSEEVENTTYPE_DOWN)
end
TriggerAddCondition(MissleClick, Condition(function() return BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT end))
TriggerAddAction(MissleClick, function()
-- весь код экшена
local u=udg_Butcher
local mx=BlzGetTriggerPlayerMouseX()
local my=BlzGetTriggerPlayerMouseY()
local x=GetUnitX(u)
local y=GetUnitY(u)
local a=bj_RADTODEG*Atan2(my - y, mx - x)
--print(a)
local unit ud=CreateUnit(GetOwningPlayer(u), AbilityId('e000'), MoveX(x,30,a) ,MoveY(y,30,a) , a)

local nx=MoveX(x,2500,a)
local ny=MoveY(y,2500,a)
IssuePointOrder(ud, "move", nx, ny)

end)

-- триггер отлова урона через жар


gg_trg_DamageEvent = CreateTrigger(  )
for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
    TriggerRegisterPlayerUnitEvent(gg_trg_DamageEvent, Player(i), EVENT_PLAYER_UNIT_DAMAGING)
end
TriggerAddCondition( gg_trg_DamageEvent, Condition( function () return  BlzGetEventAttackType( ) == ConvertAttackType( 0 ) and  BlzGetEventDamageType( ) == ConvertDamageType( 8 ) and  BlzGetEventWeaponType( ) == ConvertWeaponType( 0 ) end))
TriggerAddAction( gg_trg_DamageEvent, function ()
BlzSetEventDamage(300)
KillUnit(GetEventDamageSource())
--print("урон от жара")

    end)
     

//! endusercode
Как же их колбасит, система дефолтного движения вызывает дикие дёрганья и неадекватность (но я её и не рассматривал, просто решил проверить на новом патче)
гифка
Загруженные файлы
20
движение юнита-снаряда (приказ мув)
Так, а скорость будет 522 максимальной.. Или нет?
33
да, но мб для снарядов от которых надо увернуться это достаточно, в любом случае уже сделал триггерное движение, это так экспериментик был
Кароче, пока что больше 200 юнитов снарядов не держит используется (просто не держит 200 юнитов не важно движутся ли они), использовался:
lua+ юниты +1 таймер + детект урона через жар,
Следующий на очереди:
lua + смещение позиций эффектов + loopfirstOfGroup
1
Можно сделать юнитов, у которых заменена моделька, нету текстуры пути, и маленькая область жара преисподней внутри. И сделать, чтобы каждые 0.3 секунды они двигались со смещением на заданное направление. Смещение гдето на 5-20, чтобы не сильно быстро было.
30
Начал писать систему
//! beginusercode
do
    -- На момент патча 1.31 эта функция всегда возвращает 0. Поэтому создадим её локальный аналог.
    local function AbilityId(id)
        return id:byte(1) * 0x1000000 + id:byte(2) * 0x10000 + id:byte(3) * 0x100 + id:byte(4)
    end

    local BULLETS = {}
    local TIMER_PERIOD = 0.03125 --> 1/32
    local SPEED = 600
    local SPEED_INC = SPEED/(1/TIMER_PERIOD)

    -- Настройки
    local ABILITY_ID = AbilityId('SIWh')
    local ARC = 0.3
    local EFFECT = 'Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl'
    
    -- Код
    local GetTerrainZ_location = Location(0, 0)
    local function GetTerrainZ(x,y)
        MoveLocation(GetTerrainZ_location, x, y);
        return GetLocationZ(GetTerrainZ_location);
    end

    local function InMapXY(x, y)
        return
            x > GetRectMinX(bj_mapInitialPlayableArea)
            and
            x < GetRectMaxX(bj_mapInitialPlayableArea)
            and
            y > GetRectMinY(bj_mapInitialPlayableArea)
            and
            y < GetRectMaxY(bj_mapInitialPlayableArea)        
    end

    TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
        if #BULLETS == 0 then return end

        for i=#BULLETS, 1, -1
        do
            BULLETS[i].x = BULLETS[i].x + SPEED_INC*BULLETS[i].cos
            BULLETS[i].y = BULLETS[i].y + SPEED_INC*BULLETS[i].sin
            if InMapXY(BULLETS[i].x, BULLETS[i].y)
            then
                BlzSetSpecialEffectX(BULLETS[i].effect, BULLETS[i].x)
                BlzSetSpecialEffectY(BULLETS[i].effect, BULLETS[i].y)
            else
                DestroyEffect(BULLETS[i].effect)
                table.remove(BULLETS, i)
            end
            
        end

    end)

    local trigger = CreateTrigger()
    for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
        TriggerRegisterPlayerUnitEvent(trigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
    end
    --TriggerAddCondition(trigger, Condition(function() return true end))
    TriggerAddAction(trigger, function()
        TimerStart(CreateTimer(), 0.5, true, function()
            local caster = GetTriggerUnit()
            local x = GetUnitX(caster)
            local y = GetUnitY(caster)
            local angle = GetUnitFacing(caster)

            table.insert(BULLETS, {
                effect = AddSpecialEffect('units\\nightelf\\Wisp\\Wisp.mdl', x, y),
                x = x,
                y = y,
                angle = angle,
                cos = Cos(angle),
                sin = Sin(angle)
            })
        end)
    end)

end
//! endusercode
Вечером тесты напишу и буду над уроном думать.
Загруженные файлы
24
NazarPunk, использование анонимной функции в триггере без необходимости это, имхо, лишнее - лишний кложур за собой тащиш. Старт таймера там где он у тебя стартует тоже выглядит опасно - этот код же выполнится еще до триггеров инициализации карты.
30
prog, это только начало, потом как определюсь с функциями добавления снарядов, переделаю.
TimerStart(CreateTimer(), 0, false, function() print('test') end) выполнится после инициализации карты.
24
NazarPunk, первое срабатывание - да, может даже со 100% надежностью, а создание и старт таймера произойдут раньше. Я предпочитаю не рисковать и выношу такие вещи в места которые гарантированно не вызываются слишком рано.
Даже систему модулей/библиотек в итоге себе для этого пилить начал, хотя изначально не собирался сильно этим заморачиваться.
33
NazarPunk, спасибо что начал, а то я уже 3 день Прога мучаю, всё какие-то проблемы всплывают (вернее 1 проблема, падения потока таймера)
NazarPunk, а insert и remove это стандартный методы структур lua (аналог самописных .create и .destroy), а так даже всё понятно!
30
а insert и remove это стандартный методы структур lua
В lua таблицы) И да, это стандартные методы для таблиц.
Загруженные файлы
30
Добавил урон и счётчик эффектов
//! beginusercode
do
    -- На момент патча 1.31 эта функция всегда возвращает 0. Поэтому создадим её локальный аналог.
    local function AbilityId(id)
        return id:byte(1) * 0x1000000 + id:byte(2) * 0x10000 + id:byte(3) * 0x100 + id:byte(4)
    end

    local BULLETS = {}
    local TIMER_PERIOD = 0.03125 --> 1/32
    local SPEED = 600
    local SPEED_INC = SPEED/(1/TIMER_PERIOD)

    local DAMAGE_PERIOD = 0.5
    local DAMAGE_TIME = 0
    local IS_DAMAGE = false
    local DAMAGE_GROUP = CreateGroup()
    
    local BOARD

    -- Настройки
    local ABILITY_ID = AbilityId('SIWh')
    local ARC = 0.3
    local EFFECT = 'Abilities\\Spells\\Orc\\WarStomp\\WarStompCaster.mdl'
    
    -- Код
    local GetTerrainZ_location = Location(0, 0)
    local function GetTerrainZ(x, y)
        MoveLocation(GetTerrainZ_location, x, y);
        return GetLocationZ(GetTerrainZ_location);
    end

    local function InMapXY(x, y)
        return
            x > GetRectMinX(bj_mapInitialPlayableArea)
            and
            x < GetRectMaxX(bj_mapInitialPlayableArea)
            and
            y > GetRectMinY(bj_mapInitialPlayableArea)
            and
            y < GetRectMaxY(bj_mapInitialPlayableArea)        
    end

    TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
        if #BULLETS == 0 then return end

        DAMAGE_TIME = DAMAGE_TIME + TIMER_PERIOD

        IS_DAMAGE = DAMAGE_TIME >= DAMAGE_PERIOD

        if
            IS_DAMAGE
        then
            DAMAGE_TIME = 0
        end

        for i=#BULLETS, 1, -1
        do
            BULLETS[i].x = BULLETS[i].x + SPEED_INC*BULLETS[i].cos
            BULLETS[i].y = BULLETS[i].y + SPEED_INC*BULLETS[i].sin
            if
                InMapXY(BULLETS[i].x, BULLETS[i].y)
            then
                BlzSetSpecialEffectX(BULLETS[i].effect, BULLETS[i].x)
                BlzSetSpecialEffectY(BULLETS[i].effect, BULLETS[i].y)
                BlzSetSpecialEffectHeight(BULLETS[i].effect, 20)

                if
                    IS_DAMAGE
                then
                    GroupEnumUnitsInRange(DAMAGE_GROUP, BULLETS[i].x, BULLETS[i].y, 100, Filter(function()
                        local target = GetFilterUnit()
                        return
                            UnitAlive(target)
                            and
                            IsPlayerEnemy(GetOwningPlayer(BULLETS[i].caster), GetOwningPlayer(target))
                    end))
                    ForGroup(DAMAGE_GROUP, function()
                        UnitDamageTarget(BULLETS[i].caster, GetEnumUnit(), 50, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
                    end)
                    GroupClear(DAMAGE_GROUP)
                end
            else
                DestroyEffect(BULLETS[i].effect)
                table.remove(BULLETS, i)
            end 
        end

        if
            BOARD ~= nil
        then
            LeaderboardSetItemValue(BOARD, 0, #BULLETS)
        end
    end)


    local trigger = CreateTrigger()
    for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
        TriggerRegisterPlayerUnitEvent(trigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
    end
    TriggerAddCondition(trigger, Condition(function() return
        GetOwningPlayer(GetTriggerUnit()) == Player(0)
    end))
    TriggerAddAction(trigger, function()
        local caster = GetTriggerUnit()
        local x
        local y
        local angle
        UnitRemoveAbility(caster, GetSpellAbilityId())

        TimerStart(CreateTimer(), 0.1, true, function()
            x = GetUnitX(caster)
            y = GetUnitY(caster)
            angle = Deg2Rad(GetUnitFacing(caster))

            table.insert(BULLETS, {
                effect = AddSpecialEffect('units\\nightelf\\Wisp\\Wisp.mdl', x, y),
                caster = caster,
                x = x,
                y = y,
                angle = angle,
                cos = Cos(angle),
                sin = Sin(angle)
            })
        end)
    end)


    -- Board
    TimerStart(CreateTimer(), 1, false, function()
        BOARD = CreateLeaderboard()

        PlayerSetLeaderboard(GetLocalPlayer(), BOARD)
        LeaderboardSetLabel(BOARD, 'Статистика')
        LeaderboardDisplay(BOARD, true)
        
        LeaderboardAddItem(BOARD, "Эффекты", #BULLETS, Player(0))

        LeaderboardSetSizeByItemCount(BOARD, 1)
        
        DestroyTimer(GetExpiredTimer())
    end)

end
//! endusercode
В районе 150-200 эффектов общий таймер может тупо остановиться. Хотя таймеры добавления эффектов исправно работают.
Загруженные файлы
33
В районе 150-200 эффектов общий таймер может тупо остановиться
вот и у меня та же беда, только нельзя сказать что это происходит при 150-200 эффектах, может и при 3 и при 500 умереть =( беда
Опять таки надо понять сколько будет одновременно снарядов максимальное число, если же 200-300, то можно и каждый снаряд на отдельный таймер
(проверял не с пулями, а с щетчиком зарядав способности, примерно до 500 отдельных таймеров не вызывали лага даже на слабых пк, но там период был 1 секунда)
30
Начал тестить и вообще какая-то магия происходит. Если из другого таймера делать print(timer), то timer не остановится. Но отпадут действия триггеров или новые таймеры не смогут запуститься. Ещё нужно будет попробовать тупо пересоздавать таймер каждые неколько секунд и насыпать оптимизации.
22
а вы пробуй через цикл делать движение без таймера =)
33
pro100master, по подробней, а на луа есть какие нить аналоги таймеров? или в цикл можно wait засунуть?
30
а на луа есть какие нить аналоги таймеров?
Есть Sleep Function, но врятли они работать будут.
33
Ох, ну вот и руки дошли до проверки системы, да печально всё, но буду ковырять код и попробую перенести на отдельный таймер по снаряду.
А может кто напомнить, какой лимит поток, и какой лимит операций на потоке для 126? и на сколько всё улучшили в 131?
30
перенести на отдельный таймер по снаряду
Я тоже так думаю.
Bergi_Bear:
какой лимит операций на потоке
Если мне не изменяет память то:
  • 1.26: ~4000
  • 1.30: ~32000
33
NazarPunk, а если учесть, что время почти всегда случайное, когда умирает не может ли сборщик мусора косячить и сгребать таймер на помойку?
30
не может ли сборщик мусора косячить и сгребать таймер на помойку?
Скорее всего, глобальный таймер нагребает на себя кучу накладных расходов и прибивается по оплимиту. Посему лучше наплодить таймеров и пусть сами умирают.
33
NazarPunk, Проверил, переделал на ручной каст (но движение на 1 таймере), и получается что блок создания эффекта даже перестаёт работать, полный игнор события и вот этого блока кода
    local trigger = CreateTrigger()
    for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
        TriggerRegisterPlayerUnitEvent(trigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
    end
    TriggerAddCondition(trigger, Condition(function() return
        GetOwningPlayer(GetTriggerUnit()) == Player(0)
    end))
    TriggerAddAction(trigger, function()
        local caster = GetTriggerUnit()
        local x
        local y
        local angle
        --UnitRemoveAbility(caster, GetSpellAbilityId())

        x = GetUnitX(caster)
        y = GetUnitY(caster)
        angle = Deg2Rad(GetUnitFacing(caster))

        table.insert(BULLETS, {
            effect = AddSpecialEffect('units\\nightelf\\Wisp\\Wisp.mdl', x, y),
            caster = caster,
            x = x,
            y = y,
            angle = angle,
            cos = Cos(angle),
            sin = Sin(angle)
        })

    end)
когда падает таймер, то эффекты продолжают создаваться под ногами героя
100% не только таймер, вот вручную кастовал до 400 накликал, и всё исчезло, перестало работать событие (проверял принтом)... всё событие уничтожается и более не существует =(
30
когда падает таймер, то эффекты продолжают создаваться под ногами героя
Потому что таймеры эффектов не падают
Чтобы оставить комментарий, пожалуйста, войдите на сайт.