Реклама
Препараты для набора мышц.
Спонсорские ссылки
-
Статья
Версия для печати
Обсудить на форуме
Введение в Lua


Ash Matheson

Введение

Недавно мой близкий друг ходил на собеседование в компанию по разработке игр. Не буду называть имен, скажу просто, что это - большая компания по разработке игр в Ванкувере. Само собой разумеется, он не получил работы, но это уже другая история. Однако я полагаю, что одна из причин, почему он не получил работу, была в его недостаточном знакомстве с языком сценариев, который они используют, называемый Lua. А это касается меня, покольку я обучаю студентов программировать игры, и скрипты - тема, которой я не уделял достаточно внимания в прошлом. Мы рассматриваем скрипты Unreal, как часть курса по использованию существующего механизма, но фактически не рассматриваем создание механизма сценариев и включения его в инструментарий или движок. Поэтому, вооруженный web-сайтом, я решил сломать этот небольшой барьер. Результат описан в этом документе.
Я не уверен только, каким длинным будет этот документ. Может, я решу разбить его на несколько частей или написать одну длинную напыщенную речь. Так или иначе, я решу этот вопрос немного позже, когда я приведу мои заметки в более интеллектуальный и последовательный формат.

Почему и прочее

Прежде всего, зачем используется язык сценариев? Большая часть игровой логики может быть задана сценарием для функциональных возможностей быстрее, чем кодирование её как части движка. Например, подумайте о загрузке или инициализации уровня. Когда уровень загружен, возможно, вы захотите подготовить сцену к игре. Или, возможно, вы захотите показать какие-нибудь титры. С системой создания сценария вы могли заставлять отдельные игровые объекты выполнять определенные задачи. Также подумайте об искусственном интеллекте. NPC (Non-Player Characters) должны знать, что им делать. Задача кодирования каждого NPC вручную в игровом движке могла бы быть упрощена. Если бы вы хотели изменить поведение NPC, вы будете должны перекомпилировать вашу систему. С системой создания сценария вы могли в интерактивном режиме изменять поведение и сохранять его снаружи. Я коснулся этой проблемы немного в прошлом параграфе, и я продолжу её немного позже. Вопрос, почему бы не написать всю игровую логику на C/C++? Давайте представим, перспективы программирования, мы начинаем беспокоиться о себе и об игровом коде, также как и о движке и инструментарии и - вот, вы получаете идею. Теперь мы можем с простым языком сценариев передать эти функциональные возможности проектировщикам уровня. А они могут пробовать ковырять и оптимизировать игровой процесс. Вот примеры:

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

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

Это довольно наглядный пример, может быть, он немного преувеличен, но я надеюсь, что он привнес ясности. Чего мы пытались добиться этой моделью - перейти к большему количеству управляемых данных в технологическом процессе. Итак, вот к чему мы пытаемся прийти:
  • Кодер заинтересован в написании кода движка/инструментария раньше, чем игровой логики.
  • Экономим время на написании игрового движка/инструментальных средств.
  • Даем проектировщикам возможность 'вертеть' вещами (они это любят). Сценарии дают простой доступ к функционалу. Это также даёт им большую гибкость при испытании предметов в уровне, для чего они обычно должны были привлекать кодера.
  • Избавляемся от необходимости перекомпиляции, если вы захотите изменить функциональные возможности в игре. Просто измените сценарий.
  • Разрушаем связь между движком и игровым кодом. Они должны быть двумя отдельными частями. Это удобно при использования движка в многопользовательских играх.
Через 5 лет проектировщик уровня должен будет делать больше, чем просто компоновка хорошего уровня. Они будут должны уметь писать сценарии к уровням. Несколько предусмотрительных игровых компаний уже применяют этот подход. Вы можете увидеть этот вид интеграции в редакторах, подобных UnrealEd и комплекту инструментальных средств Aurora Bioware.

Достаточно пониманий и напыщенных речей
Надеюсь, к этому моменту я дал каждому объяснения относительно того, почему вы захотите включить компонент скриптов в вашу игру. Cледующий вопрос: как, черт возьми, вы cделаете это?

