Все статьи / Игра Memory Trainer 3D

На этот раз мы реализуем полноценную интерактивную игру, рисуемую в 3D пространстве и позволяющую мышью активировать 3D объекты.


Основное задание на сегодня - разработать игру Memory Trainer 3D. Мы можете разделять роли, чтобы разработать проект максимально быстро.

Подготавливаем проект

Обновите локальный репозиторий cg-course-2018/QtLabs2D, переключитесь на ветку sshambir_all_samples. Затем создайте новую ветку, уникальную для вас и этого задания (например, %фамилия%_sample10).

Затем откройте QtLabs2D.sln и соберите проекты. Запустите Sample10 - вы должны увидеть следующее:

Скриншот

Откройте проект Sample10 - в нём вы будете выполнять разработку.

Геймплей Memory Trainer 3D

Суть игры заключается в следующем:

  • пользователю показывается игровое поле M * N табличек (всего табличек четное число)
  • изначально таблички повёрнуты оборотной стороной (текстура оборотной стороны одинакова для всех табличек)
  • при щелчке по плитке она плавно переворачивается лицевой стороной, на которой нарисована некоторая картинка
  • если до этого была открыта плитка с тем же рисунком, то обе плитки удаляются с поля (игрок получает 50 очков)
  • в противном случае обе плитки плавно поворачиваются обратно рубашкой вверх (игрок теряет 10 очков)
  • повторный щелчок по ячейке, повернутой к нам картинкой, вызывает ее обратный поворот рубашкой вверх.

Цель игры – убрать все плитки с игрового поля. Скриншот игры представлен ниже:

Иллюстрация

Управление состоянием плитки

Можно запрограммировать плитку таким образом, чтобы у неё было 4 дискретных состояния:

enum class State
{
    FacedFront,
    Teasing,
    FacedBack,
    Dead,
};

Правила переключения состояний будут следующими:

  • изначальное состояние будет равным FacedBack, но при этом в начале игры все плитки плавно перевернутся вокруг оси Oz вниз лицевой гранью, вместо того чтобы быть повёрнутыми изначально
  • при нажатии на плитку, повёрнутую лицевой гранью вниз, она переключается в состояние FacedFront и плавно переворачивается вокруг оси Oz
  • если две ранее перевёрнутые плитки имеют разные изображения, они переключаются в состояние Teasing и “дразнят” игрока, прежде чем перевернуться, путём поворота вокруг оси Oy
  • если две ранее перевёрнутые плитки имеют одинаковые изображения, они исчезают, при этом проигрывается анимация уменьшения масштаба плитки от 1 до 0, и всё это время плитка повёрнута лицевой гранью вверх

Задание cg10.1: триангуляция двухстороннего quad

Чтобы нарисовать плитку, имеющую разные изображения с двух сторон, добавьте двухсторонний quad, позволяющий задать разные текстурные координаты для передней и задней стороны. Также плитка будет иметь трёхмерные координаты и нормаль, при этом нормали передней и задней сторон отличаются.

В заголовок MeshP3N3T2.h добавьте объявление функции:

// Генерирует список вершин отдельных треугольников, формирующих двухсторонний quad.
// @param frontTextureRect - текстурный прямоугольник передней грани
// @param backTextureRect - текстурный прямоугольник задней грани
MeshDataP3C3 tesselateTwoSideQuad(const FloatRect& frontTextureRect, const FloatRect& backTextureRect);

Каждая из сторон плитки, по сути, является sprite, на котором рисуется картинка:

Иллюстрация

Задание cg10.2: переворот плиток (требует cg10.1)

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

Задание cg10.3: плавная анимация плиток (требует cg10.2)

Анимация переворота плитки должна быть плавной. Для создания плавной анимации создайте класс AnimationCounter, который будет следить за фазой анимации (будем считать, что фаза изменяется от 0 до 1). Используйте класс AnimationCounter вместе с классом Transform3D для реализации плавного переворота плитки. Процесс изменения фазы будет выглядеть следующим образом:

Иллюстрация

Ниже приведено примерное определение класса. Вам нужно создать одноимённый интерфейс (IAnimationCounter) с эквивалентными возможностями.

class AnimationCounter
{
public:
    AnimationCounter(float changeSpeed);

    float GetPhase()const;
    bool IsActive()const;

    // Перезапускает изменение фазы анимации.
    void Restart();

    // Продолжает прирост фазы либо делает анимацию
    //  неактивной, если фаза достигла 1.
    void Update(float deltaSeconds);

private:
    bool m_isActive = false;
    float m_phase = 1.f;
    float m_changeSpeed = 0;
};

Начальная фаза анимации равна 1, как если бы она остановилась и ещё не была перезапущена.

В реализации основную сложность представляет метод Update, который должен передвинуть фазу ровно до конечного значения, если он достижимо за deltaSeconds или за меньшее время:

AnimationCounter::AnimationCounter(float changeSpeed)
    : m_changeSpeed(changeSpeed)
{
}

float AnimationCounter::GetPhase() const
{
    return m_phase;
}

bool AnimationCounter::IsActive() const
{
    return m_isActive;
}

void AnimationCounter::Restart()
{
    m_phase = 0;
    m_isActive = true;
}

void AnimationCounter::Update(float deltaSeconds)
{
    if (!m_isActive)
    {
        return;
    }
    const float maxPhase = 1.f;
    const float delta = m_changeSpeed * deltaSeconds;
    if ((maxPhase - m_phase) < delta)
    {
        m_phase = maxPhase;
        m_isActive = false;
    }
    else
    {
        m_phase += delta;
    }
}

Задание cg10.4: рисование в два прохода

Добавьте шейдер для 2D рисования draw2d.frag, который будет копировать цвет из текстуры (используя его как текст фрагмента) без какой-либо имитации освещения. Добавьте шейдер draw2d.vert, который будет просто трансформировать переданную позицию типа vec2. Оба шейдера должны быть помещены в каталог res/.

Скопируйте из предыдущих примеров в проект Sample10 следующие функции:

// Генерирует список вершин треугольников для выпуклого многоугольника, заданного вершинами и центром.
//  @param center - геометрический центр многоугольника
//  @param hullPoints - вершины многоугольника
//  @param colorGen - генератор цвета полученных треугольников
std::vector<VertexP2C4> tesselateConvexByCenter(const glm::vec2 &center, const std::vector<glm::vec2> &hullPoints, const glm::vec4 &fillColor);

// Генерирует список вершин треугольников для выпуклого многоугольника, заданного вершинами.
std::vector<VertexP2C4> tesselateConvex(const std::vector<glm::vec2> &verticies, const glm::vec4 &fillColor);

// Функция делит круг на треугольники,
//  возвращает массив с вершинами треугольников.
std::vector<VertexP2C4> tesselateCircle(float radius, const glm::vec2 &center, const glm::vec4 &fillColor);

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

Задание cg10.5: реализовать интерфейс игры

Спроектируйте классы, ответственные за реализацию интерфейса. Реализуйте интерфейс игры.

Задание cg10.6: формируем луч для трассировки по сцене

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

Иллюстрация

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

Иллюстрация

Задание cg10.7: генерация поля

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