Урок 3. Спрайты

      Комментарии к записи Урок 3. Спрайты отключены

Помечено: , ,

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 7 мес., 3 нед. назад.

  • Автор
    Сообщения
  • #3139
    Урок 2СодержаниеДополнение к 3-му уроку

    Этот урок будет о спрайтах — объектах, из которых состоят практически все 2D-игры. К примеру, всем известная игра Super Mario. Вспомните все объекты из игры: сам Марио, враги, монеты, лестницы, кирпичи. Все это — спрайты. То есть, обычная 2D-игра состоит из спрайтов и бэкграунда (задний фон, обычно какая-нибудь картинка). Некоторые части заднего фона также могут являться спрайтами — движущееся солнце или облако например. Такие спрайты не могут воздействовать на игрока.
    lesson3-jpg

    Спрайты мы будем делать анимированные, но это не просто движение картинок, примерно как мы делали в прошлом уроке. Посмотрев на скриншот, кое-кто наверное узнал знаменитую игру от Phenomedia «Sven Bomwollen».

    Прежде чем непосредственно заняться нашими овечками, нам предстоит немного сложная работа. Мы напишем два класса (все-таки мы на С++ пишем) для комфортной работы со спрайтами: CSpriteBase и CSprite. Эти классы можно использовать практически в любой программе, например их мы и будем использовать в небольшой программе с овечками. Очень важно разобраться как работает наша спрайтовая система.

    Каждый спрайт состоит из кадров, прямо как в мультфильмах. Эти кадры отображаются по порядку с небольшой задержкой. В итоге мы видим то, что называется анимацией. Если же спрайт должен быть неподвижный, то кадр у него будет всего один. Каждый кадр — это BMP файл. Но мы же помним, что BMP-файл может быть только прямоугольником, а наши спрайты совсем нет. В предыдущем уроке забавное существо было заключено в белый прямоугольник. Но нам такие спрайты не нужны! Для этого мы сделаем спрайты частично прозрачными, а точнее — прозрачным будет все, что мы не хотим отображать при условии, что эти ненужные части одного цвета. Посмотрите на картинку:
    l3_1

    Фиолетовый цвет мы сделаем прозрачным. И у нас останется только симпатичная овечка! На этом рисунке прозрачный цвет мы назначим RGB(255,0,255) — в соответствии с кодировкой цвета RGB. При помощи SDL реализовать это очень просто. Вот небольшой пример:

    SDL_SetColorKey(surface, SDL_SRCCOLORKEY, SDL_MapRGB(surface->format,r,g,b));

    Здесь surface — это поверхность, на которую мы добавляем прозрачность, указывая RGB-составляющие цвета, который будет отображаться прозрачным (то есть не будет отображен). Чтобы убрать прозрачность просто замените SDL_SRCCOLORKEY на 0.

    Принцип работы нашего спрайтового движка таков: все кадры-картинки хранятся в отдельной папке. Здесь же находится специальный файл с именем info, в котором мы задаем кое-какие настройки для спрайта. Вот листинг одного из таких файлов:

    FILES: 4
    # SYNTAX:
    # filename of the frame, milliseconds to pause afterwards,
    # the RGB of the transparent part.
    1.bmp 190 255 0 255
    2.bmp 190 255 0 255
    3.bmp 190 255 0 255
    4.bmp 190 255 0 255

    Оператор FILES: задает количество кадров. После знака # расположен комментарий. Далее описывается каждый кадр: имя файла картинки с кадром, пауза в миллисекундах (для задержки между показами кадров) и соответственно RGB составляющие прозрачного цвета. Таков общий принцип работы.
    Начнем наконец программировать классы. Для начала нам понадобится структура CSpriteFrame. Эта структура описывает каждый кадр анимации:

    struct CSpriteFrame
    {
      SDL_Surface *image;
      int pause;
    };

    Каждый кадр состоит из поверхности (surface) с изображением и значением паузы в миллисекундах между показами кадров. Класс 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;
      }

    Далее считаем первую строчку в буфер. Мы знаем, что первая строка выглядит так: "FILE: n", где n — количество кадров. Достанем это число из буфера функцией sscanf, которая похожа на scanf, только считывает не с консоли, а со строковой переменной. Затем поместим количество кадров в член класса mNumframes и выделим память для массива CSpriteFrame:

      fgets(buffer,255,fp);
      sscanf(buffer,"FILES: %d",&mNumframes);
      mAnim = new CSpriteFrame[mNumframes];

    После этого установим значение mBuilt равным единице, что означает, что класс инициализирован. Заведем еще переменную-счетчик, чтобы считать загруженные картинки:

      mBuilt = 1;
      int count = 0;

    Теперь создадим цикл, который будет работать пока не достигнем конца файла (EOF) и пока 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)
        {

    Теперь считаем RGB, паузу и имя файла с картинкой кадра и загрузим его:

          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;

    Теперь проверим на прозрачность: если значение R (красного) равно -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();

    Далее у нас несколько очень простых inline-функций. Их код настолько прост, что встроен прямо в определение класса. Функция 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);

    В случае нажатий соответствующих клавиш обработаем координаты одного из спрайтов. Здесь я сделал управление так же, как в игре «Sven Bomvollen» от PhenomediaAG. Все изображения также принадлежат этой компании.

       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-му уроку

Для ответа в этой теме необходимо авторизоваться.