1 /** 2 * @fileOverview 3 * socket.enchant.js 4 * @version beta (2011/12/03) 5 * @require enchant.js v0.4.1+ 6 * @author UEI Corporation 7 * 8 * @description 9 * enchant.js extension for online game 10 * 11 * @usage 12 * see http://wise9.jp/archives/5659 13 */ 14 15 (function() { 16 enchant.socket = {}; 17 enchant.socket.Socket = enchant.Class.create({ 18 initialize: function(gameID, twitterID) { 19 // timeout(ms) 20 this.timeout = 10000; 21 22 if (gameID === undefined) { 23 if (location.hostname === 'r.jsgames.jp') { 24 gameID = location.pathname.match(/^\/games\/(\d+)/)[1]; 25 } else { 26 alert('gameID required. (it will be autodetected if you upload it to 9leap.net)'); 27 } 28 } 29 this.gameID = gameID; 30 // デバッグ用引数があるかどうか 31 if (twitterID !== undefined) { 32 this.twitterID = twitterID; 33 } 34 35 /* 36 * 変数初期化 37 */ 38 var socket = this; 39 this.matching = false; // マッチ中かどうか 40 this.timer = undefined; // timeout用タイマー 41 42 43 /* 44 * 通信系定義 45 * 1番アウトプットに近いレイヤーです 46 * XHRとPusherを隠蔽します 47 * @type {connection} 48 */ 49 var connection = function(pusherKey, url) { 50 var pusher = new Pusher(pusherKey); 51 52 var channelList = {}; 53 var callbackList = {}; 54 55 // JSONP用CallBack 56 window._onlineCallback = function(callbackPath) { 57 return function(data) { 58 if (typeof data === 'string') data = JSON.parse(data); 59 callbackList[callbackPath](data); 60 }; 61 }; 62 63 return { 64 // JSONPでAPIアクセス 65 send: function(roomType, apiName, option, func) { 66 callbackList[roomType + '-' + apiName] = func; 67 var jsonpCallback = '?callback=_onlineCallback("' + roomType + '-' + apiName + '")'; 68 console.log(socket.gameID); 69 var src = [url, 'api/online/', socket.gameID, '/', roomType, '/', apiName, '.json', jsonpCallback, '&twitterID=' + socket.twitterID].join(''); 70 71 if (option) 72 for (var key in option) src += '&' + key + '=' + option[key]; 73 74 var script = document.createElement('script'); 75 script.type = 'text/javascript'; 76 script.src = src; 77 document.head.appendChild(script); 78 }, 79 // WebSocketから受信する 80 receiveBinder: function(roomType, channelID) { 81 var pusherChannelID = socket.gameID + '-' + roomType + '-' + channelID; 82 channelList[pusherChannelID] = pusher.subscribe(pusherChannelID); 83 return function(channel) { 84 return function(eventName, func) { 85 channel.bind(eventName, function(data) { 86 if (typeof data === 'string') data = JSON.parse(data); 87 func(data.data); 88 }); 89 }; 90 }(channelList[pusherChannelID]); 91 }, 92 releaseBinder: function(roomType, channelID) { 93 var pusherChannelID = socket.gameID + '-' + roomType + '-' + channelID; 94 channelList[pusherChannelID] = pusher.unsubscribe(pusherChannelID); 95 } 96 }; 97 }('551141852cce2fe668d5', 'http://9leap.net/'); 98 99 100 /* 101 * Online APIのラッパー 102 * サーバーで実装しているAPIを関数化しています 103 * また、Pusherのbinderをpatt throughしています 104 */ 105 var apiClosure = function(connection) { 106 return function(roomType) { 107 var send = function(apiName, option, callback) { 108 connection.send(roomType, apiName, option, callback); 109 }; 110 111 var retObject = { 112 join: function(channelID, callback) { 113 if (channelID === undefined) channelID = -1; 114 115 if (channelID === -1) 116 send('join', {}, callback); 117 else 118 send('join', {channelID: channelID}, callback); 119 }, 120 list: function(callback) { 121 send('list', {}, callback); 122 }, 123 broadcast: function(data, channelID, callback) { 124 send('broadcast', {data: JSON.stringify(data), channelID: channelID}, callback); 125 }, 126 quit: function(channelID, callback) { 127 send('quit', {channelID: channelID}, callback); 128 connection.releaseBinder(roomType, channelID); 129 }, 130 pong: function(channelID, callback) { 131 send('pong', {channelID: channelID}, callback); 132 }, 133 binder: function(channelID) { 134 return connection.receiveBinder(roomType, channelID); 135 } 136 }; 137 return retObject; 138 }; 139 }(connection); 140 141 /* 142 * room処理のベースになるオブジェクト 143 * broadcastパケットに意味付け(送り元、宛先の付与)等を行っています。 144 * また、Pusherへの接続なども隠蔽しています。 145 */ 146 var roomClosure = function(apiClosure) { 147 return function(roomType) { 148 var api = apiClosure(roomType); 149 var channelID = -1; 150 var bind = {}; 151 152 var toPrev = ''; 153 154 var retObject = { 155 // public 156 api: api, 157 setToPrev: function(_toPrev) { 158 toPrev = _toPrev; 159 }, 160 onreceive: function(data) { 161 162 }, 163 onjoin: function() { 164 165 }, 166 onresult: function() { 167 168 }, 169 onquit: function() { 170 171 }, 172 /* 173 * 一般的なjoin実装 174 * channelIDが指定されれば送り、指定されなければ送らない 175 * また、pusherへのbroadcastAPIからのbindingもここで処理しています。 176 * channelIDが指定されれば送り、指定されなければ送らない 177 */ 178 join: function(_channelID) { 179 var joinCallback = function(data) { 180 if (data.code === 200) { 181 socket.twitterID = data.twitterID; 182 channelID = data.channelID; 183 bind = api.binder(channelID); 184 bind('MSG', function(data) { 185 if (data.playerID !== socket.twitterID) 186 return; 187 retObject.onreceive(data); 188 }); 189 bind('PING', function(data) { 190 api.pong(channelID, function() { 191 }); 192 }); 193 194 retObject.onjoin(data); 195 } else { 196 console.log('error: cannot connect'); 197 } 198 }; 199 200 if (_channelID === undefined) { 201 api.join(-1, joinCallback); 202 } else { 203 api.join(_channelID, joinCallback); 204 } 205 }, 206 // broadcastパケットに送信元、送信先を付与します。 207 broadcast: function(data, to) { 208 var broadcastCallback = function(data) { 209 retObject.onresult(data); 210 }; 211 212 if (to === undefined) 213 if (toPrev === '') { 214 console.log('error'); 215 return; 216 } else { 217 data.playerID = toPrev; 218 } 219 else 220 data.playerID = to; 221 222 toPrev = data.playerID; 223 224 data.myPlayerID = socket.twitterID; 225 api.broadcast(data, channelID, broadcastCallback); 226 }, 227 quit: function() { 228 api.quit(channelID, retObject.onquit); 229 } 230 }; 231 return retObject; 232 }; 233 }(apiClosure); 234 235 236 /* 237 * lobbyの実体 238 * 1:1 でのランダムマッチの実装を行っている部分 239 * onjoin等イベントリスナーになっている箇所を書き換えるとlobbyから勝手にgameRoomへ入る処理が崩れるのでご注意を 240 */ 241 var lobby = roomClosure('lobby'); 242 243 /* 244 * 自分が入室したときに呼ばれる 245 * マッチングできる人がいればランダムマッチング 246 * いなければ、JOINイベント待機 247 */ 248 lobby.onjoin = function(data) { 249 // 対戦可能プレーヤの確認 250 if (data['playerList'].length !== 0) { 251 setTimeout(function() { 252 socket.matching = true; 253 lobby.broadcast({op: 'apply'}, data['playerList'][Math.floor(Math.random() * data['playerList'].length)]); 254 socket.timer = setTimeout(function() { 255 socket.matching = false; 256 }, socket.timeout); 257 }, 1000); 258 } 259 }; 260 261 /* 262 * メッセージを受信したときに呼ばれる 263 * マッチング確認 264 * 自分宛でなければ無視 265 */ 266 lobby.onreceive = function(data) { 267 if (data.playerID !== socket.twitterID) 268 return; 269 270 if (data.op === 'apply' && socket.matching === false) { 271 socket.matching = true; 272 setTimeout(function() { 273 lobby.broadcast({op: 'accept'}, data.myPlayerID); 274 socket.timer = setTimeout(function() { 275 socket.matching = false; 276 }, socket.timeout); 277 }, 1000); 278 } else if (data.op === 'accept' && socket.matching === true) { 279 clearTimeout(socket.timer); 280 gameRoom.onjoin = function(joinData) { 281 lobby.broadcast({op: 'goGameRoom', channelID: joinData.channelID}); 282 lobby.quit(); 283 socket.matching = false; 284 gameRoom.member[0] = [data.myPlayerID]; 285 }; 286 gameRoom.join(); 287 } else if (data.op === 'goGameRoom' && socket.matching === true) { 288 console.log(data); 289 clearTimeout(socket.timer); 290 lobby.quit(); 291 socket.matching = false; 292 gameRoom.member[0] = [data.myPlayerID]; 293 gameRoom.onjoin = function() { 294 gameRoom.broadcast({op: 'ready', isLead: false}, data.myPlayerID); 295 gameReceiveList['ready']({op: 'ready', isLead: true}); 296 }; 297 gameRoom.join(data.channelID); 298 } else { 299 console.log('error : ' + data.op); 300 console.log(socket.matching); 301 } 302 }; 303 304 /* 305 * デフォルトの動作ではなく、channelIDが指定されなかったらすでにchannelが存在しているかどうか確認して、あれば入るような実装。 306 */ 307 lobby.join = function(join) { 308 return function(channelID) { 309 if (channelID === undefined) { 310 // 既存のチャネルの確認、あればつなぐ。なければ新規取得 311 lobby.api.list(function(data) { 312 if (data['code'] == 403) { 313 window.location.replace('http://9leap.net/api/login?after_login=' + window.location.href); 314 return; 315 } 316 if (data['channelList'].length === 0) 317 join(); 318 else 319 join(data['channelList'][0]); 320 }); 321 } else { 322 join(channelID); 323 } 324 }; 325 }(lobby.join); 326 this.lobby = lobby; 327 328 329 /* 330 * gameRoomオブジェクト定義 331 * ユーザーによるイベント管理をbroadcast上のパケットに構築 332 * そのイベント管理方法等を付与した 333 */ 334 var gameRoom = roomClosure('gameRoom'); 335 var gameReceiveList = {}; 336 gameReceiveList['ready'] = gameRoom.onready; 337 gameRoom.onready = function() { 338 }; 339 gameRoom.member = []; 340 gameRoom.onreceive = function(data) { 341 var op = data.op; 342 //delete data.op; 343 if (op === 'ready') { 344 gameRoom.setToPrev(data.myPlayerID); 345 } 346 gameReceiveList[op](data); 347 }; 348 gameRoom.push = function(eventName, data) { 349 data.op = eventName; 350 gameRoom.broadcast(data); 351 }; 352 gameRoom.addEventListener = function(eventName, func) { 353 gameReceiveList[eventName] = func; 354 }; 355 this.gameRoom = gameRoom; 356 } 357 }); 358 359 })();