Воскресенье, 22.09.2024, 16:23
Приветствую Вас Гость | RSS
F.R.O.N.T.
Главная | Статьи | Регистрация | Вход
Меню сайта
Все новые статьи находяться на форуме!!! Видеть их могут только рарегистрированые пользователи...
Категории каталога
Мои статьи [28] Сценарии [0]
Пишите сюда свои сцинарии
Модострой [152]
Статьи по созданию модов
КАК СНЯТЬ ФИЛЬМ??? [0]
Статьи о том как снять фильмы
Мини-чат
Наш опрос
Почему зашли на сайт
1. Хотел узнать о том как сделать мод
2. Фанат сталкера
3. Нечайно
4. Автор - хороший человек
5. Посаветовали
6. Хотел узнать о том как снять фильм
Всего ответов: 98
Главная » Статьи » Модострой

Как правильно писать скрипты, не приводящие к вылетам

 

Как правильно писать скрипты, не приводящие к вылетам

 

Автор: kamikazze

             с дополнениями от  Меченый(Стрелок)  и xStream

            Отладив несколько скриптов на предмет вылетов решил написать это небольшое руководство, чтобы помочь скриптописателям избежать потенциальных пробем. Сразу оговорюсь - я программист, поэтому к отладке скриптов подхожу со своей профессиональной точки зрения, и то что я тут изложу вам - сие для программиста есть непреложная истина, и если вы хотите уменьшить количество глюков - старайтесь придерживаться нижеприведённого стиля программирования.

 

            Для начала маленький экскурс в историю. Итак, скрипты в сталкере написаны на языке Lua - разработали его бразильцы, он очень гибок, легко встраивается в игры и поэтому очень часто используется для написания скриптов любой степени сложности. Так вот, у этого языка есть несколько исключительно важных особенностей, которые, при их несоблюдении, будут приводить к вылетам вашего скрипта. Итак:

 

            Особенность первая. Типы данных и западло с nil.

 

            Сначала сделаю маленькое отступление, чтобы понятнее было. Итак, в Lua используются следующие операторы:

 

             =      оператор присваивания

             ==      сравнение, равно ли значение

            ~=      сравнение, НЕ равно ли значение

             <      сравнение, меньше ли значение

             >      сравнение, больше ли значение

            <=      сравнение, меньше ли значение или равно

            >=      сравнение, больше ли значение или равно

            and      логический оператор И

             not      логический оператор НЕ

             or      логический оператор ИЛИ

 

 

 

            Всё. Теперь поехали дальше... Язык Lua Изначально создавался для работы с большими строчными базами данных, поэтому ВСЕ виды конструкций в языке - это типы данных, т.е. по сути либо переменные, либо константы. Это касается так же и функций! Поэтому даже с функциями в Lua можно и нужно обращатся как с константами. Далее. Касательно, собственно "привычных" переменных. Обычные переменные в Lua получают свой тип данных только в момент присвоения им значения (запомните, это важно). При этом, в Lua есть такой важный и полезный тип данных как nil. nil - Это "пустота", т.е. отсутствие какого-либо значения. При этом этот тип используется компилятором скриптов для "сбора мусора", т.е. для освобождения занимаемой памяти. Смотрите пример, я объясню подробнее, что это значит:

 

local reminder_count

local exposure_count = 0

 

            Тут у нас 2 локальные переменные, одна из которых просто создана, а вторая - создана и инициализирована. Если сейчас обратиться к переменной reminder_count, считав её значение, то мы получим в качестве значения - nil, т.е. пустоту. Если же мы обратимся к переменной exposure_count - то получим, как и ожидали, 0 - так как мы это значение проинициализировали ЗАРАНЕЕ. При этом не надо путать nil и 0 - так как 0 - это всё-таки какая-то информация, а вот nil - это её полное отсутствие. Так вот, собственно, чем это чревато. Я уже сказал, что nil используется для сбора мусора. Вручную это работает так - когда вам переменная уже не нужна, вы просто пишете:

 

test_variable = nil

 

            И ваша переменная test_variable сотрётся из памяти, и любые ссылки на неё будут выдавать на выходе nil (типа нет такой переменной, "абонент вне зоны действия сети"). Ну так собственно вот, зачем я это всё рассказываю... если не определить значение переменной сразу, как это было сделано с переменной reminder_count, то любые попытки обратиться к ней внутри скрипта приведут к вылетам. Происходит это следующим образом: допустим, reminder_count - это у нас счётчик напоминаний. Т.е. мы о чём-то напоминаем игроку текстовыми сообщениями, и помечаем, сколько раз мы это сделали. Определили мы переменную именно так, как в примере выше, т.е. не задав ей никакого значения. Значение же её должно присваиваться ниже по коду, после первого оповещения юзера. При этом у нас есть в коде обращение к этой переменной, по которому решается что делать дальше.            

            Выглядит это примерно так:

function check_antirad_supplies()

if auto_injection_active then

      if antirad_check_delay == nil or game.get_game_time():diffSec(antirad_check_delay) > 60 then

         if not db.actor:object("antirad") and not (use_scientific_kit and db.actor:object("medkit_scientic")) then

            --обратите внимание сюда
          if reminder and reminder_count == 0 or reminder_count >= mins_till_next_remind then
            --обратите внимание сюда
            if use_text then
               local news_text = "%c[255,160,160,160]Автоматическая система ввода медицинских препаратов\\n".."%c[default]Напоминаю: %c[255,230,0,0]Противорадиационные препараты отстутствуют! %c[default]Автоматический ввод препаратов невозможен."
                  db.actor:give_game_news(news_text, "ui\\ui_iconsTotal", Frect():set(0,188,83,47), 0, 3000)
            end
            if use_sounds then
                local snd_obj
                  if use_custom_sounds then
                        snd_obj = xr_sound.get_safe_sound_object( [[HEV\no-anti-rad]] )
                  else
                        snd_obj = xr_sound.get_safe_sound_object( [[device\pda\pda_tip]] )
                  end
                if snd_obj then
                    snd_obj:play_no_feedback(db.actor, sound_object.s2d, 0, vector(), 1.0)
                 end
             end
            reminder_count = 1
            elseif reminder then
              reminder_count = reminder_count + 1
            end
          else
           radiation_warning = true
           reminder_count = 0
          end
         antirad_check_delay = game.get_game_time()
       end
    end
end

 

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

В итоге при запуске вышеприведённой функции check_antirad_supplies() нас получится что reminder_count не существует - он равен nil, и в итоге проверка

 

if reminder and reminder_count == 0 or reminder_count >= mins_till_next_remind then

выдаст ошибку и приведёт к вылету!

Почему? Да потому что НЕЛЬЗЯ сравнивать НИЧТО с вещественным значением.
            Именно поэтому, когда вы создаёте новую переменную, вы ОБЯЗАНЫ задать ей значение по умолчанию, даже если вы полностью уверены, что она нигде не будет использована до инициализации.

            Поверьте - в программировании бывает всё, и запросто может так случиться, что ваша переменная будет использована и спровоцирует вылет.

begin xStream upd. ---
Простой способ обойти такую кучу проверок и лишних инициализаций - использование or-оператора. пример

if (reminder_count or 0) >= mins_till_next_remind then

если переменная равна nil, то выражение вычисляется дальше и получается равным 0, а это уже число и сравнение происходит безболезненно
end xStream upd. ---

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

1) "Убийство" переменных

            Положим, у нас с reminder_count происходят ещё какие-нибудь занимательные вещи, например мы его значение зачем-нибудь присваиваем другой переменной вот так:

 antirad_check_delay = reminder_count

            Ну и вот, если у нас reminder_count в момент присвоения ещё не имеет никакого значения, то мы получим занимательную штуку - у нас это выражение сработает как

antirad_check_delay = nil

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

2) Ошибки деления на 0.

            Теперь положим что у нас reminder_count используется в каком-нибудь расчёте вот так:

antirad_check_delay = exposure_count/reminder_count

Как мы помним, у нас reminder_count не инициализирована, и как следствие равна nil.

 В итоге мы получим попытку поделить exposure_count на ноль (в нашем случае на nil, что суть одно и то же в нашем случае) и получим "классический" программистский вылет из-за ошибки деления на ноль.
3) Ошибки присвоения.

Предположим что мы написали скрипт, который помогает выбирать оружие при наличии вблизи врагов. И вот, у нас в скрипте есть такой кусок кода, где NPC вынимает из рюкзака оружие:

 

function weapon_manager:set_weapon(wpn)


   local enemy = self.npc:best_enemy()
   if wpn then
      --обратите внимание сюда
      self.weapon_id = wpn:id()
      --обратите внимание сюда         
      self:return_items(self.weapon_id)
--      printf("set_weapon[%s]:set %s[%s]",self.npc:character_name(),wpn:id(),wpn:section())
   else
      printw("set_wpn:weapon not exist")
   end
   if self.modes.process_mode == "3" and enemy then
      for k,v in pairs(self.weapons) do
         for i,w in ipairs(v) do
            if w.id ~= self.weapon_id then
               local item = level.object_by_id(w.id)
               if item and item:parent() and item:parent():id() == self.npc_id then
                  printw("set_weapon[%s]:process %s[%s]",self.npc:character_name(),w.id,w.sec)
                  self:process_item(item)
               end
            end
         end
      end
   end
   if enemy then
      self.npc:set_item(object.idle,wpn)
   end
   self.weapons = nil

