Hatena::ブログ(Diary)

mizchi log

@mizchiの雑記帳

2012-02-06

テストフレームワークmochaとファイル監視watchrで自動テスト

f:id:mizchi:20120206095924j:image

npm install mocha -g
npm install should

mochaの--watchオプションが期待通り動けば問題ないんだけど、ホットリロード動いてないのでファイル監視はwatchrでやらせることにした。
guardでもよかったんだけど、guardは皆決まりきったサンプル動かしてる人達が多くて、独自な挙動をとらせようとするとRuby詳しくない自分にはwatchrの方が取り回しがよかった。

gem install rb_fsevent watchr

rb_fseventはMacの場合。それ以外の環境だと別のモジュール(ぐぐれ)が必要

たぶんgrowlnotifyが必要
Growl - Extras

mochaに渡している項目はこんな感じ。

mocha -c --reporter list -r should --ignore-leaks --growl
    • ignore-leaksしてるのはmochaの仕様かはわからないけど、どのクロージャからも参照されない変数があるとリークしてるよ〜って警告が出る。

watchr保存時に対応したspecファイルを自動で実行されるようにする。フォーマットは*.spec.coffeeみたいな感じ。
プロジェクトのルートで設定ファイルを書く。

spec.watchr

watch("(lib|test)(/.*)+.coffee") {|m|
  fname = m.to_s.split('/')[-1].split('.')[0]+".spec.coffee"
  puts "======= Tests run #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"

  if File.exist?("test/#{fname}")
    print  `mocha -c --reporter list -r should --ignore-leaks --growl test/#{fname}`
  end
}

Rubyあまり書かないのでうろ覚えな感じで書いた。


ファイルの変更の監視を開始する。

$ watchr spec.watchr

これでlib/Character.coffeeを編集するとtest/Character.spec.coffeeが実行されるようになり幸福が実現した。

2012-02-02

node/webosocketによるオンラインゲームの実装を考える / オンメモリ、KVS、RDBMS、圧縮プロトコル、そのゲームデザイン + 就活の話


派手で見栄えがする大規模なプロダクトを作ろう!っていうことで、一人でフルスタックなネトゲを作っている。大きなプログラムを書いても破綻しないようにテスト書きまくってテストファーストを心がけたり、Travis-CIによる継続的インテグレーションで頑張ったり。

というわけで作っているのはMMORPGなんだけど、ここで実装するのはまあ平均的なMMORPGを想像してもらいたい。自分がやろうとしているのは、モダンなOSSとさくらの安いVPSで、独学の学生一人でもフルスタックなネトゲみたいなのが組める、ということの実証。

なんでそんなことをしているかって言うと、一応就活中で、見栄えがするアプリ提出できるとおいしいなーっていう下心。


*追記* ここでは https://github.com/mizchi/wanderer のことを言ってるんだけど大規模リファクタリング中なのでここで言ってることは半分ぐらいしか実装してない

前置き

自分が得意なのでnodeで実装しているけど、 たぶんC++/TCPで組んでも考えることは同じだと思われる。
Web+DB的なことは、はてなのホッテントリウォッチャーとしてひと通り目を通していて、オンラインゲームを支える技術、大規模ウェブサービスを支える技術をナナメ読みした程度で、高負荷サービスを自分で運用したことはないんだけど、これに関しては手元でチューニングしつつ色々考えたことをまとめる。

WebSocketで如何に効率よくデータを配るか

基本的な考え方は、サーバー上にはステートマシンとしてすべての状態を持つゲームロジックがあり、メインループが回ってデータが更新され続けている。プレイヤーはそこにキー情報やクリック情報を送って自分のキャラクターを操作する。

Apacheなどと違ってnodeによるセッションはコネクションを維持し続けるので、Cometのような擬似ポーリングをする必要がない。通信のオーバーヘッドが少ないので、小さいデータを断続的に送るのに向いている。

小さなデータを断続的に送るのと、ある程度まとまったデータをドンと送るのはシチュエーション次第。
マップにログインしたときはマップをレンダリングするための配列情報が必要だし、基本的にその後変化することはない。というか変化する部分と切り離す必要がある。そこでインタラクションを取りたい場合は、その都度再構成するのが良いと思う。

1ネームスペース、1マップ

nodeでwebsocketを使う際は socket.ioを使うと思う。socket.ioではネームスペースを区切ることが出来る。ネームスペースを作成すると、そのネームスペースに属している接続に、ブロードキャストすることができる。これによって必要な範囲に必要な分量だけデータを送りつけることが出来る。

websocketで配るようなデータは基本的にキャッシュがきかない(動的に変化する)ので、フロントサーバーを置く必要性は感じない。nginxが対応するまではnode-http-proxyなどを使って直に晒すのがいい。サーバーに担当してもらうのは画像やjs(たぶん分量が多い)の配信だ。

nodeで書くと嬉しいのは、圧縮、復元を同じロジックで書いて使いまわせること。
たとえば、僕はまず通信用に圧縮変換テーブルを作った。それらを プレーヤーネーム、プレーヤID、x座標、y座標、残りHP率、アニメーションフラグを1つの配列に押し込んで、それらをさらにnode-msgpackで圧縮しクライアントに送りつける。クライアントでも同じロジックを使って再現し、同じ方式で圧縮してサーバーに送る。

あとは演出上、サーバーと同じロジックをクライアントもある程度持つ必要がある。
AがBに投射攻撃をしたいとする。このとき、AからBへ攻撃が発動可能ならばアイコンを緑に、発動不可ならばアイコンを赤にしたい、とする。
サーバーではAが所属するマスからBのマスへのブレゼンハム線分を引き、障害がなければ発動する、というロジックで動いている。このコードをクライアントでも使いまわせると、サーバーへ「それが可能であるか、可能であるならばどういうデータ構造で表現するか」という問い合わせを減らすことが出来る。もちろん、基本的にマスターデータはサーバー上にあり、クライアントのデータとはズレがあるのだが、通信のラグが有ることをプレーヤーも認識せざるを得ない以上、納得できる話ではあると思う。

clusterによる分散

nodeにはclusterというモジュールで別のnodeのシングルコールバックのメインループとTCPで通信する機能を持っている。これも一定の単位で処理を分散するのに使えるだろう。
とはいえ、スレッド分割でスケールするのがコア数までなので、諦めて無難にサーバー側でスケールするのがよい。

nodeのボトルネック V8拡張 どこでトレードオフを取るか

敵モンスターに毎フレームA*による経路探索させると、計算が重すぎてラグった。1秒に1回、探索打ち切りも早めに制限することにした。実際には、ある程度近ければ一秒に一回プレイヤーの方向を向き直り向かってくる、という挙動になったので、ゲーム的にはちょうどよかったのだが…。

V8とC++ではさすがに10倍ほどの速度差が出る。これを考えるとC++でA*アルゴリズムを書きなおせば単純に毎秒10回は経路探索しても構わなくなる。nodeはnodeという名前通り、APIを介する役割だけを追って、重いアルゴリズムをnodeのV8拡張で書きなおせば、かなりボトルネックは解消される。
非同期メソッドにするとより良いだろうが、そうするとゲームデザイン側で工夫しなければいけない。たとえば、ニューラルネットで書かれた状況判断エンジンに次の行動を選ばせて、その結果が出るまで前の結果を繰り返し続ける、とか。結果が遅延することが許されないといけない。

nodeを使ってる以上、ある程度のところで妥協するのが妥当で、本当にここだけやばい!みたいな箇所でピンポイントでC++に置き換えるのがいいだろう。とはいっても、3Dの物理エンジンを使いたいのだったら全部nodeで書くのは無謀で、バックにbulletなどを置いてV8経由でAPIを提供する方が良いだろう。

リレーショナルか、KVSか

モデルはJSONで保存されている。開発中はnstore(ローカルのKVS)を使っているが、Redis、Kyoto-TyrantなどのKVSを使うことを考える。このときリレーショナルなDBMを使う必要はあまりない。基本的にプレイヤーネームとセーブデータは一対一に紐づいているおり、内部パラーメータでクエリを投げる必要は考えなくていいとすると、探索コストが低く実装も容易なKVSで組んでしまうのが手っ取り早い。リレーショナルである必要がないものをリレーショナルで組むと、実装が無駄にややこしくなる(バグを含みやすい)。

リレーショナルなDBMが必要なシチュエーションは、たとえばオークションシステムを導入したいとする。持つべき情報は 出品者ID、アイテムID(アイテムIDを主キーとした別テーブルあり)、希望価格、残り時間等だろうか。こういうものは価格順や種別等でソートできる必要があるだろうからリレーショナルが適していると思われる。

どこからDBで、どこからメモリか

キャメルケースはクラスってことで、だいたいの概念図

Character - 複雑な状態を持つ
JSON <-> CharacterModel -> Player(extends Character)

Stage 
  - map:Map
  - players: [Player (extends Character)]
  - monsters: [Monster (extends Character)]
  - connections:[Stage]


CharacterModelはJSONのデータを内部にコピーし、書きだす機能を持つ。CharacteModelはJSONのデータに基づいてCharacterクラスのインスタンスを生成してメモリに乗せる。CharacterModelの持つ情報はJSONで表現可能なデータフォーマットに基づくが、Characterはその限りではない。Playerと紐付くCharacterはシングルトン化する。monstersはCharacterModelを介さず、直接パラメータを打ち込んでCharacterクラスを生成する。

Stageは上記で述べたように1つのネームスペースと対応していて、「D砂漠・東」などのゲーム的な名前を持っている。connectionsは隣接するStageとその接続点の情報を持つ。Mapは設置オブジェクト情報、当たり判定、などのゲーム的なデータを持ち、Characterから参照されている。

Characterのゲームロジックの中でのアクションによってCharacterModelが更新され、CharacterModelが更新されるとCharacterのオブジェクトも再構築される。
CharacterModelの更新はDBアクセスなため、できるだけ控えたい。たとえば敵を倒して経験値を入手する、というイベントは高頻度に発生すると考えられるが、一回の経験値の入手自体は重要度は高くない。
更新しないといけないのは、たとえばレベルアップのロジック。CharacterクラスはCharacterModelから引き継いで経験値情報を持ち、規定値に達してレベルアップした際にモデルを更新、DBへ反映し、Characterクラス自身も新しいステータス情報を元に更新する。

他には

  • 15分に一度(不慮の事態でロールバックした時、プレイヤーのモチベーションが落ちないような頻度)
  • レアアイテムの入手
  • シナリオフラグの変更(ボス撃破)

こうしておけばほとんどDBに負荷をかけずに済む。その分メモリの負荷がきついが、その場合はスケールするか、頑張って圧縮するしかない。マップを適切に区切ると負荷が調節できる。が、ROのプロンテラやウルティマオンラインのブリ三叉路みたいに超過密地帯はサーバー負荷的にはきついだろうが、そういう場所があることでプレーヤーのお祭り感が演出できるとも言えるだろうし、ベンチマークにもなる。


以上、作りながら考えていたこと。書き殴りになったけど、参考になれば。

最後に


大学生で就活中です。あんまり大手企業に惹かれないので、楽しそうなベンチャーを見て回っています。そういえば昨日カヤックさんのワンクリック就活出しました。ワンクリックだったので。(他に面接待ちも一件あるけど)
とりあえず遊びに来いやっていうお誘いがあれば mizcki@gmail.comにメールください。JavaやLLならどの言語でもそこそこ触れます。得意なのはnodeとpythonです。C++等低レベルは苦手です。絵を書いたりフォトショ触ったりは出来ませんが教養程度にはHTML/CSS組めます。鯖管ほどじゃないですがLinux(Debian系)いじれます。

企画は経験値低いですが興味はあります。とくにゲームの企画興味ありますが、普通のソーシャルゲーは苦手です。トラビアンは好きでした。どっちかっていうとコンシューマゲームの方が好きなんですが、そうなるとスキルがミスマッチな気がするので誰かアドバイスください。ネットウォッチ力は高いです。


勉強会出る方ではなくそれ系のコネが少ないのでブログで書きました。疲れた。

2012-01-29

非同期メソッドを書きやすく拡張されたIcedCoffeeScript が登場


IcedCoffeeScript

IcedCoffeeScriptなんてのができていた。(フォーク元は最新のv1.2.0)
非同期関連を書きやすくしたもの。その意味ではtame.jsなどとコンセプトは同じ。
生成されるコードはIcedCoffeeScript -> CoffeeScript

使い方

公式サンプルより

search = (keyword, cb) ->
  host = "http://search.twitter.com/"
  url = "#{host}/search.json?q=#{keyword}&callback=?"
  await $.getJSON url, defer json
  cb json.results

await の非同期関数の返り値を、defer 受け取ってそのクロージャに吐き出す。直列に記述されているようだが、実際にはawait以下はコールバックの中になる。(その操作を隠蔽している)
非常にシンプル。

await for ~も使えるらしい。

out = []
await 
  for k,i in keywords
    search k, defer out[i]


雑感

生成されるコードを覗くと、resultとかlaunchとかの名前空間を平気で汚していて、せめてアンダーバープレフィックスを付けてほしい気持ちはある(CoffeeScriptは_refs1みたいな感じ)

自分はちょっとまだ信頼できないので使わないかなといった印象
これがメジャーになるのは考えにくいとは思うが、本家でも議論されている非同期メソッドAPI提案として実装されたのはGJだと思う。本家で採用されたら使ってもいい、ぐらい。

そういえば自分もこんなことできるやつを書いてみた、けど使ってない。

fs = require 'fs'
main = ->
  err,txt1 <- fs.readFile 'hello1.txt', next
  err,txt2 <- fs.readFile 'hello2.txt', next
  err,txt3 <- fs.readFile 'hello3.txt', next
  console.log i.toString() for i in [txt1,txt2,txt3]
main()

CoffeeScriptの文法拡張して非同期でネストが深くならないようにしてみた - mizchi log


みんなが納得できるシンタックスが提案されて採用されるといいですね

続・gl.enchant.js を触ってみた


TPSカメラっぽいができた。拡大縮小・回転に対応した。
http://mizchi.github.com/3dcam.html

ソース
https://github.com/mizchi/mizchi.github.com

とりあえず9leapに置いても良かったけど、あそこにはなんとなく動くものを置いたほうがいいのかと思った

正直なんで動いてるのかちゃんとわかってない
いまんところバグなのか自分のミスなのかわからないところ

  • Colladaが動かない(daeファイルを読み込んだ時点で壊れる)
  • primitiveのPlaneオブジェクトが光を反射しない(GL系の仕様なのか???)
  • 普通に回転させようとするとカメラの水平がずれるの だが 、フレームごとにupVectorZを+=Math.PI/4すると治るのだが、ねじれる理由はわかるのだが、治る理由がわからない(適当に変数突っ込んでたら治った)

公式サンプルとか、 gl.enchant.js で2Dキャラの表示をしてみた - 強火で進めみてると皆さんdae動いてるので、自分のせいかと疑ってるのだが…
一応公式に追従している

マップ生成


この前作ったネトゲのインターフェースをこっちに差し替えようとしてるのだけど
グリッドベースの自動生成アルゴリズム使ってるけど、範囲が狭いせいかそれっぽくならない。
というかグリッドベースでセルごとにオブジェクトを置くのは難しいと思われるので、オブジェクト数を減らすためには大きな板を複数作成テクスチャを張るほうがよさそう
(ネトゲだと当たり判定はサーバーが持つので、マップ生成時に同時に当たり判定を作っている)


マップ生成アルゴリズムを、大きな橋+中継ポイントで書きなおしてみる