Сповіщення
Очистити все

Як писати скрипти, щоби у вас не вилітала гра


Ранг:
Майстер
Роль:
Гість
Записи:
752
Приєднався:
7 місяців тому
 

Передмова

Налагодивши кілька скриптів на предмет вильотів, вирішив написати це невелике керівництво, щоб допомогти початківцям (та й досвідченим теж) скриптописувачам уникнути потенційних проблем.

Відразу обмовлюся - я програміст, тому до налагодження скриптів підходжу зі своєї професійної точки зору, і те, що я тут викладу вам - це для програміста є непорушна істина, і якщо ви хочете зменшити кількість глюків - намагайтеся дотримуватися нижченаведеного стилю програмування.

Спочатку маленький екскурс в історію. Отже, скрипти в сталкері написані мовою 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_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_count == 0  або reminder_count >= mins_till_next_remind then

видасть помилку та призведе до вильоту! Чому? Та тому що НЕ МОЖНА порівнювати НІЩО з речовим значенням. Саме тому, коли ви створюєте нову змінну, ви повинні задати їй значення за замовчуванням, навіть якщо ви повністю впевнені, що вона ніде не буде використана до ініціалізації. Повірте — у програмуванні буває все, і може так статися, що ваша змінна буде використана і спровокує виліт.

Простий спосіб обійти таку купу перевірок та зайвих ініціалізацій – використання or-оператора. приклад

if  ( reminder_count or  0 ) >= mins_till_next_remind then

якщо змінна дорівнює nil, то вираз обчислюється далі і виходить рівним 0, а це вже число та порівняння відбувається безболісно.

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

«Смерть» змінних

Припустимо, у нас з reminder_count відбуваються ще якісь цікаві речі, наприклад ми його значення навіщо-небудь присвоюємо інший змінної ось так:

antirad_check_delay = reminder_count

Ну і ось, якщо у нас reminder_count у момент присвоєння ще не має жодного значення, то ми отримаємо цікаву штуку – у нас цей вираз спрацює як

antirad_check_delay = nil

Внаслідок чого змінна antirad_check_delay «здохне» і буде діловито прибрана «збирачем сміття» з пам'яті, що миттєво призведе до вильоту, коли надалі якась частина коду звернеться до значення antirad_check_delay.

Помилки поділу на 0

Тепер покладемо що у нас reminder_count використовується в якомусь розрахунку ось так:

antirad_check_delay = exposure_count/reminder_count

Як ми пам'ятаємо, у нас reminder_count не ініціалізована і як наслідок дорівнює nil. У результаті ми отримаємо спробу поділити exposure_count на нуль (у нашому випадку на nil, що суть те саме в нашому випадку) і отримаємо «класичний» програмістський виліт через помилку поділу на нуль.

 

Помилки присвоєння

Припустимо, що ми написали скрипт, який допомагає вибирати зброю за наявності поблизу ворогів. І ось, у нас у скрипті є такий шматок коду, де 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)
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(), і все начебто б добре, тому що варто перевірка if wpn then, але іноді двигун примудряється передати об'єкт функціям неправильно, або сам скриптер може переплутати при виклику ігровий об'єкт з об'єктом а-лайфа , і присвоєння self.weapon_id = wpn:id() або прирівнює self.weapon_id до nil або одразу призведе до вильоту через те, що відповідна властивість об'єкта не була виявлена ​​в його класі.

Як із цим боротися

Боротися із такими ситуаціями дуже просто насправді. По-перше: ЗАВЖДИ ІНІЦІАЛІЗУЙТЕ ЗМІННІ. Тобто такий опис змінної:

local reminder_count

НЕДОПУСТИМО! ВИ ОБОВ'ЯЗКОВО МАЄ ЗАДАТИ ЗМІННОЇ ЗНАЧЕННЯ! Якщо змінна числова, зробіть це так:

local reminder_count = 0

Якщо малий — то зробіть це так:

local reminder_count = ""

(вийде замість nil порожній рядок)

Якщо логічна, то так:

local reminder_count = false

Загалом, робіть так, як вам зручніше, але ЗРОБИТЕ ОБОВ'ЯЗКОВО!

І по-друге: ЗАВЖДИ ПЕРЕВІРЮЙТЕ ЗМІННУ ПЕРЕД ВИКОРИСТАННЯМ!

Тобто в шматках коду, що загрожують вильотами (до них відносяться Абсолютно всі частини, де йде робота з речами або зброєю) обов'язково вставляйте перевірки змінних на nil наступним чином:

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

Тоді спочатку виконається перевірка, і якщо обидві змінні мають значення, то операція відпрацює, а якщо один із параметрів порожній, то функція просто пропустить цей код. Тут не завадить ще й перевірку вставити на існування self.weapon_id, хоча це, напевно, вже моя паранойя 🙂

До речі, що безглузде — помилки, наведені вище, часто в ліг потрапляють абсолютно несхожим на помилки скриптів чином. Наприклад, помилка присвоєння з прикладу 3 призводила до вильоту з повідомленням «Assertion failed», і зловити її я зміг лише вивчивши уважно попередні рядки лога, де зафіксувалися операції з речами.

Точно так само, якщо ви використовуєте БУДЬ-ЯКІ математичні операції наприклад розподіл (але не тільки розподіл, решта теж стосується), теж ОБОВ'ЯЗКОВО повіряйте змінну. Ось так:

if reminder_count and reminder_count ≈ 0  then
antirad_check_delay = exposure_count/reminder_count
end

Таким чином, потенційні вильоти будуть акуратно ізольовані, і більше не створюватимуть проблем. І ще одне невелике доповнення до цієї теми. Отже, ви вставили обхідні перевірки таким чином:

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

Або таким:

if reminder_count and reminder_count ≈ 0  then
antirad_check_delay = exposure_count/reminder_count
end

І у вас у разі некоректного значення компілятор спокійно пропустив цей код, нічого не зробивши ні з self.weapon_id (з першого прикладу), ні з antirad_check_delay (з другого). Однак вам все-таки потрібно, щоб з ними щось відбувалося, навіть якщо змінні, що перевіряються, невірні. Тоді я радив би вам доповнити цей обхід відпрацюванням позаштатної ситуації. Робиться це просто:

if wpn and wpn:id ( ) then
self.weapon_id = wpn:id ( )
elseif wpn == nil then
--вставте сюди код, що робити якщо wpn не існує
elseif wpn:id ( ) == nil then
--а сюди , якщо wpn:id() не існує
--тут швидше за все переплутаний об'єкт, і можна спробувати
--звернутися до wpn.id
end

І для другого випадку аналогічно:

if reminder_count and reminder_count ≈ 0  then
antirad_check_delay = exposure_count/reminder_count
else
--а тут що ми зробимо якщо reminder_count не існує
--я зробив би наприклад ось так:
antirad_check_delay = exposure_count/ 1
--і всі справи
end

Більш конкретна реалізація залежить від того, що саме ви намагаєтеся зробити — тут вам видніше…

Типові помилки при іменуванні функцій, присвоєння та ініціалізації змінних.

Типова помилка, що веде до вильоту:

function remove_item ( remove_item ) 
if remove_item~= nil then
alife ( ) :release ( alife ( ) :object ( remove_item:id ( ) ) , true )
return true
end
return false
end

У цьому вся коді агрумент функції збігається з її ім'ям, й у результаті функція намагається передати функції alife():release замість покажчика на річ — покажчик саму себе. Оскільки у функції remove_item немає ніякого id, код випадає з помилкою:

Arguments : LUA error: ...\stalker\gamedata\scripts\test.script:486: attempt to call method 'id' (a nil value)

Слід пам'ятати, що Lua - це мова прямого (JIT) компілювання, і він, як і будь-яка така мова не перевіряє помилки при компіляції. Тому потрібно дуже акуратно роздавати імена змінним та функціям, щоб уникнути подібних помилок. Цю функцію потрібно переписати так:

function remove_item ( item_to_remove ) 
if item_to_remove then
alife ( ) :release ( alife ( ) :object ( item_to_remove:id ( ) ) , true )
return true
end
return false
end

Якось, налагоджуючи скрипт, натрапив на такий код:

function hit_callback ( npc_id ) 
if db.storage [ npc_id ] .wounded ≈ nil then
db.storage [ npc_id ] .wounded.wound_manager:hit_callback ( )
end
end

Здавалося б, все правильно, і значення перевіряється перед використанням. Та ні, скрипт викликав виліт, причому саме на перевірці. Методом тику було з'ясовано, що nil, що викликав виліт, був не в результаті обробки, а був переданий фукнції. Це був параметр npc_id.

Довелося зробити так:

function hit_callback ( npc_id ) 
if npc_id and db.storage [ npc_id ] .wounded ≈ nil then
db.storage [ npc_id ] . _
_
_

Введя попередньо перевірку аргументу npc_id. Висновок - якщо ставите перевірку, намагайтеся перевіряти не тільки саму функцію, а й спочатку аргумент, який їй передається. Мало як він до вас доїде ... 🙂

Щодо іменування змінних - не забувайте давати їм атрибут local якщо ви не збираєтеся їх використовувати у зовнішніх скриптах. Інакше, якщо потрапить десь аналогічне ім'я — буде збій логіки чи виліт, пам'ятайте про це.

І ще одна порада — для початківців:

Коли пишете скрипти, не забувайте про дотримання синтаксису! Тобто уважно і вдумливо ставте в кінцях функцій і підстав ключові слова end — пам'ятайте, що як відсутність потрібних end'ів, так і наявність зайвих призводять до того, що межі фукцій збиваються, скрипт не парситься обробником і викликає при спробі виклику виліт, приблизно ось такий:

Arguments : LUA error: ...g\stalker\gamedata\scripts\bind_stalker.script:75: attempt to index 'test_main_new' (a nil value)

Для того, щоб вам простіше було стежити за синтаксисом і не допускати зайвих end'ів, вибудовуйте текст скриптів каскадними драбинками:

Початок_Роботи 
Початок_Внутрішньої_Функції
Процедури_Внутрішньої_Функції
Кінець_Внутрішньої_Функції
Кінець_Роботи

..і користуйтеся редакторами з підсвічуванням синтаксису, такими як Notepad++ наприклад. Через війну кожен рівень вкладеності перебуває у своєму відступі. Ви завжди побачите, яку конструкцію потрібно закрити, а яку ні. Я ж наприклад, роблю ще простіше - я, відкриваючи нову функцію, відразу ставлю відразу в її кінці, парою рядків нижче, end, і потім вже пишу текст, що наповнює її, з відступами як у прикладі вище.

І ще щодо життя змінних: не забувайте, що певні всередині перевірок змінні живуть тільки до виходу з перевірки. Тобто, наприклад:

if check == 1  then 
local test = true
else
local test = false
end

У разі змінна test створиться, але проіснує лише до виходу з перевірки, і далі звернення до неї видасть nil. Щоб цього уникнути, визначення змінних треба виносити «за дужки» ось так:

local test = false ; - інітимо змінну і задаємо 	відразу їй значення за замовчуванням 
 
if check == 1



Смертельні цикли за таблицями

Якщо ви будуєте цикл по таблиці так:

for i = 1 , table . getn ( table_name )  do

То ніколи, ні в якому разі не використовуйте всередині цього циклу видалення/додавання рядків, тобто:

table_name [ i ] = nil

або

table . remove ( table_name, index ) 
table.insert ( table_name, index )

стосовно таблиці за якою ганяєте цикл! При використанні це призводить до того, що кількість рядків у таблиці зменшується, а цикл намагається отримати з таблиці рядки понад існуючу кількість, що в результаті призводить до виходу циклу за відведений діапазон пам'яті. Результатом буде віяло найрізноманітніших наслідків, найнешкідливіший з яких це безлоговий виліт на робочий стіл, а серйозне — збій у роботі а-лайфу з наступним боєм сейвів! Тяжкість наслідків залежить від того, які дії ви виконуєте всередині циклу, крім видалення рядка.

Щоб уникнути подібних ситуацій, робіть цикли по таблиці краще наступним чином:

for k, v in  pairs ( table_name )

А видалення рядків усередині них робіть так:

table_name [ k ] = nil

Цикли ж for i = 1, table.getn (table_name) do слід використовувати тільки для операцій, що не змінюють структуру таблиці, що змінюється!


   
Цитата
Ранг:
Майстер
Роль:
Гість
Записи:
752
Приєднався:
7 місяців тому

   
ВідповіcтиЦитата