Я собираюсь использовать для моего компонента скриптов встраиваемый механизм сценариев по имени Lua. Для начала скажу, что я не мастер по Lua, но это - относительно простой язык, так что вам не потребуется много времени, чтобы получить власть над ним. Некоторые из примеров, которые я дам позже, довольно прямолинейны. Вы можете получить информацию по Lua непосредственно в www.lua.org. Также, в конце этого документа, я собираюсь включать ссылки на некоторый дополнительный материал. Есть и другие языки сценариев, подобные Small, Simkin, Python и Perl. Однако Lua - хороший и чистый язык. Также он имеет действительно хорошее преимущество. Lua - это Открытые Исходники. Это хорошо, потому что (a) вы получаете исходник к языку, если вы хотите глубоко в него зарыться, и (b) он бесплатный. Вы можете использовать его в коммерческих приложениях, не тратя на это денег. Поскольку независимое развитие игр идет свободным == хорошим (free == good).

Так, кто в настоящее время использует Lua? Lua - простой язык, который используется только бедными? Да, но не совсем. Lua крутится уже некоторое время, и его использует больше чем пара относительно важных разработчиков:
- Lucasarts
  * Grim Fandango
  * Escape from Monkey Island
- Bioware
  * Neverwinter Nights
  * MDK2

Всё, достаточно с "Кто есть кто?" из lua разработчиков. Вы можете видеть всех разработчиков, использующих lua на www.lua.org.

Давайте начнем с действительно простого. Первое, что мы должны показать: как использовать lua как простой интерпретатор. Для это надо:
  • Получить код интерпретатора lua.
  • Настроить вашу среду разработки.
  • Собрать интерпретатор.

Эй, я думал, что вы сказали достаточно напыщенных речей?

Да, я сказал, не правда ли? Так, давайте приниматься за дело. Вы можете получить весь lua исходный код, зайдя на www.lua.org. Я также хотел бы обратить ваше внимание, что готовится к выходу новая версия Lua 5.0. Я не собираюсь обсуждать эту версию в этой статье. Я исследую это позднее, ну а пока мы будем использовать 4.0.1.

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

Сборка интерпретатора очень проста теперь, когда у нас есть библиотека. Я собираюсь сгенерировать простое консольное Win32 приложение, которое берет данные с клавиатуры и использует lua, чтобы интерпретировать их.

Состояния! (или: У кого есть VM?)

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

Прежде всего, в значительной степени каждая функция в lua имеет дело с lua_State. Оно, по существу, определяет текущее состояние интерпретатора; следит за функциями, глобальными переменными и дополнительным интерпретатором, связанным с информацией в этой структуре. Вы создаете lua_State запросом к lua_open. Эта функция выглядит так:
 
Lua_State *lua_open(int initialStackSize);

Если хотите, вы можете думать о lua_State как о дескрипторе к текущему образцу lua интерпретатора. Это справедливая аналогия. Получая непустой указатель lua_State, мы знаем, что lua сумел инициализироваться правильно. И это хорошо и означает, что мы теперь можем работать lua. Но по умолчанию мы можем делать только ограниченный набор действий с lua. Больше об этом вскоре.

Давайте напишем некоторый код который сможем использовать как отправную точку. Lua_open() определена в lua.h (их можете найти в каталоге библиотек lua). Так, мы должны скомпилировать следующий фрагмент кода как консольное win32 приложение:
Код:

#include <stdio.h>
#include <lua.h>
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   return 0;
}

К сожалению, оно не будет работать. Более того, это не будет линковаться. Это - довольно простая проблема, но одна из тех небольших проблем, которые вы получаете, когда имеете дело с проектами на Открытых Исходниках. По существу, lua был написан совместимым с ANSI C. Пожалуйста, обратите внимание: я сказал ANSI C. Обратите внимание, что все файлы в lua библиотеке имеют расширение "c" на конце. Это, по существу, означает, что способ, которым компилятор компонует имена всех функций в lua, основан на соглашении о вызовах для C. И есть разница в том, как обрабатываются имена заголовков C++ и С. Это не сложно исправить, больше раздражает. Чтобы исправить это, просто перенесите  #include <lua.h> и любой #include из библиотеки lua внутрь:
Код:

extern "C"
{
   
}

