← к содержанию

Урок 19-1. Игра «Пятнашки» – подготовка игрового поля

Система инфопанелей в DDD делалась для построения интерфейсов к 3D-программам, и не предназначена для полноценных 2D-игр. Тем не менее, что-то уж совсем простое на них создать возможно. В этом уроке мы, чтобы поглубже изучить инфопанели, создадим игру «Пятнашки», в которой нужно двигать фишки с цифрами по квадратному полю.

Урок получается довольно большой, поэтому я разделил его на две части. В этой первой части мы, фактически, только подготовим сцену и несколько вспомогательных функций, а весь игровой процесс заработает только в следующей части. Файл с исходными кодами модуля, соответствующий только этому уроку, называется scena1.d.

  1. Начнём с пустого модуля scena.d. Добавим в него импорты библиотек, которые потребуются нам в дальнейшем, и сразу создадим функцию сцена(), в которой определим для этой сцены цвет фона и обработчик событий (см. урок 14), а также для правильной обработки нажатий мыши получим текущую высоту окна в пикселях:

module scena;

import std.math, std.conv, std.functional, std.string;
import ddd, ddd.mesto, ddd.lamp, ddd.vector, ddd.quaternion, ddd.ddd_object, ddd.okno;
import ddd.infopanel, ddd.element_infopaneli;
import ddd.zavisimost.sobytija_SDL2;
import my_utils.journal;

СтандартныйОбработчикСобытий обработчик_событий;
int высота_окна;

void сцена() {
    менеджер.визуализатор.задать_цвет_фона([0.5, 0.8, 1, 1]); // Голубой цвет
    обработчик_событий = new СтандартныйОбработчикСобытий();
    менеджер.задать_обработчик_событий(обработчик_событий);
    высота_окна = размеры_окна().y;
}
  1. Определим после импортов несколько глобальных констант и переменных:

enum string имя_модуля = "сцена";

enum float ЕДИНИЧНОЕ_РАССТОЯНИЕ = 0.237; // Ширина фишки 
enum float НАЧАЛЬНАЯ_КООРДИНАТА = 0.025; // Ширина рамки

enum float[] КООРДИНАТЫ =  [НАЧАЛЬНАЯ_КООРДИНАТА, 
                            НАЧАЛЬНАЯ_КООРДИНАТА + ЕДИНИЧНОЕ_РАССТОЯНИЕ, 
                            НАЧАЛЬНАЯ_КООРДИНАТА + ЕДИНИЧНОЕ_РАССТОЯНИЕ * 2, 
                            НАЧАЛЬНАЯ_КООРДИНАТА + ЕДИНИЧНОЕ_РАССТОЯНИЕ * 3];

Инфопанель инфопанель_кнопка, инфопанель_поле;
ЭлементИнфопанели элемент_поле;

Здесь с помощью констант ЕДИНИЧНОЕ_РАССТОЯНИЕ и НАЧАЛЬНАЯ_КООРДИНАТА в константе КООРДИНАТЫ определяются значения всех координат (как для x, так и для y) позиций левых верхних углов фишек (в относительных координатах, в пределах инфопанели игрового поля). Также мы определяем две инфопанели (и элемент одной из них).

  1. Добавим игровое поле на сцену.

    1. Перед функцией сцена() добавьте функцию создать_фишку(int), в которой создаётся новый элемент инфопанели, соответствующий одной фишке, вычисляются координаты её расположения и текстурные координаты

ЭлементИнфопанели создать_фишку(int цифра) {
    auto x = (цифра-1) % 4;
    auto y = (цифра-1) / 4;
    ЭлементИнфопанели фишка = new ЭлементИнфопанели(to!string(цифра), false, 
                            КООРДИНАТЫ[x],
                            КООРДИНАТЫ[3-y],
                            ЕДИНИЧНОЕ_РАССТОЯНИЕ,
                            ЕДИНИЧНОЕ_РАССТОЯНИЕ);
    фишка.задать_координаты_текстуры(x*0.25, y*0.2, (x+1)*0.25, (y+1)*0.2);
    фишка.задать_текстуру(менеджер.получить_текстуру("15 фишки"));
    return фишка;
}

Здесь для координаты y задано значение КООРДИНАТЫ[3-y], потому что в игре мы нумеруем позиции сверху-вниз, а координаты в инфопанелях задаются снизу-вверх (да, тут есть некоторое неудобство, и оно ещё пару раз всплывёт в этом уроке). Также надо пояснить по поводу текстурных координат: посмотрите, как выглядит файл 15 фишки.png в ресурсах – там все изображения фишек (которые вообще-то должны быть квадратными) сжаты по координате y до размера 4/5 от размера по координате x, чтобы втиснуть в этот же файл изображение кнопки внизу. Поэтому x-координаты умножаются на 1/4 (или, тоже самое, 0.25), а y-координаты умножаются на 1/5 (или, 0.2).

    1. Теперь создадим и заполним элементами, изображающими фишки, инфопанель игрового поля. Добавьте этот код в функцию сцена():

    инфопанель_поле = new Инфопанель("игра", менеджер, ТипКоординат.ОтносительноX,
                                                0.25, -0.52, 0.5, 0.5);
    if (!(инфопанель_поле is null)) {
        менеджер.добавить_инфопанель(инфопанель_поле);
        элемент_поле = new ЭлементИнфопанели("поле", true, 0,0,1,1);
        if (!(элемент_поле is null)) {
            элемент_поле.задать_координаты_текстуры(0, 0, 1, 1);
            элемент_поле.задать_текстуру(менеджер.получить_текстуру("15 поле"));
            инфопанель_поле.присоединить_элемент(элемент_поле);
            for (int цифра=1; цифра<16; цифра++)
                элемент_поле.присоединить_элемент(создать_фишку(цифра));
        }
    }

Здесь мы создаём объект инфопанели, устанавливаем в него элемент, соответствующий игровому полю, а затем расставляем на нём фишки с помощью функции, созданной на предыдущем шаге.

Отдельное пояснение по поводу координат инфопанели. Здесь используется тип координат ОтносительноX, это означает, что все координаты (как по горизонтали, так и по вертикали), являются долями ширины окна. Для координаты x это просто означает, что левая граница окна – это 0, а правая – 1. А вот для координаты y всё несколько веселее… Т.к. мы задали отрицательную координату, то 0 у нас соответствует верхней границе. Отрицательная координата нижней границы окна будет зависеть от соотношения высоты к ширине. Конечно, это может привести к тому, что, например, часть поля окажется внизу за пределами окна, или, наоборот, внизу останется много пустого места. Но в случае разумных соотношений сторон окна результат выглядит нормально. Зато стало возможно привязать всё к середине окна (по горизонтали), и гарантировать квадратность игрового поля. Большинство создателей 2D-игр вместо этого предпочитают жёстко задавать все размеры в пикселях, у нас есть возможность сделать тоже самое, выбрав тип координат Абсолютно, но лично я такой подход не люблю.

  1. Добавим кнопку «Перемешать» (это отдельная инфопанель) под игровым полем:

    инфопанель_кнопка = менеджер.получить_инфопанель("15 кнопка");
    if (!(инфопанель_кнопка is null)) {
        менеджер.добавить_инфопанель(инфопанель_кнопка);
    }

Здесь я не стал мудрить и сделал отдельный простой файл, формирующий эту инфопанель. Файл находится тут: ресурсы/инфопанели/15 кнопка.ip, можете самостоятельно изучить его содержимое. Уже сейчас наша программа после компиляции должна отображать все нужные элементы игры, к сожалению, пока статично.


  1. В конце урока напишем ещё пару отдельных небольших функций, связанных с расстановкой фишек на поле, мы ими воспользуемся в следующем уроке.

Здесь требуется пояснить, что под словом «позиция» имеется ввиду число, однозначно определяющее расположение фишки на игровом поле. Нумерация ведётся с 0, который соответствует левому верхнему углу (на скриншоте там расположена фишка «1»), и заканчивается номером 15, что соответствует правому нижнему углу (сейчас там пустое место).

    1. Функция установить_фишку, которая ставит уже существующий элемент инфопанели, изображающий фишку с цифрой, на позицию с требуемым номером:

void установить_фишку(short цифра, ЭлементИнфопанели элемент_поле, int позиция) {
    auto фишка = элемент_поле.получить_элемент(to!string(цифра));
    if (!(фишка is null)) {
        auto x = (позиция) % 4;
        auto y = (позиция) / 4;
        фишка.задать_координаты(КООРДИНАТЫ[x],
                                КООРДИНАТЫ[3-y],
                                ЕДИНИЧНОЕ_РАССТОЯНИЕ,
                                ЕДИНИЧНОЕ_РАССТОЯНИЕ);
    }    
}
    1. Функция, которая по относительным координатам (задаются в диапазоне 0..1, ось x слева-направо, ось y снизу-вверх) внутри инфопанели вычисляет соответствующую позицию фишки:

short вычисление_позиции(float x, float y) {
    if ((x < НАЧАЛЬНАЯ_КООРДИНАТА) || (x > НАЧАЛЬНАЯ_КООРДИНАТА + ЕДИНИЧНОЕ_РАССТОЯНИЕ * 4))
        return -1;
    if ((y < НАЧАЛЬНАЯ_КООРДИНАТА) || (y > НАЧАЛЬНАЯ_КООРДИНАТА + ЕДИНИЧНОЕ_РАССТОЯНИЕ * 4))
        return -1;
    float x1 = x - НАЧАЛЬНАЯ_КООРДИНАТА;
    float y1 = НАЧАЛЬНАЯ_КООРДИНАТА + ЕДИНИЧНОЕ_РАССТОЯНИЕ * 4 - y;
    short позиция = to!short(floor(x1/ЕДИНИЧНОЕ_РАССТОЯНИЕ) + 4 * floor(y1 / ЕДИНИЧНОЕ_РАССТОЯНИЕ));
    return позиция;
}

Первые два оператора if вылавливают случай, когда координаты указывают на рамку игрового поля, он не соответствует какой-либо реальной позиции. Функция floor из модуля std.math стандартной библиотеки возвращает целую часть дробного числа (это описание верно, по крайней мере, для положительных чисел). Здесь нам тоже приходится вычитать координату y, т. к. позиции считаются сверху-вниз, а координаты внутри инфопанели считаются снизу-вверх.

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


продолжение урока →