ITCS - Lua для игр и не только. Часть 4 - РАЗРАБОТКА КОМПЬЮТЕРНЫХ ИГР
Сегодня: Четверг, 08.12.2016, 01:11 (МСК)| Здравствуйте, Гость| Мой профиль | Регистрация | Вход | RSS

Работаем с VirtualDub

Спецэффекты в Cinema4D

Роботы и экзоскелеты

Новинки в области цифровых камер

Программы — виртуальные гитаристы
Главная » РАЗРАБОТКА КОМПЬЮТЕРНЫХ ИГР

Lua для игр и не только. Часть 4

01.08.2010
На вопрос: «что такое хлеб», можно ответить по-разному. Например, «хлеб является продуктом питания, рецептура приготовления которого включает…». Можно и более пафосно: «хлеб — всему голова». В первом случае нам будет все понятно, второе объяснение может поставить в тупик. Именно такую же параллель можно провести с объяснением базовых принципов ООП (объектно-ориентированного программирования) и понятия классов. 

Говоря в первой части материала, что Lua является типичным процедурным языком, мы как бы обозначили его рамки. Это не совсем правильно, поскольку внутренняя структура Lua стоит как бы «над» любыми принадлежностями. ООП как таковое многое наследует из структурного программирования, и отделять их нельзя. Те же классы со всеми сопутствующими технологиями можно реализовать и в Lua.  Тем более, что там есть такой тип данных как таблицы, элементы которых… могут включать в себя другие таблицы. 

При этом очень много пересечений у Lua имеется с объектно-ориентированным языком C#. 
Также советуем присмотреться очень внимательно к листингам из этой части материала и разобраться в приведенных кодах... Язык очень прост и уникален, если вы будете ясно понимать, как он работает.




О присваивании и не только


Для более легкого дальнейшего чтения, перечислим в упрощенном виде некоторые базовые постулаты.  

Таблицы объявляются следующим образом:

t={}

Если в фигурных скобках ничего нет, то мы создаем пустую таблицу. Также в фигурных скобках могут перечисляться элементы, которые могут ассоциироваться со значениями, переменными, функциями, другими таблицами, пользовательскими данными, сопрограммами (потоками). Для простоты дальнейшего понимания, функции, таблицы, пользовательские данные и сопрограммы мы будем называть объектами. Их отличие от переменных базовых типов (bool, number, string) состоит в том, что они никогда не содержат значений.

Функции в классическом варианте написания объявляются так:

function f(аргументы)
--блок кода
end

В Lua существует два различных типа присваивания: ссылок на значения и ссылок на объекты. Например, мы объявили таблицу и функцию, а теперь вводим переменную N.
  • N становится другим именем таблицы t: 
N=t
  • N получает ссылку на значение из t, если оно определено заранее (иначе nil):
N=t[1]
  • N становится другим именем функции f:
N=f
  • N получает значение, возвращаемое f:
N=f()

В первом и третьем случаях конкретных значений у N нет, поскольку объекты не имеют значений. 


Что интересно


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

A={23,65}
function B()
return 18
end
A,B=B,A
print(B[1])

Результат: 23. На практике он вам не очень пригодится, но окончательно все прояснит. Итак, изначально A у нас является таблицей, а B — функцией. Строкой A,B=B,A мы поменяли их местами, то есть, A уже — функция, а B — таблица. О параллельном присваивании мы подробно расскажем в следующей части, а пока вам достаточно и этих знаний. 


Что более интересно


Любые объекты и переменные могут изменяться динамически. Но при этом у многих читателей может появиться легкое недопонимание в вопросе присвоения ссылок на объекты, и почему, например, перечисляя варианты присваивания, мы написали «становится другим именем» функции или таблицы, а не написали «присваивается ссылка на объект». На самом деле это сделано для упрощения описания происходящих процессов.
Пример: 

function f(a,b)
return a+b 
end
c=f
function f(a,b)
return a-b 
end
d=f
print(c(1,2),d(1,2))

Результаты: 3 и -1.

Как вы видите, мы два раза объявили функцию f, в первом случае она вычисляет сумму двух аргументов, а во втором — их разность. В промежутке между двумя объявлениями мы присвоили первой f другое имя, а именно, c. Второй f соответственно было присвоено d. Что нам дают результаты? Обращение c(1,2) фактически направлено на первую f, то есть сумму, а d(1,2) на вторую.  


Lua и ООП


По множеству сложившихся стереотипов Lua считается процедурным языком, то есть он не является в общепринятом смысле объектно-ориентированным. Это не так. В данном случае мы воспользуемся тем представлением, что определение ООП достаточно условно, его нельзя взять и вычленить из структурного программирования, поскольку ООП многое из него наследует (первоначально название С++ было «C с классами»). Классы можно представлять неявно на уровне структурированных определенным образом данных и функций. Например, ранее мы, говоря о типе данных table, указали, что в этих таблицах могут располагаться функции, то есть, таблицы имеют методы. И вообще объединение объектов по свойствам может производиться по-разному.

Пример кода на Lua:

--объявляем функцию
function f(a,b) 
return a+b
end
--создаем таблицу
t1 = {[1]="привет", world="мир",n1=f}
s="world"
--выводим результат
print(t1[1], t1.world, t1[s], t1.n1(5,4))

Результат:

привет мир мир  9

Объяснение. Итак, мы создали функцию f(a, b), которая вычисляет сумму двух чисел, после этого мы сформировали таблицу t1, в качестве элементов которой поместили [1]="привет", world="мир" (аналогично записи ["world"]="мир") и n1=f. То есть, первый элемент таблицы можно воспринимать как элемент массива, второй или как элемент массива или как свойство, третий — как метод. Подробнее о конструкторе таблиц мы расскажем чуть позже. Обратите внимание на то, что в рамках Lua стираются некоторые грани, например, написание t1.world эквивалентно t1["world"], а чтобы показать то, что в данной ситуации элемент отобразится как свойство, вместо "мир" напишите true, и это поле (элемент таблицы) автоматически станет булевым. 


Пример с кошками…


Lua является объектно-ориентированным языком, только не в том разжеванном смысле, как это есть у других. Инкапсуляция работает в стиле структурного программирования — на уровне функций, объявления локальных переменных. Классы в стандартных языках призваны в основном для реализации решения двух проблем: минимизации кода и оптимизации/автоматизации наследования. 

Но Lua представляет собой более гибкую структуру. В основном благодаря такому типу данных как table, который подразумевает и гетерогенные варианты заполнения (то есть, эти структуры могут заполняться данными любых типов кроме nil), а также реализованным возможностям присваивания. В коде, представленном выше, вы убедились, что структуры, подобные классам, можно сделать, не используя конструкторы классов, при помощи литералов и создания специальных таблиц. Литерал — это буквальная константа, имя которой одновременно хранит и ее значение.

Рассмотрим следующий фрагмент кода: 

cats={main="хищники, млекопитающие", 
food="рыба, мясо"}
h_cats={}
h_cats.main=cats.main
h_cats.food=cats.food
--добавляем новое свойство
h_cats.address="квартира"
print(h_cats.food, h_cats.address)

С одной точки зрения, cats — это таблица, с другой — на нее можно смотреть как на класс, в рамках которого мы обозначили свойства: «ключевые» (main) и «пища» (food). Допустим, мы решили создать подкласс домашних кошек h_cats, который наследует все свойства класса cats, но при этом нам необходимо добавить ряд дополнительных. Используем литералы (также можем их перечислить в фигурных скобках). 

Ошибки, которые можно допустить, более иллюстративно отображены в следующем коде:

cats={main="хищники, млекопитающие", 
food="рыба, мясо"}
h_cats=cats
h_cats.address="квартира"
print(cats.address)

Код работает, но как? Здесь мы сделали присваивание h_cats всей таблицы cats (причем речь идет не о копировании, а об управлении ссылками), а по существу мы говорим об одном и том же объекте, то есть после строки h_cats.address="квартира" такое же свойство добавляется и в cats! Получается, то мы видоизменяем один и тот же объект, но под разными именами. 

То есть в предыдущем листинге мы фактически работали с присваиванием значений переменных, а в этом — присвоили объекту второе имя. С этим моментом в Lua нужно быть осторожным. 

Как многим уже стало понятно работа с классами близка к массивам и их представлению, но это не все. Мы говорили о приравнивании объектов, но при этом таблицы по своему определению могут включать объекты любых типов, в том числе и другие таблицы. Пример кода:

cats={main="хищники, млекопитающие", 
food="рыба, мясо"}
h_cats={cats=cats, address="квартира"}
print(h_cats.cats.main,cats.food)

В этом листинге мы закрепили за свойством (полем) cats таблицы h_cats ссылку на таблицу cats. В результате, вызывая h_cats.cats.main мы по существу обращаемся к cats.main. Подробнее о создании таблиц мы побеседуем позже, пока мы работаем с классами. 

Теперь давайте создадим аналог оператора new, который широко применяется в других языках, здесь мы будем рассматривать его как функцию-конструктор и назовем ее, например, padd.

function padd(fname)
fname.food="сгущенка"
fname.phone="+375296"
--и так далее
end

Все готово. Эта функция-конструктор  добавляет свойства-поля food и phone в любую из таблиц. Вызывается просто, например:

h_cats={}
padd(h_cats)

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

Как видите, таблицы, в рамках того представления, что нам дает Lua, являются очень мощным инструментом.




Многоликие структуры


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

function padd(fname)
fname.new_food="сухой корм"
if fname.ves<4 then 
function kolvo() return fname.ves*20 
end 
else 
function kolvo() return fname.ves*20+5 
end end
fname.kolvo=kolvo
end
cat={ves=3}
padd(cat)
print(cat.new_food) 
print(cat.kolvo(), "г в день")

Итак, у нас есть объект (можем воспринимать его как класс) cat, а по существу, кот, в рацион питания которого мы решили добавить сухой корм. Но при этом нам нужно рассчитать количество на день. Если рассмотреть таблицы на пачках с «китикетами», то очевидно, что эта величина рассчитывается как «вес кошки*20 г» для кошек до 4 кг, и «вес кошки*20 г + 5 г» для 4 кг и более.

По аналогии с предыдущим подразделом мы создали функцию добавления свойств, а в данном случае и методов. Мы добавили в рацион питания сухой корм (свойство cat.new_food), а после создали каскад if then else, в котором определили все зависимости и включили формулы. Причем сделали это специальным способом, объявив для каждого из двух случаев (вес < 4 кг, >= 4 кг) функцию kolvo(), которая вычисляет в зависимости от ситуации необходимое значение и возвращает его. 

После этого, строкой fname.kolvo=kolvo мы присваиваем методу таблицы cats ссылку на одну из выбранных функций kolvo(). Если мы обратимся к функции padd от другого объекта, например, cat2, где указан другой вес, то у него появится и свой метод cat2.kolvo(), который может подразумевать другие варианты вычмслений. 

Конечно, в рамках рассматриваемого масштаба, код абсолютно не оптимален. Но как быть в ситуациях, когда имеется несколько алгоритмов для обработки какого-либо события, один из них вызывает ошибку и т.п., либо у нас есть множество случаев, в каждом из которых имеется своя специфика. 

Практическое программирование отличается от теоретического. Например, многие авторы книг и курсов пытаются показать свою крутость на уровне емких expression-oriented (ориентированных на емкость и выразительность) выражений, но иногда это фактически то же, что и учить иностранные языки по сленгу. 

В качестве самостоятельного обучения, посмотрите, что выведетcя в результатах добавления к предыдущему коду этого:

cats={}
for i=1,10,1 do
cats[i]={ves=i}
padd(cats[i])
print("для веса", i, "кг")
print(cats[i].kolvo())
end

Объясните, что, как и почему происходит. Подробнее о специфике задания элементов таблиц, представления их в виде массивов, деревьев и т.п. мы поговорим в следующей части материала.   


О чем это мы?


Запутались? Возможно, что немного. А теперь представим реальную ситуацию. В рамках RTS у нас выставлено 12 одинаковых пушек. Они относятся к одному классу, но каждый из экземпляров ведет себя отдельно в зависимости от ситуации, в которой он оказался. То есть, приближается соперник, начинается бой. Так вот, пушки как класс, могут быть представлены и на уровне Lua со всеми методами, которые для него предусмотрены. Если пушка оказалась в какой-либо ситуации, для этого экземпляра вызывается соответствующий метод. Методы могут меняться и даже добавляться в зависимости от апгрейда технологий. Например, в предыдущем подразделе, если наш кот вырос, то ему нужно по-другому рассчитывать питание. Вызов метода происходит только по мере необходимости, а сами базовые настройки можно делать в рамках Lua-файлов, не касаясь компилируемой части, она — только исполнитель. 

Таблицы в Lua могут подразумевать не только классы и массивы из стандартных языков программирования... Возможности языка неисчерпаемы, хотя, как было указано в предыдущей части материала, очень часто его используют на 5-10% от заложенного.  


Промежуточное завершение

  
На самом деле варианты структурного и объектно-ориентированного программирования очень близки по сути. Реализовать классы, их поддержку, при проектировании языка не сложно, то есть, они бы могли явно присутствовать и в Lua, но большой необходимости в этом нет. Не зачем. Разработчики подошли ко всем вопросам с более правильной стороны, сделав акцент на объектах как таковых, их представлении в виде уникальных структур — таблиц. Концепция языка проявляется именно в структуре программирования, и хочется отметить, что Lua — умница в этом плане. 

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





Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Ассоциация боевых роботов
Рекомендуем...
Новости

Разделы

Опросы

Какой язык программирования вы считаете наиболее актуальным сегодня?
Всего ответов: 308

Друзья

3D-кино






Найти на сайте:








Об авторе       Контакты      Вопрос-ответ        Хостинг от uCoz