Разбор проектов на Defold 9. Селектор уровня

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

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

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

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

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

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

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


Скриншот 3. Открываем разархивированную папку(наименование может отличаться от вашего названия скаченной папки)
Скриншот 4. Открываем папу с примерами.

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

Файловая структура проекта:

Рассмотрим атласы:

episode[1..4].atlas — добавлены фоновые изображения для каждого эпизода в отдельный атлас.
map.atlas — добавлены изображения деревьев, которые будут расставляться для каждого эпизода.

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

Создан игровой объект-курсор, который невидим для нашего глаза. При этом он сможет взаимодействовать с другими игровыми объектами, имеющий компонент “объект столкновений” (collision object). Можете добавить компонент “sprite” с изображением в игровой объект и запустить пример, вы увидите, что курсор может быть обладать визуальным представлением.
Этот игровой объект нам нужен, чтобы мы могли выбирать уровень и перемещаться по эпизодам игровой карты.

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

Этот игровой объект нужен для того, чтобы перейти на определенный уровень. Он также содержит компонент “объект столкновения”. Объект столкновения курсора взаимодействует с объектом столкновения маркера.
Метка отвечает за отображения номера уровня.

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


Эта загрузочная коллекция, содержит в себе игровой объект controller. Он отвечает за загрузку/выгрузку прокси-коллекций, переходом между картой. Скрипт реализует ту самую выгрузку/загрузку коллекций.

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

Коллекция, предназначенная для игрового уровня.
Содержит метку и скрипт.

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

Расставлены маркеры, фоновое изображение, изображения деревьев. Добавлен курсор для взаимодействия с маркерами. Скрипт эпизода, для перехода на соответствующий маркеру уровень.
С другими коллекциями episode всё также.

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


У нас имеется игровой объект controller, он позволяет прокручивать карту вверх-вниз.
Каждый эпизод (например, ep1, ep2…) — это collectionproxy, в котором размещены маркеры уровней.
Эпизоды привязаны к якорным объектам /ep1, /ep2, и т.д., для синхронизации их позиции.
Когда эпизод попадает в зону видимости (LOADED_THRESHOLD) — он загружается (async_load), когда выходит — выгружается (unload).
Пользователь может скроллить карту вверх/вниз, и уровни в эпизодах будут обновляться и доступны для выбора

Непонятно только то, зачем здесь фабрика? Объекты динамически не создаются :grinning_face_with_smiling_eyes:

Рассмотрим game.script:
function on_message(self, message_id, message, sender)
	if message_id == hash("play_level") then
		label.set_text("#label", tostring(message.level_id))
		print("playing level", message.level_id)
	end
end

Приходит сообщение "play_level", вместе с ним приходит и значение message.level_id, соответствующее выбранному уровню.
Устанавливается текстовое значение метке из message.level_id.

Рассмотрим cursor.script:
function init(self)
	msg.post(".", "acquire_input_focus")
end

function on_message(self, message_id, message, sender)
	if message_id == hash("trigger_response") then
		if message.enter then
			self.marker_id = message.other_id
		else
			self.marker_id = nil
		end
	end
end

В on_message(...) происходит обработка сообщений между объектами столкновений курсора и маркера.

Когда курсор входит в trigger (например, игрок входит в зону), движок отправляет сообщение trigger_response. В этом сообщении:

  • message.enter == true — объект вошёл в зону.
  • message.enter == false — объект вышел из зоны.

Также в message.other_id содержится список ID объектов, которые пересеклись с триггером.
Можете вывести print(message.enter).

triger_response относится к обработке столкновений (collisions), в частности, к событиям, вызываемым телами с типом trigger. Триггеры — это объекты, которые регистрируют простейшие явления. Они просто считывают, не взаимодействуют.

function on_input(self, action_id, action)
	go.set_position(vmath.vector3(action.x, action.y, 0))
	if action_id == hash("touch") and action.released and self.marker_id then
		msg.post("map:/controller", "marker_selected", { id = self.marker_id })
	end
end

Устанавливаем курсору положение, соответствующее входному вводу мыши. Из-за того, что в привязках ввода у нас стоит input и action в разделе мыши, скрипту есть что обрабатывать.
Если мы уберём привязку действий, то мы не сможем обрабатывать ввод мыши. Если мы уберём go.set_position(vmath.vector3(action.x, action.y, 0)), то курсор не будет двигаться в том же положение, что и ваша мышь на компьютере.

if action_id == hash("touch") and action.released and self.marker_id then
		msg.post("map:/controller", "marker_selected", { id = self.marker_id })
	end

Если игрок кликнул на мышь и отпустил кнопку, и курсор находится в области маркера, то передадим контроллеру сообщение, что уровень выбран, и с данными, какой был выбран уровень.

Рассмотрим controller.collection:
local MAP = msg.url("controller:/controller#mapproxy")
local GAME = msg.url("controller:/controller#gameproxy")

function init(self)
	msg.post(".", "acquire_input_focus")
	msg.post(MAP, "load")
end

function on_message(self, message_id, message, sender)
	if message_id == hash("show_game") then
		self.level_id = message.level_id
		msg.post(GAME, "load")
		msg.post(MAP, "unload")		
	elseif message_id == hash("show_map") then
		msg.post(MAP, "load")
		msg.post(GAME, "unload")		
	elseif message_id == hash("proxy_loaded") then
		print("proxy_loaded", sender)
		msg.post(sender, "enable")
		if sender == GAME then
			msg.post("game:/game", "play_level", { level_id = self.level_id })
		end
	elseif message_id == hash("proxy_unloaded") then
		print("proxy_unloaded", sender)
	end
end
local MAP = msg.url("controller:/controller#mapproxy")
local GAME = msg.url("controller:/controller#gameproxy")

Этот код создаёт локальные переменные MAP и GAME, в которые сохраняются URL-адреса к collectionproxy объектам внутри Defold. Эти переменные используются для указания на объекты #mapproxy и #gameproxy.

msg.post(MAP, load)

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

...
	if message_id == hash("show_game") then
		self.level_id = message.level_id
		msg.post(GAME, "load")
		msg.post(MAP, "unload")		
	elseif message_id == hash("show_map") then
		msg.post(MAP, "load")
		msg.post(GAME, "unload")
...

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

...
	elseif message_id == hash("proxy_loaded") then
		print("proxy_loaded", sender)
		msg.post(sender, "enable")
		if sender == GAME then
			msg.post("game:/game", "play_level", { level_id = self.level_id })
		end
	elseif message_id == hash("proxy_unloaded") then
		print("proxy_unloaded", sender)
	end
...

Если коллекция загружена, выводим сообщение в консоль print("proxy_loaded", sender). Если коллекция выгружается, то выводим сообщение в консоль print("proxy_unloaded", sender).

if sender == GAME then
		msg.post("game:/game", "play_level", { level_id = self.level_id })
	end

Отправим в коллекцию game, компоненту скрипт game сообщение. Помните, в game.script имеется обработка сообщения по изменению текста компоненту label ?

Рассмотрим episode.script:
go.property("episode", 1)

function init(self)
	self.target_pos = go.get_position()
	for i=1,5 do
		local marker_id = hash("/marker" .. i)
		local level_id = i + ((self.episode - 1) * 5)
		label.set_text(msg.url(nil, marker_id, "label"), tostring(level_id))
		msg.post("map:/controller", "register_marker", { level_id = level_id, marker_id = marker_id })
	end
end

function on_message(self, message_id, message, sender)
	if message_id == hash("update_position") then
		go.set_position(message.position)
	end
end
go.property("episode", 1)

Этот код определяет свойство объекта. С помощью этого кода можно будет задать номер эпизода. Это позволяет использовать один и тот же игровой объект с одним скриптом для разных уровней.

