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

Для начала маленький экскурс в историю. Итак, скрипты в сталкере написаны на языке 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

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


Плюсануть
Поделиться
Класснуть
Запинить
Категория: Статьи о сталкере | Добавил: CTRELOK (19.03.2010)
Просмотров: 3810 | Комментарии: 2 | Рейтинг: 5.0/6
Всего комментариев: 2
2 witoscha
5270
  (28.10.2011 21:37)
dry angry

1 Tris
2878
  (14.04.2011 21:20)
Полезно smile

Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]

Мини-профиль
Доброй ночи, Прохожий

К сожалению, наша видеокамера не распознала Вас. Предъявите документы или зарегистрируйтесь.
Интернет без назойливой рекламы! Скачай AdBlock!

Логин:
Пароль:

Меню сайта
Расширенное меню


Меню категорий

Энциклопедия [32]
Статьи о сталкере [15]
Полезные статьи [36]
Любительские рассказы [22]
Уроки моддинга [1]

Мини-чат

Новые сообщения на форуме
True Stalker
Дата: 10.12.2016
Бар 100 Рентген
Дата: 10.12.2016
Столовая НИИЧАЗ
Дата: 10.12.2016

Поиск

Группа Вконтакте (new)

Статистика
Stalker Mods - лучший сайт по Сталкеру! Мир Геймеров В тылу врага, моды, аддоны и другое! Новости общение и многое другое, у нас на exelent731.ucoz.ru
Новости,файлы,общение и многое другое, у нас на world-survarium.ucoz.ru   Все в Бар-Реатор Тот самый S.T.A.L.K.E.R.

Моды Тени Чернобыля, Чистое Небо, Зов Припяти. Карты, Читы, Прохождение модовСкрипты, Графика, Шаблоны, Моды, Патчи, Ролевая - Всё для настоящего сталкера!
Все для игры Stalker. Моды, патчи, трейнеры, читы, сохранения, билды.
Онлайн всего: 7
Гостей: 7
Пользователей: 0

Нас сегодня посетили:

Хостинг от uCoz | Design by XemorDio | MODSTALKER © 2009-2016