Edited at

phina.jsでの多人数参加型ゲームの作製

More than 1 year has passed since last update.


今回使う技術の要素


クライアントサイド


phina.js

ver0.2.0

今回の主役

http://phinajs.com/

日本産のjavascriptでのゲームライブラリ

これを用いて多人数参加型ゲームをどう作るかという例


通信


websocket

リアルタイム通信を行う方法はいくつもあるが、今回用いるのはこれ

通信プロトコルの一種でブラウザが古いものでは動作しない点に注意。

細かい話は省略

今回はphina.jsと連携させる実装例を紹介します。


サーバーサイド


Spring-framework

ver4.3.2

Javaの「Spring」を扱うためのフレームワーク

websocketを扱えるということで今回サーバーサイドに使用


目標

多人数参加型ゲームを作るにあたって今回は要素として必要な通知機能を作成する。


詳細

画面上に「ベル」ボタンがあり、押下すると同じページを開いている全員に音が鳴る


製造

他のページで説明されていることはどんどん省略していく


サーバーサイド

http://acro-engineer.hatenablog.com/entry/2014/01/17/170607

を参考にTextWebSocketHandlerを継承し特定のメッセージ(「a」一文字とかでよい)が来たら音を鳴らすメッセージ(「b」一文字とかでよい)をブロードキャストするハンドラーを作成。

web.xmlなどを適宜設定

以下設定の必要な部分抜粋


web.xml

    <servlet>

<servlet-name>bell</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>bellWebSocket</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>bell</servlet-name>
<url-pattern>/bell</url-pattern>
<url-pattern>/bell.jsp</url-pattern>
<url-pattern>/bell.html</url-pattern>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>bellWebSocket</servlet-name>
<url-pattern>/ws</url-pattern>
</servlet-mapping>


bell-servlet.xml

    <context:component-scan base-package="hoge.bell.controller" />

<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>



bellWebSocket-servlet.xml

    <websocket:handlers>

<websocket:mapping path="/ws" handler="bellHandler" />
</websocket:handlers>

<bean id="bellHandler" class="hoge.bell.handler.BellHandler" />



Bell.java

@Controller

public class Bell {
@RequestMapping("/bell")
public ModelAndView index() {
return new ModelAndView("bell");
}
}


BellHandler.java

public class BellHandler extends TextWebSocketHandler {

/** セッション一覧 */
private Map<String, WebSocketSession> sessionsMap = new ConcurrentHashMap<String, WebSocketSession>();

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
this.sessionsMap.put(session.getId(), session);
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
this.sessionsMap.remove(session.getId());
}

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String mes = message.getPayload();
if (mes.equals("a")) {
TextMessage broadcastMessage = new TextMessage("b");
for (Entry<String, SessionInfo> entry : this.sessionsMap.entrySet()) {
entry.getValue().sendMessage(broadcastMessage);
}
}
}
}


などとしてwebsocketのアクセスに/wsを、ページの表示に/bellを割り当てる


クライアントサイド

こちらが今回のメイン

websocketを生成しonloadでGameAppをrunする。

onmessageの処理とphina.jsを連携させる。


bell.jsp

<html>

<head>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://cdn.rawgit.com/phi-jp/phina.js/v0.2.0/build/phina.js"></script>
<script src="./js/bell.js"></script>
</head>
<body>
</body>
</html>


bell.js

    phina.globalize();

var SOUNDS = {
sound: {
'bell': './se/bell.mp3',
},
};
phina.define('WebsocketGameApp', {
superClass: 'GameApp',
init: function(params) {
this.superInit(params);
if (params.ws) {
this.ws = params.ws;
}
if (params.mesHandler) {
for (header in params.mesHandler) {
this.addMesHandler(header, params.mesHandler[header]);
}
}
},
send: function(mes) {
if (this.ws) {
this.ws.send(mes);
}
},
addMesHandler: function(header, handler) {
this.on('mes' + header, handler);
},

_accessor: {
ws: {
get: function() { return this._ws; },
set: function(ws) {
this._ws = ws;
var that = this;
this._ws.onmessage = function(mes) {
that.flare('mes' + mes, {});
};
},
},
},
});
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(params) {
this.superInit(params);
var that = this;

this.bell = Button({
text: 'ベル',
height: this.gridY.span(2),
width: this.gridX.span(5),
fontSize: 16,
})
.addChildTo(this)
.setPosition(this.gridX.center(), this.gridY.center());
this.bell.onpush = function(){
that.app.send('a');
};
},
});
phina.main(function() {
var ws = new WebSocket("ws://yourdomain/contextPath/ws");
// アプリケーション生成
var app = WebsocketGameApp({
startLabel: 'main', // メインシーンから開始する
assets: $.extend(true, {}, SOUNDS),
ws: ws,
mesHandler: {
b: function() {
AssetManager.get("sound", "bell").play();
},
},
});
ws.onopen = function() {
// アプリケーション実行
app.run();
};
ws.onclose = function() {};
ws.onerror = function(e) {
alert("エラー");
};
});


今回の実装では新たにWebsocketGameAppを定義した。

mesHandlerにオブジェクトとして渡してやり、キーのメッセージが来た際に行う処理をバリューに渡す。

これはメッセージによる処理の判定を外部から注入できるようにするためで、

GameApp内部で規定してしまっても問題はない。

メッセージとしてくる内容がヘッダーとボディーに分かれてボディーを引数として渡してやりたい場合はwsのセッターを


bell.js

                set: function(ws) {

this._ws = ws;
var that = this;
this._ws.onmessage = function(mes) {
var header = mes.split(SEPARATOR)[0];
var body = mes.split(SEPARATOR)[1];
that.flare('mes' + header, {
body: body,
});
};
},

などと変えてやりhandlerの引数で以下のように受け取る


bell.js

            mesHandler: {

sound: function(arg) {
// ここでは引数に音の種類を表す文字列が入ってくるものとする。
// 上記ベルと同じ処理ならばSEPARATOR = ","の場合「sound,bell」のメッセージが渡されるということである。
AssetManager.get("sound", arg.body).play();
},
},

今回オブジェクトのbodyとして渡しているが、ボディをそのまま渡してもよい。