Паттерн Команда (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
}
Команда — поведенческий шаблон проектирования, используемый при объектно-ориентированном программировании, представляющий действие. Объект команды заключает в себе само действие и его параметры.(c)
Компонент - хранит вектор наших чисел в данном примере
Коллекция - хранит указатели на компоненты
Инвокер - хранит в данном случае две команды и доступ к абстрактному "документу" по указателю
class InsertCommand : public Command<T>//в имени отражено действие команды
{
int line; //выборка укажет куда вставить/в удалении откуда удалить (строка не елемент)
T* str; //микробуфер то что вставляем в данном случае конечный елемент строки
...
class Invoker
{
std::vector<Command<T>*> DoneCommands; //выполненные команды
Collection<T>* doc; //абстрактный документ в нашем случае коллекция
Command<T>* command;//комманда которая будет добавлена в вектор выполненых команд
...
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); },""}
...
...
void setAction( //действие
//сначало вставить куда
//откуда удалить
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/Команда_(шаблон_проектирования)
Комментарии (14)
dv0ich
15.11.2024 04:33-funroll-all-loops
Это псевдооптимизаторский флаг, который обычно ухудшает производительность, поверьте гентушникам.
Jijiki Автор
15.11.2024 04:33Генту очень хороший дистрибутив, верю
в примере и если так применить в игре с известным количеством элементов до 100 вроде разворачивается если смотреть на ассемблер
https://developers.redhat.com/blog/2018/03/21/compiler-and-linker-flags-gcc#recommended_build_flags
можно еще тут посмотреть, в своё время смотрел
JordanCpp
15.11.2024 04:33ValidRules - вектор, который хранит общие нюансы игровые - т.к. в данном случае мы рассматриваем 2Д, то это я считаю очень удобно, например какие-то общие правила. Например как на доске двигаются фигуры и т.д.
ValidQuests - вектор, который хранит уже частные квесты - то есть то что уже ближе к персонализации игрока, банально какой-то квест задание в игре например прокликать 3 раза.
По кнопке Влево мы наполняем наши 2 вектора числами. По кнопке Вправо переносим из конца первого вектора в конец второго. По кнопке Вниз отменяем последнее перемещение числа.
Объясните плиз, что вы делаете? И зачем здесь нужна команда?
Очень прошу пояснительную бригаду, для моего случая непонимания:)
olegchir
15.11.2024 04:33В этой статье есть некая проблема в том, что ты сразу описываешь полученные результаты. Самого квеста, который ты прошел, здесь нет - он есть только в твоей голове. Ты говоришь - пришел к некоторым вопросам про использование C++, и где эти вопросы? Есть только ответы.
В основном, в таких статьях интересен набор решений, которые ты делал, когда кодил. Почему результат, которого ты достиг, интересный и уникальный. Попробуй в следующий раз написать более лучше)
Например, для олдов, можно напомнить, что SDL теперь работает на лицензии zlib, а не GPL как раньше. По сравнению с MIT, zlib понятней, легче читается, и позволяет изменять способ информирования о ее наличии: "This notice may not be removed or altered from any source distribution." Это значит, его действительно можно использовать в чем-то, что дальше ты будешь продавать. Без этого весь этот код можно было бы сразу выкинуть в помойку и писать на чем-то совершенно другом.
Jijiki Автор
15.11.2024 04:33спасибо учту, простите квест действительно есть. просто я слаб в простоту такого он даже подготовлен и по рандому я сам честно его даже 1 раз проделал)
ValidRules - общие правила что где находится или что как двигается или откуда берется
ValidQuests - конкретика достижения
я начинал с хаотичной разработки без паттернов, пробовал 3Д, сделал пару приложений для себя, на некой итерации второго приложения 2Д, пошли сдвиги что я не на правильном пути. Сначала был прототип который я написал сразу за 1 час, далее просто группировка, и потом осознание что для функционала конкретно уже нужны паттерны действительно (тоесть в хаотичной разработке нет желания переизобретать паттерны)
Serpentine
15.11.2024 04:33Можно не только про лицензию, в SDL3 теперь добавилась куча новых фич.
Например, можно определять функции обратного вызова (main callbacks) вместо main() (см. обзор из документации), которые возвращают/принимают значение SDL_AppResult:
SDL_AppInit() - инициализирует приложение в начале
SDL_AppIterate() - главная функция (выполняется на каждой итерации)
SDL_AppEvent() - работает в поступающими в приложение событиями
SDL_AppQuit() - завершает работу
Т.е. на самом высоком уровне можно совершенно по-другому описать программу без использования стандартной точки входа.
Еще появился переносимый GPU API, т.е. может и в 3D c аппаратным ускорением.
Даже отрисовка текста теперь из коробки есть, правда, только для дебага.
Jijiki Автор
15.11.2024 04:33всё так, еще есть возможность портирования на андроид и другие платформы
Serpentine
15.11.2024 04:33Меня впечатлил их пример игры Змейка, 350 строк кода на Си, при этом можно собрать даже для запуска в браузере.
Jijiki Автор
15.11.2024 04:33змейка интересная да, делал, у них не смотрел реализацию - она же простейшая )
max-daniels
15.11.2024 04:33Я так и не понял, что делает этот паттерн, чего нельзя было сделать без него?
Deosis
Про сам паттерн Команда не сказано ни слова.
Jijiki Автор
спасибо, дополнил