Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Создание компьютерных игр


Информационный Канал Subscribe.Ru

Cоздание компьютерных игр.
Выпускается еженедельно по пятницам.
Автор рассылки - Евгений Казеко.

Выпуск 20. (от 13 февраля 2004 года)
Прыгающие мячи.

--------------------------------------------------------------

Довольно забавен тот факт, что выпуск "с круглым номером" выходит в пятницу 13 числа. Что же, будем надеяться, что это никак не отразится на выпуске.

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

Сегодня мы как раз и поговорим о синхронизации. Я собирался так и назвать данный выпуск, но решил, что название "прыгающие мячи" лучше раскроет его суть. В самом деле, речь пойдет не только о синхронизации. Каждый раз, обдумывая новый выпуск, я представляю себе небольшую программку, которая будет демонстрировать ту или иную тему. Но когда я сажусь за ее написание, мое представление коренным образом изменяется. Вот и на этот раз - для иллюстрации такой вещи, как синхронизация, было бы вполне достаточно программы, которая просто выводит на экране через определенные интервалы времени сообщения, например "За прошедшие 30 секунд сообщение 1 выводилось уже 3 раза. За то же самое время сообщение 2 успело вывестись целых 10 раз. А сообщение 3 появлялось на экране всего 1 раз." Но для меня это слишком скучно. Уже и так полно таких сухих и неинтересных примеров, которые, хотя и очень просты, но совершенно бесполезны. Уж лучше пускай сегодняшний пример будет немного посложнее, но гораздо интереснее. Мы рассмотрим синхронизацию на примере трех прыгающих мячей, которые будут двигаться с разной скоростью и отталкиваться от границ окна.

Как всегда, я стараюсь делать примеры простыми и понятными, иногда в ущерб скорости, эффективности и хорошей структурированности программы. (Ну, например, объявления функций лучше делать в .h файлах, переменные группировать в структуры... Вообще, частенько бывает оправданным использование объектов, но я нарочно обхожу тему объектно-ориентированного программирования. Учебные примеры должны быть простыми.) И на сей раз я как всегда придерживался этого принципа, но все же без нововведений не обошлось. Например, для передачи переменных в функцию, которая изменяет их, я использовал не указатели, а передачу по ссылке - понятие С++, которое отсутствует в стандартном С. Это упростило исходный код.

Также, я не стал давать в исходном коде программы подробные комментарии. Попробуем обойтись объяснениями вне программы, а уже потом приведем ее текст. Но довольно вступительных слов, пора уже приступить к сегодняшней теме.

Итак, все игровые события должны быть синхронизированы. Как нам сделать, чтобы три мяча двигались с разной скоростью? Нужно перемещать их на одно и то же расстояние через разные интервалы времени. Помните, из физики, что скорость - это расстояние, деленное на время? Чем меньше этот интервал, тем больше скорость. Сегодня мы отойдем от "матричного" представления игрового поля, и будем работать с координатами объектов в пикселях. Таким образом, пройденное при перемещении расстояние будет также измеряться в пикселях, а вот время будем измерять в миллисекундах. Почему? Да потому что миллисекунда - минимальная единица измерения функции GetTickCount(), которую мы будем использовать. Но об этом чуть позже. Сперва мне хотелось бы рассмотреть то, как мы будем перемещать мячи.

Центр мяча обозначим координатами х и у. И тогда, для того, чтобы двигать мяч горизонтально или вертикально, нам нужно просто изменять координату х или у, оставляя другую неизменной. А для того, чтобы двигать мяч под углом 45 градусов, нужно одновременно изменять обе координаты. И наконец, чтобы двигать мяч под другими углами, нам нужно изменять обе координаты неравномерно - скажем, задать смещение по х равное 3, а по у равное 1. В программе мы так и изменяем координаты - прибавляем к ним смещение, обозначив его xv и yv. Например, при движении под углом 45 градусов мы прибавляем к обеим координатам по 2 пикселя. (Конечно, чем больше пикселей мы прибавляем за один раз, тем быстрее движется мяч. Можно изменять его скорость и таким способом, но сегодня мы рассматриваем способ с синхронизацией).

Еще нужно рассказать про то, как мы реализуем отталкивание мячей от границ окна. Делается это очень просто. Мы смотрим, не выходит ли край мяча за границы экрана, и если да, то изменяем знак смещения (с плюса на минус и с минуса на плюс). И тогда мяч начинает двигаться в обратном направлении, отражаясь от края окна.

В программе движение мяча реализуется функцией MoveBall. Давайте рассмотрим ее, пока мы окончательно не запутались.


// Функция, которая перемещает мяч и проверяет
// столкновения с границами окна

/*
И сразу же новое понятие - передача переменной по ссылке.
В С++ мы можем не использовать указатели, а просто в
объявлении функции указать перед именем переменной знак &.
Тем самым мы говорим, что хотим позволить функции 
изменять значение переменной.

Мы передаем в функцию координаты мяча, а также смещение
по х и у, которое может быть и отрицательным.
*/

void MoveBall(int& x, int& y, int& xv, int& yv)
{

// Изменяем координаты мяча, двигая его
 x += xv;
 y += yv;

// Проверяем условия столкновений
// windowrect это структура с координатами границ окна
// Мы проверяем, не пересекаются ли края мяча с границами
// окна. Мяч имеет радиус 10 пикселей.

// Пересекается ли левый край мяча с левой границей?
 if ((x-10) < windowrect.left) 
 {
  // если да, то возвращаем центр мяча на прежнее место
  x = 10;
  // и изменяем направление движения по х
  xv = -xv;
 }

// верхний край
 if ((y-10) < windowrect.top)
 {
  y = 10;
  yv = -yv;
 }

// правый край
 if ((x+10) > windowrect.right)
 {
  x = windowrect.right-10;
  xv = -xv;
 }

// нижний край
 if ((y+10) > windowrect.bottom)
 {
  y = windowrect.bottom-10;
  yv = -yv;
 }

}


Мяч мы изобразим кружком диаметром 20 пикселей. Поскольку, как мы знаем, функции рисования эллипса требуются координаты углов прямоугольника, в который этот эллипс можно вписать, то нужно задать эти координаты относительно центра мяча. Например вот так: Ellipse(hdc, x1-10, y1-10, x1+10, y1+10);

Итак, мы знаем, как рисовать и двигать мяч. И наконец-то мы подходим к синхронизации, реализовать которую очень просто. Нам всего лишь нужно двигать и рисовать мяч через определенные интервалы времени, и чем меньше эти интервалы, тем больше скорость движения. В программе мы сначала определяем переменные, которые я назвал "таймерами". На самом деле в них всего лишь хранится время последнего передвижения. И кроме того, мы определяем "относительные переменные скорости". Вот так: int timer1 = 0; int speed1 = 20;

А теперь рассмотрим функцию синхронизации. Она возвращает true, если временной интервал прошел, и пора снова двигать и рисовать мяч, и false, если еще нет.


// Функция, определяющая, пора ли перемещать мяч
// Параметры - переменная-таймер, скорость мяча

bool SyncMovement(int& lastTime, int speed)
{
 // Сперва получаем системное время в миллисекундах
 int currentTime = GetTickCount(); 

 // Вот здесь и происходит вся синхронизация.
 // Вычитаем из текущего времени время
 // последнего перемещения и сравниваем полученный
 // интервал с заданным.
 // Если он превышает заданный интервал
 // (50 миллисекунд деленные на скорость -
 // чем больше скорость, тем меньше интервал)
 // значит уже пора снова перемещать мяч
 if((currentTime - lastTime) > 50 / speed)
 {
  // обновляем время перемещения
  lastTime = currentTime; 
  return true;
 }

 // Ну а если еще не пора - значит не пора
 return false;
}


Конечно же, можно и нужно самостоятельно устанавливать временной интервал, который у нас при скорости 20 равен двум миллисекундам (50/20 = 2.5, а поскольку деление целочисленное, дробная часть отбрасывается). А как вызывается эта функция, вы можете увидеть в примере, к которому, кстати, уже давно пора перейти.


#include <windows.h>


// размеры окна
#define WIN_WID 400 
#define WIN_HGT 400 

// Глобальные переменные для трех мячей

// координаты x и y центра
int x1,y1,x2,y2,x3,y3;

// направления движения и смещения
int x1v,y1v,x2v,y2v,x3v,y3v;

// переменные-"таймеры" 
int timer1 = 0;
int timer2 = 0;
int timer3 = 0;

// скорость движения 
int speed1 = 20;
int speed2 = 10;
int speed3 = 5;


// прямоугольник рабочей области окна
RECT windowrect;

// Функции

// функция синхронизации
bool SyncMovement(int& lastTime, int speed);
// функция перемещения мяча
void MoveBall(int& x, int& y, int& xv, int& yv);

LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);



int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprev, PSTR cmdline, int ishow)
{  
 
    HWND hwnd;
    HDC hdc;

    MSG msg;
    WNDCLASSEX wndclassex = {0};


// Создаем окно
    wndclassex.cbSize = sizeof(WNDCLASSEX);
    wndclassex.style="CS_HREDRAW" | CS_VREDRAW;
    wndclassex.lpfnWndProc = WinProc;
    wndclassex.hInstance = hinstance;
    wndclassex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclassex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclassex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wndclassex.lpszClassName = "Syncronize";

    RegisterClassEx(&wndclassex); 

    hwnd = CreateWindowEx(NULL, 
     "Syncronize",
     "Syncronize",
     WS_SYSMENU,
     CW_USEDEFAULT, 
     CW_USEDEFAULT, 
     WIN_WID,
     WIN_HGT,
     NULL,
     NULL,
     hinstance,
     NULL);

 if (!hwnd) return 0;


// Получаем его контекст устройства

 hdc = GetDC(hwnd);
 if (!hdc) return 0;


// Определяем перья и кисти
 HBRUSH oldbrush;
 HPEN oldpen;


 HBRUSH ball1_brush = CreateSolidBrush(RGB(255,0,0));
 HBRUSH ball2_brush = CreateSolidBrush(RGB(0,255,0));
 HBRUSH ball3_brush = CreateSolidBrush(RGB(0,0,255));

 HPEN ball1_pen = CreatePen(PS_SOLID, 1, RGB(255,0,0));
 HPEN ball2_pen = CreatePen(PS_SOLID, 1, RGB(0,255,0));
 HPEN ball3_pen = CreatePen(PS_SOLID, 1, RGB(0,0,255));
 
// Выбираем текущие, сохраняя старые

 oldpen = (HPEN)SelectObject(hdc, ball1_pen);
 oldbrush = (HBRUSH)SelectObject(hdc, ball1_brush);

// Определяем прямоугольник окна
 GetClientRect(hwnd, &windowrect);
   


// Задаем начальные значения координат
// наших мячей
 x1 = 100;
 x2 = 200;
 x3 = 300;
 y1 = y2 = y3 = 200;

// Направление движения и смещение
 x1v = 1;
 y1v = 3;
 x2v = y2v = 2;
 x3v = y3v = -2;


// Выводим окно

    ShowWindow(hwnd, ishow);
    UpdateWindow(hwnd);


// Главный цикл программы  
    while(1)
 {
  // в первую очередь нужно обрабатывать сообщения
  if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
  {
   if(msg.message == WM_QUIT)
    break;
    
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
  // а если их нет, то мы можем с чистой
  // совестью заняться игровым процессом
  else
  {
   // если пришло время перемещать мяч
   if (SyncMovement(timer1, speed1))
   {
    // стираем его на старом месте
    SelectObject(hdc, GetStockObject(BLACK_PEN));
    SelectObject(hdc, GetStockObject(BLACK_BRUSH));
    Ellipse(hdc, x1-10, y1-10, x1+10,y1+10);

    //передвигаем
    MoveBall(x1,y1,x1v,y1v);

    // и рисуем на новом
    SelectObject(hdc, ball1_pen);
    SelectObject(hdc, ball1_brush);
    Ellipse(hdc, x1-10, y1-10, x1+10,y1+10);

   }
   
   // то же самое для остальных мячей
   if (SyncMovement(timer2, speed2))
   {
    SelectObject(hdc, GetStockObject(BLACK_PEN));
    SelectObject(hdc, GetStockObject(BLACK_BRUSH));
    Ellipse(hdc, x2-10, y2-10, x2+10,y2+10);

    MoveBall(x2, y2, x2v, y2v);

    SelectObject(hdc, ball2_pen);
    SelectObject(hdc, ball2_brush);
    Ellipse(hdc, x2-10, y2-10, x2+10,y2+10);
   }

   if (SyncMovement(timer3, speed3))
   {

    SelectObject(hdc, GetStockObject(BLACK_PEN));
    SelectObject(hdc, GetStockObject(BLACK_BRUSH));
    Ellipse(hdc, x3-10, y3-10, x3+10,y3+10);

    MoveBall(x3, y3, x3v, y3v);

    SelectObject(hdc, ball3_pen);
    SelectObject(hdc, ball3_brush);
    Ellipse(hdc, x3-10, y3-10, x3+10,y3+10);

   }
     
  }

 }

 // Восстанавливаем старые кисть и перо
 SelectObject(hdc, oldpen);
 SelectObject(hdc, oldbrush);

 // Удаляем наши, освобождая память
 DeleteObject(ball1_pen);
 DeleteObject(ball2_pen);
 DeleteObject(ball3_pen);
 DeleteObject(ball1_brush);
 DeleteObject(ball2_brush);
 DeleteObject(ball3_brush);


 ReleaseDC(hwnd, hdc);
  
 UnregisterClass("Syncronize",hinstance); 

 return msg.wParam;
}




// Оконная процедура

LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
 PAINTSTRUCT ps;
 
    switch(message)
    {         
  case WM_PAINT:
            
   BeginPaint(hwnd,&ps);
   EndPaint(hwnd,&ps);
    return 0;

        case WM_DESTROY:
  case WM_CLOSE:
            
   PostQuitMessage(0);
             return 0;

    } 

    return DefWindowProc(hwnd, message, wparam, lparam);
}





// Функция, определяющая, пора ли перемещать мяч

bool SyncMovement(int& lastTime, int speed)
{
 
 int currentTime = GetTickCount(); 

 if((currentTime - lastTime) > 50 / speed)
 {
  lastTime = currentTime; 
  return true;
 }

 return false;
}




// Функция, которая перемещает мяч и проверяет
// столкновения с границами окна
void MoveBall(int& x, int& y, int& xv, int& yv)
{

 x += xv;
 y += yv;

 if ((x-10) < windowrect.left)
 {
  x = 10;
  xv = -xv;
 }

 if ((y-10) < windowrect.top)
 {
  y = 10;
  yv = -yv;
 }


 if ((x+10) > windowrect.right)
 {
  x = windowrect.right-10;
  xv = -xv;
 }

 if ((y+10) > windowrect.bottom)
 {
  y = windowrect.bottom-10;
  yv = -yv;
 }

}

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

P.S. Я по прежнему не имею возможности отвечать на письма, но я их читаю, и они так или иначе влияют на содержание рассылки и стиль изложения. Так что, пишите.

--------------------------------------------------------------

Архив рассылки вы найдете по адресам http://subscribe.ru/catalog/comp.games.gamecoder и http://www.gamecoder.nm.ru/subscribe.htm.

Евгений Казеко.
kazeko@list.ru
www.gamecoder.nm.ru
-----------------------------
Рассылка "Создание компьютерных игр", выпуск 20.
Выпускается еженедельно по пятницам.



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное