Урок 5. Side Scroller – разгоняем 2D

      Комментарии к записи Урок 5. Side Scroller – разгоняем 2D отключены

Помечено: , ,

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

  • Автор
    Сообщения
  • #3150
    Урок 4 Содержание Урок 6

    lesson5
    В своей нелегкой геймерской жизни вы не раз встречались с эффектом “side-scrolling“. Эффект заключается в том, что задний фон движется, создавая иллюзию движения объектов сцены. В основном это находит применение в аркадных играх. Примером может служить игрушка от Phenomedia AG “Catch The Sperm”. Немного измененный вариант очень часто применяется в 2D-стратегиях – перемещая курсор мыши к границе экрана, мы перемещаемся по карте. В этом уроке я расскажу о своих попытках реализовать сей эффект и какие последствия имели место.

    И так, давайте поразмышляем об алгоритме сайд-скроллера (извините за выражение). Самое простое, что может прийти в голову – сделать изображение заднего фона в несколько раз шире (или выше) размеров экрана и прокручивать его. Мой вам совет: никогда так не делайте. Это глупый метод, а изображение будет занимать огромный объем. Вы наверное думаете, если это такой глупый метод, то зачем я о нем упомянул… Дело в том, что я видел такие реализации сайд-скроллеров. Предлагаю вам более удачный алгоритм: мы имеем текстуру (назовем так картинку заднего фона) размерами с экран. Причем такую текстуру, что если мысленно (или не мысленно) совместить одну сторону с другой (например левую и правую), то получим плавное продолжение картинки. Эту текстуру мы не будем непосредственно изменять, а будем использовать ее в качестве источника кусочков, которые будем выводить на экран. Далее, мы за каждый проход цикла (главного игрового цикла) во время отрисовки очередного кадра сцены режем это изображение на две части. Предположим, что нужно двигать задний фон слева направо и за каждый кадр сцены (или фрейм) нужно скроллировать экран на 5 пикселей. Первый кусок бэкграунда вырезаем в координатах (0, 0) и до (ширина-5, 0). Кусок номер два вырезаем в позиции (ширина-5, 0) и до конца (т.е. ширина, 0). Далее мы меняем эти куски местами – второй кусок ставим перед первым.

    sdl-side-scroller

    Получается, что текстура как будто уплывает вправо за границы экрана, а слева тянется ее продолжение. В следующий проход нам нужно немного изменить координаты: вместо "5" сделаем "10". То есть, пятерка – это константа, которую мы прибавляем к переменой: var = var + 5. Эта константа есть ни что иное, как скорость перемещения. Тогда получаем некий алгоритм:

    do {
        Slice1 = Cut_Background(0, 0, width-var, 0);  //порядок: x1,y1,x2,y2
        Slice2 = Cut_Background(width-var, 0, width, 0);
    
        Paste_Slice2(0, 0, width-var, 0);    //порядок аналогичный
        Paste_Slice1(width-var, 0, width, 0);
    
        var = var + speed;
    } while(true);

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

    SDL_Surface *back; // глобальная переменная - сурфейс с задним фоном
    int ScrollSpeed = 5; // глобальная переменная скорость 
    int ScrollWidth = 0; // глобальная переменная, var из псевдокода
    
    void ScrollBG(){
     DrawIMG(back, ScrollWidth, 0, back->w - ScrollWidth, back->h, 0, 0);
     DrawIMG(back, 0, 0, ScrollWidth, back->h, back->w - ScrollWidth, 0); 
    
     ScrollWidth += ScrollSpeed;
     if(ScrollWidth > back->w) // если var больше ширины экрана, то обнуляем ее
     	ScrollWidth = 0;
    }

    Функция DrawIMG() знакома вам из предыдущих уроков. Она выводит часть заданного сурфейса на экран. Теперь мы сделаем немного странный поступок… Вместо того, чтобы по шагам разобрать код примера, а потом попытаться собрать его, вы просто скомпилируете урок и немного понаблюдаете за происходящим. У вас должно появиться окно с двигающимся задним фоном. В консоль каждую секунду пишутся какие-то непонятные циферки… О них мы сейчас и поговорим.

    Эти цифры – ни что иное, как очень важный показатель производительности графической программы FPS, Frames per Second (кадров в секунду). Соответственно показывает сколько кадров сцены (или фреймов) было нарисовано в течении секунды. Чем выше это значение, тем меньше так называемых “фликеров” – мерцаний экрана, а также быстрее работает программа. После запуска нашей программы значение FPS в моей системе (nVidia GeForce4 MX440SE с фирменными драйверами, XFree86 4.2.0-186, SuSE 8.1) было около 14… Если кто не знает, то это ОЧЕНЬ МАЛО… На полном экране значение оставалось в тех же пределах. Вот тут-то и началось хождение по мукам. Совершенно случайно я наткнулся в FAQ на совершенно безобидный вопросик: “Как включить поддержку DGA?“… Ответ еще не лучше: “Установите значение переменной окружения SDL_VIDEODRIVER = dga“. Вот и всё… Но когда я включил эту поддержку, то FPS вырос ровно в шесть раз и стал превышать цифру 80… И так, DGA – Direct Graphics Acceleration – расширение X Window аналогичное по сути DirectDraw, то-есть предоставляет программам аппаратное ускорение 2D графики. И только 2D. Приложения SDL с поддержкой DGA _всегда_ работают только на полный экран. Но это еще не все проблемы… Как вы знаете, SDL предоставляет интерфейс к OpenGL – трехмерной графической библиотеке – и много игр написано в SDL + OpenGL. Так вот, такие игры работать НЕ БУДУТ… Вам придется перед запуском 2D программы устанавливать SDL_VIDEODRIVER = dga, а потом восстанавливать значение по умолчанию SDL_VIDEODRIVER = x11.
    В оболочках Bourne (bash и т.п.) можно установить переменные так:
    user@linux$ export SDL_VIDEODRIVER=dga

    Если у вас другая оболочка, то вы скорее всего бородатый юниксоид, который не станет читать этот документ, а напишет свой :). Вообще значений этой переменной довольно много: x11, dga (XFree DGA2), fbcon (framebuffer console), directfb и т.д. Список всех переменных окружения, используемых SDL можно найти на официальном FAQ. Там есть ссылка на текстовый документ. Я включил этот документ в архив с исходниками к уроку. Убедитесь, что в конфиге к вашему X-серверу (/etc/X11/XF86Config) есть строка:

    Section "Module"
      ...
      Load  "extmod"
      ...
    EndSection

    При необходимости добавьте ее (естественно с правами root).
    К счастью этот процесс можно автоматически делать в самой программе. К тому же, DGA есть в XFree86 начиная с версии 4.0 (встроено DGA2, а в версиях 3.x – DGA1 работать не будет!). Так что проблем с запуском на других машинах не будет, поскольку старые иксы встречаются довольно редко. Стандартная библиотека stdlib предоставляет средства для работы с переменными окружения. Функция
    char* getenv(const char* name);
    возвращает значение переменной окружения name. Функция
    int setenv(const char* name, const char* value, int overwrite);

    позволяет установить переменную окружения name присвоить ей значение value. Если параметр overwrite не равен нулю, то функция перезапишет значение переменной, а если ноль, то оставит переменную окружения без изменений. Это полезно, когда вам нужно создать новую переменную окружения, а если она уже создана, то не трогать ее. Для нашей идеи – перед запуском программы изменять переменную SDL_VIDEODRIVER на dga, а потом восстанавливать ее в первоначальное состояние – хватит этих двух функций. Посмотреть их подробное описание можно так: man 3 getenv или man 3 setenv. Вот реализация этих простых функций:

    /* Закомментируйте строку, если не хотите использовать расширение DGA */
    #define USE_DGA
    
    /* глобальная переменная, хранит старое значение SDL_VIDEODRIVER */
    char* old_env; 
    
    void DGA_SetEnv(){
    #ifdef USE_DGA 
      old_env = getenv("SDL_VIDEODRIVER");
      printf("Warning: SDL_VIDEODRIVER: %s will be changed to dga!!!\n",old_env);  
      setenv("SDL_VIDEODRIVER", "dga", 1);
    #endif 
    }
    
    void DGA_RestoreEnv(){
    #ifdef USE_DGA 
      setenv("SDL_VIDEODRIVER", old_env, 1);
    #endif 
    }

    Затем, в самом начале программы вызываем DGA_SetEnv(), а перед выходом DGA_RestoreEnv(). Если у кого-то не будет работать расширение DGA, то его можно отключить закомментировав строку #define USE_DGA. Тогда эти функции не будут ничего выполнять. Настало время рассказать об очень полезной вещи, как библиотеки объектных файлов.
    До этого момента мы при компиляции создавали объектные файлы: g++ -c ex.o ex.cxx. Потом мы линковали все объектные файлы в исполняемую программу. В достаточно крупном проекте таких объектных файлов может быть десятки или даже сотни. Их можно объединить в одеу библиотеку. Например, можно создать библиотеку для нашего CSprite2. Сейчас мы будем собирать библиотеку для автоматической установки переменной окружения. И так, мы создали набор функций. Скомпилировали его в объектный файл:
    gcc -c dga_sdl.c -o dga_sdl.o

    Далее используем программу ar. Допустим наша библиотека называется DGA_SDL. В имени библиотеки должен обязательно быть префикс lib:
    ar rc libDGA_SDL.a dga_sdl.o

    Ключ r – заменяет (replace) старую библиотеку на новую, а ключ c – создает ее, если он не существует. Но пока у нас только архив с объектными файлами. Для полноценной библиотеки нужно добавить индекс – список функций:
    ranlib libDGA_SDL.a

    В некоторых системах ranlib не имеет никакого эффекта, т.к. индекс автоматически создается командой ar, но на всякий случай советую оставить эту команду. Таким нехитрым способом мы получили статическую библиотеку. Для ее использования нужно указать ее при компиляции и включить в программу заголовок.
    gcc -o ex ex.c -L./ -lSDL_DGA

    Ключ -L указывает дополнительную директорию с библиотекой, а ключ -l – имя. Существуют еще динамические библиотеки. Для тех, кто программировал в Win32 аналогия такая: статические библиотеки аналогичны *.lib, а динамические – *.dll. Там очень подробно описано создание и использование библиотек. В исходниках к уроку вы найдете еще одну библиотеку HTMLog. Она очень простая и позволяет вести log-журнал в виде HTML. Вот пример использования этой библиотеки:

    #include "htmlog.h" 
    
    int main(){
     HTMLog* log = new HTMLog(HTM_FILE);
    
    // жирный, подчеркнутый, цвет (rgb - шестнадцатиричное) #000000 (черный)
    // есть еще флаг HTM_STYLE_ITALIC - курсив
     log->Custom("This is custom message",HTM_STYLE_BOLD|HTM_STYLE_UNDERLINE,"000000");
     
     log->Info("This is info message");
     log->Warning("This is warning message");
     log->Error("This is error message");
    
     delete log;
     return 0;
    }

    При создани экземпляра класса задается параметр цели:

        HTM_FILE - запись в log.html
        HTM_STDOUT - вывод в консоль
        HTM_EMPTY - не выводить сообщения

    Методы:

    • Plain(char* message) – обычные текст
    • Info(char* message) – информационное сообщение
    • Warning(char* message) – предупреждение
    • Error(char* message) – ошибка
    • Custom(char* message, int style, char* color) – форматированный текст

    Можете использовать этот класс в своих програмах. HTML логи читать куда приятнее, чем консольные.

    Теперь самое время вернуться к FPS. Давайте разберемся, как реализовать его подсчет. Раз в секунду мы должны отображать, сколько кадров нарисовано в течении секунды. Каждый кадр по традиции мы рисуем в функции DrawScene().

    void DrawScene(void){
    
      ScrollBG();
    
      SDL_Flip(screen);
      FPS++;
    }

    На это раз все очеь просто. Мы считаем количество кадров в переменной FPS. Теперь задача – раз в секунду отобразить эту переменную и обнулить ее, чтобы не учитывать кадры из прошлой секунды. Можно реализовать вызов раз в секунду при помощи SDL_GetTicks, с которой мы уже знакомы. Но есть действительно отличный способ сделать это – SDL таймеры. Причем, эти таймеры работают на всех платформах. И так, начнем.

    Для начала создаем таймер:
    SDL_TimerID my_timer_id = SDL_AddTimer(Uint32 interval, SDL_NewTimerCallback callback, void* param);
    Мы получаем id нового таймера. interval задает время в миллисекундах, через которое произойдет вызов функции callback(). В эту функцию можно передать параметры только типа void * через param. Объявлять callback функцию нужно так:

    Uint32 my_callbackfunc(Uint32 interval, void *param);
    void* my_callback_param;

    Далее напишем эту callback функцию:

    Uint32 my_callbackfunc(Uint32 interval, void *param){
    
     printf("FPS: %d\n",FPS); 
     FPS=0;
     return interval;
    }

    Эта функция в течении interval миллисекунд выводит значение переменной FPS. Возвращать эта функция обязательно должна количество миллисекунд, через которое произойдет следующий вызов. То есть, мы можем поменять интервал, если необходимо. При инициализации SDL (SDL_Init()) нужно добавить флаг SDL_INIT_TIMER. Вот например так:

     if ( SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO) < 0 ){ 
       printf("Unable to init SDL: %s\n", SDL_GetError()); 
       exit(1); 
     } 

    Далее создаем таймер:
    SDL_TimerID my_timer_id = SDL_AddTimer(1000, my_callbackfunc, my_callback_param);

    указывая ему в параметрах 1000 мс (1 секунда) и названия функции и параметра, которые мы определили. Перед выходом из программы обязательно нужно удалить созданный таймер:
    SDL_RemoveTimer(my_timer_id);

    Вот и все. Раз в секунду мы выводим сколько раз была нарисована сцена. Внимательно изучите исходники к уроку. Программа довольно простая и обльшинство кода вам знакомо из прошлых уроков. Новые функции мы разобрали. В файле /lib/dga/dga_sdl.h раскомментируйте строку #define USE_DGA и сравните FPS настоящего апаратного ускорения с програмным. Осталось добавить кое что про Makefile. Чтобы выполнить make внешнего мэйкфайла нужно сделать так:
    $(MAKE) -C dir param

    Где dir – директория с внешним Makefile, а param – параметр, например clean. Если вам не нужен параметр, то просто не пишите его. И обратите внимание на команду #include "dga/dga_sdl.h"! Директория dga находится в директории lib и мы по идее должны писать так: #include "lib/dga/dga_sdl.h", но при компиляции мы просто укажем ключ -I./lib, который включает директорию lib в список директорий с заголовочными файлами.
    Как всегда, предлагаю вам загрузить исходники к уроку.

    Урок 4 Содержание Урок 6

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