Emacsで3DなマルチプレイヤーなFPSを作ってみた

関西Emacsで発表したデモです。

まず、「FPS」は CoD MW のようなシューティングとかではなくて(それは是非やりたかったのですが)、一人称散歩(First Person Sanpo)プログラムです。FPSは釣りです。ごめんなさい。

(2011/05/06 追記: Vimにも出来てました!! 3D in Vim — KaoriYa)

動作風景

所詮テキストエディタのやることですので、期待せず、生暖かい目で見てください。

一人用のムービー



バッファで描いた迷路を歩きます
マルチプレーヤーのムービー



Emacsでサーバーを起動して、3つのEmacsから接続します。あとでtelnetで直接接続してチートします。

動かし方:シングルプレーヤー

必要な物:

  • 64bit版Emacs23.x
    • 22でも動くかも知れません
    • 32bitだと整数桁あふれするそうです
  • banner
    • 文字のビットマップを取得する
    • banner(Mac, BSDç³») か printerbanner(Linuxç³»?)
  • deferred.el, concurrent.el
    • 非同期ライブラリ
  • matrix.el
    • 簡易行列計算
  • 3dmaze.el (本体)

各elispファイルは、auto-install.elで以下の式を評価して入れるか、ダウンロードしてload-pathの場所に置いてください。ダウンロードした場合は、なるべく高速に動作させるためにバイトコンパイルしてください。(多分2倍くらい速くなります)

;; auto-installを使う場合
(auto-install-from-url "https://github.com/kiwanami/emacs-deferred/raw/master/deferred.el")
(auto-install-from-url "https://github.com/kiwanami/emacs-deferred/raw/master/concurrent.el")
(auto-install-from-url "https://github.com/kiwanami/emacs-3d-demo/raw/master/matrix.el")
(auto-install-from-url "https://github.com/kiwanami/emacs-3d-demo/raw/master/3dmaze.el")

準備出来たら、以下のように操作します。

初期位置はランダムで決まりますので、壁の方を向いていたら真っ青から始まるかも知れません。あわてず横を向いてください。
あと、行頭行末スペース強調をしている人は、止めておいてください。

キーバインド
キー 動作
←→ 左右に回転する
↑↓ 前に進む、後ろに進む
a,s 左右に進む
b 後ろを向く
m 2Dマップ表示のトグル

終了は kill-buffer (C-x k) してください。

迷路について

「#」や「*」は壁になります。スペースは道になります。

アルファベットを書くと、アルファベットのオブジェクトになります。迷路バッファとして適当なソースファイルとかも指定できますが、オブジェクトが多いと重くなりますので注意してください。

アルファベット以外の文字(正確にはbannerが表示できない文字)を含むとエラーになるかも知れません。

動かし方:マルチプレーヤー

シングルプレーヤー版が動けば、こちらもすぐに動くと思いますが、MacのEmacs22だとIPv6の関係でサーバーになれないかもという情報があります。

シングルプレーヤー版に加えて、サーバーは server-maze.el をインストールしてください。クライアント側は client-maze.el をインストールしてください。もちろん、両方入れて自作自演も可能です。

;; auto-installを使う場合
(auto-install-from-url "https://github.com/kiwanami/emacs-3d-demo/raw/master/server-maze.el")
(auto-install-from-url "https://github.com/kiwanami/emacs-3d-demo/raw/master/client-maze.el")
サーバーの起動

カレントディレクトリの迷路ファイル(maze.txt)を読み込みますので、ホームディレクトリに迷路ファイルを置いてください。その後、Scratchバッファで以下を評価すると起動します。

(require 'server-maze)
(setq ssm:server-address "192.168.1.1") ; ←自分のマシンのIPv4アドレス
(ssm:server-start) 

起動すると、以下のような画面が表示されます。



サーバーの画面
プレーヤー(クライアント)の起動

スクラッチバッファから以下を評価してください。

(require 'client-maze)
(csm:client-start)

そうすると、以下のような画面が出てきます。



接続情報画面

「Name」は適当なアルファベット、「Server Address」はサーバーのIPアドレスやホスト名、「Server Port」は8765(デフォルトの場合)を入れてください。

OKを押して、うまくいくと迷路画面が出てきます。

操作の仕方はシングルプレーヤーと変わりません。

他人がどっちを向いているかは色で分かります。

  • 赤:こっちを向いているとき
  • 黄:横を向いているとき
  • 緑:向こうを向いているとき

散歩ゲーなので、撃つ殴るなどのインタラクションは出来ません。仮想空間でのEmacs同士のリアルなコネクションを感じながら和んでください。

サーバーの操作

特にクライアントが動き回るのを見守るぐらいしかありませんが、一応以下のようなキーバインドがあります。

キー 動作
g 画面再描画
b IDで指定してban
q サーバーの終了
プロトコル

サーバーとクライアント間の通信はTCPストリーム上をS式をやりとりしています。

プロトコルの概要は server-maze.el の上の方のコメントに書いてありますので、telnet で接続して直接プロトコルをやりとりすることも出来ます。

なんだこれ

目的
  • アプリケーションプラットフォームとしての潜在能力の調査
  • Emacsに何が向いていて、何が向いていないかを知る
  • Emacsの限界
    • 計算能力
    • 描画、表現能力
    • リアルタイム通信

ちょっとEmacsの本気を調べてみようということでやりました。

調べている途中で、EmacsのVMやバイトコンパイルの内容まで興味が出てきたので、VMインストラクション表を作りました。中の人にとってもあった方が便利だと思うのですが、今後もメンテナンスが続くと良いですね。

結果
  • Java等に比べて遙かに遅い
    • Javaでレンダラーを書いた経験から、比較して相当遅い
      • 何となく1000倍くらい遅いかも
      • 数値計算はやっぱり得意でない
  • 単純なプロパティ書き換えはそんなに遅くない
    • 幾何計算はかなり遅かったが、プロパティ書き換えの全画面描画は期待したほど遅くなかった
    • シンプルなエフェクトアニメーションはいけそう
  • 非同期通信はconcurrent.elでサポートできそう
    • ネットワーク通信をメッセージパッシング的な仕組みで書いた
    • プログラミングのアーキテクチャとしてはかなりよさそう
    • 同時6接続ぐらいでは全然問題なかったので、リアルタイム通信は使えそう
  • Emacsでサーバーはおもしろい
    • 状況表示など見た目楽しい
    • screenで起動して操作
    • 動作中のサーバーのREPLに接続して書き換え!!! (shiroさん伝説?)

技術いろいろ

3D表現方法

元ネタは米チャ氏の名作激走ラビリンスからです。MSXではSCREEN1でぐりぐりやるのが当時の常套手段でした。

3Dはポリゴンを計算しているわけではなくて、レイトレーシングのように画面上の各点から透視方向へのスキャンを行って、壁や床を描画しています。

画面の描画は、画面全体をスペースで埋めて、背景色を一つ一つセットすることで表現しています。裏画面としてvectorで配列を持っておき、3D描画はそのvectorに対して行って、最後にバッファに転送しています。

文字

コマンドラインからbannerを実行してフォント情報を取ってきて、それを拡大縮小することで文字を書いています。計算に余裕がなかったので向きによる傾きなどは考慮してないです。

前後関係や壁の裏に隠れたりするアルゴリズムは、Zバッファを使っています。先の裏画面vectorと一緒にZ情報も持っています。

間に合えばTwitterからアイコンを取ってきて表示しようとか思っていたのですが、時間がありませんでした。技術的には検証していますので、 straightfoward に出来ると思います。

キー操作

普通にキーバインドから描画するとEmacsが固まってしまうので、キー入力はイベントを通知するだけにして、描画側は非同期にイベントを拾って処理するようにしました。これにより、ある程度なめらかなキー入力移動が実現できました。

通信

通信はTCPストリームとS式のやりとりを concurrent.el のイベント通知でラップして、メッセージパッシングによる非同期通信で行っています。通信フォーマットがS式だとやっぱり楽ですね。

今回はこの方法がすごくうまくマッチしました。プログラム的にもシンプルになり、非同期なので各クライアントとサーバーがお互いを気にせずに自分のペースで処理を進めることが出来るようになりました。

いろいろ実験してみましたが、特定のクライアントがもたついても全体に影響が出ないところがおもしろかったです。全然規模とか比較になりませんが、MMOとかこんな風になっているのかなとか考えたりしました。

ベンチ取って最適化

最適化は定石通り、一度全体が動くようになってから行いました。大体以下のようなことをやりました。

  • 幾何計算のキャッシュ(vectorへのアクセス)
    • Emacs上の計算は遅いので、出来るだけ先に計算しておく
  • インスタンス(vector)キャッシュ
  • ほとんどの計算を固定小数点演算で行う
    • 浮動小数点演算より整数演算の方が6倍程度早かったため
  • faceの色文字列のキャッシュ
    • ある程度種類を絞り込んで、テーブルから文字列を引いてくる。無限に色があると辛い。

後はバイトコンパイル結果を見て、ある程度VMの動きを考えながらコードを削っていきました。
まだまだ最適化・高速化の余地はあると思いますが、自分の中である程度目標が達成されたので、もうこれ以上はしないと思います。

どこへ行くのか

これはネタですので、今後Emacsでゲームを作ろうとか思っているわけではないです。

背景色で絵を描くというテクニックが結構使えそうだと分かったので、今後この方法で表現力を上げることが出来るかなと思っています。

あと、Emacs同士の通信をうまくやる方法が分かったので、バックグラウンドでスレッドのように立ち上げて何か他の処理をさせるとか、別のサーバーのEmacsと通信をしてTrampのようなことをするとか、既存のEmacsの形にとらわれない面白いことが出来るのではないかなとか思っています。