高速なドロネー三角形分割

f:id:hadashia:20181011194454g:plain

「コンピュータジオメトリ」という書籍を参考にして、これに載ってるドロネー三角形分割のやりかたを実装してみた。

この本は、幾何アルゴリズムの「証明」や、計算量の解析が載っていて、しかも日本語で書かれているという徳の高い本。
理論どおりの計算量に近付けるための工夫というか、データ構造のもちかたも載っていて、その辺もまったく知らなかったので初学者にとっては貴重な情報源な気がする。
でもけっこう難しい。空間的な操作の翻訳の日本語での説明は、同じところを4回くらい読むという手法でなんとか読みすすめられたたけど、数式や知らない定理がでてくるところがけっこうあって、そっちの知識が不足しているからがんばってなんとか読めたかんじだった。

三角形分割は、好きな形状のなにかを画面に描きたいとおもったときに必要になりそうだったので、実装してみたかったアルゴリズム。 この本で紹介されているドロネー三角形分割は、隣接する三角形の辺を交換するというただそんだけの操作を再帰的にやるだけで実装できるため、総当たりな探索をほぼやらずににけっこう高速に実装できる余地があるものだった。 その辺の実装についてすこしだけ紹介してみようと思う。

ドロネー三角形分割とは

ドロネー三角形分割は、任意の点を与えてあげると、それらを線で結んで、三角形が敷き詰められた状態にするアルゴリズム

ドロネー三角形分割と呼ばれているけど、ボロノイ図のセル同士をあるルールで線で結んだ「ドロネー図」というグラフがあり、これをよく見ると、角度最適な三角形の集合になっとる、という特徴を持っている。ということらしい。
つまり、ドロネー三角形分割をするということと、ドロネーグラフを描くということはほぼ同じことのようです。

角度最適な三角形分割

任意の点の集合から三角形分割を得るには、結果はひとつではなく、幾通りかの結果がありえる。

また、そのうち、どの三角形分割が望ましいか、かっこいいか、についても、色々な基準があるみたいだけど、ドロネー三角分割は、「三角形の最小角度が最大になる」分割方法になる。

反対に、三角形の最小角度が最大になる三角形分割は、ドロネーグラフであることの必要充分条件であるので、これをアルゴリズムの判定につかう。

ある三角形が、ドロネーグラフの一部かどうかの判定は、ボロノイ図の性質を応用すると、その三角形の外接円の内部に他の点が含まれているかどうか? を調べるだけでわかる。
しかし、この判定のやりかただと、どの点を調べるべきかどうかが定まらないので、比較対象の点が多くなってしまう。
そこで、「三角形が不正かどうか」ではなく、「辺が不正かどうか」という概念を導入する。

不正な辺とは、もしその辺を「フリップ」して三角形をつくりなおした場合に、より角度が最適な三角形になる場合のことを言う。
辺フリップとは、つまり、その辺を一旦ぶち消して、2つの三角形を1つの四角形に戻した後で、別の対角線をつかって分割をやりなおす操作。

「不正な辺」かどうかの判定をするには、その辺と隣接している三角形だけが対象になるので、総当たりで探索する必要がなくなる。

分割の途中経過を有向グラフで管理する

もうひとつおもしろいと思った工夫が、三角形分割した結果だけをメモリに持つのではなく、分割する過程ででてきた親の三角形や、辺フリップ前の不正な三角形もとりあえずメモリにすべて残しておいて、どんな道筋で分割されてきたからの過程もすべてグラフで管理しておく。というもの。

このデータ構造をもっておくことによって、ある点が含まれている三角形を調べたいときに、ぜんぶの三角形を調べなくてもよくなる。 上から辿っていき、ヒットした三角形に子ども(つまり分割後や辺フリップ後の三角形)が存在したら、そっちも調べる。このやりかたを導入すると、高々グラフの高さの回数だけ判定すれば良くなる。

流れ

アルゴリズムの全体の流れ実装には、逐次添加法を使う。 三角形分割したい点の集合のうち、点をひとつずつ添加していて、その時点での三角形分割をつくっていく。

  1. 点をひとつ追加する
  2. 新しく追加された点を含んでいる三角形をみつける
  3. その三角形を、新しい点が頂点になるように3つに分割する (点が辺の上にある場合は4つに分割する)
  4. 分割してできた三角形は、新しく追加された辺を3つ含んでいる。この3つの辺が「不正な辺」かどうか判定する
  5. 「不正な辺」を、辺フリップする
  6. 辺フリップすると、新しい辺が生まれるので、この新しい辺についても「不正な辺」かどうか判定し、もしそうなら辺フリップする
  7. これを再帰的におこなう

サンプル

一応、このやりかたで実装してみたサンプルがこれ。

github.com

最近の学習計画

なんやかんやあって、ずっとやっていたWebサービス開発から、オンラインゲーム開発に仕事を変えた。
最近は、なんとか呼吸できるくらいには慣れてきたかなって思って気がついたら2年くらい経っていた気がする。
結局ソフトウェア開発だから、そこだけ聞くと、そんなに変わらないやん、と感じる人もいると思うけど、やるべきことや学習していくべきこと、方法論がけっこう違うと自分としては考えている。

今いる会社では、スマートフォン向けのオンラインゲーム開発を主な事業としている。
今やスマートフォンというのは、3Dの絵をリアルタイムに描くことができ、多少はリアルタイムにライティングをしたりとか色々な表現をするようなゲームが動く。びっくりである。
プロジェクトにもよるけど、自分の会社でもグラフィックに力を入れているプロジェクトは、なんというか、グラフィックに力を入れており、手描き風の品の良い絵にハッチング(線を重ねたような影?)の表現を取り入れた絵をリアルタイムにスマートフォン上で動かし、しかも多人数でオンライン同時プレイできるようなタイトルを出してる。
こうなってくると、昔ほど、携帯ゲームとコンシューマゲームで必要になる技術がまったく別物というわけでもなく、技術的には距離が縮まっていると言ってもよさそうである。実際、子どもの頃から名前を知っているようなゲーム会社が前職だった人というのもちらほらいる。

Webサービス開発をやっていた頃は、技術というのはコモディティ化するものだった。つまり、ある特定の製品でしかもっていない技術、というものが、全体でみるとそれほど重要ではないというか、目立たないというか、かなりのスピードで周辺の界隈が同じことを再現できるように同質化してしまう。そんな力が働いていた。
Webサービス開発において、有名なソフトウェアというのは、そのソフトウェアでないとつくれないものがつくれる、といった類のものじゃなくて、皆がつくっているものを圧倒的に効率化するアイデアや思想が含まれているもの、あるいは、パフォーマンスや開発コストを改善するもの、だったような気がする。
そもそもWebというものが、誰に対しても開かれていて、そこで開発をするということは、そこで特定の何かを利用するということは、それにbetする、ということであって、利用するだけではなく、発展させるような行動を取ることが基本戦略になっていたのかな。

そんななかで暮らす一兵卒エンジニアとしては「学習する」ということは、つまり、Web開発に参加する世界中の人、Googlefacebook,Appleをはじめとする界隈の巨人がリードし整備する周辺ツールや環境を知ること、最新情報を追うこと、ソフトウェアの使い方を知ること、とほぼイコールだった。誰かがつくったものや説明理解するのが学習っていうかんじ。(自分のレベルではね)
また、自分で何か応用してものをつくるということよりも、既存のソフトウェアを発展させるような行動を取ることが「偉い」とされるバイアスがあったようななかったような。まあそれは、すごくわかりやすい物差しだったから色々と利用されていた、というだけかもしれないけど。

まあ、それはいいんだけど、自分がゲーム開発はすこし違うと感じているのがこの点である。
まず、ことゲーム開発においては、エンジニアは「ツールやライブラリの最新情報を追っている、つかいかたを知っている」というレベルでは、つくりたいものをつくるのは難しいのではないかと感ずる。
そのゲームごとに、画面に出したい絵や、プログラマブルに実現したいこと、するべきことはそれなりに違っていると思うし、ハードウェアの制約によって省かなければいけなくなることも違ってくる。そういうとき、必要になってくるのは、ブラックボックスの中身とかアルゴリズムとか既存のテクニックの知識と、もっと大事なこととして、それをどう応用するかにかかっているという気がする。
そういう仕事にあこがれているのが、自分が仕事を変えてみた理由だった。

今のところ、つくりたいものがつくれるレベルにはまったく達していないし、学習のペースもむちゃ遅いんだけど、自分は基礎をすっとばしてきたので、もうちょい基礎をやりたいなあというかんじが現状。
とにもかくにも、吸う空気を変えたりした結果、学習すべきことがわかってきたといえるくらいにはなってきた気がする。
まず、もっとも大事なことは、良質な情報を読解する能力だと考えてる。これを上げていくと、全体的な学習する速度が上がるのでいろいろな能力が伸びるのが速くなる。また、世の中では、設計については広く解説が広まっていくけど、実装については、皆が皆紹介してくれるわけではないので、良質な情報をみる力がないとなんも読むものがなかったりする。

良質な情報を読解する力をつけるための基礎の基礎として、まず英語で、この分野はやっぱり英語じゃないとくわしい説明がないかんじがする。あと数学。数学は、大事なのかなとおもって細々と練習しているのだけど、意外となにをどう勉強すればいいのかわからなくなるのがはやかった。いろいろ読んでみて、最近はもうちょい解析という分野をやると、アルゴリズムとかグラフィック関連の内容を読解する能力が上がるわ。ということにようやく気がついた。「そんなの最初からわかるやろあほか」と思われるかもしれないけど、まあそういうのもわからない、そういう情報を得ることさえできてないレベルだったのがちょっと進歩してきたというかんじなのである。
ちなみに、Web屋に従事していた頃は、設計パターンや思想をすごく熱心に文学だとおもっていろいろ読んでいて、それは今でも役にたっているけど、設計よりも「実装」の知識が必要になる機会はやっぱ多いといっても良い気がする。サーバサイドの仕事もしてるけど、Socket のhalf connection 問題とか、c#ランタイムの中身とか、ヒープアロケートが発生する箇所とか、マルチスレッドでありがちな誤ちとか、Web開発をしていたことはそういうのは使ってるミドルウェアの仕事で、考える必要がそんなになかったような気がする。そして今挙げたトピックは、設計の問題というより実装の問題で、誰もが広く解説できるわけではないので、まともなドキュメントをがんばって読むのが一番はやい。
そんなかんじで、転職してからこれまでは、基礎をやるのとかインプットするのとかをしてきた。
あとは空気を吸ってるだけであっという間に時間が経ってしまって、「これつくった」て言えるものがいまだにとくにないんだけど、自分のなかの計画としてはまあそんなもんなかなと思っている。 勉強するやりかたがわかってきただけでも儲けものとおもってる。その程度の奴だしね。
でも、この後は、もっと得た情報を元に応用していかないといけないので、読んで理解するだけでは不十分で、知識を応用して分解して再構築して遊んでみないといけないことはたしかであるから、そろそろなにかつくって公開とかしていきたい、できたら良いなとおもってる。

ここになんか書いたりはそろそろしてみようかなとおもってる。

c# と非同期処理ついての初歩的なFAQ

c# でサーバを書くのおすすめです。処理の並列化がとてもやりやすいです。

golangが登場したとき、小さなコード辺をスレッドを意識せずに投げまくれる上、タイマーやI/Oをスレッドをブロックせずに使える、そんなお手頃価格な並列処理に注目が集まっていたように思いますが、それ、c# もできます。昔から。Taskとそのスケジューラ、さらにそれらを文法に統合する基盤である async/await 。

c# も、.NET Core の登場でブランドイメージが一新されようとしており、linuxでも普通に使えるしDockerコンテナも公式から配布されているし、開発もGithubで公開、ドキュメントの日本語も流暢になってる。 ふと気がついたら、サーバと名のつくものは全てc#でまかなえる時代がいつのまにか来てます。

ところで、c# でサーバを書く場合、非同期処理の挙動を意識する機会が多かったりするので、自分が入門するときに持った疑問点をまとめてみようと思います。

疑問1 awaitで非同期処理を待つと、続きは元のスレッド再開する?

答え: 場合による

c# には、デフォルトでは「メインスレッド」という概念があるわけではありません。await文も、いずれかのスレッドを特別扱いする機能ではありません。

await で非同期処理を待った後、どのスレッドで再開するかは、フレームワークに依存します。 もうちょっと正確に言うと、実行コンテキストの設定に依存します。

関連してくる設定は主に2つです。

  • TaskScheduler → Task がどのように実行されるかスケジュールする
  • SynchronizationContext → await 後の続きの処理がどのように実行されるかスケジュールする

特に明示しない限り、スレッドごとに設定されている TaskScheduler.CurrentSynchronizationContext.Current の値が自動的に使用されます。 (ちなみに、こんなかんじで、スレッドごとにどこからでも参照可能な設定に挙動が依存するパターンを Ambient Context と呼んだりします)

Task とは?

Task は、処理のかたまりを表現したオブジェクトです。 多言語でいうところの Promise や Future と呼ばれているものと思ってもらって差し支えないと思います。 「未来に実行されるかもしれないし、実行中かもしれないし、完了してるかもしれない」そんな抽象的な処理のカタマリをオブジェクトとして表現することで、非同期処理のチェインや、エラー伝搬なんかがわかりやすくなる強力なパターンです。

このTask ですが、それ自体は、どのスレッドでどのようにスケジュールされるのかはとくに決まっていません。 それを決めるのは TaskSchedulerです。

TaskScheduler.Current は デフォルトでは ThreadPoolTaskScheduler が使用されます。 これは、Task.Run(..) した場合、.NET の共通言語ランタイム(CLR) が持っているスレッドプールで実行されるというスケジューラです。(めっちゃ性能良い

ちなみにこのスケジューラは、完了の順番は保証されません。

Task.Run(() => Console.WriteLine("Im task1"));
Task.Run(() => Console.WriteLine("Im task2"));
Task.Run(() => Console.WriteLine("Im task3"));

// 出力例:
// Im task 1
// Im task 3
// Im task 2

SynchronizationContext とは?

SynchronizationContext を使うと、並行に走る処理たちを、ある一定のポイントで同期させることができます。

たとえば、別のスレッドからの要求であったり、そもそも別のネットワークのマシンからの要求みたいなものを処理するために、一定の間隔で要求が到着してないかチェックし、あれば実行してあげる、みたいなことが必要になってきますが、SynchronizationContext を使うとこうした振舞いを表現することができます。

SynchronizationContextは、 .NET 2.0 時代から提供されていて、await とは分離されているものですが、await の再開時に、継続行がどのようにスケジュールされるかを決めるものとして使われてるので、そのように紹介されてることが多い印象です。

SynchronizationContext.Current のデフォルト値ですが、 nullです。

これが何を意味するかというと、 c# の await は、デフォルトでは 同期を取ったりはしない、ということ。

つまり、別スレッドで走ったら走りっぱなし。メインスレッドには帰ってこない。

Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");

await Task.Run(() => Console.WriteLine("Im a task1"));
Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");

await Task.Run(() => Console.WriteLine("Im a task2"));
Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");

await Task.Run(() => Console.WriteLine("Im a task3"));
Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");

// 出力例:
// Thread: 3
// Im a task1
// Thread: 5
// Im a task2
// Thread: 4
// Im a task3
// Thread: 5

await で継続するたびに、実行スレッドがかわりました。

await は、内部的にはTaskとTaskのチェインなので、実行順番だけは保証されます。

しかし、どのスレッドに割り当てられるかは特に期待できないかんじです。

基本的には排他制御は必要ないわけですが、スレッドが変わってしまうということは覚えておくのが良さそうです。

疑問2. でも ASP.NET Core 環境では await すると元のスレッドで継続されるんでしょ?

答え: されません。もちろん処理の順番は保証されますが、元のスレッドで継続されるわけではありません。

現在の ASP.NET Core環境では、SynchronizationContext.Current はnullです。

ASP.NET Core SynchronizationContext この辺の説明に寄ると、パフォーマンスのためにリクエストスレッドでの同期というやりかたをやめたようです。

そもそも、ASP.NET Core は、1プロセスを多重化して 並行にリクエストを捌けるので、リクエスト時に、めちゃめちゃスコープが広いオブジェクトの状態を書き換えるようなことをするときは、排他制御が必要。ということになります。まあ、普通は状態を持つのは RDBMSとかRedisとかのちゃんとしたライブラリに任せるはずなので、意識する機会はほぼないかもいれませんが、知らないで シングルトンに書き込み可能な状態をつくってしまうとバグります。

疑問3. じゃあ await するとメインスレッドで継続される環境はあるのか?

答え: UI

UIはメインスレッドのメインループで描画が更新されるものなので、そういった環境では SynchronizationContextによって、メインスレッドから起動した非同期処理は継続時にメインスレッドに戻ってくるといった実装がなされているはずです。

つづく

c#の getter/setter はスレッドセーフか? とか、色々と書きたいことがまだあるんですが、ここからはTPLの話ではなくなってくるのでまた次回にしてみようとおもいます

キャットムル-ロム スプライン曲線

最近、ごく基本的なの曲線のアルゴリズムを実装したりしてる。

キャットムル-ロム スプライン(Catmull-Rom Spline)ていうアルゴリズムは、制御点を必ず通るという特徴があるので、絵を生成するような用途だととても使いやすい。しかも実装が簡単。 最近、これを実装する機会があったので、仕組みを説明してみようと思う。

スプライン曲線

ゲームのアルゴリズムの本をめくり、曲線について書かれているページを開くと、大抵は、次数がいくつであっても対応できる級数みたいなものの式が載っていて、難しそうである。

これはどうしてかというと、数学をもってすれば、めちゃめちゃ複雑な曲線でさえも、たった1つの式で表現できる能力があるし、数式は、低い精度から高い精度まで、どんな精度の近似にも当てはまる1つのルールを書き下すことも得意だから、一見すると、どうやって実装したら良いかよくわからない一般式が目に飛び込んできたりする。

しかし、目的が単に曲線を引くことだけであれば、全体の式がわからなくても、簡単な式をたくさんつなぎ合わせればできる。

中学校で習ったとおり、1次関数は直線、2次関数は頂点を境に折り返す放物線、3次関数はさらにもう1つ変曲点を持てる曲線-- という具合に、一般に、式の次数が増えていくと、複雑な線を近似する能力がどんどん上がっていく。

しかし、次数を上げてしまうと、計算するのも大変だし、制御するのも大変だ。

身のまわりでは、絵を描くツール等でよくみかけるベジェ曲線なんかも、すごく綺麗な線だけど、よくつかわれているのは制御しやすい三次関数だ。これをどうつなぎ合わせて作りたい線を引くかということをむしろ問題にしている。

ということからも想像できるように、一度に全体を計算するよりも、簡単な式をつなぎ合わせ、それを伸ばしていくアプローチの方が、プログラムとしては汎用性が高い。 三次ベジェ曲線だけであれば、バーンスタイン多項式というやつ読めなくても実装するのはけっこう簡単。

ベジェ曲線に代表されるように、たくさんの制御点をもとに、曲線を引くアルゴリズムのことを総称してスプライン曲線といい、ベジェ曲線の他にもいくつかある。 スプラインは、普通、簡単な式で表現できるカーブのアルゴリズムと、それらが連続するようにつなげるアルゴリズムの合わせ技でできている。

エルミート曲線 (Hermite Curve)

エルミート曲線は、曲線のアルゴリズムの1つ。 キャットムル-ロム曲線は、このエルミート曲線を連続するようにつなげるアルゴリズムなので、実装するにはまずはこれの理解が必要になる。

「制御点」を指定して多重補完するベジェ曲線とは違い、エルミート曲線は、始点と終点と開始ベクトルと終点ベクトルを入力に与えてあげる。

つまり、始点から始点ベクトルの方向に飛び出していった曲線が、終点付近では、終点ベクトルの向きで向かってきて到着する。そんな関数を考えるのである。

この関数は、始点からひとつめのカーブへ進み、ふたつめのカーブから終点へ進む、という形をしているので、3次多項式(cubic polinominal) で表現できるはずである。

\displaystyle{
f(t) = at^3 + bt^2 + ct + d \ \ \ ( 0 \leq t \leq 1)
}

こんな形をしているはず。 t は、曲線の始まりを0、終わりを1 とした場合の、曲線の進み具合のパラメータ。この式の解が、特定の位置 t における曲線の座標を表している。

式で書くことができたので、あとは4つの係数 a,b,c,d がわかれば、好きなパラメータtを代入してあげるだけで曲線の座標を得ることができる。

というわけで、4つの係数を求めてみる。

始点は t = 0 なので、t に 0を代入すると、その値は始点の座標になる。

\displaystyle{
f(0) = p_0 = d
}

反対に、tに1を代入すると 終点の座標になる。

\displaystyle{
f(1) = p_1 = a + b + c + d
}

位置だけではなく向きのベクトルも与えるので、速度の関数についても考えてみる。 一階微分したこの式が、特定の位置 t における 曲線の傾きの関数になるはずである。

\displaystyle{
f'(t) = 3at^2 + 2bt + c
}

t に 0 を代入すると、始点における曲線の傾きが得られる。

\displaystyle{
f'(0) = v_0 = c
}

t に 1 を代入すると、終点における曲線の傾きになる。

\displaystyle{
f'(1) = v_1 = 3a + 2b + c
}

ここまでの式を使って整理すると、始点&終点の座標と傾きと、係数a,bの関係が以下のようになる。

\displaystyle{
p_1 = a + b + v_0 + p_0
}

\displaystyle{
v_1 = 3a + 2b + v_0
}

あとは、連立方程式を解けば、a,bが求まる。

\displaystyle{
\begin{cases}
a + b = -p_0 + p_1 - v_0 \\
3c_0 + 2c_1 = -v_0 + v_1
\end{cases}
}

\displaystyle{
a = 2p_0 - 2p_1 - v0 + v1  \\
b = -3p_0 + 3p_1 - 2v_0 - v_1
}

これで、t を入力にとるエルミート曲線の関数を、始点/終点それぞれの座標と向きのベクトルから記述することができる。

\displaystyle{
f(t) = (2p_0 - 2p_1 - v0 + v1)t^3 + (-3p_0 + 3p_1 - 2v_0 - v_1)t^2 + v_0t + v_1
}

C#で書くとだいたいこんなかんじ。

    public struct HermitePoly
    {
        // 始点/終点の座標と ベクトルから、曲線の軌道上の座標を返す
        public Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 v0, Vector3 v1, float t)
        {
            var c0 = 2f * p0 + -2f * p1 + v0 + v1;
            var c1 = -3f * p0 + 3f * p1 + -2f * v0 - v1;
            var c2 = v0;
            var c3 = p0;

            var t2 = t * t;
            var t3 = t2 * t;
            return c0 * t3 + c1 * t2 + c2 * t + c3;
        }

        // 始点/終点の座標と ベクトルから、曲線の傾きベクトルを返す
        // この関数もつくっておくと3Dの法線とかに使えて便利
        public Vector3 GetTangent(Vector3 p0, Vector3 p1, Vector3 v0, Vector3 v1, float t)
        {
            var c0 = 6f * p0 - 6f * p1 + 3f * v0 + 3f * v1;
            var c1 = -6f * p0 + 6f * p1 - 4f * v0 - 2f * v1;
            var c2 = v0;

            var t2 = t * t;
            return c0 * t2 + c1 * t + c2;
        }
    }

キャットムル-ロム スプライン (Catmull-Rom Spline)

キャットムル-ロム スプラインは、任意の数の制御点を入力して、そこの点をすべて通るような曲線が引けるアルゴリズム。 これは実は、エルミート曲線を連続するようにつなぎ合わせる手法。

エルミート曲線自体は、始点ベクトルと終点ベクトルを自由に入力することができるアルゴリズムだけど、傾きを自由に入力するのではなく、長いスプラインのなかの、前の制御点と次の制御点との傾きを求めて、それを入力に使う。

エルミート曲線が実装できれば、後は簡単。

    public class CatmullRomSpline
    {
        public IReadOnlyList<Vector3> ControlPoints => _controlPoints;

        readonly HermitePoly _poly;
        List<Vector3> _controlPoints = new List<Vector3>
        {
            new Vector3(0f, 0f, 0f),
            new Vector3(1f, 0f, 0f),
            new Vector3(1f, 0f, 1f),
            new Vector3(1f, 0f, 2f),
            // .. 制御点はいくつでも追加できる
        };

        public CatmullRommSpline(HermitePoly poly)
        {
            _poly = poly;
        }

        public Vector3 GetPoint(float t)
        {
            var l = _controlPoints.Count;
            var progress = (l - 1) * t;
            var i = Mathf.FloorToInt(progress);
            var weight = progress - i;

            if (Mathf.Approximately(weight, 0f) && i >= l - 1)
            {
                i = l - 2;
                weight = 1;
            }

            var p0 = _controlPoints[i];
            var p1 = _controlPoints[i + 1];

            Vector3 p2;
            if (i > 0)
            {
                p2 = 0.5f * (ControlPoints[i + 1] - ControlPoints[i - 1]);
            }
            else
            {
                p2 = ControlPoints[i + 1] - ControlPoints[i];
            }

            Vector3 p3;
            if (i < l - 2)
            {
                p3 = 0.5f * (ControlPoints[i + 2] - ControlPoints[i]);
            }
            else
            {
                p3 = ControlPoints[i + 1] - ControlPoints[i];
            }
            return _poly.GetPoint(p0, p1, p2, p3, weight);
        }

これだけで、制御点をすべて通るなめらかな線が引けた。

f:id:hadashia:20171230150523p:plain

Unity に mruby 組み込んでる

実験的に、Unityにmrubyを組み込んで使ってみてる。

基本的には Unity内のコードは全て c# で書くわけだけど、ゲームの基盤部分もすべてc#で書くだけに、それとは別に、ゲームのコンテンツ部分だけを集中して記述できる分離された薄いレイヤがあると便利だ、という話がある。

たとえば、ゲーム内のキャラクタの会話や演技、そういった設定たちが、再コンパイルせずに実行中にホットリロードで変更を確認しつつ開発を進められると嬉しい。

そこで、 mruby を使ってみようかなと思い立った。

もちろん、なにもmrubyを組み込まなくたって、構造化されたただのデータ(たとえばyamlとか…) なんかでも充分、ゲームの内容を表現することはできそうだ。

しかし、それはそれで、決して安い買い物とも言えないような気がする。 複雑なデータ構造は、invalidでないことを完璧に保証するつまらないコードを書くのが案外大変だし、専用のエディタをつくるのはもっと大変だ。 ( あの人間に優しいことで有名なRailsですら、database.yamlを間違えて書いたまま起動したとき、何行目の何が間違っているかなんて教えてくれないんだから…)

しかし、言語を組み込んでしまえば、サポートされない記法を書いちゃった場合のエラーメッセージが自ずと手厚くサポートしてもらえる。構造化された内部DSLをつくるのもrubyは得意中の得意だし。

というわけで 試しに mruby つかってる。

Unity ネイティブプラグイン

さて、mruby で Unity のゲームを操縦するためには、

mrubyコード → c/c++ 等のネイティブコード → Unity C#

と、3つの段階を踏むことになる。

こう考えるとちょっと面倒臭いんだけど、 Unity はネイティブコードをビルドに含める仕組みがあるので、難しいことで悩むことはない。

Unity の Assets 内のどの階層であろうと、 Plugin と名付けられたディレクトリは特別扱いされ、この中に C#のdll、あるいはネイティブの c/c++ ソース、あるいはビルド済みのネイティブのライブラリを置くことで、ビルド時にコンパイルないしリンクしてくれることになってる。 これがいわゆる Unityの「プラグイン」という仕組みで、ネイティブコードの方を「ネイティブプラグイン」と呼ぶ。

Plugin/ 以下にはプラットフォームごとに実装を置く必要があって、たとえば、

こんなかんじのサブディレクトリをきったら、それぞれの名前に対応するプラットフォームの実装をがんばって用意して配置していく。

今回は、あらかじめビルドしたmruby をビルドして組み込みたいので、Plugins/ 以下には、ビルド済みバイナリを置いていくことになる。

C#のメソッドをネイティブコードから叩く

というわけで、ネイティブプラグインをビルドしてみよう。

僕はmacOSで開発をしてるので、まずは XCodeを開く。

  1. XCode を開いて、新規プロジェクトをつくる。
  2. プロジェクトテンプレートは macOS → Framework & Library 内の 「Bundle」 を選択。
  3. iOSで動かしたい場合は、iOS → Framework & Library 内の 「Cocoa Touch Static Library」。
  4. ちなみに、後からプロジェクトの設のターゲットを追加して、同じ操作をすると、macOSiOS 向けビルドが同じプロジェクトでやれます。

まずは、 Unity C# のメソッドをcから操縦するネイティブプラグインをつくってみるところからやってみる。

c から Unity のメソッドを叩くやりかたは、だいたい2とおりある。 1. Unityが提供している UnitySendMessage(const char *gameObject, const char *methodName, const char *message) をcから叩く 2. C#からcへ関数ポインタを渡してあげる

今回は、2の方法をとることにした。

( 1 のUnitySendMessage はめっちゃfuzzyだし遅いけど、今回の用途では充分に要件を満たすことはできそう。ところが、一旦ビルドしたやつをUnity側にリンクさせる方法をとる場合、Unity.app内にヘッダがバンドルされてない上、そもそもシンボルがなくてビルドがこけてしまう。ソースを直接 Plugins/ に置いてあげる場合でないと使えないのかもしれない )

以下は、C#の関数ポインタを受けとるcのコード。

r4u.h

// 文字列を1つ引数にとる void 関数のポインタ型
typedef void (*r4u_string_action)(const char *);

// 文字列を1つ引数にとるc# の関数ポインタを受け取る関数
void r4u_bind_debug_log(r4u_string_action);

// それを呼び出すだけの関数
void r4u_call_debug_log();

r4u.c

string_action debug_log;

void r4u_bind_debug_log(r4u_string_action action) {
   debug_log = action;
}

void r4u_call_debug_log(const char *arg) {
    debug_log(arg);
}

これで完成。 macOSのターゲットを選んでビルドすると、 ◯◯.bundle ができるので、 あとは、Unity の Plugins/macOS/ ディレクトリへそっと置いておこう。

C#の関数ポインタをネイティブ側へ渡す

続いてUnity側から、今つくったネイティブプラグインとのやりとりを書いていく。

C# には元々 P/Invoke と呼ばれる仕組みがあって、ネイティブコードとのやりとりを言語がサポートしている。Unityでも基本的には同じ記法を利用して ネイティブプラグインと通信する。

public class R4uBinding : MonoBehaviour
{
    // 相互に受け渡す関数ポインタ型は、delegate として表現する必要がある
    public delegate void StringAction(string arg);

    // R4u.bundle に実装されている関数をc#から使う宣言
    [DllImport("R4u")]
    // さっき書いたcの関数の宣言。
    public static extern void r4u_call_debug_log(string message);

    [DllImport("R4u")]
    // さっき書いたcの関数の宣言。 (次のステップで、これを直接呼ぶのではなくrubyコード経由で叩く)
    public static extern void r4u_bind_debug_log(StringAction action);

    void Start()
    {
       // こんなかんじで、関数ポインタを登録すると、
        r4u_bind_debug_log(DebugLogCallback);

       // c コードから自由にそれを呼べるようになっているはず
        r4u_call_debug_log("Hello! Hello! Hello1");
    }

    // ネイティブ側に渡せるのは、この属性のついた static メソッドのみ、という制約があるらしい
    [MonoPInvokeCallback(typeof(StringAction))]
    static void DebugLogCallback(string arg)
    {
        Debug.Log(arg);
    }
}

コンソールに Hello! Hello! Hello! がでたら成功。

mrubyのビルド

C#の 関数ポインタを受けとることができたので、あとは この関数ポインタをmruby 経由で操縦するバインディングを書くだけ。

まずは 適当な場所に mruby/mruby をクローンする。

ドキュメントは リポジトリ内の doc/ 内に書いてあるようだ。これを見たり、野良ブログで紹介してくれている build_config.rb を参考にビルドする。

iOS/Android についても既にDSL的にはサポートがあるようで、自分のマシン上のそれぞれのプラットフォームのビルド環境があれば、 toolchain での指定を変えるだけで、うまくビルドしてくれた。

# デフォルトでは 「host 」という名前でビルドされる
MRuby::Build.new do |conf|
  toolchain :clang
  enable_debug
  conf.gembox 'default'
end

# iOS
MRuby::CrossBuild.new('iphone-arm64') do |conf|
  toolchain :clang

  isysroot = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'

  conf.cc do |cc|
    cc.flags << '-arch arm64'
    cc.flags << "-isysroot #{isysroot}"
    cc.flags << '-Wall'
    cc.flags << '-Werror-implicit-function-declaration'
  end

  conf.linker do |linker|
    linker.flags << '-sdk iphoneos'
    linker.flags << '-arch arm64'
    linker.flags << "-isysroot #{isysroot}"
  end
end

# Android たぶんこんなかんじ。 https://github.com/mruby/mruby/blob/master/doc/guides/compile.md
# MRuby::CrossBuild.new('android') do |conf|
#   toolchain :android
#   conf.gembox 'default'
# end

上のような設定ファイルをつくったら、ファイルパスを指定して ./minirakeする

$ MRUBY_CONFIG=../../mruby_build_config.rb  ./minirake

すると mrubyのリポジトリ以下のようにそれぞれのビルドができる。

build
├── host
│  ├── LEGAL
│  ├── bin
│  ├── lib
│  ├── mrbgems
│  ├── mrblib
│  └── src
└── iphone-arm64
   ├── lib
   ├── mrblib
   └── src

XCode のプロジェクトに mrubyをリンクする

mrubyができあがったら、 build/host/lib/libmruby.a を、XCodeプロジェクトからリンクする設定をしておく。

  1. 「Build Settings」 の Header Search Paths に、 mrubyリポジトリ内にある include ディレクトリを追加
  2. 例) $(PROJECT_DIR)/../../ext/mruby/include
  3. 同じく Build Settings の Library Search Paths に、ターゲットのプラットフォームに対応する libmruy.a があるディレクトリを追加しておく
  4. 例) $(PROJECT_DIR)/../../ext/mruby/build/host/lib
  5. ターゲットごとに、「Build Phases」の Link Binary With Libraries という項目に、libmruby.a をドラッグ&ドロップする

設定できたらテストしてみる。 mruby の hello worldXCodeからコンパイルできたら成功。

#include <mruby.h>
#include <mruby/compile.h>

void hoge() {
    mrb_state *mrb = mrb_open();
    mrb_load_string(mrb, "5.times {|i| p 'hello' }");
    mrb_close(mrb);
}

C# から rubyコードを渡してあげて実行する

あとは、さっき作ったネイティブプラグインの、c#関数ポインタ呼び出し部分をrubyで操縦できるようにするだけ!

typedef void (*r4u_string_action)(const char *);

void r4u_bind_debug_log(r4u_string_action);

void r4u_start();  // 初期化関数
void r4u_dispose(); // 後始末関数
void r4u_exec(const char *src); // rubyコードを渡すと実行してくれる関数
#include "r4u.h"

#include <mruby.h>
#include <mruby/compile.h>

struct r4u_context {
    mrb_state *mrb;
    struct RClass *module;
    
    r4u_string_action debug_log;
    r4u_string_action debug_log_warning;
    r4u_string_action debug_log_error;
};

struct r4u_context r4u; 

mrb_value _debug_log(mrb_state* mrb, mrb_value self)
{
    mrb_value arg;
    mrb_get_args(mrb, "o", &arg);
    mrb_value message = mrb_inspect(mrb, arg);
    char *message_cstr = mrb_str_to_cstr(mrb, message);
    debug_log(message_cstr);
    return mrb_nil_value();
}

void r4u_bind_debug_log(r4u_string_action action) {
    r4u.debug_log = action;
}

void r4u_start() {
    r4u.mrb = mrb_open();
    r4u.module = mrb_define_module(r4u.mrb, "R4u");
    
    mrb_define_module_function(r4u.mrb, r4u.module, "debug_log", _debug_log, MRB_ARGS_REQ(1));
}

void r4u_dispose() {
    mrb_close(r4u.mrb);
    
    r4u.debug_log("[R4u] dispose");
}

void r4u_exec(const char *src) {
    r4u.debug_log(src);
    
    mrbc_context *context = mrbc_context_new(r4u.mrb);
    int ai = mrb_gc_arena_save(r4u.mrb);
    mrb_load_string_cxt(r4u.mrb, src, context);
    
   // このブロックはエラー処理
    if (r4u.mrb->exc != 0) {
        mrb_value exc = mrb_obj_value(r4u.mrb->exc);
        mrb_value backtrace = mrb_get_backtrace(r4u.mrb);
        r4u.debug_log_error(mrb_str_to_cstr(r4u.mrb, mrb_inspect(r4u.mrb, backtrace)));
        
        mrb_value inspect = mrb_inspect(r4u.mrb, exc);
        r4u.debug_log(mrb_str_to_cstr(r4u.mrb, inspect));
        r4u.mrb->exc = 0;
    }
    
    mrb_gc_arena_restore(r4u.mrb, ai);
    mrbc_context_free(r4u.mrb, context);
}

Unity側も修正したら完成。 C# からは、好きなrubyコードを書いて実行できる。

using System.Runtime.InteropServices;
using AOT;
using UnityEngine;

public class HogeBinding : MonoBehaviour
{
    public delegate void StringAction(string arg);

    [DllImport("R4u")]
    public static extern void r4u_start();

    [DllImport("R4u")]
    public static extern void r4u_dispose();

    [DllImport("R4u")]
    public static extern void r4u_exec(string src);

    [DllImport("R4u")]
    public static extern void r4u_bind_debug_log(StringAction action);

    [MonoPInvokeCallback(typeof(StringAction))]
    static void DebugLogCallback(string arg)
    {
        Debug.Log(arg);
    }

    void Start()
    {
        r4u_start();
        r4u_bind_debug_log(DebugLogCallback);

        // 好きなrubyコードを実行できる。あらかじめバインドしたC#関数もコールできる
        r4u_exec("include R4u\n" + "3.times{ debug_log 12345 }");
    }

    void OnDestroy()
    {
        r4u_dispose();
    }
}

うむ!

課題

  • このやりかただと、c#の操作したい関数が増えるたびに、mruby から のバインディングを書く手間がけっこう多い。
  • もし ダイナミックなバインディングが不可能であれば、コード生成する仕組みとかを入れても良いのかもしれない。
  • XCodeで完結しない Android NDK とかのビルドのワークフローをちゃんとしないと面倒

もうちょっと仕組みを整備する必要があるけど、個人開発では十分使っていけそう。

Optimizing graphics rendering in Unity games 読んだ

Optimizing graphics rendering in Unity games のメモ

https://unity3d.com/jp/learn/tutorials/temas/performance-optimization/optimizing-graphics-rendering-unity-games?playlist=44069

Unity 描画パイプライン

毎フレーム、CPUは以下を実行する 1. シーンにある全オブジェクトをチェックし、レンダリングすべきかどうか判定する。 - view frustum に入ってないオブジェクトを除外する - 他 2. レンダリング対象のオブジェクトのレンダリング情報を収集し、ソートする。 - MeshやMaterial - このフェイズで、特定の状況に当てはまると batchingが行われる 3. CPUは、「batch」と呼ばれるデータを作成し、draw call コマンドを作成。

Draw call のステップは以下に分別される。

  • Set pass call: CPUがGPUに render state を送信する。これを Set pass call と呼ぶ。Set pass call は、初めてのメッシュを描画するときや、render stat (たぶんMaterialの変数とか頂点データ)が変化わったときに送信されるコマンド。つまり実質、GPUへデータを転送しているのはこれ。
  • Draw mesh: CPUがGPUへ、 Set pass call で定義された内容を描画する命令をGPUへ送る
  • 複数パスでの描画必要な場合、都度 Set pass call、Draw call がそれぞれ走る

Set pass call 、Draw mesh、これらをひっくるめて Draw callと呼ぶ。 フレームデバッガなどでは分かれて表示されているときは、実質、GPUへデータを転送しているのは Unityの言うところのSet pass call。CPU負荷があるのもSet pass call。

if our game is CPU bound

CPU バウンドな処理をしらべるとき、まずはどのタスクが原因なのかを特定することが重要。 なぜなら、Unityの描画はマルチスレッドで動作しようとするため、重いスレッドで動いているボトルネックをピンポイントで潰さないと効果がないから。

たとえば、culling operationが遅いとき、Set pass callとか別のスレッドで動いているタスクを減らしても効果がない。

また、ハードウェアによってスレッドの本数や割り当てに違いがあるので、実機で確認する必要がある。

Unityには3種のスレッドの使いわけをする - main Thread - 1本 - 基本的にはゲームロジックが実行される - render thread → - 環境によってはマルチスレッドで動作 - マルチスレッドレンダリングは複雑かつハードウェア依存。 - GPUにコマンドを転送する特別なスレッド - worker thread - culling or mesh skinning その他のタスクを実行する - どのタスクがどのワーカスレッドを実行するかは場合による - たとえば、ハードウェアが複数のスレッドを持っていた場合、ワーカースレッドはその分多く生成される - そのため、実機でプロファイリングすることが重要

(裏技)Graphics jobs でマルチスレッド化

Player Settings の、 「Graphics jobs」にチェックを入れると、Unityがメインスレッドでやっているrender loop をworker thread へ逃がすことができる。 つまり、Camera.render 自体が並列になり、めちゃ速くなるということ。

※ただし まだ実験的な機能 なので注意

CPUの描画ボトルネックのみつけかた

CPUバウンドになる最も一般的なボトルネックは、GPUへのコマンド送信。 このタスクは render threadで実行されると考えて良い。(PS4 とかでは worker threadで実行されるらしい)

最も重い処理は、Set pass call。Set pass callを減らすことはCPU負荷軽減に多くの場合に効果が見込める。 送信するデータの量や数を減らすことも効果がある。別々の render stateなオブジェクトはできるだけ減らす。

SetPass Call 回数の軽減になるテクニックは以下↓

  • レンダリングするオブジェクトを減らす
    • 例) カメラの farClipPlane を調整することで、範囲を調整し、カリングされるオブジェクトを増やす
    • 例) カメラの layerCullDistance を設定することで、レイアごとにカリングされる距離を調整することができる
    • 例) Occlusion cullingをつかう
  • 1つのオブジェクトのレンダリング回数を減らす
    • 例) シェーダのパスを減らす。シャドウマップ、dynamic light 他

バッチング

CPUの世界で同一のオブジェクトを収集し、同一のものとしてまとめてGPUに送る

  • 基本的 に同一のMaterial、(同一のMaterialプロパティ、シェーダ変数、テクスチャ) でないとバッチングされない
  • ほぼ同じMaterialで、テクスチャだけが違う、ていうときは、1つのでかいテキスチャに結合して無理矢理Materialを1つにするというテクニックがある
  • スクリプトで Renderer.material のプロパティを書き換えると、 内部ではMaterialが複製されている。sharedMaterialをつかうと、バッチングされているMaterialが対象になる

関連する参考情報や、関連するテクニック - Unity Manual - Optimizing Unity UI - Unite Bangkok 2015 - GPU Instanting - Texture atlas

カリング/バッチングで逆に遅くなるケース

  • カリングはコスト高。カメラの数 x オブジェクトの数 のオーダーで負荷がかかる
  • 使わないカメラはdisableに。使わないRendererもdisableに。
  • バッチングは、逆に負荷になることもあるので要注意

Skinned Meshes

ボーンアニメーション。 スキンメッシュの描画関係の処理は メインスレッド or ワーカースレッド。これもハードウェアに依る。 Skinned Meshes のレンダリングはコスト高。

以下、改善策。 - 現在SkinnedMeshRendererを使用している全てのオブジェクトに対して、本当に使うべきか再検討する - SkinnedMeshRendererが刺さっているけど、実際にはMeshRendererで事足りるオブジェクトがいないかチェック - 特定の一回しかアニメーションしないようなオブジェクトは、アニメーションが終わったら SkinnedMeshRenderrerをMeshRendererに差しかえる。( SkinnedMeshRenderer.BakeMesh という機能がある - 限られたプラットフォームでは、実験的に GPU skinning がサポートされている

メインスレッドの処理は、描画タスクとは無関係

CPUバウンドだからといって、メインスレッドの処理を最適化しても無意味。 なぜならスレッドが別だから。

if our game is GPU bound

GPUの足をひっぱるのは大抵は fill rate。 特にモバイルはGPUに見合わない解像度があるので問題が大きくなる。

memory bandwidth と、頂点の処理も同様に重要ではある。

Fill rate

Fill rate とは、1秒あたりにGPUが描画しなければいけないピクセル数のこと。

Fill rateが問題になっているかどうかは、 Player Settingsで、Screen Resolutionを減らしてみて、前後のGPU timeを比較することで確認できる

Fill rate問題の改善方法は以下 - フレグメントシェーダを最適化する - オーバードロー(同じピクセルを何度も描画すること)を減らす。でかいオブジェクトが描画された後、その上にさらにでかいオブジェクトが描画されるようなとき、2つぶんのFill rateになる。render queue を適切に管理して、フラグメントシェーダの前にカリングする等が対策 - ImageEffects はFill rateがめっちゃ上がる。ImageEffectsを2つ以上使う場合は倍々でFill rateが上がる

Memory bandwidth

GPUは専用のメモリを持っている。このメモリの read/writeが性能の限界になることがある。テクスチャがあまりにもでかすぎるとか。 Memory bandwidthが問題になっているかどうかは、Quality Settingsからテクスチャの解像度を落としてみて、前後の GPU timeを見比べることで確認できる

Vertex processing

RxSwift つかってる部分の引き継ぎしてる

前回 に続き、担当してたアプリについて考えていたこと書き記してる。

とくに RxSwift がなぜ良いのか? ということは、説明がすこしむつかしいので、すこし丁寧に書くチャレンジをしてみている。冒頭のところ公開してみる。

--

Rxは何を解決できるか?

クライアントサイドのアプリケーションへの入力は、タイミングがまったく予測できず、おまけに、並列であちらこちらに到着します

イメージ図)

