Перейти к контенту

Язык Lua. Общие вопросы программирования


Malandrinus

Рекомендуемые сообщения

(изменено)

Неоднократно поступали предложения создать тему для обсуждения Lua и вопросов, связанных с программированием на нём. Также регулярно возникают достаточно интересные дискуссии в теме "Справочник" а также в других темах, типа "Скриптование и спавн" или ковырялки. Народ говорит много интересного, но либо тема непрофильная (как справочник) и приходится это всё вычищать, либо интересное обсуждение уходит по треду далеко назад и скрывается за тоннами вопросов по конфигам и пр. Поэтому есть основания надеяться, что данная тема будет полезна.

 

Здесь предлагается обсуждать язык Lua, его синтаксические особенности, производительность, приёмы использования стандартные и не очень. Также здесь предлагается постить примеры алгоритмов для решения разных достаточно общих задач, по возможности не сильно привязанных к игре. Естественно, поскольку форум пока ещё по сталкеру, привязки к игре не избежать. Также, полагаю именно здесь можно вести умные разговоры об оптимизации и всяких общих вещах, типа парадигм объектно-ориентированного программирования, обработки ошибок и технологий отладки скриптов, стиля оформления кода и "единственно правильного пути" в программировании, как вы его видите =)

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Признаться, не думал, что так для всех важен редактор. Какое вообще отношение редактор имеет к языку? Lua же платформенно-независимый!

 

Короче, от сего момента буду нещадно тереть любое упоминание о редакторах, не несущее конструктивной информации. Однако приветствуется:

- перечисление дельных фишек конкретного редактора

- приведение конкретной информации о настройках, ссылки на полезные мануалы, решение проблем и т.п.

- вопросы в этом же ключе

 

И повторяю вопрос. Знает ли кто, чем можно автоматически отформатировать Lua программу? Я не имею в виду поддержание текущего отступа, это тривиально. Я хочу как в Visual Studio, выделил фрагмент, нажал кнопочку, и мой фрагмент отформатирован. Причём, это не только красоты ради надо. Подобный инструмент безумно помогает искать потерянные/лишние концы блоков.

 

Поскольку я потёр оригинальный пост break (погорячился, прошу прощения), то привожу ссылку на ру-борд-овский форум по SciTE здесь

_http://forum.ru-board.com/topic.cgi?forum=5&topic=35160

потом, надеюсь, соберём всё в шапке

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Вот устанавливал себе SciTE по ссылкам с руборда. Сразу покритикую, пока не остыло:

1. Установщик требует прав администратора. На корпоративный компьютер поставить смог только частично и с бубном. Пришлось распаковывать установщик вручную, делать промежуточный командный файл с SET-ами и т.п.

2. Русская (как бы) версия имеет нелокализованный по умолчанию интерфейс. Как это изменить, не нашёл.

3. Настройки в виде текстового файла. Чтобы найти нужную опцию надо листать простыню из больше десятка страниц. Это шутка такая?

4. Окно About выглядит насмешкой, поскольку в нём собственно "о программе" нет ни слова

5. Мелочи, типа отсутствия на тулбаре кнопки поставить брейкпоинт, это вроде как и не недостатки, но смотря на п.3 (выше) это уже становится недостатком.

6. Да вот ещё, нет кнопочек закрытия на каждой вкладке. Очень неудобно.

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Я всё же надеюсь, что здесь начнётся разговор про Lua. Пока говорим только про SciTE. Создаётся ощущение, что изучение Lua совершенно немыслимо без SciTE.

  • Нравится 1
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Artos,

это и не скрипт (lua_help), а считай напоминалка модмейкерам ... далеко не полная и нередко устаревшая.

Не соглашусь по обоим пунктам. Это полная и в силу этого актуальная информация. Хотя разумеется полная лишь в том смысле, что там перечислено всё, что экспортировано. Конечно, описания там никакого, и потому называть это "help" было в каком-то смысле издевательством.

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Artos,

Каковы возможности в LUA (применительно к Сталкеру) по работе из скриптов со стеком?

 

В мануале по LUA достаточно куцая информация и практически только для внешнего использования (применительно к С/С+) ...

Как мне помнится, не такая уж и куцая. Работа со стеком там в пространстве имён debug. По причине отсутствия этого пространства имён в версии сталкера этих возможностей там нет.

 

Название языка Lua пишется именно так, с большой буквы. Это не аббревиатура, а имя собственное, означает на бразильском "Луна". Разработчики языка особенно настаивают именно на таком написании названия.

 

Теоретически, поскольку Lua в составе сталкера сделан в виде динамической библиотеки, то можно пересобрать эту библиотеку из исходников и добавить туда то, чего не хватает.

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Artos,

Возможно ли по аналогии с перловыми методами из языка Perl составить патерн в Lua для парсинга по нескольким кускам слов?

 

Т.е. имеем несколько масок: Masks = ("one", "two", "three")

Возможно ли составить единый патерн, а не использовать три:

 

if string.match(s,"one") or string.match(s,"two") or string.match(s,"three") then --/< 's' - исходная строка

 

Вроде в общем случае никак. То что в Perl - это регулярные выражения, там возможностей гораздо больше. Была бы возможность заменить библиотеку в сталкере, можно было бы и регулярные выражения прикрутить. Библиотеки для Lua уже есть.

 

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

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Gun12,

при наборе выскочит окошко с возможным макросом, который вставляется по нажатию клавиши Enter/

Имея привычку пользоваться данным инструментом можно не беспокоится о "забытом" end-е

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

 

Между тем, в реальной жизни программист проводит 9/10 времени отнюдь не создавая новый текст. Основное время занимает поиск ошибок, отладка, внесение изменений любого рода и с самыми разными целями: исправление, оптимизация, расширение возможностей и т.п. Часть "созидательных" фишек при этом бесполезны, часть - попросту будут мешать (в том числе и загромождая рабочее пространство).

 

По-поводу end-ов. На самом деле мало-мальски опытному программисту не нужна помощь в завершении блоковых конструкций. Вот я к примеру выработал в себе привычку набивать первым делом пустую рамочную конструкцию уже очень давно. Руки пишут завершающую скобку или end просто на автомате. Я бы даже сказал, что попытки редактора сделать это за меня будут мне мешать, и я это по-любому отключу.

 

Пока искал автоматический форматировщик текста Lua (так и не нашел, кстати) вдосталь начитался мнений типа "научись пользоваться табом и ентером, кретин, и не будет проблем с отступами". Примерно так и пишут, я не преувеличиваю. Между тем, я могу назвать как минимум три причины для использования такого инструмента:

1. Потеря форматирования при изменениях. Код может остаться корректным, но ручное проставление табуляций в километровом тексте может быть утомительно.

2. Возможность искать ошибки потерянных/отсутствующих скобок/завершителей. Такие ошибки легко вносятся при изменениях. Никакие инструменты этого не в состоянии предотвратить, а уже отформатированный код при этом даже в каком-то смысле скрывает ошибку.

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

 

Исходя из сказанного, могу сформулировать перечень жизненно важных для программисткого редактора фишек:

1. Моноширинный шрифт

2. Подсветка синтаксиса

3. Поддержание текущей позиции табуляции

4. Изменение уровня табуляции фрагментов текста

5. Блоковое выделение (это обычно с зажатым Alt-ом) и операции с блоками - удаление, перемещение и пр.

6. Outlining (сворачивание текста)

7. Авторазметка (автоформатирование) текста по запросу

 

Из этого перечня разве только п.3 относится к первичному набиванию текста и то имеет применение также и при редактировании. Остальное предназначено в основном для удобства работы с уже существующим кодом, в частности для лучшего его понимания. Вот этим программист и пользуется 90% всего времени. Остальное - вторично и стоит при первом знакомстве оставить за кадром.

 

Добавлено через 51 мин.:

Artos,

оператор # возвращает номер последнего индекса в таблице, за которым отсутствуют как минимум два следующих индекса подряд.

Почему два? Возвращает индекс n, такой что "t[n] не равен nil, а t[n+1] равно nil". Т.е. такой, за которым идёт "дырка" в значениях. Одной вполне хватит.

 

Однако, выходит так, что это правило не соблюдается для "таблиц, рождённых массивами". Т.е. к примеру в этом случае

t = {1, 2, nil, 4}

мы получим 4 в качестве длины. В принципе, это можно понять, если рассмотреть реализацию таблиц, описанную автором в статье The implementation of Lua 5.0. В каждой таблице есть часть "линейный массив" и часть"хэш-массив". При создании с помощью конструкции {a, b, c, d, e} очевидно всё попадает в часть линейного массива, а оператор длины видимо в этом случае оптимизирован и вместо перебора просто берёт длину массива.

