日経ソフトウェア2014年10月号「HTML5でゲームを作ろう(第11回)」に掲載されたオンライン対戦型の「卓球ゲーム」です。
右の画像をクリックするとゲームページを表示します。
今回も、プログラムソースには手を加えていません。
2014/08/30
gameClient.js の33〜38行目を追加。
画面を閉じたメッセージをサーバーに送信。
オンライン対戦ゲームなので、相手がゲームページを開いてくれないとゲームができません。
・・・「対戦相手が来るまで待ってね」と表示されます。
2人がゲームページを開いていると、対戦可能です。3番目以降にゲームページを開いた方は、現在プレイ中の方が画面を閉じるまで(=サーバとの接続を切るまで)プレイできません。
・・・3番目以降に接続した方には「サーバから切断されました」と表示されます。
現在、プレイ中の方が、画面を閉じたとき、サーバー上のgameServer.jsがエラーを吐き停止してしまいます。
・・・今回、自宅サーバー上に構築したが、インターネット上では使い物になりませんでした・・・。
・・・ローカル環境(LAN内)では正常に動くのですが・・・。
(Nodeモジュールの WebSocket.js にバグがあるのかも???)
・・・client.sendを実行する部分でエラーになるようです。
・・・WAN側で2人接続後の切断時のみNG。
・・・単独(1人)接続時は画面を閉じてもgameServer.jsは停止しない。
と言う事は、→initGame()→gameLoop()に入ってから、クライアントが切断した際に起こるのか。
・・・う〜ん。よくわからん。
・・・ローカル環境(LAN環境)では問題なく動いたので、ルーター周りの設定ミスだろうか???
(でも、画面を閉じる前まではプレイできるので、それも考えにくい・・・。)
・・・Windows系のサーバーでは確認していないので、そちらの環境の方は設置して試してみて下さい。
遊び方は、画面下部に情報が表示されているように、Spaceキーでボールを打ちます。
赤色のラケットが自分のもので、カーソルキーの上下で動かすことができます。
3ポイント先取したほうが勝ちです。(打ち返せず3ポイント先取されたら負けです。)
ポイントは画面上部に表示されます。
勝敗が決まると画面下部に結果が表示されます。もう一度勝負したい場合は、Enterキーを押します。
(キー操作が必要なので、PCでないとプレイできません。)
卓球ゲームと言っても、ボールの軌跡は放物線は描かず(直線で)、ボールが上下の壁でバウンドするので、実際の卓球とは異なります。このようなゲームは「ポンゲーム」と呼ばれるそうです。
今回は、自宅サーバー機(CentOS 6.5)に、以下のコマンドで、Node.js、winsocket.ioを導入しました。
$ su -
# rpm -ivh http://ftp.riken.jp/Linux/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
# yum install nodejs npm --enablerepo=epel
# npm install websocket.io
サーバー機側では、以下のコマンドを実行して、JavaScriptをサーバサイドで動かしています。
# node /var/www/html/game/takkyu/gameServer.js
また、ファイヤーウォール(iptables)と光ルータの設定を変更し、ポートを開放しました。
ソースはこちら。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>TAKKYU</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
background-color: black;
color: white;
text-align: center;
}
#game {
margin: 10px;
}
</style>
<script type="text/javascript"
src="gameClient.js"></script>
</head>
<body>
<h3>* T A K K Y U *</h3>
<div id="score"></div>
<div id="game">
<canvas id="mainCV" width="600" height="300">
</canvas>
</div>
<div id="info"></div>
</body>
</html>
// クライアント側の卓球ゲームプログラム
// グローバル変数
var SERVER_URI = "ws://www.a21-hp.com:8888";
var DISP_W = 600, DISP_H = 300; // 画面サイズ
var ws; // WebSocketのオブジェクト
var playerNo; // プレイヤーID
var pNo = 0;
var cv, ctx; // <canvas>と描画コンテキスト
var ball, players; // ボールとプレヤーの情報
var resetAnime = -1; // 画面を光らせるためのカウンター
// 初期化イベント
window.onload = function () {
// 描画コンテキストの取得
cv = $("mainCV");
ctx = cv.getContext("2d");
// キーイベントの設定
window.onkeydown = keyDownHandler;
window.onkeyup = keyUpHandler;
// サーバーへ接続する
connectToServer();
};
// WebSocketサーバーへ接続する
function connectToServer() {
ws = new WebSocket(SERVER_URI);
ws.onopen = function () {
console.log("サーバーに接続");
};
ws.onclose = function (e) {
info("サーバーから切断されました");
};
ws.onmessage = commandFromServer;
// サーバーから切断(ブラウザのタブを閉じる)
window.addEventListener("beforeunload", function() {
pNo = Number(playerNo) + 1;
ws.send("disconnect=" + pNo); // WebSocket close
ws.onclose();
});
}
// サーバーから届くメッセージを処理
function commandFromServer(e) {
var data = e.data;
var ps = data.split(">", 2);
var cmd = ps.shift();
switch (cmd) {
case "msg":
info(ps[0]);
break;
case "you":
playerNo = ps[0];
break;
case "start":
resetAnime = 5;
info(ps[0]);
break;
case "draw":
var o = JSON.parse(ps[0]);
console.log(o.ball.x, o.ball.y);
ball = o.ball;
players = o.players;
draw();
break;
case "close":
ws.close();
info(ps[0]);
break;
case "gameover":
info("勝負あり!" +
((ps[0] == playerNo) ? "勝ち" : "負け") +
"...再勝負はEnterキー");
break;
default:
console.log(data);
}
}
// 画面の描画
function draw() {
ctx.clearRect(0,0,DISP_W,DISP_H);
if (resetAnime-- > 0) {
ctx.fillStyle
= (resetAnime % 2 == 0) ? "blue" : "purple";
ctx.fillRect(0,0,DISP_W,DISP_H);
}
ctx.strokeStyle = "gray";
ctx.lineWidth = 1;
ctx.strokeRect(0,0,DISP_W,DISP_H);
// 中央に線
var w = DISP_W / 2;
ctx.beginPath();
ctx.moveTo(w, 0);
ctx.lineTo(w, DISP_H);
ctx.stroke();
// プレイヤーのラケットを表示
for (var i = 0; i <= 1; i++) {
var p = players[i];
ctx.fillStyle = (playerNo == i) ? "red" : "gray";
var w2 = p.w /2;
ctx.fillRect(p.x - w2, p.y, p.w, p.h);
}
// ボールを描画
ctx.beginPath();
ctx.fillStyle = "white";
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
// スコア表示
$("score").innerHTML = players[0].score + " : " + players[1].score;
}
// キーの処理
function keyDownHandler(e) {
var code = e.keyCode;
if (code == 38) { // up
ws.send("key>" + playerNo + ">-1");
}
else if (code == 40) { // down
ws.send("key>" + playerNo + ">1");
}
else if (code == 13) { // Enter
ws.send("enter>" + playerNo);
}
else if (code == 32) { // Space
ws.send("space>" + playerNo);
}
}
function keyUpHandler(e) {
ws.send("key>" + playerNo + ">0");
}
function info(msg) {
$("info").innerHTML = msg;
}
function $(id) {
return document.getElementById(id);
}
// WebSocketによるゲームサーバー
var ws = require("websocket.io");
// グローバル変数の宣言
var WS_PORT = 8888; // Socketのポート番号
var DISP_W = 600, DISP_H = 300; // 画面サイズ
var BALL_R = 8; BALL_SPEED = 20; // ボールの情報
// ラケットの情報
var BAR_W = 16, BAR_H = 70, BAR_SPEED = 15;
var WIN_POINT = 3; // 勝ち点の指定
var timerId = 0; // タイマーID
var ball = {}; // ボール情報を管理
// プレイヤーとキー情報を管理
var players = [], keys = [];
var isStop = false, startPlayer = 0, isGameOver = true;
// WebSocketサーバーの起動
var server = ws.listen(WS_PORT, function () {
console.log("起動しました!\nport=" + WS_PORT);
});
// サーバーへの接続イベント
server.on("connection", function (client) {
console.log("clients=" + server.clientsCount);
// 最大接続数の確認
if (server.clientsCount > 2) {
client.send("close>人数オーバー");
client.close();
return;
}
if (server.clientsCount == 1) {
client.send("msg>対戦相手が来るまで待ってね");
client.send("you>0");
}
else if (server.clientsCount == 2) {
client.send("you>1");
initGame();
}
console.log("connect");
client.on('message', onMessage);
client.on('error', function (err) {
console.log('error:' + err);
});
});
// ゲームの初期化
function initGame() {
isGameOver = false;
isStop = true;
// 後から入った方が開始タイミングを決める
startPlayer = 1;
// ラケットの位置をセット
var p0 = {no:0}, p1 = {no:1};
p0.score = p1.score = 0;
p0.x = BAR_W * 2;
p1.x = DISP_W - BAR_W * 2;
p0.y = p1.y = Math.floor((DISP_H - BAR_H) / 2);
p0.w = p1.w = BAR_W;
p0.h = p1.h = BAR_H;
players = [p0, p1];
keys = [0, 0];
// ボールの位置をセット
ball.x = p1.x - BAR_W;
ball.y = p1.y + BAR_H / 2;
ball.r = BALL_R;
ball.dx = -1 * BALL_SPEED;
ball.dy = randSelect([1,-1]) * rand(BALL_SPEED);
// パラメータをクライアントに送信
sendDrawInfo();
// タイマーをセット
if (timerId == 0) {
timerId = setInterval(gameLoop, 100);
}
// 開始を合図
sendAll("start>ゲーム開始! Spaceキーで玉を打つ");
}
// ゲームループ
function gameLoop() {
moveBall();
movePlayer();
sendDrawInfo();
}
// プレイヤーの移動
function movePlayer() {
for (var i = 0; i < 2; i++) {
if (keys[i] == 0) continue;
var y = players[i].y + keys[i] * BAR_SPEED;
if (0 <= y && y < DISP_H -BAR_H) {
players[i].y = y;
if (isStop && startPlayer == i) {
ball.y = players[i].y + BAR_H / 2;
}
}
}
}
// ボールの移動
function moveBall() {
if (isStop || isGameOver) return;
ball.x += ball.dx;
ball.y += ball.dy;
var x = ball.x, y = ball.y;
// ボールの反射(Y軸)
if (y < 0 || y > DISP_H) {
ball.dy *= -1;
ball.y += ball.dy;
}
// ボールの反射(X軸)
var isHit = function (p, newPosX, dir, ep) {
var r = ball.r; // 当たり判定を少し甘く
var isOK = (p.y-r <= y && y <= p.y+p.h+r);
ball.x = newPosX;
ball.dx = BAR_SPEED * dir;
ball.dy = randSelect([-1,1]) * rand(BALL_SPEED);
if (!isOK) {
ep.score++;
isStop = true;
startPlayer = p.no;
sendAll("start>Spaceキーでボールを打つ");
ball.y = p.y + p.h /2;
if (ep.score >= WIN_POINT) {
gameOver(ep.no);
}
}
return isOK;
};
var p0 = players[0], p1 = players[1];
if (x < p0.x) {
isHit(p0, p0.x + p0.w, 1, p1);
}
else if (x > p1.x) {
isHit(p1, p1.x - p1.w, -1, p0);
}
}
// クライアントからのメッセージが来たとき
function onMessage(msg) {
var ps = msg.split(">");
var cmd = ps.shift();
if (cmd == "key") {
var no = parseInt(ps[0]);
var v = parseInt(ps[1]);
keys[no] = v;
}
else if (cmd == "enter") {
if (isGameOver) {
initGame();
}
}
else if (cmd == "space") {
if (ps[0] == startPlayer) isStop = false;
}
else {
console.log(msg);
}
}
//ゲームオーバー
function gameOver(winner) {
sendDrawInfo();
isStop = true;
starPlayer = winner;
isGameOver = true;
sendAll("gameover>"+winner);
}
// プレイヤーとボールの位置をクライアントに送信
function sendDrawInfo() {
var o = {"ball":ball,"players":players};
sendAll("draw>" + JSON.stringify(o));
}
// すべてのクライアントにデータを送信
function sendAll(msg) {
server.clients.forEach(function (client) {
if (server.clientsCount != 2) return;
if (client == null) return;
client.send(msg);
});
}
// 整数の乱数を発生する
function rand(c) {
return Math.floor(Math.random() * c);
}
// 配列内の要素のいずれかをランダムに返す
function randSelect(a) {
return a[rand(a.length)];
}