Основы DirectDraw на C++
Second Edition
[Предыдущая часть]
[Следующая часть] [Приложения
к этой части]
...Это не Фича, это - Бага!
некий ]PD[-System
Часть третья: Творим!
Да уж давно бы пора! - подумают некоторые. Ну ладно, не злитесь, в этой главе
мы разработаем и претворим в жизнь самую настоящую DirectX'овую игруху!
Не будем очень оригинальны и сделаем PingPong; ну знаете, такая штука, где
ездят две ракетки и пинают мячик стараясь чтобы противник по нему не попал.
Так вот, мы постараемся сделать полностью (ну или почти) работающий ПингПонг
для одного игрока против компьютера. Как подключить второго игрока я надеюсь
вам будет абсолютно понятно - это останется в качестве упражнения. Кроме того,
для упрощения задачи я пока не буду рассматривать вывод текстовых сообщений
- это в раздел эффектов в следующую часть. А для усложнения задачи - выработаем
такой алгоритм, в котором поле у нас будет представлено в виде матрицы в которой
есть элементы 0 - свободно и 1 - блок. От блоков мячик соответственно отскакивает.
Это сделано для того, чтобы мяч мог передвигаться независимо от расположения
блоков, вам надо будет только поменять ссответствующее значение в матрице. Это
уже будет нечто действительно похожее на движок и вы можете делать различные
игровые поля.
Итак, приступаем
Что сначала?
Ну с самого начала, еще даже перед тем как начать рисовать графику, надо решить
один такой
маленький вопросик: а какой у нас будет ПингПонг: вертикальный или горизонтальный?
Я выбрал вертикальный; не спрашивайте почему, вы всегда сможете сделать другой.
Разрешение экрана будет 640x480x16, а размер матрицы - 12*16, то есть одна
клетка - 40*40 пикселей.
Ну вот, а теперь можно рисовать графику.
Процесс этот утомительный только по одной причине: кропотливость. Сначала надо
выбрать размерность каждого объекта: бита будет размером 80*20, мяч - 20*20,
а блок - 40*40, чтобы занимал одну клетку. Размеры мяча и биты абсолютно произвольные,
но если вы захотите взять другие, учтите, что придется исправлять все нижеследующие
математические расчеты.
Итак, зайдите куда-нибудь типа FreeHand8 и сотворите шедевр в стиле Малевича!
Каждый объект рисуйте по отдельности и сохраняйте в 24-разрядном файле BMP.
Не забывайте, что потребуется две разных биты. Нарисовали - вперед в Painbrush!
Выбирайте пункт меню "Вставить из файла" и скомпонуйте все спрайты
на одном рисунке. Затем подгоните размер рисунка под границы спрайтов (Пожалуйста,
не делайте рисунок больше, чем главная поверхность. Это поддерживают не все
видеокарты). Да! И не забудьте, что нам потребуется "прозрачный" цвет.
Я взял для этого черный (RGB(0,0,0)), поэтому контуры на рисунках спрайтов у
меня вроде как черные, а на самом деле нет (RGB(12,12,12)).
Я нарисовал
вот такую картину и далее подразумевается, что координаты расположения спрайтов
будут как у меня.
Примечание: как всегда я где-то напортачил, поэтому размерности спрайтов
у меня с погрешностями +/- 1 пиксел.
Первые аккорды кода
Давным давно, еще в школе, нам преподавали бейсик (простой!). Поэтому я еще
некоторое время то и дело боролся с искушением свалить все в одну кучу. Это
не только непрактично, но и просто как-то неэстетично и плохо выглядит, поэтому
давайте обсудим структуру программы.
Заместо того, чтобы вводить кучу переменных, для описания объектов используем
типы BallInf и BetInf, описывающие соответственно мячик и биту. Создадим экземпляр
мячика Ball и два экземпляра биты Bet1 и Bet2 "As BetInf".
Модуль mdlDirectDraw7 у нас уже есть (ведь есть, правда?! Если нет, то сюда).
У нас также будет уже написанный файл startup.cpp, главный "управляющий"
файл main.cpp, а также новый файл mdlBall.h, который мы вместе напишем и который
будет содержать функции, не относящиеся напрямую к DirectDraw, а вбольшей степени
к самой игре.
Программу будем писать постепенно, создавая сначала костяк, а потом наращивая
все более и более завороченные фичи (не баги!).
Для начала, создадим новый проект. Как всегда, это должен быть пустое Win32
приложение (не консольное!). Конфигурацию менять не придется - все линки прописаны
в коде. Не забудьте в папочку с откомпилированным кодом (папка проекта, Debug
или Release) положить наш подготовленный графический файл.
Теперь, все готово и можно приступать к написанию кода.
Начнем с инициализации экрана. Далее мы пишем в файл main.cpp По ходу дела
я буду все пояснять:
//******************************************************************
// Name: main.c
// Autor: Antiloop
// Desc: Собственно, сама программа
//******************************************************************
#include "mdlBall.h"
//Инициализация DD, создание поверхностей и т.п.
int Start(HWND hwin)
{
//Инициализировать DirectDraw
CreateDDFullscreen(hwin, 640, 480, 16);
//Загрузить графику из файла на новую оффскринную
поверхность
ddsPic=CreateDDSFromBitmap("tball.bmp");
if (ddsPic==NULL)
{
MSG("Буфер картинки пуст");
return 0;
}
Reset(); //Сбросить настройки
LoadMap(1); //Загрузить первый уровень
return 1;
}
//Очередной кадр
int Update()
{
ClearBuffer(); //Очистить "полотно"
DrawMap(); //Нарисовать карту
GetBetMovement(); //Переместить биту игрока (если нажата
клавиша)
DrawBet (1); //Нарисовать биту игрока
//DrawBet (2); //Переместить (если надо) и нарисовать
биту компьютера
//DrawBall(); //Переместить и нарисовать мяч
ddsPrimary->Flip(NULL, DDFLIP_WAIT); //Показать все
это на экране
return 1;
}
//Убить все объекты DirectDraw
void End()
{
DestroyDD();
}
На этом код main.cpp заканчивается. Вы видите, что функции DrawBet(2) и DrawBall
пока закомментированы. Как я уже говорил, писать программу мы будем постепенно,
поэтому тела этих функций мы напишем чуть позже.
Внимательно присмотрясь к тому что будет делать программа, вы увидите, что после
инициализации, она войдет в цикл, в котором будет находиться до самого своего
конца. Идея такова, чтобы в этом цикле заставить программу отображать все нужные
нам объекты каждый раз на заданных местах. Места же эти каждый раз перевычисляются
на очередном витке цикла. Таким образом, вычисления сводятся к определению новых
координат объекта, заданию структуры RECT, о которой говорилось в прошлой главе,
и применению метода BltFast.
Почти все действия, относящиеся к DirectX мы уже сделали и теперь приступаем
к математике.
Добавим функциональности
Переходим в файл mdlBall.h Сперва, как я уже говорил, определим структуры BallInf
и BetInf, а также создадим нужные переменные.
Типы выглядят так:
//******************************************************************
// Name: mdlBall.h
// Autor: Antiloop
// Desc: Модуль с функциями относящимися к шарику, бите и т.п.
//******************************************************************
#include "mdlDirectDraw7.h"
#include <time.h>
//Структура биты
typedef struct {
int x;
int speedX;
} BetInf;
//Структура мяча
typedef struct {
int x,y;
int speedX, speedY;
} BallInf;
//Две биты и мяч
BetInf Bet1, Bet2;
BallInf Ball;
//Карта
int map[12][16];
Инициализация закончена. Текущее положение ракетки зависит от координаты X.
Y у обеих ракеток постоянная - у нижней - 460, у верхней - 0. Чтобы посчитать
новое положение ракетки исходя из старого, надо прибавить к координате X приращение
SpeedX. Если приращение отрицательное, то бита двигается влево и наоборот. Bet1
управляется игроком, поэтому приращение задается нажатием клавиши "влево"
или "вправо". Если клавиша отпускается, то приращение равно нулю.
Приращение Bet2 вычисляется компьютером.
Положение мяча задается координатами X и Y. У него также существуют два осевых
приращения.
Карта размера 16*12 (16 - по-горизонтали, 12 - по-вертикали). Учтите, что массив
начинается с нуля и сначала идут строки.
//Заголовки здешних функций
VOID Reset();
VOID LoadMap(int Level);
VOID DrawMap();
VOID DrawBet(int Num);
int CheckPoint();
VOID DrawBall();
VOID MoveBall();
VOID DoBetAI();
VOID GetBetMovement();
int randInt(int low, int high);
BOOL IsKeyDown (int KeyCode);
Давайте напишем функцию, которая сбрасывает положения мяча и бит до начальных.
//Сброс в начало
VOID Reset()
{
Bet1.x=320;
Bet1.speedX= 0;
Bet2.x=320;
Bet2.speedX=0;
Ball.x=320;
Ball.y=240;
Ball.speedY= -5;
Ball.speedX=randInt(0, 10) - 5;
}
Функция randInt возвращает случайное число от первого аргумента до второго.
Оба аргумента должны быть положительными, но возможно и отрицательное приращение
(движение влево), поэтому применяем прием с вычитанием. Вот тело функции randInt:
//Генерация случайного числа
int randInt(int low, int high)
{
int range = high - low;
srand( (unsigned)time( NULL ) );
int num = rand() % range;
return( num + low );
}
Теперь, функция, которая инициализирует карту. Ее нужно вызывать только один
раз, когда загружается уровень. Вы можете сами поэкспериментировать, дописывая
новые уровни.
//Загрузка карты, aka формирование массива
VOID LoadMap(int Level)
{
int i;
switch(Level)
{
case 1: //Просто стенки по бокам
for (i=0; i<=11; i=i+1) {
map[i][0]=1;
map[i][15]=1;
}
break;
}
}
Мы всего-лишь задали матрицу. Нам еще понадобится процедура, которая будет
рисовать карту каждый виток цикла по этой матрице.
//Нарисовать карту
VOID DrawMap()
{
int i,j;
for (i=0; i<=11; i++) {
for (j=0; j<=15; j++) {
//Просматриваем весь массив. Если находим элемент со
//значением 1, рисуем в этом месте "кирпич"
if (map[i][j]==1) {
rc.top=0;
rc.left=102;
rc.right=rc.left+40;
rc.bottom=40;
ddsBack->BltFast(40 * j, 40 * i, ddsPic, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY)
;
}
}
}
}
Теперь, процедура, которая будет рисовать биты. В параметре ей указывается
какая бита будет рисоваться, 1-нижняя, 2-верхняя
//Рисовать биту
VOID DrawBet(int Num)
{
int BY,BX;
//Заполняем rc в соответствии с тем, чью биту рисуем
if (Num==1) { //бита игрока
rc.left=0;
Bet1.x=Bet1.x+Bet1.speedX;
if ((Bet1.x+80) > 600) Bet1.x=600-80;
if (Bet1.x<40) Bet1.x=40;
BX=Bet1.x;
BY=460; }
else { //бита компьютера
//вычисление действий компьютера.
//Поставьте здесь ремарку пока не напишете тело этой функции
DoBetAI();
rc.left=142;
Bet2.x=Bet2.x+Bet2.speedX;
if ((Bet2.x+80) > 600) Bet2.x=600-80;
if (Bet2.x <40) Bet2.x=40;
BX=Bet2.x;
BY=0;
}
//Заполняем недостающие элементы структуры RECT и
рисуем биту на заднем буфере
rc.top = 0;
rc.bottom = rc.top + 20;
rc.right = rc.left + 80;
ddsBack->BltFast(BX, BY, ddsPic, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);
}
Отлично, теперь "приделаем" нашей нижней бите управление.
//Обработка движения биты игрока
VOID GetBetMovement()
{
Bet1.speedX=0;
if (IsKeyDown(VK_LEFT)) Bet1.speedX= -5;
else if (IsKeyDown(VK_RIGHT)) Bet1.speedX=5;
}
//Нажата эта клавиша?
BOOL IsKeyDown (int KeyCode)
{
if (GetAsyncKeyState(KeyCode) & 0x8000) return TRUE;
return FALSE;
}
Запустите программу. Вы увидите, что уровень уже нарисован и вы можете двигать
свою битку в пределах стенок. Эти стенки по определению должны быть всегда,
учитывайте это. Выходите из программы как и раньше, с помощью Escape.
Свободный полет
Осталось самое сложное - запрограммировать мячик, чтобы он летал и отскакивал
от тех мест, которые в массиве помечены единичкой. Я сделал это вот каким нехитрым
образом. Итак, представьте себе двухмерный мячик. В процессе полета он может
находиться целиком в одном секторе (каждый сектор - 40*40, если помните, а мяч
- 20*20), в двух секторах одновременно, в трех не может, а вот в четырех - запросто.
Теперь представьте, что у мячика есть крайние точки, так называемые - "контрольные
точки". Можно сказать, что если одна из этих точек находится в каком-то
секторе, то некая часть мячика тоже находится в этом секторе. Таким образом,
если контрольная точка находится в секторе, который помечен 0, то все нормально,
однако, если контрольная точка попадает в сектор с 1, то он уже занят блоком
и мячик должен отскочить. В какую сторону? Посмотрите на такой рисунок:

Вы видите на нем четыре контрольных точки, отмеченных красным, а также их координаты
в системе координат мяча. Если мяч попал в занятый сектор точкой номер 1, то
очевидно, что он стукнулся о преграду, находящуюся сверху него и должен отсочить
вниз. Это легко задать формулой SpeedY=-SpeedY. Если "просигналила"
точка номер два, то мяч отскакивает влево и т. д. При этом, мяч нужно "отпозиционировать"
то-есть поставить просигналившую точку ровно на границу с занятым сектором,
чтобы мяч не "залипал" там.
Конечно, для правильного отображения четырех точек явно мало, мяч может уже
быть в занятом секторе и ни одна точка не просигналит, поэтому в идеале надо
рассматривать каждую точку окружности мяча, лезть в дебри тригонометрии... бррр...
Я вас еще не убедил?
Хорош этот способ или нет, решайте сами. По крайней мере он работает.
Теперь функция, которая выдает контрольную точку, если мяч попал в занятый
сектор или ноль, если все нормально.
//Проверка попадания контрольной точки мячика в занятый
блок
int CheckPoint()
{
int Ret=0;
if (map[(int)((Ball.y+10)/40)][(int)(Ball.x/40)] != 0) Ret=4;
if (map[(int)((Ball.y+20)/40)][(int)((Ball.x+10)/40)] != 0) Ret=3;
if (map[(int)((Ball.y+10)/40)][(int)((Ball.x+20)/40)] != 0) Ret=2;
if (map[(int)(Ball.y/40)][(int)((Ball.x+10)/40)] != 0) Ret=1;
return Ret;
}
Функция будет всегда выдавать только одну точку с приоритетом по часовой стрелке.
Настал момент рисования мяча.
//Рисовать мячик
VOID DrawBall()
{
MoveBall(); //Перед рисованием посчитаем новые координаты
мяча
//А вот теперь рисуем
rc.top = 0;
rc.left = 81;
rc.bottom = 21;
rc.right = 102;
ddsBack->BltFast(Ball.x, Ball.y, ddsPic, &rc, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);
}
//Большая функция - считаем новые координаты мяча
VOID MoveBall()
{
int Pt;
//Делем следующий шаг
Ball.x = Ball.x + Ball.speedX;
Ball.y = Ball.y + Ball.speedY;
Pt = CheckPoint(); //И проверяем, где после этого оказался
мяч
//Проверяем попадание мяча одной из контрольных точек
в занятый сектор
if (Pt>0) {
switch (Pt) {
case 1:
Ball.speedY= -Ball.speedY;
Ball.y=(int)((Ball.y/40) * 40 + 40);
break;
case 2:
Ball.speedX= -Ball.speedX;
Ball.x=(int)(((Ball.x+20)/40) * 40 - 20);
break;
case 3:
Ball.speedY= -Ball.speedY;
Ball.y=(int)(((Ball.y+20)/40) * 40 - 20);
break;
case 4:
Ball.speedX= -Ball.speedX;
Ball.x=(int)((Ball.x/40) * 40 + 40);
break;
}
}
else //Мы не попали в занятую область, но может быть
нас пропустила бита?
{
//Бита игрока нас случаем не пропускала?
if ((Ball.y+20) > 460) { //Нет - значит мяч отбит
if (((Ball.x+15) >= Bet1.x) && ((Ball.x+5) <= (Bet1.x + 80)))
{
Ball.speedY = -Ball.speedY;
Ball.speedX = (int)(((Ball.x + 10) - (Bet1.x + 40)) / 8);
Ball.y = 460 - 20;
}
else //Мяч пропущен. Начинаем все сначала
Reset();
}
if (Ball.y<20) { //Теперь то же, но для биты компьютера
if (((Ball.x+15)>= Bet2.x) && ((Ball.x + 5) <= (Bet2.x +80)))
{
Ball.speedY = -Ball.speedY;
Ball.speedX = randInt(0, 10) - 5;
Ball.y = 20;
}
else
Reset();
}
}
}
Наконец, последняя функция - вычисление приращения по X ракетки компьютера
в зависимости от позиции мяча. Все очень просто. Компьютер всегда старается,
чтобы левый и правый край мяча попадал в поле его ракетки.
//Интеллект у биты компьютера небольшой.
//Всего в три строчки поместился :)
//Зато попробуйте теперь у него выйграть!!!!
VOID DoBetAI()
{
if (Ball.x < (Bet2.x+10)) Bet2.speedX= -5;
if ((Ball.x + 20) >= (Bet2.x +80)) Bet2.speedX=5;
if ((Ball.x > Bet2.x) && ((Ball.x +20) < (Bet2.x+80))) Bet2.speedX=0;
}
Громкие апплодисменты! Написание модуля завершено. Снимите ремарки с двух функций
в main.cpp о которых я говорил раньше. Так же снимите ремарку с вызова DoBetAI(),
если вы ее ставили.
И запустите программу. Работает? Ура!
Надеюсь, теперь вам немного стало ясно, как делаются игрушки на DirectX. Если
нет - то это нормально. Просто надо упражняться, упражняться и еще раз упражняться...
Не помню кто сказал...
На этом части три пришел конец. В следующей части поговорим об эффектах.
Приложения
Проект
DXPong (45k)
[Вверх]
Приятного программирования, Antiloop
Posted: 23.01.2k1
Autor: Antiloop
<anti_loop@mail.ru>
|