self.target_pos = go.get_position()

Сохраняет текущую позицию объекта как целевую target_pos.

...
for i=1,5 do
	local marker_id = hash("/marker" .. i)
	local level_id = i + ((self.episode - 1) * 5)
...

Цикл создаёт 5 маркеров.

marker_id — это ID вроде hash(“/marker1”), hash(“/marker2”) и т.д.

level_id рассчитывает номер уровня с учётом эпизода:
Если episode == 1, то уровни: 1–5.
Если episode == 2, то уровни: 6–10.

label.set_text(msg.url(nil, marker_id, "label"), tostring(level_id))
  • Устанавливает текст на компонент label, расположенный внутри каждого маркера.
  • msg.url(nil, marker_id, "label") — создаёт URL к label-компоненту маркера.
...
   msg.post("map:/controller", "register_marker", { level_id = level_id, marker_id = marker_id })
end
...

Отправляет сообщение "register_marker" на контроллер карты в map.collection, передаёт:

  • level_id — номер уровня.
  • marker_id — ID объекта-маркера.

Это нужно, чтобы контроллер карты знал, какие маркеры соответствуют каким уровням.

...
if message_id == hash("update_position") then
	go.set_position(message.position)
end
...

Обрабатывает сообщение “update_position”, чтобы переместить объект на новую позицию.
Коллекция эпизода будет устанавливаться на полученную позицию(якорь из другой коллекции).

Рассмотрим map.script:

Этот код представляет собой основной контроллер для прокручиваемой карты эпизодов в Defold, где каждый эпизод представлен как отдельная коллекция (collectionproxy). Он реализует динамическую загрузку/выгрузку эпизодов, обработку прокрутки, а также выбор уровней внутри эпизодов.

local episodes = {
	{
		proxy = msg.url("map:/episodes#ep1proxy"), markers = {},
	},
	{
		proxy = msg.url("map:/episodes#ep2proxy"), markers = {},
	},
	{
		proxy = msg.url("map:/episodes#ep3proxy"), markers = {},
	},
	{
		proxy = msg.url("map:/episodes#ep4proxy"), markers = {},
	},
}

episodes — таблица с информацией для эпизодов.

Каждый эпизод:

  • proxy — ссылка на collectionproxy эпизода.
  • markers — таблица маркеров уровней в эпизоде.
-- precalculate some links and ids
for i,episode in ipairs(episodes) do
	episode.id = i
	-- the anchor is the game object this episode should position itself as
	episode.anchor = hash("/ep" .. i)
	-- collection name (the socket part of a defold url) this episode belongs to
	episode.socket = hash("ep" .. i)
	-- url to the root game object of the episode
	episode.root = msg.url(episode.socket, "/bg", nil)
end

Дополнительные свойства к таблице добавляются через цикл:
for i, episode in ipairs(episodes):

  • id: порядковый номер.
  • anchor: якорный объект, например, /ep1, /ep2 — по нему определяется, где должен быть эпизод.
  • socket: используется в URL-адресе как имя коллекции.
  • root: URL к корневому GameObject эпизода (например, /bg), используется для обновления позиции.
local EPISODE_HEIGHT = 1920
local SCREEN_HEIGHT = sys.get_config("display.height")
local LOADED_THRESHOLD = (SCREEN_HEIGHT / 2) + EPISODE_HEIGHT * 1.5

EPISODE_HEIGHT — высота одного эпизода.
SCREEN_HEIGHT— высота экрана, указанного в game.project.
LOADED_THRESHOLD — вертикальный диапазон видимости: если эпизод попадает в него — он должен быть загружен.

SCREEN_HEIGHT

  • Это половина высоты экрана — от центра до верхнего или нижнего края.
  • Почему половина? Потому что якорь (episode.anchor) обычно находится в центре эпизода, и мы хотим сравнивать центр эпизода с краем видимой области.

