Love движок: LÖVE — Free 2D Game Engine

Любовь в пикселях или что такое Love2d / Хабр

Что же такое Love2d и о чем этот пост? Это кроссплатформенный фреймворк для разработки 2d игр. Почему именно love2d? Потому что это бесплатный, легкий, кроссплатформенный, с открытым исходным кодом, а главное сделанный с любовью движок.


Подготовка

Писать игры на нем можно хоть в блокноте, но я буду использовать Sublime Text 2, так как он прост и гибок. Скачать love можно вот здесь под все популярные десктопные платформы. Запускаем Sublime text и сразу идем в Tools->Build System->New Build System… И пишем туда вот такое:
{
    "selector": "source.lua",
    "cmd": ["c:\\Program Files\\LOVE\\love", "${project_path:${folder}}"]
}

(Если у вас x64 меняем путь к love)

Это для удобства запуска, теперь все, что нужно это нажать Ctrl+B. Создаем папку где мы будем хранить нашу игру. В ней создаем main.lua. В этом файле будет хранится основа нашей игры. А в Sublime text добавляем папку в проект. Все готово. Должно получиться что то вроде этого:

.

Хочу код!

Вся логика будет обновляться в функции love.update(dt), а прорисовка у нас в love.draw(), инициализация происходит love.load(). Поэтому давайте сразу напишем их:
function love.load() end function love.update(dt) end function love.draw() end

Теперь давайте добавим загрузим картинку и сразу ее нарисуем. Картинки загружаются с помощью функции love.graphics.newImage(filename), а рисуются в love.graphics.draw(image, x, y). Добавляем в папку вот эту картинку. И пишем код:

local habrImage, width, height

function love.load()
	habrImage = love.graphics.newImage("habr.png")
	width = love.graphics.getWidth()
	height = love.graphics.getHeight()
end

function love.update(dt)
	
end

function love.draw()
	love.graphics.draw(habrImage, width / 2 - habrImage:getWidth() / 2, height / 2 - habrImage:getHeight() / 2)
end

Ctrl + B и у нас что-то не красивое, давайте подправим и сделаем фон белым. Добавим в начале отрисовки вот такую строчку:

love.graphics.setBackgroundColor(255, 255, 255)

И вот, приятная глазу, картинка.

Это все, конечно круто, но давайте добавим жизни нашей игре и сделаем действия с картинкой, а именно:
— По клавише R картинка будет крутится
— По клавише S картинка будет увеличиваться\уменьшаться
— По клавише M картинка будет двигаться.

Чтобы это реализовать, добавим переменные state (будет отвечать за происходящие), rotation (угол картинки), scale (ее размер), ox, ox (смещение центра картинки) и delta(эта переменная будет отвечать за апдейт переменных). Теперь о инпуте, в love когда клавиша опускается вызывается функция love.keypressed(key, unicode), а при поднятии love.keyreleased(key). Мы будем отслеживать опускание клавиш. Теперь сам код:

local habrImage, width, height, state, rotation, scale, ox, oy, delta

--инициализируем все
function love.load()
	habrImage = love.graphics.newImage("habr.png")
	width = love.graphics.getWidth()
	height = love.graphics.getHeight()
	state = "none"
	resetVariables()
end

--никто не любит писать одно и тоже много раз
function resetVariables()
	rotation = 0
	scale = 1
	ox = 0
	oy = 0
	delta = 1
end

