Разработка на LÖVE

  • Tutorial
image

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

Уголок почемучки


Что такое LÖVE и почему именно это?

LÖVE — фреймворк для двухмерных игр. Он не является движком, только прослойкой между Lua и SDL2, с дополнительными приятными фишками, вроде чистоты синтаксиса, минимумом дополнительных телодвижений чтобы заставить работать OpenGL, и набором библиотек (вроде Box2d), позволяющих сразу сделать что-то забавное, и, не сходя с места, поковырять то что получилось. Но, притом, LÖVE отличается минимумом отсебятины и низким уровнем взаимодействия с железом, что позволяет делать свой движок вокруг фреймворка (для самообучения/дальнейшего применения) или сразу хардкодить игрушку.

Простота фреймворка позволяет писать простые прототипы или даже мини-игры тем, кто не является программистом, концентрируясь на процессе программирования, а не на освоении конкретной движковой технологии. Моя практика показала, что обучаемые возраста 14-17 лет, с гораздо большим удовольствием занимаются разработкой простых игр, чем выполняют классические лабораторные работы по вычислению корней квадратных уравнений или подсчёта кредитных ставок, а некоторые ученики начинают самостоятельно углубляться в материал, после чего, порой, становятся неплохими программистами.

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

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

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



Для дальнейшей работы, нам потребуется выполнить следующий набор действий:
  1. Загружаем последнюю версию LÖVE с официального сайта;
  2. Настраиваем запуск текущего проекта в LÖVE, стандартный метод тестового запуска — открыть директорию с файлом main.lua в исполняемом файле love. Так же, можно паковать содержимое директории с файлом main.lua в zip-архив, и или перетаскивать на исполняемый файл, или переименовать .zip в .love и настроить ассоциации файлов. Я считаю что проще настроить шорткат для текущего редактора, у notepad++ это, например:
    <Command name=...>path/to/love.exe $(CURRENT_DIRECTORY)</Command>
    Примеры для sublime можно найти в соседней статье;
  3. Создаём пустую директорию и добавляем в неё файл с именем main.lua. Желательно чтобы в пути не было пробелов и кириллицы, а то некоторые напихают пробелов, а потом жалуются, но для обхода можно чуть изменить шорткат или метод запуска;
  4. Открываем в любимом редакторе наш чистый и незапятнанный файл main.lua, и LÖVE-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.

image

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

1. Пространственная индексация, для ускорения обсчёта объектов;
2. Более качественная организация менеджера, с ключами-идентификаторами;
3. Всё таки, применить наследование в классах игровых объектов, наследовать их от «сферического в вакууме» (буквально) объекта, имеющего координаты и радиус, и т.п.

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

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

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

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

