Инструкция по написанию скриптов. (Обязательно к прочтению, соблюдению)

Статус
В этой теме нельзя размещать новые ответы.
Оффтоп: Давно меня просили написать гайд : "Как писать скрипты", простите я не напишу этот гайд - т.к. нужно сначала гайд "Как научится программировать".
Для начала полезные ссылки, которые нам пригодятся во время написания\тестирования:
  1. - все что доступно из встроенных функций DOTA 2, подглядывайте в скрипты других людей, которые я буду рекомендовать.
  2. - все что доступно из самого LUA, тут также всегда можно просить помощь у Google.
  3. - тут ищем , , если нужно будет добавлять картинки\частицы.
  4. - сюда обращаемся при синтаксических ошибках - в логе это "try call a string value".
Теперь очень важно прочитать следующий список тестов от которых я буду дальше отталкиваться .
А там на самом деле очень важные правила, которые нужно будет соблюдать - чтобы ваши скрипты работали и при этом не кушали такой важный для пользователей FPS.

Самое важное правило - Используйте таймеры, если вам не нужно 30-50 раз в секунду (а именно столько вызывается OnUpdate) что-то рассчитывать своим скриптом или проверять - то делаем проверку по времени.
Код:
--Объявляем таймеры и выставляем время обновления.
InfoScreen.EnemyTeamStatus.Timer = 0;
InfoScreen.EnemyTeamStatus.TimerInterval = 0.5;
--.. тут ваш код функций...
function InfoScreen.EnemyTeamStatus.OnUpdate()
  if Menu.IsEnabled(InfoScreen.Menu.EnemyTeamStatus)
  and Engine.IsInGame()
  and (InfoScreen.EnemyTeamStatus.Timer <= GameTime)
  then
    InfoScreen.EnemyTeamStatus.Timer = GameTime + InfoScreen.EnemyTeamStatus.TimerInterval;
--полезный код ниже
  end;
end;
--вызов самой функции
function InfoScreen.OnUpdate()
--обновляем время в локальную переменную об этом ниже
GameTime = GameRules.GetGameTime();
InfoScreen.Menu.LoadSettings();--вызывается раз в 0.5 не часто в меню люди меняют что-то
InfoScreen.EnemyTeamStatus.OnUpdate();--обновляем ВСЕХ вражеских героев раз в 0.3 для записи всех кд и статов НР\МР и т.д. чаще не нужно
end;
Когда это нужно делать:
  • Обновление ваших списков (0.2-0.5) - предметы, способности, обновление меню, пере расчеты относительно экрана(разрешения), другие вещи не часто меняющиеся со временем, еще какие-то вещи не сильно привязанные к СКОРОСТИ использования пользователем.
Когда это НЕ нужно делать:
  • При рисовании (статику рисуем в OnUpdate, динамику в OnDraw), когда настройки висят на кнопке (другой инпут от пользователя), когда нужно выполнить действие мгновенно (допустим рассчитали позицию и ждем когда можно дать санстрайк\хук или другой спел, но только когда это действительно нужно)
