Posted at
Unity 2Day 22

Unity+FuelPHP+PhotonNetworkでオンラインゲームの基礎作り

More than 1 year has passed since last update.


はじめに

この記事は Unity2 AdventCalendar 22日目の記事になります

http://qiita.com/advent-calendar/2016/unity2

本エントリでは、ネトゲ製作の入り口的な事を行います

具体的な内容は、UnityからHTTPでサーバーにログイン情報をPOSTし、ログイン処理を行い、ユーザー名を受け取ってPhotonNetworkに繋ぐという事を行います

本エントリの内容を応用すれば、サーバー側に所持金やアイテム所持情報を持たせて通信を行うなど、ネットゲームに欠かせない処理のアレコレができるようになります。


想定している読者のレベル

ターゲットは、Unityは扱えるがPHPやサーバー回りはサッパリという方です。ある程度プログラミングを理解でき、C#とUnityを使える事を前提にしていますので、Unityの操作などは割愛させていただきます。

PHPコードの詳しい解説は省略します

サーバーにLinux環境を使用しますが、極力コマンド入力などは避けるようにしました。

今回は基礎部分の作り方・考え方を簡潔に紹介するように書いているため、セキュリティ部分などはガバガバになっています。パスワードのハッシュ化すらも省いてますしコードも簡潔です。その点お許しください。その手に持ってるマサカリをしまってください・・・


Linux環境を用意しよう - Cloud9のセットアップ

今回は、簡単にLAMP(Linux Apache MySQL PHP)環境を用意でき、IDEも付属しているCloud9を利用してサーバーを構築します

https://c9.io/

メールアドレスの他、GitHub、BitBucketのアカウントを使用する事ができます

登録が完了したらWorkspaces画面に移動するので「Create a new workspace」をクリックしてワークスペースを作成します。

このワークスペースはUbuntuベースでVPSのように構築されており、sudoやaptを利用したパッケージ追加もできるなど、かなり自由度の高い構成になっています。

なお、一定時間経過でインスタンスがスリープするので、常時公開のサーバーとしての利用はできません。


Cloud9 ワークスペースを作成する

・Workspace name - ワークスペース名を入力します

・Description - 説明を入力します。空欄のままでも構いません。

・公開設定 - Publicを選択しましょう

・Clone from Git or Mercurial URL - 既にリポジトリを所持している場合、ワークスペースへのクローンが行えますが、今回は使用しません

・Choose a Template - ここで選択した環境がセットアップ済みの状態でワークスペースが作成されます。今回は「PHP,Apache & ~」を選択しましょう

 

設定が終わったら、Create Workspaceをクリックして完了します

セットアップまで少し時間がかかりますが、完了するとCloud9 IDEへ移動します

この画面がCloud9 IDEになります。

左ペインがファイルブラウザ、右ペイン上部がエディタ、右ペイン下部がターミナルになります。


PHPフレームワーク FuelPHPのインストール

http://fuelphp.com/

トップページの「Download v1.8.0 now!」をクリックし、FuelPHPをダウンロードします

次にローカルでzipを解凍し、ワークスペースへ転送します

ワークスペースにファイルを転送する方法は、Cloud9 IDEの左ペインにドラッグアンドドロップするだけです。

転送が完了したら、FuelPHPのディレクトリをリネームして「fuelphp」にしましょう

ファイル・ディレクトリのリネーム方法は、対象を右クリックしてRenameを選択します。

ここで一度、FuelPHPが正常に動作しているか確認しましょう

IDE上部、メニューバー右の「Run Project」をクリックします

IDE右下ペインのタブが切り替わり、Apacheのログが出力されます

一番上の行に「Starting Apache httpd, serving ~」とURLが表示されるので、クリックしてOpenを選択

fuelphp->publicと辿って、Welcomeページが表示されたらFuelPHPの導入に成功です!


データベースをセットアップしよう - phpmyadminnのインストール

次は、FuelPHPから利用するデータベースのセットアップを行います

MySQLは既に導入されていますので、データベースの作成のみ行います

まず、管理用のGUIツール「PHPMyAdmin」を導入します

右下ペインをbashに切り替えて、次のコマンドを入力してください

phpmyadmin-ctl install

 

コマンドが正常に実行されると

「PHPMyAdmin Installation complete. You can log in at: https://hogehoge」

のようなログが出力されます。ここで表示されるURLとUsernameを記録しておきます

 

上記で表示されたURLを開くと、PHPMyAdminのログイン画面が表示されます。

先ほど表示されたUsernameを入力してログインします(パスワードは空)

ログインに成功したら、画面上部の「データベース」をクリックします

次に、データベースを作成する の所に情報を入力し、データベースを作成します

・名前 - unity

・照合順序 - utf8_general_ci

 

必要な情報を入力したら「作成」をクリックします

データベースの作成に成功したら、データベース一覧に先ほど作成した「unity」が表示されるのでクリックします

「このデータベースにはテーブルがありません」というメッセージが表示されるので、テーブルを作成します

今回は、ログインに必要なアカウント情報を格納するテーブル「account」を作成します

必要な要素は

・DB側のインデックス(id)

・ログイン用のid(今回はメールアドレス形式を想定します)

・パスワード

・ゲーム内のプレイヤー名

なので

名前 - account

カラム数 - 4

と入力して「実行」をクリックします

各項目を次の通りに設定してください

idのインデックスをPRIMARYにして、A.I(オートインクリメント 自動で連番にする)のチェックを入れ忘れないで!

入力が終わったら「保存する」を押します。

これで、テーブルが作成されました。


テストデータの作成

次に、FuelPHPからDBの接続を確認するためのテストデータを作成します

先ほど作成したaccountテーブルを選択し、PHPMyAdminの上部メニューより「挿入」を選択します

次の通りに入力し、実行します

正常に完了すると

** 1 行挿入しました。**

id 1 の行を挿入しました

と表示されます


FuelPHPとDBをつなぐ

次のステップでは、FuelPHPからMySQLを利用するための設定を行います

今回は、FuelPHPのORMという機能を利用します

まず、ORMを使用するためにFuelPHPでORMパッケージを有効にします。次のファイルを開きます

fuelphp/fuel/app/config/config.php

設定ファイルは次の場所にあります

fuelphp/fuel/app/config/development/db.php

always_loadと書かれている場所を探し、先頭の//を消してコメントアウトをはずします

最後の// ),の部分のコメントアウトもはずし忘れないように!

次に、always_loadの数行下の次の部分を探します

// 'packages'  => array(

// //'orm',
// ),

これも全てコメントアウトをはずし、保存しましょう


'packages' => array(
'orm',
),

これでFuelPHPのORMを使用できるようになりました

config配下にはdevelopmentディレクトリの他に、productionというディレクトリも存在します

FuelPHPではApacheの環境変数を設定することで、development(開発サーバー)とproduction(本番運用環境)で設定を切り替えて動作させることができます

デフォルトではdevelopmentになっているので、今回はdevelopmentの方の設定ファイルを変更します

db.phpを開くと、次のようになっています

<?php

/**
* The development database settings. These get merged with the global settings.
*/

return array(
'default' => array(
'connection' => array(
'dsn' => 'mysql:host=localhost;dbname=fuel_dev',
'username' => 'root',
'password' => 'root',
),
),
);

これを次のように変更しましょう。自分のユーザー名や、MySQLで設定したパスワード等は適宜変更してください

<?php

/**
* The development database settings. These get merged with the global settings.
*/

return array(
'default' => array(
'connection' => array(
'dsn' => 'mysql:host=localhost;dbname=unity(他の名前でDBを作成していたらそのDB名)',
'username' => 'Cloud9のユーザー名',
'password' => 'MySQLのパスワード、設定していなければ空',
),
),
);

変更が完了したら保存します。ショートカットキーのCtrl+S(MacならCmd+S)で保存できます


コントローラーとモデルの作成

このステップでは、データベースの操作を行うためのモデルと、データを扱ってリクエストを返すコントローラーを作成します

まずはModelを作成します

fuel/app/classes/model/ を開き、右クリックしてNew Fileを選択、ファイル名はaccount.phpにしましょう

account.phpの内容を次のようにします

<?php

<?php

class Model_Account extends Orm\Model{

protected static $_properties = array(
'id',
'email',
'password',
'player_name',
);

protected static $_table_name = 'account';
protected static $_primariy = array('id');

public static function login($data){

$query_result = array();
$result = array(
'result' => false,
'player_name' => ''
);

$query = self::find('all', array(
'where' => array(
array('email', $data['email']),
array('password', $data['password']),
)
));

foreach ($query as $query) {
array_push($query_result, $query->to_array());
}

if(!empty($query_result)){
$result['result'] = true;
$result['player_name'] = $query['player_name'];
} else {
$result['result'] = false;
}

return $result;
}

}

?>

次に、Modelにデータを渡す、またModelからデータを受け取ってリクエストとして返すControllerを作成します

fuel/app/classes/controller/ を開き、右クリックしてNew Fileを選択、ファイル名は同じくaccount.phpにしましょう

内容は以下のようにします

<?php

class Controller_Account extends Controller
{

public function action_test()
{

$test_account = array(
'email' => 'test@example.com',
'password' => 'test',
);

$response = Model_Account::login($test_account);

if($response['result'] == true){
print_r('login success Player_Name :' . $response['player_name']);
} else {
print_r('login error!');
}
}
}

ControllerとModelの作成が終わったら動作チェックを行ってみましょう。次のURLにアクセスしましょう

https://Cloud9のワークスペースのURL/fuelphp/public/account/test

fuelPHPでは

publicのURL/コントローラー名/メソッド というルールでアクセスします

action_indexメソッドに処理を記述した場合は、URLからメソッドを省略できます

ワークスペースのURLは、Run Projectを実行したときに表示される

「Starting Apache httpd, serving https://hoehoge.c9users.io/.」

で確認できます

データベースとの接続が上手くいけば

「login success Player_Name :testdata」

と表示されます。

次は、テスト用ではなく、Unityからのログイン処理を行うメソッドを作りましょう

先ほど作成したaction_testメソッドの下に、次のメソッドを追加します

public function post_login()

{
$post = file_get_contents('php://input');
$account = json_decode(file_get_contents('php://input'), 'json');
$result = array();

//データチェック
if(!isset($account['email']) && !isset($account['password'])){
$result['error_msg'] = "ユーザー名もしくはパスワードが入力されていません";
$result['result'] = false;

print_r(Format::forge($result)->to_json());
return null;
}

$query = Model_Account::login($account);

if($query['result'] == true){
$result['player_name'] = $query['player_name'];
$result['result'] = true;
} else {
$result['error_msg'] = "ログインに失敗しました";
$result['result'] = false;
}

print_r(Format::forge($result)->to_json());
return null;
}

FuelPHPでは、action_またはget_から始まるメソッドはGETリクエストに対し、post_で始まるメソッドはPOSTリクエストに対するメソッドとして機能します

今回は、json形式でemailとpasswordをPOSTで投げる想定で作っています。

こちらも出来たら動作確認を行いましょう。GETではないので今度はブラウザでアクセスして確認はできません

Google Chromeを使用している場合は、DHCという機能拡張でPOSTの動作確認ができます

https://chrome.google.com/webstore/detail/dhc-restlet-client/aejoelaoggembcahagimdiliamlcdmfm

Cloud9のワークスペース/fuelphp/public/account/login に、次の値をPOSTしてみましょう

{"email":"test@example.com","password":"test"}

上手くいけば、次のようにJSONでレスポンスが帰ってきます

{"player_name":"testdata","result":true}

パラメータを変えてテストしてみましょう。存在しないアカウントでPOSTするときちんとエラーメッセージが帰ってきます

以上でログインサーバーの準備が完了しました


UnityでHTTP POSTしてみる

このステップではUnityから先ほど構築したログインサーバーへ、HTTP POSTでログインする処理を作成します

Unity5.3から追加されたJsonUtilityを使用するので、Unity 5.3以降を使用します

Unityで新規プロジェクトを作成し、以下のGameObjectを作成しましょう

 

・メールアドレスの入力フィールド(UI/Input Field)

・パスワードの入力(UI/Input Field)

・ログインボタン(UI/Button)

次に、プロジェクト内に「LoginController.cs」を作成し、以下の通りにします

using UnityEngine;

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;

public class LoginController : MonoBehaviour {

//POSTするJSONデータ構造
[System.Serializable]
public class Item
{
public string email;
public string password;
}

//サーバーから帰ってくるJSONデータ構造
[System.Serializable]
public class login_result
{
public string player_name;
public bool result;
public string error_msg;
}

public InputField mailInput;
public InputField passInput;

public string LoginServer;
public string PlayerName;

private login_result result;

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}

//ログインボタンを押したときの動作
public void Login() {
if (!string.IsNullOrEmpty(mailInput.text) && !string.IsNullOrEmpty(passInput.text))
{
//JSONデータを準備
string baseJson = "{ \"email\": \"" + mailInput.text + "\", \"password\": \"" + passInput.text + "\" }";

Item json = JsonUtility.FromJson<Item>(baseJson);
string serialisedJson = JsonUtility.ToJson(json);

//WWWをセットアップ
Dictionary<string, string> header = new Dictionary<string, string>();
header.Add("Content-Type", "application/json");

byte[] data = System.Text.Encoding.UTF8.GetBytes(serialisedJson);
WWW www = new WWW(LoginServer, data,header);

StartCoroutine(PostLogin(www));

} else {
Debug.Log("IDかパスワードが入力されていません");
}
}

//HTTP POSTを行うコルーチン
private IEnumerator PostLogin(WWW www) {

//POST結果が返ってくるまで待つ
yield return www;

//POSTに成功した場合
if (www.error == null)
{
result = JsonUtility.FromJson<login_result>(www.text);

if (result.result)
{
Debug.Log("ログインに成功!");
PlayerName = result.player_name;
} else {
Debug.Log("ログインに失敗");
}
} else {
Debug.Log("HTTPエラー:" + www.error);
}
}
}

新規に空のGameObjectを作成し、各プロパティを設定しましょう

Mail InputにはメールアドレスのInputFieldを

Pass InputにはパスワードのInputFieldを

Login ServerにはログインサーバーのURLを入れましょう

次に、作成したログインボタンを選択し、On Click()にクリックしたときのイベントを登録します

先ほどLoginController.csをアタッチしたオブジェクトを入れ、LoginControllerのLoginを入れます

ここまでエラーなく出来たら、一度シーンをPlayしてテストしてみましょう

DBに登録したIDとパスワードを正しく入力し、POSTが正常に行えていれば結果が返ってきます

接続できない場合は、LoginServerのURLが正しいか、Cloud9のProjectはRunになっているか、確認しましょう


Photon Unity Networkを導入し、接続する

このステップでは、Unityで使用できるゲームネットワーク「Photon」のセットアップを行います

https://www.photonengine.com/ja-JP/pun/

 

アカウントの取得、Appの登録の説明は今回は省きます

Photon RealtimeのダッシュボードでAppIDを作成します

https://www.photonengine.com/ja-JP/Dashboard/Realtime

フリープランでは20CCU(同時接続数)まで利用することができます。

 

ダッシュボードで表示されるアプリケーション IDをメモしておきましょう

(アプリケーション IDは省略されていますがクリックすると全て表示されます)

次に、UnityのAsset Storeから「Photon Unity Networking Free」を導入してImportします

Importが完了すると、PhotonのApp IDを入力するよう求められます

先ほど取得したApp IDを入力しましょう


PhotonNetworkのコントローラを作る

このステップでは、PhotonNetworkに接続するコントローラを作ります

プロジェクト内にPhotonNetworkManager.csを作成します

PhotonNetworkManager.csのコードを以下のようにしてください

using UnityEngine;

using System.Collections;

public class PhotonNetworkManager : Photon.PunBehaviour {

public string playerName;

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}

public void ConnectPhotonNetwork(string playerName) {
//Photonロビーに接続
Debug.Log("ConnectPhotonNetwork");
PhotonNetwork.ConnectUsingSettings("v1.0");
this.playerName = playerName;
}

public override void OnConnectedToMaster()
{
Debug.Log("PhotonNetwork ロビーに接続成功");

//プレイヤー名を設定
PhotonNetwork.playerName = playerName;

//ランダムなルームに入室を試みる
PhotonNetwork.JoinRandomRoom();
}

//ルームへの入室に失敗したときに呼ばれる
public void OnPhotonRandomJoinFailed()
{
Debug.Log("ルームへの入室に失敗、ルームを作成します");
PhotonNetwork.CreateRoom(null);
}

public override void OnJoinedRoom()
{
Debug.Log("ルームへの入室に成功");
Debug.Log("プレイヤー名:" + PhotonNetwork.playerName);
}

}

普通Unity上で動かすクラスを作成すると、MonoBehaviorを継承します

しかし、Photonを使用するクラスの場合はPhoton.PunBehaviorやPhoton.MonoBehaviorを継承する点に気をつけてください

 

各メソッドの説明です

ConnectPhotonNetwork

public void ConnectPhotonNetwork(string playerName) {

//Photonロビーに接続
Debug.Log("ConnectPhotonNetwork");
PhotonNetwork.ConnectUsingSettings("v1.0");
this.playerName = playerName;
}

このメソッドを外部のスクリプトから叩いてPhotonNetworkに接続させています

PhotonNetwork.ConnectUsingSettings("v1.0");

この中で大事なのはこの1行で、PhotonNetwork.ConnectUsingSettings() が叩かれるとPhotonNetworkに自動で接続されます

引数に渡している文字列は、渡す値が違うプログラム同士ではマッチングしないようになっています

主に、古いバージョンのプログラムと新しいバージョンのプログラムが通信してしまわないように分けるために使います

たとえば1.0と1.1を渡したプログラム同士を放り込んでもマッチングしません。

OnConnectedToMaster

public override void OnConnectedToMaster()

{
Debug.Log("PhotonNetwork ロビーに接続成功");

//プレイヤー名を設定
PhotonNetwork.playerName = playerName;

//ランダムなルームに入室を試みる
PhotonNetwork.JoinRandomRoom();
}

PhotonNetwork.ConnectUsingSettings() によってPhotonNetworkに接続に成功すると自動で呼ばれます

overrideしないといけません。気をつけましょう

この時のクライアントの状態は、PhotonNetwork内のロビーに居る状態になります

ロビーは一番大きいグループの単位です。

それよりさらに分割されたルームがあり、各クライアントはルームに入室して、ルーム単位でゲームを行います。

FPSゲームのロビーとルームのような関係を想像していただけたら良いかと。

 

ルームに入室するためには、次のメソッドを叩きます

PhotonNetwork.JoinRandomRoom();

JoinRandomRoom()は、ロビー内に存在するランダムなルームへ入室を試みます

ルームが存在しない場合は入室に失敗するので、ハンドリング処理が必要です。

ランダムではなく指定したルームに入室する方法もありますが、今回は省略

 

OnPhotonRandomJoinFailed

//ルームへの入室に失敗したときに呼ばれる

public void OnPhotonRandomJoinFailed()
{
Debug.Log("ルームへの入室に失敗、ルームを作成します");
PhotonNetwork.CreateRoom(null);
}

ルームへの入室に失敗したときに呼ばれるメソッドです

先ほどのJoinRandomRoom()で入室を試みたが部屋が無かった等の場合に呼ばれるメソッドもここです

PhotonNetwork.CreateRoom()を叩くと、新しいルームを作成して入室します

引数としてルーム名を渡す事ができます。

 

OnJoinedRoom

public override void OnJoinedRoom()

{
Debug.Log("ルームへの入室に成功");
Debug.Log("プレイヤー名:" + PhotonNetwork.playerName);
}

OnJoinedRoomは、ルームへの入室に成功したときに自動で呼ばれます

今回は、Debug.Logで接続成功のメッセージと、PhotonNetwork内でのプレイヤーネームを出力してみました。

 

これでPhotonコントローラは完成です

空のGameObjectを作ってスクリプトをアタッチしましょう


ログインコントローラとPhotonを繋ぐ

最後のステップです。先ほど作成したPhotonNetworkManagerとLoginControllerを繋げて、POSTでサーバーにログインしてユーザー名を受け取り、そのユーザー名を使ってPhotonNetworkに接続できるようにします

LoginController.csを開きます

Startメソッドの前、変数を定義しているところに、次の1行を足します

public PhotonNetworkManager photonNetworkManager;

次に、IEnumerator PostLoginのところ、ログインに成功した時の処理のところに、次の1行を足します

photonNetworkManager.ConnectPhotonNetwork(this.PlayerName);

これで、ログインに成功した時に、PhotonNetworkManagerを叩いてPhotonNetworkに接続できるようになりました

 

最終的なLoginController.csのコードは次の通りになります

using UnityEngine;

using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;

public class LoginController : Photon.MonoBehaviour {

//POSTするJSONデータ構造
[System.Serializable]
public class Item
{
public string email;
public string password;
}

//サーバーから帰ってくるJSONデータ構造
[System.Serializable]
public class login_result
{
public string player_name;
public bool result;
public string error_msg;
}

public InputField mailInput;
public InputField passInput;

public string LoginServer;
public string PlayerName;

private login_result result;

public PhotonNetworkManager photonNetworkManager;

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}

//ログインボタンを押したときの動作
public void Login() {
if (!string.IsNullOrEmpty(mailInput.text) && !string.IsNullOrEmpty(passInput.text))
{
//JSONデータを準備
string baseJson = "{ \"email\": \"" + mailInput.text + "\", \"password\": \"" + passInput.text + "\" }";

Item json = JsonUtility.FromJson<Item>(baseJson);
string serialisedJson = JsonUtility.ToJson(json);

//WWWをセットアップ
Dictionary<string, string> header = new Dictionary<string, string>();
header.Add("Content-Type", "application/json");

byte[] data = System.Text.Encoding.UTF8.GetBytes(serialisedJson);
WWW www = new WWW(LoginServer, data,header);

StartCoroutine(PostLogin(www));

} else {
Debug.Log("IDかパスワードが入力されていません");
}
}

//HTTP POSTを行うコルーチン
private IEnumerator PostLogin(WWW www) {

//POST結果が返ってくるまで待つ
yield return www;

//POSTに成功した場合
if (www.error == null)
{
result = JsonUtility.FromJson<login_result>(www.text);

if (result.result)
{
Debug.Log("ログインに成功!");
PlayerName = result.player_name;

//PhotonNetworkに接続
photonNetworkManager.ConnectPhotonNetwork(this.PlayerName);

} else {
Debug.Log("ログインに失敗");
}
} else {
Debug.Log("HTTPエラー:" + www.error);
}
}

}

保存し終えたら、LoginControllerをアタッチしているGameObjectを選択し、プロパティのPhotonNetworkManagerのところにPhotonNetworkManagerをアタッチしたオブジェクトを指定します

ここまでの作業が終わったら、Sceneをプレイして、ログインしてみましょう

Photonへの接続に成功すると、Consoleにプレイヤー名のログが出力されます


あとがき

今回はサーバーにログインしてプレイヤー名を取得し、ログインするまでの処理を作りました

本当はユニティちゃんを配置して同期させる処理まで解説したかったのですが、時間不足で断念しました

以下に、自分が作ったサーバーとクライアントファイルを公開します

クライアント側はキャラクター同期まで作る予定でしたが、間に合わなかったので現状のものを公開します。年内を目標にバージョンアップしたクライアントを公開します

サーバー

https://github.com/Erish/advent2016-server

クライアント

https://github.com/Erish/advent2016-client