EPISODE_HEIGHT * 1.5

  • Это буферная зона в 1.5 высоты эпизода.
  • Она нужна, чтобы начать загружать эпизод ещё до того, как он полностью войдёт на экран.
  • Это создаёт эффект предзагрузки, чтобы эпизод успел подгрузиться к тому моменту, когда пользователь до него доскроллит.

Без буфера:

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

С буфером:

  • Эпизод загружается заранее, когда он ещё чуть ниже или выше экрана.
  • Это даёт время на async_load() и proxy_loaded, чтобы загрузка прошла незаметно для игрока.

local function update_positions()
	for i,episode in pairs(episodes) do
		-- the episode will follow the anchor game object
		local position = go.get_world_position(episode.anchor) 
		-- update the position of the episode if it's loaded, unless we're unloading it
		if episode.loaded and not episode.unloading then
			msg.post(episode.root, "update_position", { position = position})
		end

		-- toggle loaded/unloaded state based on position of episode
		local should_be_loaded = position.y > -LOADED_THRESHOLD and position.y < LOADED_THRESHOLD
		if should_be_loaded and not episode.loaded and not episode.loading then
			load_episode(i)
		elseif not should_be_loaded and episode.loaded and not episode.unloading then
			unload_episode(i)
		end
	end
end

Этот фрагмент кода отвечает за управление загрузкой, выгрузкой и позиционированием эпизодов на карте, привязанных к якорным объектам (anchor). Он вызывается каждый кадр через update().

for i, episode in pairs(episodes) do
Перебирает все эпизоды (episodes) — таблицу, содержащую данные об эпизодах, включая proxy, anchor, loaded, и т.д.

local position = go.get_world_position(episode.anchor)

Получает мировую позицию якорного объекта (anchor) текущего эпизода.

Что такое “мировая позиция”?
В 2D или 3D играх у каждого объекта есть:
Локальная позиция (local position)

  • Это позиция относительно родителя.
  • Например, если объект находится на позиции (100, 0) в родителе, и родитель сам смещён, то итоговая позиция будет другая.

Мировая позиция (world position)

  • Это абсолютная позиция объекта в мире с учётом всех вложенностей и смещений родителей.

Якорь (episode.anchor) — это Game Object, к которому привязан эпизод. Он “ведёт” за собой коллекцию.
Для лучшего понимания я добавил к якорю спрайт(синяя полоска — это спрайт якоря):


Якорь расположен на позиции. На позицию которого будет загружена коллекция.

if episode.loaded and not episode.unloading then
    msg.post(episode.root, "update_position", { position = position })
end

Если эпизод уже загружен (episode.loaded == true) и не находится в процессе выгрузки:
Отправляется сообщение update_position в корневой Game Object (ep1:/bg) коллекции.
Это позволяет синхронизировать положение эпизода с якорем — например, при скролле экрана.

local should_be_loaded = position.y > -LOADED_THRESHOLD and position.y < LOADED_THRESHOLD

Вычисляется, находится ли эпизод в пределах экрана (или рядом):
Если position.y попадает в диапазон (-LOADED_THRESHOLD, +LOADED_THRESHOLD), значит эпизод должен быть загружен.
LOADED_THRESHOLD — заранее рассчитанный диапазон видимости (на основе высоты экрана и эпизода).

if should_be_loaded and not episode.loaded and not episode.loading then
	load_episode(i)

Если эпизод должен быть видим:
И он ещё не загружен (episode.loaded == false)
И в процессе загрузки он не находится (episode.loading == false), тогда вызывается load_episode(i), чтобы начать загрузку через async_load.

elseif not should_be_loaded and episode.loaded and not episode.unloading then
	unload_episode(i)
end

Если эпизод вышел за пределы видимости, и он:
Уже загружен.
Ещё не выгружается. Тогда вызывается unload_episode(i) для освобождения ресурсов.


