Паттерн Команда (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)


  1. Deosis
    15.11.2024 04:33

    Про сам паттерн Команда не сказано ни слова.


    1. Jijiki Автор
      15.11.2024 04:33

      спасибо, дополнил


  1. dv0ich
    15.11.2024 04:33

    -funroll-all-loops

    Это псевдооптимизаторский флаг, который обычно ухудшает производительность, поверьте гентушникам.


    1. Jijiki Автор
      15.11.2024 04:33

      Генту очень хороший дистрибутив, верю

      в примере и если так применить в игре с известным количеством элементов до 100 вроде разворачивается если смотреть на ассемблер

      https://developers.redhat.com/blog/2018/03/21/compiler-and-linker-flags-gcc#recommended_build_flags

      можно еще тут посмотреть, в своё время смотрел


  1. JordanCpp
    15.11.2024 04:33

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

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

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

    Объясните плиз, что вы делаете? И зачем здесь нужна команда?
    Очень прошу пояснительную бригаду, для моего случая непонимания:)


    1. olegchir
      15.11.2024 04:33

      Всё же в коде написано


  1. olegchir
    15.11.2024 04:33

    В этой статье есть некая проблема в том, что ты сразу описываешь полученные результаты. Самого квеста, который ты прошел, здесь нет - он есть только в твоей голове. Ты говоришь - пришел к некоторым вопросам про использование C++, и где эти вопросы? Есть только ответы.

    В основном, в таких статьях интересен набор решений, которые ты делал, когда кодил. Почему результат, которого ты достиг, интересный и уникальный. Попробуй в следующий раз написать более лучше)

    Например, для олдов, можно напомнить, что SDL теперь работает на лицензии zlib, а не GPL как раньше. По сравнению с MIT, zlib понятней, легче читается, и позволяет изменять способ информирования о ее наличии: "This notice may not be removed or altered from any source distribution." Это значит, его действительно можно использовать в чем-то, что дальше ты будешь продавать. Без этого весь этот код можно было бы сразу выкинуть в помойку и писать на чем-то совершенно другом.


    1. Jijiki Автор
      15.11.2024 04:33

      спасибо учту, простите квест действительно есть. просто я слаб в простоту такого он даже подготовлен и по рандому я сам честно его даже 1 раз проделал)

      ValidRules - общие правила что где находится или что как двигается или откуда берется

      ValidQuests - конкретика достижения

      я начинал с хаотичной разработки без паттернов, пробовал 3Д, сделал пару приложений для себя, на некой итерации второго приложения 2Д, пошли сдвиги что я не на правильном пути. Сначала был прототип который я написал сразу за 1 час, далее просто группировка, и потом осознание что для функционала конкретно уже нужны паттерны действительно (тоесть в хаотичной разработке нет желания переизобретать паттерны)


    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 аппаратным ускорением.

      Даже отрисовка текста теперь из коробки есть, правда, только для дебага.


      1. Jijiki Автор
        15.11.2024 04:33

        всё так, еще есть возможность портирования на андроид и другие платформы

        https://wiki.libsdl.org/SDL3/Android


        1. Serpentine
          15.11.2024 04:33

          Меня впечатлил их пример игры Змейка, 350 строк кода на Си, при этом можно собрать даже для запуска в браузере.


          1. Jijiki Автор
            15.11.2024 04:33

            змейка интересная да, делал, у них не смотрел реализацию - она же простейшая )


  1. max-daniels
    15.11.2024 04:33

    Я так и не понял, что делает этот паттерн, чего нельзя было сделать без него?


    1. Jijiki Автор
      15.11.2024 04:33

      общий функционал отмены последнего успешного действия