end

            В помеченном выражении self.weapon_id = wpn:id(), чтобы было понятнее, self.weapon_id - это оружие, которое сейчас в руках у персонажа, а wpn:id() ссылается на лучшее оружие из инвентаря NPC. Т.е. это выражение заставляет NPC заменить ТЕКУЩЕЕ оружие на лучшее. Всё было бы отлично если бы не одна характерная вещь - когда NPC сидит например у костра отдыхает, У НЕГО НЕТ В РУКАХ СТВОЛА! И разумеется, в этом случае значение self.weapon_id не существует! Оно равно nil. В итоге при подобном выражении

self.weapon_id = wpn:id()

произойдёт попытка присвоить значение ПУСТОТЕ, т.е. nil = wpn:id(), и это МГНОВЕННО приведёт к вылету...


Как с этим бороться.

Бороться с такими ситуациями очень просто на самом деле. Во-первых:
ВСЕГДА ИНИЦИАЛИЗИРУЙТЕ ПЕРЕМЕННЫЕ.

Т.е. вот такое описание переменной:
            local reminder_count

НЕДОПУСТИМО! ВЫ ОБЯЗАТЕЛЬНО ДОЛЖНЫ ЗАДАТЬ ПЕРЕМЕННОЙ ЗНАЧЕНИЕ! Если переменная числовая, сделайте это так:

            local reminder_count = 0

Если строчная - то сделайте это так:

            local reminder_count = "" (получится вместо nil пустая строка)

Если логическая, то так:

            local reminder_count = false

В общем, делайте так, как вам удобнее, но ДЕЛАЙТЕ ОБЯЗАТЕЛЬНО!

И во-вторых:
ВСЕГДА ПРОВЕРЯЙТЕ ПЕРЕМЕННУЮ ПЕРЕД ИСПОЛЬЗОВАНИЕМ! Т.е. в кусках кода, чреватых вылетами (к ним относятся АБСОЛЮТНО ВСЕ части, где идёт работа с вещами или оружием) обязательно вставляйте проверки переменных на nil следующим образом:

Код: Выделить всё

if self.weapon_id ~= nil and wpn:id() ~= nil then
   self.weapon_id = wpn:id()
end



Тогда сначала выпонится проверка, и если обе переменные имеют значение, то операция отработает, а если один из параметров пуст - то функция просто пропустит этот код.
Точно так же, если вы используете ЛЮБЫЕ математические операции например деление (но не только деление, всего остального тоже касается), тоже ОБЯЗАТЕЛЬНО поверяйте переменную. Вот так:

Код: Выделить всё

if reminder_count ~= nil and reminder_count ~= 0 then
   antirad_check_delay = exposure_count/reminder_count
end



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

16/02/2009 KamikaZze

 

 

Дополнения:

            Небольшое дополнение итак, вы вставили обходные проверки вот таким образом:

     if self.weapon_id ~= nil and wpn:id() ~= nil then
         self.weapon_id = wpn:id()
     end


Или таким:

 

     if reminder_count ~= nil and reminder_count ~= 0 then
         antirad_check_delay = exposure_count/reminder_count
     end


            И у вас в случае некорректного значения компилятор спокойно пропустил этот код, ничего не сделав ни с self.weapon_id (из первого примера), ни с antirad_check_delay (из второго). Однако вам всё-таки нужно, чтобы с ними что-то происходило, даже если проверяемые переменные неверны. Тогда я бы советовал вам дополнить этот обход отработкой нештатной ситуации. Делается это банально просто:

     if self.weapon_id ~= nil and wpn:id() ~= nil then
          self.weapon_id = wpn:id()
     elseif self.weapon_id == nil then
     --вставьте сюда код, что делать если self.weapon_id не существует
     elseif wpn:id() == nil then
     --а сюда, если wpn:id() не существует
     end


И для второго случая аналогично:

 

     if reminder_count ~= nil and reminder_count ~= 0 then
          antirad_check_delay = exposure_count/reminder_count
     else
     --а тут что мы сделаем если reminder_count не существует
     --я бы сделал например вот так:
          antirad_check_delay = exposure_count/1
     --и все дела
     end


Более конкретная реализация зависит от того, что именно вы пытаетесь сделать - тут уже вам виднее...

Категория: Модострой | Добавил: front (14.08.2009)
Просмотров: 1638 | Рейтинг: 0.0/0 |
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Форма входа
Поиск
счетчик посещенийсчетчик посещений
Друзья сайта
 
Фан сайт игры S.T.A.L.K.E.R.

Бар_У_границы_Зоны_бар_настоящего_сталкера!

Статистика

На портале: 3
Новичков: 3
Участников: 0

Copyright MyCorp © 2024