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を支える技術」読む予定。タンスの奥に放ってあって、忘れていた。