Ну. И что?
Реклама
Комментарии 32
  • 0
    Глянул в очередной раз на сайт. Не понял до конца — можно на мобилки билдить или нет? Там есть какая-то Android version, но это вроде сам редактор.

    И наверное общий вопрос: чем принципиально отличаются Lua движки типа Defold / Corona / Love? На что в первую очередь обратить внимание при выборе?
    • +4
      1. Билдить на мобилки — можно, хоть и с некоторыми телодвижениями. Модуль touch и фунции вроде vibrate из вики — ориентированы на мобильные телефоны.
        Для тестирования, в google play есть порт, с яблоками слегка сложнее, но, вроде, тоже цепляет. С ПК всё гораздо проще, подробнее можно посмотреть тут.
      2. Принципиальные различия — defold является движком, со всеми причитающимися: менеджеры сцен, сущности, коллизии (без box2d) и т.п. Он заставляет отвлекаться от программирования в сторону изучения самой движковой технологии. Corona SDK — тоже набор функций/библиотек, на начале своего развития, частично списывалась с LÖVE, а потом пошла куча своих (сложных) ништяков. Это хорошо, если твоя задача — быстро-быстро делать игры, пока солнце ещё высоко и ты уже знаешь технологию, но не очень, если ты хочешь учиться/учить, или иметь почти полностью своё приложение без чьих-либо претензий на «использование чужого кода», хоть корона и бесплатна, код закрыт, и расширять/фиксить не всегда получится. Ну, я не использую потому, что нет LuaJIT, без этого — медленно, и я не могу делать игровые тайловые карты 100500х9000 пикселей битмапой, как с FFI.
        В заключение, могу сказать что сравнение LÖVE и Defold/Corona, аналогично сравнению Lua и Python: Python старается дать всё что можно, Lua даёт всё что необходимо.
      • 0
        За без малого 20 лет в индустрии заметил странную штуку. Если в названии есть нелатинские символы, то у проекта меньше вероятности быть успешным.

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

        В общем, название ИМХО увеличивает шансы, что фреймворк тихо умрет.
        • 0
          Фреймворк развивается, без одной недели, десять лет.
          Тут дело не в названии или ещё чём-то, а в простоте и комфорте применения, с чем тут всё хорошо. Есть несколько игрушек в Steam/GooglePlay, крупных проектов немного, но это не очень страшно.
          Фреймворк делался и назывался «по приколу», и этим он тоже цепляет, как зацепил меня, четыре года назад, когда я не задумывался чтобы вообще стать программистом. Названия пользовательских библиотек — тоже забавные (но не всегда цензурные, всё таки love-тематика), и это тоже вклад в развитие общества.
          Цитируя Спольски:
          В 2003 году Nullsoft выпустили новую версию плеера Winamp, сопроводив выпуск вот такими комментариями на веб-сайте:
          • Клевый новый дизайн!
          • Навороченные фичи!
          • Большинство функций таки работает!

          Я о последнем… «Большинство функций таки работает!» — это забавно. И все счастливы, довольны плеером, используют его, говорят всем своим знакомым и друзьям. И они полагают, что Winamp — это хороший продукт, потому что они таки взяли и написали на своем сайте: «Большинство функций таки работает». Круто, да?

          Вот такие вещи и заставили нас влюбиться в Winamp.

          А с тех пор, как корпоративные слизняки из AOL Time Warner прибрали эту штуку к рукам, весь юмор с сайта исчез. Вы теперь просто-таки явно можете представить их себе, этих сальери, убийственно подавляющих малейшие признаки творческого подхода, который может отпугнуть какую-нибудь немолодую даму из Минессоты. Ценой уничтожения всего того, что заставило людей реально полюбить этот продукт.


          В данном случае, LÖVE «утирает нос конкурентам» своим дружелюбием, взаимопомощью комьюнити, не в последнюю очередь — отличной документацией и возможностью повлиять на развитие забавного и жизнеспособного опенсурс-проекта с десятилетней историей.

          Не стоит цепляться к мелочам, вроде «Нелатинских символов», они не играют роли в сравнении со всем остальным.
          • 0
            то ли гуглить инфу про это в разы сложнее
            • 0
              Как правило, можно заменить нелатинскую букву на схожую латинскую.
              По LÖVE, запросы составляются как «love2d spartial hash map», или похожим образом.
              В обиходе, применяется название «love» или «love2d», это нормально.
              • 0

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

                • 0

                  Тут вопрос про лёгкость гуглинга, а не про простоту набора символов.

                  • 0

                    Гугл как раз выдаёт его сходу, благодаря этому символу.

            • 0

              API до смешного прост, почему love2d до сих пор не стал мейнстримом?

              • 0
                Потому что есть Unity и Unreal
                • 0

                  но они компилят громоздкие билды, а тут — аккуратненько и емко, особенно это критично для мобильных платформ.

                  • 0

                    Для того чтобы написать что чуть более крупное на LÖVE и не превратить это в кашу, необходим высокий уровень навыков программирования и проектирования приложений, а так же значительно больше усилий: API прост и позволяет вытворять сногсшибательные штуки, но все инструменты придется или брать у сообщества, или писать самостоятельно. Это все равно что делать адаптивные веб-сайты, с ajax'ом на каждый чих, без движков и библиотек: маленькие получаются сравнительно быстро и просто, но что-то крупное, при лимитах по времени, растягивается на всю твою жизнь (требуя высоких навыков проектирования), и или ты берешь библиотеки/фреймворки, или ты впихиваешь готовый движок.

                    • 0
                      Ну, не такие уж и громоздкие билды. Просто сами Unity и Unreal весят по несколько гигабайт. А Lua- интерпретатор с sdl2 компактный. Если вдруг захотелось странного, например закодить прототип на подручном телефоне, то Love или Instead весьма кстати будут.
                      • 0
                        Кстати, и Unity и Unreal, в 2d-играх, тем не менее работают в трёхмерном пространстве. Там просто отсутствует двухмерный режим, поэтому 2d эмулируется через 3d, у которого камера перемещается по одной плоскости.

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

                        P. S. В версии LÖVE 0.11.x должна появиться система перевода SDL-OpenGL в трёхмерный режим, со всеми сопутствующими фичами.
                        • 0
                          поэтому 2d эмулируется через 3d, у которого камера перемещается по одной плоскости.

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


                          Для GPU, кстати, вообще нет понятия «2Д» — она в любом случае рисует трёхмерные примитивы (меши). Именно такой способ «ускоренного» рендеринга (через «эмуляцю» 2Д через 3Д) позволяет существенно увеличить производительность, особенно на телефонах. Именно таким ускорением (переводом рендеринга с CPU на GPU) в своё время занимались Adobe с их Molehill (Stage3D). И скорее всего именно так уже работает Love, если посмотреть на love.graphics API (например вот — явный отсыл к графическому Stencil Buffer love2d.org/wiki/love.graphics.setInvertedStencil) Точнее сказать не могу, т.к. уже написал выше что сам не пробовал Love. Но очень советую поглубже копнуть в эту тему, интересно уточнить как именно работает рендеринг в Love
                          • 0
                            Помнится, для GPU нет понятия 2d или 3d, она просто выдаёт фреймбуфер требуемого размера с произвольными вычислениями посередине.
                            Стандартный pipeline выглядит примерно так:
                            image

                            В love2d это выглядит следующим образом:
                            function love.load()
                                -- default pixel shader
                                local pixelcode = [[
                                    vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords )
                                    {
                                        vec4 texcolor = Texel(texture, texture_coords);
                                        return texcolor * color;
                                    }
                                ]]
                             
                                -- default vertex shader
                                local vertexcode = [[
                                    vec4 position( mat4 transform_projection, vec4 vertex_position )
                                    {
                                        return transform_projection * vertex_position;
                                    }
                                ]]
                                -- "copy" of default shader
                                shader = love.graphics.newShader(pixelcode, vertexcode)
                            end
                             
                            function love.draw()
                                love.graphics.setShader(shader)
                                -- draw things
                                love.graphics.setShader()
                                -- draw more things
                            end
                            


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

                            Ускорение, особенно на телефонах, может появиться в случае если мы пытаемся рисовать кучу всего «за камерой» благодаря отсечению невидимых граней, но когда я что-то пишу на love2d, я ручками проверяю видимость объектов и не рендерю их в случае чего (спасибо, пространственные индексации).
                            • 0
                              Помнится, для GPU нет понятия 2d или 3d

                              Да, именно об отсутствии понятия 2Д для GPU я написал выше. Все вершины считаются трёхмерными, вся математика с положением — в трёхмерном пространстве.

                              Пайплайн со скриншота как раз явно содержит передачу массива вершин — input geometry and attributes, которые определяют трёхмерные объекты (даже если они плоские)

                              Более интересен ваш пример с шейдерами — это и есть обычный режим работы OpenGL. Расчёт экранных координат переданных вершин расчитывается в vertex shader — там где умножается матрица 4х4 на четырёхмерный вектор. Мерность матрицы и положения вершины как бы намекает на трёхмерную природу — вектор вершины хранит её положение в трёхмерном пространстве (в гомогенном виде — четвёртая составляющая), а матрица — обычная матрица трансформации в трёхмерном пространстве (translate — rotate — scale). То есть Love тоже работает как «эмулятор 2Д в 3Д пространстве», и это крайне эффективно.

                              Отсечение невидимой геометрии за пределами камеры — это пользовательский код, которого нет в графическом драйвере по умолчанию. То есть если вы будете использовать сырой OpenGL для отрисовки 3Д геометрии, там тоже не будет фильтрации геометрии, выходящей за границы камеры (неважно — перспективной или ортографической, либо вообще камеры с кастомной матрицей проекции) пока её не напишешь вручную (frustum culling)
                              • 0
                                Но это же просто матрица, квантерион как бы «вне контекста».
                                Трёхмерное пространство или двухмерное — не важно.
                                Конкретно translate — rotate — scale — это в т.ч. двухмерные операции.
                                Тут есть ещё shear, который «искажает перспективу» как бы трёхмерной плоскости того что сейчас рисуем, и операции как бы с четырёхмерными матрицами, но само пространство — не трёхмерно, это данные для нескольких умножений/смещение вершинок, и всё, ничто не выстраивается относительно чего-то другого в пространстве. Хотя с другой стороны, в таком случае, видяха вообще не умеет работать ни с каким пространством, только с текстурами, векторами и матрицами. Хм.

                                Я не очень умный в данной области, ибо не было практики ковыряния голого OpenGL.
                                • 0
                                  ибо не было практики ковыряния голого OpenGL.

                                  Очень советую, крайне полезный опыт, большинство вопросов сразу пропадут :)
                                  • +1
                                    Благодарю. Если честно, я брал Love2d как раз для того чтобы не ковыряться в голом OpenGL, но это было несколько лет назад, когда всё сишное казалось страшным, отсутствовала математическая база и всё такое. Сейчас уже можно пробовать, найти бы время. Как раз Love2d привлёк возможностью сразу что-то быстро натыкать в «детском OpenGL» и пощупать, вместо того чтобы разбираться в движках и технологиях. Могу сказать что при должной усидчивости, полный профан (со средними способностями) который делает маленькие игрушки на Love2d, с повышением сложности архитектур приложений, изобретением инструментов для себя и т.п, за три-пять лет может стать неплохим программистом, способным делать движки и довольно крупное ПО. На самом деле с тем же успехом можно было бы взять сишку и OpenGL, но это пугает профанов, сами потом подтянут если понадобится. В общем, голь на выдумку хитра.

                                    После Lua + Love2d, я вполне себе работаю мультиязычным (в основном, та же Lua, хех) скриптовым бекенд-программистом с большим объёмом бизнес-логики, и могу строить архитектуры таких штук. С любым «полноценным движком» этого бы не было, я был бы узко заточен ровно под него.
                              • 0
                                Это, насколько я знаю, двухмерный режим OpenGL, трёхмерный чуть иначе работает: там посылаем кучу всякой фигни, отсекаем невидимые камерой грани, строим z-буфер, по нему рисуем плоскости с натянутой текстурой, хитро обсчитанной

                                У OpenGL нет «двухмерного режима», есть только API команды, которые описывают текущий вариант использования механизма отсечения невидимых граней (glCullFace), либо использования z-buffer (glDepthFunc). В случае если он не используется — мы просто пишем что не используем его (просто не включаем его в текущий render buffer). Но это не значит что его нельзя включить для 2Д рендеринга.

                                Опять же — OpenGL не знает что вы через него пытаетесь рисовать, он всё считает трёхмерной геометрией.
                                2Д обычно рисовать быстрее просто потому, что не нужно считать попиксельное освещение. Но при этом 2Д может быть медленнее в случае если оно рисуется через alpha-blend с огромным количеством накладывающихся слоёв. Хороший пример — закрытие игрового «мира» чёрной подложкой и рисование чего-то другого поверх (например экрана опций). В случае, если появляется такое большое количество «перерисовок» (overdraw) мобилки начинают сильно захлёбываться из-за «физического» ограничения на количество отрисовываемых пикселей за кадр — fillrate. ПК от этого обычно меньше стадают из-за большей общей производительности.
                              • 0
                                Прошу прощения за двоепостие, я сейчас напишу в комментариях ещё одну статью, мда.
                                Стенсил у love2d — это чисто двухмерная фиговина.
                                Фактически, это маска отрисовки:
                                мы как бы рисуем на одном фреймбуфере произвольную фигню функцией стенсила, и на скрин-фреймбуфер уже рисуем уже то что хотели нарисовать изначально, через «фильтр» фреймбуфера-стенсила по какой-то методике.
                                Инверсия — это значит фильтровать пиксели «в обратную» сторону.
                                Это больше похоже на режимы смешивания, только с пометкой «рисовать данный пиксель или нет, а если да — с какой прозрачностью»:
                    • 0

                      А есть список игр, написанных на LÖVE?

                    • 0

                      Как всегда жду подобные статьи, с более глубоким погружением в дебри LOVE.

                      • 0

                        Вот тут я попробовал слегка погрузиться в ООП и способы хранения игровых объектов. В "похожих публикациях" есть более простые примеры, так что это — лёгкое нарастание сложности.

                      • 0

                        Почему-то во всех этих статьях использут Саблайм/Блокнот/и тому подобные редакторы общего назначения, хотя есть замечательный ZeroBrane Studio с отладчиком и live-кодингом.

                        • 0
                          Лично я пользуюсь notepad++ потому, что привык с самого начала.
                          В моём варианте, когда постоянно переключаешься между lua/python/C(малые модули)/json/xml и т.п. — проще пользоваться одним инструментом, у которого уже помнишь все шорткаты, всё настроено и уже написаны все необходимые аддоны.
                          Zerobrane — хорош и восхитителен, в своей узкой области — lua-программирование. Но где ты найдёшь lua-разработчиков, которые пишут только и исключительно на Lua?
                          Хоть я таких и знаю, сам таким был пару лет назад, но с тех пор многое изменилось, notepad++ и lua/LÖVE просто оказались для меня первыми попавшимися ЯП/редактором/игровым фреймворком, за исключением DevC++ (мои самые первые попытки что-то писать), который я тогда не осилил. Утка? Ну что поделать, зато хорошо выучился.

                          Но вот к чему всё: редакторов много, люди выбирают под свой вкус или потребности.
                          Я даю описание того с чем работаю сам (жутко популярная фигня на венде, сложно найти машинку разработчика без NP++), и ссылку на крайне известную альтернативу.
                          Если хочешь инструкций для своей среды — можешь или загуглить, или отдельно спросить, это нормально.
                        • 0
                          Love еще жив?
                          Когда я последний раз его пробовал он жутко тупил при трех-четырех одновременных касаниях экрана на ipad, после чего был сразу выкинут
                          • 0
                            Боюсь что ты использовал не love а что-то ещё (на ipad довольно сложно билдить, сомневаюсь что ради одной пробы кто-то будет этим морочиться), плюс, на моей памяти (всеобъемлющей), love2d никогда не тупил на тачскринах в т.ч. ipad. Или это было особо специфичное использование фреймворка, тут, как на сишке — за корректность и скорость исполнения отвечает программист.

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

                          Самое читаемое