Разбор проектов на Defold 11. Coin Magnet (Магнит монет)

Всем привет :waving_hand:
Продолжаем серию разборов чужого кода с этой страницы: Публичный пример Defold.

Сегодня разбираем пример от britzlМагнит монет.
Попробуйте этот проект в действии: запустите пример.
Исходная папка с проектом: ссылка на github.
Исходная папка с публичными примера Defold на github: ссылка на github.

Обращение к новичкам:

Я предполагаю, что у вас уже установлен Defold, если нет, перейдите по этой ссылке: Добро пожаловать в Defold.
Также, будет плюсом, если вы хотя бы поверхностно знакомы со строительными блоками Defold. Если нет, ознакомиться с основными концепциями Defold можно на официальном сайте — перейдите по этой ссылке. 10.

Пример того, как скачать и открыть готовый проект:

Скачиваем архив с примерами проектов из github
Распаковываем скачанный ZIP архив примеров в любую папку во вашему усмотрению.
Переходим в папку examples.
Ищем проект play_animation.
Открываем game.project.

Внимание: скриншоты представлены ниже, это пример скачивания и открытия проекта, в этом примере мы рассматриваем проект с названием play_animation, потому название папок проекта будет отличаться.





Этот пример от britzl демонстрирует, как реализовать эффект «магнита», притягивающего монеты к кораблю.

Рассмотрим файловую структуру:

Рассмотрим powerup.go:


Игровой объект, содержащий спрайт и объект столкновения, маски и группы настроены на взаимодействие с player.

Тип объекта столкновения стоит как Kinematic, т.к этот объект не должен подчиняться полностью физическим силам и этот игровой объект будет управляться через скрипт. А вообще, можно установить тип Trigger.

Рассмотрим coin_magnet.collection:


Имеются три игровых объектов в качестве фона: bg1, bg2, bg3.
Игровой объект player, который содержит в себе визуальное представление(sprite), две фабрики, которые отвечают за создание монет и усилителей. Объекты столкновений для магнита и корабля. Скрипт для обработки логика игры.

Рассмотрим coin.go:


Всё аналогично предыдущему игровому объекту, за исключением того, что к маске добавляется и другой тег coinmagnet для магнита.

Рассмотрим coin_magnet.atlas:


Здесь хранятся все используемые изображения в проекте.

Рассмотрим coin_magnet.script:
-- Список имён игровых объектов с фонами
local backgrounds = { "bg1", "bg2", "bg3" }
-- Высота одного фонового изображения. Используется, чтобы фоны циклично повторялись(эффект бесконечного скроллинга).
local background_size = 256 * 3
-- Интервал появления монет (каждые 0.15 сек).
local coin_spawn_interval = 0.15
-- Интервал появления усилений(каждые 6 секунд)
local powerup_spawn_interval = 6
-- Сколько секунд работает магнит после активации
local magnet_lifetime = 10
Функция init(self):
-- захватывает ввод, чтобы можно было обрабатывать клавиши.
msg.post(".", "acquire_input_focus")
-- устанавливает начальное значение для случайных чисел
math.randomseed(os.time())
-- Скорость прокрутки фона, монет и усилений вниз
self.speed = 400

self.powerups = {}  -- список усилений на сцене
self.coins = {}     -- все монеты на сцене
self.coins_to_collect = {} -- монеты, которые магнит притягивает

-- Таймеры для спавна монет и усилений
self.coin_spawn_timer = coin_spawn_interval
self.powerup_spawn_timer = powerup_spawn_interval
-- Направление движения игрока (изменяется в on_input)
self.player_direction = vmath.vector3()
-- Таймер для работы магнита. Пока 0 — магнит не работает.
self.magnet_timer = 0
-- Выключаем магнит: он невидим и неактивен по умолчанию.
msg.post("coinmagnet", "disable")
msg.post("#coinmagnetcollisionobject", "disable")
Функция on_input(sefl, action_id, action):
  • Эта функция автоматически вызывается Defold’ом, когда игрок нажимает клавишу или совершает другое действие.
  • action_id — имя действия (например, "left", "right", "touch"), из input_binding.
  • action — таблица с данными о событии (нажата/отпущена, позиция и т.д.).
if action_id == hash("left") then

Проверка: было ли действие “left” (нажатие влево)?

if action.released then
	self.player_direction.x = 0
else
	self.player_direction.x = -1
end

Если кнопку отпустили — движение останавливается.
Если нажали и держим — двигаем игрока влево (по X со знаком минус).