В отрисовке - ТОЛЬКО ОТРИСОВКА, максимум код который ниже, никаких вызовов глобальных функций т.е. только функции из Renderer:
Код:
function InfoScreen.ManaBar.OnDraw()
  if User and InfoScreen.Menu.ManaBar or InfoScreen.Menu.HealthText and Engine.IsInGame() then
    for heroent, EnemyHero in pairs(EnemyHeroes) do
      if EnemyHero.Valid then
        local origin = Entity.GetAbsOrigin(heroent);
        local HBO = EnemyHero.HBO;
        origin:SetZ(origin:GetZ() + HBO);
        local hx, hy = Renderer.WorldToScreen(origin);
        if Renderer.IsOnScreen(hx, hy) and InfoScreen.Menu.ManaBar then
          if (InfoScreen.Renderer.XBarOffset == 0) or (InfoScreen.Renderer.YBarOffset == 0) then
            InfoScreen.Renderer.CalcPanelSize();
          end;
          local MP = ceil(EnemyHero.MP);
          local MaxMP = ceil(EnemyHero.MaxMP);
          local MPText = MP.."/"..MaxMP;
          Renderer.DrawHPBar(hx + InfoScreen.Renderer.XBarOffset, hy + InfoScreen.Renderer.YBarOffset, InfoScreen.Renderer.HPBarWidth, InfoScreen.Renderer.HPBarHeight, {79, 120, 250, 255}, (MP / MaxMP), MPText, -2);
        end;
        if Renderer.IsOnScreen(hx, hy) and Menu.IsEnabled(InfoScreen.Menu.HealthText) then
          local HP = ceil(EnemyHero.HP);
          local MaxHP = ceil(EnemyHero.MaxHP);
          local HPText = HP.."/"..MaxHP;

          Renderer.SetDrawColor(222, 222, 222, 255);
          local w, h = Renderer.GetTextSize(InfoScreen.Renderer.Font, HPText);
          Renderer.DrawTextAlign(InfoScreen.Renderer.Font, hx + InfoScreen.Renderer.XBarOffset,
          hy + InfoScreen.Renderer.YBarOffset + InfoScreen.Renderer.HPBarHeight, InfoScreen.Renderer.HPBarWidth, InfoScreen.Renderer.HPBarHeight, HPText, 0, 0);
        end;
      end;
    end;
  end;
end;

Следующее правило - Локализуйте Переменные, вызовы, списки - кешируйте данные!!! (Тесты 1, 8, 11, 12, 13)
В тестах конечно же хорошие примеры, но я Вам приведу рабочие примеры:
  • Выше уже видели локальную переменную GameTime, если у Вас вызовов функции больше 5-10 то нужно ОБЯЗАТЕЛЬНО локализовать их в одну переменную.
    Код:
    --наиболее часто встречающейся вещи
    local GameTime = 0;--функция в переменную
    local User = nil;--объект\значение
    local ceil = math.ceil;--локализация функции
    local floor = math.floor;--локализация функции
    --проверка большого меню
    function InfoScreen.Menu.LoadSettings()
      InfoScreen.ItemPanel.EnabledSide = Menu.IsEnabled(InfoScreen.Menu.ItemPanel.EnabledSide);
      InfoScreen.ItemPanel.HeroPanel = Menu.IsEnabled(InfoScreen.Menu.ItemPanel.HeroPanel);
    end;
  • НЕ ИСПОЛЬЗУЙТЕ ВООБЩЕ, лучше объявите ее и передайте\вызовите их!
  • Локализация\Кеширование объектов\таблиц объектов, чаще всего это LocalHero и все наши параметры и вражеские герои.
    Код:
    --подобные функции можно сделать для любых обьектов которые вы используете и ТОЛЬКО ТЕ ДАННЫЕ КОТОРЫЕ ВАМ НУЖНЫ
    function UpdateHeroInfo(object, entity)
      if (entity and (entity ~= 0)) then
        if not object then
          object = {};
          object.LastUpdateTime = 0;
          object.Entity = entity;
          --static properties ЭТИ ДАННЫЕ НЕ МЕНЯЮТСЯ СО ВРЕМЕНЕМ
          object.Name = NPC.GetUnitName(entity);
          object.IsRanged = NPC.IsRanged(entity);
          object.TeamNum = Entity.GetTeamNum(entity);
        end;
        object.LastUpdateTime = GameTime;
        --получем\нужны ли нам данные
        object.Visible = NPC.IsVisible(entity);
        object.Alive = Entity.IsAlive(entity);
        object.Valid = (object.Visible and object.Alive);
        if object.Valid then
          object.HP = Entity.GetHealth(entity);
          object.MaxHP = Entity.GetMaxHealth(entity);
          object.MP = NPC.GetMana(entity);
          object.MaxMP = NPC.GetMaxMana(entity);
          object.Damage = NPC.GetTrueDamage(entity);
          object.Level = NPC.GetCurrentLevel(entity);
        end;
        return true;
      end;
    end;
    UpdateHeroInfo(User,Heroes.GetLocal());
    --Далее в функциях используем
    function InfoScreen.OnUpdate()
      GameTime = GameRules.GetGameTime();
      if ((GameTime - User.LastUpdateTime) > User.UpdateTime) then
        UpdateHeroInfo(User,Heroes.GetLocal());
      end;
      --и используем уже данные которые хотим
      if (User.HP < (User.MaxHP * 0.15)) then
        -- у нас мало НР можно выпить фласку
      end;
    end;
Дальше пойдут простые правила как и в тестах, но не много по другому опишу их:
  • Умножение - быстрее деления, если это возможно лучше умножайте на 0.1, а не /10.
  • Рассчитывайте один раз, одинаковые расчеты - часто нужно для рисования панелек или расчета DMG по героям, если не изменились артефакты и скилы - нет смысла пересчитывать урон по герою.
  • Не используете Insert для добавления в таблицу - лучший способ TableName[index]=value;
  • Не используйте pairs, ipairs в циклах, где можно обойтись без этого - добавьте в список циклом и запомните где-то количество, можно использовать #TableName.
  • Если знаете кол-во элементов в таблице - задайте это заранее при создании (врагов чаще всего 5), если часто нужно таблица из 5ти элементов зададим ее как TableName = {true, true, true, true, true}; Если делаете это в цикле и часто - создайте локальную переменную (Тест 13 листинг 1)
Теперь общие правила по программированию, для LUA:
  • Инкапсулируйте код, это читается и смотрится на много лучше и в итоге потом проще редактировать, чем тонны кода:
    Код:
    function InfoScreen.OnUpdate()
      GameTime = GameRules.GetGameTime();
    
      InfoScreen.Menu.ItemPanel.LoadSettings();
      InfoScreen.Renderer.CalcPanelSize();
       
         User:Update(Heroes.GetLocal());
      InfoScreen.EnemyHeroes.OnUpdate();
    end;
  • Все что повторяется (расчеты какие-то или проверки, для одного параметра) - тоже оформляйте в виде функции отдельной, как я это показывал в примерах выше.
  • Называйте переменные своими именами (да это иногда сложно) и всегда в одинаковом стиле (можете отдельно почитать об этом в интернете), не используйте нижнее подчеркивание в именах!
  • Хорошая IDE - Visual Studio Code (там есть для АПИ доты даже библиотеки).
  • Отступы и переносы строк, не делайте if .. then .. end; в одну строку.
 
Тема на жесткой модерации, не пишите бреда, для благодарности - есть кнопка, если есть серьезные замечания - пишите конкретно и подробно, ну или если есть что добавить по оптимизации - аналогично.
 
Вот подъехал и отличный пример
Код:
local wisp = {}
wisp.optionEnable = Menu.AddOptionBool({"Hero Specific", "Io"}, "Auto TP After Relocate", false)
wisp.toggleKey = Menu.AddKeyOption({"Hero Specific", "Io"}, "Toggle Key", Enum.ButtonCode.KEY_NONE)
-переменные не локальные
font = Renderer.LoadFont("Tahoma", 18, Enum.FontWeight.BOLD)
toggled1 = false
base = nil
function wisp.OnUpdate()
    if not Heroes.GetLocal() or not Menu.IsEnabled(wisp.optionEnable) then return end
--каждый раз одно и тоже получаем, хотя можно было один раз за игру
    local myHero = Heroes.GetLocal()
    local myTeam = Entity.GetTeamNum(myHero)
    if NPC.GetUnitName(myHero) ~= "npc_dota_hero_wisp" then return end
    if Menu.IsKeyDownOnce(wisp.toggleKey) then
        if toggled1 == false then
            toggled1 = true
        else
            toggled1 = false
        end
    end
    if not toggled1 then return end
--два раза получаем модифаер
    if NPC.HasModifier(myHero, "modifier_wisp_relocate_return") then
        local mod = NPC.GetModifier(myHero, "modifier_wisp_relocate_return")
--каждый раз ищем тп свиток
        local tp = NPC.GetItem(myHero, "item_tpscroll", true)
        if not tp then
            tp = NPC.GetItem(myHero, "item_travel_boots", true)
            if not tp then
                tp = NPC.GetItem(myHero, "item_travel_boots_2", true)
            end
        end
-каждый раз пересчитываем
        local dieTime = Modifier.GetCreationTime(mod) + 12
        if myTeam == 3 then
            base = Vector(7264.000000, 6560.000000, 512.000000)
        else
            base = Vector(-7317.406250, -6815.406250, 512.000000)
        end
--проверка не верная, нужно уточнить значение.
        if tp and Ability.IsReady(tp) and dieTime - GameRules.GetGameTime() <= 3 then
            Ability.CastPosition(tp, base)
        end
    end
end
function wisp.OnDraw()
    if not Menu.IsEnabled(wisp.optionEnable) or not Heroes.GetLocal() then return end
--В Он Драв - только отрисовка!
    if NPC.GetUnitName(Heroes.GetLocal()) ~= "npc_dota_hero_wisp" then return end
    local x, y = Renderer.GetScreenSize()
--если разрешение изменится на 1 пиксель, это все не сработает
    if x == 1920 and y == 1080 then
        x, y = 1150, 910
    elseif x== 1600 and y == 900 then
        x, y = 950, 755
    elseif x== 1366 and y == 768 then
        x, y = 805, 643
    elseif x==1280 and y == 720 then
        x, y = 752, 600
    elseif x==1280 and y == 1024 then
        x, y = 800, 860
    elseif x==1440 and y == 900 then
        x, y = 870, 755
    elseif x== 1680 and y == 1050 then
        x, y = 1025, 885
    end
    if toggled1 then
        Renderer.SetDrawColor(90, 255, 100)
        Renderer.DrawText(font,x,y, "[Auto-TP: ON]")
    else
        Renderer.SetDrawColor(255, 90, 100)
        Renderer.DrawText(font, x, y, "[Auto-TP: OFF]")
    end
end
return wisp
Минимальные изменения на быструю руку КАК НУЖНО было написать.
Код:
local wisp = {}
wisp.optionEnable = Menu.AddOptionBool({"Hero Specific", "Io"}, "Auto TP After Relocate", false)
wisp.toggleKey = Menu.AddKeyOption({"Hero Specific", "Io"}, "Toggle Key", Enum.ButtonCode.KEY_NONE)
--переменные теперь локальные, но лучше их в 1 массив по типу перенести конечно
local font = Renderer.LoadFont("Tahoma", 18, Enum.FontWeight.BOLD)
local toggled1 = false
local base = nil
local myHero = nil;
local myTeam = 0;
local casted = false;
local modWispRelocate = nil;
local TPitem = nil;
local dieTime = 0;
--как я сделал тут
local StatusScreenPos = {};
StatusScreenPos.x = 0;
StatusScreenPos.y = 0;

--получаем данные не меняющиейся со временем
function wisp.init()
--только если мы в игре
    if Engine.IsInGame() then
--героя обычно в игре не меняют.
        myHero = Heroes.GetLocal();
        myTeam = Entity.GetTeamNum(myHero);
        dieTime = 0;
        if myTeam == 3 then
            base = Vector(7264.000000, 6560.000000, 512.000000)
        else
            base = Vector(-7317.406250, -6815.406250, 512.000000)
        end
--расчет один раз по формуле, а не неивестными условиями и значениями.
        local x, y = Renderer.GetScreenSize();
        StatusScreenPos.x = math.floor(x * 0.594);
        StatusScreenPos.y = math.floor(y * 0.84);
    end;
end;
--если игра уже стартанула и Софт включили
wisp.init();
--или на старте игры вызываем их
function wisp.OnGameStart()
    wisp.init();
end;

function wisp.OnUpdate()
    if not myHero or not Menu.IsEnabled(wisp.optionEnable) then return end
--проверку по названию лучше тоже делать один раз на весь скрпит, но я оставил тут, так делат ьне хорошо
    if NPC.GetUnitName(myHero) ~= "npc_dota_hero_wisp" then return end
    if Menu.IsKeyDownOnce(wisp.toggleKey) then
        if toggled1 == false then
            toggled1 = true
        else
            toggled1 = false
        end
    end
    if toggled1 then
--локализация частого вызова
        local GameTime = GameRules.GetGameTime();
--вызов функции только когда это необходимо, но лучше еще добавть таймер в 0.5 сек, когда у нас целых 10 есть не пропустим
        if (dieTime <= 0) or ((dieTime - GameTime) < 0) then
--получаем модифаер один раз
            modWispRelocate = NPC.GetModifier(myHero, "modifier_wisp_relocate_return");
--и проверяем его один раз
            if modWispRelocate then
-расчитываем время один раз если получили, больше этого не будет
                dieTime = Modifier.GetCreationTime(modWispRelocate) + 12;
                Log.Write(dieTime - GameTime);
            end;
        else
            if modWispRelocate and not casted then
--уточнили время, учли пинг
                if (dieTime - GameTime <= 2.96 + NetChannel.GetAvgLatency(Enum.Flow.FLOW_OUTGOING)) and (dieTime- GameTime > 2.8) then
--проверяем ТП только, если можем сделать комбину, но лучше и это вынести в отдельную функцию
                    local TPitem = NPC.GetItem(myHero, "item_tpscroll", true)
                    if not TPitem then
                        TPitem = NPC.GetItem(myHero, "item_travel_boots", true)
                        if not TPitem then
                            TPitem = NPC.GetItem(myHero, "item_travel_boots_2", true)
                        end
                    end
                    if TPitem and Entity.IsAbility(TPitem) and Ability.IsReady(TPitem) then
                        Log.Write(dieTime - GameTime);
                        Ability.CastPosition(TPitem, base)
                        casted = true;
                    end;
                end
            else
                casted = false;
            end
        end;
    end;
end

function wisp.OnDraw()
    if not Menu.IsEnabled(wisp.optionEnable) or not Heroes.GetLocal() then return end
    if toggled1 then
        Renderer.SetDrawColor(90, 255, 100)
        Renderer.DrawText(font, StatusScreenPos.x, StatusScreenPos.y, "[Auto-TP: ON]")
    else
        Renderer.SetDrawColor(255, 90, 100)
        Renderer.DrawText(font, StatusScreenPos.x, StatusScreenPos.y, "[Auto-TP: OFF]")
    end
end
return wisp

Проверка оптимизации вашего скрипта:
Код:
https://github.com/ivanius51/UmbrellaD2LUA/blob/master/PerfomanceTest.lua
В логе при включении будут данные по затратам в мс на каждый апдейт.
Для хорошего(компьютера 150+ фпс):
Маленькие скрипты
- AVGTime=1, ScriptTime=1-5, Update=0-3
Средние (скрипт героя, универсальный большой) - AVGTime=<15, ScriptTime=<50, Update=<35
Большие (несколько скриптов в одном FAIO) - AVGTime=<50, ScriptTime=<150, Update=<100

Код:
FAIO ScriptTime=66.2(=0/0) Update=65.5/148 Draw=0.7/522 AVGTime=14
  LastHitCreep ScriptTime=5.3(=0/0) Update=5.1/151 Draw=0.2/638 AVGTime=1
  много крипов рядом - LastHitCreep ScriptTime=16.0(=0/0) Update=16.0/151 Draw=0.0/215 AVGTime=1
InfoScreen ScriptTime=108.8(=0/0) Update=94.3/148 Draw=14.5/489 AVGTime=20

Если хотите по практиковаться писать скрипты, или игры:
- заходите сюда, выбираете язык (LUA) и учитесь.
 
Последнее редактирование модератором:
Статус
В этой теме нельзя размещать новые ответы.