Первый Pattern, первый квест

от автора

Паттерн Команда (Hello World + Undo)

Всем привет. В этой небольшой статье хочу поделиться методом, к которому пришел, когда разрабатывал игру 2D. Та игра, где столкнулся впервые с такими вопросами, натолкнула меня на написание этой статьи.

Подготовка рабочего пространства

Линукс, компилятор 14.2 gcc/g++, cmake , SDL3, X11, clangd для lsp.

Понадобятся некоторые библиотеки/софт если их нету

sudo apt update; sudo apt upgrade sudo apt install libglm-dev cmake libxcb-dri3-0 libxcb-present0 \ libpciaccess0 libpng-dev libxcb-keysyms1-dev libxcb-dri3-dev libx11-dev \ g++-14 gcc-14 g++-multilib libwayland-dev libxrandr-dev libxcb-randr0-dev \ libxcb-ewmh-dev git python3 bison libx11-xcb-dev liblz4-dev libzstd-dev \ ocaml-core ninja-build pkg-config libxml2-dev wayland-protocols \ python3-jsonschema clangd build-essential

Создадим структуру проекта

Структура проекта

Структура проекта
mkdir TestDirPC cd TestDirPC mkdir third_party touch CMakeLists.txt touch main.cpp mkdir build

TestDirPC/CMakeLists.txt:

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

cmake_minimum_required(VERSION 3.27)  project(test) add_compile_options(-std=c++23 -Ofast -funroll-all-loops) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # does not produce the json file set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") # works  set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin") set (EXECUTABLE_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/bin") set (test CXX_STANDARD 23)  add_subdirectory(third_party)  add_executable(test main.cpp )  target_link_libraries(test pthread dl m z)  target_include_directories(test PUBLIC third_party/SDL/include) target_link_libraries(test SDL3-static)
  • версия cmake 3.27, название проекта в последующем бинарника в папке bin — test

  • -std=c++23 -Ofast -funroll-all-loops — укажем версию С++23, -Ofast оптимизация на скорость без возможности отладки, -funroll-all-loops компилятор по возможности развернет известные циклы

  • далее по тексту мы говорим что команды компиляции будут сохранены в json файл, далее что будетиспользован кэш

  • указываем папку bin — туда будет сохранятся после сборки наш бинарник

  • добавим в конфигурацию папку third_party

  • добавим в конфигурацию исходный файл main.cpp

  • начинаем линковать с библиотеками pthread — многопоток Posix, dl — для работы с библиотеками, m — математика, z — сжатие

  • покажем папку include

  • слинкуем наш бинарник с SDL3-static

TestDirPC/main.cpp:

#include <SDL3/SDL.h> #include <SDL3/SDL_main.h>   int main(int argc, char const *argv[]) {   SDL_Init(SDL_INIT_EVENTS|SDL_INIT_VIDEO);   SDL_Event e;   bool run=false;   SDL_Window* win;   SDL_Renderer* ren;   SDL_CreateWindowAndRenderer(0,1200,900,0,&win,&ren);    while(!run)   {     while(SDL_PollEvent(&e))     {       switch(e.type)       {         case SDL_EVENT_QUIT:           run=true;           break;         case SDL_EVENT_KEY_DOWN:       switch(e.key.key)     {       case SDLK_LEFT:               break;             case SDLK_RIGHT:               break;             case SDLK_UP:               break;             case SDLK_DOWN:               break;           }           break;       }     }     SDL_RenderClear(ren);     SDL_SetRenderDrawColor(ren, 10, 10, 10, 255);            SDL_RenderPresent(ren);     SDL_Delay(60);   }      SDL_Quit();   return 0; }
  • подключим главный хидер библиотеки SDL3/SDL.h

  • подключим на всякий случай хидер SDL3/SDL_main.h

  • проинициализируем подсистему событий и подсистему видео

  • объявим структуру SDL_Event

  • настроим главный цикл переменной run установив в false

  • объявим необходимые структуры чтобы окно открылось — SDL_Window/SDL_Renderer

  • запустим окно передав в качестве аргументов (вместо const char* title — 0, ширина окна 1200, высота 900,флаги окна — 0 — так как запуск нужен для кнопок, адрес окна, адрес рендера — адреса потому что будет их инициализация)

  • в цикле мы организовываем по события возможность закрытия окна по крестику

  • возможность нажатия клавиш — Влево, Вправо, Вверх, Вниз

    Отрисовка

  • Очистить поверхность рендера

  • установить поверхность рисуемой области рендера 10 10 10 255 — этим цветом

  • показать поверхность рендера

  • задержка на около 60 миллисекунд

    Закрытие выделенных ресурсов библиотекой SDL

    Далее конфигурирование проекта

#Создадим папку, проклонируем библиотеку, создадим файл для сборки зависимости  cd third_party  git clone https://github.com/libsdl-org/SDL.git SDL  touch CMakeLists.txt

TestDirPC/third_party/CMakeLists.txt:

set(BUILD_SHARED_LIBS OFF) set(SDL_TEST_LIBRARY OFF) set(SDL_SHARE OFF) set(SDL_STATIC ON)  add_subdirectory(SDL)  include_directories(SDL/include)
  • отключим сборку динамической библиотеки

  • отключим сборку тестов в самой библиотеке есть свои тесты

  • сборку SDL динамическую отключаем

  • указываем сборку статической библиотеки SDL

  • добавим директорию на том же уровне SDL

  • добавим папку include

Минимальный проект создан

#перейдём в папку build; конфиг таргетов; сборка зависимости и main.cpp   cd ../build; cmake ..; cmake --build . --target all   #откроем еще папку bin   ./test  #появится черное окно которое по крестику закроется #у нас задача сделать простенькое приложение - обкатать паттерн #т.е. интересен случай считывания нажатых клавиш с клавиатуры
Окно приложения

Окно приложения

Сразу к делу!

Скрытый текст
#include <SDL3/SDL.h> #include <SDL3/SDL_main.h> #include "SDL3/SDL_keycode.h" #include "SDL3/SDL_render.h" #include <functional> #include <vector> #include <cstdlib> #include <iostream>  class Number { private:   int N; public:   Number(int n){ N=n; }   Number(Number& n){N=n.getNumber();}   Number(Number&& n){N=n.getNumber();}   int getNumber(){ return N; } };  template<typename T> class Component { private:   std::vector<T> c;   int ID; public:   virtual void Add(T t)   {     c.push_back(std::move(t));   }   void Del()   {     if(c.size()>0)       { c.pop_back();       }   }   int getSize()   {     return c.size();   }   typename std::vector<T>::iterator GetBegin()   {     return c.begin();   }   typename std::vector<T>::iterator GetEEnd()   {     return c.end();   }   typename std::vector<T>::reverse_iterator GetRBegin()   {     return c.rbegin();   }   typename std::vector<T>::reverse_iterator GetREnd()   {     return c.rend();   }   T GetEnd()   {     return c.back();   }   T GetI(int i)   {     return c[i];   }   int GetID()   {     return ID;   }   void SetID(int i)   {     ID=i;   }   virtual void Show()   {   }   void Clear()   {     for(int i=0;i<c.size();i++)       {         delete c[i];       }     c.clear();   } };   template<typename T> class Collection:public Component<T> { private:   std::vector<T*> test; public:   Collection()   {   }   void Insert(int p,T* t)   {     test.push_back(std::move(t));     test[p]->SetID(p);   }   void GetEnd(int p)   {     test.pop_back();   }   T* GetI(int i)   {     return test[i];   }   void Show()   {     for(int i=0;i<test.size();i++)       { test[i]->Show();       }   }   void Clear()   {     for(int i=0;i<test.size();i++)       {         test[i]->Clear();       }          test.clear();   }   ///////////////for-range-based   auto begin()   {     return test.begin();   }   auto end()   {     return test.end();   }   auto cbegin() const   {     return test.begin();   }   auto cend() const   {     return test.end();   }   auto begin() const   {     return test.begin();   }   auto end() const   {     return test.end();   }   //////////////// };    template<typename T> class Command { protected:   Collection<T>* doc;  public:   virtual ~Command() {}   virtual void Execute() = 0;    virtual void unExecute() = 0;    void setDocument(  Collection<T>* _doc )   {     doc = _doc;    } };  template<typename T> class InsertCommand : public Command<T> {   int line;    T* str; public:   InsertCommand( int _line, T* _str ): line( _line )   {     str = new T;       str->Add(_str->GetEnd());   }   void Execute()   {     Command<T>::doc->GetI(line)->Add( str->GetEnd() );   }    void unExecute()   {     Command<T>::doc->GetI( line )->Del();     delete str;   } };   template<typename T> class DeleteCommand : public Command<T> {   int line;    T* str; public:   DeleteCommand( int _line,T* _str ): line( _line )   {     str = new T;   }   void Execute()   { str->Add( Command<T>::doc->GetI(line)->GetEnd() ); Command<T>::doc->GetI(line)->Del();   }   void unExecute()   {     Command<T>::doc->GetI(line)->Add( str->GetEnd() );      delete str;   } };   template<typename T> class Invoker {   std::vector<Command<T>*> DoneCommands;    Collection<T>* doc;    Command<T>* command;  public:   void Insert( int line, T* str )   {     command = new InsertCommand( line, str);             command->setDocument( doc );       command->Execute();      DoneCommands.push_back( command );    }   void Delete(int line, T* str)   {     command = new DeleteCommand(line,str);             command->setDocument( doc );       command->Execute();      DoneCommands.push_back( command );    }   void Undo()   {     if( DoneCommands.size() == 0 )       { //std::cout << "There is nothing to undo!" << std::endl;        }     else       { command = DoneCommands.back();  DoneCommands.pop_back();  command->unExecute(); delete command;       }   }   void SetDoc( Collection<T> *_doc )   {     doc=_doc;   }   void Show()        {     doc->Show();   }   void Clear()   {     for(auto& e: DoneCommands)       { delete e;       }     DoneCommands.clear();     doc->Clear();   } };  template<typename T> class One:public Component<T> { private:    public:   One()   {    }   void Show() override {     typename std::vector<T>::iterator it=this->GetBegin();     for(;it<this->GetEEnd();it++)       { //SDL_Log("%d",(*it)->getNumber()); std::cout << (*it)->getNumber();       }     std::cout<<std::endl;   }   ~One()   {       } };  template<typename T> class Two:public Component<T> { private:    public:   Two()   {    }   void Show() override {     typename std::vector<T>::iterator it=this->GetBegin();     for(;it<this->GetEEnd();it++)       { std::cout << (*it)->getNumber();       }     std::cout<<std::endl;   }   ~Two()   {       } };  class ValidationEntry { public:   int id;   std::function<Number*()> getNumber;//every   const char* message; };   class ValidRules { public:   int id;   std::function<bool()> getRule;//everyRule/everyQuest   const char* message; };  void setAction(Invoker<Component<Number*>>& inv,std::vector<int> pT,Component<Number*>* cG);  bool Iteration(Collection<Component<Number*>> coll) {   return (coll.GetI(0)->getSize()==5&&coll.GetI(1)->getSize()==5); }  Number* getEnd(Collection<Component<Number*>> coll,int i) {   return  coll.GetI(i)->GetEnd(); }  int main(int argc, char const *argv[]) {   SDL_Init(SDL_INIT_EVENTS|SDL_INIT_VIDEO);   SDL_Event e;   bool run=false;   SDL_Window* win;   SDL_Renderer* ren;   SDL_CreateWindowAndRenderer(0,1200,900,0,&win,&ren);    One<Number*>* one=new One<Number*>();   Two<Number*>* two=new Two<Number*>();      one->Add(new Number(rand()%6));   two->Add(new Number(rand()%6));          Collection<Component<Number*>> collection;   Invoker<Component<Number*>> inv;      std::vector<int> pT={0,1};      collection.Insert(0,one);   collection.Insert(1,two);      inv.SetDoc(&collection);    std::vector< ValidationEntry > validations = {//justvalidation     {0,[&]() -> Number* { return getEnd(collection,0); },""},     {1,[&]() -> Number* { return getEnd(collection,1); },""}   };    std::vector< ValidRules > validQuests = {//quest     {0, [&]() -> bool { return Iteration(collection); }, "For check questEnd"}   };   ////   int counter=0;   srand(time(NULL));   while(!run)     {       while(SDL_PollEvent(&e)) {   switch(e.type)     {     case SDL_EVENT_QUIT:       run = true;       break;     case SDL_EVENT_KEY_DOWN:       switch(e.key.key) { case SDLK_LEFT:   //SDL_Log("Left");   if(!Iteration(collection)&&counter<5)     {       one->Add(new Number(rand()%6));       two->Add(new Number(rand()%6));       counter++;     }      inv.Show();         break; case SDLK_UP:   //SDL_Log("UP");   break; case SDLK_DOWN:   //SDL_Log("DOWN");   inv.Undo();   inv.Undo();   inv.Show();   break; case SDLK_RIGHT:   //SDL_Log("RIGHT");   setAction(inv,pT,one);   inv.Show();   break; }       break;     } }       SDL_RenderClear(ren);       SDL_SetRenderDrawColor(ren, 10, 10, 10, 255);              SDL_RenderPresent(ren);       SDL_Delay(60);     }    inv.Clear();   SDL_Quit();   delete one;   delete two;   return 0; }  void setAction(Invoker<Component<Number*>>& inv,std::vector<int> pT,Component<Number*>* cG) {   inv.Insert(pT[1],cG);//second-to   inv.Delete(pT[0],cG);//first-from } 

Компонент — хранит вектор наших чисел в данном примере

Коллекция — хранит указатели на компоненты

Инвокер — хранит в данном случае две команды и доступ к абстрактному «документу» по указателю

ValidRules — вектор, который хранит общие нюансы игровые — т.к. в данном случае мы рассматриваем 2Д, то это я считаю очень удобно, например какие-то общие правила. Например как на доске двигаются фигуры и т.д.

ValidQuests — вектор, который хранит уже частные квесты — то есть то что уже ближе к персонализации игрока, банально какой-то квест задание в игре например прокликать 3 раза.

По кнопке Влево мы наполняем наши 2 вектора числами. По кнопке Вправо переносим из конца первого вектора в конец второго. По кнопке Вниз отменяем последнее перемещение числа.

Результат

Результат

Ресурсы:

https://vulkan.lunarg.com/doc/sdk/latest/linux/getting_started.html

https://www.amazon.com/Beginning-C-Through-Game-Programming/dp/1435457420

https://ru.wikipedia.org/wiki/Команда_(шаблон_проектирования)


ссылка на оригинал статьи https://habr.com/ru/articles/858722/