elseif action_id == hash("right") then
	if action.released then
		self.player_direction.x = 0
	else
		self.player_direction.x = 1
	end

То же самое, но направление вправо (по оси X со знаком +1).

В update(self, dt) есть строка:

local player_pos = go.get_position() + self.player_direction * 200 * dt

То есть, переменная self.player_direction.x (−1, 0 или 1) умножается на скорость, и игрок перемещается влево, стоит на месте или вправо.

Функция update(sefl, dt):
-- move background
for _,bg in pairs(backgrounds) do
	local pos = go.get_position(bg)
	pos = pos + vmath.vector3(0, -self.speed * dt, 0)
	if pos.y <= -background_size then
		pos.y = pos.y + #backgrounds * background_size
	end
	go.set_position(pos, bg)
end

Этот код отвечает за движение фона вниз и создание эффекта бесконечного прокручивающегося фона.
В начале файла была создана таблица backgrounds, именно по этой таблице и мы шагаем в цикле. Каждый шаг (bg) — это имя игрового объекта, указанного в таблице backgrounds.

-- получаем текущую позицию фона bg
local pos = go.get_position(bg) 
-- смещаем фон вниз по оси Y, со скоростью self.speed. Умножение на dt (дельта времени) нужно для плавного движения независимо от FPS.
pos = pos + vmath.vector3(0, -self.speed * dt, 0)
     -- проверяем: если фон вышел ниже нижнего края экрана (например, у < -768), то пора переместить его вверх, чтобы он снова появился сверху
     if pos.y <= -background_size then
          -- фон перекидывается вверх на высоту всех фонов, создавая эффект   зацикливания (например, 3 фона по 768 —> перенос вверх на 2304 пикселя).
          pos.y = pos.y + #backgrounds * background_size 
     end
-- устанавливаем новую позицию для текущего фона bg
go.set_position(pos, bg)

Этот блок кода:

  • Плавно двигает все фоновые объекты вниз.
  • Когда фон выходит за экран снизу, он “телепортируется” обратно наверх.
  • В результате получается бесконечная вертикальная прокрутка.
-- проходим по всем монетам в таблице self.coins
-- coin — это ID Game Object монеты 
for coin,_ in pairs(self.coins) do
    -- получаем текущую позицию этой монеты
	local pos = go.get_position(coin)
    -- смещаем монету вниз по оси Y со скоростью self.speed
    -- dt — дельта времени, чтобы движение было плавным независимо от FPS
	pos = pos + vmath.vector3(0, -self.speed * dt, 0)
    -- устанавливаем новую позицию монеты
	go.set_position(pos, coin)
    -- если монета ушла слишком низко (за пределы экрана), она:
    -- удаляется как из сцены, так и из таблицы self.coins
	if pos.y <= -50 then
		go.delete(coin)
		self.coins[coin] = nil
	end
end

Этот блок кода:

  • Плавно двигает все монеты вниз.
  • Автоматически удаляет монеты, которые вышли за нижний край экрана, чтобы:
  • избежать утечек памяти,
  • не нагружать игру лишними объектами.
for powerup,_ in pairs(self.powerups) do
	local pos = go.get_position(powerup)
	pos = pos + vmath.vector3(0, -self.speed * dt, 0)
	go.set_position(pos, powerup)
	if pos.y <= -50 then
		go.delete(powerup)
		self.powerups[powerup] = nil
	end
end

Этот код делает то же самое для усиления(магнита), что и ранее объясненный код для монеты.

-- уменьшает таймер каждую секунду (-dt)
self.coin_spawn_timer = self.coin_spawn_timer - dt
if self.coin_spawn_timer <= 0 then
    -- сбрасывается в начальное значение
	self.coin_spawn_timer = coin_spawn_interval
    -- создаётся монета через фабрику на случайной позиции(вне экрана сверху)
	local id = factory.create("#coinfactory", vmath.vector3(math.random(20, 620), 1500, 0), nil, {}, 0.25)
    -- новый объект заносится в таблицу, чтобы отслеживать его
	self.coins[id] = true
end
self.powerup_spawn_timer = self.powerup_spawn_timer - dt
if self.powerup_spawn_timer <= 0 then
	self.powerup_spawn_timer = powerup_spawn_interval
	local id = factory.create("#powerupfactory", vmath.vector3(math.random(20, 620), 1500, 0))
	self.powerups[id] = true
end

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

