1. Lua TTS

1. Lua TTS

Введение


1. Lua TTS image 1

Предполагается, что ты УЖЕ знаешь язык программирования Lua. Но если нет, то и ладно. Всё равно я люблю разжёвывать всё до блевоты. В любом случае советую почитать (хотя бы полистать?) книжку "Programming in Lua".

Для тех, кто не умеет искать, подсказываю. Наберите в гугле:

Программирование на языке LUA 2014 PDF RUS rutracker

Здесь же речь пойдёт о том, как Луа встроен в Tabletop Simulator, как с ним жить и как юзать. Конечно, если ты знаком с английским, то можешь сразу проследовать к официальной документации[berserk-games.com] , а эту статью закрыть и забыть.

Оглавление для этой серии статейУрок 1. Скприты Lua для TTS

Урок 2. Инструменты

Урок 3. Раскраска API для Notepad++

Урок 4. Отложенные задачи во времени

Урок 5. Колоды и карты

Прежде, чем начать...


1. Lua TTS image 12
1. Lua TTS image 13

1. Lua TTS image 14

...Нужно понять, каким местом скрипт сохраняется в игру. В общем, скрипт можно применить лишь к ЗАГРУЖЕННОЙ (из сейва) игре. Таким образом, прежде чем начать программировать, нужно загрузить игру. А для этого, в свою очередь, нужно её сохранить.

1) Сохрани игру.

2) Загрузи игру.

Круто, да? Теперь ты готов. Но я хочу, чтобы ты прочувствовал этот момент. Так что мы разберем некоторые казусы.

Предположим, ты применил скрипт к текущей игре. Затем ты кое-что изменил и сохранил игру под другим именем (чтобы не портить хороший сейв). И теперь ты хочешь изменить скрипт. Ты его меняешь и применяешь к текущей игре, наивно думая, что всё ок. Но - сюрприз! Скрипт применяется к предыдущему сейву, и он тут же загружается. Круто, да? Нет, совсем не круто. Фишка в том, что последним был загружен предыдущий сейв, а текущий сейв ни разу не загружался, поэтому скрипт применяется именно к последнему загруженному. Так что все изменения после сохранения теряются. Кстати, старый скрипт тоже теряется, затирается новым.

Следующий казус в том, что когда ты применяешь скрипт, то он сохраняется в предыдущий (загруженный) сейв, но прочие изменения теряются! Предположим, ты изменил стол, передвинул элементы, создал новые, потом написал скрипт и нажал злосчастную кнопку "SAVE&PLAY". В результате игра сохраняет скрипт в сейв и сразу загружает его, при этом теряется всё остальное, что ты сделал. Потом ты привыкнешь, и это даже будет удобно.

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

Последний совет. Пиши скрипты в другом редакторе типа Notepad++, а потом просто копируй в игру. Так надёжней (и привычней для бывалых программеров).

Ну, теперь ты вроде точно готов. Можно начинать.

Пишем простецкий скрипт


1. Lua TTS image 25

1. Lua TTS image 26
1. Lua TTS image 27

Ну, ты понял, это "Hello World". Выглядит он так:

print("Hello World") Жми на кнопку "Scripting". Слева будут непонятные кнопки, справа рабочая область для написания скрипта, а сверху кнопка "SAVE&PLAY" (Сохранить и перезапустить). Скопируй туда наш свежий скрипт и жми кнопку. Старый текст можешь удалить или оставить. Ну, сделал?

Теперь возрадуйся - скрипт сработал! :) Об успешной загрузке тебя известило зелёное сообщение "Loading complete." посреди экрана. Если в скрипте есть синтаксическая ошибка, то он просто не сработает, а сообщение об ошибке выведется красным цветом на экран. Также многие сообщения дублируются в лог - это наше средство отладки и обратной связи, чтобы знать, что там вообще происходит в скрипте и правильно ли он работает.

Где этот лог? Внизу слева есть окошко с чатом и логом. Если ты случайно нажал F11 и убрал весь интерфейс нафиг, то нажми ещё раз - и он включится, и ты увидишь чат\лог внизу. Вкладка "Game" - это и есть лог событий. Именно здесь появляется различная информация по игре. Также сюда будет писать твой скрипт о том, что он делает. На картинке справа ты видишь мой лог после волшебной кнопки SAVE&LOAD: 1) Игра пишет, что сохраняет скрипт в сейв. Он у меня называется "test_script", без палева. 2) Инфа о сейве и о том, что он изменился. 3) Это отработал наш скрипт - и в лог попала наша строка "Hellow World". 4) Игра ещё раз напоминает, что все прочие изменения улетели в трубу.

В будущем ты будешь писать скрипты так, чтобы они сообщали о своей работе через функцию print. К сожалению, это чуть ли ни единственный способ отлаживать скрипты в Tabletop Simulator, так что морально готовься общаться со скриптом через это окошко.

Глобальное пространство и Global


1. Lua TTS image 34
1. Lua TTS image 35

Не путать!

К слову, здесь уже начинаются трудности, так что лентяев прошу откланяться, до свидания.

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

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

Давай-ка сначала пощупаем, а потому уже покумекаем. Создай обычный блок на столе и снова сохрани игру. Вообще можно любой объект, не важно. Правой кнопкой - вызываем редактор Луа. Как видишь, теперь у нас два скрипта! Сразу напишем print(2) и перезапустим. Ну, сделал?

Теперь у нас в логе две записи - Hellow world из главного скрипта и "2" из второго скрипта.

Вся трагедия в том, что у каждого скрипта своё глобальное пространство _G, и они никак не пересекаются. Если в одном написать x=5, то в другом по-прежнему будет x==nil.

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

В общем, разработчики решили проблему оригинальным способом (через одно место). Они просто предоставили интерфейс для "общения" между разными скриптами. Он заключается в том, что данные тупо копируются. Если речь идёт о таблице, то она тоже копируется целиком со всеми вложенностями. Ссылка на таблицу не передаётся, таблица именно копируется. И если таблицу нужно изменить, то шаги следующие: 1) копируем таблицу через API 2) меняем один или несколько элементов 3) копируем таблицу обратно поверх старой, меняется даже её адрес. Вот такие пироги с котятами!

Каждый скрипт в игре (вместе со своим глобальным пространством имён) соответствует одному объекту. Доступ к этому объекту из скрипта возможен посредством ключевого слова "self". Причем, это не локальная переменная функции-обработчика, а глобальная переменная, доступная отовсюду и в любое время, даже с самой первой строчки скрипта.

Сразу после создания объект не инициализирован. На это нужно некоторое время. Хотя "извне" доступны некоторые его свойства. Как только объект полностью инициализирован, в этот момент происходит проверка наличия скрипта - на данном этапе это просто строка с текстом программы на языке Луа. Если скрипт есть, то он компилируется и сразу же выполняется. Именно поэтому переменная self, как и другие функции, доступна с самого начала.

Однако для глобального скрипта нет соответствующего объекта. Вместо этого разработчики придумали переменную Global (типа userdata, кто бы сомневался). Через эту переменную любой другой скрипт может получить доступ к пространству имён глобального скрипта через вызовы Global:getVar(), Global:setVar(), Global:getTable(), Global:setTable().

Не путай Global (главный объект игры) и "глобал" как глобальное пространство имён отдельно взятого скрипта (_G). Ещё именем "Global" называется сам глобальный скрипт в списке скриптов и переименовать нельзя (хотя в целом разумно).

GetObjectFromGUID


1. Lua TTS image 50

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

Чтобы узнать guid СВОЕГО куба, кликни правой кнопкой по нему - и в пункте "Scripting" он будет указан, его можно скопировать в буфер обмена. Дело в том, что я создал свой куб, а ты - свой, и у нас разные guid. Вот если бы я сохранил игру и переслал тебе свой сейв, тогда было бы одинаково, а так тебе приходится самому выяснять.

function onload() local cub = getObjectFromGUID('ca018a') --> заменить на свой cub:setColorTint{1,1,0} --rgb end Ого! Куб перекрасился в жёлтый. Сам!! Конечно, можно перекрасить его руками, но это не интересно.

Здесь ты применил функцию getObjectFromGUID, которая принимает на вход GUID (строку), а на выход даёт объект типа userdata, с которым можно взаимодействовать. Например, уже здесь мы можем получить доступ к пространству имён кубика - cub:getVar(), cub:getTable() и т.д. Можно использовать функции изменения позиции, цвета, поворота, ускорения, размера, менять имя, подсветку и всё, что доступно через официальный API игры. Документация есть на официальном сайте, мы к ней ещё вернёмся.

setColorTint - функция изменения цвета. Она есть у всех объектов. И почти для всех работает, но присутствует вообще у всех. На вход она принимает таблицу с цветами RGB (красный-R, зелёный-G, синий-B). Ты же прочёл ту книгу, которую я советовал в начале? Молодец. Тогда ты уже знаешь, что если функция принимает лишь один аргумент и это таблица, то круглые скобки не нужны (для красоты). Но для пущей понятности и привычности можно написать и так:

local color = {1,1,0} cub:setColorTint(color)

Цикл игры и функция Update

А слабó перекрашивать куб каждую секунду то в красный, то в жёлтый? Ну, мы же не лыком шиты, это-то как раз легко:

--При создании нового объекта, ему присваивается новый GUID. local CUBE_GUID = 'ca018a' --Идентификатор нашего куба. local cube --Будущая ссылка на объект. --Функция вызывается после загрузки игры и всех объектов. function onload() cube = getObjectFromGUID(CUBE_GUID) --Получаем ссылку на объект. end local COL_RED = {1,0,0} --Красный. local COL_YELLOW = {1,1,0} --Жёлтый. local last_time = 0 --Время перекрашивания. local is_color_red = true --Красный ли куб? --Функция вызывается каждый тик. Потом оптимизируем, а сейчас лень. function update() local time_now = os.clock() if (time_now - last_time < 1) then --Прошло меньше 1 секунды. return --Завершаем работу функции. end local color = is_color_red and COL_YELLOW or COL_RED --Новый цвет cube:setColorTint(color) --Применяем цвет к нашему кубику. is_color_red = not is_color_red --Цвет поменялся. last_time = time_now --Запоминаем время изменения. end

Сразу учись программировать красиво. Без красоты нет понимания! Нужны отступы[ru.wikipedia.org] , комментарии "для тупых" (да, именно такие я пишу для себя), правильное именование имён, функций[ru.wikipedia.org] и т.д.

Погоди-ка, ты всё ещё не умеешь программировать на Луа? Если так, то вынужден огорчить, - дальше без основ уже никак. Начинается креатив, создание чего-то нового, а не просто копипаст. Так что, пожалуйста, прочти ТУ САМУЮ книгу. И на этом моменте я перестаю напоминать тебе об этом. Будем считать, что ты уже профи.

Итак, здесь есть два ключевых места - это функция onload и функция update. Это как бы "точки входа", потому что эти функции вызывает игра. Они обязаны быть в глобальном пространстве, так что не вздумай поставить перед ними слово "local", иначе они сразу станут невидимыми для игры и просто не сработают никогда.

ОптимизацияА вот переменные, константы и вспомогательные функции можно смело делать локальными. Это "невидимое" локальное пространство на самом деле работает чуть шустрее, чем глобальное. Хотя в целом это экономия на спичках, конечно же.

Переменную cube мы теперь выносим на уровень выше, чтобы она была доступна в обеих функциях. В onload она инициализируется, а в update - используется.

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

Также на заметку - старайся делать степень вложенности как можно меньше. Чем меньше, тем красивее и понятней.

updateО, это самая коварная функция, и её желательно не использовать. Но если надо, то нужно свести вычисления к минимуму. Это сейчас всё тип-топ, а когда у тебя будет сотня-другая объектов на столе, начнутся тормоза. Ведь функция вызывается каждый тик.

Тик - условная единица времени в игре, обычно равная одному кадру. Как ты знаешь, хороший FPS имеет значение не ниже 60. А это значит, что на каждый тик приходится 16мс (миллисекунд). За это время нужно вычислить все скрипты и отрендерить новый кадр в Tabletop Simulator. И проблема в том, что мы не знаем, сколько времени занимает рендеринг. Более того, он тоже зависит от количества объектов и детализации моделек. А ещё время зависит от мощности компа.

Поэтому, в идеале не следует использовать эту функцию вообще! Иначе её нужно оптимизировать максимально. Хорошо, если работу скрипта можно "размазать" по нескольким тикам. Иначе остаётся лишь маленький хитрый приём:

local my_counter = 0 function update() my_counter = my_counter + 1 if my_counter < 50 then return --Пропуск 50 тиков. end my_counter = 0 --Сброс счётчика тиков. --Далее основная работа функции. end Это не полноценная разгрузка. Но так хотя бы при перегрузе лаг будет лишь каждые 50 кадров, подёргивание будет наблюдаться сносное, да и то только у хоста.

Конечно, тебе ещё далеко до такого перегруза. Но сразу бери на заметку, что архитектуру программы лучше сразу продумывать хорошо, а не переделывать по 100 раз. К слову, перегруза и проседания FPS на слабых компах можно добиться вообще без скриптов.

Сохранение и загрузка данных скрипта

Шутки кончились. Это уже серьёзно. Работает это так: при сохранении игра ищет функцию onSave, и если находит, то вызывает и требует с неё строку для сохранения в файл игры. А при загрузке из сейва игра передаёт эту строку (если она есть) в функцию onload. Вот так относительно просто можно сохранять и загружать данные. Благо разработчики предусмотрительно оставили нам доступ к библиотеке JSON, чтобы запаковать в строку любую таблицу. Правда, там не должно быть ссылок на функции, userdata и прочие непотребства (было бы странно, если бы адреса удачно сохранялись и потом загружались).

local SAVED = {} Сразу рекомендую сделать специальную таблицу и назвать её, например, SAVED, - и туда уже заносить то, что нужно сохранить. Здесь возможны два подхода: 1) постоянно обновлять данные в таблице SAVED, либо 2) формировать таблицу SAVED заново при каждом сохранении. Всё зависит от того, что чаще. Автосохранения происходят примерно раз в минуту. А вообще в целом без разницы, какой подход применить. Но решать тебе.

Здесь я просто оставлю готовую шпору для копипаста. Лишнее нужно удалить.

local SAVED = { Red = { name = 'Красный игрок', player_cube = 'ca018a', }, Blue = { name = 'Синий игрок', player_cube = 'f6626f', }, Green = { name = 'Зелёный игрок', player_cube = '74f5ae', }, } --Функция восполняет недостающие поля в только что загруженном сейве. --Ориентируется по базовому сейву, так что много перебирать не придётся. --Она полезна, когда добавляются новые поля в скрипт, а в старом сейве их нет. local function FixSave(basic,tbl) for k,v in pairs(basic) do if type(tbl[k]) ~= type(v) then if v ~= nil then --допустимо "false", например. print("Wrong type [",tostring(k),"]: ",type(tbl[k]),', ', type(v)) tbl[k] = v --Как правило, это замена nil на таблицу (но не наоборот!) end elseif type(v) == "table" then --типы равные, так что второе тоже таблица. FixSave(v,tbl[k]) --Рекурсия end end end local function InitGame() --Здесь инициализируем игру. cube = getObjectFromGUID(CUBE_GUID) --Например, таким образом. end --Срабатывает при загрузке и сигнализирует о начале работы скрипта. function onload(save_state) --Облом случается в начале игры и при Ctrl+Z на начало игры. if save_state == nil or save_state == '' then InitGame() --Сейва нет. Но return end local BASIC_SAVE = SAVED --Сохраняем ссылку на изначальную структуру сейва. SAVED = JSON.decode(save_state) FixSave(BASIC_SAVE, SAVED) --Вытаскиваем нужные временные данные. last_time = SAVED.last_time --Далее всё как обычно. InitGame() end function onSave() --Подготавливаем SAVED. Дописываем туда нужные данные. SAVED.last_time = last_time --Потом просто возвращаем строку. return JSON.encode(SAVED) end

Документация и API


1. Lua TTS image 78

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

Важные странички в официальной документации всего две:

1) Базовый API[berserk-games.com]

Здесь перечислены функции API, доступные из глобального пространства игры. Также здесь перечислены некоторые обработчики событий (их не много, к сожалению).

Например, ты можешь отслеживать момент, когда один объект касается другого, создав специальную функцию onCollisionEnter, которая будет вызвана в этот самый момент. Почитай описания функций, там всё более-менее очевидно. Если в описании сказано "this Object", то пользоваться функцией можно только в скрипте, привязанном к объекту, иначе можно также и в глобальном скрипте.

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

2) API объекта (Object)[berserk-games.com]

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

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

Если возникнут сложные или нестандартные вопросы, то ещё есть официальный форум[www.berserk-games.com] . Там, хоть и не сразу, но должны помочь. Но вообще грамотный человек должен быть способен дойти до всего сам, пользуясь лишь документацией и собственными экспериментами, а на форум отписывать лишь про откровенные баги.

Удачи тебе в твоих начинаниях на поприще Tabletop Simulator!

Source: https://steamcommunity.com/sharedfiles/filedetails/?id=842872619					

More Tabletop Simulator guilds