Если же создавать массив вразнобой, типа такого:

t = {}

t[4] = 4

t[2] = 2

t[1] = 1

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

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

В рубордовской версии SciTE так и не нашёл возможности ставить точки останова. Непонятно, как отлаживать программу. Кто-нибудь знает?

 

Добавлено через 16 мин.:

Artos,

Моя фраза была уже опровергнута постом #30 Gun12

Извиняюсь, невнимательно читал.

 

Достоинство SciTE для данного раздела "Школа модинга" в том, что он имеет встроенный русский мануал по Lua, который неплохо иметь под рукою любому модмейкеру и то, что не теряя времени на запуски/перезапуски целевой программы (той же игры)

Я себе давно наладил тестовый полигон для запуска скриптов в игре без перезгарузки. Запуск в игре имеет свою ценность, поскольку скрипт тестируется именно там, где будет реально применяться.

 

Gun12,

запрещенную тобой тему редакторов

Ощущаю себя исчадием модераторского ада :shok: ... Я не запрещал. Во-первых, я не могу ничего запретить, поскольку всего лишь куратор. Во-вторых, я сказал, что буду тереть бессмысленный флуд. То, что навалили здесь в самом начале, не содержало ни грамма информации, сплошное меряние ... редакторами.

 

 

Добавлено через 15 мин.:

Artos,

Требуется определить, что десятичное число (любое - целое/дробное/отриыательное) лежит в диапазонах 10...15, 100...255, 1000...4095 и т.д.

А почему не хочешь сделать по-простому, с перебором диапазонов и проверкой на вхождение в них? На мой взгляд, это даже будет быстрее работать.

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Artos,

1. Что мешает поделиться опытом, а не поверхностной 'частной' информацией? ;-)

Эээ... два года как это уже сделал

 

4. В данном топике мы говорим с ориентиром на Lua, а игра вторична или ... наоборот? ;-)

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

 

Gun12,

у меня просьба. Не мог бы ты завершить свой введение в SciTE и дополнить его краткой информацией о том, как можно запустить написанный скрипт. Вот ты написал:

После этого запустить SciTE, написать - print('Hello Word')

В открывшейся консоли должно напечатать - Hello Word

Написать где, в тексте документа? А почему консоль должна открыться? Что-то надо нажать? Попробуй представить себе, что аудитория нулевая и никаким сторонним багажом не обладает.

 

И, если знаешь, то не мог бы подсказать уже для меня, как в рубордовской версии поставить точку останова?

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Gun12,

Извини, никак не пойму что это за точка такая?:blush:

Может я и знаю что это такое, но по-своему.

Точка останова для отладчика, иначе breakpoint. Это чтобы остановить в нужном месте программу, посмотреть что и как, затем продолжить дальше. Также при наличии отладчика обычно имеется возможность выполнять программу по шагам. В том SciTE, что идёт вместе с Lua, эти команды вынесены прямо на тулбар. А есть отладчик в рубордовском варианте или вообще нет, пока не понял.

 

Artos,

Каков ответ координатора о первичности? :-)

Зачем так усложнять? Первично то, что будет поддерживать интерес к этой теме.

 

Есть ли способ определить текущий каталог для Lua однозначно и быстро, т.е. не экспериментально?

Есть ли способ изменять текущий каталог Lua и/или хотя бы организовать типа удобного линка на него?

по большому счёту, кроме как применительно к пространству имён io понятие текущего каталога больше никак и не использовать. Кроме того, в стандартной библиотеке Lua такого и понятия не вводится явно, как нет и средств управления текущим каталогом. Я бы старался избегать использования каталога "по умолчанию" хотя бы в силу того, что он именно в игре неизвестно какой. Вместо этого, лучше использовать явно заданные абсолютные пути. В движке есть класс FS, который позволяет узнать все пути к каталогам игры.

 

В полигоне я использовал текущий каталог, но мог бы вместо этого использовать класс FS, выяснить каталог со скриптами и использовать его полный путь вместо текущего пути "." (точка - текущий каталог). На тот момент использовать текущий путь казалось проще, кроме того на тот момент я вполне возможно не знал ещё, как пользоваться классом FS в полной мере, только и всего =)

 

Gun12,

Собственно к ответу Artos-а остаётся только добавить.

т.е. для запуска надо нажать F5 или такую зелёную кнопочку со стрелочкой вправо на тулбаре, или из меню "Tools" выполнить команду "Go". Я правильно понял? =)

 