local player_pos = go.get_position() + self.player_direction * 200 * dt
go.set_position(player_pos)
  • Получаем текущую позицию игрока.
  • Прибавляем направление движения (-1, 0, 1) по X, умноженное на скорость (200) и dt.
  • Перемещаем игрока по X (влево или вправо).

self.player_direction устанавливается в on_input() при нажатии клавиш.

for coin,_ in pairs(self.coins_to_collect) do
	local coin_pos = go.get_position(coin)
	local delta = player_pos - coin_pos
	go.set_position(coin_pos + delta * 5 * dt, coin)
	if vmath.length(delta) < 30 then
		go.delete(coin)
		self.coins_to_collect[coin] = nil
	end
end
  • Проходим по всем монетам, которые должны притягиваться магнитом (self.coins_to_collect).
  • Вычисляем вектор от монеты к игроку (delta).
  • Двигаем монету к игроку с небольшим множителем (плавное движение).
  • Если расстояние до игрока меньше 30 — удаляем монету (считаем, что игрок её “собрал”).

Может быть полезно: математика или Математика в Defold. Часть 1: Действия с векторами - #2 от пользователя vovs03

if self.magnet_timer > 0 then
	self.magnet_timer = self.magnet_timer - dt
	if self.magnet_timer <= 0 then
		msg.post("coinmagnet", "disable")
		msg.post("#coinmagnetcollisionobject", "disable")
	end
end
  • Если магнит активен (таймер > 0), уменьшаем таймер.
  • Когда он становится 0 или меньше:
  • Отключаем объект coinmagnet.
  • Отключаем его collision object, чтобы он больше не притягивал монеты.
Функция on_message(self, message_id, message, sender):

Эта функция вызывается каждый раз, когда объект получает сообщение. В данном случае — при столкновении (collision).

  • message_id: тип сообщения (например, "collision_response").
  • message: содержит данные о столкновении (с кем, какой группой и т.д.).
  • sender: кто прислал сообщение.
function on_message(self, message_id, message, sender)
    -- обрабатываем столкновения
	if message_id == hash("collision_response") then
        -- столкновение с монетой (если игрок столкнулся с монетой)
		if message.group == hash("coin") then
           -- удаляем монету из таблицы(больше не отслеживаем её)
			self.coins[message.other_id] = nil
           -- проверяем: работает ли магнит?
			if self.magnet_timer > 0 then
           -- добавляем монету в таблицу, чтобы она притягивалась к игроку
				self.coins_to_collect[message.other_id] = true
				-- отключаем у этой монеты компонент столкновения(чтобы больше не сталкивалась)
				local url = msg.url(message.other_id)
				url.fragment = "collisionobject"
				msg.post(url, "disable")
			else 
           -- если магнит не включён, просто удаляем моенту
				go.delete(message.other_id)
			end
           -- столкновение с усилителем(магнитом)
		elseif message.group == hash("powerup") then
           -- удаляем из списка и из игры
			self.powerups[message.other_id] = nil
			go.delete(message.other_id)
           -- активируем магнит на заданное количество секунд
			self.magnet_timer = magnet_lifetime
           -- включаем магнит и его коллизию — теперь он может притягивать монеты
			msg.post("coinmagnet", "enable")
			msg.post("#coinmagnetcollisionobject", "enable")
		end
	end
end

Надеюсь, кому-нибудь этот материал будет полезен.
Всем спасибо за внимание :light_blue_heart:

Другие разборы:
  1. Разбор проектов на Defold 1. Tilemap Collisions [tilemap]
  2. Разбор проектов на Defold 2. Параллакс [parallax]
  3. Разбор проектов на Defold 3. Движение игровых объектов [animation, movement, input]
  4. Разбор проектов на Defold 4. Воспроизвести анимацию [animation, movement, input]
  5. Разбор проектов на Defold 5. Меню и игра. Прокси-коллекции [proxy-collection, gameloop, collection, gui]
  6. Разбор проектов на Defold 6. Пауза [пауза]
  7. Разбор проектов на Defold 7. Простая кнопка [простая кнопка]
  8. Разбор проектов на Defold 8. Фабрики и свойства [фабрики и свойства]
  9. Разбор проектов на Defold 9. Селектор уровня [селектор уровня].
  10. Разбор проектов на Defold 10. Боец комбо [ввод и анимация]
  11. Разбор проектов на Defold 11. Coin Magnet (Магнит монет) [фабрика]
  12. Разбор проектов на Defold 12. Simple Ai (простой ИИ) [ИИ][AI]