読者です 読者をやめる 読者になる 読者になる

オルトプラスエンジニアの日常をお伝えします!

Reactive Extension in C++

※この記事は「Altplus Advent Calendar 2016」の10日目の記事です。

こんにちは。id:mitsutaka-takeda です。

今年はVRゲームの開発に参加してきました。VRゲームの開発はUnityで行っています。Unity開発の中でUniRx(Reactive Extension for Unity)に触れる機会があり、複雑な処理も簡潔に記述できるReactive Extensionの魅力にすっかり魅せられてしまいました。

UniRxに関する素晴らしい入門・解説の記事は多くあるので、そちらの方を参照していただくとして、今日はReactive ExtensionのC++実装であるRxCppの紹介をしたいと思います。

RxCppはMicrosoftのKirk Shoopさんによって開発されているOSSです。

RxCppを使用すると「時間的に分散した値」に対して、アルゴリズムを書けるようになります。「時間的に分散した値」というと、何やらよくわかりませんが、Webサーバへのリクエスト・レスポンス、ユーザの入力(マウス・クリックなどのイベント)が「時間的に分散した値」の例です。

Webサーバからのレスポンスは複数回に分かれてサーバから送られてくるかもしれません。ユーザはいつ・何回マウスをクリックするかわかりません。このように時間的に離れているタイミングで発生する値に対して、値の変換、フィルタリングなどアルゴリズムを記述できるようするためのライブラリがRxCppです。

サンプル・プログラム

以下、「マウスの位置を中心に赤い円を書く」簡単なサンプル・プログラムで、RxCppを使用したプログラミングがどのようになるか見ていきます。

プログラムにはSiv3DというC++のゲーム・メディアアート用ライブラリを使用します。

まずは、RxCppを使用しないSiv3Dのコードです。簡潔で素晴らしいですね。System::Update関数がグラフィックスやマウスの現在位置を更新しています。Mouse::Pos関数で現在のマウスの位置を取得して、マウスの位置に半径50のCircleオブジェクトを生成しCircle::draw関数で赤色の円を描画しています。

# include <Siv3D.hpp>

void Main()
{

  while (System::Update())
  {
    Circle(Mouse::Pos(), 50).draw({ 255, 0, 0, 127 });
  }

}

同じコードをRxCppで書くと以下のようになります。もとのコードより大分長くなりました。。。framebus、frameout、frameは、RxCppを毎フレーム駆動するためのおまじないです。RxCppによるプログラミング・ロジックのコアは、Rx::mapとsubscribe関数になります。Rx::mapでフレームを現在のマウスの位置に変換しています。subscribe関数でこのマウスの位置に円を表示します。このsubscribe関数のパラメータpに渡される値は各フレームでのマウスの現在位置なので、その値に対してCircle::drawで円を表示するだけになります。

#include "Siv3d.hpp"
#include <chrono>
#include "rxcpp/rx.hpp"

namespace Rx {
using namespace rxcpp;
using namespace rxcpp::subjects;
using namespace rxcpp::operators;
using namespace rxcpp::util;
}

void Main()
{
  Rx::subject<int> framebus;
  auto frameout = framebus.get_subscriber();
  auto frame = framebus.get_observable();

  auto mouse_pos 
  = frame
    | Rx::map([](int) {
        return Mouse::Pos();
    });

  auto subscription = mouse_pos.subscribe([](s3d::Point p) {
    Circle(p, 50).draw({ 255, 0, 0, 127 });
  });

  while (System::Update())
  {
    frameout.on_next(1);
  }

  subscription.unsubscribe();
}

さて、ただ現在のマウスの位置に円を表示するだけだと退屈なので、少し時間差(100ms後)で円がマウスの位置を追いかけるようにしましょう。サッカーをテレビで見ているときボールの位置を追うカメラが少しボールを遅れて追ってる感じを実装していると思ってください。

RxCppを使用しない場合、whileループの外でマウスの位置を覚えるための変数を定義して、whileループでは、そこに現在のマウスの位置を保存、かつ、100ms前の位置を読み込んでそこに円を表示する処理になるdしょうか。状態管理が難しそうです。コードは書きません(パット書けません)。

RxCppを使用したコードは以下の通りです。変更箇所は1か所だけです。Rx::mapで現在のマウス位置を取得した後に、Rx::delay演算子で値を100ms遅らせています。subscribe関数では受け取ったマウスの位置を表示するだけで、それが100ms遅れているかは関知しないため、変更は不要です。

#include "Siv3d.hpp"
#include <chrono>
#include "rxcpp/rx.hpp"

namespace Rx {
  using namespace rxcpp;
  using namespace rxcpp::subjects;
  using namespace rxcpp::operators;
  using namespace rxcpp::util;
}

void Main()
{
  using namespace std::chrono_literals;

  Rx::subject<int> framebus;
  auto frameout = framebus.get_subscriber();
  auto frame = framebus.get_observable();

  auto mouse_pos 
  = frame
    | Rx::map([](int) {
        return Mouse::Pos();
    })
    | Rx::delay(100ms);
  
  auto subscription = mouse_pos.subscribe([](s3d::Point p) {
    Circle(p, 50).draw({ 255, 0, 0, 127 });
  });

  while (System::Update())
  {
    frameout.on_next(1);
  }

  subscription.unsubscribe();
}

100ms値の発行を遅らせるといった複雑な操作をdelayのようにアルゴリズムとして独立させることで、再利用性が高くなり、コードの意図が明確になることが分かります。

複雑なUIを提供するアプリケーションやデータのストリームを扱うデータ解析などのアプリケーションではReactive Extensionの手法がこれから一層浸透していく思われます。C++プログラマの皆さまもぜひRxCppでReactive Extensionに入門されるのはいかがでしょうか。

RxCppの作者Krik Shoopさんが、最近RxCpp & Twitter APIを利用してツイートを解析するGUIアプリの解説ブログを始めました。RxCppを利用した完全なアプリケーションがどのようになるか、興味があるかたはチェックしてみてください。

Realtime analysis using the twitter stream API