Програмування логічної гри з використанням DirectX. Хрестики нолики.

Не так давно я почав вивчати бібліотеку DirectX, і, природно, мені тут же захотілося написати свою власну іграшку. Інформації на цю тему маса, починаючи від опису базових алгоритмів, і закінчуючи повністю готовими іграми. З вибором теми гри я довго не мучився. Варіанти типу DOOM4 і Elder Scroll's 5 :-) я відкинув відразу. Хотілося написати щось по швидкому, і, в той же час, повністю самостійно (без використання готових движків, моделей і т.п.). Тому вибір припав на просту логічну іграшку - хрестики-нулики. Крім простоти реалізації ця гра має ще одним дуже важливим гідністю - нікому не потрібно розповідати правила 🙂.

Про програму
опис програми
висновок
завантажити

Про програму

Ця програма істотно відрізняється від більшості прикладів, розміщених на сайті. По-перше, вона написана на С ++ з допомогою Visual Studio.NET.
По-друге, в ній використовуються дві бібліотеки: API Windows і DirectX9. Відповідно, для складання проекту повинен бути встановлений DirectX SDK дев'ятої версії.
API Windows використовується тільки для створення вікна і обробки повідомлень миші. DirectX - для створення інтерфейсу користувача (хрестики, нолики, сітка, меню і т.п.).
Крім того, хочу відразу попередити, це одна з моїх перших програм, написана з використанням DirectX. Тому не варто використовувати її як зразок для ваших власних розробок, особливо, якщо вони великі (наприклад, движок). З іншого боку, працює вона правильно (саме так, як було задумано), а не зовсім оптимальний код практично не помітний в іграх такого масштабу :-). Так що, вибір за вами.
По-третє, для роботи програми крім файлу xzgame.exe необхідні файли моделей і текстур (якщо ви використовуєте інсталятор, то вони встановляться автоматично).
По-четверте, я зробив інсталятор з допомогою програми Inno Setup. Конфігураційний файл називається dist.iss.


опис програми

Програму можна умовно розділити на дві частини:

  • перша - зберігає інформацію про розташування хрестиків та нуликів, виконує розрахунок чергового ходу;
  • друга - малює ігрове поле, виконує обробку команд користувача і т.п.

Так як гра досить проста, то всю роботу, пов'язану зі зберіганням інформації про гру і вибором чергових ходів, виконує лише один клас - XZField. Його структура показана на рис.1.
Так як гра досить проста, то всю роботу, пов'язану зі зберіганням інформації про гру і вибором чергових ходів, виконує лише один клас - XZField

Рис.1. Клас XZField.

Користуватися ним дуже просто. За допомогою методу setElement можна додати хрестик або нулик в задану клітинку. Метод getElement повертає елемент, розташований в заданій комірці. Вибір наступного ходу здійснюється методом findNextMove. Як параметри цього методу передаються посилання на змінні, в яких потрібно зберегти координати обраного ходу. Якщо новий хід був знайдений, метод повертає true, в іншому випадку - false.

приклад:

XZField f; . . . int row, col; // змінні, в яких зберігаються координати наступного ходу // шукаємо наступний хід if (f.findNextMove (row, col)) {f.setElement (f.ZERO, row, col); // встановлюємо елемент}

Метод isGameOver дозволяє визначити чи закінчена гра, і хто виграв. Якщо гра закінчена, повертає true, в іншому випадку - false. У параметрі winner зберігаються дані про переможця (хрестики, нолики або нічия).

Більш докладний опис роботи методів класу можна почитати в коментарях до вихідного коду класу.

Друга частина програми значно об'ємніше. Вона відповідає за промальовування всіх елементів гри (створення вікна, меню, хрестики, нолики і т.д.), взаємодія з користувачем (обробка повідомлень миші).

Таким чином, нам потрібно кілька об'єктів з різними властивостями і призначенням:

  • об'єкти гри (хрестик, нулик, сітка, прямокутник виділення) - кожен з них є 3D моделлю і зберігається в файлі з розширенням .х в папці models (cross.x, zero.x, grid.x, selectionPlane.x). Всі ці моделі повинні бути завантажені перед початком гри, також необхідний метод для промальовування (бажано один);
  • меню - являє собою квадрат з текстурою, на якій намальовані зображення кнопок, текстові повідомлення та ін .. Текстура повинні завантажуватися з файлів при завантаженні програми. Потрібні методи для зміни текстур і промальовування меню;
  • штучний інтелект (AI) - це вже розглянутий клас XZField;
  • створення вікна і обробка введення користувача - потрібно створити звичайне вікно, створити об'єкт і пристрій Direct3D, встановити обробники повідомлень миші, також потрібен метод для перемальовування вікна;
  • повідомлення про помилки - навіть в найпростіших програмах, можуть виникнути десятки різних помилок, тому необхідний загальний підхід до їх обробці.

На рис.2 зображена діаграма класів гри. На ній прямокутниками зображені класи, а стрілками показані зв'язку між ними. Розберемо докладніше, що це все означає і як працює.
На рис

Рис.2. Діаграма класів гри

Отже, в цій грі я використовував наступні класи:

  • BaseWindowClass
  • XZGame
  • ExceptionBase
  • XZField
  • NewGameMenu
  • XZMesh

Тепер подивимося, що вони роблять, і як пов'язані один з одним.

Клас BaseWindowClass використовується для створення порожнього вікна. Використовувати його безпосередньо не можна, тому що він містить абстрактний метод Render (). Тому нам потрібно створити дочірній клас, в якому цей метод буде виконувати рендеринг об'єктів гри. Але про це трохи пізніше, а зараз розглянемо як користуватися іншими методами класу BaseWindowClass.

Конструктор - нічого не робить.
Create - створює вікно. Як параметри можна вказати:

  • hInstance - ідентифікатор додатки;
  • szTitle - назва програми;
  • iStyle - стиль вікна;
  • iWidth - довжина вікна;
  • iHeight - ширина вікна.

Це звичайні параметри, які вказуються при створенні віконних додатків, тому я не буду їх тут докладно пояснювати.
WndProc - це віконна процедура. Викликається операційною системою, для передачі повідомлень вікна.

Run - цей метод запускає цикл обробки повідомлень. У кожному циклі є виклик методу Render ().

MessageHandler - в цьому методі виконується обробка повідомлень, відправлених вікна. Написаний обробник повідомлення WM_DESTROY, яке додаток отримує, коли користувач закриває додаток. Обробка цього повідомлення є однаковою для більшості додатків (якщо користувач натиснув кнопку «закрити», значить треба завершувати роботу), тому я написав обробник в базовому класі. Ви, напевно, помітили, що цей метод оголошений віртуальним. Навіщо нам це потрібно? В першу чергу давайте подивимося, хто викликає цей метод. Повідомлення вікна передає операційна система за допомогою віконної процедури, яка в свою чергу викликає обробник. Тепер уявімо, що нам потрібно обробляти інші повідомлення (наприклад, переміщення миші). Можна, звичайно додати відповідні обробники прямо в текст методу, тобто кожен раз міняти код базового класу, а це не правильно. Набагато зручніше створити в похідному класі такий же метод (MessageHandler), і додати обробники в нього. Тобто у нас буде два методу MessageHandler, один - в базовому класі, інший - в похідному. При цьому нам потрібно, щоб віконна процедура викликала метод похідного класу (з нього, при необхідності, ми можемо викликати метод базового класу). Так ось, оголошення методу віртуальним гарантує, що ми отримаємо саме така поведінка. Тим, хто хоче розібратися докладніше з цим моментом, я раджу почитати якусь книжку з об'єктно-орієнтованого програмування.

Render - оголошений абстрактним. Це означає, що в похідному класі нам доведеться написати його реалізацію. Тобто оголосити точно такий же метод, і написати код, який він буде виконувати (в принципі, цей метод можна залишити порожнім, якщо ви не хочете нічого малювати).

Тепер давайте подивимося на клас XZGame. Він є нащадком BaseWindowClass, і движком гри, тобто пов'язує всі компоненти гри разом, виконує обробку команд користувача, виводить результати гри і ін.

Роботу цього класу в якості движка ми розглянемо далі, а зараз я тільки покажу які методи класу BaseWindowClass ми переобумовленої і які додаємо. В першу чергу нам потрібен метод initD3DAndGameObjects (). Він виконує ініціалізацію DirectX і всіх об'єктів гри. Далі ми метод setMatricesAndRenderStates () в якому налаштовуємо пристрій Direct3D і матриці (світу, виду і проекційну). Потім ми переобумовленої метод MessageHandler, в якому у нас містяться обробники повідомлень. І останній метод - Render (). У ньому ми будемо малювати об'єкти гри (хрестики, нолики, ігрове поле, меню).

Тепер подивимося як користуватися цим класом. Все дуже просто. У функції WinMain нам потрібно написати приблизно такий код:

// створюємо екземпляр класу XZGame XZGame game; // створюємо вікно game.Create (hInst, "Хрестики-нулики 1.0", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 640, 480); // инициализируем Direct3D і об'єкти гри game.initD3DAndGameObjects (); // встановлюємо матриці і налаштовуємо параметри відображення сцени game.setMatricesAndRenderStates (); // входимо в цикл обробки повідомлень game.Run ();

