フューチャー技術ブログ

究極の?トランザクションスクリプト用言語Verse

Unreal Engineで有名なEpic Gamesが、現在開催中のGDC(Game Developer Conference)でVerseの詳細を解説しており、その動画が公開されています。

ドキュメントもすでに公開されています。

https://dev.epicgames.com/documentation/en-us/uefn/verse-language-reference

現在処理系は、Unreal Editor for Fortnite(UEFN)の中に組み込まれているようですが、Windowsにしか対応していません。ちょろっと動かした程度で、まだコードをしっかり書いてはなくて、プレゼンを見た感想で書いているので、いろいろ間違ったところとかもあるかもしれませんが、そういうのはご指摘いただいたり、Pull Request(ブログの上の鉛筆アイコンで送れます)を出してもらえれば、と思います。

image.png

UEFNのVerseボタンを押すと、こんな感じでVSCodeがぴょこっと起動します。

スクリーンショット_2023-03-23_18.57.26.png

Verseはメタバース用の言語です。メタバースではすでにある3Dの空間の上に、いろいろな企業や個人がコンテンツを作っていきます。一般的なゲームと違い、どのような3Dモデルがあるか、どのような動き(プログラム)があるかは事前には決まらず、後から追加されたコンテンツをロードしてユーザーが実行するという、スクリプト言語が必要であり、そのために作られた、とあります。

将来的にはオープンソースになり、ブラウザでも動くことを目指しているようです。

10億人規模のユーザーがプレイしている、今まさに動いている環境に後から100万人規模の開発者がコンテンツを作っていくという、そういう世界観です。ある意味、Smalltalkが目指していた世界のさらに先、という感じですね(おっさんにしか伝わらない例)。

基本的な言語設計

関数型言語、オブジェクト指向、命令型のエッセンスを集めて作られた言語となっています。12月に発表されたときは関数型言語、というのを押し出した感じの紹介になっていて、特に追いかけてなかったのですが、色んなパラダイムをバランスよく取り入れている感じがしました。

基本的な設計は今時の言語っぽい感じです。

  • mutable/immutable
  • 静的型つけ(TypeScript的な後置)
  • 構造体
  • クラス、インタフェース(継承も)
  • ジェネリクス(クラス、関数)
  • Pathと呼ばれるネームスペース(Javaのpackageっぽい)

タプルとかもあるし、配列、辞書もあって、タプルで関数の引数をまとめて渡したり、デフォルト引数があったり。

関数型っぽい要素

forループ構文は、他の言語にも良くある構文に一見見えますが、forとは見ないで、C#のLINQだったり、Javaのストリームだったり、汎用リスト処理っぽいもの、と考えた方が良さそうです。

シンプルな例としては、配列の中の要素に対するループだけど、追加でフィルタを指定できたりするところは面白いですね。

for (X : SomeArray; X <> 100):
Print(X)

なんかすごいループの例がこれです。マインスイーパの隣接するセルの爆弾の数のカウントを1つのループでするそうな。Y->CellRow:Cellsはループごとに、インデックスをYに、値をCellRowにアサインするようです。その下のX->Cell:CellRowはそれをXCellにやっていて2重ループになります。そのX, Yに対して隣接する9つのX, Y(それぞれ-1から1のオフセット)の組みを次の2行で作り出しており、4重ループになりました。その後はCell<>AdjacentCellで自分自身をのぞき、 AdjacentCell.Mined?で爆弾があるセルだけにフィルタリングするという・・・値からリストを作り出したりしつつ多重ループをしていくという流れですね。このあたりは関数型を意識した機能になっているように見えます。

for:
Y->CellRow:Cells
X->Cell:CellRow
AdjacentX:=X-1..X+1
AdjacentY:=Y-1..Y+1
AdjacentCell := Cells[AdjacentY][AdjacentX]
Cell<>AdjacentCell
AdjacentCell.Mined?
do:
set Cell.AdjacentMines += 1

Fail

if文も他の言語とちょっと違っていて、成功・失敗の可能性のある式に対して適用する、ということになっています。次の式は、インデックスが配列の範囲に入っていて成功したときだけ後ろが実行される、という感じです。今時の他の言語だと、null安全とかで、「nullになっていない」という条件を媒介にして「失敗しないコード」を書きますが、Verseだと、失敗した場合は、関数型言語でいうところの空配列に対してリスト処理する(実行されない)ぐらいの軽い感覚で、アプローチは違いますが、安全なコードをシンプルに書けます。こういう割り切りは面白いな、と思いました。

if (Element := MyArray[Index]): Log(Element)

これらに近いのは、ReactのSuspenseとかErrorBoundaryですかね。正常ケースで書いていくが、未ロード時とか失敗時にはこれらのコンポーネントが拾ってフォールバックします。

トランザクション

ifは他の言語と違うと紹介しましたが、前のFailはまだまだ序の口です。これには述語的な条件文が書けます。この場合、実行後にこの条件にマッチしないと、それまでに行った操作が巻き戻るらしいです。ドキュメントによると、ファイルI/Oやログ出力などの言語の外へのインタラクションとか、no_rollbackがついた処理以外にはこれが適用されるとのことです。

if:
DoSomeEffect()
X < 100
then:
SomethingElse()

これはプレゼンテーションで紹介された言語の基盤となるアイデアの2番目にも書かれています。100万人規模の開発者が並行性を管理する唯一の方法とあります。確かに、絶対成功するのであれば、Goで採用されているCSP(Communicating Sequential Process)はパフォーマンスを維持しつつ並行処理を進めるには最適です。あるいは、やりとりの方向性が一方通行であっても問題はないでしょう。しかし、失敗時のロールバックを別のプロセスに依頼というをミスなく組み込むのは困難です。

スクリーンショット_2023-03-23_18.11.04.png

ゲームの場合は、たくさんのオブジェクトが並行で動いていて相互に複雑にインタラクションしますので、こういう方向性になっているんじゃないかと思います。ゲームは速度が命、と思われるかもしれませんが、「ボタンを押した」「物体同士がぶつかった」「ビヘイビアツリーの思考ルーチンで何かやることを決めた」みたいなイベントはそれほど多くは起きません。そして、それらによって決まったアクションを毎フレーム実行する(たくさんの破片が回転しながら飛んでいくなど)部分では大量に並行で動作します。このトランザクションは前者のコントロールフロー部分で使い、後者の大量にCPU使って効率を上げられるところはエンジン内部で・・・みたいな使い分けなんじゃないかと思います。

非同期周りの機能が面白い

ゲームだと、同時に発生するさまざまな事象を同時に扱う必要があります。格闘ゲームの開始前のシーケンスを見ても、1P側のキャラクターのアニメーションが再生されて、その後2P側のアニメーションも再生されるが、ボタンを押すとスキップできるよ、とか、ゲージ類が移動アニメーションしながら画面にカットインするとか、そういうやつです。フレームを跨いで処理を記述する必要があり、たいていゲームエンジンには、直列だったり並列のイベントを扱う仕組みがあったりします(以下はCocos2d-xの例)。

スクリーンショット_2023-03-23_15.23.46.png

Verseでは同期のblockも含めて、5つの制御構文があります。JavaScriptのPromiseと一部似ていますね。syncPromise.all()相当です。Promise.race()相当はrushで、raceは別物な点は要注意です。

先ほどのFail/トランザクションとの組み合わせでゲームのコンテンツを作る事例が動画にあります。1本のスクリプトだけど、プレイヤーーが特定のエリアに行ったら敵がでる、火が燃える、安全なエリアに到達したら敵を削除、ヘリに乗ったらアニメーション再生して完了など、ユーザーの体験に従ったスクリプトになっています。この複雑なユーザー体験を駆動する部分に、これらの非同期のブロックが活用されています。

Cypressのテストコードがこれに近いものを目指していますが、JavaScriptの上に作り上げているので通常の構文と組み合わせられなくて変数の取り扱いがちょっと不便だったりしますが、これは最初からこの非同期をうまく使うことを考慮しているので、なんかエレガントですね。

トランザクションスクリプト

エリック・エヴァンスのDDD本では、オブジェクト指向型のドメインモデルと対比してトランザクションスクリプトはダメだ、と言っています。

利口なUIについて議論したのは、レイヤ化アーキテクチャのようなパターンが、ドメイン層を隔離するために必要になる理由と、それがどういう時なのかを明確にするためだけだ。利口なUIとレイヤ化アーキテクチャとの中間には、他にも解決策がある。例えば、Fowler(2002)が説明しているトランザクションスクリプト(TRANSACTIONSCRIPT)は、ユーザインタフェースをアプリケーションから分離はするが、オブジェクトモデルは提供しない。
:
他の開発スタイルにもそれなりの存在意義があるが、複雑さと柔軟性において、さまざまな限界があることを認めなければならない。ドメインの設計を分離し損ねると、状況によっては実に悲惨なことになりかねない。アプリケーションが複雑で、モデル駆動設計に取り組むつもりなら、歯を食いしばり、必要な専門家をそろえた上で、利口なUIを避けるべきである。

Eric Evans. エリック・エヴァンスのドメイン駆動設計 (Japanese Edition) (pp.76-77). Kindle 版.

オブジェクト指向はそれぞれのオブジェクトが責務を果たせば、すべてのタスクが正しく完了するという考え方です。センサーオブジェクトが何か信号を得たら、それをオブザーバーオブジェクトが検知して、異常かどうか判定するストラテジーオブジェクトが状態を・・・というように、責務ごとに分担します。DDDはこれを推しています。しっかりしたモデルができれば、新しい機能を追加しようとした場合に、オブジェクトが勝手にやってくれる(ようにすでにプログラミングされている)ので、少ないコードで重複を減らせる、みたいな考えなんじゃないかと思います。

ですが、業務はまずみんなフローで考えます。オブジェクトにしてしまうと、そのフローは複数のメソッドに分かれてしまいます。ステートによって条件分岐が変わるとすると、そのステートが変わるイベントも探して、1本の業務フローがようやく見つけられる、ということになります。

現在の世の中も、ウェブフロントエンドの関数型を取り入れたUIフレームワークが流行ったりして、オブジェクト指向でモデリングというのとは別の流れを作っています。もちろん、オブジェクト指向は有用なことには変わりはないのですが、どちらかというと、配列クラスとか、リクエストクラスとか、便利な部品を作るところがオブジェクト指向で、それらを活用してアプリケーションの流れを作る、変化が大きい部分に関数型や、手続型を組み合わせる、という流れが一般的になってきているように思います。

エンプラ開発でも、フレームワーク的なレイヤーや共通部品はオブジェクト指向で作っても、SQLを使ってDBの読み書きをして、それをもとに別のクエリーを発行するとか、DBが中心でオブジェクト指向でモデルってあんまり作らないですよね。オブジェクトを作って元気にN+1してもいいんですけど。

Verseは他の言語などをしっかり研究している、とプレゼンの最初の説明にもありましたが、まさに今の世の中が目指していて既存の言語の上に作られたフローをゼロベースから最適な形で実現した言語と言えます。

まとめ

Unreal Engineは、新しいビジョンを作り出し、それを実現するための機能を載せてきます。12月の発表では、このVerseはOSSであり、Unityなどの別のゲームエンジンでも使えるオープンな標準にしていこう、としています。とはいえ、おそらく業務システムに使えるようになるかどうかというと、そういう未来はすぐには来ないかもしれません。ですが、Verseが解決しようとしている問題とか、それに対するアプローチは、ゲーム業界ではない人にも刺激があるんじゃないでしょうか? 少なくとも、僕はかなり刺激を受けました。今までの言語とかフレームワークでちょっといまいちだな? と思っていた部分も鮮やかに解決されていたりして、別の言語の開発でも参考にしたくなりますよね?(トランザクションは難しいですが)。今後もちょくちょく追いかけてみようと思います。

スクリーンショット_2023-03-23_19.52.29.png
  • 参考: Haskellのイベントで発表された言語の紹介