Паттерн Команда (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/Команда_(шаблон_проектирования)
Автор: Jijiki