Пошаговое создание игры «Same Game». Финальный урок №9

  Дмитрий Бушуев  | 

    | 

  Обновл. 27 Авг 2019  | 

 738

 ǀ   2 

Мы уже практически завершили создание нашей игры, по пути обсудив немало тем: начиная от обработки возникающих событий до программирования GDI-графики. Некоторые темы, которых мы касались, выходят за рамки программирования игр. Создание приложений MFC является одной из таких тем, т.к. не многие игры написаны с использованием технологии MFC, но при этом список прикладных программ, которые её используют, очень и очень большой. В этом уроке мы рассмотрим реализацию функционала «Отмена/Повтор» действий игрока. «Отмена/Повтор» является важной функцией для большинства приложений.

Реализация функционала «Отмена/Повтор»

Я называю эту функцию стеком «Отмена/Повтор» из-за того, что в её основе лежит принцип «Абстрактных типов данных», к которым относится и стек. Стек — это набор объектов, который похож на стопку тарелок на столе. Единственный способ добраться до нижних тарелок — снять верхние. Чтобы добавить тарелку в стопку, вам нужно поместить её поверх других. Таким образом, реализуется принцип LIFO (с англ. «Last In, First Out» = «Последним Пришёл, Первым Ушёл»).

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

Нам нужно будет создать конструктор глубокого копирования. Для этого добавьте прототип функции конструктора копирования прямо между конструктором по умолчанию и деструктором в SameGameBoard.h:

Мы используем конструктор глубокого копирования, потому что у нас есть указатель на некоторую область динамически выделенной памяти. Это означает, что мы не можем просто скопировать указатель, нам нужно динамически выделить новый блок памяти, а затем скопировать содержимое прошлого блока памяти в новый. Ниже описывается добавление конструктора копирования в исходный файл для игрового поля SameGameBoard.cpp:

Код конструктора копирования очень простой:

   сначала мы копируем переменные-члены класса;

   затем указателю на доску присваиваем значение NULL;

   после этого вызываем функцию CreateBoard(), которая создаёт новый двумерный массив игрового поля того же размера, что и исходный (т.к. мы уже установили количество строк и столбцов перед вызовом функции).

   последними идут 2 цикла for, которые копируют все цвета блоков со старой доски в новую.

Большая часть работы будет выполняться самим Document, т.к. именно он будет создавать цепочку действий «Отмена/Повтор» и хранить оба типа стека. Для этого мы воспользуемся библиотекой STL. Она содержит класс стека, который очень прост в использовании.

Мы передаём классу тип переменной (указатель на SameGameBoard), а он предоставляет нам несколько простых функций для работы со стеком:

   функция push() добавляет новый элемент в стек;

   функция pop() удаляет самый последний элемент из него;

   функция top() возвращает элемент, находящийся на вершине стека;

   функция empty() определяет, является ли стек пустым.

Ниже расположен полный исходный код заголовочного файла SameGameDoc.h со всеми изменениями:

Прежде всего, нам нужно подключить заголовочный файл стека. Поскольку мы собираемся изменить переменную m_board на указатель, то нам придётся перейти от использования оператора прямой принадлежности «точка» к оператору непрямой принадлежности «стрелка» (или к разыменованию указателя в каждой функции Document). Далее мы помещаем реализацию функции DeleteBlocks() в исходный файл.

Затем мы добавляем шесть новых функций, четыре из которых будут располагаться в public-разделе класса, а две другие — в protected-разделе. public-функции разделены на две группы: функции UndoLast() и RedoLast() выполняют «Отмена/Повтор», в то время как функции CanUndo() и CanRedo() являются обычными тестами, которые мы будем использовать для включения и отключения параметров меню, когда они недоступны. protected-функции являются простыми вспомогательными функциями для очистки и освобождения памяти, связанной с обоими типами стеков. В конце, мы добавляем два объявления стеков «Отмена/Повтор».

Файл SameGameDoc.cpp:

Мы предполагаем, что всегда будет действительная игровая доска, на которую ссылается указатель m_board, поэтому она должна создаваться в конструкторе, а удаляться — в деструкторе. Как только объект будет удалён в деструкторе, то мы также должны удалить все другие игровые доски, которые были сохранены, вызвав функции Clear…() для очистки стеков «Отмена/Повтор».

Затем мы добавили изменения в функцию OnNewDocument(), которые очищают стеки «Отмена/Повтор», чтобы новая игра начиналась со свежего набора стеков. Последнее изменение в этом файле — это перемещение функции DeleteBlocks() из заголовочного файла в исходный файл .cpp.

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

Функции UndoLast() и RedoLast() похожи друг на друга. Они выполняют действия в обратном порядке. Сначала мы должны убедиться, что у нас есть возможность отменить или повторить действие. Для этого можно было бы использовать функцию CanUndo() или CanRedo(), но, из соображений эффективности, мы используем STL-функцию empty().

Поэтому, если у нас есть действие, для которого мы можем выполнить «Отмена/Повтор», то мы берём текущую игровую доску и помещаем её в соответствующий противоположный стек: в стек «Повтор», если отменяем действие и в стек «Отмена», если повторяем действие. Затем указатель текущей игровой доски мы устанавливаем на доску, расположенную на вершине стека «Отмена» или стека «Повтор» и достаём её оттуда. Функции CanUndo() и CanRedo() нужны нам для того, чтобы определить, можем ли мы отменить/повторить действие.

Последние две функции, добавленные в класс Document, нужны для очистки и освобождения памяти, используемой различными стеками. Мы просто перебираем все указатели в стеке, удаляя объект, а затем выталкиваем указатель из стека. Это гарантирует то, что вся память будет освобождена.

Сейчас нам нужно внести изменения во View. Эти изменения являются добавлением простых обработчиков событий для параметров меню «Отмена» и «Повтор». Сначала мы создаём их с помощью кнопки «События» (молния) на панели «Свойства» файла CSameGameView.h. Мы хотим добавить обработчики ON_COMMAND и ON_UPDATE_COMMAND_UI. Обработчик ON_UPDATE_COMMAND_UI позволяет нам отключить параметры меню, когда нет действий для выполнения «Отмена/Повтор». После добавления всех четырёх обработчиков событий следующий код будет автоматически добавлен в заголовочный файл SameGameView.h:

Примечание: При попытке изменить ID функции On32771() и OnUpdate32771() на ID_EDIT_REDO, MS Visual Studio (2017/2019) выдавал ошибку, поэтому пришлось оставить всё, как есть.

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

Получаем указатель на Document, вызываем функцию в Document и, наконец, перерисовываем View. Сделаем это для «Отмена/Повтор»:

Теперь очередь пришла и до обработчиков событий для ON_UPDATE_COMMAND_UI. Новой здесь является функция Enable(), которая указывает на то, следует ли включить или отключить опцию меню на основе результата, который возвращает функция CanUndo() или CanRedo():

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

Акселераторы


Попробуйте нажать Ctrl+Z после того, как вы сделали несколько ходов. Вы увидите, что отмена действия работает также и по комбинации клавиш. Теперь попробуйте нажать Ctrl+Y для «повтора» действия. Сработало? Нет? Мы можем это исправить. Обратите внимание, что в опции меню для повтора мы указали пользователю, что Ctrl+Y отправит ON_COMMAND в ID_32771. Вот это и называется акселератором.

Чтобы получить доступ к акселераторам, откройте «Редактор ресурсов» из меню Вид > Другие окна или нажмите сочетание клавиш Ctrl+Shift+E. Затем откройте опцию акселератора в SameGame.rc и дважды щёлкните по IDR_MAINFRAME, чтобы вызвать редактор акселератора. На скриншоте ниже мы добавили акселератор для команды «Повтор»:

Чтобы добавить свой собственный акселератор, нажмите на пустую строку после последнего акселератора в столбце ID — это вызовет выпадающее меню, которое позволит выбрать ID_EDIT_32771 (идентификатор опции меню для команды повтора). Присвойте ему ключ Y и модификатор Ctrl (Ctrl+Y). Теперь скомпилируйте свою игру и запустите её. Мы только что добавили комбинацию клавиш, которая теперь отправляет ON_COMMAND в ID_EDIT_32771.

Заключение

Это было ещё то путешествие! Мы с нуля сделали полноценную игру, рассмотрели множество тем, связанных с разработкой игр и работой с платформой Windows. Создание игр — это круто, и я надеюсь, что вы тоже получили удовольствие от процесса и увидели, что в создании игр нет чего-то сверхъестественного. Я думаю, что вы вдохновлены на самостоятельное продвижение и воплощение своих идей.

Есть ещё много опций, которые мы могли бы добавить в эту игру, включая ведение счёта и отслеживание максимальных результатов по баллам или сохранение параметров в реестре, чтобы в следующий раз, когда вы будете открывать игру, программа запомнила, что вы играете на уровне с 7 цветами и блоками 10×10 на игровом поле 40×40. Можно было бы также реализовать функционал подсказок, которые подсказывали бы следующие возможные ходы. Попробуйте сами реализовать некоторые из этих идей.

Удачи!

Исходный код SameGame. Финальный урок №9


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (3 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 2

  1. Аватар Sergey:

    Будет очень круто, если будет рассмотрен, ещё какой-нибудь проект с MFC. Спасибо за уроки.

    1. Дмитрий Бушуев Дмитрий Бушуев:

      Пожалуйста. Насчет еще одного проекта с MFC — я думаю, что в ближайшем будущем можно рассмотреть что-нибудь еще 🙂

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *