Епізод 06. Пора б вже й щось намалювати!

Все, що ми робили до сьогодні, було пов'язано з підготовою вікна. Сьогодні пора нам продовжити розвивати наш фреймворк і спробувати щось намалювати. Оскільки переважна кількість ігор використовують попередньо створені ресурси (а саме графіка, звуки тощо), то ми сьогодні додамо можливість виводити в потрібному місці попередньо загатовлене зображення.

Поки для початку скористаємось нашим базовим класом. Додамо в списку приватних полів ще декілька:

    //Тимчасово для тестування
    FTmpTexture : PSDL_Texture;  //тут буде зображення
    FTmpSrcRect,
    FTmpDstRect : TSDL_Rect;     //а це для визначення областей вихідного зображення і місця виведення  

В конструкторі класу спробуємо заввантажити наше зображення. Для тестування я на популярному ресурсі завантажив деякі зображення, створені Kelvin Shadewing й доступні для використання по ліцензіях CC-BY-SA 4.0 та GPL 3.0, які дозволяють використовувати ці зображення безкоштовно з певними умовами. З текстом ліцензій можна ознайомитись за відповідними посиланнями.

Я скачав зображення "tux.png", яке розмістив в папці "assets" біля виконуваного файлу проекту.

Але є одна невелика проблема - бібліотека SDL 2.0 "з коробки" підтримує лише формат BMP, а зображення, які я хочу використати, збережені в форматі PNG, який має ряд переваг. На щастя, нічого конвертувати не доведеться, достатньо підключити в розділі uses ще один додатковий модуль - sdl2_image. За допомогою цього модуля можна скористатись офіційним розширенням бібліотеки, яке дозволяє використовувати обширний список графічних форматів і напряму їх використовувати. Про те, як користуватись цим розширенням, непогано описано в статті "Частина 7 - SDL2_image: Завантаження файлів зображень різних форматів", тому відразу приступлю до написання коду.

В кінці конструктора після ініціалізації всього необхідного спробуємо завантажити нашу текстуру:

      //тимчасово - завантаження текстури з файлу
 FTmpTexture:=IMG_LoadTexture(FRenderer,'assets'+DirectorySeparator+'tux.png'); 
       if FTmpTexture=nil then begin
          FErrorInfo:=SDL_GetError;
          FError:=true;
          WriteLn(FErrorInfo);
       end;

Наступним кроком спробуємо вивести зображення на екран. Логічно це зробити в методі Draw():

    //тимчасово - вивести зображення
    SDL_RenderCopy(FRenderer,FTmpTexture,nil,nil);

І не забути в деструкторі коректно звільнити зайняту під текстуру пам'ять:

   //тимчасова текстура
   SDL_DestroyTexture(FTmpTexture); 

Виконуємо контрольний запуск проекту, і, якщо все зроблено правильно, в нашому вікні ми повинні отримати зображення, розтягнуте до розмірів вікна. Початок вже є, ми можемо вивести картинку в наше вікно, але для гри цього мало. При розробці ігор використовується одночасно велика кількість зображень, які виводяться в потрібний час в потрібному місці і навіть потрібним фрагментом. Зображення, яке я використав для прикладу - це так званий "спрайтшіт" - набір кадрів анімації персонажа. І щоб вивести цю анімацію, потрібно виводити певні фрагменти зображення один за одним.

Щоб спростити собі життя для зручного керування зображеннями в грі, напишемо ще один клас, який і буде займатись завантаженням та всією іншою рутиною включаючи малювання. Назвемо його TGoTextureManager і визначимось, чим він буде займатись.

Основна вимога - одночасно обробляти потрібну кількість зображень. Зручно буде мати доступ до них по унікальних назвах. Для цього зручно скористатись дженериком:

type
  TGoTextureMap = specialize TFPGMap; 

