Разработка на LOVE

Автор: admin от 6-01-2018, 12:50, посмотрело: 103

Разработка на LOVE


Цель поста — в максимально простой форме описать основные этапы разработки с помощью фреймворка LOVE, на примере классической игры atari-автоматов Asteroids.

достаточно прост для освоения, проще чем javascript и Python, но с него достаточно просто переходить как на вышеуказанные, так и на низкоуровневые (С/С++). Так же он достаточно популярен в разработке видеоигр, как часть чего-то более крупного (cryEngine, GMod, OpenComputers в Minecraft, etc), и если в какой-то игре присутствует моддинг — с очень высокой вероятностью, он использует Lua.

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



Плюс LOVE по умолчанию поставляется с виртуальной машиной LuaJIT, которая многократно ускоряет исполнение (критично для игр), и позволяет использовать FFI: подключение библиотек написанных на C, инициализация и использование C-структур, которые, с метатаблицами, можно превратить в lua-объекты, и которые экономят время создания/память и т.п.



Чуть ближе к делу





Для дальнейшей работы, нам потребуется выполнить следующий набор действий:


  • Загружаем последнюю версию LOVE с официального сайта;

  • Настраиваем запуск текущего проекта в LOVE, стандартный метод тестового запуска — открыть директорию с файлом main.lua в исполняемом файле love. Так же, можно паковать содержимое директории с файлом main.lua в zip-архив, и или перетаскивать на исполняемый файл, или переименовать .zip в .love и настроить ассоциации файлов. Я считаю что проще настроить шорткат для текущего редактора, у notepad++ это, например:
    <Command name=...>path/to/love.exe $(CURRENT_DIRECTORY)</Command>
    Примеры для sublime можно найти в соседней статье;

  • Создаём пустую директорию и добавляем в неё файл с именем main.lua. Желательно чтобы в пути не было пробелов и кириллицы, а то некоторые напихают пробелов, а потом жалуются, но для обхода можно чуть изменить шорткат или метод запуска;

  • Открываем в любимом редакторе наш чистый и незапятнанный файл main.lua, и LOVE-Wiki в любимом браузере.



  • Ещё ближе, но не совсем



    Первое что стоит узнать, это то, что фреймворк функционирует через набор колбеков, которые мы пишем в глобальную таблицу love, которая уже объявлена:



    function love.load(arg)
    	-- Код в функции love.load будет вызван один раз, 
    	-- как только проект будет запущен.
    end
    
    function love.update(dt)
    	-- Код функций update и draw будут запускаться каждый кадр, 
    	-- чередуясь, в бесконечном цикле:
    	-- "посчиталинарисовалипосчиталинарисовали"
    	-- пока не будет вызван выход из приложения.
    end
    
    function love.draw()
    	-- Все функции взаимодействия с модулями фреймворка - 
    	-- аналогично прячутся внутри таблицы love.
    	love.graphics.print('Hello dear Love user!', 100, 100)
    end
    


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



    Уже что-то похожее на дело



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



    Первым делом, так как мы делаем Asteroids, мы хотим получить кораблик, которым крайне желательно ещё и рулить.



    Далее, мы хотим чем-то стрелять и цели, в которые можно попасть.

    Аналогично, хотелось бы чтобы где-то вёлся подсчёт очков и манипулирование всем подряд.



    Далее будет очень много кода, но надеюсь, комментарии будут достаточно содержательными.



    -- Заранее инициализируем ссылки на имена классов, которые понадобятся,
    -- ибо вышестоящие классы будут использовать часть нижестоящих.
    local Ship, Bullet, Asteroid, Field
    
    Ship = {}
    -- У всех таблиц, метатаблицей которых является ship,
    -- дополнительные методы будут искаться в таблице ship.
    Ship.__index = Ship 
    
    -- Задаём общее поле для всех членов класса, для взаимодействия разных объектов
    Ship.type = 'ship'
    
    -- Двоеточие - хитрый способ передать таблицу первым скрытым аргументом 'self'.
    function Ship:new(field, x, y)
    	-- Сюда, в качестве self, придёт таблица Ship.
    
    	-- Переопределяем self на новый объект, self как таблица Ship больше не понадобится.
    	self = setmetatable({}, self)
    
    	-- Мы будем передавать ссылку на игровой менеджер, чтобы командовать им.
    	self.field = field
    
    	-- Координаты:
    	self.x = x or 100 -- 100 - дефолт
    	self.y = y or 100
    
    	-- Текущий угол поворота:
    	self.angle = 0
    	
    	-- И заполняем всё остальное:
    	
    	-- Вектор движения:
    	self.vx = 0
    	self.vy = 0
    
    	
    	-- Ускорение, пикс/сек:
    	self.acceleration  = 200
    	
    	-- Скорость поворота:
    	self.rotation      = math.pi
    	
    	-- Всякие таймеры стрельбы:
    	self.shoot_timer = 0
    	self.shoot_delay = 0.3
    	
    	-- Радиус, для коллизии:
    	self.radius   = 30
    		
    	-- Список вершин полигона, для отрисовки нашего кораблика:
    	self.vertexes = {0, -30, 30, 30, 0, 20, -30, 30}
    	--[[ 
    		Получится что-то такое, только чуть ровнее:
    	  /
    	 /  
    	/_/_  
    	]]
    	
    	-- Возвращаем свежеиспечёный объект.
    	return self 
    end
    
    function Ship:update(dt)
    	-- Декрементов нема, и инкрементов тоже, но это не очень страшно, правда?
    	-- dt - дельта времени, промежуток между предыдущим и текущим кадром.
    	self.shoot_timer = self.shoot_timer - dt
    	
    	
    	-- Управление:
    	
    	-- "Если зажата кнопка и таймер истёк" - спавним новую пулю.
    	if love.keyboard.isDown('x') and self.shoot_timer < 0 then
    		self.field:spawn(Bullet:new(self.field, self.x, self.y, self.angle))
    
    		-- И сбрасываем таймер, потому что мы не хотим непрерывных струй из пуль, 
    		-- хоть это и забавно.
    		self.shoot_timer = self.shoot_delay
    	end
    	
    	if love.keyboard.isDown('left') then 
    
    		-- За секунду, сумма всех dt - почти ровно 1,
    		-- соответственно, за секунду, кораблик повернётся на угол Pi,
    		-- полный оборот - две секунды, все углы в радианах.
    		self.angle = self.angle - self.rotation * dt
    	end
    
    	if love.keyboard.isDown('right') then 
    		self.angle = self.angle + self.rotation * dt
    	end
    
    	if love.keyboard.isDown('up') then 
    
    		-- Вычисляем вектор ускорения, который мы приобрели за текущий кадр.
    		local vx_dt = math.cos(self.angle) * self.acceleration * dt
    		local vy_dt = math.sin(self.angle) * self.acceleration * dt
    
    		-- Прибавляем к собственному вектору движения полученный.
    		self.vx = self.vx + vx_dt
    		self.vy = self.vy + vy_dt
    	end
    
    	-- Прибавляем к текущим координатам вектор движения за текущий кадр.
    	self.x = self.x + self.vx * dt
    	self.y = self.y + self.vy * dt
    	
    	-- Пусть это и космос, но торможение в пространстве никто не отменял: 
    	-- мы тормозим в классике, и тут должны.
    	-- Торможение получается прогрессивным -
    	-- чем быстрее двигаемся, тем быстрее тормозим.
    	self.vx = self.vx - self.vx * dt
    	self.vy = self.vy - self.vy * dt	
    	
    	--Тут уже проверки координат на превышение полномочий:
    	--как только центр кораблика вылез за пределы экрана,
    	--мы его тут же перебрасываем на другую сторону.
    	local screen_width, screen_height = love.graphics.getDimensions()
    	
    	if self.x < 0 then
    		self.x = self.x + screen_width
    	end
    	if self.y < 0 then
    		self.y = self.y + screen_height
    	end
    	if self.x > screen_width then
    		self.x = self.x - screen_width  
    	end
    	if self.y > screen_height then
    		self.y = self.y - screen_height
    	end
    
    end
    
    function Ship:draw()
    	-- Говорим графической системе, 
    	-- что всё следующее мы будем рисовать белым цветом.
    	love.graphics.setColor(255,255,255)
    	
    	-- Вот сейчас будет довольно сложно, 
    	-- грубо говоря, это трансформации над графической системой.
    		
    	-- Запоминаем текущее состояние графической системы.
    	love.graphics.push()
    	
    	-- Переносим центр графической системы на координаты кораблика.
    	love.graphics.translate (self.x, self.y)
    	
    	-- Поворачиваем графическую систему на нужный угол.
    	-- Прибавляем Pi/2 потому, что мы задавали вершины полигона 
    	-- острым концом вверх а не вправо, соответственно, при отрисовке
    	-- нам нужно чуть довернуть угол чтобы скомпенсировать.
    	love.graphics.rotate (self.angle + math.pi/2)
    	
    	-- Рендерим вершины полигона, line - контур, fill - заполненный полигон.
    	love.graphics.polygon('line', self.vertexes)
    	
    	-- И, наконец, возвращаем топологию в исходное состояние 
    	-- (перед love.graphics.push()).
    	love.graphics.pop()
    	
    	-- Это было слегка сложно,
    	-- рисовать кружочки/прямоугольнички значительно проще:
    	-- там можно прямо указать координаты, и сразу получить результат
    	-- и так мы будем рисовать астероиды/пули.
    
    	-- Но на такой методике можно без проблем сделать игровую камеру.
    	-- За полной справкой лучше залезть в вики, 
    end
    
    -- "Пушка! Они заряжают пушку! Зачем? А, они будут стрелять!"
    -- Мы тоже хотим стрелять. 
    -- Для стрельбы, нам необходимы пули, которыми мы будем стрелять.
    -- Всё почти то же самое что у кораблика:
    
    Bullet = {}
    Bullet.__index = Bullet
    
    -- Это - общие параметры для всех членов класса,
    -- пули летят с одинаковой скоростью и имеют один тип,
    -- поэтому можем выделить это в класс:
    Bullet.type = 'bullet'
    Bullet.speed = 300
    
    function Bullet:new(field, x, y, angle)
      self = setmetatable({}, self)
    	
    	-- Аналогично задаём параметры
    	self.field = field
    	self.x      = x
    	self.y      = y
    	self.radius = 3
    
    	-- время жизни
    	self.life_time = 5
    	
    	-- Нам надо бы вычислить 
    	-- вектор движения из угла поворота и скорости:
    	self.vx = math.cos(angle) * self.speed
    	self.vy = math.sin(angle) * self.speed
    	-- Так как у объекта self нет поля speed, 
    	-- поиск параметра продолжится в таблице под полем 
    	-- __index у метатаблицы
    	
    	return self
    end
    
    function Bullet:update(dt)
    	-- Управляем временем жизни:
    	self.life_time = self.life_time - dt
    	
    	if self.life_time < 0 then
    		-- У нас пока нет такого метода,
    		-- но это тоже неплохо.
    		self.field:destroy(self)
    		return
    	end
    	
    	-- Те же векторы
    	self.x = self.x + self.vx * dt
    	self.y = self.y + self.vy * dt
    
    	-- Пулям тоже не стоит улетать за границы экрана
    	local screen_width, screen_height = love.graphics.getDimensions()
    	
    	if self.x < 0 then
    		self.x = self.x + screen_width
    	end
    	if self.y < 0 then
    		self.y = self.y + screen_height
    	end
    	if self.x > screen_width then
    		self.x = self.x - screen_width
    	end
    	if self.y > screen_height then
    		self.y = self.y - screen_height
    	end
    end
    
    function Bullet:draw()
    	love.graphics.setColor(255,255,255)
    	
    	-- Обещанная простая функция отрисовки.
    	-- Полигоны, увы, так просто вращать не получится
    	love.graphics.circle('fill', self.x, self.y, self.radius)
    end
    
    -- В кого стрелять? В мимопролетающие астероиды, конечно.
    Asteroid = {}
    Asteroid.__index = Asteroid
    Asteroid.type = 'asteroid'
    
    function Asteroid:new(field, x, y, size)
      self = setmetatable({}, self)
    	
    	-- Аналогично предыдущим классам.
    	-- Можно было было бы провернуть наследование, 
    	-- но это может быть сложно для восприятия начинающих.
    	self.field  = field
    	self.x      = x
    	self.y      = y
    
    	-- Размерность астероида будет варьироваться 1-N.
    	self.size   = size or 3
    		
    	-- Векторы движения будут - случайными и неизменными.
    	self.vx     = math.random(-20, 20)
    	self.vy     = math.random(-20, 20)
    
    	self.radius = size * 15 -- модификатор размера
    	
    	-- Тут вводится параметр здоровья,
    	-- ибо астероид может принять несколько ударов
    	-- прежде чем сломаться. Чуть рандомизируем для интереса.
    	-- Чем жирнее астероид, тем потенциально жирнее он по ХП:
    	self.hp = size + math.random(2)
    	
    	-- Пусть они будут ещё и разноцветными.
    	self.color = {math.random(255), math.random(255), math.random(255)}
    	return self
    end
    
    -- Тут сложный метод, поэтому выделяем его отдельно
    function Asteroid:applyDamage(dmg)
    
    	-- если урон не указан - выставляем единицу
    	dmg = dmg or 1
    	self.hp = self.hp - 1
    	if self.hp < 0 then
    		-- Подсчёт очков - самое главное
    		self.field.score = self.field.score + self.size * 100
    		self.field:destroy(self)
    		if self.size > 1 then
    			-- Количество обломков слегка рандомизируем.
    			for i = 1, 1 + math.random(3) do
    				self.field:spawn(Asteroid:new(self.field, self.x, self.y, self.size - 1))
    			end
    		end
    		
    		-- Если мы были уничтожены, вернём true, это удобно для некоторых случаев.
    		return true
    	end
    end
    
    -- Мы довольно часто будем применять эту функцию ниже
    local function collide(x1, y1, r1, x2, y2, r2)
    	-- Измеряем расстояния между точками по Теореме Пифагора:
      local distance = (x2 - x1) ^ 2 + (y2 - y1) ^ 2
    
    	-- Коль это расстояние оказалось меньше суммы радиусов - мы коснулись.
    	-- Возводим в квадрат чтобы сэкономить пару тактов на невычислении корней.
    	local rdist = (r1 + r2) ^ 2
    	return distance < rdist
    end
    
    function Asteroid:update(dt)
    
    	self.x = self.x + self.vx * dt
    	self.y = self.y + self.vy * dt
    
    	-- Астероиды у нас взаимодействуют и с пулями и с корабликом,
    	-- поэтому можно запихнуть обработку взаимодействия в класс астероидов:
    	for object in pairs(self.field:getObjects()) do
    		-- Вот за этим мы выставляли типы.
    		if object.type == 'bullet' then
    			if collide(self.x, self.y, self.radius, object.x, object.y, object.radius) then
    				self.field:destroy(object)
    				-- А за этим - возвращали true.
    				if self:applyDamage() then
    					-- если мы были уничтожены - прерываем дальнейшие действия
    					return
    				end
    			end
    		elseif object.type == 'ship' then
    			if collide(self.x, self.y, self.radius, object.x, object.y, object.radius) then
    				-- Показываем messagebox и завершаем работу.
    				-- Лучше выделить отдельно, но пока и так неплохо.
    				
    				local head = 'You loose!'
    				local body = 'Score is: '..self.field.score..'nRetry?'
    				local keys = {"Yea!", "Noo!"}
    				local key_pressed = love.window.showMessageBox(head, body, keys)
    				-- Была нажата вторая кнопка "Noo!":
    				if key_pressed == 2 then
    					love.event.quit()
    				end
    				self.field:init()
    				return
    			end
    		end
    	end
    	
    	-- Границы экрана - закон, который не щадит никого!
    	local screen_width, screen_height = love.graphics.getDimensions()
    	
    	if self.x < 0 then
    		self.x = self.x + screen_width
    	end
    	if self.y < 0 then
    		self.y = self.y + screen_height
    	end
    	if self.x > screen_width then
    		self.x = self.x - screen_width
    	end
    	if self.y > screen_height then
    		self.y = self.y - screen_height
    	end
    end
    
    function Asteroid:draw()
    	-- Указываем текущий цвет астероида:
    	love.graphics.setColor(self.color)
    	
    	-- Полигоны, увы, так просто вращать не получится
    	love.graphics.circle('line', self.x, self.y, self.radius)
    end
    
    
    -- Наконец, пишем класс который соберёт всё воедино:
    
    Field = {}
    Field.type = 'Field'
    -- Это будет синглтон, создавать много игровых менеджеров мы не собираемся,
    -- поэтому тут даже __index не нужен, ибо не будет объектов, 
    -- которые ищут методы в этой таблице.
    
    -- А вот инициализация/сброс параметров - очень даже пригодятся.
    function Field:init()
    	self.score   = 0
    
    	-- Таблица для всех объектов на поле
    	self.objects = {}
    
    	local ship = Ship:new(self, 100, 200)
    	print(ship)
    	self:spawn(ship)
    end
    
    
    function Field:spawn(object)
    	
    	-- Это немного нестандартное применение словаря:
    	-- в качестве ключа и значения указывается сам объект.
    	self.objects[object] = object
    end
    
    function Field:destroy(object)
    
    	-- Зато просто удалять.
    	self.objects[object] = nil
    end
    
    function Field:getObjects()
    	return self.objects
    end
    
    function Field:update(dt)
    
    	-- Мы хотим создавать новые астероиды, когда все текущие сломаны.
    	-- Сюда можно добавлять любые игровые правила.
    	local asteroids_count = 0
    	
    	for object in pairs(self.objects) do
    		-- Проверка на наличие метода
    		if object.update then
    			object:update(dt)
    		end
    		
    		if object.type == 'asteroid' then
    			asteroids_count = asteroids_count + 1
    		end
    	end
    	
    	if asteroids_count == 0 then
    		for i = 1, 3 do
    			-- Будем создавать новые на границах экрана
    			local y = math.random(love.graphics.getHeight())
    			self:spawn(Asteroid:new(self, 0, y, 3))
    		end
    	end
    end
    
    function Field:draw()
    	for object in pairs(self.objects) do
    		if object.draw then
    			object:draw()
    		end
    	end
    	love.graphics.print('n  Score: '..self.score)
    end
    
    
    -- Последние штрихи: добавляем наши классы и объекты в игровые циклы:
    
    function love.load()
    	Field:init()
    end
    
    
    function love.update(dt)
    	Field:update(dt)
    end
    
    function love.draw()
    	Field:draw()
    end
    


    При попытке копипасты и первого запуска вышеуказанной простыни, мы можем получить что-то похожее на классический asteroids.



    Разработка на LOVE



    Смотрится неплохо, но можно сделать лучше:



    1. Пространственная индексация, для ускорения обсчёта объектов;

    2. Более качественная организация менеджера, с ключами-идентификаторами;

    3. Всё таки, применить наследование в классах игровых объектов, наследовать их от «сферического в вакууме» (буквально) объекта, имеющего координаты и радиус, и т.п.



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



    Да, данный материал написан для версии LOVE 0.10.2.

    Для людей из будущего, которые застанут версии 0.11.X и старше: в данном исходном коде, необходимо поправить таблицу цветов, изменив значения с диапазона 0-255 на соответствующие пропорции 0-1, т.е. например:



    	-- Цвет вроде такого:
    	color = {0, 127, 255} 
    	-- Преобразовать во что-то похожее на:
    	color = {0, 0.5, 1}
    


    P. S.: Буду рад фидбеку и ответам на тему «будут ли иметь ценность статьи про создание маленьких игрушек и/или инструментов для данного фреймворка».

    Источник: Хабрахабр

    Категория: Программирование » Game Development

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *