субота, 18 листопада 2017 р.

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


Виведення зображення на екран

Давно вже була остання публікація циклу, тому що я був досить сильно завантажений різними справами і питаннями, які вимагали не відкладати їхнього вирішення, тому виділити час на розробку фреймворка не вдавалось. Зараз я трохи прихворів, і вирішив доки підліковуюсь продовжувати тему.
Отже, ми вже навчились працювати з вікном, тепер будемо виводити зображення на екран. Я не художник, сам малювати вмію не дуже добре, тому будемо розглядати лише програмну частину роботи з зображеннями. Я вирішив скористатись вже готовими малюнками, які можна без особливих проблем знайти і скачати з інтернету. В кінці цієї статті я приведу список сайтів, де можна безкоштовно скачати графіку (і не тільки) для відеоігор.
Для подальшої роботи тобі буде потрібно зображення. Я взяв наступне (якщо знаєш, з якої це гри, напиши в коментарії):


Це зображення збережене в форматі BMP. Бібліотека SDL вміє працювати з цим форматом. Фактично, тут є три зображення персонажа гри для створення анімації стрибка (я спеціально так підібрав), але як створити таку анімацію я розповім пізніше. Спочатку тобі потрібно зберегти це зображення з іменем sample.bmp, наприклад, в папку assets, яку можеш створити в папці bin проекту. В мене повний шлях (відносно проекту) виглядає наступним чином:
sdl2engine/bin/assets/sample.bmp
Для того, щоб зображення можна було вивести на екран, спочатку потрібно створити текстуру (тип PSDL_Texture), куди воно буде завантажене. Але безпосередньо завантажити текстуру з файла SDL не вміє, тому потрібно спочатку створити поверхню (тип PSDL_Surface), куди завантажиться зображення, а тоді створити з поверхні текстуру. Чому ми бужемо використовувати текстуру, а не поверхню? Тому що є одна важлива відмінність: текстура зберігається в пам’яті відеокарти, що забезпечує апаратне прискорення при обробці, а поверхня знаходиться в оперативній пам’яті і не може використовувати апаратного прискорення.
Для простого завантаження малюнка і виведення його на екран я створю новий проект в тій самій папці, що й попередні, а коли ми навчимося повністю працювати з зображеннями, ми інтегруємо це в наш фреймворк.
Запускай Lazarus, відкривай наш самий перший проект demo.lpr і додай в нього наступні зміни (відразу після опису змінних):
     SDLTexture  : PSDL_Texture;
​
procedure LoadImage;
//щоб код краще сприймався,
//винесемо завантаження малюнка окремою процедурою
 var tmpsurface : PSDL_Surface;
begin
  tmpsurface:=SDL_LoadBMP(’assets/sample.bmp’);
  SDLTexture:=SDL_CreateTextureFromSurface(SDLRenderer,tmpsurface);
  SDL_FreeSurface(tmpsurface);
end;
Виклик цієї процедури постав перед головним циклом.
Тепер ми маємо текстуру з малюнком, і потрібно її вивести на екран. Для цього просто додай ще один рядок в головному циклі так, як це зроблено в наступному сніпеті:
          //очищаємо вікно
          SDL_RenderClear(SDLRenderer);
          //виводимо текстуру
          SDL_RenderCopy(SDLRenderer,SDLTexture,nil,nil);
          //показуємо вікно на екрані
          SDL_RenderPresent(SDLRenderer);
Після запуску в тебе з’явиться вікно, на яке повністю буде розтягнутий наш малюнок.

“Добре,” - скажеш ти, - “а як мені вивести малюнок з врахуванням його оригінального розміру і в тій позиції, де я схочу?” Цим ми й займемось зараз.
SDL має спеціальний тип - TSDL_Rect, який містить чотири поля: x,y,w,h (координати лівого верхнього кутка, ширину і висоту). Тому додамо ще дві змінних такого типу, щоб вивести малюнок в потрібному місці і з потрібними розмірами:
     SRCRect, DSTRect : TSDL_Rect;
Тепер опитаємо текстуру, щоб дізнатись її розміри. Зробимо це відразу після створення:
  SDL_QueryTexture(SDLTexture,nil,nil,@SRCRect.w,@SRCRect.h);
Тепер при виведенні текстури на екран змінимо наступні параметри:
          //виводимо текстуру
          SDL_RenderCopy(SDLRenderer,SDLTexture,nil,@SRCRect);
Після запуску на екрані з’явиться вікно з текстурою у верхньому лівому кутку:

Якщо змінити координати в SRCRect, текстура змінить свою позицію відносно вікна. Можеш спробувати в головному циклі випадковим чином ставити координати в межах максимального розміру вікна, тільки ще додай затрримку процедурою SDL_Delay(100), щоб не так швидко перемальовувалось вікно і побачити переміщення текстури.
А тепер починається найцікавіше. Зміни код після опитування текстури наступним чином:
  DSTRect.x:=20;
  DSTRect.y:=20;
  DSTRect.h:=SRCRect.h;
  DSTRect.w:=SRCRect.w div 3;
  SRCRect.w:=SRCRect.w div 3;
а виведення текстури зміни так:
          //виводимо текстуру
          SDL_RenderCopy(SDLRenderer,SDLTexture,@SRCRect,@DSTRect);
І після запуску програми в тебе повинно вивести лише частину зображення:
Як бачиш, ми можемо самі вибирати, що і де повинно малюватись на екрані.

Підтримка прозорості та інші графічні формати

Здається, все у нас добре, і картинка на екран виводиться, і керувати що і куди малювати ми можемо, але є один недостаток - усі картинки лише в форматі BMP, який не підтримує прозорості і займає досить багато місця порівняно з іншими графічними форматами. Звісно, є можливість визначити колір прозорості для зображення, але для цього потрібно знати точно який колір має бути прозорим у форматі R,G,B (пізніше я покажу, як дізнатись колір конкретного пікселя). Набагато зручніше використати, наприклад, формат PNG, який підтримує прозорість, але як ним користуватись? Відповідь - офіційне розширення бібліотеки SDL2 SDL2_Image, яке підтримує роботу з багатьма графічними форматами. Оскільки при підготовці ми вже все встановили, достатньо буде лише підключити відповідний модуль sdl2_image і змінити функцію завантаження зображення з файлу на IMG_Load(’assets/sample.png’) (відповідно усі функції розширення мають префікс IMG_). Ось те ж саме зображення, яке ми використовували, але у форматі PNG і з прозорим фоном:
Спробуй змінити код, так як я описав, і запустити - тепер персонаж не має зеленого фону.
На сьогодні поки що буде усе, а в наступній статті я розповім про те, як можна визначити колір фона і зробити його прозорим, а також зробимо анімацію персонажа та спробуємо інтегрувати все це в наш фреймворк.
Код і ресурси для цієї статті можна скачати тут.
Готові ассети для відеоігор безкоштовно можна знайти на наступних ресурсах:

неділя, 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, не перериваючи роботу програми.
Ми багато доробили до нашого класу, але код тестової програми не міняли зовсім (за виключеням коли потрібно було потестити нові фішки які ми дописували). Такий підхід до розробки дуже практичний, оскільки при нових версіях фреймворку не доведеться міняти багато коду в іграх, які будуть створені на його основі.
В основному як працювати з вікном ми вияснили і зробили досить зручний клас, який при потребі можна буде досить просто вдосконалити, додавши необхідні додаткові властивості і не ламаючи при цьому існуючий код. В наступній частині почнемо прцювати з графікою і виводити зображення на екран.
Сирці до цієї статті можна скачати тут.

субота, 14 жовтня 2017 р.

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


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

Частина 3. Вдосконалюємо роботу з вікном.

Оптимізація класу

В попередній частині ми створили каркасний клас для фреймворка, який створює вікно засобами SDL2. Проте для комфортної роботи цей клас потрібно вдосконалити, використовуючи можливості бібліотеки SDL2 для роботи з вікнами.
В останньому варіанті коду я допустив помилку: код, який відповідає за ініціалізацію, виніс в конструктор класу, а потрібно було його описати в методі Initialize. В новому варіанті класу ми цю помилку виправимо, а також розширимо можливості створення та керування вікном.
Щоб не заплутуватись в лістингах, надалі я не буду приводити код повністю після кожної зміни, а лише сніпети коду, який додається або змінюється
І ще одна особливість, яку я виявив при виведенні заголовку вікна: для того, щоб коректно відображалась кирилиця, потрібно використовувати тип UTF8String, який приводиться до типу PChar. Але для того, щоб такий метод відпрацьовував коректно, заголовок вікна винесемо окремою властивісю класу:

//заголовок вікна
    property Caption : String;
    

Натискуй Ctrl+Shift+C і Lazarus сам створить весь додатковий код, тобі лише потрібно зробити виправлення в методі Init(): рядок

fSDLWindow:=SDL_CreateWindow(PChar(аCaption),SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,_height,_width,SDL_WINDOW_SHOWN);

потрібно замінити на наступні рядки:

fSDLWindow:=SDL_CreateWindow(PChar(Utf8String(Caption)),
                  SDL_WINDOWPOS_UNDEFINED,
                  SDL_WINDOWPOS_UNDEFINED,
                  Height,Width,
                  SDL_WINDOW_SHOWN);
   

Тепер значення заголовку буде зберігатися у відповідній властивості класу і коректно відображатись на екрані.
Аналогічно винесемо розміри вікна у окремі властивості класу Width і Height:

//ширина і висота вікна
    property Width : Integer;
    property Height : Integer;
 
Теж натискуй Ctrl+Shift+C щоб Lazarus автоматично завершив код і поки що ці методи лишай без змін, до них ми повернемось трохи пізніше.
Ще, на мою думку, зручно було б винести обробку робочого циклу теж окремим методом (аналогічно як в класі Application в LCL). Тож я вирішив додати метод Run:

procedure TGOEngine.Run;
begin
 while IsRunning do begin
    DoEvents;
    Render;
   end;
end;


Відповідно в тестовій програмі весь цикл while потрібно замінити лише одним рядком TestEngine.Run. Можеш спробувати запустити програму і переконатись, що все працює так як і раніше.

Повноекранний режим SDL

Для створення вікна бібліотека SDL використовує певний набір параметрів. Це ширина і висота вікна, початкові координати верхнього лівого кута вікна, а також набір різних додаткових ознак. Якщо звернутись до документації по SDL, то можна використовувати наступні ознаки:
Ознака Призначення
SDL_WINDOW_FULLSCREEN Вікно повинне бути на весь екран
SDL_WINDOW_OPENGL Вікно може використовуватись як контекст OpenGL
SDL_WINDOW_SHOWN Вікно видиме на екрані
SDL_WINDOW_HIDDEN Вікно сховане
SDL_WINDOW_BORDERLESS Вікно не має обрамлення і заголовку
SDL_WINDOW_RESIZABLE Вікно може змінювати розміри
SDL_WINDOW_MINIMIZED Вікно згорнуте
SDL_WINDOW_MAXIMIZED Вікно розгорнуте
SDL_WINDOW_INPUT_GRABBED Вікно захопило фокус вводу
SDL_WINDOW_INPUT_FOCUS Вікно має фокус вводу
SDL_WINDOW_MOUSE_FOCUS Вікно має фокус вказівника мишки
SDL_WINDOW_FOREIGN Вікно створене не бібліотекою SDL
Оскільки більшість відеоігор зручніше грати в повноекранному режимі, нас цікавить сама перша ознака SDL_WINDOW_FULLSCREEN. Спробуй змінити ознаку в функції створення вікна в методі Init() нашого класу і подивись результат. Щоб закрити вікно в повноекранному режимі, натисни Alt+F4, оскільки заголовок зі стандартними кнопками відсутній.
Проте періодично виникають ситуації, коли потрібно швидко переключитись між повноекранним/віконним режимами без виходу з програми. SDL дозволяє таке зробити за допомогою функції SDL_SetWindowFullscreen(), яка в якості параметрів приймає вікно, з яким відбувається дія, та ознаку 0 для віконного режиму або 1 для повноекранного.
Отже, для встановлення ознаки повноекранного режиму я ввів ще одну властивість:

property IsFullScreen : Boolean default true;

після чого натиснув вже добре знайому комбінацію Ctrl+Shift+C для автоматичного завершення коду. Далі, переходимо в згенерований Лазарусом внутрішній метод SetIsFullScreen() і в кінці додаємо наступні рядки коду:

if FIsFullScreen then SDL_SetWindowFullscreen(fSDLWindow,SDL_WINDOW_FULLSCREEN)
    else SDL_SetWindowFullscreen(fSDLWindow,0);
  SDL_UpdateWindowSurface(fSDLWindow);


Цей код реалізує перехід в повноекранний або віконний режим в залежності від поточного значення поля FIsFullScreen. Останній рядок необхідний для того, щоб вікно обновилось після зміни режиму.
Добре, код для реалізації зміни режиму вже є, тепер потрібно назначити коли цей код повинен виконуватись. В більшості програм для переключення між віконним та повноекранним режимами прийнято використовувати комбінацію клавіш Alt+Enter. Я вирішив теж реалізувати переключення між режимами цією комбінацією клавіш, і мене переклинило більше ніж на годину часу. Справа в тому, що потрібно відловити натискання комбінації клавіш, а за один раз вдається відловити лише натискання однієї клавіші. Звісно, є ще можливість перевірити які з клавіш-модифікаторів були в цей час натиснуті, але конкретного прикладу як це правильно зробити я знайти так і не зміг (навіть для C++!), тож довелось практичним шляхом виясняти самому.
Спочатку потрібно відловити подію натискання клавіш. Бібліотека SDL для цього має два типа подій, які виникають перша при натисканні клавіші, а друга при відпусканні. В нашому випадку краще підходить подія відпускання клавіші. Переходь в метод DoEvents() та після рядка

if fSDLEvent^.type_ = SDL_QUITEV then fRunning:=false;
      

додай наступний код:

if (fSDLEvent^.type_ = SDL_KEYUP) then begin
         //відловлюємо Alt+Enter для зміни режиму
         if (fSDLEvent^.key.keysym.sym = SDLK_RETURN) and ((fSDLEvent^.key.keysym._mod and KMOD_ALT) <> 0) then
            IsFullScreen:=not IsFullScreen;
      end

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

субота, 23 вересня 2017 р.

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


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

Частина 2. Перше вікно SDL2.

В попередній частині ми підготували усе необхідне для створення ігрового фреймворку на основі мультимедійної бібліотеки SDL2, а тепер почнемо роботу безпосередньо над самим фреймворком.
Оскільки сам термін “відеогра” передбачає виведення на екран, відповідно, основою для відеогри буде екран або якась його частина, у нашому випадку вікно. Спочатку я покажу тобі, яким способом можна отримати вікно за допомогою бібліотеки SDL2. Оскільки ми не будемо використовувати LCL, нам не потрібно нічого лишнього, тому в Lazarus створи новий проект типу “Проста програма”, і набери наступний код:
{******************************}program demo;{*********************************}
{*                                                                            *}
{*     Демонстрація створення пустого вікна за допомогою бібліотеки SDL2      *}
{*                                                                            *}
{******************************************************************************}
 {$mode objfpc}
uses sdl2;

 var SDLWindow   : PSDL_Window;
     SDLRenderer : PSDL_Renderer;
     SDLEvent    : PSDL_Event;
     isRun       : Boolean;

begin
   //спочатку пробуємо ініціалізувати бібліотеку
   if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
      //якщо все пройшло добре - створюємо вікно
     SDLWindow:=SDL_CreateWindow('Game over!',SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,640,480,SDL_WINDOW_SHOWN);
     //якщо вікно створене, потрібно створити рендерер
     if SDLWindow<>nil then begin
        SDLRenderer:=SDL_CreateRenderer(SDLWindow,-1,0);
        if SDLRenderer=nil then begin
           SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,'Error!',SDL_GetError,SDLWindow);
           Exit;
        end;
     end;
     New(SDLEvent);
     isRun:=true; //ознака виконання робочого циклу
     while isRun do begin
          //встановлюємо колір вікна в голубий
          SDL_SetRenderDrawColor(SDLRenderer,0,128,255,255);
          //очищаємо вікно
          SDL_RenderClear(SDLRenderer);
          //показуємо вікно на екрані
          SDL_RenderPresent(SDLRenderer);
          //відловлюємо подію закриття вікна
          if SDL_PollEvent(SDLEvent)=1 then begin
              if SDLEvent^.type_ = SDL_QUITEV then isRun:=false;
          end;
     end;
     //прибираємо за собою
     Dispose(SDLEvent);
     SDL_DestroyRenderer(SDLRenderer);
     SDL_DestroyWindow(SDLWindow);
     //зупиняємо бібліотеку
     SDL_Quit;
   end;
end.


Я не буду в деталях зупинятись на описі кожної функції бібліотеки SDL2, ця інформація доступна як в сирцях трансляції заголовочних файлів, так і на багатьох сайтах. Розглянемо лише що робить цей код.
Для створення вікна нам необхідні наступні змінні: для вікна, рендерера (це така штука, яка відповідає за малювання зображення), відловлювання подій SDL2 та ознака виконання робочого циклу.
Після ініціалізації усіх підсистем SDL2 створюємо спочатку вікно, тоді рендерер. У випадку, якщо не вдається створити рендерер, програма виводить відповідне повідомдення і перериває виконання.
Коли все пройшло успішно, ми маємо вікно і рендерер, який на ньому буде промальовувати все, що нам потрібно.
Для того, щоб вікно можна було закрити, ми мусимо відловити відповідну подію SDL2. Оскільки спеціальної функції для ініціалізації змінної для роботи з повідомленнями немає, потрібно просто виділити для цього пам’ять стандартною функцією Pascal New(). Встановленням ознаки виконання робочого циклу в true завершується розділ ініціалізації і починається безпосередньо робочий цикл.
Сам цикл буде тривати до того моменту, поки змінна isRun буде мати значення true. Все, що в нас вібувається з кожною ітерацією циклу - встановлення кольору для вікна, заливка вікна цим кольором і показ його на екрані, після чого йде відловлювання події SDL2 і перевірка чи це саме та подія, яка нам потрібна. І коли нарешті настає момент закриття вікна, ми відловлюємо відповідну подію SQL_QUITEV. Змінна isRun приймає значення false і виконання циклу перериваєьтся. Програма переходить до наступної стадії - очистка використаної пам’яті. Усі змінні очищаємо в порядку, оберненому до порядку їх створення: спочатку звільняємо пам’ять, виділену під вказівник змінної для відловлювання подій, тоді рендерер, вікно і зупиняємо усі підсистеми SDL2.
Варто пам’ятати: усі змінні, які ініціалізуються функціями типу SDL_CreateXXX(), звільняються відповідними функціями SDL_DestroyXXX() в порядку, оберненому до порядку їх ініціалізації. Ті змінні, пам’ять для яких виділялась явно функцією New(), повинні очишатись теж в оберненому порядку функцією Dispose().
Здається, тут все досить просто, але й близько не видно, яке це має відношення до ігрового фреймворку. Що ж, давай тепер розглянемо, за яким принципом побудовані усі відеоігри.

Ігровий алгоритм, або з чого складається відеогра

Незалежно від жанру, ігрового дизайну, сукупності використовуваних технологій, ігрова механіка будь-якої відеогри складається із взаємодії різних підсистем, таких як графіка, ігрова механіка і ввід корисувача. При цьому графічна підсистема може нічого не знати про логіку гри, яким чином відбувається введення користувачем, тощо. Ми можемо думати про структуру гри наступним чином:
Рис.1. Типовий алгоритм будь-якої гри.
Після одноразової ініціалізації усіх необхідних даних запускається ігровий цикл, який перевіряє ввід користувача, прораховує ігрову логіку і встановлює необхідні значення усіх об’єктів і змінних до того, як вивести графіку на екран.
Коли користувач вибирає завершити гру, ігровий цикл переривається, і гра переходить до стадії очищення даних та виходу.
Спробуємо розділити попередній код на окремі логічні блоки, які будуть відповідати за усі перечислені кроки:
{******************************}program demo_v2;{*********************************}
{*                                                                            *}
{*     Демонстрація створення пустого вікна за допомогою бібліотеки SDL2      *}
{*                                                                            *}
{******************************************************************************}
 {$mode objfpc}
uses sdl2;

 var SDLWindow   : PSDL_Window;
     SDLRenderer : PSDL_Renderer;
     SDLEvent    : PSDL_Event;
     isRun       : Boolean;

 function Initialize(aCaption : PChar; _height, _width : Integer) : Boolean;
 //ініціалізація SDL2 і створення вікна та рендерера
 begin
   result:=false;
   //спочатку пробуємо ініціалізувати бібліотеку
   if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
      //якщо все пройшло добре - створюємо вікно
     SDLWindow:=SDL_CreateWindow(aCaption,SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,_height,_width,SDL_WINDOW_SHOWN);
     //якщо вікно створене, потрібно створити рендерер
     if SDLWindow<>nil then begin
        SDLRenderer:=SDL_CreateRenderer(SDLWindow,-1,SDL_RENDERER_ACCELERATED);
        if SDLRenderer<>nil then begin
           New(SDLEvent);//потрібно виділити пам'ять для вказівника
           isRun:=true; //ознака виконання робочого циклу
           Result:=true;
           Exit;
        end;
     end;
   end;
 end;

procedure DoEvents;
begin
 //відловлюємо подію закриття вікна
  if SDL_PollEvent(SDLEvent)=1 then begin
     if SDLEvent^.type_ = SDL_QUITEV then isRun:=false;
  end;
end;

procedure Draw;
begin
  //встановлюємо колір вікна в бірюзовий
  SDL_SetRenderDrawColor(SDLRenderer,0,128,255,255);
  //очищаємо вікно
  SDL_RenderClear(SDLRenderer);
  //показуємо вікно на екрані
  SDL_RenderPresent(SDLRenderer);
end;

procedure Cleanup;
begin
  //прибираємо за собою
  Dispose(SDLEvent);
  SDL_DestroyRenderer(SDLRenderer);
  SDL_DestroyWindow(SDLWindow);
  //зупиняємо бібліотеку
  SDL_Quit;
end;

begin
   //спочатку пробуємо ініціалізувати бібліотеку
   isRun:= Initialize('Game over! v.2.0',640,480);
   if not isRun then begin
           SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,'Error!',SDL_GetError,SDLWindow);
           Exit;
    end;
   while isRun do begin
     DoEvents;
     Draw;
   end;
   Cleanup;
end.


Коли ти запустиш цей код на виконання, то побачиш, що кінцевий результат не змінився, створюється те ж саме вікно, але тепер є окремі чітко виділені підпрограми для ініціалізації, обробки подій, рендерингу і очистки пам’яті. Це вже ближче до того, що нам потрібно. Не вистачає лише підпрограми для обчислення усієї ігрової логіки. Крім цього, такий підхід при розробці гри є не дуже гнучким, оскільки на кожну нову гру доведеться по новомо все це описувати. Це не наш метод, я хочу отримати в результаті фреймворк, який мені буде спрощувати написання коду і дозволить зосередитись безпосередньо на написанні гри, а не рутини по створенню вікон, обробці подій, виведенні на екран тощо. Для цього давай опишемо окремий клас, який стане основою для майбутнього фреймворку.
Оскільки ми вже впритул підійшли до написання фреймворку, потрібно придумати йому назву. Я пропоную “Game Over 2D Engine”, ти можеш придумати іншу назву.
Повернемось до нашого коду. Оскільки це буде базовий клас, я назвав його TGOEngine (скорочено від Game Over), і ось що в мене в результаті вийшло:
unit ugoengine;

{$mode objfpc}{$H+}

interface

uses sdl2;

type

 { TGOEngine }

 TGOEngine =class
  private
    fWindow   : PSDL_Window;
    fRenderer : PSDL_Renderer;
    fError    : Boolean; //якщо відбулась помилка
    fIsRun    : Boolean; //чи запущений ігровий цикл
  public
    constructor Create;
    destructor Destroy;override;

    procedure Initialize(aCaption : PChar; _height, _width : Integer);
    procedure DoEvents;
    procedure Update;
    procedure Draw;
  published
    property Error : Boolean read fError;
    property IsRunning : Boolean read fIsRun;
  end;

implementation

{ TGOEngine }

constructor TGOEngine.Create;
begin
  inherited Create;
  //ініціалізація SDL2 і створення вікна та рендерера
   begin
     fError:=true;
     //спочатку пробуємо ініціалізувати бібліотеку
     if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
        //якщо все пройшло добре - створюємо вікно
       fWindow:=SDL_CreateWindow('Game over!',SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,640,480,SDL_WINDOW_SHOWN);
       //якщо вікно створене, потрібно створити рендерер
       if fWindow<>nil then begin
          fRenderer:=SDL_CreateRenderer(fWindow,-1,0);
          if fRenderer<>nil then begin
             fError:=false;
             Exit;
          end;
       end;
     end;
   SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,'Error',SDL_GetError,fWindow);
   end;
end;

destructor TGOEngine.Destroy;
begin
   //прибираємо за собою
   SDL_DestroyRenderer(fRenderer);
   SDL_DestroyWindow(fWindow);
   //зупиняємо бібліотеку
   SDL_Quit;
   inherited Destroy;
end;

procedure TGOEngine.Initialize(aCaption : PChar; _height, _width : Integer);
begin
  //тут буде проходити ініціалізація даних
   fIsRun:=true;
end;

procedure TGOEngine.DoEvents;
var _Event : PSDL_Event;
begin
   New(_Event);
  //тут буде проходити обробка подій
   if SDL_PollEvent(_Event)=1 then begin
      case _Event^.type_ of
SDL_QUITEV: fIsRun:=false;
      end;//case
   end;
   Dispose(_Event);
end;

procedure TGOEngine.Update;
begin
  //тут буде оновлюватись поточний стан виконання
end;

procedure TGOEngine.Draw;
begin
  if fError then Exit;
  //встановлюємо колір вікна в бірюзовий
  SDL_SetRenderDrawColor(fRenderer,0,128,255,255);
  //очищаємо вікно
  SDL_RenderClear(fRenderer);
  //показуємо вікно на екрані
  SDL_RenderPresent(fRenderer);
end;


end.


А ось код основної програми:
{******************************}program demo_v3;{*********************************}
{*                                                                            *}
{*     Демонстрація створення пустого вікна за допомогою бібліотеки SDL2      *}
{*                                                                            *}
{******************************************************************************}
 {$mode objfpc}{$H+}

uses ugoengine;

var TestEngine : TGOEngine;

begin
   //створюємо екземпляр класу
   TestEngine:=TGOEngine.Create;
   //ініціалізуємо початкові дані
   TestEngine.Initialize('Game Over v.3.0',640,480);
   while TestEngine.IsRunning do begin;
    TestEngine.DoEvents;
    TestEngine.Update;
    TestEngine.Draw;
   end;
   TestEngine.Free;
end.


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

неділя, 17 вересня 2017 р.

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

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

Частина 1. Підготовка системи.

В Інтернеті на різних сайтах по програмуванню є дуже багато інформації щодо мультимедійної бібліотеки SDL2, але вся вона має два суттєвих недоліки з точки зору розробки відеоігор мовою програмування Object Pascal:
  1. Більшість такої інформації приводиться з використанням мови програмування C++, а дійсно якісна документація доступна лише англійською мовою. Але далеко не всі програмісти на потрібному рівні володіють англійською, як і не всі пишуть на C++/.
  2. Серед усієї доступної інформації по використанню SDL2 переважаюча більшість лише описує можливості самої бібліотеки з наведенням абстрактних прикладів. Ніби й нічого, але як з усім цим зробити конкретну гру?
Своїм циклом статей про розробку фреймворку для 2-Д відеоігор з кількома прикладами готових ігор я спробую заповнити цей недолік інформації.
Що я спробую охопити:
  • підготовка і настройка системи для використання SDL2
  • створення фреймворка і розробка на основі нього декількох відеоігор різних жанрів
Що я не буду розглядати:
  • основи програмування на Object Pascal - такий об’єм інформації виходить далеко за рамки кількох статей.
  • усі тонкощі використання функцій SDL2 і офіційних розширень цієї бібліотеки - документації про це вже і так є достатньо.
Необхідні інструменти:

Підготовка системи

Якщо в тебе ще немає встановленого компілятора FPC та середовища Lazarus, саме час їх встановити. Зрештою, можна використовувати лише сам компілятор, але в Lazarus набагато зручніше працювати з проектами. Проте це моя суб’єктивна думка, і ти можеш користуватись тими інструментами, якими зручно саме тобі.
Спочатку тобі потрібно встановити саму бібліотеку SDL2. Розгляну процес встановлення по черзі для кожної операційної системи.

Встановлення SDL2 в ОС Linux

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

Рис.1. Запуск менеджера пакунків Synaptic в Linux Mint.
Отже, тобі будуть потрібні пакунки з самою бібліотекою та її офіційними розширеннями, а також відповідні пакунки для розробки - без них неможливо буде лінкеру зібрати програму, яка використовує SDL2. Тож запускай Synaptic (Рис.1), вводь пароль і в вікні пошуку вводь “libsdl2” - як результат, ти побачеш відібрані усі необхідні пакунки. Відмічай ті, що на Рис.2 відмічені зеленим і натискуй кнопку “Застосувати”. Через кілька хвилин усі необхідні компоненти вже будуть встановлені, і твій дистрибутив буде готовий до розробки програм з використанням цієї надзвичайної мультимедійної бібліотеки.
Рис.2. Пакунки необхідні для розробки з використанням усіх можливостей SDL2.

Встановлення SDL2 в ОС Windows

Оскільки я вже досить давно не використовую в роботі ОС Windows, приведу лише загальні рекомендації.
Саму бібліотеку можна скачати на офіційному сайті, потрібно лише вибрати правильну розрядність твоєї системи (32 або 64 біти). Офіційні розширення можна скачати за посиланням, нам потрібні будуть лише наступні:
  • SDL_image
  • SDL_mixer
  • SDL_net
  • SDL_ttf
Усі скачані архіви тобі потрібно розпакувати, і результат скопіювати в папку c:\Windows\System32 (незалежно від розрядності твоєї системи), якщо ти маєш відповідні права доступу. Якщо ж немає можливості скопіювати файли в систему, ти можеш їх скопіювати просто до виконуваного файла твоєї програми - цього буде достатньо.

Настройка Lazarus на використання SDL2

Для того, щоб ти зміг використовувати усі можливості SDL2 у своїх програмах, тобі потрібно скачати трансляцію заголовочних файлів для компілятора FPC. Знайти їх можна у розділі “Bindings” офіційного сайту SDL (взагалі, там доступні трансляції для різних мов програмування, але тобі потрібно лише для Pascal). Я користуюсь Pascal SDL 2 - на відміну від Bare Game, тут є також трансляція усіх офіційних доповнень та збережено коментарі з детальними описами структур даних, констант та функцій SDL2 та офіційних розширень.
Після того, як скачаєш архів, тобі потрібно його розпакувати і пояснити компілятору, де шукати ці файли. Особисто мені для будь-якого нового проекту зручно, коли “мухи від котлет окремо”, тому в мене наступна структура каталогів проекту - в папці проекту є три вкладені папки: bin, lib, src. В папку bin в мене компілюється виконуваний файл проекту, в папці lib - проміжні результати компіляції (з врахуванням операційної системи і архітеркури процесора), а в папці src - сирці самого проекту. Таким чином, щоб почистити результари збирання проекту і перенести його на інший комп’ютер, мені достатньо очистити папку lib і в папці bin видалити виконуваний файл (або декілька файлів). Для цього в кореневій папці проекту для кожної ОС можна створити відповідний скрипт для автоматизації цього процесу.
Оскільки я використовую Lazarus, в настройках проекту потрібно вказати що саме мені необхідно. Настройки, які відрізняються від стандартних, я виділив червоним на рис.3.
Рис.3. Настройки проекту Lazarus.
Отже, в розділі “Інші файли” потрібно прописати шлях до трансляції заголовочних файлів, вихідну теку модулів додати перед шляхом “../” (це означає піднятись на рівень вище), аналогічно в розділі “Назва цільового файлу” теж додати “../bin/” (що означає піднятись на один рівень вище і зайти в папку “bin”). Опція “Застосовувати умови” означає, що ім’я виконуваного файлу буде формуватись відповідно до стандарту операційної системи. Так, для проекту з назвою demo в ОС Windows буде створено файл demo.exe, в ОС Linux - demo (без розширення, оскільки тут можливість запустити файл на виконання залежить лише від атрибуту), а в OS X - demo.app.
Ну що ж, усе необхідне для роботи ми вже підготували, в наступній частині статті я покажу тобі типову програму з використанням SDL2, ми розглянемо типовий алгоритм будь-якої відеогри і спробуємо разом це все адаптувати для ігрового фреймворку.

пʼятниця, 15 вересня 2017 р.

Перезавантаження...

Всім привіт! Я довго обдумував, як краще мені викладати матеріали на мій блог, і нарешті вирішив зробити перезавантаження , що уроки буду формувати і публікувати по мірі можливості, наскільки мені буде вистачати часу працювати над ними. Але регулярно буду публікувати різні цікаві матеріали на тему програмування і не тільки, частина з яких потім можливо буде використано для уроків. Таким чином, хоча б раз на тиждень блог буде доповнюватись новими матеріалами, і обіцяю, що ні плагіату, ні копіпасту з інших сайтів у мене не буде. Вже на цих вихідних викладу початок циклу по використанню чудової мультимедійної бібліотеки SDL2, а точніше - почну розробку фреймворка для створення ігор.

субота, 27 травня 2017 р.

Повернення до навчання

На протязі року з минулої публікації практично не було зовсім вільного часу, який можна було б використати для викладення нових уроків на цей блог, проте час минув не зовсім даремно. За цей період я змінив основне місце роботи, набрався практичного досвіду розробки клієнт-серверних програм, в роботі повністю перейшов на використання ОС Linux Mint, освоїв основи програмування мовою Java та ще багато чого.
І ось, нарешті, зміг виділити трохи часу на приведення в порядок блогу.
Поки що лише додав можливість перекладати матеріал на будь-яку доступну мову за допомогою вбудованого перекладача, лічильник відвідувань в нижню панель  блогу, а також виніс публікації уроків окремими сторінками, виділеними в блок бокової панелі.
Також вже є опрацьовано багато матеріалу, частково взятого з попередньої версії сайту, частково написаного заново. Також до уроків буде додаватись відеоматеріал, який наочно демонструє те, про що описано в уроці (вже зроблено скрінкасти, залишилось лише доробити монтаж).
Можливо, вже за день-два буде опубліковано перший урок, в якому я покажу, як правильно встановити останню версію Lazarus/FPC для ОС Windows та Linux, а також розгляну основні складові середовища розробки та анатомію проекту.
Залишайтесь і програмуйте граючи!