Для його використання необхідно підключити модуль FGL, який є в стандартній комплектації Lazarus. Я описав відповідне приватне поле нашого менеджера текстур (нам непотрібно буде напряму звертатись до цього поля - весь механізм роботи з ним буде заховано "під капотом" менеджера текстур). Також нам потрібно буде конструктор та декструктор, де буде створюватись та знищуватись об'єкт карти текстур. А так як менеджер текстур буде використовуватись всередині нашого базового класу TGoEngine, то він теж буде як сінглтон, відповідно конструктор теж буде приватним.

    constructor TGoTextureManager.Create;
    begin
      inherited Create;
      FTextureMap:=TGoTextureMap.Create;
    end; 
    
    destructor TGoTextureManager.Destroy;
    var
      i: Integer;
    begin
      for i:= FTextureMap.Count-1 downto 0 do begin
          SDL_DestroyTexture(FTextureMap.Data[i]);
          FTextureMap.Delete(i);
      end;
      FreeAndNil(FTextureMap);
      inherited Destroy;
    end; 

Наступною вимогою до менеджера текстур буде завантаження файлів зображень і додавання їх до карти разом з назвою, яка буде слугувати ключем доступу до зображення.

  function TGoTextureManager.Load(aFileName: String; aID: String): Boolean;
    var TmpSurface : PSDL_Surface;
        TmpTexture : PSDL_Texture;
  begin
     Result := False;
     TmpSurface:=IMG_Load(PChar(aFileName));
     if TmpSurface=nil then begin
      WriteLN( SDL_GetError(),' Error loading '+aFileName);
      Exit;
     end;
     TmpTexture:=SDL_CreateTextureFromSurface(GoEngine.FRenderer,TmpSurface);
     SDL_FreeSurface(TmpSurface);
     if TmpTexture<>nil then begin
      FTextureMap[aID]:=TmpTexture;
      Result:=True;
     end;
  end;

Також менеджер текстур повинен вміти намалювати в заданих координатах зображення, за потреби масштабуючи до потрібних розмірів та перевертаючи горизонтально/вертикально чи навіть одночасно горизонтально і вертикально:

  procedure TGoTextureManager.Draw(aID: String; x, y, w, h: Integer;
      Flip: Integer);
    var srcRect, dstRect : TSDL_Rect;
        p : TSDL_Point;
  begin
     srcRect.x := 0;
     srcRect.y := 0;
     srcRect.w := w;
     dstRect.w := w;
     srcRect.h := h;
     dstRect.h := h;
     dstRect.x := x;
     dstRect.y := y;
     p.x:=0;
     p.y:=0;
     SDL_RenderCopyEx(GoEngine.FRenderer,FTextureMap[aId],@srcRect,@dstRect,0.0,@p,Flip);
  end;

Коли доведеться використовувати спрайтшіти і тайлмапи, потрібно буде намалювати відповідний кадр (будемо орієнтуватись на те, що всі кадри однакових розмірів і розміщені по рядках і стовбцях):

    procedure TGoTextureManager.DrawFrame(aID: String; x, y, w, h: Integer;
      CurrentRow, CurrentFrame: Integer; Flip: Integer);
    var srcRect, dstRect : TSDL_Rect;
  begin
     srcRect.x := w*CurrentFrame;
     srcRect.y := h*CurrentRow;
     srcRect.w := w;
     dstRect.w := w;
     srcRect.h := h;
     dstRect.h := h;
     dstRect.x := x;
     dstRect.y := y;
     SDL_RenderCopyEx(GoEngine.FRenderer,FTextureMap[aId],@srcRect,@dstRect,0.0,nil,Flip);
  end;

В кінцевому варіанті в нас вийшов наступний клас:

  TGoTextureManager = class
   private
    FTextureMap : TGoTextureMap;
    constructor Create;
   public
    function Load(aFileName : String; aID : String) : Boolean;
    procedure Draw(aID : String; x,y,w,h : Integer; Flip : Integer = SDL_FLIP_NONE);
    procedure DrawFrame(aID : String; x,y,w,h : Integer; CurrentRow, CurrentFrame : Integer; Flip : Integer = SDL_FLIP_NONE);
    destructor Destroy; override;
  end; 

Тепер нам потрібно інтегрувати його в фреймворк. Для цього опишемо відповідне поле, яке буде використовуватись для доступу до менеджера текстур:

   //Менеджер текстур
   property Texturemanager : TGoTextureManager read FTexturemanager;  

Також нам потрібно створити екземпляр менеджера текстур, для цього в конструкторі фреймворка після створення візуалізатора і перед виділенням пам'яті для структури обробки подій додамо наступні рядки коду:

        else begin
          FTexturemanager:= TGoTextureManager.Create;
        end;

І не забути в декструкторі фреймворка звільнити пам'ять, виділену під нього:

    FreeAndNil(FTexturemanager); 

Для тестування роботи менеджера текстур нам потрібно завантажити з диска файли зображень. Я зробив це в коді тестового проекту перед викликом ігрового циклу (поки що для тесту використав зображення, про які я згадував вище):

    GoEngine.TextureManager.Load('assets'+DirectorySeparator+'tux.png','Tux');
    GoEngine.TextureManager.Load('assets'+DirectorySeparator+'tuxfire.png','Tux Fire');  

Для того, щоб можна було виводити потрібний кадр і створити анімацію, поки що тимчасово додамо два приватні поля в класі фреймворку:

    fr,fc : Integer; 

Ці поля будуть відповідати за номери рядка і стовпчика, в якому знаходиться потрібний кадр. Я попередньо відкрив зображення через графічний редактор і визначив, що розміри кадру будуть 32х32 пікселі, а кожен зі спрайтшітів, які я використав для демонстрації, має по 10 рядків, в кожному з яких по 8 зображень. Прорахунок номера кадра для тесту я описав в методі фреймворка GameLogic():

    //тут буде прораховуватись ігрова логіка
   if fc >=7 then begin
    fc :=0;
    Inc(fr);
    if fr>=9 then fr:=0
       else Inc(fr);
   end else Inc(fc);
   SDL_Delay(75); 

Розрахунок затримки виводу наступного кадру я виставив виходячи зі швидкості анімації. В реальному проекті потрібно буде виконувати розрахунок такої затримки, цим ми займемось в одному з наступних епізодів. І, нарешті, саме цікаве - отримати анімацію на екрані. Для цього тимчасово в методі Draw() фреймворка після очистки вікна я написав наступні рядки коду:

    //тимчасово - вивести зображення на екран
    TextureManager.DrawFrame('Tux', 0, 0, 32, 32, fr, fc);
    TextureManager.DrawFrame('Tux', 64, 0, 32, 32, 9-fr, 7-fc, SDL_FLIP_HORIZONTAL);

    TextureManager.DrawFrame('Tux Fire', 0, 64, 32, 32, fr, fc);
    TextureManager.DrawFrame('Tux Fire', 64, 64, 32, 32, 9-fr, 7-fc, SDL_FLIP_HORIZONTAL);

Пробуємо запустити на виконання, і, якщо все зроблено правильно, ми повинні побачити чотири анімованих пінгвіна.

Ну що ж, ми сьогодні написали багато коду і значно розширили можливості фреймворку для виводу зображень. В результаті ми отримали зручний менеджер текстур, який дозволяє всього в кілька рядків коду заванажити та вивести зображення або потрбний його фрагмент і легко додавати в гру потрібну кількість зображень.

Як для одного епізоду, це достатньо, код проекту можна скачати на Github (в розділі Releases окремі архіви до кожного епізоду), а весь процес можна подивитись на відео:

← попередня частина | наступна частина →

В наступному епізоді перейдемо до роботи з ігровими об'єктами, а це ще на крок ближче до створення повноцінної гри.

Немає коментарів:

Дописати коментар