Урок №9: Финальные штрихи в создании игры «SameGame» на C++/MFC

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

  |

  Обновл. 20 Июл 2021  | 

 23843

 ǀ   8 

Мы уже практически завершили создание нашей игры, по пути обсудив немало тем, начиная с обработки возникающих событий до программирования 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 позволяет нам отключить параметры меню, когда нет действий для выполнения «Отмена/Повтор». После добавления всех 4 обработчиков событий следующий код будет автоматически добавлен в заголовочный файл 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. Можно было бы также реализовать функционал подсказок, которые подсказывали бы следующие возможные ходы. Попробуйте сами реализовать некоторые из этих идей.

Удачи!

  GitHub / Исходный код — Урок №9: Финальные штрихи в создании игры «SameGame» на C++/MFC


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

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

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

  1. Сергей Федоров:

    Спасибо большое за курс! Эх как же далеко ушли технологии программирования от моих ДВК-ашных уровней вообще мало чего осталось… Теперь мало выучить язык и даже знание функция библиотек поможет только отчасти.
    Важно уметь пользоваться IDE почти на уровне вслепую…

  2. Илья:

    Здравствуйте, можете подсказать как сделать ведение счета?

  3. Владимир:

    Спасибо за уроки. А c большим количеством документов MFC на много сложней?

  4. Raimok:

    Файл SameGameDoc.h не отредактирован до конца, нет 4 прототипов новых функций в public, а в имеющихся не отредактирован синтаксис с "точки" на "стрелку".

    Большое спасибо за курс, познавательно и доходчиво =)

  5. Sergey:

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

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

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

      1. tr.:

        Добрый вечер! А будут ли уроки по WinApi?

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

          А что конкретно из WinApi интересует? 🙂

Добавить комментарий для Raimok Отменить ответ

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