Епізод 03. Хочу ООП!!!

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

Оскільки це буде не лише гра, а й фреймворк, який можна буде використовувати і для інших 2D ігор, потрібно придумати йому назву. Пропоную "GameOver 2D Engine" - хто може запропонувати іншу назву, пишіть коментарі під цим епізодом або під відео на Youtube. Відповідно до назви фреймворку, пропоную усе, що буде до нього відноситись, починати з превіксу "GO_" (аналогічно як в SDL усе має відповідний префікс "SDL_").

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

Щоб не писати весь код по-новому, засобами Lazarus (або Typhon) збережемо копію проекту "demo02" в нову папку і назвемо його "demo03". Копіюючи проект таким чином, нам не доведеться вказувати по-новому в налаштуваннях проекту шляхи.

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

Найперше, нам потрібно в модулі підключити використання бібліотеки SDL, додавши в роздію uses sdl2.

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

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

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

В коді проекту далі була функція Initialize, за допомогою якої відбувалась ініціалізація бібліотеки SDL і усіх наших змінних. Для класу ця робота буде виконуватись в конструкторі, тому скопіюємо код процедури і вставимо його в код конструктора класу. Параметри конструктора і процедури будуть однаковими - заголовок вікна, ширина і висота. Але тепер конструктор не буде повертати результату, який би нам давав ознаку чи вссе пройшло успішно чи десь відбувся збій. Для цього ми опишемо ще дві властивості (теж лише для читання): Error типу Boolean та ErrorInfo типу String. Перша буде встановлюватись в True, якщо відбудеться якась помилка, а в другій буде знаходитись опис цієї помилки. Відповідно внесемо невеликі зміни в код конструктора відносно процедури Initialize:

  constructor TGOEngine.Create(aCaption: PChar; _w, _h: Integer);
  begin
    inherited Create;
    FError:=false;
    FErrorInfo:='';
    //ініціалізація бібліотеки SDL 2.0
    if SDL_Init(SDL_INIT_EVERYTHING)>=0 then begin
      //успішна ініціалізація - створюємо вікно
      SDLWindow:=SDL_CreateWindow(aCaption,
                                 SDL_WINDOWPOS_UNDEFINED,
                                 SDL_WINDOWPOS_UNDEFINED,
                                 _w,
                                 _h,
                                 SDL_WINDOW_SHOWN);
      //якщо вікно створене, створюємо візуалізатор
      if SDLWindow<>nil then begin
        SDLRenderer:=SDL_CreateRenderer(SDLWindow,-1,0);
        if SDLRenderer=nil then begin
          FErrorInfo:=SDL_GetError;
          FError:=true;
        end;
      end;
      //виділення пам'яті для структури обробки подій
      New(SDLEvent);
      //встановлення ознаки виконання циклу і його запуск
      FIsRun := true;
  end;
  end;

Що ж, конструктор ми написали. Але є конструктор - повинен бути і деструктор, в який ми винесемо код процедури Cleanup:

  destructor TGOEngine.Destroy;
  begin
   //прибрати за собою - в оберненому порядку створення
   Dispose(SDLEvent);
   SDL_DestroyRenderer(SDLRenderer);
   SDL_DestroyWindow(SDLWindow);
   SDL_Quit();

   inherited Destroy;
  end;

Далі по коду в нас була процедура DoEvents, в якій оброблялась подія закриття вікна. Опишемо відповідний метод нашого класу, скопіювавши код процедури:

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

Наступною в нас була пуста процедура-заготовка для обробки ігрової логіки. Її ми теж зробимо методом класу, поки теж як заготовку:

  procedure TGOEngine.GameLogic;
  begin
    //тут буде прораховуватись ігрова логіка
  end;

І остання процедура, яка в нас залишилась - це процедура Draw. Її ми теж винесемо у відповідний метод класу:

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

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

  TGOEngine = class
  private
    FError: Boolean;
    FErrorInfo: String;
    FIsRun: Boolean;

  public
    constructor Create(aCaption : PChar; _w,_h : Integer);
    destructor Destroy; override;

    procedure DoEvents;
    procedure GameLogic;
    Procedure Draw;
  published

   property IsRun : Boolean read FIsRun;//ознака виконання головного циклу
   property Error : Boolean read FError;//ознака збою в роботі класа
   property ErrorInfo : String read FErrorInfo; //текстовий опис помилки
  end;

В коді основної програми нам тепер не потрібно підключати модулі SDL2, натомість підключимо наш новий модуль ugoengine, а в розділі опису змінних буде лише одна, по типу нашого класу. Замість функцій, описаних в попередньому епізоді, будуть викликатись відповідні методи нашого класу. Крім цього, нам ще необхідно підключити стандартний модуль Sysutils, в якому міститься функція FreeAndNil, за допомогою якої при закінченні програми буде звільнятись виділена пам'ять.

program demo03;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  Classes
  { you can add units after this },sysutils, ugoengine;
var
  TestEngine : TGOEngine;


begin
  TestEngine:=TGOEngine.Create('Demo03',640,480);
  if TestEngine.Error then WriteLn(TestEngine.ErrorInfo)
   else
    while TestEngine.IsRun=true do begin
      TestEngine.DoEvents;
      TestEngine.GameLogic;
      TestEngine.Draw;
    end;
  FreeAndNil(TestEngine);
end.

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

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

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

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

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