Массивы. Процедурная генерация. Инвентарь.

другие уроки, мануалы, советы по Construct 2

Массивы. Процедурная генерация. Инвентарь.

Сообщение Ilyko96 » 15 мар 2013, 16:52

 1. Что такое массив. Его размерность, представление.
Наверняка, многие знают, что такое массивы, и что они из себя представляют, но я все же уделю чуточку внимания этой теме.

«Массивом называется набор однотипных элементов, расположенных в памяти друг за другом , доступ к которым осуществляется через индекс или индексы» - говорит нам Википедия.

Из описания видно, что понятие массива включает в себя определенное количество значений, к которым можно обращаться по своим уникальным индексам. Кол-во уникальных индексов зависит от размерности массива. Нас будут интересовать одномерные, двумерные и трехмерные массивы (чаще всего двумерные).

Чтобы легче ориентироваться в массивах, их надо уметь представлять. Одномерный массив представляется в виде строки с индексами от 0 (включая) и дальше.

Изображение

Выше представлен одномерный массив длиной в 5 элементов – от 0 до 4. Для получения данных из ячейки №3 в C2 надо написать выражение: Array.At(3).С другими ячейками аналогично.

Двумерный массив представляется в виде таблицы:

Изображение

Здесь каждая ячейка имеет два уникальный идентификатора: X и Y.

Например, выражение Array.At(4, 1) вернет нам 90; а выражение Array.At(4, 2) & Array.At(4, 3) & Array.At(4, 4) вернет слово “GUY”.

Не сложно догадаться, что трехмерный массив проще всего представлять в качестве параллелепипеда (или куба, если ширина=высота=глубина):

Изображение

Изображение

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

Итак, я надеюсь, читатель примерно понял суть массива, теперь посмотрим, где это можно использовать.

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

Изображение
Изображение
Изображение

Также, с помощью массива можно реализовать простенький инвентарь, который можно использовать в играх типа ТДС, РПГ или даже в обычном платформере.

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

Остановимся на самом первом и самом простом пункте – на процедурной генерации.

 2. Процедурная генерация уровня
Задача: написать алгоритм по созданию случайно сгенерированного уровня, состоящего из элементов двух типов: стена и проход.

Создаем новый пустой проект и добавляем новый объект типа Sprite. В окне графического редактора меняем размер с 256х256 на 5х5 (A) и заливаем получившийся маленький квадратик серовато-синим цветом. Теперь меняем положение центра спрайта (хот-спот) на крайнее левое верхнее положение (жмем Numpad 7) (D). Следующим шагом нам предстоит нажать ЛКМ по анимации Default и поменять скорость анимации в окне Properties с 5 на 0 (B). Затем в окне кадров анимации дублируем текущий кадр (ПКМ -> Duplicate) и второй кадр заливаем коричневым цветом (C), и закрываем окно редактора.

Изображение

Переименуем наш спрайт со Sprite на Item. И добавим еще 2 массива: первый назовем Level и зададим ему размерность W=128; H=96; D=1. Размерность высчитать легко: надо поделить ширину уровня на ширину спрайта – 640/5=128. Точно также и с высотой – 480/5=96.

Изображение

Кстати, давайте, пока не забыли, поменяем размер уровня с 1280х1024 на 640х480.

Теперь нам нужно добавить еще один массив (вообще удобнее в данном случае использовать Словарь, но ведь тема-то про массивы). Назовем его Destroyer и выставим его размеры: W=3; H=1; D=1.

Теперь немного о логике. Изначально размещаем по центру (по центру массива, т.е. Х=128/2=64, Y=96/2=48) наш объект Destroyer, который будет двигаться в случайном направлении и менять значения массива Level с 0 на 1 (если значения массива не заданы, то там лежат нули). В массиве Destroyer в 0 ячейке будет лежать его Х-координата, в 1 – Y-координата, а в 2 – направление движения (обусловимся так: 0 – вправо, 1 – вниз, 2 – влево, 3 – вверх). Процедуру «движения» нашего «уничтожителя» повторим много (раз 500) раз, а потом зальем уровень копиями спрайта Item с кадром анимации, равным текущему значению массива (0 – стена, 1 – проход).

Итак, создаем событие размещения «уничтожителя» по центру.

Изображение

Кстати, я разместил его не по уже вычисленным (64; 48), а на 1 меньше – (63; 47). Я сделал это потому, что отсчет индексов в C2 начинается с 0, соответственно когда мы размещаем нашего «землекопа» на (64; 48), по сути он будет размещен на (65; 49). Никогда нельзя забывать об этом маленьком правиле, что все индексы массивов считаются от 0!!!

Теперь наш копатель в центре, а в массиве Level все значения = 0 (т.е. все заполнено стенами). Нам надо начинать «копать». Создаем условие «Повторить 500 раз» и пишем действие выбора случайного направления, а также изменения значения в массиве Level на текущих координатах «землекопа» с 0 на 1.

Изображение

Мы помним, что в массиве Destroyer в 2ой ячейке лежит направление «уничтожителя» - от 0 до 3 (включительно). Чтобы задать случайное число в промежутке [0; 3] надо прописать следующее выражение: random(0, 4), но если в выражении random(n) всего 1 аргумент, то он возвращает число в промежутке [0; n). Причем, что очень важно, n – не включается в этот промежуток. Более того, это выражение возвращает не целое число, а число типа float, т.е. с плавающей точкой. Поэтому мы выражение random(4) берем в еще одно выражение floor(n), которое округляет число n вниз до ближайшего целого. Мы берем именно floor(), а не round(), потому что round(3.5) или round(3.8) вернет «4», наш промежуток до 3 включительно! В итоге наше выражение выглядит следующим образом: floor(random(4)).

[Кстати, в C2 есть очень полезное выражение choose(a, b, c, … n). Это выражение случайным образов выбирает один из заданных аргументов (a, b, c, d, e… и т.д.) и возвращает его значение. Причем, что делает это выражение незаменимым в некоторых ситуациях, аргументом может быть любой тип данных: целочисленный, с плавающей точкой и даже строковый!!! Поэтому, чтобы не громоздить двойные скобки, можно воспользоваться данным выражением. Для нашего случая оно будет выглядеть вот так: choose(0, 1, 2, 3). Пользуемся и не забываем об этом!]

Вторым же действием, мы задаем значение ячейки массива Level с индексами, равными X и Y координатам «землекопа» равное 1.

Следующим шагом нам надо передвинуть нашего копателя в соответствии с его направлением. Создаем проверку значения из 2ой ячейки и, в зависимости от этого значения, двигаем его на 1 клетку вверх/вниз или влево/вправо.

Изображение

Собственно здесь все должно быть понятно: на каждом повторе проверяем текущее значение 2ой ячейки и меняем в соответствии с этим значение 0 (X – координата) или 1 (Y – координата) ячейки.

Если же теперь запустить превью (Верхнее меню, вкладка Home -> Run layout), то нам будет показан белый экран. Все так и задумано, ведь данные массивов находятся в памяти, а их визуализацией мы еще не занимались. Поэтому следующим шагом у нас будет замостить экран копиями спрайта Item с правильно выбранным кадром анимации.

Изображение

Итак, сразу после выполнения 500 витков цикла Repeat, мы выполняем следующий цикл, перебирающий каждую ячейку массива Level в XY плоскостях (т.е. проще говоря, каждую ячейку нашей таблицы). На каждом витке этого цикла мы создаем новую копию объекта Item на слое 0 по координатам, равным текущим координатам ячейки, помноженным на 5. У нас ведь на каждую копию приходится по 1 ячейке, а ширина и высота спрайта = 5х5. Поэтому, чтобы избежать наложения их друг на друга и полностью разместить на уровне, мы X – координату домножаем на ширину спрайта, а Y – координату на высоту (если размер спрайта 16х10, то нам надо будет X умножить на 16, а Y – на 10). К тому же, мы сразу выбираем кадр анимации новой копии. Он равен значению, лежащему в текущей ячейке массива Level.

Кстати, еще я добавил одно действие в самом начале кода по уничтожению объекта Item, чтобы он потом не мешался при заполнении уровня его копиями.

Запускаем Превью и смотрим, что получилось. Если вы все сделали, как я Вам сказал, то у Вас должна получиться примерно такая картинка:

Изображение

Если у Вас получилось что-то наподобие:

Изображение
Изображение

То есть, либо весь уровень смещен на 3 пикселя влево-вверх, либо помимо «проходов» видны белые полоски, то Вам следует вернуться к самому первому шагу (в графический редактор) и проверить выставленные центры изображений (хот-споты). Повторюсь, они должны стоять в левом-верхнем углу (на координатах 0; 0).

Ну а если у Вас все получилось, то мы идем дальше. Теперь нам нужно избавиться от некоторой направленности нашей пещеры (если несколько раз обновить страничку, можно заметить, что пещера от центра образуется в одну из сторон экрана). Для этого создадим под-событие к событию Start of layout – Повторить 3 раза. И перенесем в него наш 500-витковый цикл. Кстати, его можно удлинить до 750 или 1000 витков.

Изображение

Помимо описанных выше изменений, перенесем также действия по центрированию нашего «землекопа». В итоге, мы получаем два вложенных цикла, которые описываются примерно так:

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

Теперь посмотрим на результат наших действий:

Изображение

Как мы видим, наша пещера очень сильно «замусорена» стенками в 1 или 2 ячейки. Нам требуется дописать скрипт, который будет подчищать эти ненужные стенки.

Изображение

После цикла продвижения нашего «землекопа» мы добавляем цикл проверки каждого элемента массива Level. Плюс, нам потребуется 1 локальная переменная (выделяем ЛКМ под-событие и нажимаем букву “v” на клавиатуре, либо ПКМ -> Add -> Add local variable). Назовем ее Cleaner. Теперь мы проверяем значение текущей ячейки уровня, и если оно равно 0 (т.е. стена), то проверяем все 8 прилежащих ячеек (на скриншоте поочередно: Право-Центр, Право-Низ, Низ-Центр, Низ-Лево, Лево-Центр, Лево-Верх, Верх-Центр, Верх-Право), и если там лежит 0 (т.е. стена), то добавляем в переменную 1. Соответственно после всех этих проверок добавляем событие Trigger once и проверяем в со-условии переменную: Cleaner < 3 || Cleaner = 3, т.е. меньше или равен 3. И, если это условие выполняется (т.е. к текущей ячейке прилегают либо всего лишь 1, 2 или всего лишь 3 стенки), то мы эту ячейку со «стены» меняем на «проход».

Итог очевиден (1ый скрин – без проверки, 2ой – Cleaner <=3, 3ий – Cleaner <=4):

Изображение
Изображение
Изображение

В принципе, на этом с процедурной генерацией можно и закончить, потому что все основные моменты были рассмотрены. Разве что можно только добавить еще одно событие – обновление уровня по ЛКМ, но это уже не сложно: добавляем объект Browser и Mouse (или Touch) и пишем следующий код:

Изображение

Информация по данному методу взята с сайта - http://scirraconstruct.ru/backup/proced ... vod-part1/ и переложена мной: Ilyko96. Ну а массивы на этом не заканчиваются, и мы идем дальше: впереди у нас «Инвентарь при помощи массивов»

 3. Инвентарь с помощью массивов
Итак, уровень генерировать мы уже научились, теперь заселим нашу пещеру пещерным жителем – нашим героем, научим его двигаться по «проходам» и запретим двигаться в «стенки», а также раскидаем по уровню предметы, которые будем нашим героем в инвентарь поднимать.

Для начала немного изменим уже существующий проект:

Изображение

Во-первых, перейдем в редактор уровня и поменяем размер спрайта с 5х5 на 10х10. Далее поменяем центровку нашего «землекопа» с 63, 47 на 31, 23 (640px/10px=64px/2-1=31, 480px/10px=48px/2-1=23) (А). Теперь немного уменьшим количество витков цикла движения копателя (B), а также поменяем координаты создания копий объекта Item, с (Level.CurX*5; Level.CurY*5) на (Level.CurX*10; Level.CurY*10) (C).

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

Теперь создаем главного героя (ГГ) на новом слое. Кстати, старый лучше переименовать на емкое название Level, а новый назовем Main.

Изображение

Меняем сразу размер на 10х10px, заливаем бирюзовым (можно любым) цветом и выставляем хот-спот в точку (0; 0). Если вы использовали не квадрат, а загруженный откуда-либо спрайт, то желательно настроить маску коллизий (A):

Изображение

Настроили – и сразу же переименовываем спрайт в Player. Теперь нам надо разместить его в самый центр. Задаем координаты: 640/2-10=310; 480/2-10=230. Кстати, давайте сразу добавим 2 приватные переменные (instance variable) этому объекту. Назовем их Mx и My, а начальное значение будет равно в обоих случаях нулю.

Изображение

Приступаем к движению нашего ГГ. Добавляем объект Keyboard и идем в редактор событий и пишем следующий код:

Изображение

В группе событий (А) мы проверяем текущую координату ГГ с записанной в соответствующей переменной (X-координату сравниваем с переменной Mx, а Y-координату – с переменной My), и в зависимости от этого двигаем нашего ГГ в нужную сторону. В группе событий (B) мы проверяем текущее положение ГГ, и если его координаты равны записанным в переменных Mx и My, то при нажатии на соответствующую клавишу (стрелку на клавиатуре) мы меняем переменную Mx или My и в силу вступают события из группы (A). Кстати, этот метод взят из темы про имитацию Grid Movement на C2: viewtopic.php?f=17&t=313

И еще: в событие Start of layout мы добавили еще 2 действия, записывающие в переменные Mx и My текущие координаты ГГ, чтобы он не стал двигаться прямо с самого начала уровня к координатам (0; 0).

Если теперь запустить превью, то мы увидим, что наш ГГ легко передвигается по клеткам. Но он еще не различает где «проход», а где «стена». Поэтому следующим шагом обучаем его этому:

Изображение

Суть этих действий такова: сначала мы проверяем на нажатие клавиши (берем в пример первое событие – нажатие «Стрелки Направо»). Если мы зажали Стрелку Вправо, то идет проверка на наложение нашего ГГ на объект Item, причем именно тот, кадр анимации которого = 0 (т.е. стена). Я использовал событие Overlap at offset, чтобы не добавлять лишний объект – индикатор. Это событие как бы переносит, смещает наш объект по заданным координатам (относительно самого спрайта), проверяет наложение, а затем возвращает спрайт на место. Таким образом мы определяем наличие «стены» с каждой из сторон и запрещаем двигаться туда нашему ГГ.

Это было маленькое отступление от темы массивов, просто, чтобы «процедурная генерация» и «инвентарь» имели хоть какую-нибудь связь, и их можно было бы объединить в 1 исходник.

Теперь приступаем непосредственно к инвентарю. Хотя нет, сначала добавим все имеющиеся у нас в редакторе событий события в группу (буква “g” или ПКМ -> Add group), которую назовем просто и незатейливо: «Процедурная Генерация + Движение ГГ». Это нужно, чтобы они просто не мешали нам собирать код для инвентаря.

Теперь точно инвентарь.

Итак, предлагаю создать еще один слой (благо во Free Edition лицензии максимально количество слоев – 4), который назовем Inventory, а первые 2 – заблокируем, чтобы не мешались.

Изображение

Итак, для начала создадим фон для нашего инвентаря. После недолгих раздумий мой выбор пал на объект Tiled Background, поэтому смело дабл-тапим по пустому пространству и выбираем этот объект. В открывшемся редакторе делаем следующее:

Изображение

Во-первых, меняем опять размер, только теперь не на 10х10, а на 20х20 (A). Далее берем инструмент «Линия» (“Line”) и проводим любым цветом две полоски толщиной в 1px (C). Затем берем инструмент «Заливка» (“Fill”) и, изменив в окне выбора цвета прозрачность (“Alpha”) на 100 заливаем оставшуюся область тем же самым цветом. Закрываем графический редактор и переименовываем объект в InventoryBack. Кстати, не забудьте поменять его размер на 100х40 и переместить на координаты (0; 0). Вот такой аккуратненький 10ти-слотовый инвентарь у нас получился. Но это еще не все.

Теперь нам предстоит добавить уже, наверное, любимый Вами объект – Array. Добавляем, переименовываем в Inventory, а размер и вовсе менять не надо, пускай остается W=10; H=1; D=1. Да это будет одномерный массив, состоящий из 10ти элементов.

Но на самом деле, это еще не все. Нам нужно нарисовать или загрузить те предметы, которые будет подбирать наш герой. Поэтому добавляем на наш 3ий слой (это важно, проверьте, чтобы активным был 3ий слой) еще один объект типа Sprite. Также меняем размер на 20х20, ставим хот-спот на (0; 0) и рисуем предметы. Если Вы загружали рисунки, то поменяйте «маску столкновений» на Bounding box, об этом я уже писал выше.

Изображение

У меня получилось какое-то жалкое подобие яблока, но да ладно, не в этом суть. После добавления, сразу меняем имя на… Допустим Fruit. Также поменяем размер на 10х10 (в самом редакторе уровня, т.е. как бы изменим масштаб этого спрайта).

Пока временно разместим его на координатах (330; 230), но потом я планирую сделать случайную генерацию «яблок» на уровне.

Наконец-то мы добрались до мозговой части нашего инвентаря. Хотя нет, я упустил еще 1 момент. Нам потребуется еще 2 приватных переменных (instance variable) для объекта Fruit. Первую назовем Active и присвоим ей тип – Boolean, а начальное значение поставим на “true”, а вторую назовем ID и это будет обычная числовая переменная с начальным значением 0.

Изображение

Теперь пишем код:

Изображение

Итак, при столкновении нашего ГГ с фруктом, переменная Active которого равняется “true”, мы выполняем цикл for, который назовем “add”, и который выполнится 10 раз: от 0 до 9. Кстати, я не говорил, как делать спаренные события? Думаю, интуиция Вам подскажет лучше меня, но я все же повторю – выбираем условие, к которому хотим добавить со-условие и жмем буковку “c” или нажимаем ПКМ -> Add another condition. Все же вернемся к коду. При коллизии между ГГ и фруктом, мы запускаем цикл, который проверяет в массиве Inventory значение в ячейке под индексом равным текущему индексу цикла, и если это значение равно 0 (т.е. там пусто, изначально везде нули, значит везде пусто), то мы меняем это значение на 1, фрукту, с которым столкнулся ГГ, меняем Boolean-переменную на “false”, записываем в переменную ID текущий индекс цикла и прерываем его. То есть, по сути, мы перебираем все ячейки начиная с 0ой и при нахождении первой свободной, кладем туда наш предмет, записываем, что она занята и прерываем цикл, чтобы не искать дальше.

Если теперь запустить проект, то никаких видимых изменений мы не увидим (прошу прощения за тавтологию), но они произошли. Теперь займемся визуальной частью произошедшего:

Изображение

Здесь мы проверяем текущее состояние переменной Active. И для всех копий объекта Fruit, значение переменной которых равно 0 (или “false”, что одно и то же) мы применяем действие изменения позиции и размера. С размером все понятно, делаем наш фрукт больше, чтобы выглядел солиднее, а в действии позиции написаны иероглифы. Расшифровываем.

Для начала разберемся со знаком процента (“%”). Ниже представлена табличка с выражениями и возвращаемыми значениями:

Изображение

Если поискать закономерность между этими соотношениями, можно понять что из себя представляет знак процента, но я долго томить не стану, просто раскрою секрет: в выражении “a%b” знак процента используется для получения остатка от деления “a” на “b”.

Теперь смотрим, зачем он нужен в нашем коде. Мы берем остаток от деления текущего индекса цикла на 5. Давайте переберем все возможные варианты (благо их тут мало):

Изображение

Таким образом, мы видим, что первая пятерка индексов (начиная с 0) останется прежней, а вторая – будет соответственно идентична первой. Разве не похоже на распределение наших предметов в инвентаре: 2 строки по 5 элементов в каждой?

А умножение на 20 идет для того, чтобы спрайты не накладывались друг на друга, а четко лежали каждый в своей ячейке. Итоговая формула: (Fruit.ID%5)*20 (кстати, если действие касается объекта Fruit, то вместо Fruit.ID можно написать self.ID, и значение будет то же самое).

Точно так же посмотрим, что происходит и с Y-координатой нашего фрукта:

Изображение

Как видно из таблицы, эта формула идеально подходит, для определения Y-позиции нашего фрукта. Итоговая формула: floor(Fruit.ID/5)*20

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

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

Для этого нам потребуется еще один одномерный массив. Добавляем его и переименовываем в FruitMemory. Размер стандартный: W=10, H=1, D=1. Теперь шагаем в редактор событий и добавляем глобальную переменную (“v” или ПКМ -> Add global variable), которую назовем FruitSpawner. Эта глобальная переменная будет строкового типа и начального значения просто не будет. Теперь открываем нашу группу по Процедурной Генерации и ищем событие визуализации уровня. В под-событии к нему пишем код:

Изображение

