Разбор проектов на Defold 2. Параллакс

Всем привет :waving_hand:

В этом посте мы будем разбирать — Parallax.
Попробовать этот проект в действии: запустить пример.
Исходная папка с проектом: ссылка на github.

Скачиваем папку с примерами и запускаем проект

Если не скачан: скачиваем архив с примерами проектов из github.

Распаковываем скачанный ZIP архив примеров в любую папку по вашему усмотрению.
Открываем скачанный архив на стартовой странице редактора.
Запускаем проект на f5 или (crtl + B).

Переходим в папку examples.
Ищем проект parallax.
Открываем game.project.

Представь, что у нас есть игра, где задний фон состоит из нескольких слоёв: передний план, задний план и самый дальний фон. Эти слои могут двигаться, когда ты наклоняешь телефон или тянешь пальцем по экрану, или зажимая ЛКМ, перемещаешь мышь по экрану. Чтобы создать ощущение глубины, нам необходимо научиться создавать эффект параллакс. В этом примере мы разберёмся как воссоздать эту иллюзию глубины.

Первоначальный взгляд на проект

Ширина экрана меньше высоты экрана.
Изображения двигаются, когда мы зажимаем ЛКМ и перемещаем мышь по экрану, наклоняем телефон.
(Если вы соберёте ваш проект, например, под ОС Android, то перемещение по карте будет осуществляться в зависимости от наклона телефона).
Как собрать проект для Android или macOS:


Затем вам нужно будет отправить apk файл на свой смартфон и установить его.

Что такое акселерометр?

Акселерометр — это датчик внутри телефона, который измеряет, в каком направлении и под каким углом ты держишь телефон.
Он “говорит”:

  • Наклонён ли телефон вперёд/назад?
  • Повернут ли влево/вправо?
  • Как быстро ты его двигаешь?

Пример в играх:

  • Когда ты наклоняешь телефон влево, и машинка в игре едет влево.
  • Или в Defold — можно использовать акселерометр, чтобы двигать фон (как в этом примере с параллаксом).
Как устроена камера в этом проекте:

В Defold, как и в этом проекте камеры как объекта по умолчанию нет — она виртуальная и всегда смотрит на координаты (0, 0).
self.eye = vmath.vector3()
Камера расположена физически в центре экрана, а логически в self.eye.
Это не настоящая камера, а вектор смещения, который ты используешь для “движения мира”, а не самой камеры.
Как работает “камера” в этом коде:

  1. Камера как будто всегда стоит на месте.
  2. Когда ты меняешь self.eye, ты двигаешь не камеру, а все слои — чтобы казалось, что камера двигается.
  3. В update() ты берёшь self.eye, умножаешь на глубину слоя и сдвигаешь слой.
    То есть…
    Когда self.eye = (0, 0) — всё на месте.
    Когда self.eye.x = 50 — все слои сдвигаются влево, значит камера смотрит вправо.
Файловая структура проекта:

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

game.project


В файле game.project, в разделе Display можно изменить разрешение экрана.
Поменяйте разрешение экрана.
Что стало с проектом?
Что стало с изображениями в изменённом примере?

Руководство по game.project.

parallax.atlas

В атласе можно хранить несколько изображений.
Об атласе: перейти к официальному мануалу.

parallax.collection

parallax.collection включает в себя игровой объект layers, который будет отвечать за расположение игровых объектов в этой коллекции: back, far,foreground.
Отмечу то, что игровые объекты back, far, foreground создаются в этой коллекции и они не являются прототипами игровых объектов, в том смысле, что если мы создадим новую коллекцию, то мы не сможем добавить в новую коллекцию игровой объектback, far, foreground из parallax.collection. Для этого, нам нужно будет создать игровой объект в файловой структуре, а потом уже добавлять его в коллекцию. В предыдущем разобранном проекте tilemap collisions, enemy.go создавался именно по такому принципу, а потом уже добавлялся в коллекцию.

Попробуй

Удалите какой-либо объект из parallax.collection, например: foreground. Попробуйте создать чертёж игрового объекта foreground.go, добавив компонент спрайт. Добавьте этот игровой объект в parallax.collection. Что вы думаете насчёт этого способа создания игрового объекта? В каких случаях стоит применять такой метод создания игровых объектов?

Также, обратите внимание на то, что игровой объектfarсодержит два компонента спрайт.

Примерное описание работы скрипта:

Сначала идёт настройка: куда и насколько можно двигать “камеру”, и сколько данных акселерометра нужно хранить для плавности.

