Класс Sprite



В коде предыдущего раздела класс Sprite использовался для представления спрайтов, проверяемых на столкновение. Давайте посмотрим, как он реализован.
Как мы уже видели, класс Sprite содержит ряд функций, с помощью которых при проверке столкновений можно получить сведения о каждом спрайте. В частности, функция GetRect() возвращает контурный прямоугольник спрайта, а функция GetSurf() — поверхность, на которой находится спрайт. Однако класс Sprite не ограничивается функциями простого контейнера для данных спрайта. Он предназначен не столько для обнаружения столкновений, сколько для их обработки.
На обнаруженное столкновение необходимо как-то прореагировать. Подробности обработки столкновения определяются приложением, но как проверка, так и обработка подчиняются некоторым общим правилам.
При столкновении двух спрайтов каждый из них может изменить направление движения или измениться иным образом (например, исчезнуть из кадра, как это бывает при уничтожении цели в компьютерных играх). Тем не менее необходимо соблюдать осторожность и не изменять статус спрайта до тех пор, пока проверка столкновений не будет выполнена для всех спрайтов. В противном случае могут возникнуть непредсказуемые ошибки.
Рассмотрим столкновение, в котором участвуют два спрайта. Наш код должен обнаруживать столкновение и сообщать об этом спрайтам. Предположим, один из спрайтов получает уведомление, немедленно вычисляет новую траекторию и изменяет свое положение. Когда сообщение о столкновении дойдет до второго спрайта, столкнувшийся с ним спрайт уже будет находиться в новом месте. Более того, перемещение первого спрайта может привести к тому, что для второго спрайта предыдущего столкновения как бы и не будет.
Чтобы избежать подобных неприятностей, необходимо соблюдать два правила:
  1. Положение спрайтов не должно изменяться до завершения цикла проверок.
  2. Каждый спрайт должен хранить информацию о положении спрайта, с которым он столкнулся, вместо того, чтобы обращаться к нему с запросом при обработке столкновения.
Класс Sprite эти правила соблюдает и, следовательно, справляется со всеми проблемами. Для этого обработка каждого столкновения осуществляется за две стадии, которые мы назовем подтверждением (acknowledgment) и реакцией (reaction). На стадии подтверждения спрайт всего лишь сохраняет статус и положение другого спрайта — его собственное положение и статус остаются неизменными. Затем, на стадии реакции, по ранее сохраненным данным определяются дальнейшие действия, вызванные столкновением. На этой стадии положение и статус спрайта могут изменяться. Функция Hit() класса Sprite используется для подтверждения, а функция Update() — для реакции. Класс Sprite определяется так:

class Sprite { public: Sprite(LPDIRECTDRAWSURFACE, int x, int y); LPDIRECTDRAWSURFACE GetSurf() { return surf; } operator LPDIRECTDRAWSURFACE() const { return surf; } int GetX() { return x; } int GetY() { return y; } int GetCenterX() { return x+w/2; } int GetCenterY() { return y+h/2; } void SetXY(int xx, int yy) { x=xx; y=yy; } void SetXYrel(int xx,int yy) { x+=xx; y+=yy; } CRect GetRect(); virtual void Update(); void Hit(Sprite*); void CalcVector(); private: LPDIRECTDRAWSURFACE surf; int x, y; int w, h; int xinc, yinc; BOOL collide; struct CollideInfo { int x, y; } collideinfo; };

Конструктор класса Sprite получает три аргумента: указатель на поверхность DirectDraw, изображающую новый спрайт, и два целых числа, определяющих начальное положение спрайта. Так как конструктору передается поверхность DirectDraw, одна и та же поверхность может использоваться для нескольких спрайтов. Конструктор можно было бы написать так, чтобы в качестве аргумента он получал имя BMP-файла и сам создавал поверхность, но тогда каждый спрайт был бы связан с отдельной поверхностью — даже если для создания нескольких спрайтов используется один и тот же BMP-файл.
Две следующие функции делают одно и то же, но имеют разный синтаксис. Функция GetSurf() и оператор-функция operator LPDIRCETDRAWSURFACE() возвращают указатель на поверхность DirectDraw, которая используется данным спрайтом. Мы уже видели, как GetSurf() используется функцией SpritesCollidePixel(). Перегруженный оператор LPDIRECTDRAWSURFACE() создан для удобства, благодаря ему объекты Sprite можно использовать вместо указателей на поверхности DirectDraw. Как вы увидите позднее, этот перегруженный оператор используется в программе Bumper.
Функции GetX(), GetY(), GetCenterX(), GetCenterY(), SetXY(), SetXYRel() и GetRect() предназначены для работы с положением спрайта. Мы уже видели, как функция GetRect() применяется на практике. В программе Bumper функции GetCenterX() и GetCenterY() используются для определения центральной точки спрайта, по которой определяется новое направление движения после столкновения.
Функция CalcVector() вычисляет вектор направления движения спрайта. Это направление выбирается случайным образом, и его в любой момент можно пересчитать заново.
Две последние функции, Hit() и Update(), уже упоминались выше. Они обеспечивают подтверждение и реакцию на столкновения.
В закрытой (private) секции объявляются переменные класса Sprite. Первая из них, surf, — указатель на интерфейс DirectDrawSurface, используемый для работы с поверхностью данного объекта Sprite. В переменных x, y, w и h хранятся положение и размеры поверхности. Переменные xinc и yinc служат для анимации спрайта. Как вы вскоре увидите, они инициализируются случайными величинами. Эти две переменные определяют направление, в котором движется спрайт.
В самом конце объявляются переменные collide и collideinfo. При обнаружении столкновения логической переменной collide присваивается значение TRUE, во всех остальных случаях она равна FALSE. Структура collideinfo содержит информацию о происшедшем столкновении. В данном случае нас интересует лишь положение второго спрайта, участвующего в столкновении.
Сейчас мы подробно рассмотрим все функции класса Sprite. Конструктор класса выглядит так:

Sprite::Sprite(LPDIRECTDRAWSURFACE s, int xx, int yy) { DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize=sizeof(desc); desc.dwFlags=DDSD_WIDTH | DDSD_HEIGHT; s->GetSurfaceDesc( &desc ); surf=s; x=xx; y=yy; w=desc.dwWidth; h=desc.dwHeight; collide=FALSE; CalcVector(); }

Конструктор получает в качестве аргументов указатель на поверхность DirectDraw и исходное положение спрайта. Сохранить эти значения в переменных класса нетрудно, однако мы еще должны инициализировать переменные ширины и высоты (w и h). Для этого необходимо запросить у поверхности DirectDraw ее размеры. С помощью структуры DDSURFACEDESC и функции GetSurfaceDesc() мы узнаем размеры и присваиваем нужные значения переменным. Переменной collide присваивается значение FALSE (потому что столкновение еще не было обнаружено). Наконец, мы вызываем функцию CalcVector(), которая определяется так:

void Sprite::CalcVector() { xinc=(rand()%7)-3; yinc=(rand()%7)-3; }

Функция CalcVector() инициализирует переменные xinc и yinc с помощью генератора случайных чисел rand(). Полученное от rand() значение преобразуется так, чтобы оно принадлежало интервалу от –3 до 3. Эти значения будут использоваться для перемещения спрайта при очередном обновлении экрана. Обратите внимание — одна или обе переменные вполне могут быть равны нулю. Если нулю равна только одна переменная, перемещение спрайта ограничивается осью X или Y. Если нулю равны обе переменные, спрайт вообще не двигается.
Функция GetRect() инициализирует объект CRect() данными о положении и размерах спрайта. Эта функция определяется так:

CRect Sprite::GetRect() { CRect r; r.left=x; r.top=y; r.right=x+w; r.bottom=y+h; return r; }

Перейдем к функции Hit(). Напомню, что эта функция вызывается при обнаружении столкновения. Функции Hit() передается один аргумент — указатель на спрайт, с которым произошло столкновение. Она выглядит так:

void Sprite::Hit(Sprite* s) { if (!collide) { collideinfo.x=s->GetCenterX(); collideinfo.y=s->GetCenterY(); collide=TRUE; } }

Функция Hit() реализует стадию подтверждения столкновений. В нашем случае она сохраняет положение каждого из столкнувшихся спрайтов и присваивает логической переменной collide значение TRUE. Обратите внимание — сохраняется лишь положение спрайта, а не указатель на сам спрайт. Это сделано намеренно, чтобы мы не смогли обратиться к спрайту во время реакции на столкновение (о ней говорится ниже). Следовательно, если вам потребуется другая информация о столкнувшемся спрайте, кроме его положения (например, тип спрайта или уровень его «здоровья» для компьютерной игры), ее необходимо сохранить в функции Hit(). Эту информацию следует получить немедленно, не дожидаясь стадии реакции, потому что к этому времени статус другого спрайта может измениться.
Функция Sprite::Update() выполняет две задачи: обновляет положение спрайта и, в случае столкновения, изменяет переменные, определяющие направление его перемещения (xinc и yinc). Функция Update() приведена в листинге 9.2.
Листинг 9.2. Функция Sprite::Update()

void Sprite::Update() { if (collide) { int centerx=GetCenterX(); int centery=GetCenterY(); int xvect=collideinfo.x-centerx; int yvect=collideinfo.y-centery; if ((xinc>0 && xvect>0) || (xinc<0 && xvect<0)) xinc=-xinc; if ((yinc>0 && yvect>0) || (yinc<0 && yvect<0)) yinc=-yinc; collide=FALSE; } x+=xinc; y+=yinc; if (x>640-w/2) { xinc=-xinc; x=640-w/2; } if (x<-(w/2)) { xinc=-xinc; x=-(w/2); } if (y>480-h/2) { yinc=-yinc; y=480-h/2; } if (y<-(h/2)) { yinc=-yinc; y=-(h/2); } }

Сначала Update() проверяет состояние логической переменной collide. Если переменная равна TRUE, мы получаем данные о положении двух спрайтов (текущего и столкнувшегося с ним) и используем их для вычисления новой траектории текущего спрайта. При этом используется схема, очень далекая от настоящей физической модели — при столкновении каждый спрайт отлетает в направлении, противоположном направлению удара.
Затем переменные x и y обновляются с учетом значений xinc и yinc. Новое положение спрайта проверяется и при необходимости корректируется. Корректировка происходит, когда спрайт более чем наполовину уходит за край экрана.
Возможно, вы заметили некоторую ограниченность в реализации класса Sprite: при каждом обновлении спрайт может отреагировать лишь на одно столкновение. При одновременном столкновении с несколькими спрайтами для расчета реакции будет использован лишь один из них. Чтобы изменить такое поведение, можно создать массив структур CollideInfo и отдельно сохранять информацию о каждом спрайте, полученную функцией Hit(). В этом случае при вычислении новой траектории курса на стадии реакции будет учитываться положение каждого спрайта, участвующего в столкновении. Однако на практике в подавляющем большинстве столкновений участвуют всего два спрайта.


Содержание Назад Вперед