Visual Basic - мастерская разработчика
Библиотеки

DirectX

Обзоры
DirectDraw
Direct3D
DirectX Audio
DirectPlay
DirectInput
Fido Topics
SourceCode
Tools&Libs

OpenGL

Статьи и учебники
Fido Topics
SourceCode
Tools&Libs

Архив по Glide

Движки

Обзоры
Учебники
SourceCode
Downloads

Создание игр

Ваши игры

Обзорные статьи
Учебники
Fido Topics
SourceCode
Download

Stuff

Программер-Чат

Псевдо-FTP
Disclaimer
Оффтопик

 

Основы 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>

 

 


Проект
Создание Народного Учебника по OpenGL

Участвовать!
Поиск
Найдите статью или файл:


Рассылка
Новости сайта
La Vision в вашем почтовом ящике








Программирование на С++ Delphi и Паскаль
Центр демо-искусства в России