неділя, 22 жовтня 2017 р.

Розробка фреймворка для 2-Д відеоігор на основі мультимедійної бібліотеки SDL2. Частина 4. Повноцінне вікно.

Зміна заголовку вікна

Ми вже навчились програмно встановлювати повноекранний режим, тепер я розповім, як можна програмно змінити заголовок вікна. Це боває корисно, наприклад, коли в віконному режимі гри ти захочеш вивести назву або номер рівня, який зараз завантажений, чи ще якусь довідкову інформацію. SDL дозволяє це зробити викликом відповідної функції - SDL_SetWindowTitle(), параметрами якої є вказівник на вікно, в якому буде мінятись заголовок, і новий текст заголовку. Щоб це спрацювало, перейди в метод нашого класу SetCaption і в самий кінець допиши наступний код:
if fSDLWindow<>nil then SDL_SetWindowTitle(fSDLWindow,PChar(UTF8String(FCaption)));
Перевірка оператором if нам потрібна для того, щоб не виникло помилки зміни заголовку вікна до того, як вікно буде створене. Аналогічну перевірку також потрібно поставити і при зміні повноекранного/віконного режимів.

Зміна геометрії вікна

Ти вже знаєш як встановити повноекранний режим, але що робити, коли потрібно змінити розміри вікна під час виконання програми? Як це можна реалізувати засобами SDL? Спробуємо розібратись разом.
Пам’ятаєш, трошки раніше ми вже додали властивості ширини і висоти вікна? Я обіцяв, що ми ще повернемось до цього пізніше, і це “пізніше” настало.
Щоб вікно могло змінювати розміри, необхідно в першу чергу при створенні вказати відповідний прапорець SDL_WINDOW_RESIZABLE (див. таблицю вище). Функція створення вікна прийме наступний вигляд:
   fSDLWindow:=SDL_CreateWindow(PChar(Utf8String(Caption)),
                  SDL_WINDOWPOS_UNDEFINED,
                  SDL_WINDOWPOS_UNDEFINED,
                  Height,
      Width,
                  SDL_WINDOW_SHOWN or SDL_WINDOW_RESIZABLE); 
Тепер після того, як вікно з’явиться на екрані, можна мишкою змінювати його розміри, але як дізнатись програмно, які будуть ширина і висота вікна після зміни його розмірів? SDL для роботи з вікном має окремий тип подій, SDL_WINDOW_EVENT, а конкретно для зміни розмірів вікна - SDL_WINDOWEVENT_SIZE_CHANGED, а в додаткових полях структури події можна взяти інформацію про нові розміри вікна. Якщо в обробку подій нашого класу додати ще один if, програмний код стане важче сприйматись, тому я вирішив використати оператор вибору case, тому що ще потрібно додати декілька подій, обробку яких теж будемо реалізовувати. В мене вийшов наступний код для методу обробки подій:
procedure TGOEngine.DoEvents;
begin
   if SDL_PollEvent(fSDLEvent)=1 then begin
      case fSDLEvent^.type_ of
      //відловлюємо подію закриття вікна
      //і встановлюємо ознаку виконання в false для переривання ігрового циклу
SDL_QUITEV: fRunning:=false;
SDL_KEYUP: begin
      //відловлюємо Alt+Enter для зміни режиму
         if (fSDLEvent^.key.keysym.sym = SDLK_RETURN) and ((fSDLEvent^.key.keysym._mod and KMOD_ALT) <> 0) then
            IsFullScreen:=not IsFullScreen;
       end;
      //обробка подій вікна
SDL_WINDOWEVENT: begin
        case fSDLEvent^.window.event of
   //зміна розмірів вікна
     SDL_WINDOWEVENT_SIZE_CHANGED: begin
            Width:=fSDLEvent^.window.data1;
            Height:=fSDLEvent^.window.data2;
           end;
        end;
       end;
      end;
   end;
end;
Щоб перевірити, чи дійсно ми дізнались нові розміри вікна, спробуємо їх вивести в заголовок. Для цього в методи зміни висоти і ширини вікна просто додай наступний рядок в самому кінці обох методів (після тестування не забудь їх видалити):
Caption:=’Ширина:’+IntToStr(FWidth)+’/Висота:’+IntToStr(FHeight);
Тепер спробуй запустити програму і змінити розміри вікна, в заголовку тобі мають відображатись поточні розміри.

Обробка інших корисних подій.

Основні потрібні методи ми вже розглянули, але при розробці ігор нам може знадобитись ще дещо корисне. Розглянемо по порядку.
Найперше - коли вікно отримує або втрачає фокус. Під час гри це може знадобитись для встановлення паузи. Також встановити паузу може бути потрібно при згортанні вікна в трей. Для цього додамо нову властивість:
property Active : Boolean read
і знову натиснемо Ctrl+Shift+C - ця властивість буде доступною лише для читання, а її стан буде змінюватись при відловлюванні певних подій вікна, які зараз ми додамо. Тож переходь в метод DoEvents() і дописуй наступний код відразу після зміни розмірів вікна:
      //втрата фокусу, згортання вікна
     SDL_WINDOWEVENT_FOCUS_LOST,
     SDL_WINDOWEVENT_MINIMIZED: FActive:=false;
     //отримання фокусу
     SDL_WINDOWEVENT_TAKE_FOCUS: FActive:=true;
Ми поки що лише перевіряємо втрату фокусу, згортання вікна і отримання вікном фокусу (згорнуте вікно не може отримати фокус).
Коли нам будуть потрібні інші події вікна щоб на них зреагувати, ти вже знаєш як і куди їх додати.

Апаратне прискорення та інші додаткові “плюшки”

Ну як же в відеоіграх і без апаратного прискорення! В SDL за це відповідає рендерер, при створенні якого потрібно вказати відповідний прапорець. Взагалі, можна вказати декілька прапорців через логічний оператор OR. Можливі варіанти описані в документації по SDL, але тут ми не будемо розглядати їх усі в деталях. Отже, я пропоную просто винести окрему властивість, яка буде відповідати за апаратне прискорення:
    //апаратне прискорення
    property HardwareAcceleration : Boolean default true;
Знову натискуй вже звичне Ctrl+Shift+C, і потрібний код згенерується автоматично. Але потрібно пам’ятати, що встановлення апаратного прискорення можливе лише при створенні рендерера, тому бажано додати перевірку і виводити відповідне повідомлення користувачеві:
  if (fSDLRenderer<>nil) then begin
      SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION,
        ’Warning!’,
        ’Настройки будуть задіяні після перезапуску!’,
        nil);
  end;
В документації по SDL описано усі можливі прапорці, але я всіх тут розглядати не буду, їх можна при потребі легко додати в клас. Оскільки прапорців може бути декілька в різних комбінаціях, я вирішив зробити два додаткових поля для внутрішнього використання: в одному будуть прапорці для створення вікна, а в іншому - для створення рендерера:
    //прапорці для вікна
    fWindowFlags : Integer;
    //прапорці для рендерера
    fRendererFlags : Integer;
Початкова ініціалізація цих прапорців буде відбуватись перед створенням вікна і рендерера на основі встановлених відповідних властивостей. Але тепер параметрів стало більше, і методи можуть використовуватись різні, тому давай на цьому питанні зупинимось детальніше.

Перегрузка методу ініціалізації

Ми вже додали досить багато властивостей в наш клас, але ніяк не змінювали метод ініціалізації. Як на мене, можна ініціалізувати вікно кількома способами:
  • передача потрібних параметрів під час виклику методу ініціалізації;
  • встановлення потрібних властивостей після створення об’єкта шляхдм присвоєння значень відповідним полям, після чого викликати метод ініціалізації;
  • при виклику методу ініціалізації вказувати шлях і назву файлу, де збережені параметри ініціалізації.
Завдячуючи такій можливості, як перегрузка методів, ми можемо реалізувати усі ці варіанти з одним і тим же іменем, але різними параметрами. І першим я пропоную описати метод без параметрів, який буде ініціалізувати усе з встановлених значень:
procedure TGOEngine.Init;
begin
  //поки ще нічого не ініціалізовано, ознаку виконання ставимо в false
  fRunning:=false;
  //спочатку пробуємо ініціалізувати бібліотеку
  if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
    //якщо все пройшло добре - створюємо вікно
    //прописуємо потрібні прапорці для вікна
    fWindowFlags:=SDL_WINDOW_RESIZABLE OR SDL_WINDOW_SHOWN;
    if IsFullScreen then fWindowFlags:=fWindowFlags OR SDL_WINDOW_FULLSCREEN;
    //пробуємо створити вікно
    fSDLWindow:=SDL_CreateWindow(PChar(Utf8String(Caption)),
                   SDL_WINDOWPOS_UNDEFINED,
                   SDL_WINDOWPOS_UNDEFINED,
                   Height,Width,
                   fWindowFlags);
    //якщо вікно створене, потрібно створити рендерер
    //прописуємо для нього прапорці
    fRendererFlags:=0;
    if HardwareAcceleration then fRendererFlags:=fRendererFlags OR SDL_RENDERER_ACCELERATED;
    if fSDLWindow<>nil then begin
       fSDLRenderer:=SDL_CreateRenderer(fSDLWindow,-1,fRendererFlags);
       if fSDLRenderer<>nil then begin
          New(fSDLEvent);//потрібно виділити пам’ять для вказівника
          fRunning:=true; //ознака виконання робочого циклу
          Exit;
       end;
    end;
  end;
end;
Як бачиш, відмінностей від попереднього варіанту ініціалізації небагато - немає встановлення значень полів класу та додалась ініціалізація прапорців для вікна та рендерера і підстановка їх у відповідні місця.
Тепер настала черга змінити варіант методу з передачею параметрів. Оскільки параметрів трохи побільшало, потрібно розширити їхній список. Код, який відповідає за ініціалізацію, ми вже прописали в методі вище і дублювати його немає потреби. Просто встановимо значення відповідних полів з параметрів і викличемо попередній перегружений метод:
procedure TGOEngine.Init(aCaption : String; _height, _width : Integer; Accelerated : Boolean = true; Fullscreen : Boolean = true);
begin
   Caption:=aCaption;
   Height:=_height;
   Width:=_width;
   HardwareAcceleration := Accelerated;
   IsFullScreen:=Fullscreen;
   Init;
end;
І нарешті останній перегружений метод, який буде читати настройки з файла. Я зупинився на форматі файла INI - він дуже простий і добре підтримується в усіх системах. Для комфортної роботи з цим форматом необхідно пвдключити модуль INIFiles. В якості параметрів методу будуть передаватись ім’я файлу та назва секції з настройками:
procedure TGOEngine.Init(aFile : TFilename; aSection : String);
 var INI : TINIFile;
begin
  INI := TIniFile.Create(aFile);
  Height:=INI.ReadInteger(aSection,’Height’,Height);
  Width:=INI.ReadInteger(aSection,’Widht’,Width);
  Caption:=INI.ReadString(aSection,’Caption’,Caption);
  IsFullScreen:=INI.ReadBool(aSection,’FullScreen’,IsFullScreen);
  HardwareAcceleration:=INI.ReadBool(aSection,’HardwareAcceleration’,HardwareAcceleration);
  INI.Free;
  Init;
end;
Як бачиш, все досить просто. Навіть в тому випадку, якщо файл настройок відсутній, значення полів залишаться без змін і програма зможе коректно працювати далі.
Оскільки можна настройки брати з файлу, логічно було б їх і зберігати в файл - в багатьох відеоіграх користувач може змінювати настройки і зберігати їх. Пропоную зробити аналогічний метод, який буде записувати поточні настройки вікна в файл:
function TGOEngine.SaveSettings(aFile: TFilename; aSection: String): Boolean;
var INI : TIniFile;
begin
  result := false;
  try
   INI := TIniFile.Create(aFile);
   INI.WriteInteger(aSection,’Height’,Height);
   INI.WriteInteger(aSection,’Widht’,Width);
   INI.WriteString(aSection,’Caption’,Caption);
   INI.WriteBool(aSection,’FullScreen’,IsFullScreen);
   INI.WriteBool(aSection,’HardwareAcceleration’,HardwareAcceleration);
   Result := true;
  finally
   INI.Free;
  end;
end;
Думаю, особливих пояснень ця функція не потребує - код говорить сам за себе. У випадку, якщо настройки не вдається записати, функція коректно обробляє ексцепшен і повертає false, не перериваючи роботу програми.
Ми багато доробили до нашого класу, але код тестової програми не міняли зовсім (за виключеням коли потрібно було потестити нові фішки які ми дописували). Такий підхід до розробки дуже практичний, оскільки при нових версіях фреймворку не доведеться міняти багато коду в іграх, які будуть створені на його основі.
В основному як працювати з вікном ми вияснили і зробили досить зручний клас, який при потребі можна буде досить просто вдосконалити, додавши необхідні додаткові властивості і не ламаючи при цьому існуючий код. В наступній частині почнемо прцювати з графікою і виводити зображення на екран.
Сирці до цієї статті можна скачати тут.

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

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