Епізод 04. Нарощуємо функціонал

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

  1. виправимо деякі логічні помилки з минулого проекту і зробимо базовий базовий клас сінглтоном
  2. перенесемо виконання циклу всередину базового класу
  3. пропишемо додаткові властивосіт вікна

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

program demo04;
{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  Classes
  { you can add units after this }, ugoengine;

Отже, почнемо з виправлення. По-перше, зробимо наш клас сінглтоном. Це потрібно для того, щоб нам випадково не створити новий екземпляр класу та не ініціалізувати повторно бібліотеку SDL 2. Отже, змінні для вікна, візуалізатора і структури подій перенесемо в розділ private нашого класу:

  private
    FWindow : PSDL_Window;
    FRenderer : PSDL_Renderer;
    FEvent : PSDL_Event;       

Відповідно виправимо в коді методів класу всі відповідні посилання на попередні змінні на наші поля. Розділ var в частині модуля implementation поки що можемо видалити - наразі він нам не потрібний.

Щоб запобігти створенню додактових екзекмплярів об'єктів типу нашого класу, кнструктор перенесемо в розділ Private опису класу, опишемо в розділі Var модуля зміну по типу нашого класу і додамо автоматичне створення та знищення об'єкту в частинах initialization та finalization модуля. Крім цього, ще додамо в частину implementation модуля додаткову зміну CountInstances типу Byte, яка буде ознакою чи вже створено екземпляр класу. Значення цієї змінної буде перевірятись в конструкторі класу.


  TGOEngine = class
  private
    FWindow : PSDL_Window;
    FRenderer : PSDL_Renderer;
    FEvent : PSDL_Event;
    FError: Boolean;
    FErrorInfo: String;
    FIsRun: Boolean;

    constructor Create;
var
  GoEngine : TGOEngine;


implementation

var CountInstances : Byte;

  { TGOEngine }

  constructor TGOEngine.Create;
  begin
   if CountInstances>0 then Exit;
    inherited Create;
    FError:=false;
    FErrorInfo:='';
    //ініціалізація бібліотеки SDL 2.0
    if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
      //успішна ініціалізація - створюємо вікно
      FWindow:=SDL_CreateWindow('',
                                 SDL_WINDOWPOS_UNDEFINED,
                                 SDL_WINDOWPOS_UNDEFINED,
                                 0,
                                 0,
                                 SDL_WINDOW_SHOWN);
      //якщо вікно створене, створюємо візуалізатор
      if FWindow<>nil then begin
        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;


initialization
CountInstances:=0;
GoEngine:=TGOEngine.Create;

finalization
FreeAndNil(GoEngine);
end.

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

   property Caption : String read FCaption write SetCaption;//заголовок вікна
   property Width : Integer read FWidth write SetWidth default 640; //ширина вікна
   property Height : Integer read FHeight write SetHeight default 480; //висота вікна

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

procedure TGOEngine.SetCaption(AValue: String);
begin
  if FCaption=AValue then Exit;
  FCaption:=AValue;
  SDL_SetWindowTitle(FWindow,PChar(UTF8String(FCaption)));
end;

procedure TGOEngine.SetHeight(AValue: Integer);
begin
  if FHeight=AValue then Exit;
  FHeight:=AValue;
  if (FWindow<>nil) then begin
   SDL_SetWindowSize(FWindow, FWidth, FHeight);
   SDL_SetWindowPosition(FWindow,0,0);
  end;
end;

procedure TGOEngine.SetWidth(AValue: Integer);
begin
  if FWidth=AValue then Exit;
  FWidth:=AValue;
  if (FWindow<>nil) then begin
   SDL_SetWindowSize(FWindow, FWidth, FHeight);
   SDL_SetWindowPosition(FWindow,0,0);
  end;
end;

В цьому коді є два моменти, на які я хотів би звернути увагу. Перший - це встановлення заголовку вікна. Для того, щоб можна було використати не лише англійські літери, потрібно Паскалівський тип String приводити до PChar, але в цьому випадку втрачається інформація про локаль і на виході маємо те, що маємо. Щоб коректно вивести будь-який текст, нам попередньо потрібно тип String привести до UTF8String, а тоді вже до PChar. Проте якщо відразку вказати властивість заголовку типу UTF8String, це не працює. Тому треба зробити саме таке подвійне перетворення типу.

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

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

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

  procedure TGOEngine.Run;
  begin
    if (FError=true) then begin
      WriteLn(FErrorInfo);
    end else while FIsRun do begin
      DoEvents;
      GameLogic;
      Draw;
    end;
  end;

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

program demo04;
{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  Classes
  { you can add units after this }, ugoengine;

begin
  if GoEngine<>nil then begin
   GoEngine.Caption:='демо04';
   GoEngine.Width:=640;
   GoEngine.Height:=480;
   GoEngine.Run;
  end;
end.

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

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

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

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

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