Паттерн Команда (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/
Добавить комментарий