В C++17 предлагают добавить еще средств для поддержки асинхронности. Например, в виде ключевого слова await. На isocpp.org недавно проскочила ссылка на статью с демонстрацией якобы удобства await в более-менее приближенном к реальности коде из области игростроения: Await, coroutines, what could that bring for game development. Там же, рядом со статьей, есть код демонстрационного проекта, можно скачать посмотреть повнимательнее.
Так вот, толи лыжи реально не едут, толи я идиот, но что-то при попытке разобраться в деталях, что и как будет работать, какой-то простоты при использовании await и короутин заметить не удалось :(
Единственное место, в котором задействовано новое ключевое слово __await из Visual Studio 2015, это вот эта функция, определяющая игровую логику:
GameAwaitableSharedPromise<void> gameLogic(Engine* engine) { auto animatedText = std::make_shared<AnimatedText>(); engine->addSceneObject(animatedText); std::default_random_engine re((unsigned int)(std::chrono::steady_clock::now().time_since_epoch().count())); std::uniform_real_distribution<float> dist(0.0f, 0.7f); while (true) { __await animatedText->fadeIn(); __await engine->waitForMouseClick(); __await animatedText->fadeOut(); engine->changeBackground(DirectX::XMFLOAT4(dist(re), dist(re), dist(re), 1.0f)); __await engine->waitFor(duration_cast<steady_clock::duration>(.5s)); } } |
Автор статьи утверждает, что подобный код легче воспринимать, чем код, написанный в стиле конечных автоматов. Попробовал разобраться. Что-то как-то не верится.
Суть этого кода в том, что в цикле нужно выполнять простые действия: сначала посредством анимации отобразить сообщение, затем дождаться пока пользователь кликнет мышкой, затем посредством анимации убрать сообщение, затем поменять цвет фона, затем подождать 0.5 секунды и повторить все снова.
Как я понял, автор ведет к тому, что запустив все эти действия в виде короутин, которые где-то как-то будут исполнятся, можно не блокировать нить, на которой крутится главный цикл функции gameLogic(), что позволит обрабатывать обычные Windows-сообщения (например, о нажатии на кнопку мыши, об изменении размеров окна, о необходимости перерисовки). А так же выполнять дополнительные действия, которые должны выполняться в игре (например, перемещать персонажей на экране).
В теории все это представляется красиво. Наверное.
Но вот попытавшись разобраться в деталях, начинает казаться, что благими намерениями таки вымощена дорога в ад спагетти-кода и излишней связности программных сущностей :(
Например, в исходниках обнаруживается, что в приложении есть старый-добрый цикл обработки Windows-сообщений. Да-да, тот самый while с PeekMessage(), TranslateMessage() и DispatchMessage() внутри. А еще голая WndProc, коих я лично не видел уже очень и очень давно :)
Далее выясняется, что для поддержки "фоновой" активности игры внутри цикла обработки сообщений регулярно дергается метод run() некоторого Engine. А потом выясняется, что этот Engine заточен под конкретную бизнес-логику, что выражается, например, в наличии методов waitForMouseClick() и changeBackground(). Более того, и главный цикл обработки Windows-сообщений так же плотно завязан на Engine, т.к. при получении WM_LBUTTONDOWN нужно дергать метод onClick(), который должен привести к запуску короутин, завязанных на ожидание клика мышкой.
Код в run() знает, что у него есть таймеры, которые должны отсчитывать свое время, есть игровые объекты, состояние которых должно обновляться, а потом еще что-то должно перерисовываться:
void run() { _clock.onBeginNewFrame(); for (auto it = _activeTimers.begin(); it != _activeTimers.end(); ) { auto next = it; ++next; if (it->onTick(_clock.currentFrameTime())) { _activeTimers.erase(it); } it = next; } for (auto& obj : _sceneObjects) { obj->updateState(_clock); } _ctx->ClearRenderTargetView(_rtv.Get(), (float*)&_bgColor); _ctx->OMSetRenderTargets(1, _rtv.GetAddressOf(), nullptr); UINT vpCount = 1; D3D11_VIEWPORT vp; _ctx->RSGetViewports(&vpCount, &vp); for (auto& obj : _sceneObjects) { obj->draw(_ctx.Get(), _rtv.Get()); } _swapchain->Present(0, 0); } |
Тот самый анимированный текст, который должен появляться и исчезать, он и есть один из игровых объектов, для которых внутри run() вызывается updateState(). А метод updateState() для AnimatedText превращается во что-то вроде:
void AnimatedText::updateState(const GameClock & clock) { if (_opacityAnim) { bool animEnded; _opacity = _opacityAnim->update(clock.lastFrameDuration(), animEnded); } if (_transformAnim) { bool animEnded; _transform = _transformAnim->update(clock.lastFrameDuration(), animEnded); } } |
Где конкретный update может выглядеть как-то вот так (если я правильно разобрался в исходниках):
T update(const std::chrono::steady_clock::duration& ellapsed, bool& ended) { if (_ellapsed >= _duration) { ended = true; return _endValue; } _ellapsed += ellapsed; if (_ellapsed >= _duration) { ended = true; // the animation just ended, raise the coroutine completion callback _promise.setResult(); } else { ended = false; } float progress = ((float)_ellapsed.count()) / ((float)_duration.count()); if (progress > 1) { progress = 1; } progress = _interpolation(progress); return lerp(_startValue, _endValue, progress); } |
Т.е. конкретный update -- это не что иное, как тот самый конечный автомат с состояниями и одним входным воздействием.
Что еще смущает: всем этим SceneObject-ам нужно вести отсчет времени, для чего в метод updateState() передается ссылка на GameClock. А ведением этого GameClock() занимается Engine. Т.е. между SceneObject-ами и Engine устанавливается еще одна довольно жесткая связь.
Ну а поскольку метод run() для Engine вызывается из цикла обработки Windows-сообщений, то и сам Engine, и принадлежащий ему GameClock -- суть те же самые конечные автоматы.
Получается, что внутри игры все самые важные объекты есть КА, но в одном месте действия части КА оформляются посредством асинхронных операций с короутинами. Якобы для того, чтобы бизнес-логика выглядела "прозрачно". Для чего КА должны выставлять наружу некие GameAwaitablePromise. Т.е. все сущности внутри игры оказывают связаны не только знаниями об Engine и GameClock, но еще и об GameAwaitablePromise.
Собственно, пора переходить к сути поста. Понятно, что я уже не могу учиться так же быстро, как мои молодые коллеги. Посему новомодные и перспективные штучки заходят тяжело и вызывают серьезный скепсис. Плюс к тому я профессионально деформирован долгими годами работы с агентами в виде конечных автоматов и явном обмене асинхронными сообщениями. В итоге этот более-менее реальный код с использованием await мне совсем не кажется простым и понятным. И что использование await для таких задач -- это перспективно. Скорее, это "наведение тени на плетень", а не написание понятного, надежного и сопровождаемого кода.
Но, т.к. я, с большой вероятностью, ошибаюсь, хотелось бы услышать: а есть ли среди C++ников (и не только C++ников), которые читали эту статью и смотрели этот код, люди, которые согласны с автором статьи? Что такое использование await действительно улучшает код и упрощает разработку?
Комментариев нет:
Отправить комментарий