function on_message(self, message_id, message, sender)
	if message_id == hash("register_marker") then
		local episode = find_episode_from_url(sender)
		episode.markers[message.marker_id] = message.level_id
	elseif message_id == hash("marker_selected") then
		local episode = find_episode_from_url(sender)
		local level_id = episode.markers[message.id]
		msg.post("controller:/controller", "show_game", { level_id = level_id })
	elseif message_id == hash("proxy_loaded") then
		print("loaded", sender)
		msg.post(sender, "enable")
		local episode = find_episode_from_proxy(sender)
		episode.loading = false
		episode.loaded = true
		msg.post(episode.root, "update_position", { position = go.get_world_position(episode.anchor), instant = true })
	elseif message_id == hash("proxy_unloaded") then
		print("unloaded", sender)
		local episode = find_episode_from_proxy(sender)
		episode.unloading = false
		episode.loaded = false
	end
end
...
if message_id == hash("register_marker") then
	local episode = find_episode_from_url(sender)
	episode.markers[message.marker_id] = message.level_id
...

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

elseif message_id == hash("marker_selected") then
	local episode = find_episode_from_url(sender)
	local level_id = episode.markers[message.id]
	msg.post("controller:/controller", "show_game", { level_id = level_id })

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

  • Определяется, из какого эпизода пришло нажатие (find_episode_from_url(sender)).
  • По message.id (это marker_id) находится нужный level_id.
  • Посылается сообщение "show_game" в контроллер, чтобы запустить нужный уровень.
elseif message_id == hash("proxy_loaded") then
	print("loaded", sender)
	msg.post(sender, "enable")
	local episode = find_episode_from_proxy(sender)
	episode.loading = false
	episode.loaded = true
	msg.post(episode.root, "update_position", { position = go.get_world_position(episode.anchor), instant = true })

Это системное сообщение от collectionproxy, когда загрузка эпизода завершена.
episode.loading = false, episode.loaded = true — обновляет флаги состояния.
Отправляется сообщение "update_position" в корень объекта эпизода, чтобы установить его позицию по якорю сразу после загрузки.
Это нужно, что бы правильно отобразить загруженный эпизод и синхронизировать его позицию.

elseif message_id == hash("proxy_unloaded") then
	print("unloaded", sender)
	local episode = find_episode_from_proxy(sender)
	episode.unloading = false
	episode.loaded = false

Это сообщение от collectionproxy, когда эпизод полностью выгружен.

  • Выводит отладку.
  • Снимает флаг unloading и ставит loaded = false.
    Это нужно, чтобы отметить, что эпизод выгружен, и его ресурсы освобождены.