В результаті буде запущена наша іграшка.
Наступний клас - ExceptionBase. Це сервісний клас. Він використовується всіма класами для передачі повідомлень про помилки. Такий підхід дозволяє створити загальну для всієї програми систему повідомлення про помилки.
Користуватися ним дуже просто. Якщо виникла помилка, ми створюємо об'єкт цього класу із зазначенням опису помилки, та генеруємо виняток. наприклад:

ExceptionBase error (111, "Якась помилка"); throw error;

Тут, 111 - код помилки, «Якась помилка» - опис помилки.
Перехоплення винятків виконується як звичайно:

try {// код програми} // обробка винятків типу ExceptionBase catch (ExceptionBase err) {MessageBox (NULL, (const char *) err, "Помилка", MB_ICONERROR); } // обробка винятків інших типів catch (...) {}

Тут все просто, але я хочу звернути вашу увагу на оператор (const char *) err. Він виконує перетворення об'єкта типу ExceptionBase до типу const char *. Використовуючи таке перетворення можна легко отримати рядок, що містить опис помилки. Є також оператор приведення до типу int, який повертає код помилки.
Клас XZMesh.

Як вам відомо, практично всі об'єкти в іграх представляють собою меши, тобто набори пов'язаних між собою вершин. Для їх створення найзручніше скористатися яким-небудь 3D редактором. Наприклад, я користуюся Blender'ом (повністю безкоштовна програма, причому дуже не великого розміру). Ви можете скористатися будь-яким іншим редактором в залежності від ваших уподобань, знань, навичок і т.п., головне щоб ви могли в ньому намалювати те, що вам хочеться :-).
І ще один важливий момент з приводу редакторів. Після того, як ви намалювали красивий об'єкт, вам потрібно буде завантажити його в вашу програму, і тут виникає цікавий момент. Кожен редактор використовує свій власний формат для зберігання файлів, тому переконайтеся, що у вас є можливість імпортувати файли в .х формат (це єдиний формат, який підтримується бібліотекою DirectX). Для Blender'а я використовував DirectX8ExporterMod , Для 3Dmax'a і Maja конвертер є в DirectX SDK. Втім, якщо ви займаєтеся створенням движка або просто великим проектом, то можна створити завантажувач файлів для потрібного формату (процедура дуже трудомістка, але зазвичай себе виправдовує: збільшується швидкість завантаження, можна завантажувати додаткову інформацію і т.д.).
Але повернемося до нашого класу. Він дуже простий (практично всю роботу виконує DirectX). При виклику конструктора потрібно вказати ім'я файлу, в якому знаходиться mesh. Для того, щоб намалювати mesh викликаємо метод render (). Наприклад так:

XZMesh cross = new XZMesh (pD3DDevice, "modelscross.x"); ... cross.render ();

І це все. Ніяких наворотів, тільки те, що необхідно.
Наступний клас NewGameMenu. Він створює меню, яке бачить гравець перед початком і після закінчення гри. Отже, що являє собою меню? Це просто прямокутник, на який «натягнута» текстура з зображення меню. Тут все просто, але скільки нам потрібно текстур? Давайте рахувати. Існує 4 можливих варіанти:

  • гравець тільки що запустив програму, і ми пропонуємо почати гру;
  • гравець виграв попередню партію, ми виводимо відповідне повідомлення і пропонуємо зіграти ще раз;
  • гравець програв попередню гру;
  • попередня гра закінчилася внічию.

Крім цього я вирішив реалізувати підсвічування кнопок меню, тобто потрібно по одній додатковій текстурі для кожної кнопки (кожне меню в грі має дві кнопки). Таким чином, виходить, що потрібно по 3 текстури на кожен варіант меню. Разом, 4 * 3 = 12 текстур.
Подивимося, як користуватися нашим класом.
При створенні меню нам потрібно вказати положення центру меню на екрані, передати покажчик на масив з іменами текстур, кількість текстур в масиві, довжину і ширину меню, прямокутник в якому потрібно намалювати меню.
Для промальовування меню, як ви здогадалися, використовується метод render ().
Два додаткових методу класу - checkSelectedButtons і setCurrentMenu використовуються разом. Навіщо вони потрібні? Пам'ятайте, я писав, що ми будемо підсвічувати кнопки меню, так ось, щоб включити підсвічування ми повинні перевірити, де знаходиться курсор миші, і встановити відповідну текстуру. Метод checkSelectedButtons як параметр приймає координати курсору миші, і повертає номер обраної кнопки, а метод setCurrentMenu встановлює потрібне меню. Таким чином, якщо викликати ці методи при в обробнику події WM_MOUSEMOVE (виникає при переміщенні курсору миші), то ми отримаємо звичайне меню: навели курсор на кнопку, вона підсвітити, прибрали - підсвічування зникла.
Наводити приклад використання цього класу я не буду, тому що він дуже тісно пов'язаний з іншим кодом проекту (обробкою повідомлень, установкою матриць перетворень і т.п.), тому буде краще, якщо ви подивитеся вихідні програми, і почитаєте коментарі.
Тепер, як я і обіцяв, повернемося до класу XZGame, і розглянемо, як він здійснює управління ходом гри (тобто його роботу в якості движка).
В першу чергу подивимося на рис.3. На ньому зображена діаграма станів класу, на якій зображені всі можливі варіанти ходу гри (ситуації, на зразок натискання на кнопку «Reset» або удару молотком по системному блоку 🙂 тут не враховуються).
Крім цього я вирішив реалізувати підсвічування кнопок меню, тобто  потрібно по одній додатковій текстурі для кожної кнопки (кожне меню в грі має дві кнопки)

Рис.3. Діаграма станів класу

Давайте розберемо все по порядку. У нас є шість основних станів гри, кожному з яких відповідає своя константа в перерахуванні states (див. Вихідний код):

  • NEW_GAME_MENU - початок нової гри, користувач тільки що запустив програму;
  • NEW_GAME_MENU_WIN - початок нової гри, користувач виграв попередню гру;
  • NEW_GAME_MENU_LOST - початок нової гри, користувач програв попередню гру;
  • NEW_GAME_MENU_DRAW - початок нової гри, попередня гра закінчилася внічию;
  • PLAYER_MOVE - очікування ходу гравця;
  • COMP_MOVE - очікування ходу комп'ютера (чекаємо, поки метод findNextMove класу XZField вибере хід).

Отже, гравець тільки що запустив програму.

Об'єкт класу XZGame знаходиться в стані NEW_GAME_MENU, і, відповідно, на екрані відображається меню з пропозицією почати гру. Якщо гравець натискає кнопку «ні» - гра завершується (не зрозуміло - навіщо взагалі він її запускав? Від нудьги клацав по всьому ярликів поспіль :-)). А ось якщо натиснута кнопка «так» - починаємо грати. В першу чергу, програма вибирає, хто першим буде ходити (випадковим чином, за допомогою функції rand ()). Далі, в залежності від результатів попереднього етапу, ми потрапляємо в стан PLAYER_MOVE або COMP_MOVE. У стані PLAYER_MOVE ми просто чекаємо, поки гравець зробить хід (до речі, гравець завжди грає нуликами). А в стані COMP_MOVE ми чекаємо результатів методу findNextMove.

Потім ми перевіряємо стан гри (завершена, не завершена, якщо завершена, то з яким результатом). Якщо гра не була завершена, то ми переходимо або в стан PLAYER_MOVE, або COMP_MOVE, в залежності від того, хто ходив перед цим. Якщо гра завершена, то в залежності від її результатів, ми переходимо до одного з трьох станів: NEW_GAME_MENU_WIN (якщо переміг гравець), NEW_GAME_MENU_LOST (якщо переміг комп'ютер), NEW_GAME_MENU_DRAW (якщо гра закінчилася внічию). Ці стани дуже схожі. Всі три малюють меню з пропозицією почати нову гру. Різниця тільки в структурах, які використовуються для меню (у верхній частині текстури намальована рядок з результатами гри). Далі все просто, якщо гравець натисне кнопку «так» - переходимо в стан розіграшу першого ходу, і процес повторюється, якщо гравець натиснув кнопку «ні» - завершуємо роботу.


висновок

Це дуже проста гра, але, тим не менш, тут використовуються ті ж прийоми, що і в великих проектах. Проблема з великими проектами в тому, що в них, як то кажуть, «за лісом дерев не видно» (особливо, якщо ви тільки почали вивчати DirectX і принципи створення ігор). Звичайно, моя іграшка написана далеко не ідеально. До моменту закінчення роботи над нею, я бачив купу недоліків, і, якби я робив її заново, то можливо вона вийшла б краще і красивіше. Але гра працює. У неї можна грати. А це все-таки позитивний результат.

Загалом, сподіваюся, ця стаття вам допоможе.


Завантажити:

гру хрестики-нулики (setup.exe - 578кБ);

вихідний код (xzgame.zip - 475кБ).

постовий

Просування сайту зробить ваш товар ближче до споживача

Навіщо нам це потрібно?
Отже, що являє собою меню?
Тут все просто, але скільки нам потрібно текстур?
Навіщо вони потрібні?
Не зрозуміло - навіщо взагалі він її запускав?

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

Или позвоните нам по телефонам: (048) 823-25-64

Организация (обязательно) *

Адрес доставки

Объем

Как с вами связаться:

Имя

Телефон (обязательно) *

Мобильный телефон

Ваш E-Mail

Дополнительная информация: