Епізод 05. Прискорюємо і зберігаємо

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

  1. додамо можливість переключатись в повноекранний режим і назад
  2. додамо реакцію на втрату і повернення фокусу вікна
  3. по можливості застосуємо апаратне прикорення
  4. всі налаштування класу будемо зберігати в файлі налаштувань і додамо можливість завантажувати їх з файла

Як вже робили це раніше, створимо новий проект та пропишемо потрібні шляхи для підключення модулів, також скопіюємо наш модуль ugoengine.pas з попереднього проекту, я не буду на цьому детально зупинятись.

Перше, що сьогодні додамо - це можливість переключення в повноекранний режим та назад. Більшість відеоігор зручніше грати в повноекранному режимі, і в багатьох програмах є можливість швидкого переключення між повноекранним і віконним режимами. Найчастіше для цього використовується клавіша <F11&rt;, тож саме її ми і задіємо.

Для реалізації цієї можливості нам необхідно завести відповідну властивість класу, логічно буде її назвати FullScreen. Буде вона логічного типу, і по замовчуванню в нас повноекранний режим буде виключений. Пропишемо відповідні зміни в коді:

 property FullScreen : Boolean read FFullScreen write SetFullScreen default False; //повноекранний режим

Натискаємо вже знайому корисну комбінацію Ctrl+Shift+C і дописуємо автоматично згенерований код код:

  if Ffullscreen=Avalue then Exit;
  Ffullscreen:=Avalue;
  if Ffullscreen then SDL_SetWindowFullscreen(FWindow,SDL_WINDOW_FULLSCREEN)
     else SDL_SetWindowFullscreen(FWindow,0);
  SDL_UpdateWindowSurface(FWindow);

Тепер необхідно дописати реакцію на клавішу для переключення повноекранного режиму. Не важко здогадатись, що це буде відбуватись в методі обробки подій нашого класу. Будемо реагувати на подію відпускання клавіші Переходимо до його реалізації і змінюємо існуючий код з врахуванням можливого розширення реакції на інші події в майбутньому:

   if SDL_PollEvent(FEvent)=1 then begin
    case FEvent^.type_ of
    //намагаємось відловити подію закриття вікна
SDL_QUITEV: FIsRun:=false;
SDL_KEYUP: begin
    //відловлюємо клавішу F11
    if (FEvent^.key.keysym.sym = SDLK_F11) then FullScreen:=not FullScreen;
    end;
   end;
  end;                                        

Все, що нам залишилось - запустити програму на виконання і перевірити чи все працює так, як нам потрібно.

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

Опишемо ще одну властивість нашого класу - Active, теж логічного типу, але зробимо її лише для читання:

   property Active : Boolean read FActive default True;//чи вікно має фокус 

Ми не будемо реалізовувати окремий метод встановлення цієї властивості, а напряму будемо змінювати відповідне поле класу при відловлюванні відповідних подій. Не буду затягувати, перейдемо одразу до коду і допишемо в кінці методу DoEvents наступні рядки:

    //втрата фокусу, згортання вікна
SDL_WINDOWEVENT_FOCUS_LOST, SDL_WINDOWEVENT_MINIMIZED: begin
     Factive:=False;
     WriteLN('Focus Lost!!!');
    end;
    //отримання фокусу
SDL_WINDOWEVENT_TAKE_FOCUS: begin
     Factive:=True;
     WriteLN('Focus Gained!!!');
    end; 

Оскільки програма в нас поки ще ніде не пірівіряє втрату чи отримання фокусу вікном, я просто зробив виведення відповідного повідомлення в консоль. Коли в нас виникне потреба в інших подіях вікна для відповідної реакції, то додати їх в цей код буде досить просто.

Тепер перейдемо до наступного пункту сьогоднішнього епізоду, а саме апаратного прискорення, яке використовують практично усі більш-менш серйозні ірги. Цілком логічно усю робото з графікою перекласти з процесора на відеокарту, і SDL нам в цьому допоможе.

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

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

    FWindowFlags : Integer; //прапорці для вікна
    FRendererFlags: Integer; //прапорці для візуалізатора 

Початкова ініціалізація цих прапорців буде відбуватись перед створенням вікна та візуалізатора на основі всановвлених властивостей відповідних полів. Це зручно використовувати, наприклад, при читанні налаштувань з файлу, що ми теж сьогодні реалізуємо, але трошки згодом. А поки повернемось до апаратного прискорення, і для цього введемо ще одну властивість класу:

   property HardwareAcceleration : Boolean read FHardwareAcceleration write SetHardwareAcceleration default True; //апаратне прикорення 

Знову використовуємо Ctrl+Shift-C для автодоповнення, і в принципі цей метод готовий. Але потрібно пам'ятати, що встановлення апаратного прискорення можливе лише під час створення візуалізатора, тому потрібно звільняти і по-новому ініціалізувати всі ресурси, які пов'язані з візуалізатором. Найпростіше це зробити шляхом перезапуску програми. Тому просто допишемо відповідне повідомлення для користувача:

  If (FRenderer<>nil) then begin
    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION,
       'Warning!!!',
       'Налаштування будуть задіяні після перезапуску!!!',
       FWindow);
  end;                                       

І останнє, що сьогодні додамо - це можливість зберігати налаштування в файл і читати з файла. Для того, щоб можна було обрати потрібні налаштування перед ініціалізацією SDL, пропоную створити звичайну форму засобами Lazarus зі стандартними компонентами. Напевно, вам доводилось бачити аналогічні форми при запуску деяких ігор для встановлення початкових параметрів. Зробимо аналогічно - в нас перед запуском гри теж буде відображатись вікно налаштувань, де можна буде задати чи змінити потрібні параметри, і після цього запустити наш фреймворк.

Найпоширенійший (і напевно найзручніший) спосіб зберігати налаштування в файлах формата INI. Це простий текстовий формат, де файл розділений на секції, назви який взяті в квадратні дужки, і в кожній секції є певна кількість параметрів у форматі "ключ"="значення".

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

В Lazarus є стандартний компонент для зручної роботи з файлами такого формату, називається він TINIFile й знаходиться в модулі INIFiles. Підключемо його в розділі uses нашого модуля ugoengine.

Для роботи з файлом налаштувань опишемо додаткові методи - ReadSettings і WriteSettings. Також потрібно описати внутрішні поля FSetingsFile і FSettingsSection. Їхні значення будуть передаватись через параметри конструктора. Оскільки читання і запис налаштувань в нас будуть відбуватись автоматично, найзручніші місця для їхнього виклику в конструкторі й деструкторі класу, а тому ці методи будуть приватними.

procedure Tgoengine.Loadsettings;
var INI : TINIFile;
begin
 INI := TIniFile.Create(FFileName);

 FHeight:=INI.ReadInteger(FSection,'HEIGHT',640);
 FWidth:=INI.ReadInteger(FSection,'WIDTH',480);
 FCaption:=INI.ReadString(FSection,'CAPTION','');
 Ffullscreen:=INI.ReadBool(FSection,'FULLSCREEN',True);
 Fhardwareacceleration:=INI.ReadBool(FSection,'HARDWARE ACCELERATION',True);

 FreeAndNil(INI);
end;

function Tgoengine.Savesettings: Boolean;
var INI : TINIFile;
begin
 Result := False;
 try
  INI := TIniFile.Create(FFileName);

  INI.WriteInteger(FSection,'HEIGHT',FHeight);
  INI.WriteInteger(FSection,'WIDTH',FWidth);
  INI.WriteString(FSection,'CAPTION',FCaption);
  INI.WriteBool(FSection,'FULLSCREEN',Ffullscreen);
  INI.WriteBool(FSection,'HARDWARE ACCELERATION',Fhardwareacceleration);
  Result := True;
 finally
  FreeAndNil(INI);
 end;
end;                                                         

При читанні/запису налаштувань ми працюємо безпосередньо з полями даних класу, а не через методи. Хоча це не зовсім коректно з точки зору парадигм ООП, але в нашому випадку оправдано тим, що під час читання налаштувань в нас ще не буде створено ні вікна, ні візуалізатора, а тому зміна цих властивостей через методи не буде коректно працювати.

І на останок, повернемось до прапорців вікна і візуалізатора, про які я згадував трохи раніше. Ми будемо встановлювати їхні значення перед створенням вікн і візуалізатора на основі даних полів класу, оскільки налаштування з кожним запуском програми можуть відрізнятись від попередніх. Код, який за це відповідає, знаходиться в конструкторі й буде наступним:

  constructor Tgoengine.Create(Afile: Tfilename = 'goengine.ini'; Asection: String = 'ENGINE');
  begin
   if CountInstances>0 then Exit;
    inherited Create;
    FError:=false;
    FErrorInfo:='';
    FFileName:=AFile;
    FSection:=ASection;
    LoadSettings;
    //ініціалізація бібліотеки SDL 2.0
    if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
     //прописуємо відповідні прапорці для вікна
     FWindowFlags:=SDL_WINDOW_SHOWN;
     if Ffullscreen then FWindowFlags:=FWindowFlags OR SDL_WINDOW_FULLSCREEN;
      //успішна ініціалізація - створюємо вікно
      FWindow:=SDL_CreateWindow('',
                                 SDL_WINDOWPOS_UNDEFINED,
                                 SDL_WINDOWPOS_UNDEFINED,
                                 FWidth,
                                 FHeight,
                                 FWindowFlags);
      //якщо вікно створене, створюємо візуалізатор
      if FWindow<>nil then begin
        FRendererFlags:=0;
        if Fhardwareacceleration then FRendererFlags:=FRendererFlags OR SDL_RENDERER_ACCELERATED;
        FRenderer:=SDL_CreateRenderer(FWindow,-1,0);
        if FRenderer=nil then begin
          FErrorInfo:=SDL_GetError;
          FError:=true;
          WriteLn(FErrorInfo);
        end;
      //виділення пам'яті для структури обробки подій
      New(FEvent);
      //встановлення ознаки виконання циклу і його запуск
      FIsRun := true;
      end else begin
       FErrorInfo:=SDL_GetError;
       WriteLn(FErrorInfo);
      end;
    end;
  end;                                               

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

  begin
   //зберігаємо налаштування в файл
   if not  SaveSettings then WriteLN('Не вдалось записати налаштування!');
   //прибрати за собою - в оберненому порядку створення
   Dispose(FEvent);
   SDL_DestroyRenderer(FRenderer);
   SDL_DestroyWindow(FWindow);
   SDL_Quit();

   inherited Destroy;

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

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

P.S. Пора б нам вже щось в це вікно намалювати! Не пропустіть наступний епізод - буде цікаво!

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

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