А на хpcall тоже?

а, с тех пор много воды утекло, и я уже выяснил, в чём была проблема. Вылет вызывало использование техники выдачи сообщения в лог get_console:execute("text"). Причём, что самое неприятное, вылет происходил не сразу, а позже, и иногда его не было. Поэтому я долго думал, что проблема в чём-то другом. Так что в общем проблемы нет, надо просто использовать вывод в произвольный файл через пространство имён io (благо в ЧН/ЗП оно уже есть). Это в общем и удобнее намного, поскольку можно выводить в разные файлы и в логе нет мусора.

 

В модифицированном движке можно добавить нормальный вывод в лог (а не как побочную реакцию на ошибочную команду). Тоже будет работать.

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Не все действия требуется делать с максимальной частотой, некоторые действия надо просто сделать "когда-то". Ставим низкоприоритетные действия в очередь. При очередном обновлении выполняем очередное действие из очереди, они естественным образом "размазываются" по времени. На каждый апдейт вешаем только то, что реально требует быстрой реакции: имитацию критического хита, к примеру (требует мгновенной реакции на падение здоровья). Остальные стоят в очереди =)

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Gun12,

Один хороший товарищ уже всё подключил к ТЧ. Luа в полном составе и работает.

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

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

RvP,

Основные претензии к встроенному random не столько насчёт скорости, сколько по поводу качества. Каково происхождение новой функции? Работает ли с ней randomseed?

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

RvP,

Что мешает писать "нормальную систему классов" так и вообще не понятно...

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

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Gun12,

if (i==0.4) then print("TRY")

end[/code]И объясните - почему?

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

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Венгерская нотация существует с единственной целью облегчить понимание типов переменных. Её использование было актуально в эпоху, когда экономили на символах для имени переменной, а среды разработки были несовершенны и не обеспечивали подсказок, проверки синтаксиса на лету, быстрых переходов к объявлениям и т.п. В сущности её назначение - скомпенсировать некоторые врождённые недостатки одного конкретного языка программирования, а именно СИ (не С++, обращаю внимание) и причём, именно в ту древнюю эпоху несовершенных сред разработок.

 

В настоящее время во многих языках эта нотация бесполезна, поскольку не соответствует современным реалиям. К примеру, она ничего не даёт в случае массированного использования ООП, поскольку в этом случае основной упор делается на использование разных классов, а на каждый класс префиксов не напасёшься. Также, одним из самых полезных элементов венгерской нотации является различение переменных и указателей, но во многих языках (и в частности Lua) указателей нет, а почти всё представлено единообразно ссылками.

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

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

 

Именно поэтому в стандартной библиотеке С++, а также в boost принят подход записи всего только нижним регистром, а макросов - только верхним. Лично я нахожу такой подход достаточно комфортным и в других языках и практикую его в том числе в Lua. Моё субъективное мнение, что сам идентификатор вполне способен обеспечить понимание сути того, что за ним стоит, если конечно дать ему внятное имя.

 

ЗЫ:

Препод ругает студента: "Что у вас тут, A,B,C... Надо делать длинные мнемоничные идентификаторы.". В общем, проел всю плешь. На следующий раз студент приносит программу, а там переменные названы: длинный_мнемоничный_идентификатор_номер_один, длинный_мнемоничный_идентификатор_номер_два, ...

 

  • Согласен 1
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Artos,

По производительности - безразлично. Хотя методы - не всегда просто ссылки. В luabind функции реализованы на перегруженном операторе индексации, который возвращает функции. Их даже удалить скорее всего не получится. Методы, добавленные в Lua, это скорее всего просто ссылки. Я не пробовал, но можно попытаться их удалить. Прототипный подход к наследованию такую возможность в принципе допускает.

Однако, если честно, то непонятно в принципе, зачем может потребоваться строить один класс на основе другого, никак с ним не связанного. Ещё раз, здесь принят прототипный подход к наследованию. Мы всегда начинаем создание класса с некой заготовки. Можно взять пустую: таблицу или класс luabind, а можно взять ежа и сделать из него ужа. Но зачем? Вообще наследование имеет только один смысл - уменьшить время разработки и/или уменьшить объём кода за счёт повторного использования готового кода. Если это не получается, то в наследовании и смысла нет.

Изменено пользователем ColR_iT
Полностью цитировать предыдущий пост - не имее смысла.
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение

Artos,

У класса события и таймера разные роли. В этой системе есть две роли: событие и подписчик на событие. В библиотеке boost они названы соответственно signal и slot. В .NET - event и delegate. Роль события - во первых предоставлять программе интерфейс для вызова события. Во вторых, событие взаимодействует со слотами или подписчиками. Для этого где-то как-то имеется список "подписантов" и имеется внутренний функционал для их перебора и передачи им аргументов события. С событием же может быть связан интерфейс подписывания/отписывания хотя и не обязательно.

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

 

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

 

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

 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
(изменено)

Предлагаю взглянуть на мою реализацию системы событий, хранилища и таймеров, которую я делал в рамках OGSE. Реализация независимая, возможно кому-то покажется интересной.

http://rghost.net/35846466

Здесь привожу только описание использования, детали смотри внутри.

 

Система событий

Названа по аналогии с boost "сигналы и слоты". Подписчики - это слоты, их соответственно дёргают сигналы. Подписывать можно функции, функциональные объекты и методы объектов. Слот можно подписать для вызова как обычный (или высокоприоритетный) и очерёдный (низкоприоритетный) вызов. Обычные вызываются все разом при срабатывании сигнала, очерёдные - по очереди, по одному на срабатывание сигнала. Всё это используется в таймерах. Имеется сервис автоподключения модулей.

Общие принципы

Вся работа с системой происходит через глобальный объект менеджера сигналов. Получаем его функцией

local sm = ogse_signals.get_mgr()

Для подписывания есть метод менеджера

sm:subscribe(slot_desc)

для отписывания

sm:unsubscribe(slot_desc)

slot_desc - это описание слота, дескриптор слота. Представляет собой таблицу

slot_desc = {signal = "signal_name", self = obj, fun = function, queued = true}

идею вы уже надеюсь поняли.

signal - имя сигнала, по нему потом будем вызывать.

fun - глобальная функция, метод класса или функциональный объект

self - объект класса, если fun - это метод

queued - если true, вызов будет вызываться с низким приоритетом. Это можно сделать для любого сигнала, но очевидно в основном имеет смысл для периодических проверок по апдейтам.

Для отписывания надо передать в точности тот же набор данных, поэтому он и объединён в таблицу. Имеет смысл эту таблицу сохранить, если потом надо отписать вызов. Соответственно, я предпочитаю называть такую таблицу дескриптором слота.

 

Вызов сигнала происходит следующим образом:

local sm = ogse_signals.get_mgr()

sm:call("signal_name", arg1, arg2,...)

Я предпочитаю передавать аргументы так, как показано выше, по старинке списком. С другой стороны ничто не мешает передать их таблицей в стиле

sm:call("signal_name", {arg1 = valu1, arg2 = value2})

цепочку вызовов можно прервать. Для этого вызов должен вернуть true. Возвращаемые значения false, nil или ничего просто игнорируются.

 

Примеры

 

Подписывание глобальной функции

function some_global_function(arg1, arg2)
end
local slot_desc = {signal = "signal_name", fun = some_global_function, queued = true}
ogse_signals.get_mgr():subscribe(slot_desc) -- подписали в низкоприоритетную очередь
--...
--вызвали сигнал
local sm = ogse_signals.get_mgr()
--...
sm.call("signal_name", arg1, arg2)
--...
ogse_signals.get_mgr():unsubscribe(slot_desc) -- отписали

 

Подписывание произвольного метода класса

автор идеи для примера Andrey07071977:

Как быть в ситуациях когда в классе несколько методов которые необходимо подписать на разные события? Например, есть класс "actor_hit_effect", в нем два метода-подписчика - один на actor_hit, другой на item_use.

local hit_effect = actor_hit_effect()
sm:subscribe({signal = "on_actor_hit", self = hit_effect, fun = hit_effect.on_hit})
sm:subscribe({signal = "on_item_use", self = hit_effect, fun = hit_effect.on_use})

или даже так. В коде ниже класс подписывает свои же методы в своём конструкторе. В методе unsubscribe_itself объект класса может отписать методы от событий, используя запомненные ранее дескрипторы.

class "actor_hit_effect"
function actor_hit_effect:__init()
    self.sm = ogse_signals.get_mgr()
    self.desc_on_hit = {signal = "on_actor_hit", self = self, fun = self.on_hit}
    self.sm:subscribe(self.desc_on_hit)
    self.desc_on_use = {signal = "on_item_use", self = self, fun = self.on_use}
    self.sm:subscribe(self.desc_on_use)
