公開ソースコード - SSE/PHP/JavaScriptでオンラインゲームを実装する

開発/実行環境

OS
Windows 7
サーバー言語
PHP
言語バージョン
ローカル 5.6.30
リモート 5.6.21
実行環境
Xampp v5.6.30
開発環境
Brackets
サーバー
ローカル Apache/2.4.25
リモート Apache
RDBS
ローカル MySQL Ver 15.1 Distrib 10.1.21-MariaDB
リモート MySQL Ver 14.14 Distrib 5.6.11, for Linux

ディレクトリ・ファイル構成

SSE/PHP/JavaScriptによるオンラインゲーム・デモページ

  root(http://wiz-code.digick.jp/dev/html5/stream/online-game/)
   │
   ├─.htaccess
   │
   ├─pdo-config.php
   │
   ├─lobby.php
   │
   ├─redirect.php
   │
   ├─game-field.php
   │
   ├─request-alive-list.php
   │
   ├─start-game.php
   │
   ├─send-event-data.php
   │
   ├─game-data-stream.php
   │
   ├─event-data-stream.php
   │
   ├─constants.php
   │
   ├─functions.php
   │
   └─Data_Stream.php
  
  ────────────────────────────────
  root(http://wiz-code.digick.jp/)
     │
     ├─audio
     │   │
     │   ├─bgm
     │   │   Battle-impalpable_loop.ogg
     │   │
     │   └─sound
     │        │
     │        ├─powerdown02.mp3
     │        │
     │        └─shoot1.mp3
     │
     ├─css
     │   online-game-lobby.css
     │  
     ├─js
     │  │
     │  ├─online-game-lobby.js
     │  │
     │  └─online-game.js
     │
     ├─lib
     │   │
     │   ├─jquery-2.2.2.min.js
     │   │
     │   ├─bootstrap-3.3.1.min.js
     │   │
     │   ├─bootstrap-3.3.1.min.css
     │   │
     │   ├─underscore-1.8.3.min.js
     │   │
     │   └─phina-0.2.0.min.js
     │
     └─log
         debug.php
                

データベースの構成

データベース名
online_game
エンコーディング/照合順序
utf8_general_ci

テーブル一覧

game_list

カラム名 種別 ヌル(NULL) デフォルト値 その他
no INT いいえ なし 主キー/AUTO_INCREMENT
id varchar(200) いいえ なし インデックス
data BLOB いいえ なし

player_list

カラム名 種別 ヌル(NULL) デフォルト値 その他
no INT いいえ なし 主キー/AUTO_INCREMENT
id varchar(200) いいえ なし インデックス
game_id varchar(200) いいえ なし インデックス(id, game_idの複合インデックス)
data BLOB いいえ なし

projectile_list

カラム名 種別 ヌル(NULL) デフォルト値 その他
no INT いいえ なし 主キー/AUTO_INCREMENT
id varchar(200) いいえ なし インデックス
game_id varchar(200) いいえ なし インデックス
data BLOB いいえ なし

event_list

カラム名 種別 ヌル(NULL) デフォルト値 その他
no INT いいえ なし 主キー/AUTO_INCREMENT
game_id varchar(200) いいえ なし インデックス
data BLOB いいえ なし

ソースコード

.htaccess

<Files ~ "^pdo-config\.php$">
Deny from all
</Files>
                

pdo-config.php

<?php

/* データベース接続情報 */
$dbname = 'online_game';
$host = '127.0.0.1';
$dsn = "mysql:dbname={$dbname};host={$host};charset=utf8";
$username = 'wizard-code';
$password = 'password';
$driver_options = array(
	PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
	PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
	PDO::ATTR_EMULATE_PREPARES => false,
);

?>
                

lobby.php

<?php

date_default_timezone_set('UTC');
/* スクリプトの実行時間 */
set_time_limit(60);
/* セッション・クッキーはブラウザ終了で破棄。HTTPSであれば第4引数はtrueを指定する */
session_set_cookie_params(0, '/', '', false, true);

require_once('pdo-config.php');
require_once('constants.php');
require_once('functions.php');

header('Content-type: text/html; charset=utf-8');

@session_start();

/* ページの初回訪問者への処理 */
if (!isset($_SESSION['client_id'])) {
	$_SESSION['client_id'] = random_str();
}

/* 推測困難なランダム文字列をノンスとする */
$nonce = random_str();
$_SESSION['nonce'] = $nonce;

/* 現在のモードを「待機中」に指定 */
$_SESSION['mode'] = 'waiting';

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	$sql_game_number = "SELECT COUNT(*) FROM game_list";
	$stmt = $pdo->prepare($sql_game_number);
	$stmt->execute();
	
	$game_number = $stmt->fetchColumn();
    $game_number = intval($game_number);
	/* ゲームデータが存在しない場合は新規に作成 */
	if ($game_number === 0) {
		$sql_insert_game_data = "INSERT INTO game_list (id, data) VALUES(:id, :data)";
		
		for ($i = 0; $i < MAX_GAME_FIELD_LIMIT; $i++) {
			$id = random_str();
			
			$game_data = array(
				'id' => $id,
				'state' => 'inactive',
				'start_time' => 0,
				'remaining_time' => 0,
				'player_number' => 0,
			);
			
			$serialized_data = serialize($game_data);
			
			$stmt = $pdo->prepare($sql_insert_game_data);
			$stmt->bindParam(':id', $id, PDO::PARAM_STR);
			$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
			$stmt->execute();
		}
	}
} catch (PDOException $e) {
	$_SESSION['message'] = '不明なエラーが発生しました。';
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
}

/* ページ間のメッセージやり取りがあれば変数に入れる */
$message = '';
if (isset($_SESSION['message'])) {
	$message = $_SESSION['message'];
	unset($_SESSION['message']);
}

session_write_close();

?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>対戦ロビー - SSE/PHP/JavaScriptでリアルタイム・オンラインゲームを実装する | ウィザード・コード - WIZARD-CODE</title>
    <link href="/lib/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/online-game-lobby.css" rel="stylesheet">
  </head>
  <body>
    <div class="container">
      <h1 class="h2">SSE/PHP/JavaScriptでリアルタイム・オンラインゲームを実装する</h1>
      <p>このページは<strong>HTML5</strong>関連の技術を使用した実装のテストページです。サーバーからのPush通信を実現する<strong>Server Sent Events</strong>(以下<strong>SSE</strong>)のAPIを使用して、リアルタイムのオンラインゲームを構築します。テストの具体的な説明は<a href="">こちらのページ</a>で、すべてのソースコードは<a href="">こちらのページ</a>で公開しています。</p>
      <form id="play-game" method="post" action="/dev/html5/stream/online-game/redirect.php">
        <div class="form-group">
          <label for="player-name" class="control-label">プレイヤー名を入れてください</label>
          <input type="text" class="form-control" id="player-name" name ="player-name" placeholder="文字数は3文字以上20文字以下の範囲で入力してください。">
          <input type="hidden" id="nonce" name="nonce" value="<?=h($nonce)?>">
          <input type="hidden" id="selected-id" name="selected-id" value="">
        </div>
      </form>
      <div id="game-items"></div>
      <div id="message" class="alert alert-warning" data-message="<?=h($message)?>"></div>
    </div>
    <script src="/lib/jquery/jquery-2.2.2.min.js"></script>
    <script src="/lib/bootstrap/3.3.1/js/bootstrap.min.js"></script>
    <script src="/lib/underscore/1.8.3/underscore-min.js"></script>
    <script src="/js/online-game-lobby.js"></script>
  </body>
</html>
                

redirect.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);

require_once('constants.php');
require_once('functions.php');

$lobby_url = LOBBY_URL;
$game_field_url = GAME_FIELD_URL;

/* Post/Redirect/Getパターン */
$method = @$_SERVER['REQUEST_METHOD'];
/* redirect.phpにGETでアクセスした場合はロビーページへ戻す */
if ($method !== 'POST') {
	$message = 'post method required';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	header("Location: {$lobby_url}");
	exit;
	
/* 正規にPOSTリクエストしてきた場合の処理 */
} else {
	$raw_post_data = @file_get_contents('php://input');
	parse_str($raw_post_data, $params);
	
	$game_id = $params['selected-id'];
	$player_name = $params['player-name'];
	$nonce = $params['nonce'];
	
	/* ゲームIDが存在しなかったら、ロビーページへリダイレクト */
	if (!isset($game_id)) {
		$message = 'game id not found';
		format_error_log($message, __FILE__, __LINE__, 'warn');
		header("Location: {$lobby_url}");
		exit;
	}
	
	@session_start();
	
	/* アクセスがロビー経由でない場合、ロビーページへリダイレクト */
	if (!isset($_SESSION['client_id'])) {
		$message = 'client id not found';
		format_error_log($message, __FILE__, __LINE__, 'warn');
		header("Location: {$lobby_url}");
		exit;
	}
	
	/* ノンスが異なっていたら、ロビーページへリダイレクト */
	if ($_SESSION['nonce'] !== $nonce) {
		$message = 'invalid nonce';
		format_error_log($message, __FILE__, __LINE__, 'warn');
		header("Location: {$lobby_url}");
		exit;
	}
	
	/* ゲームモードが「待機中」でなければ、ロビーページへリダイレクト */
	if ($_SESSION['mode'] !== 'waiting') {
		$message = 'different game mode';
		format_error_log($message, __FILE__, __LINE__, 'warn');
		header("Location: {$lobby_url}");
		exit;
	}
	
	/* ゲーム状態を「準備中」に変更 */
	$_SESSION['mode'] = 'ready';
	
	/* プレイヤーIDを作成。データはブラウザを閉じるまで保存される */
	if (!isset($_SESSION['player_id'])) {
		$player_id = 'player-' . random_str();
		$_SESSION['player_id'] = $player_id;
	}
	
	/* ゲームIDとプレイヤー名をセッションに保存 */
	$_SESSION['game_id'] = $game_id;
	$_SESSION['player_name'] = $player_name;
	
	session_write_close();
	
	header("Location: {$game_field_url}");
}

?>
                

game-field.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);

require_once('pdo-config.php');
require_once('constants.php');
require_once('functions.php');

$lobby_url = LOBBY_URL;
$game_field_timeout = GAME_FIELD_TIMEOUT * 60;

header('Content-type: text/html; charset=utf-8');

@session_start();

/* ノンスを再発行する */
$nonce = random_str();
$_SESSION['nonce'] = $nonce;

/* redirect.phpで保存したセッション変数を取り出す */
$game_id = $_SESSION['game_id'];
$player_id = $_SESSION['player_id'];
$player_name = $_SESSION['player_name'];

/* アクセスがロビー経由でない場合、ロビーページへリダイレクト */
if (!isset($_SESSION['client_id'])) {
	$message = 'client id not found';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	header("Location: {$lobby_url}");
	exit;
}

/* redirect.phpからのリダイレクトでなければ、ロビーにリダイレクトする */
if ($_SESSION['mode'] !== 'ready') {
	$message = 'different game mode';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	header("Location: {$lobby_url}");
	exit;
}

/* ゲーム状態を「プレイ中」に変更 */
$_SESSION['mode'] = 'playing';
/* 直前に配信されたイベントデータのIDを入れる変数 */
$_SESSION['last_token'] = null;

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	/* プレイ中のプレイヤーのリストを取得 */
	$alive_list = get_alive_list($pdo, $game_id);
	
	/* ゲームフィールドがすでに満員の場合はロビーにリダイレクト */
	if (count($alive_list) >= MAX_PLAYER_LIMIT) {
		/* ロビーページに表示するメッセージ */
		$_SESSION['message'] = 'ゲームフィールドが定員オーバーです。別のフィールドを選択してください。';
		
		$message = 'overcrowded';
		format_error_log($message, __FILE__, __LINE__, 'warn');
		header("Location: {$lobby_url}");
		exit;
	}
	
	/* ゲームデータを読み込む */
	$game_data = get_game_data($pdo, $game_id);
	
	/* ゲームフィールドをアクティベートする */
	if ($game_data['state'] === 'inactive') {
		$game_data['state'] = 'active';
		$game_data['start_time'] = time();
		$game_data['remaining_time'] = $game_field_timeout;
	}
	
	/* ゲームデータの値を更新 */
	update_game_data($pdo, $game_id, $game_data);
	
} catch (PDOException $e) {
	$_SESSION['message'] = '不明なエラーが発生しました。';
	
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
	header("Location: {$lobby_url}");
	exit;
}

session_write_close();

?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ゲームフィールド画面 - SSE/PHP/JavaScriptでリアルタイム・オンラインゲームを実装する | ウィザード・コード - WIZARD-CODE</title>
    <link href="/css/online-game.css" rel="stylesheet">
  </head>
  <body>
    <div id="nonce" data-nonce="<?=h($nonce)?>"></div>
    <div id="game-id" data-game-id="<?=h($game_id)?>"></div>
    <div id="player-id" data-player-id="<?=h($player_id)?>"></div>
    <div id="player-name" data-player-name="<?=h($player_name)?>"></div>
    <script src="/lib/jquery/jquery-2.2.2.min.js"></script>
    <script src="/lib/bootstrap/3.3.1/js/bootstrap.min.js"></script>
    <script src="/lib/underscore/1.8.3/underscore-min.js"></script>
    <script src="/lib/phina/0.2.0/phina.min.js"></script>
    <script src="/js/online-game.js"></script>
  </body>
</html>
                

request-alive-list.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);

require_once('pdo-config.php');
require_once('constants.php');
require_once('functions.php');

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

$response = array();

$raw_post_data = @file_get_contents('php://input');
parse_str($raw_post_data, $params);

$nonce = $params['nonce'];

@session_start();

/* アクセスがロビー経由でない場合は不成功 */
if (!isset($_SESSION['client_id'])) {
	$message = 'client id not found';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* ノンスが異なっていたら不成功 */
if ($_SESSION['nonce'] !== $nonce) {
	$message = 'invalid nonce';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* Ajax通信でなければ不成功 */
if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	$message = 'invalid access';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* redirect.phpで保存したセッション変数を取り出す */
$game_id = $_SESSION['game_id'];


try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	$sql_get_event_list = "SELECT data FROM event_list WHERE game_id=:game_id ORDER BY no DESC";
	$stmt = $pdo->prepare($sql_get_event_list);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$result = $stmt->fetchAll(PDO::FETCH_COLUMN);
	$event_number = count($result);
	
	/* ストリーミング開始時にサーバー・クライアント間でラグが生じることへの対策 */
	if ($event_number > 0) {
		$latest_event_data = unserialize($result[0]);
		$lag_start_token = $latest_event_data['token'];
	} else {
		$lag_start_token = null;
	}
	
	$_SESSION['lag_start_token'] = $lag_start_token;
	
	/* すでにゲームフィールドに存在するプレイヤーのデータを取得する */
	$alive_list = get_alive_list($pdo, $game_id);
	if (count($alive_list) > 0) {
		$alive_list = array_map('filter_player_data', $alive_list);
	}
	
} catch (PDOException $e) {
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
	$response['error'] = UNKNOWN_ERROR;
	echo json_safe_encode($response);
	exit;
}

session_write_close();

$response['alive_list'] = $alive_list;
echo json_safe_encode($response);

function filter_player_data($serialized_data) {
	$data = unserialize($serialized_data);
	
	$result = array(
		'id' => h($data['id']),
		'name' => h($data['name']),
		'state' => $data['state'],
		'score' => $data['score'],
		
		'entity_type' => h($data['entity_type']),
		'friend_id' => h($data['friend_id']),
		
		'shape' => h($data['shape']),
		'color' => h($data['color']),
		'size' => $data['size'],
		
		'hp' => $data['hp'],
		'atk' => $data['atk'],
		'def' => $data['def'],
		
		'x' => $data['x'],
		'y' => $data['y'],
		'vx' => $data['vx'],
		'vy' => $data['vy'],
		'ax' => $data['ax'],
		'ay' => $data['ay'],
		'dir' => $data['dir'],
	);
	
	return $result;
}

?>
                

start-game.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);

require_once('pdo-config.php');
require_once('constants.php');
require_once('functions.php');

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

$response = array();

$raw_post_data = @file_get_contents('php://input');
parse_str($raw_post_data, $params);

$nonce = $params['nonce'];
$token = $params['token'];

@session_start();

$game_id = $_SESSION['game_id'];
$player_id = $_SESSION['player_id'];
$player_name = $_SESSION['player_name'];

/* アクセスがロビー経由でない場合は不成功 */
if (!isset($_SESSION['client_id'])) {
	$message = 'client id not found';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* ノンスが異なっていたら不成功 */
if ($_SESSION['nonce'] !== $nonce) {
	$message = 'invalid nonce';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* Ajax通信でなければ不成功 */
if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	$message = 'invalid access';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* 二重送信対策。同じトークンが送信されてきたら、処理済みの結果だけ渡して終了する */
if (isset($_SESSION[$token])) {
	$response = $_SESSION[$token];
	echo json_safe_encode($response);
	exit;
}

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	/* プレイヤーの生成を実行 */
	spawn_player($token);
	
} catch (PDOException $e) {
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
	$response['error'] = UNKNOWN_ERROR;
	echo json_safe_encode($response);
	exit;
}

/* 二重送信対策 */
$_SESSION[$token] = $response;
	
session_write_close();

echo json_safe_encode($response);

function spawn_player($token) {
	global $pdo, $game_id, $player_id, $player_name;
	
	$now = microtime(true);
	/* プレイヤーデータを読み込む */
	$player_data = get_player_data($pdo, $game_id, $player_id);
	
	if ($player_data !== false) {
		/* すでにプレイヤーデータが存在する場合(再プレイ)は前のデータを使う */
		$player_data['name'] = $player_name;
		$player_data['state'] = 'alive';
		$player_data['friend_id'] = $player_id;
		$player_data['start_time'] = $now;
		$player_data['current_time'] = $now;
		$player_data['hp'] = PLAYER_HP;
		
		/* プレイヤーデータの値を更新 */
		update_player_data($pdo, $game_id, $player_id, $player_data);
		
	} else {
		/* プレイヤーデータが存在しない場合は新規にデータを作成 */
		$player_data = array(
			
			'id' => $player_id,
			'name' => $player_name,
			'state' => 'alive',
			'score' => 0,
			
			'entity_type' => 'player',
			'friend_id' => $player_id,
			
			'start_time' => $now,
			'current_time' => $now,
			
			'shape' => 'circle',
			'color' => random_hsl(),
			'size' => PLAYER_RADIUS,
			
			'hp' => PLAYER_HP,
			'atk' => PLAYER_ATK,
			'def' => PLAYER_DEF,
			
			'x' => mt_rand(0, VIEW_WIDTH - 1),
			'y' => mt_rand(0, VIEW_HEIGHT - 1),
			'vx' => 0,
			'vy' => 0,
			'ax' => 0,
			'ay' => 0,
			'dir' => 0,
		);
		
		$serialized_data = serialize($player_data);
		
		/* プレイヤーデータを新規保存 */
		$sql_insert_player_data = "INSERT INTO player_list (id, game_id, data) VALUES(:id, :game_id, :data)";
		$stmt = $pdo->prepare($sql_insert_player_data);
		$stmt->bindParam(':id', $player_id, PDO::PARAM_STR);
		$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
		$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
		$stmt->execute();
	}
	
	/* ゲームデータを読み込む */
	$game_data = get_game_data($pdo, $game_id);
	
	$player_number = count(get_alive_list($pdo, $game_id));
	$game_data['player_number'] = $player_number;
	
	/* ゲームデータを更新する */
	update_game_data($pdo, $game_id, $game_data);
	
	/* プレイヤー生成イベント */
	$event_data = array(
		'token' => random_str(),
		'event_type' => 'spawned',
		
		'id' => $player_data['id'],
		'name' => $player_data['name'],
		'state' => $player_data['state'],
		'score' => $player_data['score'],
		
		'entity_type' => $player_data['entity_type'],
		'friend_id' => $player_data['friend_id'],
		
		'shape' => $player_data['shape'],
		'color' => $player_data['color'],
		'size' => $player_data['size'],
		
		'hp' => $player_data['hp'],
		'atk' => $player_data['atk'],
		'def' => $player_data['def'],
		
		'x' => $player_data['x'],
		'y' => $player_data['y'],
		'vx' => 0,
		'vy' => 0,
		'ax' => 0,
		'ay' => 0,
		'dir' => 0,
	);
	
	/* イベントデータを作成し保存 */
	insert_event_data($pdo, $game_id, $event_data);
}

?>
                

send-event-data.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);

require_once('pdo-config.php');
require_once('constants.php');
require_once('functions.php');

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

$response = array();

/* POSTデータを読み込む */
$raw_post_data = @file_get_contents('php://input');
parse_str($raw_post_data, $params);

$data = $params['data'];
$nonce = $params['nonce'];
$token = $params['token'];

$data = json_decode($data, true);

if (!is_array($data)) {
	$message = 'invalid data';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_DATA;
	echo json_safe_encode($response);
	exit;
}

$data = validate($data);

$event_type = $data['event_type'];
$valid_type = array('movement', 'firing', 'collision', 'vanishment', 'withdrawal');
/* 不正な入力を弾く */
if (!in_array($event_type, $valid_type, true)) {
	$message = 'invalid type';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_DATA;
	echo json_safe_encode($response);
	exit;
}

session_start();

$game_id = $_SESSION['game_id'];
$player_id = $_SESSION['player_id'];

/* ノンスが異なっていたら不成功 */
if ($_SESSION['nonce'] !== $nonce) {
	$message = 'invalid nonce';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* アクセスが lobby.php 経由でない場合は不成功 */
if (!isset($_SESSION['client_id'])) {
	$message = 'client id not found';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* Ajax通信でなければ不成功 */
if (@$_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
	$message = 'invalid access';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	$response['error'] = INVALID_ACCESS;
	echo json_safe_encode($response);
	exit;
}

/* 二重送信対策。同じトークンが送信されてきたら、処理済みの結果だけ渡して終了する */
if (isset($_SESSION[$token])) {
	$response = $_SESSION[$token];
	echo json_safe_encode($response);
	exit;
}

$now = microtime(true);

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	switch ($event_type) {
		case 'movement':
		movement($data);
		break;
		
		case 'firing':
		firing($data);
		break;
		
		case 'vanishment':
		$result = vanishment($data);
		if ($result === false) {
			$message = 'fail to vanishment()';
			format_error_log($message, __FILE__, __LINE__, 'warn');
			$response['error'] = DUPLICATE_PROCESS;
			echo json_safe_encode($response);
			exit;
		}
		break;
		
		case 'collision':
		collision($data);
		break;
		
		case 'withdrawal':
		withdrawal($data);
		break;
	}
	
} catch (PDOException $e) {
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
	$response['error'] = UNKNOWN_ERROR;
	echo json_safe_encode($response);
	exit;
}

/* 二重送信対策 */
$_SESSION[$token] = $response;

session_write_close();

echo json_safe_encode($response);

function validate($data) {
	$int_group = array('score', 'hp', 'atk', 'def', 'ax', 'ay', 'dir');
	$float_group = array('x', 'y', 'vx', 'vy');
	
	foreach ($data as $key => $value) {
		if (in_array($key, $int_group, true)) {
			$data[$key] = intval($value);
		} elseif (in_array($key, $float_group, true)) {
			$data[$key] = floatval($value);
		} else {
			$data[$key] = (string) $value;
		}
	}
	
	return $data;
}

function movement($data) {
	global $pdo, $game_id, $player_id, $now;
	
	/* プレイヤーデータを読み込む */
	$player_data = get_player_data($pdo, $game_id, $player_id);
	
	$player_data['current_time'] = $now;
	
	$player_data['x'] = $data['x'];
	$player_data['y'] = $data['y'];
	$player_data['vx'] = $data['vx'];
	$player_data['vy'] = $data['vy'];
	$player_data['ax'] = $data['ax'];
	$player_data['ay'] = $data['ay'];
	$player_data['dir'] = $data['dir'];
	
	/* プレイヤーデータを保存 */
	update_player_data($pdo, $game_id, $player_id, $player_data);
	
	/* イベントデータを保存 */
	$data['token'] = random_str();
	insert_event_data($pdo, $game_id, $data);
}

function firing($data) {
	global $pdo, $game_id, $player_id, $now;
	
	$projectile_id = 'projectile-' . random_str();
	$friend_id = "{$data['friendId']}|{$projectile_id}";
	$dir = deg2rad($data['dir']);
	
	$projectile_data = array(
		
		'id' => $projectile_id,
		'state' => 'alive',
		
		'entity_type' => 'projectile',
		'friend_id' => h($friend_id),
		
		'start_time' => $now,
		'current_time' => $now,
		
		'shape' => 'circle',
		'color' => 'yellow',
		'size' => PROJECTILE_RADIUS,
		
		'hp' => PROJECTILE_HP,
		'atk' => PROJECTILE_ATK,
		'def' => PROJECTILE_DEF,
		
		'x' => $data['x'] + sin($dir) * PLAYER_RADIUS,
		'y' => $data['y'] + cos($dir) * PLAYER_RADIUS,
		'vx' => sin($dir) * PROJECTILE_SPEED,
		'vy' => cos($dir) * PROJECTILE_SPEED,
		'ax' => 0,
		'ay' => 0,
		'dir' => 0,
	);
	
	/* 投射物データを保存 */
	insert_projectile_data($pdo, $game_id, $projectile_id, $projectile_data);
	
	/* 投射物生成イベント */
	$event_data = array(
		'token' => random_str(),
		'event_type' => 'spawned',
		
		'id' => $projectile_data['id'],
		'state' => $projectile_data['state'],
		
		'entity_type' => $projectile_data['entity_type'],
		'friend_id' => $projectile_data['friend_id'],
		
		'shape' => $projectile_data['shape'],
		'color' => $projectile_data['color'],
		'size' => $projectile_data['size'],
		
		'hp' => $projectile_data['hp'],
		'atk' => $projectile_data['atk'],
		'def' => $projectile_data['def'],
		
		'x' => $projectile_data['x'],
		'y' => $projectile_data['y'],
		'vx' => $projectile_data['vx'],
		'vy' => $projectile_data['vy'],
		'ax' => 0,
		'ay' => 0,
		'dir' => $projectile_data['dir'],
	);
	
	/* イベントデータを保存 */
	insert_event_data($pdo, $game_id, $event_data);
	
	/* 作成した投射物のfriend_idリストにあるオブジェクト群の同プロパティに自身のIDを追加する */
	$friend_id = explode('|', $friend_id);
	for ($i = 0, $l = count($friend_id); $i < $l; $i++) {
		$id = $friend_id[$i];
		if ($id !== $projectile_id) {
			if (strpos($id, 'player-') !== false) {
				$player_data = get_player_data($pdo, $game_id, $id);
				$player_data['friend_id'] .= "|{$projectile_id}";
				update_player_data($pdo, $game_id, $id, $player_data);
				
			} elseif (strpos($id, 'projectile-') !== false) {
				$projectile_data = get_projectile_data($pdo, $id);
				$projectile_data['friend_id'] .= "|{$projectile_id}";
				update_projectile_data($pdo, $game_id, $id, $projectile_data);
			}
		}
	}
}

function vanishment($data) {
	global $pdo, $game_id;
	$result = false;
	
	try {
		vanish_projectile($pdo, $game_id, $data['id']);
		$result = true;
		
	} catch (Exception $e) {
		$message = $e->getMessage();
		format_error_log($message, __FILE__, __LINE__, 'error');
	}
	
	return $result;
}

function collision($data) {
	global $pdo, $game_id, $now;
	
	/* プレイヤー(およびプレイヤー所属の投射物)のID */
	$object_id = $data['id'];
	/* 衝突した相手プレイヤーまたは投射物のID */
	$target_id = $data['target_id'];
	
	if (strpos($object_id, 'player-') !== false) {
		$object_data = get_player_data($pdo, $game_id, $object_id);
		
	} elseif (strpos($object_id, 'projectile-') !== false) {
		$object_data = get_projectile_data($pdo, $object_id);
	}
	
	if (strpos($target_id, 'player-') !== false) {
		$target_data = get_player_data($pdo, $game_id, $target_id);
				
	} elseif (strpos($target_id, 'projectile-') !== false) {
		$target_data = get_projectile_data($pdo, $target_id);
	}
	
	/* 衝突時間のクライアント間の遅延を考慮して二つの座標を算出する */
	$object_coords = getCurrentPosition($object_data, $now);
	$target_coords = getCurrentPosition($target_data, $now);
	$object_late_coords = getCurrentPosition($object_data, $now - LATENCY);
	$target_late_coords = getCurrentPosition($target_data, $now - LATENCY);
	
	$distance_squared = ($object_coords['x'] - $target_coords['x']) * ($object_coords['x'] - $target_coords['x']) + ($object_coords['y'] - $target_coords['y']) * ($object_coords['y'] - $target_coords['y']);
	$collision_range_squared = (($object_data['size'] + $target_data['size']) * ERROR_RATE) * (($object_data['size'] + $target_data['size']) * ERROR_RATE);
	
	$late_distance_squared = ($object_late_coords['x'] - $target_late_coords['x']) * ($object_late_coords['x'] - $target_late_coords['x']) + ($object_late_coords['y'] - $target_late_coords['y']) * ($object_late_coords['y'] - $target_late_coords['y']);
		
	if ($collision_range_squared <= $distance_squared && $collision_range_squared <= $late_distance_squared) {
		$message = "'{$object_id}' failed collision test";
		format_error_log($message, __FILE__, __LINE__, 'warn');
		$result = 'invalid collision';
		return $result;
	}
	
	/* 衝突イベントデータにトークンを付与して保存 */
	$data['token'] = random_str();
	insert_event_data($pdo, $game_id, $data);
	
	/* 衝突相手のステータスを変化させる */
	$target_data['hp'] -= $object_data['atk'] - $target_data['def'];
	if ($target_data['hp'] <= 0) {
		$target_data['hp'] = 0;
	}
	
	/* 衝突相手のステータスを保存 */
	if ($target_data['entity_type'] === 'player') {
		update_player_data($pdo, $game_id, $target_id, $target_data);
		/* ステータス変化のイベントデータを作成 */
		$status_change_data = array(
			'token' => random_str(),
			'event_type' => 'status_change',
			'id' => h($target_id),
			'hp' => $target_data['hp'],
		);
		
		insert_event_data($pdo, $game_id, $status_change_data);
		
	} elseif ($target_data['entity_type'] === 'projectile') {
		update_projectile_data($pdo, $game_id, $target_id, $target_data);
	}
	
	/* スコア乗算値。相手を撃破した場合ボーナス値が乗算される */
	$score_multiplier = $target_data['entity_type'] === 'player' ? SCORE_MULTIPLIER : 1;
	
	/* 衝突相手のHPがゼロになったら、ゲームから除外する */
	if ($target_data['hp'] === 0) {
		if ($target_data['entity_type'] === 'player') {
			$score_multiplier *= DEFEAT_BONUS;
			kill_player($pdo, $game_id, $target_id);
		} elseif ($target_data['entity_type'] === 'projectile') {
			vanish_projectile($pdo, $game_id, $target_id);
		}
	}
	
	$master_data = get_player_data($pdo, $game_id, $data['master_id']);
	
	$score = floor(($object_data['atk'] - $target_data['def']) * $score_multiplier);
	$master_data['score'] += $score;
	
	update_player_data($pdo, $game_id, $master_data['id'], $master_data);
	
	/* スコア加算のイベントデータを作成 */
	$score_change_data = array(
		'token' => random_str(),
		'event_type' => 'score_change',
		'id' => h($master_data['id']),
		'score' => $master_data['score'],
	);
	
	insert_event_data($pdo, $game_id, $score_change_data);
}

function withdrawal($data) {
	global $pdo, $game_id, $player_id;
	
	@session_start();
	$_SESSION['message'] = '不明のエラーが発生しました。';
	session_write_close();
	
	/* 離脱したプレイヤーをゲームフィールドから削除する */
	kill_player($pdo, $game_id, $player_id);
}

?>
                

game-data-stream.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);
mb_http_output('pass');

/* クライアントの接続切断を検知するための設定 */
ignore_user_abort(true);

require_once('pdo-config.php');
require_once('Data_Stream.php');
require_once('constants.php');
require_once('functions.php');

$timeout = STREAMING_TIMEOUT * 1000;
$sleep_time = UPDATE_FREQUENCY * 1000 * 1000;
$game_field_timeout = GAME_FIELD_TIMEOUT * 60;

/* ストリーミング中の排他ロックを防ぐためセッションは明示的に終了させる */
@session_start();

/* アクセスがロビー経由でない場合終了 */
if (!isset($_SESSION['client_id'])) {
	$message = 'client id not found';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	abort_stream();
	exit;
}

/* Last-Event-IDをチェックする */
$last_event_id = @$_SERVER['HTTP_LAST_EVENT_ID'];
$last_event_id = is_null($last_event_id) ? 0 : intval($last_event_id) + 1;

session_write_close();

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	/* データストリーミング用のクラスを初期化 */
	$ds = new Data_Stream;
	$ds->start();
	
	/* イベントIDは再接続時に前回からの連番にする */
	if ($last_event_id > 0) {
		$ds->setId($last_event_id);
	}
	
	/* タイムアウトする時間まで処理を繰り返す */
	while ($ds->getElapsedTime() < $timeout) {
		/* クライアントが接続を切断した場合、ループを抜ける */
		if (connection_aborted()) {
			$message = 'client aborted connection';
			format_error_log($message, __FILE__, __LINE__);
			break;
		}
		
		/* ゲームデータのリストを読み込む */
		$sql_get_game_list = "SELECT id, data FROM game_list ORDER BY no ASC LIMIT :limit";
		$stmt = $pdo->prepare($sql_get_game_list);
		$stmt->bindValue(':limit', MAX_GAME_FIELD_LIMIT, PDO::PARAM_INT);
		$stmt->execute();
		
		while ($result = $stmt->fetch()) {
			$game_id = $result['id'];
			$data = $result['data'];
			$game_data = unserialize($data);
			
			/* アクティブなゲームフィールドの経過時間とプレイ人数を更新する */
			if ($game_data['state'] === 'active') {
				$alive_number = count(get_alive_list($pdo, $game_id));
				$game_data['player_number'] = $alive_number;
				
				$remaining_time = $game_field_timeout - (time() - $game_data['start_time']);
				$game_data['remaining_time'] = $remaining_time;
				
				update_game_data($pdo, $game_id, $game_data);
			}
			
			/* ゲームリストをクライアントに送る。クライアントに渡すゲームデータのパラメータは必要な分だけにする */
			$filtered_data = filter_game_data($game_data);
			$json = json_safe_encode($filtered_data);
			$ds->storeData($json);
		}
		
		$ds->output('game-data');
		usleep($sleep_time);
	}
	
	$ds->end();
	
} catch (PDOException $e) {
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
	abort_stream();
	exit;
}

function filter_game_data($data) {
	$result = array();
	$result['id'] = h($data['id']);
	$result['state'] = h($data['state']);
	$result['remaining_time'] = $data['remaining_time'];
	$result['player_number'] = $data['player_number'];
	return $result;
}

/* クライアントにストリーミングの中断を指示する */
function abort_stream() {
	$message = 'connection aborted';
	format_error_log($message, __FILE__, __LINE__);
	
	$ds = new Data_Stream;
	$ds->start();
	$ds->flush('connection aborted', 'abort-game-data-stream');
	$ds->end();
}

?>
                

event-data-stream.php

<?php

date_default_timezone_set('UTC');
set_time_limit(60);
session_set_cookie_params(0, '/', '', false, true);
mb_http_output('pass');

/* クライアントの接続切断を検知するための設定 */
ignore_user_abort(true);

require_once('pdo-config.php');
require_once('Data_Stream.php');
require_once('constants.php');
require_once('functions.php');

/* ストリーミング中の排他ロックを防ぐためセッションは明示的に終了させる */
@session_start();

/* アクセスがロビー経由でない場合終了 */
if (!isset($_SESSION['client_id'])) {
	$message = 'client id not found';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	abort_stream();
	exit;
}

/* Last-Event-IDをチェックする */
$last_event_id = @$_SERVER['HTTP_LAST_EVENT_ID'];
$last_event_id = is_null($last_event_id) ? 0 : intval($last_event_id) + 1;

if ($_SESSION['mode'] !== 'playing') {
	$message = 'different game mode';
	format_error_log($message, __FILE__, __LINE__, 'warn');
	abort_stream();
	exit;
};

$game_id = $_SESSION['game_id'];
$player_id = $_SESSION['player_id'];

/* 最後に送ったイベントデータのIDをセッション変数から取り出す */
$last_token = $_SESSION['last_token'];

$lag_start_token = $_SESSION['lag_start_token'];

session_write_close();

$timeout = STREAMING_TIMEOUT * 1000;
$sleep_time = 1000 * 1000 / STREAMING_FPS;
$game_field_timeout = GAME_FIELD_TIMEOUT * 60;
$interval = STREAMING_FPS * UPDATE_FREQUENCY;
/* イベントデータの初期送信数 */
$init_output_length = ceil(STREAMING_FPS / 2);
$loop_counter = 0;

try {
	$pdo = new PDO($dsn, $username, $password, $driver_options);
	
	/* データストリーミング用のクラスを初期化 */
	$ds = new Data_Stream;
	$ds->start();
	
	/* イベントIDは再接続時に前回からの連番にする */
	if ($last_event_id > 0) {
		$ds->setId($last_event_id);
	}
	
	while ($ds->getElapsedTime() < $timeout) {
		/* クライアントが接続を切断した場合、プレイヤーをゲームフィールドから削除してループを抜ける */
		if (connection_aborted()) {
			$message = 'client aborted connection';
			format_error_log($message, __FILE__, __LINE__);
			
			@session_start();
			$_SESSION['message'] = 'プレイ中のゲームを離脱しました。';
			session_write_close();
			
			kill_player($pdo, $game_id, $player_id);
			break;
		}
		
		/* ゲームフィールドが時間切れかどうか定期的にチェックする */
		if ($loop_counter++ % $interval === 0) {
			/* ゲームデータを読み込む */
			$game_data = get_game_data($pdo, $game_id);
			
			/* ゲームの経過時間を更新 */
			$remaining_time = $game_field_timeout - (time() - $game_data['start_time']);
			$game_data['remaining_time'] = $remaining_time;
			
			/* ゲームデータを更新する */
			update_game_data($pdo, $game_id, $game_data);
			
			if ($game_data['state'] === 'active') {
				/* ゲーム経過時間が一定時間を超えたらクロージング処理に移る */
				if ($remaining_time <= 0) {
					/* ゲームフィールドをクローズする */
					close_game_field($pdo, $game_id);
				}
			} else {
				/* ストリーミングを中断する */
				$message = 'game state is not active';
				format_error_log($message, __FILE__, __LINE__, 'error');
				abort_stream();
				exit;
			}
		}
		
		/* イベントデータを読み込む */
		$sql_get_event_list = "SELECT data FROM event_list WHERE game_id=:game_id ORDER BY no DESC";
		$stmt = $pdo->prepare($sql_get_event_list);
		$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
		$stmt->execute();
		
		$result = $stmt->fetchAll(PDO::FETCH_COLUMN);
		$event_list = array_map('unserialize_data', $result);
		
		/* 初回だけタイムラグ期間のイベントデータを一括して送る */
		if (is_null($last_token)) {
			
			/* タイムラグのあいだにデータの変化がないかチェック */
			if (!is_null($lag_start_token)) {
				/* タイムラグ期間に発生したイベントデータを取得する */
				foreach ($event_list as $index => $data) {
					if ($data['token'] === $lag_start_token) {
						$length = $index;
						break;
					}
				}
				
				/* FPSに応じて取得数にリミットをつける */
				$length = min($length, $init_output_length);
			} else {
				/* 自身の「Birth」イベントが必ず含まれる */
				$length = 1;
			}
			
			$sliced_data = array_slice($event_list, 0, $length);
			
			for ($i = 0, $l = count($sliced_data); $i < $l; $i++) {
				$data = $sliced_data[$i];
				if ($i === 0) {
					$last_token = $data['token'];
					
					@session_start();
					$_SESSION['last_token'] = $last_token;
					session_write_close();
				}
				
				/* 文字列データはエスケープする */
				$sliced_data[$i] = escape_event_stream_data($data);
			}
			
			if (empty($sliced_data)) {
				$ds->flush('', 'event-data');
				usleep($sleep_time);
				continue;
			}
			
			/* 初回の送信は配列データを送る */
			$json = json_safe_encode($sliced_data);
			$ds->storeData($json, true);
			
		/* 以降は新規のイベントデータだけを送る */
		} else {
			$length = 0;
			foreach ($event_list as $index => $data) {
				if ($data['token'] === $last_token) {
					$length = $index;
					break;
				}
			}
			
			if ($length === 0) {
				$ds->flush('', 'event-data');
				usleep($sleep_time);
				continue;
			}
			
			$sliced_data = array_slice($event_list, 0, $length);
			
			for ($i = 0, $l = count($sliced_data); $i < $l; $i++) {
				$data = $sliced_data[$i];
				if ($i === 0) {
					$last_token = $data['token'];
					
					@session_start();
					$_SESSION['last_token'] = $last_token;
					session_write_close();
				}
				
				/* 文字列データはエスケープする */
				$data = escape_event_stream_data($data);
				$json = json_safe_encode($data);
				$ds->storeData($json, true);
			}
		}
		
		/* イベントデータをフラッシュして一定時間スリープ */
		$ds->output('event-data');
		usleep($sleep_time);
	}
	
	$ds->end();
	
} catch (PDOException $e) {
	$message = $e->getMessage();
	format_error_log($message, __FILE__, __LINE__, 'error');
	abort_stream();
	exit;
}

function close_game_field(&$pdo, $game_id) {
	/* プレイヤーリストを読み込む */
	$sql_get_player_list = "SELECT data FROM player_list WHERE game_id=:game_id";
	$stmt = $pdo->prepare($sql_get_player_list);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$result = $stmt->fetchAll(PDO::FETCH_COLUMN);
	$player_list = array_map('unserialize_data', $result);
	
	foreach ($player_list as $data) {
		if ($data['state'] !== 'alive') {
			continue;
		}
		
		/* 全プレイヤーデータの状態を「死亡」に指定し、ゲームデータをリセットする */
		kill_player($pdo, $game_id, $data['id']);
	}
}

function unserialize_data($data) {
	return unserialize($data);
}

function escape_event_stream_data($data) {
	/* 外部入力の文字列に対してエスケープ処理 */
	$str_group = array('event_type', 'id', 'friend_id', 'target_id');
		
	foreach ($data as $key => $value) {
		if (in_array($key, $str_group, true)) {
			$data[$key] = h($value);
		}
	}
	
	return $data;
}

/* クライアントにストリーミングの中断を指示する */
function abort_stream() {
	$message = 'connection aborted';
	format_error_log($message, __FILE__, __LINE__);
	
	$ds = new Data_Stream;
	$ds->start();
	$ds->flush('connection aborted', 'abort-event-data-stream');
	$ds->end();
}

?>
                

constants.php

<?php

/* アプリケーション共通の定数 */
define('LOBBY_URL', '/dev/html5/stream/online-game/lobby.php');
define('GAME_FIELD_URL', '/dev/html5/stream/online-game/game-field.php');

/* ゲームフィールドの数 */
define('MAX_GAME_FIELD_LIMIT', 5);

/* ゲームフィールドのタイムアウト時間 */
define('GAME_FIELD_TIMEOUT', 2);

/* ゲーム画面のサイズ。クライアントの値と必ず同期させる */
define('VIEW_WIDTH', 1280);
define('VIEW_HEIGHT', 720);

/* 全体のプレイヤー登録数の最大値。クライアントの値と必ず同期させる */
define('MAX_PLAYER_LIMIT', 5);

/* UPDATE_FREQUENCY: 何秒ごとにデータストリームを送るか */
define('UPDATE_FREQUENCY', 1);
/* ストリーミングのタイムアウト時間を指定 */
define('STREAMING_TIMEOUT', 30);

/* ゲーム画面の描画メソッドのFPSとフレーム間隔(ミリ秒)。クライアントの値と必ず同期させる */
define('FPS', 30);

/* イベントデータの送信間隔(フレーム毎秒) */
define('STREAMING_FPS', 12);

/* ゲームで使用するパラメータ */
define('PLAYER_RADIUS', 50);
define('PROJECTILE_RADIUS', 30);
define('ERROR_RATE', 1.2);

/* レイテンシ(遅延)の想定時間(秒) */
define('LATENCY', 0.2);

/* 獲得スコアの乗算値 */
define('SCORE_MULTIPLIER', 3);
define('DEFEAT_BONUS', 10);

/* プレイヤーの初期HP。クライアントの値と必ず同期させる */
define('PLAYER_HP', 100);
define('PLAYER_ATK', 20);
define('PLAYER_DEF', 10);

/* 投射物の初期HP。クライアントの値と必ず同期させる */
define('PROJECTILE_HP', 20);
define('PROJECTILE_ATK', 40);
define('PROJECTILE_DEF', 0);

/* オブジェクトの最高速度と加速度。クライアントの値と同期させること */
define('MAX_PLAYER_SPEED', 0.2);
define('ACCEL_RATE', 0.0006);
define('DECEL_RATE', 0.0006);
define('PROJECTILE_SPEED', 0.8);

/* 投射物データの登録数の最大値 */
define('MAX_PROJECTILE_DATA_LIMIT', 100);

/* イベントリストにスタックできるイベントデータ数 */
define('MAX_EVENT_DATA_LIMIT', 50);

/* エラーコード。クライアントの値と同期させる必要がある */
define('INVALID_ACCESS', 1);
define('INVALID_DATA', 2);
define('INVALID_COLLISION', 3);
define('DUPLICATE_PROCESS', 4);
define('UNKNOWN_ERROR', 5);
define('FAIL_TO_SEND_DATA', 6);

?>
                

functions.php

<?php

function get_alive_list(&$pdo, $game_id) {
	$alive_list = array();
	
	$sql_get_player_number = "SELECT data FROM player_list WHERE game_id=:game_id";
	$stmt = $pdo->prepare($sql_get_player_number);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$result = $stmt->fetchAll(PDO::FETCH_COLUMN);
	$alive_list = array_filter($result, 'detect_alive_player');
	
	return $alive_list;
}

function detect_alive_player($blob_data) {
	$data = unserialize($blob_data);
	
	if (isset($data['state'])) {
		if ($data['state'] === 'alive') {
			return true;
		}
	}
	return false;
}

function remove_player(&$pdo, $game_id, $player_id) {
	/* プレイヤーデータを読み込む */
	$player_data = get_player_data($pdo, $game_id, $player_id);
	if ($player_data['state'] === 'dead') {
		throw new Exception('player is already dead');
	}
	
	$player_data['state'] = 'dead';
	$player_data['score'] += floor(microtime(true) - $player_data['start_time']);
	
	/* プレイヤーデータの値を更新 */
	update_player_data($pdo, $game_id, $player_id, $player_data);
}

function notify_death(&$pdo, $game_id, $player_id) {
	/* プレイヤー削除イベントを発行 */
	$event_data = array(
		'token' => random_str(),
		'event_type' => 'killed',
		'id' => $player_id,
	);
	
	insert_event_data($pdo, $game_id, $event_data);
}

function vanish_projectile(&$pdo, $game_id, $projectile_id) {
	/* 投射物データを読み込む */
	$projectile_data = get_projectile_data($pdo, $projectile_id);
	if ($projectile_data === false || $projectile_data['state'] === 'dead') {
		throw new Exception('projectile has already been removed');
	}
	
	$projectile_data['state'] = 'dead';
	
	/* 投射物データの値を更新 */
	update_projectile_data($pdo, $projectile_id, $projectile_data);
	
	/* 投射物削除イベントを発行 */
	$event_data = array(
		'token' => random_str(),
		'event_type' => 'vanished',
		'id' => $projectile_id,
	);
	
	insert_event_data($pdo, $game_id, $event_data);
	
	/* 作成した投射物のfriend_idリストにあるオブジェクト群の同プロパティから自身のIDを削除する */
	$friend_id = explode('|', $projectile_data['friend_id']);
	for ($i = 0, $l = count($friend_id); $i < $l; $i++) {
		$id = $friend_id[$i];
		if ($id !== $projectile_id) {
			if (strpos($id, 'player-') !== false) {
				$player_data = get_player_data($pdo, $game_id, $id);
				$player_friend_id = explode('|', $player_data['friend_id']);
				$player_friend_id = array_diff($player_friend_id, array($projectile_id));
				$player_friend_id = array_values($player_friend_id);
				$player_data['friend_id'] = implode('|', $player_friend_id);
				update_player_data($pdo, $game_id, $id, $player_data);
				
			} elseif (strpos($id, 'projectile-') !== false) {
				$projectile_data = get_projectile_data($pdo, $game_id, $id);
				$projectile_friend_id = explode('|', $projectile_data['friend_id']);
				$projectile_friend_id = array_diff($projectile_friend_id, array($projectile_id));
				$projectile_friend_id = array_values($projectile_friend_id);
				$projectile_data['friend_id'] = implode('|', $projectile_friend_id);
				update_projectile_data($pdo, $game_id, $id, $projectile_data);
			}
		}
	}
}

function notify_vanishment(&$pdo, $game_id, $projectile_id) {
	/* プレイヤー削除イベントを発行 */
	$event_data = array(
		'token' => random_str(),
		'event_type' => 'killed',
		'id' => $player_id,
	);
	
	insert_event_data($pdo, $game_id, $event_data);
}

function get_game_data(&$pdo, $game_id) {
	$sql_get_game_data = "SELECT data FROM game_list WHERE id=:id";
	$stmt = $pdo->prepare($sql_get_game_data);
	$stmt->bindParam(':id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$result = $stmt->fetchColumn();
	$game_data = unserialize($result);
	return $game_data;
}

function get_player_data(&$pdo, $game_id, $player_id) {
	$sql_get_player_data = "SELECT data FROM player_list WHERE id=:id AND game_id=:game_id";
	$stmt = $pdo->prepare($sql_get_player_data);
	$stmt->bindParam(':id', $player_id, PDO::PARAM_STR);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$result = $stmt->fetchColumn();
	if ($result === false) {
		return false;
	}
	

	$player_data = unserialize($result);
	return $player_data;
}

function get_projectile_data(&$pdo, $projectile_id) {
	$sql_get_projectile_data = "SELECT data FROM projectile_list WHERE id=:id";
	$stmt = $pdo->prepare($sql_get_projectile_data);
	$stmt->bindParam(':id', $projectile_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$result = $stmt->fetchColumn();
	if ($result === false) {
		return false;
	}
	
	$projectile_data = unserialize($result);
	return $projectile_data;
}

function update_game_data(&$pdo, $game_id, $data) {
	$serialized_data = serialize($data);
	
	/* ゲームデータを更新する */
	$sql_update_game_data = "UPDATE game_list SET data=:data WHERE id=:id";
	$stmt = $pdo->prepare($sql_update_game_data);
	$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
	$stmt->bindParam(':id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
}

function update_player_data(&$pdo, $game_id, $player_id, $data) {
	$serialized_data = serialize($data);
	
	/* プレイヤーデータの値を更新 */
	$sql_update_player_data = "UPDATE player_list SET data=:data WHERE id=:id AND game_id=:game_id";
	$stmt = $pdo->prepare($sql_update_player_data);
	$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
	$stmt->bindParam(':id', $player_id, PDO::PARAM_STR);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
}

function update_projectile_data(&$pdo, $projectile_id, $data) {
	$serialized_data = serialize($data);
	
	/* 投射物データの値を更新 */
	$sql_update_projectile_data = "UPDATE projectile_list SET data=:data WHERE id=:id";
	$stmt = $pdo->prepare($sql_update_projectile_data);
	$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
	$stmt->bindParam(':id', $projectile_id, PDO::PARAM_STR);
	$stmt->execute();
}

function insert_projectile_data(&$pdo, $game_id, $projectile_id, $data) {
	$serialized_data = serialize($data);
	
	/* イベントデータを作成し保存 */
	$sql_insert_projectile_data = "INSERT INTO projectile_list (id, game_id, data) VALUES(:id, :game_id, :data)";
	$stmt = $pdo->prepare($sql_insert_projectile_data);
	$stmt->bindParam(':id', $projectile_id, PDO::PARAM_STR);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
	$stmt->execute();
	
	/* イベントデータ数が上限を超えたら削除 */
	$sql_projectile_number = "SELECT COUNT(*) FROM projectile_list WHERE game_id=:game_id";
	$stmt = $pdo->prepare($sql_projectile_number);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$projectile_number = $stmt->fetchColumn();
	
	$overload = $projectile_number - MAX_PROJECTILE_DATA_LIMIT;
	if ($overload > 0) {
		$sql_delete_projectile_data = "DELETE FROM projectile_list WHERE game_id=:game_id ORDER BY no ASC LIMIT :overload";
		$stmt = $pdo->prepare($sql_delete_projectile_data);
		$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
		$stmt->bindParam(':overload', $overload, PDO::PARAM_INT);
		$stmt->execute();
	}
}

function insert_event_data(&$pdo, $game_id, $data) {
	$serialized_data = serialize($data);
	
	/* イベントデータを作成し保存 */
	$sql_insert_event_data = "INSERT INTO event_list (game_id, data) VALUES(:game_id, :data)";
	$stmt = $pdo->prepare($sql_insert_event_data);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->bindParam(':data', $serialized_data, PDO::PARAM_LOB);
	$stmt->execute();
	
	/* イベントデータ数が上限を超えたら削除 */
	$sql_event_number = "SELECT COUNT(*) FROM event_list WHERE game_id=:game_id";
	$stmt = $pdo->prepare($sql_event_number);
	$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
	$stmt->execute();
	
	$event_number = $stmt->fetchColumn();
	
	$overload = $event_number - MAX_EVENT_DATA_LIMIT;
	if ($overload > 0) {
		$sql_delete_event_data = "DELETE FROM event_list WHERE game_id=:game_id ORDER BY no ASC LIMIT :overload";
		$stmt = $pdo->prepare($sql_delete_event_data);
		$stmt->bindParam(':game_id', $game_id, PDO::PARAM_STR);
		$stmt->bindParam(':overload', $overload, PDO::PARAM_INT);
		$stmt->execute();
	}
}

function watch_game_data(&$pdo, $game_id) {
	
	$alive_list = get_alive_list($pdo, $game_id);
	$alive_number = count($alive_list);
	
	/* ゲームデータを読み込む */
	$game_data = get_game_data($pdo, $game_id);
	
	/* プレイヤーがゼロになったらゲームデータをリセットする */
	if ($alive_number > 0) {
		$game_data['player_number'] = $alive_number;
		
	} else {
		$game_data['state'] = 'inactive';
		$game_data['start_time'] = 0;
		$game_data['remaining_time'] = 0;
		$game_data['player_number'] = 0;
	}
	
	/* ゲームデータを更新する */
	update_game_data($pdo, $game_id, $game_data);
}

function kill_player(&$pdo, $game_id, $player_id) {
	
	try {
		remove_player($pdo, $game_id, $player_id);
		
	} catch (Exception $e) {
		$message = $e->getMessage();
		format_error_log($message, __FILE__, __LINE__, 'warn');
		return false;
	}
	
	notify_death($pdo, $game_id, $player_id);
	watch_game_data($pdo, $game_id);
}

function getCurrentPosition($data, $time) {
	$delta_time = ($time - $data['current_time']) * 1000;
	
	if ($data['entity_type'] === 'projectile') {
		return array(
			'x' => $data['x'] + $data['vx'] * $delta_time,
			'y' => $data['y'] + $data['vy'] * $delta_time,
		);
	}
	
	$ax = $data['ax'];
	$ay = $data['ay'];
	$vx = $data['vx'];
	$vy = $data['vy'];
	
	if ($ax > 0) {
		if ($vx < 0) {
			$x = calc1($vx, $ax, $delta_time);
		} else {
			$x = calc2($vx, $ax, $delta_time);
		}
	} elseif ($ax < 0) {
		if ($vx > 0) {
			$x = calc1($vx, $ax, $delta_time);
		} else {
			$x = calc2($vx, $ax, $delta_time);
		}
	} else {
		$x = calc3($vx, $delta_time);
	}
	
	if ($ay > 0) {
		if ($vy < 0) {
			$y = calc1($vy, $ay, $delta_time);
		} else {
			$y = calc2($vy, $ay, $delta_time);
		}
	} elseif ($ay < 0) {
		if ($vy > 0) {
			$y = calc1($vy, $ay, $delta_time);
		} else {
			$y = calc2($vy, $ay, $delta_time);
		}
	} else {
		$y = calc3($vy, $delta_time);
	}
	
	return array(
		'x' => $data['x'] + $x,
		'y' => $data['y'] + $y,
	);
}

function calc1($v, $a, $t) {
	$t1 = -$v / ($a * (ACCEL_RATE + DECEL_RATE));
	if ($t1 >= $t) {
		$x1 = $v * $t + (($a * (ACCEL_RATE + DECEL_RATE)) * $t * $t) / 2;
		$result = $x1;
	} else {
		$x1 = $v * $t1 + (($a * (ACCEL_RATE + DECEL_RATE)) * $t1 * $t1) / 2;
		$t2 = ($a * MAX_PLAYER_SPEED) / ($a * ACCEL_RATE);
		
		if ($t1 + $t2 >= $t) {
			$x2 = (($a * ACCEL_RATE) * ($t - $t1) * ($t - $t1)) / 2;
			$result = $x1 + $x2;
		} else {
			$x2 = (($a * ACCEL_RATE) * $t2 * $t2) / 2;
			
			$t3 = $t - ($t1 + $t2);
			$x3 = ($a * MAX_PLAYER_SPEED) * $t3;
			$result = $x1 + $x2 + $x3;
		}
	}
	
	return $result;
}

function calc2($v, $a, $t) {
	$t1 = (($a * MAX_PLAYER_SPEED) - $v) / ($a * ACCEL_RATE);
	
	if ($t1 >= $t) {
		$x1 = $v * $t + (($a * ACCEL_RATE) * $t * $t / 2);
		$result = $x1;
	} else {
		$x1 = $v * $t1 + (($a * ACCEL_RATE) * $t1 * $t1) / 2;
		$t2 = $t - $t1;
		$x2 = ($a * MAX_PLAYER_SPEED) * $t2;
		$result = $x1 + $x2;
	}
	
	return $result;
}

function calc3($v, $t) {
	$decel = $v >= 0 ? -DECEL_RATE : DECEL_RATE;
	
	$t1 = -$v / $decel;
	if ($t1 >= $t) {
		$x = $v * $t + ($decel * $t * $t / 2);
		$result = $x;
	} else {
		$x = $v * $t1 + ($decel * $t1 * $t1) / 2;
		$result = $x;
	}
	return $result;
}

function json_safe_encode($val) {
	return json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
}

function h($str) {
	return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

function random_str($length = 32) {
	return substr(bin2hex(openssl_random_pseudo_bytes($length)), 0, $length);
	//return strtr(substr(base64_encode(openssl_random_pseudo_bytes($length)), 0, $length), '/+', '_-');
}

function random_hsl() {
	$rand_num = mt_rand(0, 359);
	return "hsl({$rand_num},100%,50%)";
}

function format_error_log($message, $file = null, $line = null, $level = 'info') {
	$message = is_string($message) ? $message : strval($message);
	$file = !is_null($file) ? basename($file) : '--';
	$line = !is_null($line) ? $line : '--';
	$level = strtoupper($level);
	
	$date = new DateTime();
	$date->setTimezone(new DateTimeZone('Asia/Tokyo'));
	$format_date = $date->format(DATE_ISO8601);
	
	$message = "{$level}: {$file} line {$line} $message [$format_date]" . PHP_EOL;
	/* ログファイルの場所を指定 */
	error_log($message, '3', '/home/users/*****/web/log/debug.log');
}

?>
                

Data_Stream.php

<?php

date_default_timezone_set('UTC');

class Data_Stream
{
	private $id = 1;
	private $options = array(
		'defaultEvent' => 'message',
		'retry' => 0,
	);
	private $started = false;
	private $data;
	private $elapsedTime;
	private $startTime;
	
	public function __construct($options = NULL)
	{
		if (!is_null($options)) {
			$this->setOptions($options);
		}
		
		if (headers_sent() === true) {
			header_remove('Content-type');
			header_remove('Cache-Control');
		}
		
		header('Content-type: text/event-stream; charset=utf-8');
		header('Cache-Control: no-cache');
	}
	
	public function setOption($options)
	{
		foreach ($options as $key => $value) {
			if (isset($value)) {
				$this->options[$key] = $value;
			}
		}
	}
	
	public function getId()
	{
		return $this->id;
	}
	
	public function setId($id)
	{
		$this->id = is_int($id) ? $id : intval($id);
		return $this->id;
	}
	
	public function start()
	{
		ob_end_clean();
		ob_start();
		
		$this->data = array();
		$this->elapsedTime = 0;
		$this->startTime = microtime(true) * 1000;
		$this->started = true;
	}
	
	public function end()
	{
		ob_end_clean();
		$this->started = false;
	}
	
	public function getElapsedTime()
	{
		$result = 0;
		
		if ($this->started === true) {
			$result = $this->elapsedTime = (microtime(true) * 1000) - $this->startTime;
			return $result;
		}
		return $result;
	}
	
	public function storeData($data, $reverse = false)
	{
		if (is_string($data)) {
			if ($reverse === false) {
				array_push($this->data, $data);
			} else {
				array_unshift($this->data, $data);
			}
			return true;
		}
		return false;
	}
	
	public function output($event = null, $id = null)
	{
		$event = !is_null($event) ? $event : $this->options['defaultEvent'];
		$id = !is_null($id) ? $id : $this->id;
		
		$data_list = "";
		if (!empty($this->data)) {
			for ($i = 0, $l = count($this->data); $i < $l; $i++) {
				$value = $this->data[$i];
				$data_list .= "id: {$id}" . PHP_EOL . "event: {$event}" . PHP_EOL . "data: {$value}" . PHP_EOL . PHP_EOL;
				$id = ++$this->id;
			}
		}
		
		if ($this->started === true && is_string($data_list)) {
			$message = <<< EOM
: keep alive
retry: {$this->options['retry']}
{$data_list}


EOM;
			
			echo $message;		
			@ob_flush();
			@flush();
			
			$this->data = array();
		}
	}
	
	public function flush($data, $event = null, $id = null)
	{
		$event = !is_null($event) ? $event : $this->options['defaultEvent'];
		if (is_null($id)) {
			$id = $this->id;
			$this->id++;
		}
		
		if ($this->started === true && is_string($data)) {
			if (!empty($data)) {
				$data = "data: {$data}";
			} else {
				$data = '';
			}
			
			$message = <<< EOM
: keep alive
id: {$id}
event: {$event}
retry: {$this->options['retry']}
{$data}


EOM;
			
			echo $message;		
			@ob_flush();
			@flush();
		}
	}
}

?>
                

online-game-lobby.css

@charset "UTF-8";

.game-id {
	font-size: 80%;
}

.game-item {
	font-size: 120%;
}

#message {
	display: none;
}
                

online-game-lobby.js

/* このファイルはコメントを削除したり圧縮しないでください */

;(function () {
	var heredocReg, validReg, fragment, getStorageItem, setStorageItem, GAME_DATA_STREAM, MAX_PLAYER_LIMIT, NAME_LENGTH_LIMIT;
	heredocReg = /^function\s+\([^)]*\)\s*\{\s*|\s*\/\*\s*|\s*\*\/\s*\}$/g;
	validReg = /^[\x20\x21\x23-\x25\x28-\x3B\x3D\x3F-\x7E\u2010-\u2027\u3000-\u3036\u303F\u3041-\u3094\u3099-\u309E\u30A1-\u30F6\u30FB-\u30FE\u4E00-\u9FA5\uFF01-\uFF5E\uFF61-\uFF9F\uFFE0-\uFFE6]+$/;
	
	fragment = {};
	/* ルーム情報表示用のHTML断片 */
	fragment.game = (function () {  
/*
<div class="game-item alert">
  <div class="row">
    <div class="col-sm-12 col-md-2 col-lg-2">ゲームフィールド <span class="game-index"></span></div>
    <div class="col-sm-12 col-md-3 col-lg-3">ID: <span class="game-id"></span></div>
    <div class="col-sm-4 col-md-3 col-lg-3">現在プレイ中の人数 <span class="player-number"></span> 人</div>
    <div class="col-sm-4 col-md-2 col-lg-2">残り時間 <span class="remaining-time"></span> 秒</div>
    <div class="col-sm-12 col-md-2 col-lg-2 text-right"><button class="btn btn-primary btn-sm">プレイする</button></div>
  </div>
</div>
*/
	}).toString().replace(heredocReg, '');
	
	/* ゲームデータのストリーミングURL。サーバーの値と同期させること */
	GAME_DATA_STREAM = '/dev/html5/stream/online-game/game-data-stream.php';
	
	/* ゲーム内の最大プレイ人数。サーバーの値と同期させること */
	MAX_PLAYER_LIMIT = 5;
	NAME_LENGTH_LIMIT = [3, 20];
	
	$(function () {
		var playGame, gameItems, playerName, selectedId, message, gameDataStream, aborted;
		
		playGame = $('#play-game');
		gameItems = $('#game-items');
		playerName = $('#player-name');
		selectedId = $('#selected-id');
		message = $('#message');
		
		aborted = false;
		
		playerName.on('keypress', function (e) {
			/* テキストフォームのエンターキー押し下げによるイベントを抑止 */
			if (e.key === 'Enter') {
				e.preventDefault();
			}
		}).on('keyup', function (e) {
			var formGroup, text;
			e.preventDefault();
			
			formGroup = playGame.children('.form-group');
			text = $(this).val();
			
			if (!_.isEmpty(text)) {
				if (formGroup.hasClass('has-error')) {
					formGroup.removeClass('has-error');
				}
			}
		});
		
		/* ページ間のメッセージを表示する */
		if (!_.isEmpty(message.data('message'))) {
			message.show().text(message.data('message'));
		}
		
		/* 現在のルーム使用状況を確認するストリーム */
		gameDataStream = new EventSource('/dev/html5/stream/online-game/game-data-stream.php');
		
		gameDataStream.addEventListener('game-data', function (e) {
			var data;
			if (!_.isEmpty(e.data)) {
				data = JSON.parse(e.data);
				updateGameData(data);
			}
		}, false);
		
		gameDataStream.addEventListener('open', function (e) {
			console.info('サーバーに接続しました。ゲームデータのストリーミングを開始します。');
		}, false);
		
		gameDataStream.addEventListener('error', function (e) {
			console.info('サーバーに再接続します。');
		}, false);
		
		gameDataStream.addEventListener('abort-game-data-stream', function (e) {
			aborted = true;
			console.warn('ゲームデータのストリーミングを中断しました。');
			gameDataStream.close();
		}, false);
		
		/* BFCache対策 */
		$(window).on('unload', _.noop);
		
		$(window).on('beforeunload', function () {
			if (!aborted) {
				console.warn('ゲームデータのストリーミングを中断しました。');
				gameDataStream.close();
			}
		});
		
		/* フォームデータの保存にWeb Storageを使用する */
		if (!_.isUndefined(window.sessionStorage)) {
			
			getStorageItem = function (key) {
				var value;
				value = sessionStorage.getItem(key);
				
				if (_.isNull(value)) {
					return void(0);
				}
				
				value = JSON.parse(value);
				return value;
			};
			
			setStorageItem = function (key, value) {
				value = JSON.stringify(value);
				sessionStorage.setItem(key, value);
			};
		}

		/* プレイヤー名をWeb Storageに保存する */
		if (!_.isUndefined(getStorageItem('player-name'))) {
			playerName.val(getStorageItem('player-name'));
		}

		function updateGameData(data) {
			var gameItem, lastIndex, prevClass, button, remainingTime;
			
			lastIndex = gameItems.children().length + 1;
			gameItem = gameItems.find('[id="' + data.id + '"]');
			
			remainingTime = Math.max(data.remaining_time, 0);
			
			if (gameItem.length) {
				/* すでにゲームデータの項目が存在する場合 */
				
				prevClass = gameItem.hasClass('alert-info') ? 'alert-info' :
					gameItem.hasClass('alert-success') ? 'alert-success' : 'alert-warning';
				
				gameItem.removeClass(prevClass).
					find('.player-number').text(data.player_number).end().
					find('.remaining-time').text(remainingTime);
				
			} else {
				gameItem = $(fragment.game).attr('id', data.id).
					find('.game-index').text(lastIndex).end().
					find('.game-id').text(data.id).end().
					find('.remaining-time').text(remainingTime).end().
					find('.player-number').text(data.player_number).end().
					appendTo(gameItems);
			}
			
			if (data.state !== 'active') {
				gameItem.find('.remaining-time').text('--');
			}
			
			if (data.player_number < MAX_PLAYER_LIMIT) {
				if (data.player_number === 0) {
					gameItem.addClass('alert-info');
				} else {
					gameItem.addClass('alert-success');
				}
				
				button = gameItem.find('button').prop('disabled', false).on('click', submitPlayerName);
			} else {
				gameItem.addClass('alert-warning');
				button = gameItem.find('button').prop('disabled', true);
			}
		}
		
		function submitPlayerName(e) {
			var name, selectedGame, id, formGroup;
			e.preventDefault();
			
			selectedGame = $(this).parents('.game-item');
			name = playerName.val();
			id = selectedGame.attr('id');
			formGroup = playGame.children('.form-group');
			
			if (name === '') {
				if (!formGroup.hasClass('has-error')) {
					formGroup.addClass('has-error');
				}
				formGroup.find('label').text('プレイヤー名が空白です');
				playerName.focus();
				return;
				
			} else if (name.length < _.first(NAME_LENGTH_LIMIT) || name.length > _.last(NAME_LENGTH_LIMIT)) {
				if (!formGroup.hasClass('has-error')) {
					formGroup.addClass('has-error');
				}
				formGroup.find('label').text('プレイヤー名は3文字以上20文字以下で指定してください');
				playerName.focus();
				return;
				
			} else if (!validReg.test(name)) {
				if (!formGroup.hasClass('has-error')) {
					formGroup.addClass('has-error');
				}
				formGroup.find('label').text('特殊文字(& < > " \'など)は使用できません');
				playerName.focus();
				return;
			}
			
			/* Web Storageにプレイヤー名を保存する */
			setStorageItem('player-name', name);
			
			$(this).off('click', submitPlayerName);
			
			/* 隠しフォームに選択したゲームIDを入れる */
			selectedId.val(id);
			
			playGame.submit();
		}
		
	});
}());
                

online-game.js

;(function () {
	var VIEW_WIDTH, VIEW_HEIGHT, GRID_COLUMNS, MESSAGE_X_OFFSET, MESSAGE_Y_OFFSET, MESSAGE_TIMEOUT, MESSAGE_DALAY, MAX_PLAYER_SPEED, PROJECTILE_SPEED, ACCEL_RATE, DECEL_RATE, ACCEL, DECEL, PLAYER_HP, AJAX_TIMEOUT, AJAX_TIMEOUT_INCREMENTAL_VALUE, AJAX_INTERVAL, LOBBY_URL, REQUEST_ALIVE_LIST, REQUEST_START_GAME, SEND_EVENT_DATA_URL, EVENT_DATA_STREAM_URL, INVALID_ACCESS, INVALID_DATA, INVALID_COLLISION, DUPLICATE_PROCESS, UNKNOWN_ERROR, FAIL_TO_SEND_DATA, FPS, FRAME_INTERVAL, STREAMING_FPS, FIRING_INTERVAL, INVINCIBLE_TIME, SOUND_VOLUME, ASSETS, floor, ceil, sin, cos, atan2, PI;
	
	/* ゲーム画面のサイズ。サーバーの値と同期を保つ必要がある */
	VIEW_WIDTH = 1280;
	VIEW_HEIGHT = 720;
	
	GRID_COLUMNS = 16;
	MESSAGE_X_OFFSET = 8;
	MESSAGE_Y_OFFSET = 0.5;
	
	MESSAGE_TIMEOUT = 3000;
	MESSAGE_DALAY = 1000;
	
	/* 描画メソッドのフレームレートとフレーム間隔(ミリ秒)。サーバーの値と同期を保つ必要がある */
	FPS = 30;
	FRAME_INTERVAL = 1000 / FPS;
	
	/* オブジェクトの最高速度と加速度。サーバーの値と同期を保つ必要がある */
	MAX_PLAYER_SPEED = 0.2;
	PROJECTILE_SPEED = 0.8;
	ACCEL_RATE = 0.0006;
	DECEL_RATE = 0.0006;
	ACCEL = ACCEL_RATE * FRAME_INTERVAL;
	DECEL = DECEL_RATE * FRAME_INTERVAL;
	
	/* プレイヤーの初期HP。サーバーの値と同期を保つ必要がある */
	PLAYER_HP = 100;
	
	/* ストリーミングのFPS。サーバーの値と同期を保つ必要がある */
	STREAMING_FPS = 12;
	
	AJAX_TIMEOUT = 3000;
	AJAX_TIMEOUT_INCREMENTAL_VALUE = 1000;
	AJAX_INTERVAL = 1000 / STREAMING_FPS;
	
	/* リダイレクト先URL。サーバーの値と同期を保つ必要がある */
	LOBBY_URL = '/dev/html5/stream/online-game/lobby.php';
	REQUEST_ALIVE_LIST = '/dev/html5/stream/online-game/request-alive-list.php';
	REQUEST_START_GAME = '/dev/html5/stream/online-game/start-game.php';
	SEND_EVENT_DATA_URL = '/dev/html5/stream/online-game/send-event-data.php';
	EVENT_DATA_STREAM_URL = '/dev/html5/stream/online-game/event-data-stream.php';
	
    /* エラーコード。サーバーの値と同期させる必要がある */
	INVALID_ACCESS = 1;
	INVALID_DATA = 2;
	INVALID_COLLISION = 3;
	DUPLICATE_PROCESS = 4;
	UNKNOWN_ERROR = 5;
	FAIL_TO_SEND_DATA = 6;
	
	FIRING_INTERVAL = 300;
	INVINCIBLE_TIME = 1000;
	
	SOUND_VOLUME = 0.2;
	ASSETS = {
		sound: {
			bgm: '/audio/bgm/Battle-impalpable_loop.ogg',
			shot: '/audio/sound/powerdown02.mp3',
			damage: '/audio/sound/shoot1.mp3',
		},
	};
	
	floor = Math.floor;
	ceil = Math.ceil;
	sin = Math.sin;
	cos = Math.cos;
	atan2 = Math.atan2;
	PI = Math.PI;
	
	phina.globalize();
	
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		
		messageDefaultStyle: {
			fontSize: 35,
			fontWeight: 'bolder',
			fill: 'white',
		},
		
		init: function (options) {
			this.superInit(options);
			
			/* 参加プレイヤー全員のオブジェクトを入れる配列 */
			this.objects = [];
			
			/* プレイヤー自身のオブジェクトを入れるプロパティ */
			this.self = null;
			
			/* 画面上部のメッセージ表示エリア */
			this.messageWindow = Label({
				text: '',
				fontSize: 35,
				fill: 'white',
			});
			
			this.messageWindow.x = this.gridX.span(MESSAGE_X_OFFSET);
			this.messageWindow.y = this.gridX.span(MESSAGE_Y_OFFSET);
			this.messageWindow.addChildTo(this);
			
			/* メッセージのスタック */
			this.message = {};
		},
		
		displayMessage: function (text, style) {
			var id, delay;
			id = Random.uuid();
			delay = 0;
			
			text = text || '';
			style = style || {};
			
			style = _.defaults(style, this.messageDefaultStyle);
			
			if (_.size(this.message) > 0) {
				delay = MESSAGE_DALAY;
			}
			
			this.message[id] = {
				style: style,
				text: text,
			};
			
			window.setTimeout(_.bind(function (messageId) {
				var message, messageWindow;
				message = this.message[messageId];
				
				messageWindow = this.messageWindow;
				messageWindow.fontSize = message.style.fontSize;
				messageWindow.fontWeight = message.style.fontWeight;
				messageWindow.fill = message.style.fill;
				messageWindow.text = message.text;
				
				window.setTimeout(_.bind(function () {
					delete this.message[messageId];
					
					if (_.size(this.message) === 0) {
						messageWindow.text = '';
					}
					
				}, this), MESSAGE_TIMEOUT);
			}, this), delay, id);
		},
		
		addPlayer: function (data, playerId) {
			var object, gunport, shape, weapon, gauge, nameLabel, scoreLabel;
			
			object = DisplayElement({
				width: data.size * 2,
				height: data.size * 2,
			}).addChildTo(this);
			
			gauge = Gauge({
				x: 0,
				y: data.size * 1.4,
				width: data.size * 2,
				height: 5,
				cornerRadius: 1,
				maxValue: PLAYER_HP,
				value: data.hp,
				fill: 'white',
				gaugeColor: 'skyblue',
				stroke: 'silver',
				strokeWidth: 5,
			}).addChildTo(object);
			
			object.gauge = gauge;
			
			gunport = DisplayElement({
				width: data.size * 2,
				height: data.size * 2,
			}).addChildTo(object);
			
			weapon = RectangleShape({
				y: data.size,
				width: data.size,
				height: data.size,
				strokeWidth: 0,
				fill: 'white',
			}).addChildTo(gunport);
			
			if (data.shape === 'circle') {
				shape = CircleShape({
					radius: data.size,
					stroke: 'silver',
					strokeWidth: 6,
					fill: data.color,
				}).addChildTo(object);
				
				object.shape = shape;
			}
			
			gunport.rotation = data.dir;
			object.gunport = gunport;
			
			nameLabel = Label({
				fontSize: 22,
				text: data.name,
				fill: '#FFF',
			});
			nameLabel.addChildTo(object);
			
			scoreLabel = Label({
				y: 25,
				fontSize: 25,
				text: data.score,
				fill: '#FFF',
			});
			scoreLabel.addChildTo(object);
			object.scoreLabel = scoreLabel;
			
			/* データがプレイヤー自身の場合 */
			if (data.id === playerId) {
				shape.stroke = 'white';
				shape.strokeWidth = 10;
				shape.fill = 'blue';
				this.self = object;
			}
			
			object.isStiff = false;
			object.stiffTime = 0;
			
			object.id = data.id;
			object.name = data.name;
			object.state = data.state;
			
			object.entityType = data.entity_type;
			object.friendId = data.friend_id.split('|');
			
			object.hp = data.hp;
			object.atk = data.atk;
			object.def = data.def;
			
			object.x = data.x;
			object.y = data.y;
			
			object.vx = data.vx;
			object.vy = data.vy;
			
			object.ax = data.ax;
			object.ay = data.ay;
			
			object.dir = data.dir;
			
			this.objects.push(object);
			this.objects[object.id] = object;
			
			return object;
		},
		
		addProjectile: function (data) {
			var object, shape;
			
			object = DisplayElement({
				width: data.size * 2,
				height: data.size * 2,
			});
			
			this.addChildAt(object, 0);
			
			if (data.shape === 'circle') {
				shape = CircleShape({
					radius: data.size,
					stroke: 'silver',
					strokeWidth: 6,
					fill: data.color,
				}).addChildTo(object);
				
				object.shape = shape;
			}
			
			object.id = data.id;
			object.state = data.state;
			
			object.entityType = data.entity_type;
			object.friendId = data.friend_id.split('|');
			
			object.hp = data.hp;
			object.atk = data.atk;
			object.def = data.def;
			
			object.x = data.x;
			object.y = data.y;
			
			object.vx = data.vx;
			object.vy = data.vy;
			
			object.ax = data.ax;
			object.ay = data.ay;
			
			object.dir = data.dir;
			
			this.objects.push(object);
			this.objects[object.id] = object;
			
			_.each(object.friendId, _.bind(function (id) {
				var friend;
				if (id !== object.id) {
					friend = this.objects[id];
					if (!_.isUndefined(friend)) {
						friend.friendId.push(object.id);
					}
				}
			}, this));
			
			return object;
		},
		
		removeObject: function (id) {
			var object, index, friendId, friend, i, l;
			
			object = this.objects[id];
			object.remove();
			
			/* Scene.objectsリストから削除 */
			index = _.indexOf(this.objects, object);
			if (index >= 0) {
				this.objects.splice(index, 1);
			}
			
			/* friendIdリストにこのオブジェクトを登録しているオブジェクトから自身を削除 */
			for (i = 0, l = object.friendId.length; i < l; i += 1) {
				friendId = object.friendId[i];
				if (friendId !== id) {
					friend = this.objects[friendId];
					if (!_.isUndefined(friend)) {
						friend.friendId = _.without(friend.friendId, id);
					}
				}
			}
			
			delete this.objects[id];
			
			return object;
		},
		
		moveObject: function (data) {
			var object;
			/* プレイヤー自身のイベントデータであればスキップ */
			if (this.self.id === data.id) {
				return;
			}
			
			object = this.objects[data.id];
			if (_.isUndefined(object)) {
				console.warn('オブジェクトが見つかりません。');
				return;
			}
			
			object.x = data.x;
			object.y = data.y;
			
			object.vx = data.vx;
			object.vy = data.vy;
			
			object.ax = data.ax;
			object.ay = data.ay;
			
			object.dir = data.dir;
		},
		
		statusChange: function (data) {
			object = this.objects[data.id];
			if (_.isUndefined(object)) {
				console.warn('オブジェクトが見つかりません。');
				return;
			}
			
			if (object.entityType === 'player') {
				object.gauge.value = data.hp;
			}
		},
		
		scoreChange: function (data) {
			object = this.objects[data.id];
			if (_.isUndefined(object)) {
				console.warn('オブジェクトが見つかりません。');
				return;
			}
			
			object.scoreLabel.text = floor(data.score);
		},
		
		damageEffect: function (targetId) {
			var object, star, defaultSize;
			
			object = this.objects[targetId];
			defaultSize = object.shape.radius * 0.8;
			star = StarShape({
				radius: defaultSize,
				sides: 10,
				sideIndent: 0.5,
				rotation: 0,
			}).addChildTo(object);
			
			star.tweener.to({
				radius: defaultSize * 2,
				rotation: 90,
			}, 300, 'easeOutBounce').call(function () {
				star.remove();
			});
			
			SoundManager.play('damage');
		},
		
		update: function (app) {
			var self, pointer, keyboard, moved, trigger, eventData, i, l, j, m, object, friendId, c1, c2, dir, recoilVx, recoilVy;
			
			self = this.self;
			pointer = app.pointer;
			keyboard = app.keyboard;
			trigger = false;
			moved = false;
			
			/* プレイヤー自身がゲームフィールドにまだ生成されていなければ更新しない */
			if (_.isNull(self)) {
				return;
			}
			
			if (keyboard.getKeyDown('left') || keyboard.getKeyDown('a')) {
				moved = true;
				self.ax -= 1;
			}
			
			if (keyboard.getKeyDown('right') || keyboard.getKeyDown('d')) {
				moved = true;
				self.ax += 1;
			}
			
			if (keyboard.getKeyUp('left') || keyboard.getKeyUp('a')) {
				moved = true;
				self.ax += 1;
			}
			
			if (keyboard.getKeyUp('right') || keyboard.getKeyUp('d')) {
				moved = true;
				self.ax -= 1;
			}
			
			if (keyboard.getKeyDown('up') || keyboard.getKeyDown('w')) {
				moved = true;
				self.ay -= 1;
			}
			
			if (keyboard.getKeyDown('down') || keyboard.getKeyDown('s')) {
				moved = true;
				self.ay += 1;
			}
			
			if (keyboard.getKeyUp('up') || keyboard.getKeyUp('w')) {
				moved = true;
				self.ay += 1;
			}
			
			if (keyboard.getKeyUp('down') || keyboard.getKeyUp('s')) {
				moved = true;
				self.ay -= 1;
			}
			
			if (keyboard.getKeyDown('space')) {
				trigger = true;
			}
			
			if (!self.isStiff) {
				self.dir = getDegree(self.x, self.y, pointer.x, pointer.y);
				
			} else {
				self.stiffTime -= app.deltaTime;
				if (self.stiffTime < 0) {
					self.isStiff = false;
					self.stiffTime = 0;
				}
			}
			
			/* 移動イベントが発生したらデータをAjax送信 */
			if (moved) {
				eventData = JSON.stringify({
					event_type: 'movement',
					id: self.id,
					
					x: self.x,
					y: self.y,
					vx: self.vx,
					vy: self.vy,
					ax: self.ax,
					ay: self.ay,
					dir: self.dir,
				});
				
				app.sendDataProcess('movement', eventData);
			}
			
			/* 弾丸発射 */
			if (trigger) {
				self.isStiff = true;
				self.stiffTime = FIRING_INTERVAL / 2;
				
				dir = self.dir.toRadian();
				recoilVx = sin(dir) * MAX_PLAYER_SPEED / 2;
				recoilVy = cos(dir) * MAX_PLAYER_SPEED / 2;
				self.vx -= recoilVx;
				self.vy -= recoilVy;
				
				app.firing(self);
			}
			
			/* 衝突判定に使用するオブジェクト */
			c1 = Circle();
			c2 = Circle();
			
			/* オブジェクトの移動処理 */
			for (i = this.objects.length - 1, l = 0; i >= 0; i -= 1) {
				object = this.objects[i];
				
				/* オンラインプレイヤーの移動処理 */
				if (object.entityType === 'player') {
					if (object.ax > 0) {
						if (object.vx < 0) {
							object.vx += ACCEL + DECEL;
						} else {
							object.vx += ACCEL;
						}
						
						if (object.vx > MAX_PLAYER_SPEED) {
							object.vx = MAX_PLAYER_SPEED;
						}
					} else if (object.ax < 0) {
						if (object.vx > 0) {
							object.vx -= ACCEL + DECEL;
						} else {
							object.vx -= ACCEL;
						}
						
						if (object.vx < -MAX_PLAYER_SPEED) {
							object.vx = -MAX_PLAYER_SPEED;
						}
					} else {
						if (object.vx > 0) {
							object.vx -= DECEL;
							
							if (object.vx < 0) {
								object.vx = 0;
							}
						} else if (object.vx < 0) {
							object.vx += DECEL;
							
							if (object.vx > 0) {
								object.vx = 0;
							}
						}
					}
					
					if (object.ay > 0) {
						if (object.vy < 0) {
							object.vy += ACCEL + DECEL;
						} else {
							object.vy += ACCEL;
						}
						
						if (object.vy > MAX_PLAYER_SPEED) {
							object.vy = MAX_PLAYER_SPEED;
						}
					} else if (object.ay < 0) {
						if (object.vy > 0) {
							object.vy -= ACCEL + DECEL;
						} else {
							object.vy -= ACCEL;
						}
						
						if (object.vy < -MAX_PLAYER_SPEED) {
							object.vy = -MAX_PLAYER_SPEED;
						}
					} else {
						if (object.vy > 0) {
							object.vy -= DECEL;
							
							if (object.vy < 0) {
								object.vy = 0;
							}
						} else if (object.vy < 0) {
							object.vy += DECEL;
							
							if (object.vy > 0) {
								object.vy = 0;
							}
						}
					}
				}
				
				object.x += (object.vx * app.deltaTime);
				object.y += (object.vy * app.deltaTime);
				
				/* プレイヤーオブジェクトは砲口の向きを更新する */
				if (object.entityType === 'player') {
					object.gunport.rotation = -object.dir;
				}
				
				/* オブジェクトがエリア外に出たときの処理 */
				if (object.entityType === 'player') {
					if (object.x < 0) {
						object.x = 0;
						object.vx = 0;
						
					} else if (object.x > VIEW_WIDTH) {
						object.x = VIEW_WIDTH;
						object.vx = 0;
					}
					
					if (object.y < 0) {
						object.y = 0;
						object.vy = 0;
						
					} else if (object.y > VIEW_HEIGHT) {
						object.y = VIEW_HEIGHT;
						object.vy = 0;
					}
				} else if (object.entityType === 'projectile') {
					if (object.x < 0 || object.x > VIEW_WIDTH || object.y < 0 || object.y > VIEW_HEIGHT) {
						/* オブジェクトの更新を止めて、「消滅」イベントをAjax送信 */
						if (object.isAwake()) {
							object.sleep();
							
							if (self.friendId.indexOf(object.id) !== -1) {
								app.vanishment(object);
							}
						}
						continue;
					}
				}
				
				/* 衝突判定 */
				for (j = 0, m = self.friendId.length; j < m; j += 1) {
					if (!_.contains(self.friendId, object.id)) {
						friend = this.objects[self.friendId[j]];
						
						c1.x = friend.x;
						c1.y = friend.y;
						c1.radius = friend.shape.radius;
						
						c2.x = object.x;
						c2.y = object.y;
						c2.radius = object.shape.radius;
						
						if (Collision.testCircleCircle(c1, c2)) {
							app.collision(friend, object, self);
						}
					}
				}
			}
		},
	});
	
	phina.define('MyApp', {
		superClass: 'CanvasApp',
		
		init: function(options) {
			var mainScene, loadingOptions, loadingClass, loading;
			
			this.superInit(options);
			
			SoundManager.setVolume(0.2);
			
			mainScene = this.mainScene = MainScene(options);
			this.pushScene(mainScene);
			
			/* Ajax送信頻度を制限する */
			this.sendDataProcess = _.throttle(this.sendDataProcess, AJAX_INTERVAL);
			
			/* 発射間隔を制限する */
			this.firing = _.throttle(
				_.bind(this.firing, this),
				FIRING_INTERVAL,
				{trailing: false}
			);
			
			/* 衝突判定が発生する頻度を制限する */
			this.collision = _.throttle(
				_.bind(this.collision, this),
				INVINCIBLE_TIME,
				{trailing: false}
			);
			
			if (!_.isUndefined(options.assets)) {
				loadingOptions = ({}).$extend(options, {
					exitType: '',
				});
				loadingClass = phina.global.LoadingScene || phina.game.LoadingScene;
				loading = loadingClass(loadingOptions);
				this.replaceScene(loading);
				
				loading.onloaded = function() {
					/* BGMを流す */
					SoundManager.playMusic('bgm');
					
					this.replaceScene(mainScene);
					if (options.debug) {
						this._enableDebugger();
					}
				}.bind(this);
			} else {
				this.replaceScene(mainScene);
				if (options.debug) {
					this._enableDebugger();
				}
			}
		},
		
		firing: function (object) {
			var eventData;
			
			eventData = JSON.stringify({
				event_type: 'firing',
				id: object.id,
				friendId: object.friendId.join('|'),
				x: object.x,
				y: object.y,
				dir: object.dir,
			});
			
			this.sendDataProcess('firing', eventData);
		},
		
		collision: function (object, target, master) {
			var eventData;
			
			eventData = JSON.stringify({
				event_type: 'collision',
				id: object.id,
				target_id: target.id,
				master_id: master.id,
			});
			
			this.sendDataProcess('collision', eventData);
		},
		
		vanishment: function (object) {
			var eventData;
			
			eventData = JSON.stringify({
				event_type: 'vanishment',
				id: object.id,
				friendId: object.friendId.join('|'),
			});
			
			this.sendDataProcess('vanishment', eventData);
		},
		
		sendDataProcess: function (eventType, eventData, token) {
			token = !_.isUndefined(token) ? token : Random.uuid();
			
			this.sendEventData(eventType, eventData, token).then(
				function (info) {
					if (info === 'invalid-collision') {
						console.log('サーバー側の衝突判定が無効でした。');
					} else if (info === 'withdrawal') {
						/* リダイレクト処理 */
						window.location.href = LOBBY_URL;
					}
				},
				_.bind(function (error) {
					if (error === 'timeout') {
						/* タイムアウトした場合は時間を延長して再試行 */
						AJAX_TIMEOUT += AJAX_TIMEOUT_INCREMENTAL_VALUE;
						this.sendDataProcess(eventType, eventData, token);
						
					} else if (error === 'withdrawal') {
						eventData = JSON.stringify({
							'event_type': 'withdrawal',
							'id': this.playerId,
						});
						this.sendDataProcess('withdrawal', eventData, token);
						
					} else if (error === 'retry') {
						this.sendDataProcess(eventType, eventData, token);
					}
				}, this)
			);
		},
		
		sendEventData: function (eventType, eventData, token) {
			var deferred, jqXHR;
			
			deferred = new $.Deferred();
			jqXHR = $.ajax({
				type: 'POST',
				url: SEND_EVENT_DATA_URL,
				dataType: 'text',
				data: {
					nonce: this.nonce,
					token: token,
					data: eventData,
				},
				timeout: AJAX_TIMEOUT,
			});
			
			jqXHR.then(
				function (response) {
					if (_.isUndefined(response.error)) {
						deferred.resolve(eventType);
					
					/* サーバーエラー。エラーコード別に対処 */
					} else {
						/* サーバー側の衝突判定の検証が無効だった場合は処理続行 */
						if (response.error === INVALID_COLLISION) {
							deferred.resolve('invalid-collision');
							
						/* Ajax送信を再試行 */
						} else if (response.error === FAIL_TO_SEND_DATA) {
							deferred.reject('retry');
						
						/* プレイヤーの離脱処理へ */
						} else {
							deferred.reject('withdrawal');
						}
					}
				},
				function (response, status) {
					if (status === 'timeout') {
						deferred.reject('timeout');
						
					/* 通信エラーその他。ゲーム離脱の処理 */
					} else {
						deferred.reject('withdrawal');
					}
				}
			);
			
			return deferred.promise();
		},
	});
	
	/* ページロード後、最初に呼び出されるメソッド */
	phina.main(function () {
		var nonce, gameId, playerId, playerName, aborted, options, app, scene, eventDataStream;
		
		/* サーバーから送られたノンスを変数に代入する */
		nonce = $('#nonce').data('nonce');
		
		/* サーバーから渡されるゲーム情報 */
		gameId = $('#game-id').data('game-id');
		playerId = $('#player-id').data('player-id');
		playerName = $('#player-name').data('player-name');
		
		/* ストリーミングが中断されたかどうかのフラグ */
		aborted = false;
		
		/* ゲームエンジン起動 */
		options = {
			width: VIEW_WIDTH,
			height: VIEW_HEIGHT,
			backgroundColor: '#000',
			fontFamily: 'HiraKakuProN-W3, sans-serif',
			columns: GRID_COLUMNS,
			assets: ASSETS,
		};
		
		/* アプリケーションを起動 */
		app = MyApp(options);
		app.fps = FPS;
		app.nonce = nonce;
		app.playerId = playerId;
		
		scene = app.mainScene;
		requestProcess1();
		

		app.run();
		
		/* BFCache対策 */
		$(window).on('unload', _.noop);
		
		$(window).on('beforeunload', function () {
			if (!aborted) {
				console.warn('イベントデータのストリーミングを中断しました。');
				eventDataStream.close();
			}
		});
		
		function requestAliveList() {
			var deferred, jqXHR;
			deferred = new $.Deferred();
			
			jqXHR = $.ajax({
				url: REQUEST_ALIVE_LIST,
				data: {nonce: nonce},
				type: 'POST',
				dataType: 'json',
				timeout: AJAX_TIMEOUT,
			});
			
			jqXHR.then(
				function (response) {
					var aliveList;
					if (_.isUndefined(response.error)) {
						aliveList = response.alive_list;
						
						_.each(aliveList, function (data) {
							scene.addPlayer(data);
						});
						
						deferred.resolve();
					} else {
						/* サーバーエラー。ゲーム離脱の処理 */
						deferred.reject('withdrawal');
					}
				},
				function (response, status) {
					if (status === 'timeout') {
						/* タイムアウトした場合は時間を延長して再試行 */
						AJAX_TIMEOUT += AJAX_TIMEOUT_INCREMENTAL_VALUE;
						deferred.reject('timeout');
					} else {throw Error('aaa');
						/* 通信エラー。ゲーム離脱の処理 */
						deferred.reject('withdrawal');
					}
				}
			);
			
			return deferred.promise();
		}
		
		function startGame(token) {
			var deferred, jqXHR;
			
			deferred = new $.Deferred();
			jqXHR = $.ajax({
				type: 'POST',
				url: REQUEST_START_GAME,
				data: {
					nonce: nonce,
					token: token,
				},
				dataType: 'text',
				timeout: AJAX_TIMEOUT,
			});
			
			jqXHR.then(
				function (response) {
					if (!_.isEmpty(response)) {
						deferred.resolve();
					} else {
						/* サーバーエラー。ゲーム離脱の処理 */
						deferred.reject('withdrawal');
					}
				},
				function (response, status) {
					if (status === 'timeout') {
						deferred.reject('timeout');
					} else {
						/* 通信エラー。ゲーム離脱の処理 */
						deferred.reject('withdrawal');
					}
				}
			);
			
			return deferred.promise();
		}
		
		function requestProcess1() {
			var promise;
			promise = requestAliveList();
			promise.then(
				requestProcess2,
				function (status) {
					var eventData;
					if (status === 'timeout') {
						/* タイムアウトした場合は時間を延長して再試行 */
						AJAX_TIMEOUT += AJAX_TIMEOUT_INCREMENTAL_VALUE;
						requestProcess1();
					} else if (status === 'withdrawal') {
						eventData = JSON.stringify({
							'event_type': 'withdrawal',
							'id': app.playerId,
						});
						app.sendDataProcess('withdrawal', eventData);
					}
				}
			);
		}
		
		function requestProcess2(token) {
			var promise;
			token = !_.isUndefined(token) ? token : Random.uuid();
			
			promise = startGame(token);
			promise.then(
				function () {
					/* Server-sent Eventsを開始 */
					eventDataStream = new EventSource(EVENT_DATA_STREAM_URL);
					
					/* サーバーからのストリームデータを受信 */
					eventDataStream.addEventListener('event-data', function (e) {
						var eventList, eventData, eventType, entityType, object, message, i, l;
						eventList = JSON.parse(e.data);
						
						if (!_.isArray(eventList)) {
							eventList = [eventList];
						}
						
						for (i = 0, l = eventList.length; i < l; i += 1) {
							eventData = eventList[i];
							eventType = eventData.event_type;
							
							switch (eventType) {
								/* 新規プレイヤーが参入 */
								case 'spawned':
								entityType = eventData.entity_type;
								/* ストリームデータがプレイヤータイプの場合 */
								if (entityType === 'player') {
									object = scene.addPlayer(eventData, playerId);
									
									message = object.name + 'が乱入しました!';
									scene.displayMessage(message, {fill: 'red'});
								/* ストリームデータが弾丸タイプの場合 */
								} else if (entityType === 'projectile') {
									SoundManager.play('shot');
									scene.addProjectile(eventData);
								}
								
								break;
								
								case 'killed':
								if (eventData.id === playerId) {
									/* ロビーページへリダイレクト */
									window.setTimeout(function () {
										window.location.href = LOBBY_URL;
									}, 500);
									break;
								}
								
								object = scene.objects[eventData.id];
								message = object.name + 'が離脱しました!';
								scene.displayMessage(message, {fill: 'skyblue'});
								
								scene.removeObject(eventData.id);
								
								break;
								
								case 'movement':
								scene.moveObject(eventData);
								break;
								
								case 'status_change':
								scene.statusChange(eventData);
								break;
								
								case 'score_change':
								scene.scoreChange(eventData);
								break;
								
								case 'collision':
								scene.damageEffect(eventData.target_id);
								break;
								
								case 'vanished':
								scene.removeObject(eventData.id);
								break;
								
							}
						}
					}, false);
					
					eventDataStream.addEventListener('open', function (e) {
						console.info('サーバーに接続しました。ゲームデータのストリーミングを開始します。');
					}, false);
					
					eventDataStream.addEventListener('error', function (e) {
						console.info('サーバーに再接続します。');
					}, false);
					
					eventDataStream.addEventListener('abort-event-data-stream', function (e) {
						aborted = true;
						console.warn('イベントデータのストリーミングを中断しました。');
						eventDataStream.close();
					}, false);
				},
				function (status) {
					var eventData;
					if (status === 'timeout') {
						/* タイムアウトした場合は時間を延長して再試行 */
						AJAX_TIMEOUT += AJAX_TIMEOUT_INCREMENTAL_VALUE;
						requestProcess2(token);
					} else if (status === 'withdrawal') {
						eventData = JSON.stringify({
							'event_type': 'withdrawal',
							'id': app.playerId,
						});
						app.sendDataProcess('withdrawal', eventData);
					}
				}
			);
		}
	});
	
	function getDegree(x1, y1, x2, y2) {
		var radian, degree, disX, disY;
		
		disX = x2 - x1;
		disY = y2 - y1;
		
		radian = atan2(disX, disY);
		
		if (radian < 0) {
			radian += PI * 2;
		}
		
		degree = radian * 180 / PI;
		
		return floor(degree);
	}
	
}());