--смотрим что было нажато
function love.keypressed(key, unicode)
	if key == "r" then
		state = "rotation"
		resetVariables()
	elseif key == "s" then
		state = "scalling"
		resetVariables()
	elseif key == "m" then
		state = "moving"
		resetVariables()
	elseif key == "space" then -- чтобы не залипать (:
		state = "none"
		resetVariables()
	end
end

--логика игры
function love.update(dt)
	if state == "rotation" then
		--крутим картинку
		rotation = rotation + delta * dt
	elseif state == "scalling" then
		--увеличиваем
		scale = scale + delta * dt
	elseif state == "moving" then
		--здесь немного посложнее, но все же просто:
		--каждый раз мы увеличивыем дельту
		--а потом берем ее за угол для косинису и синуса
		--и крутим картинку
		delta = delta + delta * dt
		local radius = 50
		ox = radius * math.sin(delta)
		oy = radius * math.cos(delta)
	end
end

--рисуем
function love.draw()
	--рисуем белым
	love.graphics.setBackgroundColor(255, 255, 255)
	--рисуем картинку
	love.graphics.draw(habrImage, width / 2 - habrImage:getWidth() / 2, height / 2 - habrImage:getHeight() / 2, rotation, scale, scale, ox, oy)
end

На этом все, господа. Удачи с созданием игр! Для помощи вот вам вики и офф сайт.

Продолжение.

З.Ы. Это все писалось на 0.8.0.
З.З.Ы. Не забудьте выбрать Build System, чтобы запускать.

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


Цель поста — в максимально простой форме описать основные этапы разработки с помощью фреймворка 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.

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

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.: Буду рад фидбеку и ответам на тему «будут ли иметь ценность статьи про создание маленьких игрушек и/или инструментов для данного фреймворка».

Создание танка на движке Love2D

Всем доброго времени суток. В данной статье (если это можно назвать статьёй, в основном код), как вы уже наверно догадались, пойдёт речь о создании танка на игровом движке LÖVE.

Прежде всего определим, что должен делать наш создаваемый танк:

  • первое и самое главное на мой взгляд — это перемещение, которое соответствует «нормальному» танку. Т.е. при нажатии на клавиши W и S танк будет двигаться вперёд и назад, а при нажатии на клавиши A и D танк будет поворачиваться влево и вправо соответственно.
  • второе немаловажное действие — стрельба. Левой кнопкой мыши будет осуществляться стрельба обычными патронами, а на правую кнопку мыши мы «повесим» стрельбу ракетами.
  • при движении у танка должны двигаться гусеницы.
  • у танка будут выходить выхлопные газы.
  • ну и последнее — танк должен оставлять за собой следы, которые через определённый промежуток времени будут исчезать.

Для начала нам нужно создать все изображения и поместить их в какую-нибудь папку, например data/images.

Художник из меня плохой, поэтому всю графику мне предоставил пользователь форума GcUp silver52rus, за что ему ОГРОМНОЕ СПАСИБО.

Корпус танка с анимацией гусениц Башня танка След, оставляемый танком при движении Выхлопные газы Патрон Ракета Частицы для ракеты

Далее нам нужны вспомогательные библиотеки:

  • HUMP для работы с классами, векторами и таймером.
  • anim8 для работы с анимацией.
  • модуль Input (в архиве), который я написал для более удобного контроля над вводом (по крайней мере для меня). Это не финальная его версия, планируется ещё добавить поддержку Android.
Нужно их скачать и поместить в папку classes.

В итоге должна получиться такая структура проекта:

Для начала разберёмся с файлом main.lua:


-- Подключаем необходимые библиотеки
Class = require "classes.hump.class"
Vector = require "classes.hump.vector"
Timer = require "classes.hump.timer"
Input = require "classes.Input"
Anim = require "classes.anim8.anim8"

function love.load()
  -- Здесь будет создаваться экземпляр класса "Танк"
end

function love.run()
  if love.math then
    love.math.setRandomSeed( os.time() )
  end

  if love.event then
    love.event.pump()
  end

  if love.load then
    love.load( arg )
  end

  if not (love.window and love.graphics and love.window.isCreated() and love.timer) then
    return
  end

  love.timer.step()
  love.graphics.setBackgroundColor( 50, 50, 50 )  -- Цвет фона

  -- Главный цикл приложения
  while Input.running() do
    love.timer.step()
    local dt = love.timer.getDelta()

    Input.update()

    -- Здесь будет функция обновления танка

    love.graphics.clear()
    love.graphics.origin()

    -- Здесь будет функция отрисовки танка

    love.graphics.present()

    Input.reset()

    love.timer.sleep( 0.001 )
  end
end

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

Ниже приведён код файла настроек приложения conf.lua. Здесь можно указать заголовок, ширину и высоту окна, включить или отключить какие-то модули, сделать окно на весь экран ну и прописать какие-нибудь дополнительные настройки. Более подробно о настройках проекта можно ознакомиться здесь.


function love.conf(c)
  c.version = "0.9.1"
  c.title = "Tanks"
  c.window.width = 1024
  c.window.height = 768
  c.window.resizable = false
  c.window.fullscreen = false
  c.window.fullscreentype = "normal"
  c.window.vsync = true
  c.window.fsaa = 4
  c.modules.audio = true
  c.modules.event = true
  c.modules.graphics = true
  c.modules.image = true
  c.modules.joystick = false
  c.modules.keyboard = true
  c.modules.math = true
  c.modules.mouse = true
  c.modules.physics = false
  c.modules.sound = true
  c.modules.system = true
  c.modules.timer = true
  c.modules.window = true
end

Наконец-то дошли до самого интересного момента — создание класса «Танк».


local Tank = Class {
  -- конструктор класса
  init = function( self, pos )
    -- позиция танка
    self.pos = pos

    -- направление корпуса танка
    self.body_dir = Vector( 0, 0 )

    -- направление башни танка
    self.top_dir = Vector( 0, -1 )

    -- изображения корпуса и башни танка
    self.body = love.graphics.newImage("data/images/tank_body.png")
    self.top = love.graphics.newImage("data/images/tank_top.png")

    -- размер одного кадра корпуса танка
    self.size = Vector( 128, 128 )

    -- сетка анимаций корпуса танка
    -- в функцию передаётся размер одного кадра анимации и размер изображения с анимациями
    local grid = Anim.newGrid( self.size.x, self.size.y, self.body:getWidth(), self.body:getHeight() )

    -- анимация "покоя" танка (по сути первый кадр)
    self.iddle = Anim.newAnimation( grid( 1, 1 ), 1 )

    -- анимация движения танка
    -- "1-9" - с 1-го по 9-й кадр
    -- 1 - номер строки с кадрами анимации (в данном примере одна строка, в которой 9 кадров)
    -- 0.05 - время смены кадров анимации
    self.run = Anim.newAnimation( grid( "1-9", 1 ), 0.05 )

    -- текущая анимация - "покой"
    self.animation = self.iddle

    -- изображение следа, оставляемого танком
    self.trail_image = love.graphics.newImage("data/images/tank_trail.png")

    -- шаг следов (чтобы следы шли не сплошной полосой, а через определённый промежуток)
    self.trail_step = 0

    -- массив со следами
    self.tank_trails = {}

    -- угол поворота корпуса танка
    self.body_angle = 0

    -- угол поворота башни танка
    self.top_angle = 0

    -- скорость перемещения танка
    self.max_speed = 70

    -- скорость поворота башни танка
    self.max_rot_speed = 100

    -- скорость полёта пуль
    self.bullet_speed = 350

    -- скорость полёта ракет
    self.rocket_speed = 250

    -- массивы с пулями и ракетами
    self.bullets = {}
    self.rockets = {}

    -- дым от танка
    self.tank_smoke = love.graphics.newImage("data/images/cloud.png")
    self.ps = love.graphics.newParticleSystem( self.tank_smoke, 32 )
    self.ps:setAreaSpread( "uniform", 0, 0 )
    self.ps:setDirection( math.rad( self.body_angle + 90 ) )
    self.ps:setEmissionRate( 10 )
    self.ps:setEmitterLifetime( -1 )
    self.ps:setInsertMode("bottom")
    self.ps:setLinearAcceleration( 0, 1, 0, 2 )
    self.ps:setParticleLifetime( 0.5, 1 )
    self.ps:setRadialAcceleration( 0, 5 )
    self.ps:setRotation( math.rad(0), math.rad( 360 ) )
    self.ps:setSpeed( 30, 50 )
    self.ps:setSpread( math.rad( 60 ) )
    self.ps:setPosition( self.pos.x, self.pos.y + 60 )
    self.ps:start()
  end,

  -- функция обновления
  update = function( self, dt )
    -- делаем текущую анимацию - анимация "покоя"
    self.animation = self.iddle

    -- плавный поворот башни танка за курсором
    local mousePos = Input.mousePos()
    local dir = self.pos - mousePos

    local targetAngle = -math.atan2( dir.x, dir.y ) / (math.pi / 180)
    local curAngle = self.top_angle

    if curAngle  180 then
      a = a - 360
    elseif a = -s - 0.5 and a  s + 0.5 then
      self.top_angle = self.top_angle - s
    elseif a = 360 then
        self.body_angle = 0
      end

      -- функция "оставления" следов
      self:makeTrail()
    end

    -- поворот корпуса танка против часовой стрелки
    if Input.keyHeld("a") then
      self.animation = self.run
      self.body_angle = self.body_angle - self.max_speed * dt

      if self.body_angle 

Делаем Space Invaders на Love2d и Lua / Хабр

Добрый день! Сегодня будем делать классическую игру Space Invaders на движке Love2d. Для любителей «кода сразу» окончательную версию игры можно посмотреть на гитхабе. Тем же кому интересен процесс разработки, добро пожаловать под кат.


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

Итак, план работы:


Подготовка


В main.lua добавим вызовы основных методов love2d. Каждый элемент или функция, которые мы сделаем впоследствии должны прямо или косвенно быть связаны с этими методами, иначе пройдут незамеченными.
function love.load()
end

function love.keyreleased( key )
end

function love.draw()
end

function love.update( dt )
end

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


Добавляем в корень проекта файл player.lua

local player = {}

player.position_x = 500
player.position_y = 550
player.speed_x = 300

player.width = 50
player.height = 50

function player.update( dt )
    if love.keyboard.isDown( "right" ) and 
            player.position_x < ( love.graphics.getWidth() - player.width ) then
        player.position_x = player.position_x + ( player.speed_x * dt )
    end
    if love.keyboard.isDown( "left" )  and player.position_x > 0 then
        player.position_x = player.position_x - ( player.speed_x * dt )
    end
end

function player.draw()
    love.graphics.rectangle(
                       "fill",
                       player.position_x,
                       player.position_y,
                       player.width,
                       player.height
                 )
end

return player

А также обновим main.lua
local player = require 'player'

function love.draw()
        player.draw()
end

function love.update( dt )
        player.update( dt )
end

Если запустить игру, то мы увидим чёрный экран с белым квадратом снизу, которым можно управлять клавишами «влево» и «вправо». Причём выйти за пределы экрана он не может в силу ограничений в коде Игрока:
player.position.x < ( love.graphics.getWidth() - player.width )
player.position.x > 0

Добавим врагов


Так как бороться мы будем против иноземных захватчиков, то и файлик с ними назовём invaders.lua:

local invaders = {}

invaders.rows = 5
invaders.columns = 9

invaders.top_left_position_x = 50
invaders.top_left_position_y = 50

invaders.invader_width = 40
invaders.invader_height = 40

invaders.horizontal_distance = 20
invaders.vertical_distance = 30

invaders.current_speed_x = 50

invaders.current_level_invaders = {}

local initial_speed_x = 50
local initial_direction = 'right'

function invaders.new_invader( position_x, position_y )
    return { position_x = position_x,
                 position_y = position_y,
                 width = invaders.invader_width,
                 height = invaders.invader_height }
end

function invaders.new_row( row_index )
    local row = {}
    for col_index=1, invaders.columns - (row_index % 2) do
        local new_invader_position_x = invaders.top_left_position_x + invaders.invader_width * (row_index % 2) + (col_index - 1) * (invaders.invader_width + invaders.horizontal_distance)
        local new_invader_position_y = invaders.top_left_position_y + (row_index - 1) * (invaders.invader_height + invaders.vertical_distance)
        local new_invader = invaders.new_invader( new_invader_position_x, new_invader_position_y )
        table.insert( row, new_invader )
    end
    return row
end

function invaders.construct_level()
    invaders.current_speed_x = initial_speed_x
    for row_index=1, invaders.rows do
        local invaders_row = invaders.new_row( row_index )
        table.insert( invaders.current_level_invaders, invaders_row )
    end
end

function invaders.draw_invader( single_invader )
    love.graphics.rectangle('line',
                       single_invader.position_x,
                       single_invader.position_y,
                       single_invader.width,
                       single_invader.height )
end

function invaders.draw()
    for _, invader_row in pairs( invaders.current_level_invaders ) do
        for _, invader in pairs( invader_row ) do
            invaders.draw_invader( invader, is_miniboss )
        end
    end
end

function invaders.update_invader( dt, single_invader )
    single_invader.position_x = single_invader.position_x + invaders.current_speed_x * dt
end

function invaders.update( dt )
    local invaders_rows = 0
    for _, invader_row in pairs( invaders.current_level_invaders ) do
        invaders_rows = invaders_rows + 1
    end
    if invaders_rows == 0 then
        invaders.no_more_invaders = true
    else
        for _, invader_row in pairs( invaders.current_level_invaders ) do
            for _, invader in pairs( invader_row ) do
                invaders.update_invader( dt, invader )
            end
        end
    end
end

return invaders

Обновим main.lua
...
local invaders = require 'invaders'

function love.load()
        invaders.construct_level()
end

function love.draw()
        ...
        invaders.draw()
end

function love.update( dt )
        ...
        invaders.update( dt )
end

love.load вызывается в самом начале работы приложения. Он вызывает метод invaders.construct_level, который создаёт таблицу invaders.current_level_invaders и наполняет её по строкам и столбцам отдельными объектами invader с учётом высоты и ширины объектов, а также требуемого расстояния между ними по горизонтали и вертикали. Пришлось немного усложнить метод invaders.new_row, чтобы добиться смещения чётных и нечётных рядов. Если заменить текущую конструкцию:
for col_index=1, invaders.columns - (row_index % 2) do
    local new_invader_position_x = invaders.top_left_position_x + invaders.invader_width * (row_index % 2) + (col_index - 1) * (invaders.invader_width + invaders.horizontal_distance)

вот такой:
for col_index=1, invaders.columns do
    local new_invader_position_x = invaders.top_left_position_x + (col_index - 1) * (invaders.invader_width + invaders.horizontal_distance)

то уберём этот эффект и вернём прямоугольное заполнение. Сравнение на картинках
Текущий вариант Прямоугольный вариант

Объект invader представляет собой таблицу со свойствами: position_x, position_y, width, height. Всё это требуется для отрисовки объекта, а также позднее потребуется для проверки на коллизии с выстрелами.

love.draw вызывает invaders.draw и отрисовываются все объекты во всех рядах таблицы invaders.current_level_invaders.

love.update, а следом и invaders.update обновляют текущую позицию каждого захватчика с учётом текущей скорости, которая пока только одна — изначальная.

Захватчики уже начали двигаться, но пока только вправо, за экран. Это мы сейчас поправим.

Добавим стены и коллизии


Новый файл walls.lua
local walls = {}

walls.wall_thickness = 1
walls.bottom_height_gap = 1/5 * love.graphics.getHeight()

walls.current_level_walls = {}

function walls.new_wall( position_x, position_y, width, height )
    return { position_x = position_x,
             position_y = position_y,
             width = width,
             height = height }
end

function walls.construct_level()
    local left_wall = walls.new_wall( 0,
                                      0,
                                      walls.wall_thickness,
                                      love.graphics.getHeight() - walls.bottom_height_gap
                                )
    local right_wall = walls.new_wall( love.graphics.getWidth() - walls.wall_thickness,
                                       0,
                                       walls.wall_thickness,
                                       love.graphics.getHeight() - walls.bottom_height_gap
                                )
    local top_wall = walls.new_wall( 0,
                                     0,
                                     love.graphics.getWidth(),
                                     walls.wall_thickness
                                )
    local bottom_wall = walls.new_wall( 0, 
                                love.graphics.getHeight() - walls.bottom_height_gap - walls.wall_thickness,
                                love.graphics.getWidth(),
                                walls.wall_thickness
                                )
    walls.current_level_walls["left"] = left_wall
    walls.current_level_walls["right"] = right_wall
    walls.current_level_walls["top"] = top_wall
    walls.current_level_walls["bottom"] = bottom_wall
end

function walls.draw_wall(wall)
    love.graphics.rectangle( 'line',
                             wall.position_x,
                             wall.position_y,
                             wall.width,
                             wall.height
                        )
end

function walls.draw()
    for _, wall in pairs( walls.current_level_walls ) do
        walls.draw_wall( wall )
    end
end

return walls

И немного в main.lua
...
local walls    = require 'walls'

function love.load()
    ...
    walls.construct_level()
end

function love.draw()
    ...
    -- walls.draw()
end

Аналогично с созданием захватчиков, за создание стен отвечает вызов walls.construct_level. Стены нам нужны только для перехвата «столкновений» с ними захватчиков и выстрелов, поэтому отрисовывать их нам без надобности. Но это может понадобиться для целей отладки, поэтому у объекта Walls имеется метод draw, вызов которого происходит стандартно из main.lua -> love.draw, но пока отладка не нужна — он (вызов) закомментирован.

Теперь напишем обработчик коллизий, который был мной позаимствован отсюда. Итак, collisions.lua

local collisions = {}

function collisions.check_rectangles_overlap( a, b )
    local overlap = false
    if not( a.x + a.width < b.x or b.x + b.width < a.x or
            a.y + a.height < b.y or b.y + b.height < a.y ) then
        overlap = true
    end
    return overlap
end

function collisions.invaders_walls_collision( invaders, walls )
    local overlap, wall
    if invaders.current_speed_x > 0 then
        wall, wall_type = walls.current_level_walls['right'], 'right'
    else
        wall, wall_type = walls.current_level_walls['left'], 'left'
    end
    
    local a = { x = wall.position_x,
                y = wall.position_y,
                width = wall.width,
                height = wall.height }
    for _, invader_row in pairs( invaders.current_level_invaders ) do
        for _, invader in pairs( invader_row ) do
            local b = { x = invader.position_x,
                        y = invader.position_y,
                        width = invader.width,
                        height = invader.height }
            overlap = collisions.check_rectangles_overlap( a, b )
            if overlap then
                if wall_type == invaders.allow_overlap_direction then
                    invaders.current_speed_x = -invaders.current_speed_x 
                    if invaders.allow_overlap_direction == 'right' then
                        invaders.allow_overlap_direction = 'left'
                    else
                        invaders.allow_overlap_direction = 'right'
                    end
                    invaders.descend_by_row()
                end
            end
        end
    end
end

function collisions.resolve_collisions( invaders, walls )
    collisions.invaders_walls_collision( invaders, walls )
end

return collisions

Добавим пару методов и переменную в invaders.lua
invaders.allow_overlap_direction = 'right'

function invaders.descend_by_row_invader( single_invader )
    single_invader.position_y = single_invader.position_y + invaders.vertical_distance / 2
end

function invaders.descend_by_row()
    for _, invader_row in pairs( invaders.current_level_invaders ) do
        for _, invader in pairs( invader_row ) do
            invaders.descend_by_row_invader( invader )
        end
    end
end

И добавим проверку на коллизии в main.lua
local collisions = require 'collisions'

function love.update( dt )
    ...
    collisions.resolve_collisions( invaders, walls )
end

Теперь захватчики натыкаются на стену collisions.invaders_walls_collision и спускаются немного пониже, а также меняют скорость на противоположную.

Пришлось ввести дополнительную проверку на равенство типа той стены, на которую наткнулись захватчики, и переменной, в которой хранится допустимый тип:

if overlap then
    if wall_type == invaders.allow_overlap_direction then
    ...

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

Пора игроку научиться стрелять


Новый файлик и класс bullets.lua
local bullets = {}

bullets.current_speed_y = -200
bullets.width = 2
bullets.height = 10

bullets.current_level_bullets = {}

function bullets.destroy_bullet( bullet_i )
    bullets.current_level_bullets[bullet_i] = nil
end

function bullets.new_bullet(position_x, position_y)
    return { position_x = position_x,
             position_y = position_y,
             width = bullets.width,
             height = bullets.height }
end

function bullets.fire( player )
    local position_x = player.position_x + player.width / 2
    local position_y = player.position_y
    local new_bullet = bullets.new_bullet( position_x, position_y )
    table.insert(bullets.current_level_bullets, new_bullet)
end

function bullets.draw_bullet( bullet )
    love.graphics.rectangle( 'fill',
                             bullet.position_x,
                             bullet.position_y,
                             bullet.width,
                             bullet.height
                        )
end

function bullets.draw()
    for _, bullet in pairs(bullets.current_level_bullets) do
        bullets.draw_bullet( bullet )
    end
end

function bullets.update_bullet( dt, bullet )
    bullet.position_y = bullet.position_y + bullets.current_speed_y * dt
end

function bullets.update( dt )
    for _, bullet in pairs(bullets.current_level_bullets) do
        bullets.update_bullet( dt, bullet )
    end
end

return bullets

Здесь основной метод — bullets.fire. Мы передаём в него Игрока, т.к. хотим, чтобы пуля вылетала «из него», а значит нам надо знать его местоположение. Т.к. патрон у нас не один, а возможна целая очередь, то храним её в таблице bullets.current_level_bullets, вызываем для неё и каждого патрона методы draw и update. Метод bullets.destroy_bullet нужен, чтобы при соприкосновении с захватчиком или потолком удалять лишние патроны из памяти.

Добавим обработку коллизий пуля-захватчик и пуля-потолок.

collisions.lua

function collisions.invaders_bullets_collision( invaders, bullets )
    local overlap
    
    for b_i, bullet in pairs( bullets.current_level_bullets) do
        local a = { x = bullet.position_x,
                    y = bullet.position_y,
                    width = bullet.width,
                    height = bullet.height }
        
        for i_i, invader_row in pairs( invaders.current_level_invaders ) do
            for i_j, invader in pairs( invader_row ) do
                local b = { x = invader.position_x,
                            y = invader.position_y,
                            width = invader.width,
                            height = invader.height }
                overlap = collisions.check_rectangles_overlap( a, b )
                if overlap then
                    invaders.destroy_invader( i_i, i_j )
                    bullets.destroy_bullet( b_i )
                end
            end
        end
    end
end

function collisions.bullets_walls_collision( bullets, walls )
    local overlap
    local wall = walls.current_level_walls['top']
    
    local a = { x = wall.position_x,
                y = wall.position_y,
                width = wall.width,
                height = wall.height }
    
    for b_i, bullet in pairs( bullets.current_level_bullets) do
        local b = { x = bullet.position_x,
                    y = bullet.position_y,
                    width = bullet.width,
                    height = bullet.height }
    
        overlap = collisions.check_rectangles_overlap( a, b )
        if overlap then
            bullets.destroy_bullet( b_i )
        end
    end
end

function collisions.resolve_collisions( invaders, walls, bullets )
    ...
    collisions.invaders_bullets_collision( invaders, bullets )
    collisions.bullets_walls_collision( bullets, walls )
end

К захватчикам добавим метод для его уничтожения, а также для проверки на наличие захватчиков в конкретном ряду в общей таблице захватчиков — если никого не осталось, то и сам ряд удаляем. А также увеличиваем скорость всей армады при убийстве.

invaders.lua

...
invaders.speed_x_increase_on_destroying = 10

function invaders.destroy_invader( row, invader )
    invaders.current_level_invaders[row][invader] = nil
    local invaders_row_count = 0
    for _, invader in pairs( invaders.current_level_invaders[row] ) do
        invaders_row_count = invaders_row_count + 1
    end
    if invaders_row_count == 0 then
        invaders.current_level_invaders[row] = nil
    end
    if invaders.allow_overlap_direction == 'right' then
        invaders.current_speed_x = invaders.current_speed_x + invaders.speed_x_increase_on_destroying
    else
        invaders.current_speed_x = invaders.current_speed_x - invaders.speed_x_increase_on_destroying
    end
 end
...

И обновляем mail.lua: добавляем новый класс, отправляем его в обработчик коллизий, и вешаем вызов стрельбы на клавишу Space.
...
local bullets    = require 'bullets'

function love.keyreleased( key )
    if key == 'space' then
        bullets.fire( player )
    end
end

function love.draw()
    ...
    bullets.draw()
end

function love.update( dt )
    ...
    collisions.resolve_collisions( invaders, walls, bullets )
    bullets.update( dt )
end

Дальнейшая работа предполагает модификацию существующего кода, поэтому то, что получилось на данном этапе сохраняем как версию 0.5.

NB Код в гите отличается от разобранного здесь. Изначально использовалась библиотека hump для работы с векторами. Но потом стало ясно, что вполне можно обойтись и без неё, и в окончательной редакции выпилил библиотеку. Код одинаково рабочий и здесь и там, единственно, для запуска кода с гитхаба придётся проинициировать сабмодули:

git submodule update --init

Навешиваем текстуры



Это три стандартных врага, плюс один минибосс, устройство которого здесь рассмотрено не будет, но он есть в окончательной версии. И сам игрок-танк.

Текстуры для игры любезно предоставила annnushkkka.

Все картинки будут находиться в каталоге images в корне проекта. Меняем Игрока в player.lua

...
player.image = love.graphics.newImage('images/Hero.png')

-- from https://love2d.org/forums/viewtopic.php?t=79756
function getImageScaleForNewDimensions( image, newWidth, newHeight )
    local currentWidth, currentHeight = image:getDimensions()
    return ( newWidth / currentWidth ), ( newHeight / currentHeight )
end
local scaleX, scaleY = getImageScaleForNewDimensions( player.image, player.width, player.height )

function player.draw()  -- меняем полностью
    love.graphics.draw(player.image,
                       player.position_x,
                       player.position_y, rotation, scaleX, scaleY )
end
...

Фнкция getImageScaleForNewDimensions, подсмотренная вот отсюда, подгоняет картинку под те размеры, которые мы указали в player.width, player.height. Она используется и здесь и для врагов, впоследствии вынесем её в отдельный модуль utils.lua. Функцию player.draw заменяем.

При запуске бывший игрок-квадрат теперь — танк!

Меняем врагов invaders.lua

...
invaders.images = {love.graphics.newImage('images/bad_1.png'),
                   love.graphics.newImage('images/bad_2.png'),
                   love.graphics.newImage('images/bad_3.png')
            }

-- from https://love2d.org/forums/viewtopic.php?t=79756
function getImageScaleForNewDimensions( image, newWidth, newHeight )
    local currentWidth, currentHeight = image:getDimensions()
    return ( newWidth / currentWidth ), ( newHeight / currentHeight )
end
local scaleX, scaleY = getImageScaleForNewDimensions( invaders.images[1], invaders.invader_width,
    invaders.invader_height )

function invaders.new_invader(position_x, position_y ) -- меняем
    local invader_image_no = math.random(1, #invaders.images)
    invader_image = invaders.images[invader_image_no]
    return ({position_x = position_x,
             position_y = position_y,
             width = invaders.invader_width,
             height = invaders.invader_height,
             image = invader_image})
end

function invaders.draw_invader( single_invader ) -- меняем
    love.graphics.draw(single_invader.image,
                       single_invader.position_x,
                       single_invader.position_y, rotation, scaleX, scaleY )
end

Добавляем картинки врагов в таблице и подгоняем размеры через getImageScaleForNewDimensions. При создании нового захватчика ему в атрибут image присваивается рандомная картинка из нашей таблицы картинок. И меняем сам метод отрисовки.

Вот что вышло:

Если позапускать игру несколько раз, то можно увидеть, что рандомная комбинация врагов каждый раз одинаковая. Чтобы этого избежать надо определить math.randomseed перед началом игры. Хорошо это делать, передавая в качестве аргумента os.time. Добавим это в main.lua

function love.load()
    ...
    math.randomseed( os.time() )
    ...
end

Теперь у нас есть почти полноценная игра, версия 0.75. Разобрали всё, что планировали.

Буду рад отзывам, комментариям, подсказкам!

Пишем свою первую игру на love

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

Обмолвлюсь сразу, что у Love есть свой форум и вики, поэтому, если что можно почерпать информацию там.

Если вы программировали до love, вам будет проще понять его и Lua. Поэтому я попробую описать некоторые особенности в сравнении, например, с Game Maker.

Love содержит большое кол-во встроенных функций для работы со всеми аспектами разрабатываемых на нём проектов, но даже если вам их не хватает, вы можете их написать сами.

Как использовать love?

Чтобы начать писать игру, нужно создать в любом месте папку с любым названием и создать в нём файл main.lua, именно этот файл будет вызываться при запуске игры. Также в папку можно засунуть все нужные файлы для игры, включая другие скрипты Lua.

Чтобы запустить игру, перетяните папку на love.exe или его ярлык.

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

Чтобы компилировать игру в .exe, можно использовать эту мини-программу, написанную YellowAfterlife. Просто перетяните файл .love на compile.bat и в папке в скором времени появится .exe.

Код игр на Lua состоит из функций, выглядит это так:

function love.load()

// действие

end

По сути, функции (для лучшего восприятия, что это) можно назвать аналогом событий в GM. В этом уроке мы разберём 3 главных функции, без которых нельзя обойтись и несколько собственных.

function love.load() — аналог Create, проигрывается всего один раз в начале игры

function love.update() — аналог Step, проигрывается постоянно

function love.draw() — аналог Draw, проигрывается каждый кадр

Стоит учесть основные особенности Lua:

1) Здесь нет { } как блоки кода, эти два символа работают при создании таблиц. Вместо них в условиях и функциях нужно использовать <ничего> и end.

2) Однострочные комментарии здесь ставятся так:

— текст

Многострочные:

[[ текст 1

текст 2

текст 3]]

3) При создании условия здесь нельзя использовать =, обязательно ==. После условия должен стоять оператор then.