Итак, что мы имеем в конце:
Код:
#include <stdio.h>
extern "C"
{
 #include <lua.h>
}
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   return 0;
}

Легко, это - кандидат на включение в его собственный заголовочный файл. Трансляция в этой стадии произведет exe файл. Он ничего не делает, но выполняется.

Так, в этом пункте, вы вероятно думаете, что если мы имеем процедуру 'open', нам нужна и процедура close. И вы правы. Lua_close (), по существу, закрывает state, которое было открыто с lua_open (). Очень прозрачно. Формат lua_close () выглядит примерно так:
void lua_close (lua_State *openState);
 
Мы можем завершить наш код, написанный выше, добавляя это к приложению:
Код:

#include <stdio.h>
extern "C"
{
 #include <lua.h>
}
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // Do stuff with lua code.
   
   lua_close( luaVM );
 
   return 0;
}

И вуаля, мы имеем полностью бесполезный кусок lua. Но он еще в младенчестве с полностью внедренной системой скриптов.

Выполнение кое-чего полезного с lua.
Достаточно установок, давайте сделаем что-нибудь функциональное. В настоящее время мы имеем достаточно кода за поясом, чтобы создать интерпретатор. Я собираюсь сосредоточиться в следующем раздел на формировании очень ограниченного интерпретатора. Никаких причудливых редакторов, никакой встроенной отладки. Только консоль, которая позволяет нам печатать команды для lua и интерпретатор, исполняющий действия. Это требует знания еще одной функции lua: lua_dostring (). По существу, эта функция исполняет действия в lua. Я не знаю, как еще описать это, кроме как проиллюстрировать:
Код:

#include <stdio.h>
extern "C"
{
 #include <lua.h>
}
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // Do stuff with lua code.
   
   char* strLuaInput = "a = 1 + 1;n";
 
   lua_dostring(luaVM, strLuaInput);
 
   lua_close(luaVM);   
 
   return 0;
}

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

 
О действительно, по-настоящему бесполезно. Как мы сделаем это менее бесполезным? Хорошо бы, если бы вы сходили и загрузили lua, или посмотрели бы на некоторые примеры lua. Вы увидите, что в lua есть функция, называемая print. Так, давайте добавим это в код и перезапустим. Вот код, который мы используем.
Код:

#include <stdio.h>
extern "C"
{
 #include <lua.h>
}

int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // Do stuff with lua code.
   
   char* strLuaInput = "a = 1 + 1;nprint( a);n";
 
   lua_dostring(luaVM, strLuaInput);
 
   lua_close(luaVM);   
 
   return 0;
}

Компилирование проходит без проблем. Выполнение этого - другая история:

 
Так в чем же дело? Я солгал? Прежде всего, это говорит мне, что 'print' имеет значение nil (читай: null). Так имеется ли фактически функция, называемая print()? Да, есть, но она не определена в стандартном окружении lua. Она в библиотеке, которую, оказывается, надо подключить к нашему приложению. Этот механизм, по существу, и есть то, как мы собираемся расширять lua для наших собственных эгоистичных целей. Так или иначе, чтобы получить print () и некоторые другие нужные для работы функции, нам необходимо иметь больше функций, подключенных к системе. Мы можем сделать это следующим образом:
Код:

include <stdio.h>
extern "C"
{
 #include <lua.h>
 #include <lualib.h>
}
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // initialize lua standard library functions

   lua_baselibopen(luaVM);
   lua_iolibopen(luaVM);
   lua_strlibopen(luaVM);
   lua_mathlibopen(luaVM);

   // Do stuff with lua code.
   
   char* strLuaInput = "a = 1 + 1;nprint( a);n";
 
   lua_dostring(luaVM, strLuaInput);
 
   lua_close(luaVM);   

   return 0;
}

Выполнение кода теперь производит реальный результат:

 
Я могу также сделать вывод более понятным, изменяя команды, посылаемые lua:
Код:
#include <stdio.h>
extern "C"
{
 #include <lua.h>
 #include <lualib.h>
}
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // initialize lua standard library functions
   lua_baselibopen(luaVM);
   lua_iolibopen(luaVM);
   lua_strlibopen(luaVM);
   lua_mathlibopen(luaVM);
   // Do stuff with lua code.
   
   char* strLuaInput = "a = 1 + 1;nprint( "1 + 1: " .. a);n";
 
   lua_dostring(luaVM, strLuaInput);
 
   lua_close(luaVM);   
 
   return 0;
}

Это выводит сумму 1+1. Что я сделал: создал простую, и все же эффективную иллюстрацию подключения языка сценариев к вашему приложению. Этот исходник может быть найден в рабочем пространстве lua, как проект по имени SimpleInterpreter. Это относительно просто, чтобы можно было взять этот элементарный пример и скомпилировать простой интерпретатор. Все, что мы должны сделать к этому моменту, это добавлять некоторый текст, для увеличения возможностей. Я сделал это в проекте FunctionalInterpreter. Это элементарный интерпретатор, и нечто иное, чем простое расширение проекта SimpleInterpreter. Я не буду приводить здесь код, потому что это просто расширение того, что я показал.

Получение данных от файла.

Пока мы вводили весь код lua вручную. Это замечательно, если вы хотите набирать ваш сценарий раз за разом. Это ново в течение приблизительно 5 секунд. Итак, как мы будем получать данные из файла? Два решения напрашиваются сами собой. Но вопрос остается: как мы можем сохранить эти данные? Это действительно очень просто, как и всё, что мы видели пока в lua. В lua есть функция lua_dofile (), что, по существу, обрабатывает содержание файла. Ну ничего себе, это же делает жизнь действительно интересной. Итак, если я добавлю это к текущему исходному тексту, я могу заставить мое приложение выполнять сценарий всякий раз, когда оно выполняется. По существу, вот так:
Код:

#include <stdio.h>
extern "C"
{
 #include <lua.h>
 #include <lualib.h>
}
 
int main(int argc, char* argv[ ])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // initialize lua standard library functions
   lua_baselibopen(luaVM);
   lua_iolibopen(luaVM);
   lua_strlibopen(luaVM);
   lua_mathlibopen(luaVM);
 
   printf("Simple Functional lua interpretern");
   printf("Based on lua version 4.0.1n");
   printf("Enter lua commands. type 'exit<enter>' to exitn");
   printf("n>");
 
   lua_dofile(luaVM, "./startup.lua");
 
   lua_close(luaVM);
 
   return 0;
}

Это перемещает нас к следующему шагу, на что похож "startup.lua"? Ладно, он похож на это:
Код:
-- Simple lua script
-- comments indicated with a '--'
a = 5;
b = 10;
c = a + b;
print ("5+10=" .. c);
И что мы получаем в результате?:
 


Как вы можете видеть, это остается относительно простым. Но если вы смотрите, вы можете увидеть пару проблем с этой аналогией. Прежде всего, мы теперь демонстрируем весь наш исходник. Это, может быть, и не шикарная идея, так как это, по существу, демонстрирует функциональные возможности игры (игровую логику) каждому в доступном формате. Что опять же может быть не желательно. Так как исправить эту недоработку? Компилируем наши сценарии.

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

В исходном рабочем пространстве, которое я включил, я также включил в дополнительные проекты: lua и luac. Lua - интерпретатор, который идет с таром, распространяемым организацией lua. Luac - компилятор байт-кода. Он, по существу, компилирует lua исходник в формат, который человечески не читаем. Кроме того, байт-код выполняется немного быстрее. Не сильно, но немного.

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

 
Итак, если мы хотим откомпилировать файл "startup.lua" в "startup.lub" то должны выполнить следующее:
Код:
Luac o startup.lub startup.lua

Я переместил файл startup.lua в каталог к luac.exe, но если я пропишу эту директорию в путь, то я смогу выполнять эту программу откуда угодно. Так или иначе, вот результат:

 
И в самом низу вы видите startup.lua и startup.lub. Откройте оба в текстовом редакторе, и вы увидите, что lua текстовый файл, а lub двоичный. Я хотел бы отнять секунду и сказать, что расширения, данные этим файлам - только для разработчика, самому lua всё равно, как вы назовёте файлы.

Интегрирование с вашим кодом

К этому моменту вы увидели, что не сложно встроить lua в приложение. Но как использовать его, если вы не можете заставить его сделать хоть что-нибудь с вашим приложением. Подумайте об этом. Мы уже имеем систему сценариев, но мы не имеем никакой реальной интеграции с приложением. Я не могу использовать то, что я пока построил, чтобы создать хоть что-то подобное вражескому ИИ для поиска вашего игрока в игре. Нам необходимо что-то сделать чтобы предоставить функции, также как методы для lua. Так что это - наш следующий шаг. Чтобы сделать его, мы создадим пару простых функций, к которым мы сможем обращаться через lua. Так, что я сформирую - простой Менеджер шаблон для NPC. Это - единичный объект, который позволяет создавать, управлять, и удалять NPC объекты и который мы можем использовать в игре. Я хочу еще раз сказать, что этот менеджер, который я создал, только для образовательных целей. И он никоим образом не отражает то, что я использовал бы для NPC менеджера в реальной жизни.

Так или иначе, назад к проекту. Я создал Binding01, как пример что вы сможете делать, когда вы интегрируете lua и существующую базу. Вот краткий обзор того, что происходит. Я создал единичный объект с именем NPCManager. Он создает и управляет объектами NPCObjects и использует список STL, чтобы следить за ними (повторю, это - простой пример). В настоящее время NPCObjects содержит только 'имя' объекта. Этот объект нигде непосредственно не представлен lua. И когда мы захотим получить доступ к объекту, мы сделаем это через его имя. Это грубо и никоим образом не является образцом, того, что нужно делать, но это просто пример. Итак, NPCManager создает и управляет NPCObjects. NPCManager - единичный, так что он существует с первого использования, и до конца его жизненного цикла. Следующий вопрос, как мы предоставим функциональные возможности NPCManagerа lua? Мы сделаем это через функцию клея(glue). Так что же делает функция клея? По существу, это функция связи между lua и вашим кодом. А почему нам необходима эта связь? Это - всё, потому что lua не имеет тех же типов переменных, что и C/C++.

Специфика Lua.

Жизнь была бы действительно хороша, если бы типы данных lua и C/C ++ совпадали. Но если бы это было так, то тогда бы мы имели те же проблемы, что мы имеем с C/C++ (распределение памяти, контроль соответствия типов и еще много чего). Чтобы помочь нам, lua разработала динамический язык с контролем типов (dynamically typed language). В сущности, lua не имеет типов переменных. В lua нет никакого int, float, char или подобного объявления переменных. Сверхъестественная вещь, даже при том, что мы только что сказали, что в lua нет никаких типов, фактически они есть.
Скажешь "что"? Ну, под динамически типизированным, что мы подразумеваем - то, что переменная не имеет определенного типа, но имеет значение.

Скажешь "ЧТО"? Ну, это может быть немного трудно, если вы не имели дело с программированием COM или не работали с Visual Basic. Если у вас есть такой опыт, тогда это только обзор для вас; иначе мы идем к земле фантазий типов данных variant.

Variant - по существу 'принимает все' типы данных. По определению, любая переменная, созданная как вариант, имеет различный тип данных, подождите  да, это имеет смысл  своего рода. Давайте это попробуем. Скажем, у меня есть новый typedef (в псевдокоде):
Код:
typedef struct _variant
{
   int type;
   union
   {
      int   Integer;
      double Float;
      char  String[255];     
   }
} Variant;

И что мы имеем, вот: новый тип называемый variant. Основанный на значении в типе(type), мы бы использовали его как любой int, Float или String, чтобы добраться до данных. Каждая переменная, которую мы создаем основываясь на variant - вариант, но то что он содержит, определено его значением. Это - просто. И, это может быть очень мощным, как Вы вскоре увидите. Пожалуйста, обратите внимание, что lua не использует этот вид структуры для своего внутреннего типа данных. Он гораздо более сложный, чем этот. Это была чистая иллюстрация. Так что lua понимает тип данных variant. Вот данные, которые он может держать в этом типе variant: nil, число, строка, функция, userdata, и таблица. Как, мы можем видеть, переменная в lua может быть числовая, символьная, или функция. Про другие два типа: userdata и таблица, я объясню в более позднем документе.
И так, если мы можем помещать данные в этот variant в числовом или символьном, то есть некий путь, на стороне C++, для вывода этих данных. Есть несколько доступных функций для конвертации и управления этими данными. Вот некоторые из них:
Код:

double            lua_tonumber (lua_State *L, int index);
const char*       lua_tostring (lua_State *L, int index);
size_t            lua_strlen (lua_State *L, int index);
lua_CFunction     lua_tocfunction (lua_State *L, int index);
void*             lua_touserdata (lua_State *L, int index);

Есть еще функции для доступа к параметрам, проходящим через lua. Так что они делают? Также как и читают, они конвертируют данные из одного формата lua, в какой то формат C++. Но что - с тем индексным элементом. Что относительно того, если мы хотим пройти по паре параметров? Я подразумеваю, что это возможно. Ну и, как мы будем обрабатывать это? Ответ: стек lua.

Стек Lua.
Данные проходят от lua до C/C ++ и обратно через стек lua. Это - по сути канал связи. Когда я вызываю, функция, функция помещается в стек, первый параметр помещается в стек, и так далее. Это - не традиционный стек, обычно единственные операции, которые доступны для стека - pop и push функции. Те функции, что я описал, имеют полный доступ к отдельным элементам в стеке. Как я говорил прежде, это не совсем стек, но мы пока будем считать его стеком. Каждая операция обращения к элементу стека использует индекс. Это индексное значение может быть как положительным, так и отрицательным. Что нам говорит документация по lua на это счет:
Положительный индекс представляет собой абсолютную позицию стека (начинающийся с 1, а  не с 0 как в C); отрицательный индекс представляет смещение от вершины стека. Более подробно, если стек состоит из n элементов, индекс 1 представляет первый элемент (первый элемент, помещенный на стек), индекс n представляет последний элемент; индекс-1, также представляет последний элемент (то есть первый элемент сверху), и индекс -n представляет первый элемент. Мы говорим, что индекс валидный, если его значение представляет элемент между первым и последним элементом стека (1 <= abs(index) <= top).
Также замечу, что есть еще несколько дополнительных функций, которые сгруппированы в набор lua_toXXX, используемых для обращения к стеку lua. Проверьте документацию, которая идут с lua для справки. В более поздней обучающей программе, я в них поковыряюсь. Так или иначе, на чем я остановился? О да, функции  клея(glue)
Смотрите на мою функцию клея l_addNPC:
Код:
int l_addNPC( lua_State* luaVM)
{
   theNPCManager->AddNPC( lua_tostring(luaVM, -1) );
   lua_pushnumber( luaVM, 0 );
   return 1;
}

Итак, это по сути вызов метода AddNPC NPCManagerа. В качестве параметра я беру последнюю строку находящуюся в стеке lua. Пока AddNPC  метод типа void, я предполагаю, что все подходит хорошо, и помещаю номер, назад на стек lua, по сути это возвращаемое значение. Подобным образом поступим и с DeleteNPC. И теперь у меня есть функции клея. Теперь, как сообщить lua, что эти функции существуют? И как мы сообщим lua, сколько параметров, используют эти функции. Я, вас еще раз удивлю. С lua, можете посылать столько хотите параметров в функцию. Также как и любая функция в lua может возвращать больше чем один результат. Правильно. Любая lua или lua доступная функция может возвращать множественные значения. Это было названо Кортеж(Tuple). Это классно(cool), но поначалу это может быть немного странно. Так что же у нас со связью lua и C/C ++? Это не так сложно. Теперь, нам надо сделать функция регистрации в lua. И функция Lua lua_register обеспечивает нас этими функциональными возможностями.
lua_register (L, n, f)

Где
L: lua_State, для регистрации функции в
N: символьное имя функции, предоставляемой lua
F: функция клея
Для удивления достаточно того, что lua_register() - не функция, это - макрокоманда. Которая расширяется до вот такого:
 
(lua_pushcfunction(L, f), lua_setglobal(L, n))
 lua_pushcfunction помещает функцию C в lua_State. Также как, имя этой функции (n) добавляется к 'глобальному' пространству имен функций. Теперь, в любой момент в lua мы можем использовать это имя, и оно будет связано с той функцией. Это отношение один  к  одному.
Теперь, с макрокомандой lua_register мы добавим две lua функции, называемые addNPC и deleteNPC. Теперь я могу их использовать в любом сценарии lua, пока они регистрируются при инициализации lua. И так, возьмем предыдущий пример и изменим его следующим образом:
Код:
int main(int argc, char* argv[])
{
   lua_State* luaVM = lua_open(0);
 
   if (NULL == luaVM)
   {
      printf("Error Initializing luan");
      return -1;
   }
 
   // initialize lua standard library functions
   lua_baselibopen(luaVM);
   lua_iolibopen(luaVM);
   lua_strlibopen(luaVM);
   lua_mathlibopen(luaVM);
 
   printf("Simple Functional lua interpretern");
   printf("Based on lua version 4.0.1n");
   printf("Registering Custom C++ Functions.n");
   lua_register( luaVM, "addNPC", l_addNPC );
   lua_register( luaVM, "deleteNPC", l_deleteNPC );
 
   printf("Enter lua commands. type 'exit' to exitn");
   printf("n>");
 
   lua_dofile(luaVM, "./startup.lua");
 
   // Print out the NPC's that have been added.
   theNPCManager->Dump();
 
   lua_close(luaVM);
 
   return 0;
}

Я подсветил изменения в коде синим, чтобы было видно разницу между двумя версиями кода. Что бы я затем сделал в lua? На удивление простую вещь:
Код:
-- Simple lua script
-- comments indicated with a '--'
-- New C functions exposed to lua
--  addNPC("NPC Name")
--  deleteNPC("NPC Name")
 
addNPC("Joe");
addNPC("Sue");
addNPC("KillBot2000");
 
addNPC("BotToDelete");
addNPC("Krista");
addNPC("Brandon");
deleteNPC("BotToDelete");

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

 
Я могу добавлять и удалять NPC в систему. Потом вывожу результаты как часть движка, не сценария lua. Я также могу изменять функциональные возможности этого приложения просто, изменяя сценарий. И при этом не должен перекомпилировать движок. Я могу если захочу использовать luac компилятор. Я включил откомпилированную версию 'startup.lua', названную 'startup.lub'. Измените main.cpp, чтобы загрузить этот файл.

Заключительные слова
Ну, это довольно большой документ для меня. На это ушло лучшее время двух выходных дней (Слава Богу, за длинные выходные). Использованный в этом уроке исходный текст  на сам деле это другой проект, который я создал в период начальной практики. Это немного более сложно, и я планирую написать об этом. А для тех из Вас кто захочет поэкспериментировать с тем, что я написал здесь. Могут получить исходник к этим проектам здесь http://gamestudies.cdis.org/~amatheson/lua_examples.zip. Всё, что я могу сказать, это то, что опыт, полученный экспериментальным путём, был бы очень полезен и дал бы истинное понимание любого приложения, которое Вы создаете. Есть намного больше тем для обсуждения связанных со скрипт языками, и скоро я вернусь к этому. Вашим комментариям относительно этого урока - всегда добро пожаловать, также как предложениям/рекомендациям. Так что развлекайтесь.
Благодарность Рику Керри (editor@rickkerry.com) за помощь с корректировкой этот документ.

Ссылки:
Lua.org website: www.lua.org
Lua Wiki: http://lua-users.org/wiki/
Lua intro: http://kakariko.ne.client2.attbi.com/lua/Lua_Workshop_files/frame.html

Об авторе
Ash Matheson - Глава Отдела Игрового Обучения в Центре Цифрового Изображения и Звука (CDIS) в Барнаби, Канада. Он там преподает программирование игр в течение двух лет. До этого, он был программный инженер в Hummingbird Communications, второй по величине компании по разработке ПО в Канаде. Он также привлекался с несколькими независимыми игровыми компаниями как глава по разработке. Вы можете с ним связатся по почте: amatheso@artschool.com.
Эта статья была зарегистрирована в GameDev.net: 4/30/2003

(Обратите внимание, что эта дата не обязательно соответствует дате написания статьи)
Оригинал
Версия для печати
Обсудить на форуме