Источник: GameDev.ru

Автор: Ash Matheson

Дата: 30.4.2003

Введение

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

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

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

Почему и почему бы нет?

Прежде всего, зачем использовать скрипт-язык? Большая часть игровой логики может быть описана на скрипт-языке для различных целей, вместо того, чтобы программировать ее как часть игрового движка. Например, загрузка или инициализации уровня. После загрузки уровня, возможно Вы захотите перевести сцену к игровому плану или может быть захотите показать некоторый предварительный текст. Используя скрипт-систему, Вы могли бы заставить некоторые объекты игры выполнять определенные задачи. Также, подумайте о реализации искусственного интеллекта. Не Игровые Персонажи должны знать, что делать. Программирование каждого NPC "вручную", в теле игрового движка излишне усложнит задачу. Когда Вы захотите изменить поведение NPC, Вам придется перекомпилировать ваш проект. С скрипт-системой, Вы можете делать это в интерактивном режиме, изменяя поведение и сохраняя настройки.

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

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

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

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

1. Кодер заинтересован в написании кода движка/инструментов, а не логики игры. 

2. Время было потрачено на написание движка/инструментов игры. 

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

4. Вы не должны перекомпилировать, если хотите изменить функциональные возможности игры. Просто измените скрипт. 

5. Вы хотите разрушить связь между машинным и игровым кодом. Они должны быть двумя отдельными частями. Таким образом, будет удобно использовать движок для последующих сиквелов (я надеюсь). 

Здесь я сделаю несколько прогнозов. В течение 5 лет, дизайнеры уровней должны будут делать больше, чем просто строить уровни. Они должны быть способными использовать скрипт для игровых сцен. Несколько компаний с передовыми взглядами уже применили этот подход. Также, Вы можете увидеть этот способ интеграции в редакторах подобно UnrealEd и Aurora toolset Bioware.

Разъяснение и разглагольствования

Надеюсь сейчас Вы уже купились на мои слова и захотели включить скрипт-компонент в вашу игру. И так, следующий вопрос: как, черт возьми, Вы это делаете?

Что я собираюсь использовать для моего скрипт-компонента - это внедряемый скрипт движок Lua. В начале скажу, что я не спец в Lua, но это относительно простой язык и не потребует утомительного изучения для овладения им. Некоторые последующие примеры, по которым я буду пробегаться, довольно просты. Вы можете получить информацию непосредственно на сайте Lua http://www.lua.org/. Также, в конце этого документа, я собираюсь включить некоторый дополнительный справочный материал. По справедливости, есть и другие скрипт-языки, типа Small, Simkin, Python, Perl. Однако Lua приятный и чистый язык. Это действительно хорошее преимущество.

Lua - Открытый Источник Кода. Это хорошо, потому что: (a) Вы получаете исходники языка и можете рыться в них сколько вздумается, (b) он бесплатен. Вы можете использовать его в коммерческих приложениях, и не раскидываться деньгами. Ну а для некоммерческих проектов сами понимаете бесплатно == хорошо.

Так, кто в настоящее время использует Lua? Lua написан шарашкиной конторкой и его используют только бедные? Ммм... не совсем так. Lua появился не вчера и использовался достаточно известными личностями:

- Lucasarts
* Grim Fandango
* Escape from Monkey Island
- Bioware
* Neverwinter Nights
* MDK2

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

Ок, достаточно с кто-есть-кто из lua разработчиков. Вы можете это сами увидеть на вебсайте lua.

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

1. Получение кода lua интерпретатора. 
2. Настройка вашей среды разработки. 
3. Сборка интерпретатора с нуля. 

Эй, я подумал, Вы сказали достаточно разглагольствований?

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

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

Состояние модуля (или: Кто имеет VM?)

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

Прежде всего, в значительной степени каждая функция в lua имеет дело с состоянием lua [lua state]. Это по существу определяет текущее состояние lua интерпретатора; следит за функциями, глобальными переменными и дополнительной связанной с интерпретатором информацией в этой структуре. Вы создаете состояние lua [lua state] запросом lua_open. Эта функция выглядит примерно так:

lua_State *lua_open (int initialStackSize);

Если хотите, можете думать о lua_State как о дескрипторе текущего экземпляра lua интерпретатора. Это справедливая аналогия.

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

Давайте разработаем некоторый код и используем его как отправную точку.  lua_open() описана в lua.h (Вы можете найти его в каталоге include). Так, попробуйте скомпилировать следующий фрагмент кода как 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 lua\n");
    return -1;
  }
  return 0;
}

К сожалению, это не будет работать. Если точнее, проблема в связях. Это довольно простая проблема, но одна из тех, которые обычно происходят когда Вы имеете дело с Открытыми Исходными проектами. По существу, lua был написан на чистом ANSI C. Пожалуйста обратите внимание: я сказал ANSI C. И еще обратите внимание что все файлы в библиотеке lua имеют расширение "c". Это по существу означает, что способ, которым компилятор прессует имена всех функций lua, основан на C соглашении о вызовах. Это отличается от описателей имен функций C++. Не сложно пофиксить, но немного раздражает. Чтобы излечится от этого, просто оберните *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 lua\n");
    return -1;
  }
  return 0;
}

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

Так, сейчас Вы вероятно задумались о том, что если мы использовали оператор 'open', то соответственно нуждаемся в операторе close. И Вы были правы. lua_close() по существу закрывает состояние, которое было открыто 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 lua\n");
   return -1;
}

// Используем lua.

lua_close( luaVM );
return 0;
}

И, вуаля, мы имеем совершенно бесполезное 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 lua\n");
  return -1;
}

// Используем lua.

char* strLuaInput = "a = 1 + 1;\n";
lua_dostring(luaVM, strLuaInput);
lua_close(luaVM);   
return 0;
}

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

Это совершенно, абсолютно … бесполезно. И что нам сделать, чтобы это стало менее бесполезно? Хорошо, если Вы сползали и скачали 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 lua\n");
  return -1;
}

  // Используем lua.

  char* strLuaInput = "a = 1 + 1;\nprint( a);\n";
  lua_dostring(luaVM, strLuaInput);
  lua_close(luaVM);   
  return 0;
}

Компиляция не имеет проблем, а вот выполнение это уже другая история:

Так в чем же дело? Я лгал? Мне кажется, прежде всего, это сообщение означает, что функция 'print' является нулевым (читайте как: пустой указатель) значением. Есть ли фактически функция с именем  print() ? Да, есть, но она не определена как стандартная в среде 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 lua\n");
  return -1;
}

// инициализация стандартных библиотечных функции lua

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

// Используем lua.

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>
}
int main(int argc, char* argv[ ])
{
lua_State* luaVM = lua_open(0);
if (NULL == luaVM)
{
   printf("Error Initializing lua\n");
  return -1;
}

// инициализация стандартных библиотечных функции lua

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

// Используем lua.

char* strLuaInput = "a = 1 + 1;\nprint( \"1 + 1: \" .. a);\n";
lua_dostring(luaVM, strLuaInput);
lua_close(luaVM);   
return 0;
}

Проще говоря, результатом будет суммирование 1+1. Так, что я сделал - создал простой пример - эффективная иллюстрация использования скрипт-языка в вашем приложении. Исходник последнего примера может быть найден в предоставленном мной проекте 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 lua\n");
    return -1;
  }

  // инициализация стандартных библиотечных функции lua

  lua_baselibopen(luaVM);
  lua_iolibopen(luaVM);
  lua_strlibopen(luaVM);
  lua_mathlibopen(luaVM);
  printf("Simple Functional lua interpreter\n");
  printf("Based on lua version 4.0.1\n");
  printf("Enter lua commands. type 'exit<enter>' to exit\n");
  printf("\n>");
  lua_dofile(luaVM, "./startup.lua");
  lua_close(luaVM);
  return 0;
}

Двигаемся дальше … на что похож этот "startup.lua"? На следующее:

-- Примитивный lua скрипт
-- Комментарий добавляется как '--'

a = 5;
b = 10;
c = a + b;

print ("5+10=" .. c);

Что же мы получим в результате? Смотрите ниже:

Как Вы можете заметить, это все достаточно просто. Но, если Вы присмотритесь, то увидите несколько проблем.

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

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

В примерах, которые я приложил к статье, я также включил дополнительные проекты: lua и luac.

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

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

OK. Если мы все же решимся на это, то должны быть способны скомпилировать наш "startup.lua" в "startup.lub" используя следующее:

Luac –o startup.lub startup.lua

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

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

Расширения данные этим файлам только для разработчика. Lua без разницы, как Вы их назовете.

Интеграция в наш код.

Сейчас Вы можете заметить, что у нас не произошло сильного внедрения lua в наше приложение. Но какой прок в использование всего этого, если Вы не можете заставить скрипт делать что-нибудь с вашим приложением. Подумайте об этом. Сейчас мы имеем скрипт-систему, но не имеем никакой реальной интеграции с вашим кодом. Я не могу использовать то, что я пока создал, чтобы фактически сделать что-нибудь, типа AI врагов ищущих игрока в игре. Что нам сейчас необходимо - это поддержка функций, так же как методов 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 разработана как язык с динамическим контролем типов. Если точнее, lua вообще не имеет никаких типов переменных. Нет никакого int, float, char или подобного объявления переменной. Сверхъестественная вещь, даже при том, что мы только что сказали, что нет никаких типов в lua, все значения таких переменных могут быть использованы.

Что Вы сказали?

По порядку. Говоря 'динамический тип' мы подразумеваем то, что переменная не имеет никакого определенного типа, но имеет значение.

ЧЕГО, ЧЕГО?

Это может показаться немного сложным, если Вы не занимались COM программированием или не заглядывали под капот Visual Basic. Если Вы всё-таки имели такой опыт, то это всего лишь обзор для Вас; иначе, мы погружаемся в сказочную страну универсальных [variant] типов данных.

Variant по существу 'содержит все' типы данных. По определению, любая переменная, созданная как вариантная, имеет любой тип данных … пауза … не поверите, но у этого есть смысл … наверное. Попробуем. Скажем, есть новое определение типа (в символическом виде):

typedef struct _variant
{
  int type;
  union
  {
    int   Integer;
    double Float;
    char  String[255];     
  }
} Variant;

Что мы здесь имеем - новый тип названный variant. Основываясь на значении поля type, мы могли бы использовать Integer, Float или String поля для взятия данных. Каждая переменная, которую мы создаем основана на Variant и может содержать любые данные, но какие именно зависит от ее значения. Это очень просто. И это эффективно, Вы вскоре сами увидите. Пожалуйста обратите внимание, что lua не использует такой вид структуры, поскольку это внутренний тип данных и намного сложнее чем представлено здесь. Это была просто иллюстрация.

Так, все, что понимает lua - это вариантный тип данных. Данные, которые он может содержать в этом вариантном типе: nil, число, строка, функция, userdata и table. Так из этого видно, что переменная в lua может быть числовой, символьной или функцией. Остальные два, userdata и table, я объясню позднее.

Итак, если мы можем поместить данные в этот вариантный тип в числовом или символьном виде, то надеемся, что есть и какой-то способ извлечь их на стороне 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++. На кой нужен нам индексный [index] элемент? Что, если мы хотим передать несколько параметров? Само собой, это возможно. Так, как мы обработаем это? Ответ: lua стек.

Стек Lua.

Данные передаются из lua в C/C++ и обратно через lua стек. Это, по существу, канал связи. Когда я вызываю функцию, функция помещается в стек, затем первый параметр так же помещается в стек и так далее. Это не традиционный стек, единственные операции доступные со стеком - положить и извлечь. Эти функции описывают всю специфику доступа к элементам стека. Как упомянуто ранее, это не совсем стек, но сейчас мы рассматриваем его именно так. Все операции доступа к элементам стека осуществляются по индексам. Значение которого может быть как положительным так и отрицательным. Вот что говорится в документации lua об этом:

Положительный индекс представляет абсолютную позицию в стеке (начинающуюся с 1, а не 0 как в C); отрицательный индекс представляет смещение от вершины стека. То есть, если стек имеет n элементов, индекс 1 представляет первый элемент (первый элемент, помещенный в стек), индекс n представляет последний элемент; индекс -1 также представляет последний элемент (то есть верхний элемент), и индекс -n представляет первый элемент. Мы говорим, что индекс допустимый, если он лежит между 1 и вершиной стека (то есть (1 <= abs(индекс) <= вершина стека)).

Также замечу, что есть несколько дополнительных функций, которые сгруппированы как lua_toXXX, используемые для обращения к стеку lua. Исследуйте документацию к lua для справки. В следующих примерах, я буду копаться в них. В общем, о чем я? А, да, склеивающие функции …

Если Вы взгляните на мою склеивающую функцию l_addNPC:
int l_addNPC( lua_State* luaVM)
{
  theNPCManager->AddNPC( lua_tostring(luaVM, -1) );
  lua_pushnumber( luaVM, 0 );
  return 1;
}

По порядку, что я делаю: вызываю AddNPC метод менеджера NPCManager. Как параметр я передаю строку, которая в настоящее время является последним элементом в стеке lua. Сейчас AddNPC  ничего не делает, я считаю, что все проходит нормально и помещаю число обратно в стек lua, по существу это возвращаемое значение. Точно так же в DeleteNPC.

OK, теперь мы имеем склеивающую функции. Далее, как объяснить lua, что такие функции существуют? И как объяснить, сколько параметров они требуют. Внимание, приготовьтесь. С помощью lua Вы можете послать сколько угодно параметров. Также, любая функция в lua может вернуть больше чем один результат. Это действительно так. Любая lua или доступная lua функция может вернуть множество значений. Это называется Кортеж [Tuple]. Это круто, но вначале немного смущает. Так, что насчет связывания 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) добавляется в 'глобальное' [global] пространство имен функций. Теперь, когда нам потребуется функция в lua мы используем это имя, которое будет связано с нашей функцией.

С помощью макрокоманды lua_register, мы предоставили lua две функции с именами addNPC  и deleteNPC. Теперь я могу использовать их в любом скрипте lua. Итак, основываясь на предыдущем примере, если Вы исследуете main.cpp, то заметите такие изменения:

int main(int argc, char* argv[])
{
  lua_State* luaVM = lua_open(0);
  if (NULL ==  luaVM)
  {
     printf("Error Initializing lua\n");
    return -1;
  }
  // инициализация стандартных библиотечных функции

  lua_baselibopen(luaVM);
  lua_iolibopen(luaVM);
  lua_strlibopen(luaVM);
  lua_mathlibopen(luaVM);
  printf("Simple Functional lua interpreter\n");
  printf("Based on lua version 4.0.1\n");
  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 exit\n");
  printf("\n>");
  lua_dofile(luaVM, "./startup.lua");
// Вывод NPC которые были добавлены.
  theNPCManager->Dump();
  lua_close(luaVM);
  return 0;
}

Далее, какие изменения в lua? Хм, это очень просто:

-- Простейший lua скрипт
-- комментарий добавляется как '--'
-- Новые C функции доступны из 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

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

Спасибо Рику Керри (editor@rickkerry.com) за помощь с корректировкой этого документа.

Ссылки:
Lua.org website
Lua Wiki
Lua intro
Об авторе

Ash Matheson - Глава Отдела Игрового Обучения в Центре Цифрового Изображения и Звука (CDIS) в Барнаби, Канада. Он там преподает программирование игр в течение двух лет. До этого, он был программный инженер в Hummingbird Communications, второй по величине компании по разработке ПО в Канаде. Он также привлекался с несколькими независимыми игровыми компаниями как глава по разработке.