4) Объекты тоже создаются просто. Указывается переменная (название), ставится равно и в фигурных скобках записываются свойства. Есть несколько способов это записать:

4.1) hero = {x = 16, y = 16, speed = 10}

4.2)

hero = {}

hero.x = 16

hero.y = 16

hero.speed = 10

4.3)

hero = {

x = 16,

y = 16,

speed = 10

}

Лично я выбираю такой способ.

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

Код, с которого нужно начинать написание игры выглядит так:

function love.load()

end

function love.update()

end

function love.draw()

end

А теперь дописываем dt как аргумент к love.update. Т.е. так: love.update(dt). dt — время, которое прошло с момента использования прошлого кадра.

Я рекомендую вам не читать между строк, а просматривать код, иначе ничего не поймёте.

А теперь создадим 3 объекта: hero (игрок), friend (друг), quit (выход) и зададим им свойства.

function love.load()
    text = «Sandbox» — текст для теста
    love.graphics.setBackgroundColor(0, 128, 55) — меняем цвет фона (R, G, B)
    — герой
    hero = { — герой
    x = 16, — x
    y = 16, — y
    xstart = x, — стартовый x
    ystart = y, — стартовый y
    speed = 300, — скорость
    hp = 100, — hp
    w = 32, — ширина
    h = 32, — высота
    }

    — аналогично с другими
    — друг
    friend = {
    x = 320,
    y = 240,
    xstart = x,
    ystart = y,
    speed = 200,
    hp = 100,
    w = 16,
    h = 16,
    ai = 0 — бежит, ли за игроком (начинает бежать, после того, как дотронется по него)
    }
    — выход
    quit = {
    x = love.mouse.getX(), — X равен x курсора
    y = love.mouse.getY(), — Y равен y курсора
    xstart = x,
    ystart = y,
    w = 16,
    h = 16,
    }
end

Как видите, это совсем не сложно. Что дальше? Научим героя ходить.

Чтобы удобнее было работать, мы создадим для героя и других объектов функции движения и рисования.

К содержанию функции love.update(dt) допишем строку hero_movement(dt), я специально не создаю функции лично для каждого объекта, чтобы их могли использовать и другие объекты.

Теперь создадим саму функцию:

function hero_movement(dt)
    — вверх
    if love.keyboard.isDown(«up») or love.keyboard.isDown(«w») then — если нажата клавиша вверх или W
    hero.y = hero.y — hero.speed * dt — отнимаем от y скорость, умноженную на delta time
    end

— аналогично с остальными, я думаю, это должно быть понятно
    — вниз
    if love.keyboard.isDown(«down») or love.keyboard.isDown(«s») then
    hero.y = hero.y + hero.speed * dt
    end
    — влево
    if love.keyboard.isDown(«left») or love.keyboard.isDown(«a») then
    hero.x = hero.x — hero.speed * dt
    end
    — вправо
    if love.keyboard.isDown(«right») or love.keyboard.isDown(«d») then
    hero.x = hero.x + hero.speed * dt
    end

Теперь герой может двигаться. Но это не имеет значения, если его не видно. Поэтому в функцию love.draw() добавим строку hero_draw().

