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

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

Продолжение 19-го урока про игру «Пятнашки». В этой части мы реализуем логику игры, обработку событий мыши и анимацию движения фишек. Модуль scena.d включает в себя всё из обеих частей урока (в отличие от модуля scena1.d из первой части).

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

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

module logika;

import std.random;
  1. Объявим константы:

enum short[][short] ДОПУСТИМЫЕ_ХОДЫ = [0: [1, 4],
                                       1: [0, 2, 5],
                                       2: [1, 3, 6],
                                       3: [2, 7],
                                       4: [0, 5, 8],
                                       5: [1, 4, 6, 9],
                                       6: [2, 5, 7, 10],
                                       7: [3, 6, 11],
                                       8: [4, 9, 12],
                                       9: [5, 8, 10, 13],
                                       10: [6, 9, 11, 14],
                                       11: [7, 10, 15],
                                       12: [8, 13],
                                       13: [9, 12, 14],
                                       14: [10, 13, 15],
                                       15: [11, 14]];

enum short[16] НАЧАЛЬНОЕ_ПОЛОЖЕНИЕ = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0];
enum int ХОДОВ_ПЕРЕМЕШИВАНИЯ = 1000;

В константе ДОПУСТИМЫЕ_ХОДЫ как раз и зашита вся логика игры. В каждом элементе этого ассоциативного массива перечислены номера позиций, куда с текущей позиции возможно делать ход. Остаётся только проверить, является ли новая позиция пустой. Константа НАЧАЛЬНОЕ_ПОЛОЖЕНИЕ задаёт начальную расстановку фишек. На игровом поле положительные числа означают, что на этом месте стоит фишка с соответствующим номером, 0 означает, что позиция пустая.

  1. Добавим класс Логика, которым и будем пользоваться из модуля scena.d:

    1. Объявление класса:

class Логика {
    short[16] игра;
    short пустое_место;

}	

Класс содержит массив игра, в нём хранится состояние игрового поля, и в отдельной переменной сохраняется номер пустой позиции. Я не стал заморачиваться с инкапсуляцией этих данных, не являюсь фанатом «чистоты ООП», когда от этого только добавляется неудобств.

    1. Конструктор и вызываемая из него инициализирующая функция:

    this() {
        начальное_положение();
    }
	
    public void начальное_положение() {
        игра = НАЧАЛЬНОЕ_ПОЛОЖЕНИЕ.dup;
        пустое_место = 15;
    }	

Конструктор приводит состояние поля в начальное положение.

    1. Добавим метод, с помощью которого будет возможно сделать ход:

    public short ход(short позиция) {
        foreach (цель; ДОПУСТИМЫЕ_ХОДЫ[позиция]) 
            if (цель == пустое_место) {
                игра[пустое_место] = игра[позиция];
                игра[позиция] = 0;
                пустое_место = позиция;
                return цель;
            }
        return -1;
    }

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

    1. Последний метод класса отвечает за случайное перемешивание фишек:

    public void перемешать() {
        начальное_положение();
        for (int i=0; i < ХОДОВ_ПЕРЕМЕШИВАНИЯ; i++) {
            auto новое_место = choice(ДОПУСТИМЫЕ_ХОДЫ[пустое_место]);
            игра[пустое_место] = игра[новое_место];
            игра[новое_место] = 0;
            пустое_место = новое_место;
        }
    }

Здесь мы из начального положения делаем заданное количество случайных ходов, причём фактически у нас тут по полю «ходит» пустое место вместо фишек. Функция choice модуля std.random стандартной библиотеки возвращает случайный элемент переданного ей массива.

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

  1. Вернитесь в модуль scena.d, добавьте импорт нашего нового модуля, далее нужно объявить экземпляр нашего класса и, затем, в функции сцена() создать его:

import logika;
…
Логика логика;
…
void сцена() {
…
    логика = new Логика();
…
}
  1. Добавим в наш модуль ещё одну небольшую функцию, которая расставляет фишки по игровому полю в соответствии с переданным состоянием:

void расставить_фишки(short[16] игра, ЭлементИнфопанели элемент_поле) {
    foreach(short позиция, цифра; игра)
        установить_фишку(цифра, элемент_поле, позиция);
}
  1. Мы, наконец-то, готовы к взаимодействию с пользователем. Сделаем функцию-обработчик события мыши (по обработчикам событий см. уроки 14-15 ).

    1. Объявим функцию, в которой из общего типа событий SDL_Event получим событие, связанное с кнопкой мыши (тип SDL_MouseButtonEvent). Далее, проверяем, что если сработала какая-нибудь другая кнопка, кроме левой, то выходим, ничего не делая:

bool обработка_отжатия_клавиши_мыши(SDL_Event e) {
    SDL_MouseButtonEvent событие_мыши = e.button;
    if (событие_мыши.button != 1)
        return false;

}
    1. Далее, получаем координаты мыши в экранных пикселях. Для координаты y делаем вычитание из высоты экрана:

    int x = событие_мыши.x;
    int y = высота_окна - событие_мыши.y;
    1. Обработка случая нажатия на кнопку «Перемешать»:

    if (инфопанель_кнопка.проверить_точку(x, y)) {
        в_журнал(имя_модуля, УровниЖурнала.Информация, "Нажата кнопка «Перемешать»");
        логика.перемешать();
        расставить_фишки(логика.игра, элемент_поле);
    }
    1. Обработка случая нажатия на игровое поле. Здесь сначала получаем координаты в относительных единицах, затем вычисляем номер соответсвующей позиции. Если эта позиция положительна (т. е. нажатие на фишку, а не на рамку), то с помощью объекта логика пытаемся делать ход. Если и тут у нас положительный ответ (т. е. ход совершён), то мы соответствующим образом переставляем фишку.

    else if (инфопанель_поле.проверить_точку(x, y)) {
        auto внутренние_координаты = инфопанель_поле.внутренние_координаты_точки(x, y);
        auto позиция = вычисление_позиции(внутренние_координаты.x, внутренние_координаты.y);
        в_журнал(имя_модуля, УровниЖурнала.Информация, format("Нажатие в позиции = %s", позиция));  
        if (позиция >= 0) {
            auto новая_позиция = логика.ход(позиция);
            if (новая_позиция >= 0) {
                установить_фишку(логика.игра[новая_позиция], элемент_поле, новая_позиция);
            }
        }
    }
    1. Из функции-обработчика нужно вернуть false (как вы помните из 15-го урока, возврат значения true приводит к выходу из программы):

    return false;
    1. В конце функции сцена() зарегестрируйте наш обработчик, это функция первого типа:

    обработчик_событий.зарегистрировать_функцию1(SDL_MOUSEBUTTONUP, toDelegate(&обработка_отжатия_клавиши_мыши));

Если всё сделано правильно, уже сейчас можно полноценно играть:



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

  1. Добавьте перед всеми функциями ещё несколько глобальных констант и переменных:

enum float СКОРОСТЬ = 4.0;
enum float ЭПСИЛОН = 1e-5;

float текущий_x, цель_x, текущий_y, цель_y;
bool движение;
ЭлементИнфопанели двигающаяся_фишка;
  1. Нужно немного изменить функцию обработка_отжатия_клавиши_мыши() для её соответствия новым реалиям.

    1. Чтобы программа не реагировала на нажатие мыши во время движения фишки, добавьте эту проверку в самое начало функции:

    if (движение)
        return false;
    1. И закомментируйте строку перемещения фишки, добавленную нами в шаге 6.d., а вместо неё добавьте несколько строк для инициализации движения:

            if (новая_позиция >= 0) {
                //установить_фишку(логика.игра[новая_позиция], элемент_поле, новая_позиция);
                двигающаяся_фишка = элемент_поле.получить_элемент(to!string(логика.игра[новая_позиция]));
                текущий_x = КООРДИНАТЫ[(позиция) % 4];
                текущий_y = КООРДИНАТЫ[3- ((позиция) / 4)];
                цель_x = КООРДИНАТЫ[(новая_позиция) % 4];
                цель_y = КООРДИНАТЫ[3- ((новая_позиция) / 4)];
                движение = true;
            }
  1. Напишем функцию обновление_сцены().

    1. Мы в ней делаем что-либо, только если в данный момент есть движение:

void обновление_сцены(float длительность_кадра) {
    if (движение) {
        float смещение = длительность_кадра * СКОРОСТЬ;

    }
}
    1. Обработка случая движения по горизонтали:

        if (abs(цель_x - текущий_x) > ЭПСИЛОН) {
            if (abs(цель_x - текущий_x) < смещение) {
                текущий_x = цель_x;
                движение = false;
            }
            else {
                if (цель_x < текущий_x)
                    смещение = -смещение;
                текущий_x += смещение;
            }
        }

Как можно понять, мы выполняем движение, если значения переменных цель_x и текущий_x отличаются. Фраза «выполняем движение» означает, что мы приближаем значение текущий_x к значению цель_x на величину, не большую значения ранее высчитанного смещения. Если эти значения были ближе, чем это самое смещение, то мы заканчиваем движение.

    1. Обработка движения по-вертикали аналогична:

        if (abs(цель_y - текущий_y) > ЭПСИЛОН) {
            if (abs(цель_y - текущий_y) < смещение) {
                текущий_y = цель_y;
                движение = false;
            }
            else {
                if (цель_y < текущий_y)
                    смещение = -смещение;
                текущий_y += смещение;
            }
        }
    1. Теперь устанавливаем саму фишку в соответствии с высчитанными координатами:

        if (!(двигающаяся_фишка is null))
            двигающаяся_фишка.задать_координаты(текущий_x,
                                текущий_y,
                                ЕДИНИЧНОЕ_РАССТОЯНИЕ,
                                ЕДИНИЧНОЕ_РАССТОЯНИЕ);
  1. Осталась самая малость. В функции сцена() отключите движение – при старте программы его не должно быть:

    движение = false;
  1. В функции main() модуля urok.d после вызова функции сцена() укажите менеджеру нашу функцию из п.9 в качестве функции обновления сцены:

    менеджер.задать_функцию_обновления_сцены(&обновление_сцены);

Если всё сделано правильно, то сейчас фишки должны двигаться по игровому полю плавно. Теперь этот довольно-таки громоздкий урок можно закончить.