Привет! Сегодня разберёмся, что такое массивы и структуры данных. Создадим сетку комнаты, научимся размещать объекты по этой сетке.
P.S. Передаю привет тому человеку, который ставит минусы на всё, что я пишу. Счастья тебе, здоровья.
Ссылки на предыдущие гайды:
Первый гайд
Второй гайд
Третий гайд
Четвертый гайд
Сегодня мы поговорим о следующем:
- Что такое массивы и для чего они нужны? Что такое структуры данных?
- Какие виды структур данных существуют в GMS? Их особенности.
- Реализуем сетку карты.
Что такое массивы и для чего они нужны?
Массив - это структура данных, которая хранит набор значений. Мы можем "взять" каждое такое значение, обратившись к массиву по индексу.
Индекс - это порядковый номер элемента в массиве.
Важно знать:
1. Индексы в массивах начинаются с нуля.
2. В массив можно запихнуть массив, в него запихнуть массив и так до бесконечности. Отсюда следует:
2.1. Массив называется одномерным, если в него не вложены другие массивы. Если в массив вложен другой массив, то это уже двумерный массив. Если в массиве есть массив с массивом - трёхмерный. И так далее.
Массив - структура данных. Что такое структура данных?
Структура данных - это "контейнер", который хранит данные в определённом формате. Соответственно, массив - один форматов хранения данных.
Какие виды структур данных существуют в GMS? Их особенности.
Первый структура данных, которую мы разберём называется Array (массив). Его можно создать несколькими способами:
1.array = array_create(10, noone)
2. array = [1, 2, 3, 4] // внутри пишем любые свои значения
3.
array[0] = 1
array[1] = 2
И так далее. Также можно объявить сколько угодно мерный массив:
array[0][0][0][0] = 1.
Такие массивы преспокойно встраиваются в сохранение стандартными средствами GMS. Также здесь в автономном режиме работает "сборщик мусора". И это важно, поскольку в структурах данных (приписка DS) этого нет. Соответственно, их придётся удалять вручную.
Часто в мануале говорится, что лучше использовать именно array, а не другие аналогичные структуры, так как меньше шанс "выстрелить себе в ногу". Впрочем, если покурить тот же мануал, то использовать DS можно вполне спокойно, выигрывая при этом в скорости работы программы.
Structs или структуры.
По сути, тот же массив, но вместо индексов - у нас именованные значения. Структуры используются для упорядоченного хранения информации. Пример объявления структуры из мануала:
// Create event
mystruct =
{
pos_x : x,
pos_y : y,
count : 1000
};
// Clean Up event
delete mystruct;
Таким образом получается, что левое значение у нас выступает в качестве хранилища для какого-то значения, которое будет записано справа через двоеточие.
Обращение к struct для вычленения из него данных похоже на таковое для объектов. Нам нужно назвать структуру, а затем через точку - нужную нам "переменную".
show_debug_message(mystruct.pos_x)
С-но, в каждом значении структуры можно хранить другое значение. Напоминает HTML-код.
mystruct =
{
a :
{
aa : "Example"
},
b :
{
bb : "Another"
},
};
В таком случае, нам нужно будет обратиться к структуре, к значению, а затем к значению внутри значения.
show_debug_message(mystruct.a.aa)
Также для простоты к структуре можно обращаться через with
with(mystruct)
{
a += other.x;
}
Ну и последнее, что стоит сказать. Мы можем использовать функции-конструкторы. Они позволяют создавать новые структуры по шаблону, который будет описан в такой функции.
// Функция-конструктор.
function Vector2(_x, _y) constructor
{
x = _x;
y = _y;
static Add = function(_vec2)
{
x += _vec2.x;
y += _vec2.y;
}
}
// создаем новую структуру
v2 = new Vector2(10, 10);
Важное правило!!!
Хотите избежать утечек памяти - всегда удаляйте все структуры данных, когда они не нужны.
DS Grids или сетки.
Формально - это двумерный список. Позволяет, как ни странно, создавать сетку и назначать каждой клетке какое-либо числовое значение.
Для хранения позиций клетки всегда следует использовать целые значения. Нецелые будут автоматически округлены движком, что может быть непредсказуемо.
Как создать:
переменная = ds_grid_create(ширина в клетках, высота в клетках)
Ещё раз обращаю внимание, что с помощью
DS Grid мы можем назначить каждой клетке
отдельное значение. В случае с созданием своего алгоритма поиска путей это означает, что в зависимости от типа местности, алгоритм будет находить наиболее оптимальный путь опираясь на итоговую стоимость пути. Просто как пример.
А ещё сетка используется для создания тех самых "клеток", по которым идёт расположение построек, к примеру.
DS Lists или списки.
По своей сути схож с array. Для списка не важен порядок загрузки и выгрузки элементов. Для работы со списками по умолчанию больше инструментов, чем для работой с array. Например, встроенная функция для поиска какого-либо значения в списке, чего у array по умолчанию нет.
Применять нужно в тех местах, где важна скорость работы и неизвестно конечное число элементов. В ином случае лучше применять array.
переменная = ds_list_create();
Вставить новое значение:
ds_list_add(переменная, значение)
DS Maps или словари.
Словарь по своей сути похож на struct: он хранит данные в паре "ключ : значение". Если представить это образно, то это две колонки. Первая - это ключ. Второе - значение. Обращаясь к первой колонке, к ключу, мы можем спокойно получить значение. Или получить все ключи по определённому значению.
Мы будем их использовать в следующем гайде, когда будем разбирать пути и будем писать свои функции для их создания.
Способ создания:
переменная = ds_map_create()
Вставить новое значение:
ds_map_add(переменная, ключ, значение)
Мануал рекомендует использовать вместо DS Maps - структуры. Собственно, с ними нам тоже придётся поработать.
DS Queues, DS Priority Queues, DS Stacks
Всё это - "очереди", только по разному работающие. Разберём сначала Stacks и Queues.
Stacks это структура типа LIFO (last-in-first-out / последним пришёл - первым ушёл). Как и со списками, мы можем "вычленять" последние значения из стаков с помощь команды pop. Особенность LIFO в том, что в таком случае мы получим последнее значение, которое загружали в список.
С Queue ситуация обратная. Там используется FIFO (first-in-first-out / первым пришел - первым ушёл). То есть, забирая значение из queue (очередь), мы получим первое значение, которое вставили.
Важная оговорка. pop - берёт значение из массива, при этом удаляя его из него.
Priority Queue или приоритетная очередь - это та же очередь, но в которой каждое значение имеет свой "вес" - приоритет, что влияет на расположение данных в структуре и позволяет отбирать, скажем, наиболее дешёвые пути. Как говорит мануал, полезно для создания таблиц лидеров или информационных списков. Мы же будем это использовать для написания алгоритма поиска путей А* (A star, А звездочка).
Хоть и очень кратко, поверхностно, но мы рассмотрели структуры данных, с которыми будем или не будем работать. В последствии, когда будем их применять, я уже более подробно остановлюсь на том, почему был выбран тот или иной вид структур данных.
Создаём сетку комнаты.
1. У объекта oGameManager пропишем две дополнительные макро переменные, которые будут хранить ширину и высоту одной клетки.
#macro CellWidth 32
#macro CellHeight 32
2. Создадим объект oGrid, который расположим в нашей основной комнате.
3. У этого объекта создадим событие Create, где будем создавать нашу сетку.
3.1. Для наглядности, создадим сетку с помощью команды mp_grid_create
mp в данном случае - это "motion planning" - планирование движения. Система, которая позволяет создать сетку комнаты и сразу же по ней создавать пути, используя встроенные алгоритмы. Они не дают всего необходимого функционала, поэтому этот пункт выполняем исключительно для наглядности.
global.grid = mp_grid_create(0, 0, ceil(room_width / CellWidth), ceil(room_height / CellHeight), CellWidth, CellHeight)
3.1.1. Перейдём в событие Draw, где отрисуем сетку.
draw_set_alpha(0.1)
mp_grid_draw(global.grid)
draw_set_alpha(1)
3.1.2. Зайдём в игру и посмотрим, что получилось.
Как видно, у нас получилась квадратная сетка. Используя функции mp_grid_* мы можем закрашивать нашу сетку, делая определённые квадраты непроходимыми. Но мы не можем назначать таким клеткам свою стоимость.
Иными словами, если будет два одинаковых по длине пути, но один через условные зыбучие пески, а второй по асфальту, то получим 50/50, что персонаж выберет медленный и потенциально опасный путь, просто потому что мы не можем указать, какой из путей будет приоритетнее.
Подробнее о том, когда и какие функции поиска путей использовать, мы будем говорить в следующем гайде. Продолжим.
3.2. Удаляем или комментируем то, что мы написали. Сейчас будем работать со структурами данных.
3.2.1. Создаём сетку через ds_grid_create в событии create
var cells_x = ceil(room_width / CellWidth)
var cells_y = ceil(room_height / CellHeight)
global.grid = ds_grid_create(cells_x, cells_y)
Ок. По сути, сетка готова. Теперь по ней мы можем создавать объекты. Давайте же заполним всю нашу комнату oDirt, а дальше уже будем разбираться.
3.2.2. Всё там же в Create нам нужно пройтись циклом по осям x, y, создавая объект oDirt по указанным координатам. Затем мы передадим получившиеся координаты, в клетках, в переменные внутри объектов, для удобства.
for (var _y = 0; _y < cells_y; ++_y)
{
for (var _x = 0; _x < cells_x; ++_x)
{
ds_grid_set(global.grid, c, u, 0) // 0 - это стоимость клетки. Ноль - значит непроходима.
var inst = instance_create_layer(_x * CellWidth + 16, _y * CellHeight + 16, "Instances", oDirt)
inst.coordx = _x
inst.coordy = _y
}
}
Зачем при создании объекта мы плюсовали 16 пикселей?
Напомню, что точка origin - отсчёта - у наших
объектов находится в центре их спрайтов. Следовательно, располагая объект по координатам (предположим, (0, 0)), часть объекта будет выходить за границы комнаты.
3.2.3. После запуска всё работает. Проверим координаты каждой клетки. Для этого вернёмся к игроку и в событии Draw GUI сделаем проверку: если позиции мыши по x,y (относительно комнаты) содержит в себе oObject, то выводим на экран coordx и coordy.
if instance_position(mouse_x, mouse_y, oObject)
{
var inst = instance_position(mouse_x, mouse_y, oObject)
scrOutlinedText(dmxg + 10, dmyg + 10, c_white, c_black, string(inst.coordx) + " " + string(inst.coordy), depth, fontArialRusSmall, fa_left, fa_top)
}
Проверяем.
Работает! Впрочем, это только первая часть того, что нам нужно, не так ли? Помимо создания карты, мы должны иметь возможность располагать объекты самостоятельно, удалять их при необходимости. Да и с расположением земли не всё так гладко, как хотелось бы. Мы ведь совсем не добавили фон!
Начнём с конца, ибо это, на самом-то деле, достаточно просто.
У нас с вами есть два пути.
1. Мы создаём слой для спрайтов, на котором у нас будут спрайты фона.
2. Мы создаём для каждого нашего объекта "переднего плана" дубликат для заднего фона. По сути, увеличиваем количество объектов в комнате в два раза, соответственно увеличивая и нагрузку.
Работать мы будем с первым вариантом по той причине, что он будет работать быстрее, а также предоставляет нам весь необходимый функционал, потому и смысла во втором варианте нет.
Первый вариант, несмотря на преимущество в скорости работы, имеет существенный минус. Мы не сможем обращаться к спрайтам по их координатам, да и в целом будем сильно ограничены в работе с ними. Придётся хранить ID каждого отрисованного нами спрайта. Для этого нам придётся создать вторую переменную, которая будет хранить координаты клеток (по x, y), а также id спрайта.
Так как мы знаем размер одной клетки, мы можем легко получить индекс той клетки, на которую сейчас наведены. Следовательно, можем получить и ID текущего спрайта - удалить его или назначить новый.
Получаем порядковый номер клетки. Для этого берём координату x текущей клетки, к ней плюсуем произведение текущей координаты y и максимального размера сетки по x. Т.Е. формула:
cell_id = x + y * max_x
В коде же это будет выглядеть следующим образом:
var cell_id = mouse_x div CellWidth + mouse_y div CellHeight * ceil(room_width / CellWidth)
Итак, работаем.
1. Создаём новый слой - Asset Layer, который назовём Backs.
2. Перейдём к объекту oGrid.
3. Добавим ещё одну глобальную переменную сразу после объявления сетки - global.grid_array = []
4. Чуток допишем цикл. Сделаем так, чтобы в список у нас добавлялись значения x и y.
var cells_x = ceil(room_width / CellWidth)
var cells_y = ceil(room_height / CellHeight)
global.grid = ds_grid_create(cells_x, cells_y)
global.grid_array = []
for (var _y = 0; _y < cells_y; ++_y)
{
for (var _x = 0; _x < cells_x; ++_x)
{
ds_grid_set(global.grid, _x, _y, 0) // 0 - это стоимость клетки. Ноль - значит непроходима.
var inst = instance_create_layer(_x * CellWidth + 16, _y * CellHeight + 16, "Instances", oDirt)
inst.coordx = _x
inst.coordy = _y
array_push(global.grid_array, [_x, _y])
}
}
5. Ок. Осталось ставить спрайты на "фон". Мы могли бы делать это и в цикле, при создании объектов, но зачем нам лишняя нагрузка? Правильно, незачем. Перейдём к объекту oDirt и у него создадим событие Destroy. Этот код будет выполняться перед уничтожением объекта.
С-но, алгоритм прост. Рисуем спрайт, сохраняя его ID. Узнаём индекс нужного нам массива и в него вставляем ID нарисованного спрайта.
var back = layer_sprite_create("Backs", x - 16, y - 16, sDirtBG)
var cell_id = coordx + coordy * ceil(room_width / CellWidth)
array_push(global.grid_array[cell_id], back)
ds_grid_set(global.grid, coordx, coordy, 1) // Делаем проходимым
6. Чтобы проверить, что всё работает, перейдём к игроку и сделаем так, чтобы по нажатию на кнопку мыши, у нас удалялся объект. Это пишется в step.
if instance_position(mouse_x, mouse_y, oObject)
and mouse_check_button_pressed(mb_right)
{
var inst = instance_position(mouse_x, mouse_y, oObject)
with(inst)
{
instance_destroy();
}
}
Как видим, фон рисуется исправно. Так как мы сохранили ID спрайта, мы можем к нему обратиться и в любой момент можем его удалить. Впрочем, последнее сейчас нам нужно исключительно в целях отладки нашего проекта.
По аналогии, мы можем размещать объекты, но я бы предпочёл сразу сделать систему, которая позволит нам ставить любые объекты, которые мы захотим.
Если упрощать, то нам нужно сделать следующие шаги, чтобы данную систему реализовать в полной мере.
1) Реализовать глобальную переменную, чтобы мы могли отслеживать событие: открыто сейчас какое-либо меню или нет. Простой переключатель, который будет закрывать старое меню при переключении на новое.
2) Сделать родительский объект или функцию для всех меню, так как логика взаимодействия у нас не отличается.
3) Расписать отдельно логику для каждого пункта. Конкретно сейчас, нам нужно таким образом реализовать меню строительства.
Теперь о самом меню строительства.
При активации оно должно выводить список всех возможных к постройке объектов. Следовательно, самое оптимальное здесь - использовать массив, в который мы передадим все эти объекты. Желательно заранее предусмотреть систему, которая убережёт нас от ситуаций, когда интерфейс уходит за границы экрана.
После того, как будет готов вывод объектов - нужно будет настроить логику взаимодействия с этими объектами. Выглядеть оно должно следующим образом:
- Нажали ЛКМ по объекту - объект, на который мы сейчас наведены, стал активным. Если был иной объект - он заменяется на новый.
- Нажали ПКМ где угодно - выбранный объект сбросился.
Если же нажали ЛКМ по игровому полю - то поставили активный сейчас объект, удалив при этом старый.
Для удобства также желательно отрисовывать спрайт активного объекта под курсором мыши.
Непосредственно реализация.
Начнём с того, что заблокируем наш пользовательский интерфейс на нужных нам значениях. Пусть это будет FHD. Тогда, в объекте oGameManager, в Create, нужно будет добавить одну строку кода:
display_set_gui_size(1920, 1080);
Её же нужно будет убрать в
oSSize.
Всё, теперь наш GUI заблокирован на FHD. Даже если размер окна меньше или больше, координаты мыши будут подстраиваться под данные значения.
Также это означает, что наш пользовательский интерфейс не будет скакать при изменении размеров окна, следовательно - не нужно париться над его перерисовкой.
Дальше. Сделаем само меню и разместим его.
Нового здесь ничего нет, поэтому повторяться не буду. Всего это шесть новых объектов.
В oGameManager пропишем переменную-тумблер:
global.placing = noone
Переменную "активности" кнопки нам следует прописать у каждого объекта-меню, чтобы мы могли отслеживать, какой именно из объектов сейчас рисуется.
Теперь логика меню. Возьмём за основу кнопку "Строительство".
Сделаем список из нескольких объектов, которые мы бы хотели выводить в списке. Дополним их названиями, так как они нам пригодятся позже.
object_list = [["Земля", oDirt], ["Камень", oRock], ["Металл", oMetal], ["Уголь", oCoal]]
Реализовывать кнопки мы будем через отдельный объект -
oCell.
Для этого, его нужно создать. Задать ему спрайт в виде квадрата. Сделать так, чтобы он отрисовывался в событии GUI и при этом хранил следующие значения:
Объект, который он должен рисовать, название этого объекта, какое сейчас меню открыто.
object_name = noone
object_to_draw = noone
menu = noone
Вернёмся к объекту меню строительства.
Код в step:
dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if instance_position(dmxg, dmyg, self) and mouse_check_button_pressed(mb_left)
{
// Если не активно - делаем активным
if !active
{
active = !active
global.placing = "Building";
var startx = x + sprite_width * 2 + 5
var starty = y - sprite_height
for (var i = 0; i < array_length(object_list); ++i)
{
var inst = instance_create_layer(startx + 64 * i + 5, starty, "Instances", oCell)
inst.object_name = object_list[i][0]
inst.object_to_draw = object_list[i][1]
inst.menu = "Building"
}
}
else
{
active = !active
global.placing = noone
}
}
Теперь у
oCell:
step:
if menu != noone and global.placing != menu
{
instance_destroy();
}
Draw GUI:
draw_self()
if object_to_draw != noone
{
draw_sprite(object_get_sprite(object_to_draw), 0, x + 32, y + 32)
}
Результат:
Это только часть того, что нам нужно, верно?
Во-первых, мы не выводим название. Делать мы это будем отдельно, конечно же.
Во-вторых, у нас нет ограничения для объектов по оси X, а по оси Y они не двигаются вообще.
Начнём с первого, так как это просто. Выводить название объекта мы будем при наведении на него.
Для этого дополним Draw GUI у oCell. Если мы наведены на объект - нарисовать чёрный полупрозрачный прямоугольник и поверх него текст для лучшей отчётливости. И немного настроим "глубину", чтобы не было перекрытия этого текста.
if object_to_draw != noone
{
draw_sprite(object_get_sprite(object_to_draw), 0, x + 32, y + 32)
if instance_position(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), self)
{
depth = -1000
draw_set_font(fontButtonText20)
var width = string_width(object_name)
var height = string_height(object_name)
draw_set_alpha(0.33)
draw_set_color(c_black)
draw_rectangle(x, y - height, x + width, y, false)
draw_set_color(c_white)
draw_set_alpha(1)
scrOutlinedText(x, y - height, c_white, c_black, object_name, depth, fontButtonText20, fa_left, fa_top)
}
else
{
depth = 0
}
}
Второе.
Вернёмся к объекту меню строительства.
Так как мы знаем конечное количество объектов, мы можем посчитать, какое количество пикселей они будут занимать. Всё, что нам остаётся - определиться с количеством объектов на одну строку. Я предлагаю 10.
Мы не хотим, чтобы эти объекты создавались ниже определённой границы, потому точку старта по Y будем считать относительно неё. В общем, нужно будет заменить всего пару строк при создании объектов.
var starty = display_get_gui_height() - 64 - 64 * (array_length(object_list) div 10)
И
var inst = instance_create_layer(startx + 64 * (i mod 10) + 5, starty + (i div 10) * 64, "Instances", oCell)
Конечный результат меню:
Теперь логика взаимодействия и размещения.
Нам нужно завести ещё одну переменную, которая будет хранить объект, который мы хотим использовать. Иными словами, при клике на объект oCell, мы должны из него "вычленять" тот объект, который он хранит. Как следствие, получим спрайт, который будем отрисовывать в качестве "призрака".
Сделаем же это. Для этого у объекта oPlayer пропишем переменную:
place_object = noone;
Дальше - просто. В oCell в Step делаем проверку. Если нажали на себя - oPlayer.place_object = object_to_draw;
Код:
if instance_position(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), self)
and mouse_check_button_pressed(mb_left)
{
oPlayer.place_object = object_to_draw;
mouse_clear(mb_left)
}
Теперь у oPlayer настроим отрисовку объекта. Для этого в draw пропишем код:
if place_object != noone
{
var cell_x = mouse_x div CellWidth * CellWidth
var cell_y = mouse_y div CellHeight * CellHeight
draw_sprite(object_get_sprite(place_object), 0, cell_x + 16, cell_y + 16)
// Обводочка по границам объекта, для удобства.
draw_set_color(c_white)
draw_rectangle(cell_x, cell_y, cell_x + 32, cell_y + 32, true)
}
Результат:
Может выглядеть кривовато, но это из-за спрайта земли. Проверял на простых квадратах - всё встаёт четко в границы. :)
Всё, что осталось - настройка логики.
Нажали ЛКМ - разместили объект. При этом, фон нам нужно очистить в любом случае. Нажали ПКМ - удалили объект, на который наведены. Если объекта нет, то удаляем фон.
Естественно, всё это сейчас - в целях теста. В дальнейшем мы будем ставить не сами объекты, а их призраки, которые человечки будут строить. В общем, целиком код выглядит так:
Step у oPlayer:
var cell_x = mouse_x div CellWidth
var cell_y = mouse_y div CellHeight
var cell = cell_x + cell_y * ceil(room_width / CellWidth)
if mouse_check_button_pressed(mb_right)
{
// Сбрасываем объект или удаляем объект/фон
if place_object != noone
{
place_object = noone
}
else
{
// Если в текущей позиции нет объекта - удаляем фон, иначе - объект
var inst = instance_position(cell_x * CellWidth, cell_y * CellHeight, oObject)
if inst
{
with(inst)
{
instance_destroy();
}
}
else
{
if array_length(global.grid_array[cell]) > 2
{
layer_sprite_destroy(global.grid_array[cell][2])
array_delete(global.grid_array[cell], 2, 1)
}
}
}
mouse_clear(mb_right)
}
if mouse_check_button_pressed(mb_left)
{
var place_x = mouse_x div CellWidth * CellWidth + 16
var place_y = mouse_y div CellHeight * CellHeight + 16
if place_object != noone
and !instance_position(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), oGBuild)
{
// Удаляем объект, если находим.
var inst = instance_position(cell_x * CellWidth, cell_y * CellHeight, oObject)
if inst
{
with(inst)
{
instance_destroy()
}
}
// Удаляем фон
if array_length(global.grid_array[cell]) > 2
{
layer_sprite_destroy(global.grid_array[cell][2])
array_delete(global.grid_array[cell], 2, 1)
ds_grid_set(global.grid, cell_x, cell_y, 0)
}
// Ставим новый объект
var new_inst = instance_create_layer(place_x, place_y, "Instances", place_object)
new_inst.coordx = mouse_x div CellWidth
new_inst.coordy = mouse_y div CellHeight
mouse_clear(mb_left)
}
}
Собственно говоря, система размещения объектов сделана. Осталось её дорабатывать напильником.
Результат:
Так как в следующем гайде мы будем активно работать с персонажами, будем заставлять их двигаться по путям, предлагаю сразу сделать под них заготовку. Для этого создадим несколько объектов, настроив у них связь родитель-ребёнок.
Собственно, OChar - родитель oCharPlayer и oCharNoControl.
oCharNoControl - родитель oCharNeutral и oCharEnemy.
В следующем гайде мы много поговорим про создание путей, как они работают. Используя алгоритм поиска пути, сделаем алгоритм, по которому наши человечки будут двигаться по созданным ими путям.
План. Как обычно.
- Пути. Один большой гайд, включая скрипты поиска путей для различных сеток в т.ч. с примерами из моего личного проекта.
- Сохранение. Встроенное VS самописное.
- Звуки.
Небольшое объявление. В формате гайда я объясняю, как сам делаю те или иные вещи, как их понимаю. Когда гайды подойдут к концу - останется только заполнить игру механиками, спрайтами, звуками, музыкой. Это я планирую делать в дальнейшем в виде блога или вроде того.
Есть вопросы или что-то не получается - обращайся, помогу.
Есть пожелания или замечания - буду рад выслушать.
Ссылка на скачивание файла проекта для ЛЛ:
https://disk.yandex.ru/d/w31aDLE9ClPy9Q