Это значит, что нам нужно создать функцию для рисования. Вообще, можно, конечно, всё это делать в love.draw(), но это неудобно.

love.graphics.setColor(255, 255, 255, 255) — определяем цвет для рисования (R, G, B, прозрачность)
love.graphics.rectangle(«fill», hero.x, hero.y, hero.w, hero.h) — рисуем прямоугольник на x, y героя с заданной в love.load() шириной и высотой и заполняем его «внутренности», используя режим «fill»

Вставляем в hero_draw() этот код. Теперь герой может ходить и его видно.

Теперь создадим друга. Прописываем friend_movement(dt) в love.update(dt), а friend_draw() в love.draw().

Теперь создадим функции.

Движение:

function friend_movement(dt)
    — герой взял к себе в компанию
    if friend.ai == 1 then
        — влево
        if friend.x > hero.x then — если находится правее, чем герой
        friend.x = friend.x — friend.speed * dt
        else
        — вправо, если находится левее, чем герой
        friend.x = friend.x + friend.speed * dt
        end
        — вверх
        if friend.y > hero.y then — если находится ниже, чем герой
        friend.y = friend.y — friend.speed * dt
        else
        — вниз, если находится выше, чем герой
        friend.y = friend.y + friend.speed * dt
        end
    end
end

Как получилось, что ai стал равным 1, расскажу чуточку позже, а пока мы напишем рисование спрайта для друга уже знакомыми вам функциями:

function friend_draw()
    love.graphics.setColor(0, 112, 255, 255)
    love.graphics.rectangle(«fill», friend.x, friend.y, friend.w, friend.h)
    love.graphics.setColor(255, 255, 255, 255)
    love.graphics.print(friend.ai, 0, 10)
end

Теперь герой может ходить, друг следовать за героем, но как же всё-таки ai стало равным 1?

После соприкосновения героя с другом, значение ai меняется на 1. Как это случилось? Создадим функцию для просчёта коллизий (столкновений):

function CheckCollision(ax1, ay1, aw, ah, bx1, by1, bw, bh)
    local ax2, ay2, bx2, by2 = ax1 + aw, ay1 + ah, bx1 + bw, by1 + bh
    return ax1 < bx2 and ax2 > bx1 and ay1 < by2 and ay2 > by1
end

Теперь допишем это к hero_movement(dt):

if CheckCollision(hero.x, hero.y, hero.w, hero.h, friend.x, friend.y, friend.w, friend.h) then
    friend.ai = 1
    end

Функция CheckCollision содержит 8 аргументов и вот, что они означают:

hero.x = x первого объекта

hero.y = y первого объекта

hero.w = ширина спрайта первого объекта

hero.h = высота спрайта первого объекта

Аналогично с вторым объектом (friend).

Для окончания основной части разработки такой игры нужно создать выход. Для него будет только событие рисования.

function quit_draw()
love.graphics.setColor(0, 0, 0, 255)
love.graphics.rectangle(«fill», quit.x, quit.y, quit.w, quit.h)
— выход
    if completed() then
    love.graphics.print(«You completed the level!», love.mouse.getX(), love.mouse.getY())
    end
end

Выглядеть оно будет так. Если уровень выполнен (друг взят), то объект выхода будет рисовать на x, y курсора текст. Как узнать, пройден ли уровень? Создадим функцию completed():