入力の種類 \ 時間 0.166秒 0.33秒 0.5秒 0.66秒
 ひとさし指の動き タップ! スワイプ開始!
 おにいさん指の動き タップ!
 HTTPレスポンス その1 到着!
 JSONのデコード 完了!
 写真の読み込み 完了!
 HTTPレスポンス その2 到着!
 PNGのデコード 完了!

これらバラバラのタイミングで連続して発火する入力を漏らさず受け取り、整合性を取っていかなければいけない、この辺りにクライアントサイドの(設計面での)難しさがあります。

iOS は、アプリが起動してから落ちるまで、1秒に60回くらいのハイペースで休まず画面を描きかえ続けています。

f:id:hadashia:20160825154943p:plain

(1秒に60フレーム)

そのため、我々アプリ層のプログラマは、画面に出力すべき内容の変更を検出し次第、即、それを反映させる責任があります。画面は、プログラムの処理が終わるのを一切待ってはくれず、こうしている間にも、随時、描き直されまくられているのです。

まとめると、入力は並行してバラバラにくるし、出力は即反映させないといけない、ということです。 これは、入力 → (処理) → 出力、というごく単純なシーケンスを考えるだけでは、全体を捉えることがむつかしいです。なにかすごいパラダイムの助けを借りて、人間に理解できる脳内モデルを導入したいところ。

ところで、なぜ入力は、並行してやってくるのでしょうか? それは、iOSがしている1秒に60回の画面描き換えをブロックしてはいけないからです。

UIを書き換えているスレッドは、1/60s = 1.66ms 以内に画面を書き換え終わらなければいけないため、このスレッドで時間のかかる処理はできません。I/O待ちは当然非同期、CPUバウンドな処理は別スレッド、周りを見渡せば、アプリ層そのものが非同期だらけです。アプリ層そのものが複雑なのです。

ご存知のとおり、非同期処理が多くなると、上から下に順番に処理を書いているぶんには簡単だったことも、途端に難しくなります。

たとえば、

  • 複数の入力データをくみあわせて、ひとつの出力をはじきだす
  • エラーがどこで発生しようが、いつも同じ位置で処理する
  • 処理と処理の順番を指定する
  • 不要になった処理を途中で止める

もちろん、どれもこれも、同期的に上から下に処理が流れるだけであれば単純なことなのですが、非同期がからむと途端に難しくなります。それに、iOS はマルチスレッドの実行ができるから、並列処理特有の問題もあります。変数のデータ競合を避けるとか。

これがクライアントサイドの設計の本質的に難解な点です。ドメインロジックとプレゼンテーションレイヤを分離するとか、UIとデータをバインドするとかでは、秩序を齎すに十分ではないことがわかるとおもいます。

このアプリでは これに対応するため Rx をつかいました。じつを言うと、Rx なら、上の問題を解決することが簡単にできます。

  • 複数の入力データをくみあわせて、ひとつの出力をはじきだす → Observableを合成するだけ
  • すべてのエラーを、適切なタイミングで出す → onError にエラーが伝搬してくる
  • 複数の処理を適切に待ち合わせる → スケジューラを使えば解決
  • 不要になった処理のキャンセル → Disposableを定義してけば自動でキャンセルされる
  • race condition を避ける → イミュータブルなデータと副作用のない関数の操作がメインなので問題になりにくい。必要なときは、スケジューラでスレッドを指定する

このリストをみれば、Rx と、Rxの亜種との違いがわかると思います。 Rxは、言語仕様によるサポートを一切必要とせず、それどころかどんな言語にもまったく同じ超強力かつ型安全なインターフェイスを提供することに成功しているのですが、なかでも、Disposable 、スケジューラ などの実用的な周辺機能があるおかげで、非同期の難しさに立ち向かうことができます。これらは、みんなの力で言語をまたいで移植され、いきいきと活動し続けています。

変更可能性(ミュータビリティ)

複数の非同期処理 の整合性をとる、これを素朴に実装すると、おびただしい数の状態変数が生まれることになります。

非同期処理によって、あちらこちらに点在するコールバック関数が呼ばれるわけですが、これらを連携させるには、コールバック関数の外側のスコープに変数をつくって、読み書きするしかないからです。

だけど考えてみてください。これは、原理的にやってることがグローバル変数と一緒です。どこからどの変数が更新されるかを保証することも仮定することもできないので、デバッグが非常に困難です。

もしも、変数の内容がどこかで書き換わることがなく、関数に副作用がないとしたら、状況によって挙動が変わるということがなくなるので、とても見通しがよくなります。

リアクティブプログラミングは、そういった考え方をもたらしてくれます。中間の状態を変数で管理するという必要がなくなるのです。 イメージは、事前にたくさんのイベント同士の関連をすべて一箇所で宣言する、それだけです。 イベントとイベントの関係を事前に宣言しておけるので、たくさんのイベント間で1つの変数を読み書きする必要がありません。

書き換わることがない変数には、いくつか優れている点があります * マルチスレッド間でデータ競合を引き起こすことがありえない。安心して別スレッドへ渡せる * 参照を共有する必要がないので、データは値型でよくなる。すると参照のコピーが走らなくなるので、Swift の ARC によってリファレンスカウントを管理する必要がなくなる。余計なバイトコードが生成されなくなるし、参照を数える必要がなくなり、パフォーマンスがよい。(コピーの時間が無視できないほどの巨大なデータはこのかぎりではない)

GUI プログラミングで、 関数型言語のムーブメントが盛り上がっているのは、このような複雑な状態管理を解決できるアプローチになりうるからです。

関数の副作用を廃して、始点の入力さえあれば、中間のデータを保持することをかんがえなくてもすべてを宣言できる、この考え方は、非同期だらけのなにかを書くのに非常にマッチします。Swift が関数型の影響を受けているのは、この目的と無関係ではないでしょう。

サーバサイドプログラミングとの違い

結果が変わり次第、随時出力を求められるというのは、サーバサイドにはあまり見られない制約です。

HTTPなサーバサイドのプログラミングを考えると、アプリ層への入力は、「リクエスト」という単一の入力だけがあり、相手に対して応答するのも一度だけ。しかもその間、相手を待たせておくことができます。

入力 → (何か処理) → 出力

サーバサイドは、アプリケーション層自体よりも、むしろより低レイヤなDB層の設計のほうが大変だったり、アプリ層そのものでなく、アプリ自体を並列で並べるアーキテチャ面などに難しさがある傾向があります。

ネイティブアプリケーションは、オーソドックスなWebページとは根本的に難しさが違います。アプリ層の中そのものが非同期だらけです。そのため、クライアントサイドではRxなど、アプリ層で別のパラダイムを導入するムーブメントがあるます。

Rx 入門