Для каждой ячейки уровня, тип которой – «проход» (значение = 1), мы добавляем в новую переменную следующее: сначала будет записана X-координата текущей ячейки, затем знак “;”, далее Y-координата текущей ячейки, а затем вертикальная черта “|”. Причем, координаты записываются с добавлением незначимых нулей (3 знака до запятой). Таким образом, если у нас, допустим ячейки с координатами (5; 3), (8; 9), (2; 13), (21; 1) и (19; 17) являются проходами, то в переменную FruitSpawner запишется следующее: «005;003|008;009|002;013|021;001|019;017|».

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

Изображение

Много всего, знаю. Тем не менее, в этом надо разобраться. Во-первых, обратите внимание, я добавил действие удаления фрукта на старте уровня. Во-вторых, мы добавляем локальную переменную Rand. Она нужна для того, чтобы подбираться случайные числа в диапазоне от 0 до количества ячеек типа «проход». Далее идет цикл под названием “fruit”, который сработает 10 раз: от 0 до 9. В цикле есть вложенный цикл – While, который будет срабатывать до тех пор, пока в переменную Rand не запишется число, которого нет в массиве FruitMemory. Это сделано для того, чтобы яблоки не появились в одной и той же ячейке. Если же такого значения в массиве нет, то мы записываем это значение в массив в ячейку с индексом, равным текущему индексу цикла “fruit” (осторожно, цикл while тоже имеет индекс цикла, и если не указать индекс какого именно цикла мы берем, возьмется индекс самого последнего – вложенного – цикла!). Далее создается объект Fruit на карте по координатам, записанным в текстовой переменной FruitSpawner, под номером, равным Rand, а дальше останавливается цикл While и цикл “fruit” пойдет по новому кругу…

Вот такой сложный блок. Теперь по порядку.

1) Переменная Rand. Мы записываем следующее значение: floor(random(len(FruitSpawner)/8)). Итак, рассматриваем выражение изнутри. Сначала берется длина нашей переменной len(). Затем эта длина делится на 8 (ведь координаты в нее записывались именно по 8: 3 цифры на X-координату, 1 знак “;”, 3 цифры на Y-координату – это уже 7, плюс вертикальная черта; итого 8 знаков на 1 ячейку). В итоге длина переменной, поделенная на длину одного блока = количество блоков, то есть количество тех самых ячеек типа «проход». Идем дальше. А дальше уже знакомый из первой части алгоритм вычисления целого случайного числа от 0 и до N. В конечно счете, на выходе мы получим переменную Rand, в которой записано случайное число от 0 до количества ячеек типа «проход».

2) Проверка наличия значения в массиве. Здесь все просто: цикл While прерывается, если такой записи в массиве еще нет, то есть этот блок координат еще не был выбран, а значит на нем не может быть созданного яблока.

3) Создание копии яблока. Не стал выделять в отдельный пункт создание записи в массиве FruitMemory, потому как это слишком просто: если такой записи нет, мы ее создаем, в ячейку под номером, равным индексу цикла “fruit”, мы кладем номер блока координат из текстовой переменной FruitSpawner. Гораздо интереснее (может для кого-то сложнее) выглядят координаты создания новой копии яблока. Для X-координаты используется выражение: int(left(tokenat(FruitSpawner, Rand, "|"), 3))*10. Опять начинаем изнутри. Выражение tokenat(), позволяет выбрать часть текста между заданными разделителями по индексу разделенной части. Пример: tokenat(“Go, run, jump”, 0, “,”) = Go; tokenat(“Go, run, jump”, 1, “,”) = run и так далее… Получается, что мы разбиваем строку на части по разделителю “|” и выбираем ту часть, которая находится под номером Rand. В итоге наше выражение предположительно (беру числа в качестве примера) превращается во что-то такое: int(left(“003;005”, 3))*10. Выражение left()возвращает n-ое количество знаков с левой стороны. В данном случае left() вернет “003”. Но вернет их в строковом формате, а нам это число надо умножить на 10, поэтому предварительно мы переводим “003” в числовой формат выражением int(). В конечном итоге, мы достаем из текстовой переменной случайный блок координат, делим его на 2 части: X и Y, и создаем фрукт по этим координатам.

4) Ну и остановка цикла, здесь говорить нечего.
Вот как-то так. После такого сложного объяснения (я действительно очень сильно надеюсь, что хоть какая-то часть кода прояснилась после этих слов) предлагаю сделать что-нибудь очень легкое. Например, разбрасывание яблок по карте вновь при собирании полного инвентаря яблок. Вперед! (Советую не читать пока дальше урок, а попробовать реализовать это самим; дам даже подсказку – попробуйте использовать объект Function, хотя можно и без него).
Тем не менее свое решение последней в этом уроке задачи по повторному разбросу яблок я покажу. Вот скрины кода и его изменений:

Изображение

Как можно заметить я убрал из события Start of layout весь этот код и перенес его в функцию “fruit_create”, а в том месте, где он стоял (после зачистки карты от мелких стенок) ставим триггер и вызов этой функции.

Изображение

Сразу после нашей группы «Процедурная Генерация и Движение ГГ» я поставил тело функции, т.е. то, что будет происходить при вызове функции. Как можно увидеть на скринах, тело цикла не изменилось, но добавилось несколько новых действий и событий. Во первых, при каждом вызове функции мы чистим массивы Inventory и FruitMemory, чтобы функция работала с массивами как будто они только что были созданы (как при первом запуске), если этого не сделать – будут конфликтные ситуации и баги. Далее, я добавил уничтожение всех копий объект Fruit. Все дело в том, что функция создает новые копии, и они будут накладываться поверх старых, соответственно, чтобы этого избежать, надо удалить старое.

Ну и в самом низу я добавил 1 событие (состоящее из двух со-условий) для разброса яблок при полном их сборе. Мы проверяем, есть ли 0 в массиве инвентаря, и если их нет (т.е. нет пустых мест, следовательно, все яблоки на своих местах), мы 1 раз запускаем функцию разбрасывания по уровню яблок.


На этой прекрасной ноте, когда задача выполнена с лихвой и все работает так, как это и было задумано я хочу закончить свой туториал по массивам в программе Construct 2. Но было бы не честным в исходнике сделать одно, а описать совсем другое. В заключение, я хочу дописать еще несколько изменений, которые мне пришлось внести в исходник, не расписав это в уроке. Эти изменения связаны только с корректным выполнение кода программы и никакого радикального смысла не несут.

Уже в самом конце объявился 1 баг: когда ГГ, проходя на одну клетку выше яблока, собирал его, хотя не должен этого делать. В связи с этим я немного отредактировал маску столкновений как яблока, так и ГГ:

Изображение
Изображение

Теперь, я могу со спокойной совестью закончить начатый туториал. Подводя итог всему, о чем шла речь выше, я могу сказать, что те, кто прочитал этот туториал, осознал и понял хотя бы 60% из всего объема работы, знают массивы на уровне, позволяющем им обращаться с ними на «ты» и использовать их в своих играх. Первая часть урока – «Процедурная Генерация уровня» была заранее подготовлена, внесены поправки в исходный урок (ссылка в конце первой части), он был восстановлен из остатков старого форума и адаптирован под Construct 2. Вторая же часть урока – «Инвентарь», которая вылилась в довольно большую часть, с включениями, не имеющими отношения к массивам, писалась по мере написания данного туториала. В связи с этим, как раз, и был выявлен баг уже после написания этой статьи.

Тем не менее, я благодарю тех, кто дочитал этот туториал до конца и понял всю прелесть объекта Массив!

 Исходник проекта
Массивы.capx
C2 - r121 Исходник проекта из туториала
(8.11 КБ) Скачиваний: 3055

 От автора
Это мой, пожалуй, первый серьезный туториал, так что прошу отнестить спокойно к форме изложения. Данную идею я придумал сам и реализация точно также была придумана мной (за исключение пары моментов - ссылки на оригинал есть).
Последний раз редактировалось Ilyko96 15 май 2013, 15:41, всего редактировалось 1 раз.
Аватара пользователя
Ilyko96
Игродел
Игродел
 
Сообщения: 398
Зарегистрирован: 08 янв 2013
Откуда: Мск

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Sir_G » 15 мар 2013, 17:13

Мне очень понравилось! ) жду еще таких мощных уроков )))
Аватара пользователя
Sir_G
Игродел
Игродел
 
Сообщения: 2415
Зарегистрирован: 06 сен 2012
Двиг: Construct 2
Лицензия: Personal

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Seryiza » 15 мар 2013, 17:15

Круто! Особенно зацепила генерация уровня, буду разбираться. Спасибо :smile:
Аватара пользователя
Seryiza
Игродел
Игродел
 
Сообщения: 226
Зарегистрирован: 11 фев 2013
Откуда: Южно-Сахалинск

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Sailer » 15 мар 2013, 18:34

Ilyko96, ну ты жаришь! :biggrin:
Аватара пользователя
Sailer
Администратор
Администратор
 
Сообщения: 7330
Зарегистрирован: 05 сен 2012

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Developer » 15 мар 2013, 19:07

Хороший урок. Спасибо!+
Аватара пользователя
Developer
Игродел
Игродел
 
Сообщения: 501
Зарегистрирован: 04 янв 2013

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Zaksoid » 15 мар 2013, 20:33

Ilyko96, спс за урок. Очень хочется от тебя подробного урока по он-лайн таблице рекордов. Можно надеяться на это в обозримом будущем? )
Изображение
Аватара пользователя
Zaksoid
Игродел
Игродел
 
Сообщения: 1224
Зарегистрирован: 14 сен 2012
Откуда: Красноярск

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Ilyko96 » 15 мар 2013, 20:54

Zaksoid, нет ничего невозможного, но в данном случае придется довольно круто выйти за тематические рамки нашего форума. Как я вижу эта тема довольно популярна в здешних кругах, так что я попытаюсь придумать что-нибудь простенькое на эту тему :ok:
Аватара пользователя
Ilyko96
Игродел
Игродел
 
Сообщения: 398
Зарегистрирован: 08 янв 2013
Откуда: Мск

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение virusfun » 16 мар 2013, 03:04

Ilyko96, молодец! Отлично! Я даже не ожидал ТАКОЙ подробности и ТАКОЙ осведомленности в мощах массива! Я уверен, у тебя в голове еще много плюшек по этой теме;) Давай, отдышись и снова в бой! А все будут ждать с нетерпением!
А от меня тебе 3 плюса, а может и больше=)
Пути геймдева неисповедимы!
Аватара пользователя
virusfun
Игродел
Игродел
 
Сообщения: 916
Зарегистрирован: 23 сен 2012
Откуда: Хабаровск
Двиг: Construct 2/3
Лицензия: Personal
VK: vk.com/virusfun

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение virusfun » 29 мар 2013, 16:02

Илья, никак не могу понять... А возможно ли сделать так, чтобы уровень рисовался не сразу, а постепенно? Ну, типа, чтобы генерация оставалась той же, но объекты создавались последовательно, слева-направо и сверху-вниз, с интервалом в 0.1 сек?

Я просто не нашел способа рисовать карту без перебора массива (For each), а это событие ведь триггерное... Что-то я пока расстроен(
Пути геймдева неисповедимы!
Аватара пользователя
virusfun
Игродел
Игродел
 
Сообщения: 916
Зарегистрирован: 23 сен 2012
Откуда: Хабаровск
Двиг: Construct 2/3
Лицензия: Personal
VK: vk.com/virusfun

Re: Массивы. Процедурная генерация. Инвентарь.

Сообщение Ilyko96 » 29 мар 2013, 20:59

virusfun писал(а):Ну, типа, чтобы генерация оставалась той же, но объекты создавались последовательно, слева-направо и сверху-вниз, с интервалом в 0.1 сек?

Не понял, т.е. точно также сгенерированный уровень, но чтобы его отрисовка была не мгновенной (как это реализовано сейчас), а с интервалом 0,1 сек для каждого элемента уровня?

Если это действительно так, то я бы оставил весь код таким же, какой он есть сейчас, а с отрисовкой чуть-чуть помудрил бы... Во-первых, придется ввести одну лишнюю переменную: посколько событие On function Название_функции триггерное, а значит, что напрямую запихнуть в него действие ожидания 0,1 сек не получится. Придется функцией менять переменную, допустим с 0 на 1, если она = 1, ждать 0,1 сек, затем менять на 0 и прокручивать уже следующий виток цикла отрисовки... Схема в итоге довольно серьезно обрастет лишними событиями, но останется вполне функциональной (кстати, не могу так на вскидку сказать по поводу производительности такого метода, надо тестить)...

Или тебе нужен конкретный пример реализации, а не принципиальная схема? :scratch_one-s_head:
Аватара пользователя
Ilyko96
Игродел
Игродел
 
Сообщения: 398
Зарегистрирован: 08 янв 2013
Откуда: Мск

След.

Вернуться в Другие уроки по Construct 2

Пользователи онлайн

Зарегистрированные пользователи: нет зарегистрированных пользователей

cron