end
function actor_hit_effect:unsubscribe_itself()
    self.sm:unsubscribe(self.desc_on_hit)
    self.sm:unsubscribe(self.desc_on_use)
end

 

class "some_luabind_class"
function some_luabind_class:__init()
    local mt = getmetatable(self)
    mt.__call = self.method_to_call
end
function some_luabind_class:method_to_call(arg1, arg2)
end

local slot_desc = {signal = "signal_name", fun = some_luabind_class()}
ogse_signals.get_mgr():subscribe(slot_desc) -- подписали в высокоприоритетную очередь
-- вызвали
sm.call("signal_name", arg1, arg2)
-- отписали
ogse_signals.get_mgr():unsubscribe(slot_desc)

--Функциональный класс на таблице строится так
local t = {}
function t:method_to_call()
end
local mt = {}
mt.__call = t.method_to_call
getmetatable(t, mt)
end
-- подписывается и вызывается совершенно аналогично
ogse_signals.get_mgr():subscribe({signal = "signal_name", fun = t})

 

Примечание: вариант с функциональным классом - это в сущности редко нужные изыски. С точки зрения Lua функциональный класс - это та же функция. Поскольку можно подписать функцию, то можно и функциональный класс.

 

Для подключения модуля его надо прописать в таблицу ogse_signals.addons. В самом модуле должна иметься функция attach, в которую будет передана ссылка на объект менеджера сигналов. Для автоподключения модуля в нём должна быть переменная auto_attach = true, а имя модуля должно начинаться с "ogse_". Впрочем, это фишка нерекомендуемая для использования в релизе, скорее для отладочных и тестовых модулей, которые надо подключать без вмешательства в существующий код. У автоподключения есть также одна особенность. Если в модуле имеется синтаксическая ошибка, то он будет просто проигнорирован при автоподключении (поскольку в этом случае никак не определить, что у него внутри и есть ли там функция attach и переменная auto_attach). Если же модуль прописан в таблице и в нём ошибка, то тогда система ругнётся.

 

 

Система хранения

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

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

 

ogse.save_variable(name, value [,type]) -- сохранить переменную

ogse.load_variable(name [,default_value [,type]]) -- прочитать переменную

ogse.variable_exists(name) -- проверить существование переменной

ogse.delete_variable(name) -- удалить переменную, чтобы освободить место

name - строка с уникальным именем переменной

value - значение

type - строка с типом. Это в ряде случаев необязательный параметр.

Возможные варианты:

"string" - строка, занимает количество символов + 1

"bool" - логическое значение, 1 байт

"float" - яисло с плавающей запятой, 4 байта

"vector" - вектор из 3-х float, 12 байт

"time" - время CTime, 8 байт

"u8", "s8", "u16", "s16", "u32", "s32" - знаковые и беззнаковые целые, 1,2,4 байта соответственно

"number" - автораспознаваемый тип числа, хранится в строковом виде. длина зависит от значения. Этот тип не нужно указывать, он будет назначен переменной автоматически, если значение имеет тип числа.

"chunk" - представляет собой нетпакет, или попросту кусок двоичных данных произвольной длины. Это планировалось как основа для создания отдельных хранилищ, совместимых с текущей идеологией. Т.е. везде, где для сохранения/загрузки передаётся нетпакет, его можно заменить на наш отдельный, а его уже хранить в этом общем хранилище. Смысл этого в экономии места при организации сохранения разных подсистем. Для каждой подсистемы или

сохраняемого модуля можно конечно выделить отдельный объект хранения (из тех, что лежат в основе всего универсального хранилища), но это в общем случае слишком расточительно. Используя эти сохраняемые пакеты можно упаковывать данные более экономно.

"array" - тип массива. Значение - таблица вида {v1, v2, v3}, где тип значений массива ограничен автораспознаваемыми типами: строки, логические значения и числа.

 

параметр type не является обязательным в случае типа значения строка, логическое значение и число. В случае числа при неуказании точного типа числа оно просто сохраняется в строковом виде. Поэтому желательно его указывать. Для чисел с плавающей запятой это существенно уменьшит размер хранимых данных, так как при указании типа "float" число будет сохранено в двоичном виде как 4 байта, строка же может занять до 16-и байт. Во всех остальных случаях, вектор, объект CTime, chunk, array, параметр надо указывать обязательно, так как в этом случае нет возможности тривиально определить природу вргумента. Если в этом случае не указать тип, то будет вылет с соответствующим сообщением.

 

при чтении переменной минимально достаточно указать её имя

local value = ogse.load_variable(name)

Однако, переменная должна существовать. Если такой ранее не было сохранено, то будет вылет (намеренно). Чтобы этого избежать, надо указывать значение по умолчанию.

local value = ogse.load_variable(name, default_value [,type])

тип, как необязательный параметр, указывается по тем же правилам, что и при сохранении.

 

Таймеры

Таймеры имеются двух типов "быстрые" и сохраняемые (сериализуемые). Работают те и другие в целом по одному принципу. Создаётся объект таймера, который имеет в общем случае метод условия и действия. Условие периодически проверяется, когда возвращает true, то таймер останавливается и вызывется метод действия. Для срабатывания в определённый момент времени или через определённый промежуток есть упрощённый сервис, с которым достаточно переопределить только метод действия. Отличие сохраняемых в ряде мелких деталей, и наличии методов save/load, которые в общем работают как методы биндера или серверного класса и позволяют сохранить пользовательские данные таймера в нетпакете.

Применение "быстрого" таймера сводится к написанию простого класса, в общем случае содержащего: конструктор, условие и действие, созданию экземплара такого класса и вызову его метода start.

Пример 1. Простейший таймер, делающий что-то через заданное время после запуска.

class "simple_timer" (ogse_qt.quick_timer)
function simple_timer:__init(time) super(time)
end
function simple_timer:taction() -- наше действие по таймауту
    -- здесь что-то делаем по факту срабатывания таймера
end
--Вызов таймера
simple_timer(3000):start() -- сработает через три секунды
simple_timer(10000):start(true) -- сработает через 10 секунд, и проверяться будет реже

Пример 2 использования таймера для отслеживания удаления объекта из инвентаря:

class "release_watch_timer" (ogse_qt.quick_timer)
function release_watch_timer:__init(id) -- здесь решили не использовать таймаут
    self.client_id = id -- запомнили объект, за которым следим
end
function release_watch_timer:condition()
    return not level.object_by_id(self.client_id) -- сработает по исчезновению клиентского объекта
end
function release_watch_timer:action() -- наше действие
    -- здесь что-то делаем по факту ухода объекта в оффлайн
end
--Использование:
alife():release(sobj, true)
release_watch_timer(sobj.id):start()

Примечания к примерам:

1. Наличие вашего конструктора обязательно. Вызов конструктора базового класса super(timeout) нужен только если используется возможность завершения по таймауту. Возможность задания таймаута - это просто способ задать выполнение действия через заданный промежуток времени без написания специального условия. При использовании этой возможности можно ограничиться только заданием действия.

2. При останове по таймауту действие, заданное методом action, выполнено не будет. Вместо него выполнится действие, заданное taction.

3. Пока таймер работает, можно остановить его работу принудительно так

timer_object:stop()

В этом случае никакое действие выполнено не будет.

 

Пример создания и использования простого таймера, использующего только временнОе условие.

class "test_timer" (ogse_st_mgr.savable_timer)
function test_timer:__init() super()
    -- обязательная строка с именем класса таймера
    self._class = script_name()..".test_timer"
end
-- метод для первичной инициализации, может иметь произвольное имя
function test_timer:set_data(s)
    self.str = s
end
-- перегруженные методы загрузки/сохранения пользовательских данных
function test_timer:load(packet)
    self.str = packet:r_stringZ()
end
function test_timer:save(packet)
    packet:w_stringZ(self.str)
end
-- перегруженный метод действия по временнОму условию
function test_timer:taction()
    log1(self.str) -- что-то делаем в назначенное время
end
-- создание и запуск таймера
local t = test_timer()
t:set_data("some string")
t:set_delay(600) -- сработает через 10 минут реального времени
-- или
--t:set_gdelay(600) -- сработает через 10 минут игрового времени
-- или
--t:set_gtime(game.CTime():set(...)) -- сработает в заданный момент игрового времени
t:start() -- запустить с низким приоритетом (по умолчанию)
-- или
--t:start(true) -- запустить с высоким приоритетом (проверка на каждом апдейте)

Пример таймера с произвольным условием. Следит за неким неписем, который когда-то может быть выйдет в онлайн.

class "conditional_timer" (ogse_st_mgr.savable_timer)
function conditional_timer:__init(timer_id) super(timer_id)
    self._class = script_name()..".conditional_timer"
end
function conditional_timer:init(npc_id)
    self.npc_id = npc_id
    return self -- для вызова цепочкой
end
-- перегруженные методы загрузки/сохранения пользовательских данных
function conditional_timer:load(packet)
    self.npc_id = packet:r_u16()
end
function conditional_timer:save(packet)
    packet:w_u16(self.npc_id)
end
function conditional_timer:condition()
    self.npc = level.object_by_id(self.npc_id)
    return self.npc
end
-- перегруженный метод действия по пользовательскому условию (имя метода action, а не taction !)
function conditional_timer:action()
    self.npc:kill(db.actor) -- что-то делаем при появлении чувака
end
-- Создание таймера, инициализация и запуск с низким приоритетом
conditional_timer("my_timer"):init(12345):start()
ogse_st_mgr.get_timer("my_timer"):stop() -- передумали и остановили

Примечания:

1. При написании конструктора единственным обязательным элементом является инициализация служебного поля self._class. Туда надо записать строку с полным путём до конструктора класса. Если вы находитесь в модуле с расширением *.script, то может помочь функция script_name(), которая вернёт имя модуля, но имя класса придётся дописать самостоятельно. К сожалению, у Lua функции нет тривиального способа узнать своё собственное имя. В итоге эта строка всегда будет выглядеть примерно так:

self._class = script_name()..".test_timer"

или так:

self._class = "some_module.test_timer"

если вы решили написать имя файла явно.

2. Конструктор может иметь только один аргумент или ни одного. Если аргумент есть, то это имя таймера, по которому его можно потом получить для каких-либо манипуляций. Таймер можно остановить или изменить его условия. Если этого не планируется делать, то имя задавать не обязательно. Если имя не задано, то при конструировании таймеру будет автоматически назначено некое уникальное имя на основе первого доступного целого числа.

3. Как и в случае с "быстрыми" таймерами у сохраняемых таймеров имеется сервис срабатывания по временнОму условию без необходимости писать метод с пользовательским условием. Для задания времени останова надо после создания таймера вызвать один из следующих методов:

timer_object:set_delay(real_time_delay) -- аргумент - задержка срабатывания от текущего момента в секундах реального времени (может быть дробное число)

timer_object:set_gdelay(game_time_delay) -- аргумент - задержка срабатывания от текущего момента в секундах игрового времени (может быть дробное число)

timer_object:set_gtime(game.CTime():set(...)) -- аргумент - точное время срабатывания в терминах игрового времени. Объект CTime

4. Как и для "быстрых" таймеров можно регулировать приоритетность проверок аргументом метода start. Однако, в отличие от "быстрых" таймеров, значение по умолчанию (оно же false) будет означать проверки с низким приоритетом. Это более часто встречающийся случай для сохраняемых таймеров, которые работают долго, а точность срабатывания до секунды вполне удовлетворительна.

5. Дополнительные данные для таймера, которые будут в нём храниться и которые он может использовать как для проверки пользовательского условия так и для выполнения действия надо задавать при его создании специальным методом, который надо добавить к классу таймера и использовать в момент между созданием и запуском. Этот метод может иметь произвольное имя. В дальнейшем пользовательские данные будут загружаться в таймер при загрузке игры методом load, и сохраняться в методе save. Метод инициализации кроме как в первый раз при создании использовать нельзя. Сохранение и загрузка данных осуществляются в нетпакете, переданном как аргумент методов save/load. Тут всё работает примерно также, как и при сохранении данных в биндере или серверном объекте. В общем-то, никто не мешает использовать и другие техники сохранения и игнорировать переданный нетпакет.

6. Время срабатывания, установленное методами set_delay, set_gdelay и set_gtime, сохраняется само и никаких дополнительных манипуляций не требует.

 

Изменено пользователем malandrinus
 

Плагины Total Commander для работы с игровыми архивами:

Архиваторный плагин (для работы с одиночным архивом): link1 link2

Системный плагин (для распаковки установленной игры): link1 link2

 

Поделиться этим сообщением


Ссылка на сообщение
  • Недавно просматривали   0 пользователей

    Ни один зарегистрированный пользователь не просматривает эту страницу.

AMK-Team.ru

×
×
  • Создать...