Canvas/WebSocketなネトゲの、ざっくりとしたロジック解説
Canvas/WebSocketでディアブロクローンなネトゲを作ってみた - mizchi logの解説
ちゃんと勉強して実装したわけじゃないけど、つくってみたい!と思った人が一通り実装できる程度の解説をする。
ゲームプログラミングとウェブプログラミングの初歩を知ってるとなお良い。(というか僕自身どっちも微妙なのだが)
ソースコードはこちら
GitHub - mizchi-sandbox/ws-netgame: WebSocketを用いたネットゲーム
前提として、CoffeeScript、WebSocketを使う
なお、以下のコードは概念を説明するために簡略化したもので、socket.io , coffeescriptの微妙な挙動については検証していない。あくまで擬似コードの一種として読むように。
MVCにわけて解説する。
Model
サーバーのセーブデータ。基本的にログイン中はオンメモリとして扱い、ウェブソケットのログアウト時に保存する。
セッション永続化にはRedisを使っている。node/expressなら普通の手法。
今回はMongoDBを使った。開発環境だとスキーマがしょっちゅう変わるので、落ち着くまで明確に規定しない。(高速に開発できる代償にアドホックな初期化コードを書く必要があり、初期化失敗すると再起動しないリスクが伴う)
作ったやつはnode-mongolianを使ってるが、落ち着いたらMongooseを使う予定。
コントローラ
サーバー側のゲームロジック。
まずロジックを書く
Game.coffee
class Game constructor : -> @cnt = 0 @players = {} @objects = [] update:-> @cnt++ @objects.push {x:Math.random()*100,y:Math.random()*100} unless @cnt%60 @ws() join:(id,params)-> @players[id] = new Player Math.random()*100, Math.random()*100,params leave:(id)-> delete @players[id] setkey:(id, key,state=off)-> @players[id]?.keys[key] = state ws: -> # ウェブソケットのためにオーバライドする関数 class Player constructor : (@x,@y,savedata = {})-> @keys = {} # キーステート @load_params savedata load_params : (savedata)-> # savedataからパラメータ構成 # モジュールとして登録 exports.Game = Game
既にキー状態の変更ロジックとか書いてる。
サーバー
app.coffee
game = new require("./Game").Game FPS = 30 game.ws = -> # JSON化してクライアントにブロードキャスト objs = ([i.x,i.y] for i in game.objects) .concat( [p.x,p.y] for _,p of game.players ) # オブジェクトとプレーヤーを1つのx,y座標のArrayに io.sockets.emit "update", {objs : objs} # ブロードキャスト # mainloopをぶん回す setInterval game.update , 1000/30 # ... express(WAF)等の設定 略 ######################## #websocketの設定 socket.on "setkey",(data)-> game.join(data.id,data.key,data.state) socket.on "connection",(data)-> game.join(data.id) socket.on "disconnect",(data)-> game.leave(data.id)
socket部分はうろ覚えなのでこのままじゃ動かないけど、まあこんな感じ。
クライアントではキーイベントを送る、だけ。
window.onkeydown (e)-> socket.emit "setkey", key:e.keyCode , state:on window.onkeydown (e)-> socket.emit "setkey", key:e.keyCode , state:off
他にもボタンをクリックしたイベントとか考えられる。
ロジック部分を見せないことでクライアントからのデータの改竄をふせぐ。
ビュー
クライアントのアニメーションレンダラ。WebSocketから送られるデータを描くだけで、ゲームロジックには触れない。疎結合にすることで使うフレームワークを選べる。今回は直書きしたが、enchant.js , three.js などを使ってもいい。
on "connection" でマップデータを、on "update" でオブジェクトデータを受け取る。
二通り考えた。
1. 完全同期、シンプルなイベントドリブン
socket.on "update",(data)-> render(data)
アニメーションを行いたい際は、クライアントからカウンタを送って同期を取る。
フレームレートはソケットを送る頻度に依存。
2. アニメーションを独立したフレームで描きたい場合
buffer = {} socket.on "update",(data)-> buffer = data main_loop = -> render(buffer) requestAnimationFrame main_loop main_loop()
setIntervalではなく、requestAnimationFrameを使う。モダンなブラウザならプレフィクス付きで持ってる。アニメーションに最適化されているsetIntervalと思えばいい。このメソッドは60FPS固定なので、実際にはカウンタ挟むなどして実行するフレームレートを調整する。
2の方がアニメーションを書く際に自然になると思う。1は格ゲーやシューティングとかかな。
応用としてキーステート履歴共有してフレームレート間の保管を行うことなどが考えられる。
どのようにレンダリングするか
ゲームロジックと密接で、どういう風にやるといいか考えていたのだけど、次のようなコードをサーバークライアント共に参照するといいかなと思った
ObjectId.coffee
# ひたすらID定義 ObjectId = Player : 1 Monster : 2 Wall : 101 Path : 102 Item : 103 get_enemy: (id)-> # 敵対フラグのロジック if(id is @Player) then return @Monster if(id is @Monster) then return @Player else then return null (window or exports).ObjectId= ObjectId
ゲームロジックからは敵対関係などのロジックを、レンダラーからはレンダリングするアニメーションとの関係を紐付ける。
ゲームのコア部分の説明は以上。疲れた。
今から「大規模MMOを支える技術」読む予定。タンスの奥に放ってあって、忘れていた。