function on_input(self, action_id, action)
	local action_pos = vmath.vector3(action.x, action.y, 0)
	go.set_position(action_pos)
	
	if action_id == hash("touch") then
		if action.pressed then
			self.pressed = true
			self.last_pos = action_pos
		end
		
		-- scroll episodes while left mouse button is pressed
		if self.pressed then

			-- scroll the episodes root
			-- don't scroll horizontally
			-- or vertically outside bounds
			self.episodes_pos = self.episodes_pos + (action_pos - self.last_pos)
			self.episodes_pos.x = 0
			self.episodes_pos.y = math.max(math.min(self.episodes_pos.y, 0), -(EPISODE_HEIGHT - SCREEN_HEIGHT + (#episodes - 1) * EPISODE_HEIGHT))

			self.last_pos = action_pos
		end		
		
		if action.released then
			self.pressed = false
		end
	end
end
local action_pos = vmath.vector3(action.x, action.y, 0)
go.set_position(action_pos)

action.x и action.y — координаты касания (или мыши).
Создаётся вектор action_pos, представляющий текущую точку ввода.
go.set_position(action_pos) — перемещает текущий Game Object(controller) в точку касания.

if action_id == hash("touch") then

Проверка: используется ли идентификатор "touch" (задан в input.bindings).
Это может быть как нажатие мыши, так и касание пальцем.

if action.pressed then
	self.pressed = true
	self.last_pos = action_pos
end

При первом нажатии (pressed == true):
Сохраняется, что пользователь начал ввод (self.pressed = true)
Запоминается позиция начала (self.last_pos), чтобы затем вычислять смещение.

if self.pressed then
	self.episodes_pos = self.episodes_pos + (action_pos - self.last_pos)

Пока пользователь держит палец/кнопку мыши:
Вычисляется смещение (разница между текущей и предыдущей позицией).
Это смещение добавляется к текущей позиции корневого объекта /episodes.

self.episodes_pos.x = 0

Горизонтальное движение запрещено (фиксируем X).

self.episodes_pos.y = math.max(
    math.min(self.episodes_pos.y, 0),
    -(EPISODE_HEIGHT - SCREEN_HEIGHT + (#episodes - 1) * EPISODE_HEIGHT)
)

Это ограничение на вертикальное смещение объекта /episodes, чтобы не дать пользователю проскроллить слишком далеко вверх или вниз.

self.last_pos = action_pos

Обновляем last_pos для следующего кадра.

if action.released then
	self.pressed = false
end

Когда пользователь отпустил экран/мышь — флаг pressed сбрасывается.

local function load_episode(id)
	local episode = episodes[id]
	episode.loading = true
	msg.post(episode.proxy, "async_load")
end

Из глобальной таблицы episodes выбирается эпизод с указанным id.
Устанавливается флаг loading, чтобы пометить, что эпизод начал загружаться.
Это защищает от повторной попытки загрузки, пока он ещё не закончил загружаться.
Отправляется сообщение async_load в collectionproxy, указанный в episode.proxy.
Это запускает асинхронную загрузку эпизода — коллекция начнёт загружаться в фоне.
Позже Defold отправит сообщение proxy_loaded, когда загрузка завершится.
Зачем это нужно?
Подгрузка эпизодов только тогда, когда они находятся в зоне видимости.
За счёт async_load ресурсы не грузятся все сразу, а по мере необходимости.
Флаг loading предотвращает дублирующую загрузку.


local function unload_episode(id)
	local episode = episodes[id]
	episode.unloading = true
	msg.post(episode.proxy, "unload")
end

Из глобальной таблицы episodes выбирается эпизод с указанным id.
Устанавливается флаг unloading, чтобы пометить, что эпизод сейчас выгружается.
Это используется для защиты: пока идёт выгрузка, повторная выгрузка не начнётся. Отправляет сообщение "unload" в соответствующий collectionproxy эпизода.
Это приводит к выгрузке коллекции — все игровые объекты внутри collectionproxy будут удалены, ресурсы освобождены


local function find_episode_from_proxy(proxy_url)
	for i = 1, #episodes do
		if episodes[i].proxy == proxy_url then
			return episodes[i]
		end
	end
end

for i = 1, #episodes do

  • Проходит по всей таблице episodes, в которой хранятся данные по каждому эпизоду.
  • #episodes — это количество эпизодов.

if episodes[i].proxy == proxy_url then — сравнивает текущий proxy из таблицы с тем, что был передан в аргументе proxy_url.

return episodes[i] — возвращает найденный эпизод как таблицу (с его флагами, якорем, root, markers и т.д.).

Зачем нужна эта функция?
Чтобы сопоставить URL компонента proxy с логической структурой эпизода.
Это позволяет обновить флаги episode.loading, episode.loaded и т.д., или управлять другими действиями (например, позиционированием).


local function find_episode_from_url(url)
	for i=1,#episodes do
		if episodes[i].socket == url.socket then
			return episodes[i]
		end
	end
end

Это вспомогательная функция, которая по любой ссылке msg.url на объект внутри коллекции (эпизода) находит, к какому эпизоду он принадлежит. Возвращает таблицу с данными эпизода.

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

Надеюсь, кому-нибудь этот материал будет полезен.
Всем спасибо за внимание :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. Фабрики и свойства [фабрики и свойства]
3 Likes