function completed()
    — выход
    return CheckCollision(hero.x, hero.y, hero.w, hero.h, quit.x, quit.y, quit.w, quit.h) and friend.ai and CheckCollision(friend.x, friend.y, friend.w, friend.h, quit.x, quit.y, quit.w, quit.h)
end

Она работает с помощью оператора return. Если всё правильно: друг стоит рядом с выходом, друг следует за ним и в данный момент стоит рядом с дверью, а не плетётся позади, то всё работает как нужно и return возвращает 1, сигнал, что всё правильно. Если что-то не так, то он возвращает 0.

Должно получиться что-то вроде этого:

Взглянуть на текущий код этой игры можно здесь.

Работает на 0.7.2

P.S. Наконец-то дописал эту статью и безумно рад, что день прошёл не зря. 🙂

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


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

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


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

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

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

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

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

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

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

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

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


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

функция love.update (dt)
- Код функций update и draw будут запускаться каждый кадр,
- чередуясь, в бесконечном цикле:
- "посчитали-> нарисовали-> посчитали-> нарисовали->"
- пока не будет вызван выход из приложения.
конец

функция love.draw ()
- Все функции взаимодействия с модулями фреймворка -
- аналогично прячутся внутри таблицы любви.
love.graphics.print ('Здравствуйте, дорогой пользователь Любви!', 100, 100)
конец
  

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

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


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

Мы хотим получить кораблик, крайне желательно и рулить.

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

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

  - Заранее инициализируем ссылки на имена классов, понадобятся,
- ибо вышестоящие классы будут использовать часть нижестоящих.
местный Корабль, Пуля, Астероид, Поле

Корабль = {}
- У всех таблиц, метатаблицей которых является корабль,
- дополнительные методы будут искаться в таблице корабля.
Корабль .__ index = Корабль

- Задаём общее поле для всех членов класса, для взаимодействия разных объектов
Корабль.type = 'корабль'

- Двоеточие - хитрый способ передать таблицу первым скрытым аргументом 'self'.
функция Корабль: новый (поле, x, y)
- Сюда, в себя, придёт таблица Корабль.

- Переопределяем себя на новый объект, как таблицу Корабль больше не понадобится.
self = setmetatable ({}, сам)

- Мы будем передавать ссылку на игровой менеджер, чтобы командовать им.
self.field = поле

- Координаты:
self.x = x или 100 - 100 - дефолт
self.y = y или 100

- Текущий угол поворота:
self.angle = 0

- И заполняем всё остальное:

- Вектор движения:
я.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}
- [[
Получится что-то такое, только чуть ровнее:
/ \
/ \
/ _ / \ _ \
]]

- Возвращаем свежеиспечёный объект.
вернуть себя
конец

функция Корабль: обновление (dt)
- Декрементов нема, и инкрементов тоже, но это не очень страшно, правда?
- dt - дельта времени, промежуток между предыдущим и текущим кадром.self.shoot_timer = self.shoot_timer - dt


- Управление:

- "Если зажата кнопка и таймер истёк" - спавним новую пулю.
если love.keyboard.isDown ('x') и self.shoot_timer <0, тогда
self.field: spawn (Bullet: new (self.field, self.x, self.y, self.angle))

- И сбрасываем таймер, потому что мы не хотим непрерывных струй из пуль,
- хоть это и забавно.
self.shoot_timer = self.shoot_delay
конец

если love.keyboard.isDown ('left'), то

- За секунду, сумма всех dt - почти ровно 1,
- соответственно, за секунду, кораблик повернётся на угол Pi,
- полный оборот - две секунды, все углы в радианах.self.angle = self.angle - self.rotation * dt
конец

если love.keyboard.isDown ('right'), то
self.angle = self.angle + self.rotation * dt
конец

если love.keyboard.isDown ('вверх'), то

- Вычисляем вектор ускорения, который мы приобрели за текущий кадр.
локальный vx_dt = math.cos (self.angle) * self.acceleration * dt
локальный vy_dt = math.sin (self.angle) * self.acceleration * dt

- Прибавляем к собственному вектору движения полученный.
self.vx = self.vx + vx_dt
self.vy = self.vy + vy_dt
конец

- Прибавляем к текущим координатам вектор движения за текущий кадр.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 ()

если self.x <0, то
self.x = self.x + screen_width
конец
если self.y <0, то
self.y = self.y + screen_height
конец
если self.x> screen_width, тогда
self.x = self.x - ширина_экрана
конец
если self.y> screen_height, тогда
self.y = self.y - screen_height
конец

конец

функция Корабль: 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)

- Рендерим вершины полигона, линия - контур, заливка - заполненный полигон.love.graphics.polygon ('линия', self.vertexes)

- И, наконец, возвращаем топологию в исходное состояние
- (перед love.graphics.push ()).
love.graphics.pop ()

- Это было слегка сложно,
- рисовать кружочки значительно проще:
- там можно прямо указать координаты, и сразу получить результат
- и так мы будем рисовать астероиды / пули.

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

- "Пушка! Они заряжают пушку! Зачем? А, они будут стрелять!"
- Мы тоже хотим стрелять.- Для стрельбы необходимы пули.
- Всё почти то же самое что у кораблика:

Пуля = {}
Bullet .__ index = Bullet

- Это - общие параметры для всех членов класса,
- пули летят с одинаковой скоростью и имеют один тип,
- поэтому удерживает это в класс:
Bullet.type = 'пуля'
Bullet.speed = 300

функция Bullet: new (поле, x, y, угол)
  self = setmetatable ({}, сам)

- Аналогично задаём параметры
self.field = поле
self.x = x
self.y = y
self.radius = 3

- время жизни
я.life_time = 5

- Нам надо бы вычислить
- вектор движения из угла поворота и скорости:
self.vx = math.cos (угол) * self.speed
self.vy = math.sin (угол) * self.speed
- Так как у объекта себя нет поля скорости,
- поиск продолжится в таблице под полем
- __index у метатаблицы

вернуть себя
конец

функция Bullet: update (dt)
- Управляем временем жизни:
self.life_time = self.life_time - dt

если self.life_time <0, тогда
- У нас пока нет такого метода,
- но это тоже неплохо.self.field: destroy (сам)
возвращение
конец

- Те же рекомендации
self.x = self.x + self.vx * dt
self.y = self.y + self.vy * dt

- Пулям тоже не стоит улетать за границы экрана
local screen_width, screen_height = love.graphics.getDimensions ()

если self.x <0, то
self.x = self.x + screen_width
конец
если self.y <0, то
self.y = self.y + screen_height
конец
если self.x> screen_width, тогда
self.x = self.x - ширина_экрана
конец
если self.y> screen_height, тогда
self.y = self.y - screen_height
конец
конец

