Введение
Предполагается, что ты УЖЕ знаешь язык программирования Lua. Но если нет, то и ладно. Всё равно я люблю разжёвывать всё до блевоты. В любом случае советую почитать (хотя бы полистать?) книжку "Programming in Lua".
Для тех, кто не умеет искать, подсказываю. Наберите в гугле:
Программирование на языке LUA 2014 PDF RUS rutracker
Здесь же речь пойдёт о том, как Луа встроен в Tabletop Simulator, как с ним жить и как юзать. Конечно, если ты знаком с английским, то можешь сразу проследовать к официальной документации[berserk-games.com] , а эту статью закрыть и забыть.
Оглавление для этой серии статейУрок 1. Скприты Lua для TTS
Урок 2. Инструменты
Урок 3. Раскраска API для Notepad++
Урок 4. Отложенные задачи во времени
Урок 5. Колоды и карты
Прежде, чем начать...
...Нужно понять, каким местом скрипт сохраняется в игру. В общем, скрипт можно применить лишь к ЗАГРУЖЕННОЙ (из сейва) игре. Таким образом, прежде чем начать программировать, нужно загрузить игру. А для этого, в свою очередь, нужно её сохранить.
1) Сохрани игру.
2) Загрузи игру.
Круто, да? Теперь ты готов. Но я хочу, чтобы ты прочувствовал этот момент. Так что мы разберем некоторые казусы.
Предположим, ты применил скрипт к текущей игре. Затем ты кое-что изменил и сохранил игру под другим именем (чтобы не портить хороший сейв). И теперь ты хочешь изменить скрипт. Ты его меняешь и применяешь к текущей игре, наивно думая, что всё ок. Но - сюрприз! Скрипт применяется к предыдущему сейву, и он тут же загружается. Круто, да? Нет, совсем не круто. Фишка в том, что последним был загружен предыдущий сейв, а текущий сейв ни разу не загружался, поэтому скрипт применяется именно к последнему загруженному. Так что все изменения после сохранения теряются. Кстати, старый скрипт тоже теряется, затирается новым.
Следующий казус в том, что когда ты применяешь скрипт, то он сохраняется в предыдущий (загруженный) сейв, но прочие изменения теряются! Предположим, ты изменил стол, передвинул элементы, создал новые, потом написал скрипт и нажал злосчастную кнопку "SAVE&PLAY". В результате игра сохраняет скрипт в сейв и сразу загружает его, при этом теряется всё остальное, что ты сделал. Потом ты привыкнешь, и это даже будет удобно.
Ещё одно маленькое замечание. Скрипты должны быть включены в опциях игры. Вообще по умолчанию они и так включены. Так что не знаю, каким чудом они у тебя отключились, но упомянуть об этой детали было нужно, а то мало ли. Обычно из-за таких вот мелочей и появляются комменты типы "ничего не работает! хелп!".
Последний совет. Пиши скрипты в другом редакторе типа Notepad++, а потом просто копируй в игру. Так надёжней (и привычней для бывалых программеров).
Ну, теперь ты вроде точно готов. Можно начинать.
Пишем простецкий скрипт
Ну, ты понял, это "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
Не путать!
К слову, здесь уже начинаются трудности, так что лентяев прошу откланяться, до свидания.
Мы привыкли, что глобальное пространство - то место, где хранятся все глобальные переменные. Ещё его называют _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
Мда, что-то мы резко начали. Надо поубавить обороты. Вернёмся к азам и простым примерам. Давай-ка перекрасим наш красный куб в жёлтый цвет. Писать будем только в глобальный скрипт, остальное по желанию в другое время.
Чтобы узнать 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) Базовый 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
- All Guilds
- How to make almost any 3d object with nothing but an image
- Perfect Fog of War [D&D, Wargames, etc]
- Tabletop Simulator Guide 8
- D&D Setup [Up to 9 Players]
- Crash Course in Tabletop Simulator
- [Official] Knowledge Base
- Tabletop Simulator Guide 3
- Tabletop Simulator - Journey Into The Beyond Guide
- Tabletop Simulator - Laser Light Show Achievement Guide