Lua для игр и не только. Часть 5
Взяв какую-нибудь книгу по разработке компьютерных игр, в 99 случаях из 100 вы там не встретите упоминания о Lua, хотя использование этого языка является технологическим стандартом, причем в рамках современных реалий оно оптимально. За кажущейся простотой Lua скрывается огромная мощь. Этим языком заинтересовались не только разработчики игр, но и девелоперы кроссплатформенных решений. На Lua очень удобно реализовывать всю внутреннюю обработку данных и все вычисления, оставляя компилируемой платформе (на базе С/С++/С# и т.п.) программной части выполнение только ключевых операций по общению с «железом». К примеру, низкоуровневый движок отвечает за вывод видеоинформации на экран, но что именно должно выводиться, пишется на Lua. Если вы все это начнете писать на компилируемом языке, то сложность реализации игры как таковой значительно возрастет, не говоря уже о том, что если вы используете языки C, C++, Java, ко всему прочему вам будет нужно следить за ресурсами, бороться с утечкой памяти. В Lua это реализовано на автомате, как, впрочем, и на C#. И хотя довольно часто можно услышать, что Lua предназначен только для реализации искусственного интеллекта, это не со всем так. Можно встретить и next gen разработки, в которых на Lua реализуется практически все, что не касается прямого общения с аппаратной частью. А для Windows ситуация вообще выгодна, поскольку большую массу операций по работе с «железом» берет на себя низкоуровневый API DirectX (Direct3D, DirectSound). То есть, на вопрос: сложна ли разработка компьютерных игр, можно отвечать по-разному.
Сегодня мы продолжаем изучать язык в автономном режиме, для чего пользуемся редактором/компилятором SciTE (бесплатно скачивается с сайт , 1,2 Мб).
Параллельное присваивание
Многим читателям предыдущих частей материала стало известно, что в Lua тип имеют только значения, а переменные типизируются динамически. Мы уже говорили о специфике присваивания, когда было показано разграничение между присваиванием значений для переменных и присваиванием одним объектам ссылок на другие.
То есть выражение A=B присвоит A ссылку на значение B, если последняя не является функцией, потоком, пользовательскими данными либо таблицей, или, другими словами, объектом. В этом случае A будет ссылаться на объект и станет идентичной B.
Теперь рассмотрим операцию еще более полно. Lua поддерживает параллельное присваивание, которое также называют множественным, но определение «параллельное» более емко описывает суть происходящего. Например, чтобы поменять значения x и у местами, достаточно набрать строку: x, y =у, x. Пример кода:
x, y=5, 10
x,y=y,x
print(x, y)
Результатами окажутся «10 5». Таким же образом можно менять местами и объекты, например, таблицы:
x={prop1="мир"}
y={prop2="май"}
x,y=y,x
print(x.prop1, x.prop2)
Результаты: «nil май».
Перед выполнением присваивания список переменных согласовывается по длине со списком выражений/значений. Если список справа длиннее, то его лишние элементы просто отбрасываются. Если короче, то недостающие позиции дополняются значениями nil.
Если в правой части есть функция, возвращающая несколько значений, и она стоит в конце этого списка, то эти значения встраиваются по порядку перечисления. Если f() находится в начале или середине списка, принимается только первое возвращаемое ею значение. Другими словами, на f() список присваивания должен оканчиваться, если вы хотите получить все значения. Например:
function f(a,b)
--напомним, что в рамках
--функции a и b локальны
return a+b, a+1, b-2
end
a,b,c,d,e=1,f(5,2),5
print(a,b,c,d,e)
Результатами окажутся:
1 7 5 nil nil
Если же убрать 5-ку в конце правого списка в строке a,b,c,d,e=1,f(a1,b1),5, то результатами будут: 1 7 6 0 nil, то есть мы получили из функции три значения. Это же правило действует и при формировании списков для вывода результатов, например, строка print(f(4,3), 12, 15) выведет нам: 7 12 15, в то время как строка print(12, 15, f(4,3)) выведет результат: 12 15 7 5 1. Это особенно важно понимать, потому как неправильное обращение к функции за результатами в рамках списка приводит к потере данных.
Также стоит отметить, как работает само параллельное присваивание после сравнения списков:
i = 3
i, a[i] = i+1, 20
Перед выполнением присваивания вычисляется значение всех выражений (!) (то есть сначала высчитываются все выражения справа, а потом идет присваивание), поэтому мы присвоили значение 20 переменной a[3], а не a[4], потому как i в выражении a[i] имеет то же самое значение, что и в момент вычисления выражения i+1. То есть данная строка присваивания не эквивалентна варианту:
i=i+1
a[i]=20
равно как и:
a[i]=20
i=i+1
Хотя в последнем случае получим тот же результат, но вы уже поняли разницу между параллельным и последовательным присваиванием. Данный листинг основан на документации, но чтобы было более понятно, приведем другой пример:
i = 3
b, a[i] = a[i], 20
print(b,a[3])
Результатом окажется: nil 20. То есть в момент операции параллельного присваивания a[i] не имеет определенного значения, изменения начинают быть правомочными только после данной операции.
В принципе, это важно понимать, потому как таким образом реализован вариант быстрой перемены местами типа x,y=y,x, хотя во многих языках программирования или в рамках традиционных способов для идентичной операции потребовалась бы третья переменная. А именно, x,y=y,x идентично: z=x
x=y
y=z
Конструкторы таблиц
Мы уже столкнулись с двумя вариантами включения значений и переменных в таблицы — с помощью отдельных литералов и в рамках строк, ограниченных фигурными скобками. Давайте рассмотрим пример из предыдущего материала, в котором мы создали функцию-конструктор, заменяющую оператор new из других языков программирования.
function padd(fname)
fname.food="сгущенка"
fname.phone="+375296"
--и так далее
end
f={}
padd(f)
print(f.phone)
Итак, строкой f={} мы указали, что создается некая таблица, но параметры и соответствующие им значения мы не назвали. Функция-конструктор padd является надстройкой, то есть она добавляет свойства к уже имеющейся таблице. Напомним, что этот пример мы приводили для того, чтобы показать, как могут образовываться классы.
Что касается конструктора таблиц в буквальном смысле, то применим и другой метод:
f={food="сгущенка", phone="+375296"}
Эта запись может считаться идентичной:
do
local t={}
t["food"]="сгущенка"
t["phone"]="+375296"
f=t
end
Тут стоит напомнить, что разницы между записями t.food и t["food"] нет. Поскольку переменные определяются динамически, то достаточно часто объявления таблицы в коде можно и не встретить, но это может привести к ошибке компилирования, то есть, если вы просто напишете:
t["food"]="сгущенка"
t["phone"]="+375296"
явно не указав, что f — это таблица, то компилятор выдаст ошибку неправильного обращения к свойству переменной f. В принципе, это сравнимо, если вы попытаетесь создать функцию, забыв слово function.
Отдельно стоит сказать и о специфике. Обратите внимание на предыдущий листинг. Дело в том, что создавая перечисление в фигурных скобках, вы объявляете таблицу наново. Поэтому не применим вариант, когда вы для добавления нового элемента опять же используете фигурные скобки. Например: T={food="молоко"}
T={name="Барсик"}
В результате этого кода таблица T будет включать только один элемент — ["name"]. Правильно добавление будет выглядеть так:
T={food="молоко"}
T.name="Барсик"
И последнее важное замечание — принципы параллельного присваивания в рамках строки {} не работают. То есть вы не можете записать:
T={a,b,c=1,2,3} Почему? Потому что в рамках этой строки мы присвоили элементу T[1] ссылку на переменную a, T[2]=b, T["c"] (или T.c) =1, T[3]=2, T[4]=3.
Таблицы как массивы
Таблицы могут задаваться как массивы…
arrayt={45,18,73}
print(arrayt[1])
Результат:
45
Например, первая строка этого кода присваивает значения трем первым элементам (arrayt[1], arrayt[2], arrayt[3],), причем отсчет начинается с 1, а сами порядковые номера могут явно не указываться, все перечисляется по порядку. То есть, если в списке стоят числа, строки в кавычках, true или false, переменные простых типов (bool, number, string) без явного указания индексов, присваивание идет по порядку, начиная с 1-го элемента: a={true,2,"mail"}
print(a[1],a[2],a[3])
Результат:
true 2 mail
Причем стоит отметить, что численные индексы элементов также могут задаваться произвольно:
arrayt={[-1]=90,45,18,73}
print(arrayt[-1])
Результат:
90
Это иногда может оказаться удобным при вычислениях и неявном представлении многомерных массивов (например, a[-i] содержат имена, a[i] — данные и т.п., возможно, и другие таблицы, а поиск производится в рамках одного цикла по значению i).
Пример:
a={}
k=0
for i=1,10,1 do
a[-i]=i.."-й элемент"
a[i]=k
k=k+2
end
for i=1,10,1 do print(a[-i],a[i]) end
В результате мы видим заполненный четными числами массив с положительными индексами, а в отрицательных храним комментарии типа «i-й элемент». Вообще, это можно использовать в совершенно различных вариантах применения.
Список присваиваемых значений может содержать литералы, например:
arrayt={45,d="мир",18,73}
print(arrayt[2],arrayt.d)
Результат:
18 мир
То есть, первому элементу a[1] присваивается значение 45, потом у нас идет присваивание элементу arrayt["d"]="мир", следующее число закрепится за элементом a[2].
***
Таблицы в Lua являются необычайно гибкими структурами. Их элементы могут содержать массивы, функции, другие таблицы. И все это только приоткрывает настоящую мощь языка, на практике вы сможете делать очень сложные вещи. Хотя, если честно, то многих простота синтаксиса заставляет относиться к Lua не очень серьезно.
Что касается многомерных массивов в более явном представлении, то тут необходимо поступать по обстоятельствам, главное понимать, что каждый индекс массива может быть вместилищем для другого массива (объект ссылается на объект), то есть таблица может включать абсолютно любые элементы, в том числе и подобные себе. Пример:
arrayt={}
arrayt[1]={1,2,3,4}
arrayt[2]={5,6,7,8}
arrayt[3]={9,10,11,12}
for i=1,3,1 do
for j=1,4,1 do
print(arrayt[i][j])
end end
В результате нам выведутся цифры от 1 до 12. В данном примере мы задали массив 3х4, при этом показали, что arrayt[1], arrayt[2] и arrayt[3] просто являются одномерными массивами.
Первые четыре строчки можно заменить на одну длинную:
arrayt={{1,2,3,4},{5,6,7,8},{9,10,11,12}}
что идентично.
Вообще, в варианте задания вложенных таблиц в виде общей строки с фигурными скобками нужно быть достаточно осторожными, можно легко запутаться, поэтому часто для наглядности удобнее использовать литералы.
Функции в таблицах
Функции, равно как и возвращаемые ими значения, могут ассоциироваться или являться элементами таблиц. Рассмотрим пример:
function b()
return 18,19,20
end
a={b()}
print(a[2])
В таблицу загружаются три значения, возвращаемые функцией b в качестве элементов a[1], a[2] и a[3]. Правила точно такие же, как и в варианте с параллельным присваиванием — если функция стоит в конце списка, то берутся все ее значения. Причем, если бы вместо a={b()} написали бы b без скобок (a={b}), то элементу a[1] присвоилась бы ссылка на саму функцию, и вместо результата мы бы видели ее данные. В этом случае получение результатов получилось бы при вызове: print(a[1]()).
Промежуточное завершение
Уф-ф. Просматривая старые книги (что выкинуть, а что сохранить) я натолкнулся на учебник по Delphi, одной из самых ярких иллюстраций которого было рисование знака: «Скажи нет C++!». И действительно можно припомнить противостояние приверженцев объектного паскаля и С++. Сейчас все это выглядит наивным, но какие битвы происходили в то время! Сегодня мы можем наблюдать еще большее количество противостояний. Лично для меня Lua был в конце первого десятка среди практически изучаемых, а после и используемых мною языков, и что хочется отметить… Большинство языков является очень схожими, они немного отличаются по синтаксису и сферам применения, но базовая структура идентична. Это не относится к Lua, мощь и гибкость которого проявляется в уникальной концепции.
Кристофер christopher@tut.by
Сегодня мы продолжаем изучать язык в автономном режиме, для чего пользуемся редактором/компилятором SciTE (бесплатно скачивается с сайт , 1,2 Мб).
Параллельное присваивание
Многим читателям предыдущих частей материала стало известно, что в Lua тип имеют только значения, а переменные типизируются динамически. Мы уже говорили о специфике присваивания, когда было показано разграничение между присваиванием значений для переменных и присваиванием одним объектам ссылок на другие.
То есть выражение A=B присвоит A ссылку на значение B, если последняя не является функцией, потоком, пользовательскими данными либо таблицей, или, другими словами, объектом. В этом случае A будет ссылаться на объект и станет идентичной B.
Теперь рассмотрим операцию еще более полно. Lua поддерживает параллельное присваивание, которое также называют множественным, но определение «параллельное» более емко описывает суть происходящего. Например, чтобы поменять значения x и у местами, достаточно набрать строку: x, y =у, x. Пример кода:
x, y=5, 10
x,y=y,x
print(x, y)
Результатами окажутся «10 5». Таким же образом можно менять местами и объекты, например, таблицы:
x={prop1="мир"}
y={prop2="май"}
x,y=y,x
print(x.prop1, x.prop2)
Результаты: «nil май».
Перед выполнением присваивания список переменных согласовывается по длине со списком выражений/значений. Если список справа длиннее, то его лишние элементы просто отбрасываются. Если короче, то недостающие позиции дополняются значениями nil.
Если в правой части есть функция, возвращающая несколько значений, и она стоит в конце этого списка, то эти значения встраиваются по порядку перечисления. Если f() находится в начале или середине списка, принимается только первое возвращаемое ею значение. Другими словами, на f() список присваивания должен оканчиваться, если вы хотите получить все значения. Например:
function f(a,b)
--напомним, что в рамках
--функции a и b локальны
return a+b, a+1, b-2
end
a,b,c,d,e=1,f(5,2),5
print(a,b,c,d,e)
Результатами окажутся:
1 7 5 nil nil
Если же убрать 5-ку в конце правого списка в строке a,b,c,d,e=1,f(a1,b1),5, то результатами будут: 1 7 6 0 nil, то есть мы получили из функции три значения. Это же правило действует и при формировании списков для вывода результатов, например, строка print(f(4,3), 12, 15) выведет нам: 7 12 15, в то время как строка print(12, 15, f(4,3)) выведет результат: 12 15 7 5 1. Это особенно важно понимать, потому как неправильное обращение к функции за результатами в рамках списка приводит к потере данных.
Также стоит отметить, как работает само параллельное присваивание после сравнения списков:
i = 3
i, a[i] = i+1, 20
Перед выполнением присваивания вычисляется значение всех выражений (!) (то есть сначала высчитываются все выражения справа, а потом идет присваивание), поэтому мы присвоили значение 20 переменной a[3], а не a[4], потому как i в выражении a[i] имеет то же самое значение, что и в момент вычисления выражения i+1. То есть данная строка присваивания не эквивалентна варианту:
i=i+1
a[i]=20
равно как и:
a[i]=20
i=i+1
Хотя в последнем случае получим тот же результат, но вы уже поняли разницу между параллельным и последовательным присваиванием. Данный листинг основан на документации, но чтобы было более понятно, приведем другой пример:
i = 3
b, a[i] = a[i], 20
print(b,a[3])
Результатом окажется: nil 20. То есть в момент операции параллельного присваивания a[i] не имеет определенного значения, изменения начинают быть правомочными только после данной операции.
В принципе, это важно понимать, потому как таким образом реализован вариант быстрой перемены местами типа x,y=y,x, хотя во многих языках программирования или в рамках традиционных способов для идентичной операции потребовалась бы третья переменная. А именно, x,y=y,x идентично: z=x
x=y
y=z
Конструкторы таблиц
Мы уже столкнулись с двумя вариантами включения значений и переменных в таблицы — с помощью отдельных литералов и в рамках строк, ограниченных фигурными скобками. Давайте рассмотрим пример из предыдущего материала, в котором мы создали функцию-конструктор, заменяющую оператор new из других языков программирования.
function padd(fname)
fname.food="сгущенка"
fname.phone="+375296"
--и так далее
end
f={}
padd(f)
print(f.phone)
Итак, строкой f={} мы указали, что создается некая таблица, но параметры и соответствующие им значения мы не назвали. Функция-конструктор padd является надстройкой, то есть она добавляет свойства к уже имеющейся таблице. Напомним, что этот пример мы приводили для того, чтобы показать, как могут образовываться классы.
Что касается конструктора таблиц в буквальном смысле, то применим и другой метод:
f={food="сгущенка", phone="+375296"}
Эта запись может считаться идентичной:
do
local t={}
t["food"]="сгущенка"
t["phone"]="+375296"
f=t
end
Тут стоит напомнить, что разницы между записями t.food и t["food"] нет. Поскольку переменные определяются динамически, то достаточно часто объявления таблицы в коде можно и не встретить, но это может привести к ошибке компилирования, то есть, если вы просто напишете:
t["food"]="сгущенка"
t["phone"]="+375296"
явно не указав, что f — это таблица, то компилятор выдаст ошибку неправильного обращения к свойству переменной f. В принципе, это сравнимо, если вы попытаетесь создать функцию, забыв слово function.
Отдельно стоит сказать и о специфике. Обратите внимание на предыдущий листинг. Дело в том, что создавая перечисление в фигурных скобках, вы объявляете таблицу наново. Поэтому не применим вариант, когда вы для добавления нового элемента опять же используете фигурные скобки. Например: T={food="молоко"}
T={name="Барсик"}
В результате этого кода таблица T будет включать только один элемент — ["name"]. Правильно добавление будет выглядеть так:
T={food="молоко"}
T.name="Барсик"
И последнее важное замечание — принципы параллельного присваивания в рамках строки {} не работают. То есть вы не можете записать:
T={a,b,c=1,2,3} Почему? Потому что в рамках этой строки мы присвоили элементу T[1] ссылку на переменную a, T[2]=b, T["c"] (или T.c) =1, T[3]=2, T[4]=3.
Таблицы как массивы
Таблицы могут задаваться как массивы…
arrayt={45,18,73}
print(arrayt[1])
Результат:
45
Например, первая строка этого кода присваивает значения трем первым элементам (arrayt[1], arrayt[2], arrayt[3],), причем отсчет начинается с 1, а сами порядковые номера могут явно не указываться, все перечисляется по порядку. То есть, если в списке стоят числа, строки в кавычках, true или false, переменные простых типов (bool, number, string) без явного указания индексов, присваивание идет по порядку, начиная с 1-го элемента: a={true,2,"mail"}
print(a[1],a[2],a[3])
Результат:
true 2 mail
Причем стоит отметить, что численные индексы элементов также могут задаваться произвольно:
arrayt={[-1]=90,45,18,73}
print(arrayt[-1])
Результат:
90
Это иногда может оказаться удобным при вычислениях и неявном представлении многомерных массивов (например, a[-i] содержат имена, a[i] — данные и т.п., возможно, и другие таблицы, а поиск производится в рамках одного цикла по значению i).
Пример:
a={}
k=0
for i=1,10,1 do
a[-i]=i.."-й элемент"
a[i]=k
k=k+2
end
for i=1,10,1 do print(a[-i],a[i]) end
В результате мы видим заполненный четными числами массив с положительными индексами, а в отрицательных храним комментарии типа «i-й элемент». Вообще, это можно использовать в совершенно различных вариантах применения.
Список присваиваемых значений может содержать литералы, например:
arrayt={45,d="мир",18,73}
print(arrayt[2],arrayt.d)
Результат:
18 мир
То есть, первому элементу a[1] присваивается значение 45, потом у нас идет присваивание элементу arrayt["d"]="мир", следующее число закрепится за элементом a[2].
***
Таблицы в Lua являются необычайно гибкими структурами. Их элементы могут содержать массивы, функции, другие таблицы. И все это только приоткрывает настоящую мощь языка, на практике вы сможете делать очень сложные вещи. Хотя, если честно, то многих простота синтаксиса заставляет относиться к Lua не очень серьезно.
Что касается многомерных массивов в более явном представлении, то тут необходимо поступать по обстоятельствам, главное понимать, что каждый индекс массива может быть вместилищем для другого массива (объект ссылается на объект), то есть таблица может включать абсолютно любые элементы, в том числе и подобные себе. Пример:
arrayt={}
arrayt[1]={1,2,3,4}
arrayt[2]={5,6,7,8}
arrayt[3]={9,10,11,12}
for i=1,3,1 do
for j=1,4,1 do
print(arrayt[i][j])
end end
В результате нам выведутся цифры от 1 до 12. В данном примере мы задали массив 3х4, при этом показали, что arrayt[1], arrayt[2] и arrayt[3] просто являются одномерными массивами.
Первые четыре строчки можно заменить на одну длинную:
arrayt={{1,2,3,4},{5,6,7,8},{9,10,11,12}}
что идентично.
Вообще, в варианте задания вложенных таблиц в виде общей строки с фигурными скобками нужно быть достаточно осторожными, можно легко запутаться, поэтому часто для наглядности удобнее использовать литералы.
Функции в таблицах
Функции, равно как и возвращаемые ими значения, могут ассоциироваться или являться элементами таблиц. Рассмотрим пример:
function b()
return 18,19,20
end
a={b()}
print(a[2])
В таблицу загружаются три значения, возвращаемые функцией b в качестве элементов a[1], a[2] и a[3]. Правила точно такие же, как и в варианте с параллельным присваиванием — если функция стоит в конце списка, то берутся все ее значения. Причем, если бы вместо a={b()} написали бы b без скобок (a={b}), то элементу a[1] присвоилась бы ссылка на саму функцию, и вместо результата мы бы видели ее данные. В этом случае получение результатов получилось бы при вызове: print(a[1]()).
Промежуточное завершение
Уф-ф. Просматривая старые книги (что выкинуть, а что сохранить) я натолкнулся на учебник по Delphi, одной из самых ярких иллюстраций которого было рисование знака: «Скажи нет C++!». И действительно можно припомнить противостояние приверженцев объектного паскаля и С++. Сейчас все это выглядит наивным, но какие битвы происходили в то время! Сегодня мы можем наблюдать еще большее количество противостояний. Лично для меня Lua был в конце первого десятка среди практически изучаемых, а после и используемых мною языков, и что хочется отметить… Большинство языков является очень схожими, они немного отличаются по синтаксису и сферам применения, но базовая структура идентична. Это не относится к Lua, мощь и гибкость которого проявляется в уникальной концепции.
Кристофер christopher@tut.by
Компьютерная газета. Статья была опубликована в номере 20 за 2009 год в рубрике программирование