fun clamp:
Если значение слишком маленькое или большое — обрезает его. Нужно, чтобы камера не улетала слишком далеко.

init:
Сохраняем, где находятся слои по глубине (чем меньше z, тем “дальше”).

update:
Каждый слой сдвигается, основываясь на том, куда “смотрит глаз” (self.eye) и как глубоко расположен слой.

on_input:
Если телефон наклоняют:

  • Берём данные от акселерометра.
  • Нормализуем их (чтобы спокойно лежащий телефон давал 0).
  • Сохраняем в список.
  • Считаем среднее значение (для сглаживания).
    На основе среднего значения — двигаем “глаз” (камеру).

Ввода данных от тыкаем по экрану:

  • Если палец нажат — включаем перетаскивание.
  • Пока тянем — сдвигаем камеру.
  • В конце — ограничиваем координаты (clamp), чтобы не вылезло за пределы.
parallax.script

Реализуем идею:
Движение “камеры” (переменной self.eye) осуществляена основе акселерометра или тача. При этом движутся слои (foreground, back, far) с разным коэффициентом — это создаёт параллакс.

Задаём ограничение на перемещение камеры для параллакса и регулятор плавности:

С помощью go.property мы задаём пользовательские свойства игровому объекту через скрипт.

go.property(“имя”, значение_по_умолчанию)

go.property("left", -110)
go.property("right", 110)
go.property("top", 20)
go.property("bottom", -25)

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

Попробуй

Измени настройки и запусти проект, попробуй перемещаться по экрану. Что изменилось?

go.property("left", -510)
go.property("right", 510)
go.property("top", 500)
go.property("bottom", -500)

Это размер буфера, в котором хранятся последние 20 значений акселерометра. Используется для сглаживания (усреднения).

go.property("max_samples", 20)

В редакторе, вparallax.collection, в parallax.script появились новые свойства скрипта:

Суть функции clamp:

Убедись, что значение x находится в пределах от min до max. Например, если x = 999, min = 0, max = 100 — функция вернёт 100. Эта функция помогает ограничить “перемещение глаза”.

local function clamp(x, min, max)
	if x < min then x = min elseif x > max then x = max end
	return x
end
init

Эта строка — проверка, которая говорит:

Если self.max_samples меньше или равно 0 — останови игру и покажи ошибку.
(Проверка, чтобы буфер акселерометра не был нулевым или отрицательным).

assert(self.max_samples > 0, "Max samples must be 1 or larger")

Захватываем фокус ввода.

msg.post(".", "acquire_input_focus")

Отправляем сообщение в рендер-сцену, чтобы изменить цвет фона игры.

msg.post("@render:", "clear_color", { color = vmath.vector4(0.013, 0.173, 0.278, 1.000) })
Попробуй

Измени передаваемые RGB значения в color на: vmath.vector4(0.255, 0.128, 0.0, 1.000)
Что изменилось?

Создаём вектор self.eye со значением (0, 0, 0) — он будет использоваться как смещение камеры. Это ядро логики параллакса: куда “смотрит глаз”.

self.eye = vmath.vector3()

self.layers — таблица, где:

  • ключ — имя объекта ("foreground", "back", "far");
  • значение — его глубина по оси Z (z-координата).
    Это нужно, чтобы потом в update() применять разное смещение для каждого слоя в зависимости от его глубины. Чем дальше слой, тем меньше он двигается (эффект параллакса).
Разбор по шагам:
  1. { "foreground", "back", "far" } — это список имён игровых объектов, представляющих фоны на разных слоях.
  2. for _, id in pairs(...) — цикл, который перебирает каждое имя из этого списка:
  • id принимает значения: "foreground", потом "back", потом "far".
  1. go.get_position(id) — получаем позицию объекта с таким именем (позицию в 3D: x, y, z).
  2. .z — берём только координату глубины (z), потому что она важна для параллакса: чем дальше слой, тем слабее он должен двигаться.
  3. self.layers[id] = z — сохраняем глубину этого объекта в таблицу self.layers, где:
  • ключ — имя слоя ("foreground"),
  • значение — его z-координата.
self.layers = {}
for _,id in pairs({ "foreground", "back", "far" }) do
	self.layers[id] = go.get_position(id).z
end

Создаётся пустой список, в который будут добавляться последние значения акселерометра — для сглаживания движения.

self.samples = {}
update

Этот код вызывается каждый кадр. Он двигает объекты foreground, back, far в зависимости от того, куда “смотрит” виртуальная камера self.eye.

Обновляем позицию слоя.

function update(self, dt)
	--print(self.eye)
	for id,depth in pairs(self.layers) do
		local offset = self.eye * depth * depth
		local pos = go.get_position(id)
		pos.x = offset.x
		pos.y = offset.y
		go.set_position(pos, id)
	end
end
for id, depth in pairs(self.layers) do

Перебираем все слои из self.layers, где:

  • id — имя объекта ("foreground", "back", "far")
  • depth — значение z (глубина), взятое в init().
local offset = self.eye * depth * depth

Вычисляется смещение для этого слоя.
Чем глубже слой (depth больше), тем сильнее или слабее он двигается.
Умножение depth * depth создаёт нелинейный эффект: например, 0.1 → 0.01, 0.5 → 0.25.

local pos = go.get_position(id)

Берём текущую позицию слоя.

  • Все слои двигаются в зависимости от self.eye.
  • Чем глубже слой (меньше z), тем медленнее он двигается.
  • Это и создаёт эффект параллакса — ощущение глубины при движении.
pos.x = offset.x
pos.y = offset.y

Заменяем координаты X и Y на рассчитанное смещение (основанное на self.eye).

go.set_position(pos, id)
Попробуй

В parallax.collection:

  • К объекту, где находится controller.script, добавь компонент типа label.
  • Назови его, например, debug_label

Добавь в конец update(self, dt):

label.set_text("debug_label", string.format("eye: x=%.2f  y=%.2f", self.eye.x, self.eye.y))

label.set_text(id, текст) — обновляет текст у компонента label.
string.format("%.2f", value) — округляет до 2 знаков после запятой.

on_input

Если устройство передаёт координаты акселерометра:

if action.acc_x and action.acc_y and action.acc_z then

Делается перестановка осей (портретная ориентация).
Изначальное смещение (-0.7 по X) — это чтобы “вперёд” стало 0, когда телефон в нормальном положении. Просто, когда телефон лежит, он показывает положение приблизительно равное 0.7.

local acc = vmath.vector3(action.acc_y, action.acc_z, action.acc_x) - vmath.vector3(-0.7, 0, 0)

Ограничивает движение влево/вправо.
Делает значение от -1 до 1.

acc.x = clamp(acc.x, -0.3, 0.3) / 0.3

Добавляет новое значение в список.
Удаляет старое, если список стал длиннее max_samples.

table.insert(self.samples, acc)
if #self.samples > self.max_samples then
	table.remove(self.samples, 1)
end

Получаем усредненное(сглаженное) значение average:

local average = vmath.vector3(0)
for _,sample in ipairs(self.samples) do
	average.x = average.x + sample.x
	average.y = average.y + sample.y
	average.z = average.z + sample.z
end
	average.x = average.x / #self.samples
	average.y = average.y / #self.samples
	average.z = average.z / #self.samples

Мы берём только X и Z, потому что это наклон влево-вправо и вверх-вниз.

local horizontal = average.z
local vertical = average.x

Движение вверх/вниз: пересчитывается в координаты self.eye.y.

if vertical < 0 then
	self.eye.y = -vertical * self.top
else
	self.eye.y = vertical * self.bottom
end

Аналогично — движение влево/вправо.

if horizontal < 0 then
	self.eye.x = -horizontal * self.left
else
	self.eye.x = horizontal * self.right
end

Если пальцем дотронулись до экрана:

elseif action_id == hash("touch") then

Если pressed — начинаем перетаскивание.
Если released — заканчиваем.

if action.pressed then
	self.drag = true
	action.dx = 0
	action.dy = 0
elseif action.released then
	self.drag = false
end

Если тянем пальцем — меняем self.eye.

if self.drag then
	self.eye.x = self.eye.x + action.dx * 0.1
	self.eye.y = self.eye.y + action.dy * 0.1
end

Ограничиваем движение, чтобы не уехало за экран.

self.eye.x = clamp(self.eye.x, self.left, self.right)
self.eye.y = clamp(self.eye.y, self.bottom, self.top)

Надеюсь, кому-нибудь этот материал будет полезен.
Также, я буду рад получить обратную связь, чтобы в последующих постах я смог улучшить качество изложения/объяснений.
Всем спасибо за внимание :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]
4 Likes

А частицы будут?

Если только в будущем, т.к на данный момент я рассматриваю проекты britzl указанные на этой странице:

И вроде как, там нет примеров с применением частиц, но я могу ошибаться.