функция Bullet: draw ()
любовь.graphics.setColor (255,255,255)

- Обещанная простая функция отрисовки.
- Полигоны, увы, так просто вращать не получится
love.graphics.circle ('заполнить', self.x, self.y, self.radius)
конец

- В кого стрелять? В мимопролетающие астероиды, конечно.
Астероид = {}
Астероид .__ index = Астероид
Asteroid.type = 'астероид'

функция Астероид: новый (поле, x, y, размер)
  self = setmetatable ({}, сам)

- Аналогично предыдущим классам.
- Можно было было бы провернуть наследование,
- но это может сложно быть для восприятия начинающих.self.field = поле
self.x = x
self.y = y

- Размерность астероида будет изменяться 1-N.
self.size = size или 3

- Векторы движения будут - случайными и неизменными.
self.vx = math.random (-20, 20)
self.vy = math.random (-20, 20)

self.radius = size * 15 - модификатор размера

- Тут вводится параметр здоровья,
- ибо астероид может принять несколько ударов
- прежде чем сломаться. Чуть рандомизируем для интереса.
- Чем жирнее астероид, тем жирнее он по ХП:
я.hp = размер + math.random (2)

- Пусть они будут ещё и разноцветными.
self.color = {math.random (255), math.random (255), math.random (255)}
вернуть себя
конец

- Тут выделяем его сложный метод, поэтому выделяем его отдельно
функция Астероид: applyDamage (dmg)

- если урон не указан - выставляем единицу
dmg = dmg или 1
self.hp = self.hp - 1
если self.hp <0, то
- Подсчёт очков - самое главное
self.field.score = self.field.score + self.size * 100
self.field: destroy (сам)
если self.size> 1, то
- Количество обломков слегка рандомизируем.2
расстояние возврата  screen_width, тогда
self.x = self.x - ширина_экрана
конец
если сам.y> screen_height, тогда
self.y = self.y - screen_height
конец
конец

функция Asteroid: draw ()
- Указываем текущий цвет астероида:
love.graphics.setColor (self.color)

- Полигоны, увы, так просто вращать не получится
love.graphics.circle ('линия', self.x, self.y, self.radius)
конец


- Наконец, пишем класс который соберёт всё воедино:

Поле = {}
Field.type = 'Поле'
- Это будет синглтон, создавать много игровых менеджеров, мы не собираемся,
- поэтому тут даже __index не нужен, потому что не будет объектов,
- которые ищут методы в этой таблице.- А вот инициализация / сброс параметров - очень даже пригодятся.
Поле функции: init ()
self.score = 0

- Таблица для всех объектов на поле
self.objects = {}

local ship = Корабль: новый (self, 100, 200)
печать (доставка)
я: spawn (корабль)
конец


Поле функции: spawn (объект)

- Это немного нестандартное применение словаря:
- в качестве ключа и значения указывается сам объект.
self.objects [объект] = объект
конец

Поле функции: уничтожить (объект)

- Зато просто удалять.
self.objects [объект] = ноль
конец

Поле функции: getObjects ()
вернуть себя.объекты
конец

Поле функции: обновление (dt)

- Мы хотим создать новые астероиды, когда все текущие сломаны.
- Сюда можно добавить любые игровые правила.
local asteroids_count = 0

для объекта в парах (self.objects) делаем
- Проверка на наличие метода
если object.update, то
объект: обновление (dt)
конец

если object.type == 'asteroid', то
asteroids_count = asteroids_count + 1
конец
конец

если asteroids_count == 0, то
для i = 1, 3 сделать
- Будем создавать новые на границах экрана
местный y = math.случайный (love.graphics.getHeight ())
self: spawn (Астероид: новый (self, 0, y, 3))
конец
конец
конец

Поле функции: draw ()
для объекта в парах (self.objects) делаем
если object.draw то
объект: draw ()
конец
конец
love.graphics.print ('\ n Оценка:' ..self.score)
конец


- Последние штрихи: добавляем наши классы и объекты в игровые циклы:

функция love.load ()
Поле: init ()
конец


функция love.update (dt)
Поле: обновление (dt)
конец

функция love.draw ()
Поле: draw ()
конец
  

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

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

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

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

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

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

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

Love Engine — дата выхода, описание, трейлер и скриншоты

Код для блога:

Код для форума:
[url = https: //gamedata.club/games/strategy/1769-love-engine.html] [img] https://gamedata.club/rating-601437d84c63b35709fe312ae82d8150.png [/ img] [ / url] Счётчик картинкой — Открыть счётчик

Дата выхода: 27 октября
Год: 2016
Жанр: Стратегия, Аркада
Поджанр: Головоломки, Инди
Платформа: ПК, iOS, Android
Мультиплеер: нет
Разработчик: Renxiang Game
Издатель: Renxiang Game
Сайт игры: отсутствует


Описание / сюжет

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

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

Здешняя история аргует вас до глубины души, равно как и чарующие мелодии, а также увлекательные ролевые элементы.


Скриншоты игры Love Engine


Трейлер

Геймплейное видео

Системные требования

ОС: Windows XP / Vista / 7/8 / 8.1 / 10
Процессор: Intel Core i3 2.5 ГГц
Опер. память: 1 Гб RAM
Видеокарта: Geforce FX 5200 (DX 9.0)
HDD / SSD: 500 Мб

Обнаружили ошибку?

Сообщите нам, выделив фрагмент мышкой и упомянутой CTRL + ENTER.
СПАСИБО!

.

Love Engine — обзоры и оценки игры, дата выхода DLC, трейлеры, описание

Love Engine — это романтическая игра-головоломка, лейтмотивом которой является любовь. Разработаны совершенно новые жанры игры-головоломки, в основе которых лежит история любви между мужчиной и женщиной. При помощи искусственного сочетания дизайна держащих в напряжении локаций, нестандартной и эстетичной графики, а также изысканного оригинального озвучивания и музыки в игре ведется повествование о силе Любви, позволяющее игрокам по мере развития сюжета за шаг полностью погружаться в эту атмосферу трогающей способности до души истории, наслаждаясь локациями, ролевой игрой и прекрасными сложными мелодиями.

ГРАФИКА

Минималистский дизайн вместе с нестандартной романтической графикой несут себе особое визуальное эстетическое наслаждение.

ЛОКАЦИИ

Всего в игре 5 глав и 30 локаций, для прохождения которых требуется с умом управлять джойстиком на экране.

МУЗЫКА

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

Платформы:

Дата выхода:

27 октября 2016

Дата выхода iOS:

27 октября 2016

Дата выхода ПК:

17 января 2017

Разработчики:

Издатели:

Теги:

казуальная игра, инди, для одного игрока, стратегия, инди, казуальная игра, стратегия

Оценка пользователей 0/10

.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *