Урок 2 | Содержание | Дополнение к 3-му уроку |
CSpriteBase
и CSprite
. Эти классы можно использовать практически в любой программе, например их мы и будем использовать в небольшой программе с овечками. Очень важно разобраться как работает наша спрайтовая система.SDL_SetColorKey(surface, SDL_SRCCOLORKEY, SDL_MapRGB(surface->format,r,g,b));
surface
- это поверхность, на которую мы добавляем прозрачность, указывая RGB-составляющие цвета, который будет отображаться прозрачным (то есть не будет отображен). Чтобы убрать прозрачность просто замените SDL_SRCCOLORKEY
на 0.info
, в котором мы задаем кое-какие настройки для спрайта. Вот листинг одного из таких файлов:
FILES: 4 |
FILES:
задает количество кадров. После знака #
расположен комментарий. Далее описывается каждый кадр: имя файла картинки с кадром, пауза в миллисекундах (для задержки между показами кадров) и соответственно RGB составляющие прозрачного цвета. Таков общий принцип работы.CSpriteFrame
. Эта структура описывает каждый кадр анимации.
struct CSpriteFrame { SDL_Surface *image; int pause; }; |
CSpriteBase
является базой для спрайта. В нем хранятся все изображения кадров и другие данные. Из одной базы можно создать сколько угодно спрайтов (целое стадо овечек) и это не займет много памяти, т.к. изображения для идентичных спрайтов хранятся в базе, а каждый спрайт просто ссылается на базу.CSpriteBase
имеет тоьлко одну функцию - init
, которая инициализирует класс и загружает все изображения. Также есть указатель на структуру CSpriteFrame
, а точнее даже массив, и несколько других переменных: индикатор того, что класс создан, количество кадров, ширина и высота спрайта.
class CSpriteBase { public: int init(char* dir); CSpriteFrame *mAnim; int mBuilt, mNumframes, mW, mH; }; |
init
- принимает единственный параметр - имя папки с изображениями спрайта и файлом info. Весь этот код нужно поместить в заголовочный файл cspritebase.h
(обязательно достаньте исходники для этого урока, иначе будет очень сложно).init
сначала определим три массива типа char
, которые нужны в качестве временных хранилищ кое-каких данных. Также объявим 4 временных переменных типа int
и один указатель типа FILE
.
int CSpriteBase::init(char* dir){ char buffer[255]; char filename[255]; char name[255]; int pause=0, r=0, g=0, b=0; FILE *fp; |
sprintf
. Эта функция похожа на printf
, только печатает не на экран, а в переменную.
sprintf(filename,"%s/info",dir); if((fp=fopen(filename,"r"))==NULL){ printf("Error opening file %s\n\n",filename); return -1; } |
sscanf
, которая похожа на scanf
, только считывает не с консоли, а со строковой переменной. Затем поместим количество кадров в член класса mNumframes
и выделим память для массива CSpriteFrame
.
fgets(buffer,255,fp); sscanf(buffer,"FILES: %d",&mNumframes); mAnim = new CSpriteFrame[mNumframes]; |
mBuilt
равным единице, что означает, что класс инициализирован. Заведем еще переменную-счетчик, чтобы считать загруженные картинки.
mBuilt = 1; int count = 0; |
count < mNumframes
. Будем считывать по одной строке из файла. Если строка - комментарий, то не считаем ее.
while(!feof(fp) && count<mNumframes){ fgets(buffer, 255, fp); if(buffer[0]!='#' && buffer[0]!='\r' && buffer[0]!='\n' && buffer[0]!='\0' && strlen(buffer)!=0) { |
sscanf(buffer,"%s %d %d %d %d",name,&pause,&r,&g,&b); sprintf(filename,"%s/%s",dir,name); |
SDL_Surface
и загрузим в нее картинку.
SDL_Surface *temp; if((temp=SDL_LoadBMP(filename))==NULL) return -1; |
if(r>=0) SDL_SetColorKey(temp,SDL_SRCCOLORKEY,SDL_MapRGB(temp->format,r,g,b)); |
CSpriteFrame
загруженную картинку с помощью SDL_DisplayFormat
. Эта функция возвращает SDL_Surface
, но в том же формате, что и экранная поверхность. Это нужно для увеличения скорости отображения, т.к. не тратится время на преобразования форматов каждый раз. Наконец, уничтожим временную поверхность, чтобы не создавать утечки памяти.
mAnim[count].image = SDL_DisplayFormat(temp); SDL_FreeSurface(temp); |
pause
массива mAnim
значение из файла info. Ширину и высоту спрайта сделаем таких же размеров, как у первого кадра.
mAnim[count].pause = pause; if(!mW) mW = mAnim[count].image->w; if(!mH) mH = mAnim[count].image->h; count++; } } |
fclose(fp); return 0; } |
CSpriteBase
.CSprite
. Он описывает непосредственно сам спрайт. Заголовок этого класса будет немного больше чем у предыдущего. Для иницифлизации класса служит все та же функция init
. Эта функция принимант два параметра: указатель на экземпляр класса CSpriteBase
(мы передаем именно указатель, чтобы не загромождать память) и указатель на поверхность SDL_Surface
. Мы разберем их подробнее, но немного позже. Функции draw(), clearBG()
и updateBG()
используются для отрисовки спрайта на экране. О них тоже немного позже.
class CSprite { public: int init(CSpriteBase *base, SDL_Surface *screen); void draw(); void clearBG(); void updateBG(); |
setFrame()
изменяет значение переменой mFrame
, которая задает следующий кадр анимации. Функция getFrame()
возвращает текущий отображаемый кадр.
void setFrame(int nr) { mFrame = nr; } int getFrame() { return mFrame; } |
setSpeed()
и getSpeed()
. Они работают с переменной mSpeed
, которая задает скорость отображения спрайтов. Эта скорость умножается с паузой, то есть это коэффициент. Если mSpeed
равна 2, то скорость будет в 2 раза медленнее, а если зададим значение 0,5 то скорость будет в два раза быстрее.
void setSpeed(float nr) { mSpeed = nr; } float getSpeed() { return mSpeed; } |
toggleAnim(), startAnim()
и stopAnim()
соответственно инвертируют, запускают и останавливают анимацию. Функция rewind()
устанавливает следующий кадр в ноль (то есть на самый первый).
void toggleAnim() { mAnimating = !mAnimating; } void startAnim() { mAnimating = 1; } void stopAnim() {mAnimating = 0; } void rewind() {mFrame = 0; } |
xadd()
и yadd()
прибавляют значение к текущим координатам спрайта. Например, xadd(1)
сместит спрайт вправо на 1 единицу, а yadd(-3)
сместит спрайт вверх на 3 единицы. xset()
и yset()
позволяют вручную установить координаты. set()
устанавливает сразу обе координаты.
void xadd(int nr) { mX+=nr; } void yadd(int nr) { mY+=nr; } void xset(int nr) { mX=nr; } void yset(int nr) { mY=nr; } void set(int xx, int yy) { mX=xx; mY=yy; } |
mFrame
указывает на следующий кадр анимации. mX
и mY
содержат текущие координаты спрайта. mOldX
и mOldY
используются для перемещения спрайта (вспомните второй урок: для стирания старого изображения нам нужно помнить его предыдущие координаты). mAnimating
включает и выключает анимацию (1 - включено, 0 - выключено). mDrawn
указывает был ли спрайт хоть раз отображен или нет. mLastupdate
содержит время последнего обновления спрайта (подробнее немного позже). mSpriteBase
- указатель на базу CSpriteBase
, в которой хранятся все изображения этого спрайта. mBackreplacement
- используется для обновления экрана и содержит участок заднего фона, которым затирается предыдущий кадр анимации. И наконец mScreen
- указатель на экранную поверхность.
private: int mFrame; int mX, mY, mOldX, mOldY; int mAnimating; int mDrawn; float mSpeed; long mLastupdate; CSpriteBase *mSpriteBase; SDL_Surface *mBackreplacement; SDL_Surface *mScreen; }; |
SDL_Surface *mBackreplacement
. После этого рисуем этот кадр. Прежде чем нарисовать следующий кадр мы затираем старое изображение сохраненным участком фона. И так продолжается сколько угодно долго.init
. Она также как и в предыдущем классе служит для инициализации. Для начала мы присвоим переданный указатель на базу нашей переменной mSpriteBase
. Далее, если класс базы был создан (установлена переменная mBuilt
), то начинаем работать. Если количество кадров более одного то присваиваем переменной mAnimating
значение 1, то есть анимация есть. Далее присвоим переменной mBackreplacement
значение самого первого кадра. Это нужно для того, чтобы эта наша поверхность приняла размеры спрайта, иначе ее размеры просто не определены. Делаем это при помощи SDL_DisplayFormat
, т.к. я уже упоминал, что такое копирование работает гораздо быстрее. И наконец задаем переменную класса mScreen
, присваиваем ей указатель на экран.
int CSprite::init(CSpriteBase *base, SDL_Surface *screen) { mSpriteBase = base; if(mSpriteBase->mBuilt) { if(mSpriteBase->mNumframes>1) mAnimating=1; mBackreplacement = SDL_DisplayFormat(mSpriteBase->mAnim[0].image); } mScreen = screen; return 0; } |
clearBG()
, которая затирает предыдущий кадр участком фона. Затирать нужно в старых координатах, то есть в mOld[X/Y]
.
void CSprite::clearBG() { if(mDrawn==1) { SDL_Rect dest; dest.x = mOldX; dest.y = mOldY; dest.w = mSpriteBase->mW; dest.h = mSpriteBase->mH; SDL_BlitSurface(mBackreplacement, NULL, mScreen, &dest); } } |
updateBG()
сохраняет участок фона в текущих координатах в переменную mBackreplacement
. Делается это очень просто, как в предыдущем уроке. И конечно, не забудем присвоить старым координатам новые значения.
void CSprite::updateBG() { SDL_Rect srcrect; srcrect.w = mSpriteBase->mW; srcrect.h = mSpriteBase->mH; srcrect.x = mX; srcrect.y = mY; mOldX=mX; mOldY=mY; SDL_BlitSurface(mScreen, &srcrect, mBackreplacement, NULL); } |
draw()
, которая, как вы уже наверное догадались, рисует спрайт на экран. Первое, что мы делаем - проверяем нужно ли увеличить переменую mFrame
. Для учета времени будем использовать функцию SDL_GetTicks()
, которая возвращает количество миллисекунд с того момента, как инициализирован SDL. Также имейте ввиду, что эта функция работает корректно около 49 дней, а дальше ведет себя непредсказуемо. Проверим, если время последнего обновления (mLastupdate
) плюс пауза меньше текущего времени, то прибавляем mFrame
, показывая следующий кадр. Затем смотрим, чтобы mFrame
не превышало количества кадров в нашем спрайте, а если так, то устанавливаем следующий кадр в ноль. И наконец присваиваем переменной mLastupdate
текущее время.
void CSprite::draw() { if(mAnimating == 1) { if(mLastupdate+mSpriteBase->mAnim[mFrame].pause*mSpeed < SDL_GetTicks()) { mFrame++; if(mFrame>mSpriteBase->mNumframes-1) mFrame=0; mLastupdate = SDL_GetTicks(); } } |
mDrawn
в единицу, чтобы затереть старое изображение в clearBG()
. А дальше просто рисуем очередной кадр спрайта на экран.
if(mDrawn==0) mDrawn=1; SDL_Rect dest; dest.x = mX; dest.y = mY; SDL_BlitSurface(mSpriteBase->mAnim[mFrame].image, NULL, mScreen, &dest); } |
SDL_Surface *screen и *back
- экран и задний фон. Две базы: одна для овечек, другая для милой парочки. Спрайтов будет три: sven, sheep и sheep1.
SDL_Surface *screen, *back; CSpriteBase svenbase; CSpriteBase sheepbase; CSprite sven; CSprite sheep; CSprite sheep1; |
ImageLoad()
очень простая. Она почти такая же как с прошлого урока, но использует SDL_DisplayFormat()
для скорости.
SDL_Surface* ImageLoad(char *file){ SDL_Surface *temp1, *temp2; temp1 = SDL_LoadBMP(file); temp2 = SDL_DisplayFormat(temp1); SDL_FreeSurface(temp1); return temp2; } |
InitImages()
просто загружает картину заднего фона.
int InitImages(){ back = ImageLoad("data/bg.bmp"); return 0; } |
DrawIMG()
рисует изображение на экран в заданной позиции.
void DrawIMG(SDL_Surface *img, int x, int y){ SDL_Rect dest; dest.x = x; dest.y = y; SDL_BlitSurface(img, NULL, screen, &dest); } |
DrawBG()
отображает задний фон на экран.
void DrawBG(){ DrawIMG(back, 0, 0); } |
DrawScene()
мы рисуем спрайты. Сначала затираем задний фон, затем обновляем и рисуем. И в конце - SDL_Flip()
для обновления экрана.
void DrawScene(void){ sven.clearBG(); sheep.clearBG(); sheep1.clearBG(); sven.updateBG(); sheep.updateBG(); sheep1.updateBG(); sven.draw(); sheep.draw(); sheep1.draw(); SDL_Flip(screen); } |
main()
. Начало вам знакомо из предыдущих уроков.
int main(int argc, char *argv[]){ Uint8* keys; if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ){ printf("Unable to init SDL: %s\n", SDL_GetError()); exit(1); } atexit(SDL_Quit); SDL_WM_SetCaption("SDL Gfx Example #3","ex3"); SDL_WM_SetIcon(SDL_LoadBMP("icon.bmp"), NULL); screen=SDL_SetVideoMode(800,600,32,SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_FULLSCREEN); // if ( screen == NULL ){ printf("Unable to set 800x600 video: %s\n", SDL_GetError()); exit(1); } |
svenbase.init("data/sven"); sheepbase.init("data/sheep"); |
sven.init(&svenbase,screen); sven.set(250,400); sven.setSpeed(0.8); sheep.init(&sheepbase,screen); sheep.set(350,300); sheep.setSpeed(3.5); sheep1.init(&sheepbase,screen); sheep1.set(150,250); sheep1.setSpeed(4.5); |
SDL_ShowCursor(0); InitImages(); DrawBG(); |
int done=0; while(done == 0){ SDL_Event event; while ( SDL_PollEvent(&event) ){ if ( event.type == SDL_QUIT ){ done = 1; } if ( event.type == SDL_KEYDOWN ){ if ( event.key.keysym.sym == SDLK_ESCAPE ){ done = 1; } if ( event.key.keysym.sym == SDLK_SPACE ){ sven.toggleAnim(); } } } keys = SDL_GetKeyState(NULL); |
if(keys[SDLK_UP]){ sven.yadd(-2); sven.xadd(2);} if(keys[SDLK_DOWN]){ sven.yadd(2); sven.xadd(-2); } if(keys[SDLK_LEFT]){ sven.xadd(-2); sven.yadd(-2); } if(keys[SDLK_RIGHT]){ sven.xadd(2); sven.yadd(2); } |
DrawScene(); } return 0; } |
Урок 2 | Содержание | Дополнение к 3-му уроку |
©opyleft PLG, 2003.