Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > разработка игр, графический дизайн и моделирование > Gamedev - cоздание игр: Unity, OpenGL, DirectX
Регистрация

Восстановить пароль
Повторная активизация e-mail

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 29.07.2014, 20:05   #91
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Реализация менеджера игровых состояний - часть 2. Создание кода

Как работает менеджер состояний

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

В каждый момент времени у нас есть одно работающее состояние (менеджер содержит указатель на него), периодически оно может посылать менеджеру сообщения о своем желании передать управление другому состоянию (через функцию sendEvent). Получив такое сообщение, тот ищет в своей таблице переходов пару (тип текущего состояния, тип сообщения), и если находит – то в соответствующей «ячейке таблицы» находится тип состояния, которое должно стать следующим.

Теперь, имея на руках этот тип нового состояния, менеджер создает новый объект этого типа, передавая ему заодно некоторую информацию, которая необходима для инициализации данного объекта состояния (эта информация содержится в посланном сообщении). Однако, в таблице переходов разумно хранить только идентификатор следующего состояния (принадлежащего перечислению StateType), поэтому для создания объекта необходимо использовать фабрику.

Из двух приведенных в прошлом посте вариантов я выбрал реализацию фабрики, описанную в книге Александреску «Современное проектирование на C++», поскольку при написании собственных фабричных методов мы можем извлекать из полученного сообщения информацию, требующуюся конкретному подклассу состояния.

Однако, в классическом варианте фабрики функция create вызывается с единственным аргументом – идентификатором типа. В нашем же случае необходимо также передать данные, содержащиеся в присланном сообщении, поэтому мы делаем функцию шаблонной и передаем ей в качестве параметра объект шаблонного типа (впоследствии это будет StateEvent).

В конкретных фабричных методах (типа createXState, создающего объект класса XState) мы получаем указатель на StateEvent. Поскольку здесь речь идет об известном типе, мы также знаем, какой подкласс сообщения содержит необходимую нам информацию. Мы приводим указатель к типу YEvent, извлекаем из него нужные сведения и передаем их конструктору XState, после чего возвращаем указатель на вновь созданный объект.

Итак, получив сообщение от текущего состояния, менеджер определяет тип следующего состояния, и по этому типу создает объект, которому и передаст управление. Но что вообще подразумевается под «активным состоянием»?
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 29.07.2014, 20:05   #92
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Реализация менеджера игровых состояний - часть 2. Создание кода

Как игровое состояние выполняет свою работу

Поскольку текущее состояние начинает выполнять свою работу по вызову функции Run, а менеджер должен иметь возможность в любой момент остановить этот процесс извне, то очевидно, что работа этой функции должна происходить в отдельном потоке. Следовательно, сделать состояние активным – значит запустить для него новыйпоток и подключить его к устройствам ввода-вывода (см. предыдущий пост); сделать неактивным – только отключить его от этих устройств; напрочь удалить – остановить поток и удалить объект состояния.

С этого момента начинаются определенные сложности. Моей первой идеей было реализовать этот процесс так. Имеется класс потока Thread, у которого есть функция-член RunUntilCancelled. Эта последняя принимает указатель на выполняющую полезную работу функцию и запускает поток, в котором выполняет эту функцию, а если она завершилась, то после этого просто крутит бесконечный цикл. Это сделает работу с состояниями более единообразной – у нас не будет двух случаев для состояний, которые работают, пока нам не надоест (как, например, главное меню) и которые завершаются, справившись со своей работой (скажем, интро).

Прежде чем вернуться к специфике состояний, остановлюсь на классе потока. Такая сущность является отличным кандидатом на то, чтобы стать универсальным компонентом, который можно использовать также и в других случаях, поэтому неплохо было бы сделать его как можно более гибким. При этом есть желание сделать его код (и вообще весь код игры) как можно более платформенно-независимым – это одна из базовых идей, которые я закладываю в проект.

Осуществить этот подход нам поможет идиома pimpl (pointer to implementation). Суть ее заключается в том, что мы определяем в нашем классе вложенный класс impl, который и будет выполнять всю работу, а функции нашего класса только обращаются к нему. При этом объявление и определение impl находится в отдельном cpp-файле, который подключается к cpp-файлу внешнего класса.

Это приводит к тому, что пользователю виден исключительно внешний, платформенно-независимый интерфейс. В то же время код, зависящий от платформы, сосредоточен в одном файле. Если же мы вместо него подключим другой файл, обеспечивающий работу с другой платформой, но имеющий тот же самый интерфейс, то внешний класс сможет использовать его без изменения своего кода.

Таким образом, создавая разные реализации MyClass::impl, можно полностью сосредоточить в нем платформенно-зависимый код. Мультиплатформенность при этом будет обеспечиваться только написанием по мере надобности новых реализаций вложенного класса, в клиентском же коде ничего менять не придется.

В нашем случае мы имеем класс Thread, содержащийся в Thread.h и Thread.cpp, и класс Thread::impl, который целиком находится в файле ThreadImplQThread.cpp (последний подключается в Thread.cpp вместе с Thread.h). Функции-члены Threadделегируют все вызовы Thread::impl, не делая о нем никаких предположений, за исключением открытого интерфейса (или его части).

Это приводит к дублированию довольно большого объема кода, но зато позволяет подготовить приложение к портированию на различные платформы. Кроме того, идиома pimpl позволяет нам надежно разделить уровни абстракции – тот, что относится к предметной области (например, управлению потоками, необходимому в нашей конкретной задаче) и технические детали реализации.

Однако, вернемся к потокам в моей игре. Возможно, вас смутила фраза о функции-члене RunUntilCancelled, принимающей указатель на функцию, и если так – то не зря. Ведь мы передаем указатель на функцию-член класса состояния, а не на обычную функцию. С другой стороны, нет никаких причин ограничивать поток выполнением только функций-членов (тем более всего одного класса), если уж мы хотим создать универсальный компонент. А это значит, что нам придется передавать потоку обобщенный функтор.

Описание и реализация обобщенных функторов приводится все в той же книге Александреску, в главе 5. Если вкратце, то обобщенный функтор – это некий объект, который имеет полную семантику значений (а, значит, может вести себя как простая переменная и, в частности, передаваться в функцию в качестве аргумента) и позволяет простым способом вызывать все, что ведет себя как функции (а именно: просто функции, указатели и ссылки на них, объекты классов с перегруженным operator()функторы, функции-члены).
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 29.07.2014, 20:06   #93
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Реализация менеджера игровых состояний - часть 2. Создание кода

Понимание обобщенных функторов

К сожалению, пятая глава «Современного проектирования», хотя и описывает подробно все аспекты реализации обобщенного функтора, но, на мой взгляд, недостаточно хорошо поясняет идеологию всего происходящего. В результате понять, что же там творится, и откуда столько всяких непонятных вещей, с первого раза очень трудно. Поэтому в этот раз помимо ссылки на материал я приведу некоторые пояснения по его содержанию.

Итак, класс Functor – это тот самый класс обобщенного функтора, который реализует семантику значений и по вызову operator() производит требуемые действия. При этом конкретная специализация Functor задает список аргументов и тип возвращаемого значения той вызываемой сущности, которая в нем скрыта (смотрите, однако, раздел 5.8). Кпримеру, пусть у нас есть функция filter, работающая с контейнером значений типа double:
Код:
typedefFunctor<bool, TYPELIST_1(double)> FilterPred;
Container filter(Iter from, Iter to, FilterPred pred)
	{
	//...
	}
Третьим аргументом ее является функтор, содержащий в себе некоторый предикат, принимающий действительное число и возвращающий значение типа bool.

И пусть есть следующие объекты (назначение которых должно быть понятно из названий):
Код:
bool Positive(double);
class LessThan
	{
	public:
	LessThan(double val);
	bool operator()(double) const;
 
	private:
	double Val;
	};
class Vector
	{
	public:
	//...
	bool contain(double) const;
	//...
	};
Тогда мы можем передать в функцию filter третьим аргументом предикаты, хранящие в себе функцию (Positive), функтор (объект класса LessThan) и указатель на функцию-член (объект класса Vector и указатель на функцию-член Vector::contain). Это возможно, потому что все три предиката будут иметь один и тот же тип – Functor, параметризованный типом возвращаемого значения bool и списком аргументов TYPELIST_1(double).

Реализовать такую общность помогает делегирование вызова Functor:: operator(/**/) члену spImpl_ – указателю на «интерфейс» FunctorImpl. Последний является чисто абстрактным классом, который наследуют FunctorHandler и MemHandler.

FunctorHandler – это шаблонный класс, который реализует работу с функциями, ссылками и указателями на них, а также с функторами. MemHandler – шаблонный класс, работающий с функциями-членами.

Универсальность работы обобщенного функтора (даже если мы рассматриваем только конкретную его специализацию – FilterPred с конкретными типом возвращаемого значения и списком аргументов) достигается двумя видами полиморфного поведения.

Во-первых, FunctorHandler и MemHandler приводят к единому виду два разных типа вызова – простой и через объект класса. А именно – к вызову operator() класса, определяющегося теми же шаблонными параметрами, что и Functor. Не будь функций-членов – можно было бы обойтись без наследования от FunctorImpl.

Во-вторых, наследники FunctorImpl являются шаблонными классами, специализированными также типами вызываемых сущностей. Так, два FunctorHandler, один из которых содержит функцию Positive, а второй – функтор LessThan, определены именно этими параметрами. Однако, они унаследованы от одного и того же FunctorImpl, поэтому указатель в FilterPred может указывать на любого из них.

Может показаться странным, что шаблонные классы – а вернее, разные специализации шаблонного класса – являются наследником одного и того же нешаблонного класса (Functor<bool, TYPELIST_1(double)> – это уже вполне конкретный класс), но на самом деле в этом нет ничего удивительного. FunctorHandler<FilterPred, Positive> и FunctorHandler<FilterPred, LessThan> – два обычных нешаблонных класса, пусть и разные. Но разве нас смущает, что разные классы Circle и Rect наследуются от одного и того же класса Figure?
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 29.07.2014, 20:06   #94
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Реализация менеджера игровых состояний - часть 2. Создание кода

Итак, когда мы определяем обобщенный функтор, мы первым делом задаем ему тип возвращаемого значения и список аргументов – и вот у нас есть объект конкретного класса Functor. Затем мы передаем ему указатель на объект класса, являющегося наследником FunctorImpl (этот указатель также имеет вполне конкретный тип).

При этом этот объект либо работает с функторо-подобными объектами (FunctorHandler), либо с функциями-членами (MemHandler). Чтобы получить уже полностью работоспособный объект, мы должны специализировать эти шаблонные классы либо типом а-ля функтор, либо парой «класс и указатель на функцию-член» соответственно. После этого можно вызывать Functor:: operator().

На этом с идеологией обобщенных функторов все. Надеюсь, вышеприведенный текст поможет вам легче понять пятую главу «Современного проектирования на C++» и сэкономит от получаса до нескольких дней вашего времени. И, возможно, какое-то количество нервных клеток.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 29.07.2014, 20:06   #95
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Реализация менеджера игровых состояний - часть 2. Создание кода

Возвращаясь к работе игровых состояний

Хорошо, теперь мы в достаточной степени понимаем принципы, лежащие в основе обобщенных функторов и сможем их применить для работы с потоками. Теперь класс Thread может принимать указатель на функцию-член и создавать отдельный поток, в котором будет крутиться функция Run текущего состояния.

Следующий шаг работы менеджера состояний выглядит следующим образом: создав новый объект класса, производного от GameState, он обращается к потоку, в котором выполняется предыдущее состояние, закрывает его, а сам объект состояния уничтожает. Вновь созданный же становится текущим, создается новый поток, которому в функцию RunUntilCancelled передается функтор с указателем на функцию-член Run.

В принципе на этом реализацию менеджера, работающего как детерминированный конечный автомат (ДКА), можно завершить (вопрос о взаимодействии с устройствами ввода-вывода рассмотрим позже).

Однако, как мы помним, у нас есть еще стек состояний. Работа с ним реализуется довольно просто – в классе StateEvent есть член StackType, который определяет, что делать с текущим состоянием менеджеру, получившему это сообщение. В зависимости от его значения мы либо просто удаляем старое состояние (предварительно закрыв его поток), либо помещаем его в стек (отлучив предварительно от ввода-вывода, но поток не трогая), либо удаляем, но текущим состоянием станет то, что было на верхушке стека.

Расписывать тут было бы особенно нечего, если бы не одно «но». Игровое состояние оказывается тесно связано с потоком, в котором оно выполняется. Это значит, что в стек нужно помещать не только GameState, но и Thread – иначе как мы потом будем останавливать поток, когда нужно будет это состояние удалить?

Здесь могут быть различные варианты решения проблемы. Можно помещать состояние и поток в разные стеки – но тогда теряется связь между ними и возникает опасность возможной «рассинхронизации» двух стеков. Можно наследовать класс состояния от класса потока – но это выглядит слишком противоестественно и алогично, чтобы всерьез рассматривать такую возможность.

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

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

Наконец, еще один вариант решения проблемы хранения всех неактивных состояний и связанных с ними потоков – создать класс или структуру (скажем, ManagedState), в который поместить состояние и поток. Главный вопрос, который вызывает такой объект – есть ли в нем какой-либо семантический смысл, кроме удобной группировки данных. Как мне кажется, что-то в этом роде можно подобрать, однако это вопрос дальнейшей работы.

Таковы в общих чертах предварительные итоги работы над прототипом игрового приложения, использующего менеджер состояний. Часть этого уже воплощена в коде, часть только прорабатывается. Однако, как видите, полностью или частично реализованного уже набралось на полноценный пост. Остается лишь надеяться, что уже в следующем я смогу завершить описание разработки этого прототипа.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 15.01.2017, 14:30   #96
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Проект возобновлен - идет работа над игровым движком

С момента прошлой публикации прошло около двух с половиной лет – развитие проекта сильно затормозилось, но не прекратилось! За это время (хотя в основном – за последние полгода) была проделана довольно большая работа, о результатах которой я расскажу в новом цикле статей.

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

Почему же так долго

Итак, как же так получилось, что статей не было больше двух лет? Ведь изначально предполагалось, что следующий этап работ – набросать простой прототип менеджера игровых состояний – займет от силы пару месяцев.

Причин этому несколько. Причина номер ноль – это, конечно же, банальное отсутствие времени. Работа над проектом идет нерегулярно, в качестве хобби, и не всегда есть время и желание заниматься этим. Но это проблема общая, неспецифическая. Гораздо большую роль в данном конкретном случае сыграли следующие два фактора. Во-первых, изначальная концепция реализации игровых состояний оказалась абсолютно нежизнеспособной. Во-вторых, я так увлекся, что вместо того, чтобы сделать просто прототип менеджера состояний, принялся реализовывать прототип всего игрового движка. Но обо всем по порядку.

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

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

Ко всему этому добавляется идея о том, что состояние сообщается с внешним миром только посредством сигналов от разных систем ввода, к которым относится в том числе и система измерения времени.

"Быть может, такой менеджер состояний будет немного слишком сложным," - думал я тогда. Это оказалось не так. Такой менеджер состояний чертовски сложен!

Мало того, что вокруг банального паттерна проектирования State (Состояние) нагромоздились конечный автомат со стеком и фабрика, создающая объекты состояний – к этому добавилась еще и зубодробительная идея подключения и отключения объектов состояния к разным входным устройствам с помощью какой-нибудь разновидности сигналов.

Именно эта последняя идея и вбила последний гвоздь в крышку гроба всей концепции – я так и не смог разобраться с какими-то тонкими моментами измерения времени в пограничных режимах (кажется, это были сложности с лежащими на стеке состояниями – после их извлечения было очень сложно обеспечить более-менее точное измерение времени, основанное на сигналах).

Так и не сумев придумать работающую реализацию этой концепции, я забросил проект примерно на год. За этот год я уже успел забыть, в чем были проблемы, и наконец подумал: "А не попробовать ли мне еще раз?" Я снова взялся реализовать все с нуля, наступил на те же грабли, помучился пару месяцев, и понял, что надо перекраивать все начисто!

Принцип KISS (Keep it simple, stupid!) сработал, и "менее академическая", "менее абстрактная", но, туды в качель, более простая и понятная концепция организации состояний (о которой ниже) наконец сработала!

Примерно в сентябре-октябре 2015 года наконец началась работа над вполне вменяемо спроектированным менеджером состояний. Правда, в первой половине прошедшего 2016 мне было откровенно не до игры, поэтому работа над проектом застопорилась на несколько месяцев, но уже с лета она возобновилась, и с тех пор продолжается без существенных задержек.

Наконец, несколько слов о том, во что вылился "прототип менеджера состояний".

Если в трех словах – то в прототип игрового движка. Нормального такого, почти "взрослого" движка, на котором когда-нибудь можно будет писать серьезные игры. С одной стороны, это, конечно, хорошо. А с другой…

Вместо того, чтобы просто проверить работоспособность концепции одного несчастного менеджера (версии 2.0 – для версии 1.0 полная неработоспособность все-таки была проверена малой кровью), а все остальные элементы реализовать по принципу "бац-бац и в продакшн", я пошел всерьез прорабатывать различные подсистемы движка.

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

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

В конце концов я пришел к тому, что реализовываю прототип игрового движка уже сейчас. Конечно, в нем по-прежнему нет и не предвидится в ближайшей перспективе довольно многих вещей, которые должны быть в любом полноценном игровом движке (например, звук), но это уже неплохое начало. Впрочем, о том, что все-таки есть в движке, я расскажу позже.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 15.01.2017, 14:31   #97
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Проект возобновлен - идет работа над игровым движком

Концепция поменялась

А пока рассмотрим, какие существенные изменения претерпел проект за все это время.

Менеджер состояний

Пожалуй, наибольшие изменения коснулись именно этого аспекта. По сути, почти все базовые принципы, упомянутые в этой статье (за исключением первого, постулирующего наличие нескольких различных состояний) были отвергнуты. Рассмотрим их по порядку.
  • Состояния ничего не знают друг о друге. Поразмыслив, я решил, что ничего плохого в знании о наличии других состояний нет. Тем более, что в чистом виде это незнание все равно не было бы реализовано – состояния должны были посылать управляющие сигналы конечному автомату, которые по сути напрямую бы транслировались в некоторый идентификатор следующего состояния.
  • Логикой перехода между состояниями управляет менеджер состояний. Теперь текущее состояние само говорит, каким будет следующее состояние. Менеджер просто хранит у себя все состояния и переключается с одного на другое.
  • Менеджер работает по принципу конечного автомата. Вновь не то. Менеджер просто переключает на то состояние, которое ему сказали. Да, собственно говоря, вряд ли реализованный конечный автомат работал бы по какому-то сценарию, кроме: пришло сообщение "перейти в состояние Х" - состояние сменилось на Х.
  • Состояние первично, игровой цикл вторичен. Вместо мудреной реализации этой идеи мы теперь имеем нормальный игровой цикл, в котором просто вызывается функция update соответствующего состояния. А на случай, если состояние должно всего лишь один раз выполнить какую-то работу, а не повторять итеративно одни и те же действия, есть функция-член класса состояния start, которая вызывается в момент переключения на это состояние. Именно в этой функции и происходит вся работа, а update остается просто пустой – пусть вызывается сколько угодно раз, не жалко.
  • Менеджер состояний содержит стек, чтобы можно было на время перейти в другое состояние, и затем вернуться назад. Вообще-то я пока не дошел до реализации сценария, в котором это может быть нужно. Но простая логика подсказывает, что я спокойно обойдусь и без этого – см. следующий пункт.
  • При переключении в другое состояние экземпляр старого уничтожается, и создается экземпляр нового. Пожалуй, постоянно уничтожать-создавать состояния – это слишком брутально. Поэтому я просто храню их все (в ассоциативном массиве, по ключу-перечислению), а активное состояние просто отмечено (умным) указателем. Заодно это избавляет от необходимости использовать стек состояний, чтобы придержать в памяти временно неактивное состояние.
Подробнее и более последовательно я изложу устройство менеджера состояний в последующих статьях.

std:function вместо самописных функторов

В предыдущей статье я также много распинался по поводу того, что нужно реализовать обобщенные функторы по образу и подобию тех, о которых писал Александреску. Но дело в том, что писал он о них во времена первого стандарта C++, а начиная с C++ 11, как мне верно подсказали, появилась такая вещь как std::function.

В результате всякая необходимость в написании собственных велосипедов отпала. Как и любой компонент стандартной библиотеки, std::function более гибок, эффективен и стабилен, чем кустарный аналог. Не говоря уж о том, что как, скажем, реализовать функцию std::bind – я до сих пор слабо себе представляю.

Асинхронность на основе задач (Task) вместо потоков

Использовать такую низкоуровневую вещь, как потоки (thread) для асинхронного выполнения какого-то кода – слишком хлопотно. Поток нужно создать, запустить, остановить… Гораздо проще сказать: "сделай мне асинхронно то-то и то-то". В большинстве простых ситуаций этого будет более чем достаточно.

Задачи (Task), они же действия (Action) как раз и представляют такую концепцию – вы просто говорите, что нужно сделать (вызовом функции doAsync, которой передаете либо Action, либо просто std::function), и это действие асинхронно выполняется.

Под капотом все происходит довольно просто – мы не создаем/уничтожаем потоки (что порой может быть очень накладно по вычислительным ресурсам – для очень простых действий время на создание/уничтожение потока может на порядки превышать время выполнения полезной работы), а берем свободный поток из пула (thread pool). В этом потоке выполняется нужное нам действие, по завершении которого поток возвращается в пул и становится доступным для выполнения другой работы.

Впрочем, не буду строить из себя гуру асинхронного программирования. Опыта в этом деле у меня по-прежнему не так много, и разработка игры – как раз неплохая практика.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Старый 15.01.2017, 14:31   #98
Гром
Старожил
 
Аватар для Гром
 
Регистрация: 21.03.2009
Сообщений: 2,193
По умолчанию Проект возобновлен - идет работа над игровым движком

Continuous Integration + github

Если уж проект находится под контролем версий (с использованием Git, как я писал ранее), то почему бы не выложить его в публичный репозиторий? Так я и сделал – создал репозиторий на github и синхронизировал его со своим локальным репозиторием.

Но это далеко не конец истории. Раз у нас есть публичный репозиторий, да еще и юнит-тесты для проекта, то если добавить к ним сборочный сервер, то можно приготовить блюдо под названием непрерывная интеграция (continuous integration, CI).

Вообще-то, CI – это когда над проектом работает несколько разработчиков, периодически сливающих свои наработки в общий репозиторий, а где-то имеется специальная штука, периодически вытягивающая код из этого репозитория, автоматически собирающая проект, прогоняющая тесты и разворачивающая получившееся приложение.

Это позволяет оперативно выявлять ситуации, когда кто-то из разработчиков что-то сломал в проекте (залил нерабочий код; сломал тесты; залил код, конфликтующий с наработками других разработчиков; недозалил часть изменений). При таком режиме работы симптомами, свидетельствующими о проблеме, становятся либо ошибка компиляции, либо проваленные тесты, либо ошибка деплоя собранного приложения.

Конечно, поскольку над проектом я работаю один, то ни о каких конфликтах между кодом разработчиков речи не идет; заливка нерабочего кода тоже представляется довольно маловероятной, т.к. я все-таки обычно проверяю его работоспособность на своей локальной машине.

Но все-таки даже в таких почти тепличных условиях CI позволяет выявить кое-какие проблемы. Прежде чем вдаваться в конкретику, я вкратце опишу, как непрерывная интеграция реализована у меня.

В качестве сборочного сервера я использую Jenkins. Выбор на него пал практически случайно – в первую очередь это, вроде бы, просто неплохая система. Стоит он на виртуальной машине, работающей под Ubuntu (при этом код я пишу под Windows).

Для юнит-тестов, как я ранее писал, используется Google Testing Framework (Google Test). Удачно то, что формат файла, в который выводятся результаты прогона тестов, взят у такого для JUnit, который является родным для Jenkins, поэтому подружить их было не так сложно. Где-то в сети была статья на английском, которую я использовал при настройке сервера CI, но ссылка на нее у меня не сохранилась. Заработало – и ладно.

Как именно я его настраивал, я не помню, поэтому просто приведу список своих нынешних настроек для проекта. С именем проекта, системой контроля версий и адресом репозитория все понятно. Список веток, которые собираются прост: */master (хотя я подумываю о том, чтобы сделать отдельную ветку для сборки, чтобы в master попадало только то, что прошло проверку дженкинсом).

Шагов сборки – два. Первый собственно собирает проект (помним, что я использую библиотеку Qt):

qmake
make


Второй запускает тесты и публикует результаты в файл:

cd Test
./Test --gtest_output="xml:./test-result.xml"


Послесборочный шаг один – опубликовать данные из этого отчета (путь к файлу, соответственно – Test/test-result.xml).

В результате, поскольку код пишется на одних ОС и компиляторе, а проверяется на других, я получаю, во-первых, почти гарантированную кроссплатформенность (во всяком случае, я знаю, что код запускается под другой ОС, и даже тесты под ней проходят; хотя вообще-то само приложение я еще ни разу не запускал под Linux); во-вторых, более полную проверку корректности кода – если один компилятор посмотрит на какую-то ошибку сквозь пальцы, второй наверняка на нее заругается.

Оба пункта так или иначе уже проверены на практике. Вначале копирование файлов ресурсов игры в нужную директорию неправильно работало под Linux, из-за чего сборка на сервере упала – в результате ошибка была исправлена. Кроме того, пару раз приходилось исправлять тест-кейсы, связанные с асинхронной работой – почему-то на Linux эта работа происходила слишком быстро (я не стал разбираться в причинах, просто увеличил длительность операции).

Что касается ошибок компиляции, то мой рабочий компилятор уже дважды проглатывал некорректную ссылку на функцию-член (MyClass::func вместо правильного &MuClass::func), а компилятор на сборочном сервере ее отлавливал. Мелочь, а приятно.

Что дальше

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

Кроме того, я планирую затронуть работу с git и github, а также какие-нибудь общие темы работы над проектом.
Простые и красивые программы - коды программ + учебник C++
Создание игры - взгляд изнутри - сайт проекта
Тема на форуме, посвященная ему же
Гром вне форума Ответить с цитированием
Ответ


Купить рекламу на форуме - 42 тыс руб за месяц

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Электронно учебное пособие gloomy_jr Общие вопросы Delphi 1 23.05.2012 14:07
Создание игры::особенности коллективной разработки флеш приложений АТИКОН Gamedev - cоздание игр: Unity, OpenGL, DirectX 9 21.08.2011 19:51
Мультимедийное учебное пособие world12_tk Помощь студентам 4 21.04.2011 17:37
статья - Может-ли ПО работать быстрее или взгляд изнутри Pblog Обсуждение статей 0 27.02.2011 23:10
Электронное учебное пособие Zeibel Помощь студентам 10 31.05.2010 10:55