Здравствуйте, Дорогие друзья! Сегодня мы немного отвлечемся от создания приложения для автосервиса на WinApi и напишем настоящую мини-игру для ПК. Скажу сразу, пояснения к коду я буду давать только там, где это необходимо, то есть только в тех ситуациях, которые в прошлых статьях мы с Вами не рассматривали. Что же за игру мы будем писать? Самым простым вариантом было бы начать с игрушки из древних аркадных автоматов, в которой нужно управлять космическим кораблем и сбивать корабли пришельцев. Будет ли практическая польза от этой статьи? Будет! Ведь она задумывается не просто так: сегодня мы познакомимся с таймерами (сами понимаете, в приложении для автосервиса таймеры ни к чему, поэтому нужно рассмотреть принцип их реализации на чем-то другом). Итак, первым делом определимся с фоном приложения. Предлагаю сделать его следующим: черный фон, по бокам звезды.
Теперь продумаем логику: пусть в игре будут корабли противника, метеориты и ящики со снарядами и запчастями. Корабли мы должны сбивать, от метеоритов уворачиваться, ящики – ловить. Если столкнулся с кораблем противника или метеоритом – GAME OVER! Как мы будем реализовывать всё это? Можно конечно прорисовать курсоры в виде кораблей и метеоритов, но мы обойдемся обыкновенными статиками. Тем более фон у нашей игры черный и однотонный, а это значит и мудрить особо не придется. Нарисуем картинки, наклеим эти картинки на статики и будем двигать их постепенно по полю игры. В момент уничтожения вражеского корабля будем просто подменять картинку на соответствующем статике. Итак, рисуем корабль игрока. За размер клетки примем область 66 х 66 пиксел. «Рисуем», конечно, громко сказано: идем в Интернет и по запросу «Пиксельная графика для игр космос» находим подходящую картинку. Обрабатываем ее и ву-а-ля:
Теперь корабли противника. Найдем что-нибудь интересное. Например, вот, неплохой вариант:
Осталось сделать метеорит, ящик с патронами и ящик с запчастями:
Также нам понадобится картинка уничтожения корабля противника и картинка выстрела:
Что ж, кажется, все заготовки сделали. Теперь можно браться и за реализацию игры. Заходим в Dev и создаем классическое приложение. Я назову его SpaceFighter. Как всегда, на старте имеем пустую заготовку:
Делаем, что уже умеем: устанавливаем необходимый размер окна, меняем заголовок, загружаем фоновой рисунок. Эти действия я подробно описывать не буду, так как всё это делалось в статьях по созданию приложения для автосервиса.
Теперь добавим наш истребитель. Снизу оставим место под полосу здоровья. Думаю 50 пиксел вполне хватит. Ширина окна приложения 870 пиксел. Высоту сделаем побольше: 640. При этом нужно учитывать и высоту рамки. Размеры картинки с истребителем 66х66 пиксел. Значит координаты статика под него: х=870/2-33=402, у=640-66-50-60=476. Картинку на статик мы еще не загружали. Чтобы это сделать нужно воспользоваться функцией LoadBitmap() для создания объекта типа картинка в программе, а затем отправить сообщение статику с кодовым словом STM_SETIMAGE. В файле resources.h подгружаем картинку с истребителем:
Расписывать синтаксис функции LoadBitmap() я не буду, так как мы уже рассматривали ее в первой статье цикла по WinApi. Теперь перейдем в main.cpp и сразу после создания статика под истребитель, отправим ему сообщение с помощью функции SendMessage():
Компилируем, смотрим, что у нас получилось:
Отлично! Картинка есть, но толку от нее, если мы не можем ее двигать с помощью клавиатуры. Обрабатывать сигналы от клавиатуры мы с Вами уже умеем. Не пугайтесь: мы не будем использовать для этого хуки, ведь у нас всего один объект, которым мы должны управлять с клавиатуры, да и фокус ввода он у главного окна забирать не будет. Договоримся, что наш истребитель сможет летать только влево-вправо. Вперед-назад мы его двигать не будем. Вы уже знаете (если читали предыдущие статьи цикла), что когда мы нажимаем какую-либо клавишу на клавиатуре, система отправляет процедуре главного окна сообщение с кодом нажатой кнопки. Код сообщения для нажатой клавиши «влево» - VK_LEFT, для кнопки «вправо» - VK_RIGHT. Двигать статик мы будем уже знакомой нам функцией SetWindowPos():
Компилируем, запускаем. Сначала сдвинем статик с истребителем влево, затем вправо, потом еще раз повторим.
Мы не только кликали кнопки «влево» и «вправо», но и подолгу удерживали их. В обоих случаях программа работала без ошибок.
Теперь нам нужно ограничить движение статика внутри главного окна. Делаем это с помощью простых условий:
Двигаться наш космолет мы научили, теперь нужно научить его стрелять. Это значит, что пришло время создать наш первый таймер. Таймер – это счетчик, который задействует определенную аппаратную часть процессора для отсчета времени. Таймер должен быть независимым, то есть он не должен реагировать на наше взаимодействие с программой.
Сначала напишем код, который будет обрабатывать только одно нажатие на пробел.
Для создания таймера мы используем функцию SetTimer(). Найдем ее в справочнике:
Первый параметры функции, hwnd – дескриптор окна, которое будет получать сообщения таймера. Сюда мы укажем дескриптор главного окна.
Второй параметр, nIDEvent – Идентификатор таймера. Идентифицируем его в файле resources.h и присвоим ему произвольное числовое значение.
Третий параметр, uElapse – время таймера в миллисекундах.
Четвертый параметр, TIMERPROC – сюда либо указывается идентификатор сброса, либо NULL. Если укажем NULL, то через каждые uElapse миллисекунд таймер будет отправлять окну hwnd сообщение WM_TIMER.
Добавим в обработчик блок кода, который будет обрабатывать сообщение WM_TIMER:
Мы сделали переменные, которые отвечают за координаты статиков глобальными и теперь можем двигать их автоматически.
Проверим, как работает программа. Запустим приложение и нажмем пробел:
Таймер действительно работает, однако, если мы повторно нажмем пробел, первый «снаряд» остановится в полете, а второй мгновенно догонит его и начнет двигаться с той позиции игрека, на которой тот остановился. Это происходит потому, что при повторном нажатии на пробел, создается новый статик с именем hShot, а со старым связь теряется.
Чтобы этого избежать нам нужно создать функцию, которая будет проверять сколько выстрелов сделал истребитель и добавлять в программу новый статик. Сделаем так, чтобы одновременно в полете не могло быть больше десяти снарядов. Соответственно, добавим в программу еще десять дескрипторов: от hShot_0 до hShot_9, а также по десять переменных, в которых будем хранить координаты «снарядов».
К сожалению, мы не можем создать универсальную функцию, меняя параметры которой можно было бы создавать «снаряды». Дело в том, что в таком случае, дескрипторы окошек, которые мы будем создавать, будут инициализироваться как «обезличенные» и программа не сможет ими манипулировать. Поэтому придется создать функцию, в которой мы всё пропишем ручками для каждого «снаряда»:
Таймер мы будем запускать единожды (пока что отсюда), при первом выстреле. Позже мы станем запускать таймер при старте игры, так как нам понадобится двигать по полю корабли противника. В обработчике команд также прописываем поведение статиков при получении главным окном сообщения WM_TIMER:
Запускаем приложение, смотрим, что у нас получилось:
Отлично! Мы можем сделать сразу 10 залпов. Мало? Возможно. Но от этого игра будет только интереснее. Позже нужно будет сделать шкалу здоровья и шкалу боеприпасов. А пока что добавим в программу корабли противника. Пусть они появляются в рандомной иксовой координате, но притом в фиксированной игрековой. Корабли противника будут появляться раз в пять секунд (для первого раунда хватит). Создадим функцию, которая будет создавать корабли врага. Организуем ее по уже знакомому нам принципу:
Таймер будем создавать не по нажатию пробела, а из WM_CREATE. В обработчик команд добавим код, который будет двигать созданные корабли навстречу нашему истребителю:
Ну и, собственно, как-то так мы будем пока что задавать промежуток времени, через который будут появляться корабли:
Проверим, как работает наша программа:
Видите: таймер один, а объектов, которыми мы с помощью него управляем, много.
Истребитель есть, корабли противника есть и появляются они постепенно, как и должны. Теперь нам нужно реализовать функцию, которая будет отслеживать положение корабля противника и выпущенного нашим истребителем снаряда, и при их «встрече», уничтожать корабль, кратковременно подменяя его изображение на статике на то, которое приведено на рисунке 8, а затем удалит и сам статик. Функция будет работать по простейшему принципу: возьмет диапазон значений по иксу и проверит, попадает ли в него одна из переменных, обозначающих иксовые координаты «залпов».
В обработчик команд, в case WM_TIMER вносим правки:
Все действия выполняем однотипно: вызываем функцию boolInRange() при проверке каждого статика корабля противника, и если функция возвращает true, то картинку на соответствующем статике на ту, что приведена на рисунке 8, а при последующей итерации таймера – удаляем статик (строка 71 и подобные ей). Не бойтесь, объясняю я сумбурно только в этой статье. В остальных статьях цикла по программированию все пояснения будут как всегда подробными и понятными. Здесь же мы вскользь рассматриваем работу таймеров и создаем простое приложение – игрушку. Проверяем, как будет работать наша функция:
Добавим счетчик сбитых кораблей. По достижению 10 мы остановим игру и выведем сообщение о победе в первом туре.
Проверяем:
На зависания в гиф внимания не обращайте – это все бандикам. Первый тур игры создали. Осталось довести его до ума. Для начала нужно рандомизировать цели нашего истребителя, то есть добавить метеориты и боксы с запчастями и снарядами. Модернизируем функцию Enemy(). Добавим параметр, который будет определять, что именно программа будет направлять навстречу истребителю. Для каждой картинки пропишем свой HBITMAP:
В функции Enemy() пропишем конструкцию с условием:
В обработчике команд, в case WM_TIMER, пропишем rand для переменной, которую будем передавать в качестве параметра Selection в функцию Enemy():
Проверяем, как будет работать наша новая функция:
Функция работает, но не очень хорошо: слишком уж много бонусов выпадает игроку, и слишком мало кораблей противника. Нужно упорядочить рандомизацию. Добавить вероятностную составляющую в генерацию чисел. В WinApi подобных инструментов нет, поэтому нам придется создать его самим.
Переделаем RandomGoal из простой переменной в функцию типа int:
Думаю, одной «лечилки» и одного чемоданчика с запчастями, игроку вполне хватит. Что ж, проблему с излишней щедростью игры решили. Теперь добавим счетчик здоровья и счетчик патронов. Пусть, если мы пропустим один вражеский истребитель, наш корабль потеряет 10% здоровья, если пропустим метеор, то 20%. Ну, а если произойдет столкновение с кораблем противника или метеором – 100%. Сбить истребитель можно будет одним выстрелом, метеор – двумя. Чемоданчик с запчастями будет давать нам 50% здоровья, чемоданчик с патронами – 10 снарядов. Счетчики построим следующим образом:
В процессе игры будем отправлять статикам сообщения м STM_SETIMAGE:
И так 15 элементов. Для боезапаса всё сделаем аналогично: в начале игры создастся 10 статиков, окрашенные в «снаряды», а затем, будем убавлять или добавлять их количество, просто перекрашивая окна. Счет будем вести в специально созданных переменных типа int: ValueHealthи ValueAmm. Будем уменьшать ValueHealth на 1 после каждого пролета корабля противника на охраняемую нами территорию (для метеоритов чуть позже поправим скрипт).
Проверим, как будет работать наша функция. Запустим приложение и ничего не будем делать. После того, как мимо нас пролетит десять целей, счетчик здоровья должен обнулиться:
Включим в код счетчик снарядов? Будем вызывать его по нажатию пробела:
Проверяем. Запускаем приложение и десять раз жмем на пробел:
Теперь ограничим счетчик здоровья только кораблями и истребителями. Создадим десять интовых переменных Enemy_0…Enemy_9, в которые будем записывать тип картинки на летящем к нашему кораблю статике. Если это вражеский корабль – запишем 1, если метеорит – 2, если лечилка – 3, если патроны – 4. В зависимости от типа, значения счетчика здоровья будут уменьшаться по-разному. Если типы будут 1, то счетчик уменьшится на 1, если метеорит, то есть 2, то на 2, а если 3 или 4, то не будут уменьшаться. Задавать тип будем в момент создания статика.
Теперь, если мимо пролетит истребитель, счетчик уменьшится на 1, если метеорит – на 2, если любой чемоданчик, то ничего не изменится. Проверяем:
Теперь обработаем типы статиков 3 и 4, то есть ремонтный и снарядный чемоданчики: при столкновении с нашим кораблем они должны исчезать, а у игрока будут повышаться боезапас или количество здоровья.
Не забываем также вызвать AmmCounter() из этого условия. Проверяем, как будут работать наши условия:
Теперь обработаем столкновение истребителя с кораблем противника или с метеоритом.
При столкновении мы подменяем картинку на статике истребителя на «взрыв», обнуляем счетчик здоровья и останавливаем таймер.
Проверяем:
Осталось немного:
- Добавить функцию, которая при обнулении счетчика здоровья остановит игру и выведет сообщение с текстом «Вы проиграли!»;
- Заменить MessageBox(), который выскакивает при выигрыше на статик с аналогичным сообщением и кнопкой «Далее»;
- Организовать 10 уровней игры;
- «Пофиксить баг» с зависанием игрековых координат Shot-ов.
Идем по порядку. Создадим картинку для статика с надписью «Вы проиграли!». Пусть размер статика будет 500х300.
Подгружаем картинку в программу в файле resources.h, а в main.cpp пишем функцию, в которой создадим статик под нее. Также на этом статике отобразим две кнопки: «Try again» и «Exit». Первая, как Вы уже догадались будет перезапускать игру, а вторая закрывать приложение. При создании кнопок не забываем, что они дочерние не для статика, а для hwnd. Соответственно и координаты кнопок указываем относительно hwnd.
Данную функцию нужно будет вызывать в двух случаях: когда счетчик здоровья упадет до нуля при пропуске большого количества кораблей противника/метеоров, когда истребитель столкнется с кораблем противника или метеором. Делаем:
Проверяем:
У нас есть одна существенная ошибка: допустим, у нас осталось три деления здоровья, мы пропустили два метеора и… игра продолжается. Почему? Потому что мы можем проиграть только если счетчик здоровья станет равным 0. А при данных условиях, он будет уже отрицательным, то есть условие не сработает. Значит нужно заменить условие с «равно нулю» на «меньше либо равно нулю». Так и поступим.
Идем дальше. Нужно создать сообщение о победе в раунде. Делаем его аналогичным сообщению о проигрыше:
На статик добавляем кнопку Continue:
Вызовем ее из WM_TIMER:
Теперь, при уничтожении десятого противника мы получим сообщение о победе:
Добавим в игру счетчик уровней. Назовем его LevelCounter. После каждого нажатия на «Continue» он будет увеличиваться на единицу.
Скорость движения объектов с каждым раундом будет увеличиваться в то количество раз, которому равно LevelCounter. Также обработаем нажатия на клавиши «заново» и «выход».
При нажатии на «Заново» мы воссоздаем начальные условия игры, а при нажатии на «Выход» закрываем приложение. Перенесем счетчик уровней из case кнопки «Далее» в строку, из которой вызываем функцию YouWin(), и при достижении счетчиком значения 11, будем выводить поверх статика не кнопку «Далее», а кнопки «Заново» и «Выход». Проверим, работают ли эти кнопки:
Как видим, все кнопки работают как надо. Остается сделать три вещи:
- При каждом уничтожении снаряда сбрасывать его икс координату до нуля;
- Добавить кнопку «Старт» в начало игры, чтобы она начиналась по нажатию, а не автоматически.
Добавляем кнопку Старт. Можно сделать это прямо в WM_CREATE:
Тут же убираем отсюда всё, что относится к таймеру и переносим это под команду нажатия кнопки Старт.
Теперь при запуске приложения, игра не начинается автоматически, как раньше:
Реализовывать двойной выстрел по метеорам, пожалуй, не будем. Игра и так получилась вполне неплохой.
Вот, собственно, и всё. Мы с Вами написали простую многоуровневую мини-игру для ПК в стиле старых аркадных автоматов. Заметьте: для реализации объектов игры мы использовали только статики и ничего больше!
Ниже приведу код из всех файлов программы. Все рисунки, которые в ней использовались, есть выше в статье.
А теперь поговорим для кого написана эта статья. Для тех, кто уже прочел статьи цикла WinApi, где мы с Вами разрабатываем приложение для автосервиса. Данная статья – это своеобразная разгрузка, а также способ показать Вам, что не всегда обязательно использовать макросы для реализации сложных программ. Ну и к тому же, я сам немного отвлекся от сложных объяснений в цикле статей по С++ и написал свою первую игрушку (раньше как-то не приходилось, но давно хотелось).
Что ж, Надеюсь, что статья была для Вас интересной. Скоро мы вернемся к другим темам и предметам (физике, математике и программированию). А пока что, спасибо, что читаете. Удачи в учебе и труде!
PS: Ах да, если статья понравилась, поддержите лайком: Вам не сложно, а я буду знать, что не зря старался!
Содержимое файла resources.rc
#include "resources.h"
FonC BITMAP "Fon.bmp"
Содержимое файла resources.h
#define FonC 1
#define Again 2
#define Exit 3
#define ContinueS 4
#define StartS 5
#define TimerStart 10
#define TimerStop 11
#include <windows.h>
HWND hwnd;
HWND hFighter;
HWND hShot_0;
HWND hShot_1;
HWND hShot_2;
HWND hShot_3;
HWND hShot_4;
HWND hShot_5;
HWND hShot_6;
HWND hShot_7;
HWND hShot_8;
HWND hShot_9;
HWND hEnemy_0;
HWND hEnemy_1;
HWND hEnemy_2;
HWND hEnemy_3;
HWND hEnemy_4;
HWND hEnemy_5;
HWND hEnemy_6;
HWND hEnemy_7;
HWND hEnemy_8;
HWND hEnemy_9;
HWND hHealth_0; HWND hHealth_1; HWND hHealth_2; HWND hHealth_3; HWND hHealth_4; HWND hHealth_5; HWND hHealth_6; HWND hHealth_7;
HWND hHealth_8; HWND hHealth_9; HWND hHealth_10; HWND hHealth_11; HWND hHealth_12; HWND hHealth_13; HWND hHealth_14;
HWND hAmm_0; HWND hAmm_1; HWND hAmm_2; HWND hAmm_3; HWND hAmm_4; HWND hAmm_5; HWND hAmm_6; HWND hAmm_7; HWND hAmm_8; HWND hAmm_9;
HWND hAmm_10; HWND hAmm_11; HWND hAmm_12; HWND hAmm_13; HWND hAmm_14; HWND hAmm_15; HWND hAmm_16; HWND hAmm_17; HWND hAmm_18; HWND hAmm_19;
HWND hLose;
HWND hWinn;
HWND btnAgain;
HWND btnExit;
HWND btnContinue;
HWND btnStart;
int xFighter = 402;
int yFighter = 476;
int xShot_0 = 0; int xShot_1 = 0; int xShot_2 = 0; int xShot_3 = 0; int xShot_4 = 0;
int xShot_5 = 0; int xShot_6 = 0; int xShot_7 = 0; int xShot_8 = 0; int xShot_9 = 0;
int yShot_0 = 440; int yShot_1 = 440; int yShot_2 = 440; int yShot_3 = 440; int yShot_4 = 440;
int yShot_5 = 440; int yShot_6 = 440; int yShot_7 = 440; int yShot_8 = 440; int yShot_9 = 440;
int xEnemy_0 = 0; int xEnemy_1 = 0; int xEnemy_2 = 0; int xEnemy_3 = 0; int xEnemy_4 = 0;
int xEnemy_5 = 0; int xEnemy_6 = 0; int xEnemy_7 = 0; int xEnemy_8 = 0; int xEnemy_9 = 0;
int Enemy_0 = 0; int Enemy_1 = 0; int Enemy_2 = 0; int Enemy_3 = 0; int Enemy_4 = 0;
int Enemy_5 = 0; int Enemy_6 = 0; int Enemy_7 = 0; int Enemy_8 = 0; int Enemy_9 = 0;
int yEnemy_0 = 20; int yEnemy_1 = 20; int yEnemy_2 = 20; int yEnemy_3 = 20; int yEnemy_4 = 20;
int yEnemy_5 = 20; int yEnemy_6 = 20; int yEnemy_7 = 20; int yEnemy_8 = 20; int yEnemy_9 = 20;
int EnemyTimer = 90;
int ValueHealth = 10;
int ValueAmm = 10;
int LevelCounter = 1;
int DownedShips = 0;
int xStep_1 = 0; int xStep_2 = 0;
int RandomGoal();
int ValH();
HBITMAP bmpFighter = (HBITMAP)LoadImage(NULL, "Starfighter.bmp", IMAGE_BITMAP, 66, 66, LR_LOADFROMFILE);
HBITMAP bmpShot = (HBITMAP)LoadImage(NULL, "piu.bmp", IMAGE_BITMAP, 7, 7, LR_LOADFROMFILE);
HBITMAP bmpEnemy_1 = (HBITMAP)LoadImage(NULL, "Enemyship.bmp", IMAGE_BITMAP, 66, 66, LR_LOADFROMFILE);
HBITMAP bmpEnemy_2 = (HBITMAP)LoadImage(NULL, "Meteor.bmp", IMAGE_BITMAP, 66, 66, LR_LOADFROMFILE);
HBITMAP bmpEnemy_3 = (HBITMAP)LoadImage(NULL, "repair.bmp", IMAGE_BITMAP, 66, 66, LR_LOADFROMFILE);
HBITMAP bmpEnemy_4 = (HBITMAP)LoadImage(NULL, "ammunition.bmp", IMAGE_BITMAP, 66, 66, LR_LOADFROMFILE);
HBITMAP bmpBang = (HBITMAP)LoadImage(NULL, "bum.bmp", IMAGE_BITMAP, 66, 66, LR_LOADFROMFILE);
HBITMAP bmpHealth = (HBITMAP)LoadImage(NULL, "Health.bmp", IMAGE_BITMAP, 7, 25, LR_LOADFROMFILE);
HBITMAP bmpAmm = (HBITMAP)LoadImage(NULL, "Amm.bmp", IMAGE_BITMAP, 7, 25, LR_LOADFROMFILE);
HBITMAP bmpBlack = (HBITMAP)LoadImage(NULL, "Black.bmp", IMAGE_BITMAP, 7, 25, LR_LOADFROMFILE);
HBITMAP bmpLose = (HBITMAP)LoadImage(NULL, "Lose.bmp", IMAGE_BITMAP, 500, 300, LR_LOADFROMFILE);
HBITMAP bmpWinn = (HBITMAP)LoadImage(NULL, "Winn.bmp", IMAGE_BITMAP, 500, 300, LR_LOADFROMFILE);
bool IsTimer = FALSE;
bool IsSPACE = FALSE;
bool IsEnemy = FALSE;
bool DestEnemy_0 = FALSE; bool DestEnemy_1 = FALSE; bool DestEnemy_2 = FALSE; bool DestEnemy_3 = FALSE; bool DestEnemy_4 = FALSE;
bool DestEnemy_5 = FALSE; bool DestEnemy_6 = FALSE; bool DestEnemy_7 = FALSE; bool DestEnemy_8 = FALSE; bool DestEnemy_9 = FALSE;
bool InRange(int lxEnemy, int vyEnemy);
void Objects(HWND hwnd, HINSTANCE hInstance);
void ItShot(HWND hwndParrent);
void Enemy(HWND hwndParrent, int Selection);
void HealthCounter(int Value, HWND hWndParrent);
void AmmCounter(int Value, HWND hWndParrent);
void YouLose();
void YouWinn();
Содержимое файла main.cpp