02. Unityとの連携

プロジェクト仕組みの理解

事前の準備

Unity + PHP + MySQLプロジェクトを構築する前に、いくつか学習しておくべき項目がある。

Awaitable とは

非同期処理について

UnityからWebサーバーへの依頼:UnityWebRequest

これまではブラウザでサーバーに接続し、PHPスクリプトを実行してきたが、これからはUnityからリクエストを送信する。そのため、UnityWebRequest クラスを使用して、指定したURLに接続する機能がある。

GET リクエスト

// GETでリクエストを送信
// url:接続したアドレス
// request:待機できるオブジェクト
var request = UnityWebRequest.Get(url); 

使用例:

// ユーザーIDでログインしているかどうかを確認
var request = UnityWebRequest.Get("https://api.my-game.co.jp/api/check_login.php?user_id=45b99aef"); 

ここで:

https://api.my-game.co.jp/api APIの「根」(Root URL)
check_login.php サーバーの中で使いたい機能(ログインをチェック)
?user_id=45b99aef URLパラメータ(ユーザーのID)

もちろん、上記のAPIはあくまでの例であり、ゲームにより変わるので、具体的なAPIはサーバーエンジニアに相談してください。

POST リクエスト

// POSTでリクエストを送信
// url:接続したアドレス
// data:POST データ (WWWForm)
// request:待機できるオブジェクト
var request = UnityWebRequest.Post(url, data); 

使用例:

// POSTでデータを送るので、フォームを用意する
// サーバーが期待しているデータに合わせる
var data = new WWWForm();
data.AddField("user_id", "45b99aef");

// ログインAPIのURLへリクエストを送信
var request = UnityWebRequest.Post("https://api.my-game.co.jp/api/check_login.php", data);

これ、 GET と同じリクエストであるが、POST でデータを送信した例である。

返事を待つ

ネットワークの状況やサーバーの混雑により、返事がすぐ来ない場合もあるので、リクエストが終わるまで待機する必要がある。Awaitable を使用し、返事を待つことができる:

public async Awaitable CheckLogin(string user)
{
    // POSTでデータを送るので、フォームを用意する
    // サーバーが期待しているデータに合わせる
    var data = new WWWForm();
    data.AddField("user_id", "45b99aef");
    
    // ログインAPIのURLへリクエストを送信
    var request = UnityWebRequest.Post("https://api.my-game.co.jp/api/check_login.php", data);
    
    // 返事を待機する
    await request.SendWebRequest();

    // 返事を処理
}

返事を処理

最後、サーバーの返事を確認し、適切に処理する。データが2つの求め方がある:「テキスト(文字列)」、または「バイナリデータ」

public async Awaitable CheckLogin(string user)
{
    // POSTでデータを送るので、フォームを用意する
    // サーバーが期待しているデータに合わせる
    var data = new WWWForm();
    data.AddField("user_id", "45b99aef");
    
    // ログインAPIのURLへリクエストを送信
    var request = UnityWebRequest.Post("https://api.my-game.co.jp/api/check_login.php", data);
    
    // 返事を待機する
    await request.SendWebRequest();

    // 返事を処理。以下のどちらか使う
    // 文字列
    string textResponse = request.downloadHandler.text

    // 生のバイナリデータ(画像など)
    bytes[] byteResponse = request.downloadHandler.data;

    // なにかの処理をする
}

UnityでJSONを使用

これから Unity と PHP のやりとりは JSON で行うため、JSONデータを管理しやすくツールを使用しましょう。Unity は JSONに対応していないが、パッケージとして追加できる。Package Manager を開き、「Install Package By Name」 を選択し、次のパッケージをインストールしてください:

com.unity.nuget.newtonsoft-json

image.png

image.png

JSONのデータ管理

パッケージをインストールすると、2つの新しいクラスが利用可能にななる:JObjectJArray

JObject はディクショナリー(連想配列)として機能する。次のように、キー名を使用してJSONデータの各値にアクセスでる。

private void Start()
{
    JObject json = new JObject();
    json["player_name"] = "タロウ"; // プレーヤー名
    json["exp_points"]  = 1024;    // 経験値
    
    Debug.Log(json.ToString());   // 文字列にする
}

その結果、コンソールに次のメッセージが表示されます:

image.png

JArray は、同じ型の値からなる配列である。リストが必要な場合は、JObject の一部として使用できる。例えば、

private void Start()
{
    JObject json = new JObject();
    json["player_name"] = "タロウ"; // プレーヤー名
    json["exp_points"] = 1024;     // 経験値
    
    JArray items = new JArray();  // 配列
    items.Add(100);
    items.Add(200);
    items.Add(300);
    
    json["items"] = items;        // JSONに追加
    
    Debug.Log(json.ToString());   // 文字列にする
}

実行すると、以下のようになる:

image.png

文字列から JObject へ変換

前述のように、JObject.ToString() を使用すると、JObject を文字列に変換できる。その逆も可能です。JObject.Parse() を使用すると、文字列を JObject に変換できる。例えば:

private void Start()
{
    // JSONの文字列
    string text = "{ \"player_name\" : \"P1\", \"exp_points\" : 555, \"items\" : [40, 30, 20] }";
    
    // JObjectへ変換
    JObject json = JObject.Parse(text);
    
    Debug.Log("プレーヤー名:" + (string)json["player_name"]);
    Debug.Log("経験値:" + (int)json["exp_points"]);

    // JArrayをアクセス
    JArray items = (JArray)json["items"];
    Debug.Log("1個目のアイテム:" + (int)items[0]);
}

次のような出力が表示される:

image.png

ポップアップ表示の確認

本プロジェクトはすでにいくつかの便利な機能を実装されている。好きなメッセージを表示し、ボタンのイベントにも登録できる

ポップアップ表示、イベント登録

// ただのポップアップを表示する(決定ボタンのみ)
PopupController.Show("タイトル", "詳細メッセージ");

// 決定ボタンとキャンセルボタンを表示し、イベントに登録
var popup = PopupController.Show("タイトル", "メッセージ", PopupButtons.Ok | PopupButtons.Cancel);
popup.ButtonClicked += OnPopupButton;

そして、他のメソッドでイベントを処理する:

// ポップアップのボタンを処理
private void OnPopupButton(PopupButtons button)
{
    switch (button)
    {
        // 決定を押した
        case PopupButtons.Ok:
            break;

        // キャンセルを押した
        case PopupButtons.Cancel:
            break;        
    }
}

Unityで以下のように見えます:

image.png

ログイン

PHPでAPIを作成(固定結果)

まず、Unityから接続できるようにしたいので、サーバーでログイン API の機能を追加しましょう。データベースがまだ作成していないため、ひとまず固定の結果を返しましょう。

C:\xmpp\htdocs\api フォルダを作成しましょう。この中に本ゲームのオンライン機能を実装していくフォルダにする。その中にlogin.php というからのテキストファイルを作成してください。単純に、成功したら、ユーザーIDを返し、失敗したら -1 を返すようにしましょう:

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// ユーザーIDを返す
	$json['user_id'] = "1";
	
	// 返す
	print(json_encode($json));
?>

まだPOSTデータが要らないので、ブラウザーで確認できる:

image.png

Unityを使い、APIとの接続

仮で作ったログインAPIを試すため、UnityでAPI管理クラスを作ってみましょう。まず、単純に:

// サーバーに依頼するメソッドの一覧
public static class APIRequest 
{
    // サーバーのAPIのURL
    private const string ServerAPI = "http://localhost/api";
    
    // ログイン
    public static async Awaitable LoginAsync()
    {
        // ログインAPIのURLへリクエストを送信
        const string url = ServerAPI + "/login.php";
        var webRequest = UnityWebRequest.Get(url);
        
        // 返事を待機する
        await webRequest.SendWebRequest();

        // 返事の文字列を取得
        var webResponse = webRequest.downloadHandler.text;
        
        // デバッグ用
        Debug.Log(webResponse); 
    }
}

試すには、ログイン画面で、「ログイン」ボタンを押したら、上記のメソッドを使用しましょう:

// ログイン処理する
public class LoginController : MonoBehaviour
{
    // ログインボタン
    [SerializeField] private Button sendButton;

    // ログインボタンを押したとき
    private async void OnSendClicked()
    {
        // リクエストを送信し、待機する
        await APIRequest.LoginAsync();
    }

    // 有効になった時
    private void OnEnable()
    {
        sendButton.onClick.AddListener(OnSendClicked);
    }

    // 無効になった時
    private void OnDisable()
    {
        sendButton.onClick.RemoveAllListeners();
    }
}

Unityでログインコントローラーを追加し、ボタンを設定する:

image.png

実行すると:

image.png

サーバーの返事を確認できた!

ポップアップを表示

次、Webサーバーから戻るJSON文字列ををJObjectへ変換し、結果により、ポップアップを表示しましょう。まず、APIRequest を編集し、成功したかどうかを返しましょう。LoginAsyncJObject を返すので、定義が変わる:

// 前:
public static async Awaitable LoginAsync()

// 後:
public static async Awaitable<JObject> LoginAsync()

すべて組みあわえると:

// サーバーに依頼するメソッドの一覧
public static class APIRequest
{
    // サーバーのAPIのURL
    private const string ServerAPI = "http://localhost/api";

    // ログイン
    // 戻り値:ログインの結果(JObject)
    public static async Awaitable<JObject> LoginAsync()
    {
        // ログインAPIのURLへリクエストを送信
        const string url = ServerAPI + "/login.php";
        var webRequest = UnityWebRequest.Get(url);

        // 返事を待機する
        await webRequest.SendWebRequest();

        // 返事の文字列を取得
        var webResponse = webRequest.downloadHandler.text;

        // デバッグ用
        Debug.Log(webResponse);
        
        // JSONからJObjectへ変換
        JObject response = JObject.Parse(webResponse);

        // 返す
        return response;
    }
}

そして、LoginController で結果を確認し、ポップアップを表示しましょう:

// ログインボタンを押したとき
private async void OnSendClicked()
{
    // リクエストを送信し、待機する
    LoginResponse result = await APIRequest.LoginAsync();

    // 空の文字列 -> 失敗
    string userID = (string)result["user_id"];
    if (string.IsNullOrEmpty(userID))
    {
        PopupController.Show("エラー", "ログインできませんでした。");
    }
    else
    {
        PopupController.Show("成功!", "ログインできました!");
    }
}

「ログイン」ボタンを押すと

image.png

login.php で true / false を変えて、Unityで確かめてください。

PHPでAPIを作成(ifで分岐)

現在、login.php で成功するかどうかは固定しているので、if で分岐しましょう。ユーザー名とパスワードは "test" だったら、ログインは成功し、そうではない場合は失敗しましょう:

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 失敗を想定する
	$json['user_id'] = "";

    // ユーザー名とパスワードを確認
    $user = $_POST['user'];
    $pass = $_POST['password'];
    if ($user == "test" && $pass == "test")
    {
        $json['user_id'] = "1";
    }
	
	// 返す
	print(json_encode($json));
?>

このスクリプトは POST でユーザー名とパスワードを期待しているので、Unity から渡しましょう。

// ログイン
// user:ユーザー名
// password:パスワード
// 戻り値:ログインの結果(JObject)
public static async Awaitable<LoginResponse> LoginAsync(string user, string password)
{
    // POSTでデータを送るので、変数を用意する
    // サーバーが期待しているデータに合わせる
    var postData = new WWWForm();
    postData.AddField("user", user);
    postData.AddField("password", password);
    
    // ログインAPIのURLへリクエストを送信
    const string url = ServerAPI + "/login.php";
    var webRequest = UnityWebRequest.Post(url, postData);

    // 返事を待機する
    await webRequest.SendWebRequest();

    // 返事の文字列を取得
    var webResponse = webRequest.downloadHandler.text;

    // デバッグ用
    Debug.Log(webResponse);
    
    // JSONからJObjectへ変換
    JObject response = JObject.Parse(webResponse);

    // 返す
    return response;
}

最後に、LoginController で画面の入力との連携すれば、出来上がり!

// ログインを処理する
public class LoginController : MonoBehaviour
{
    // ユーザー名
    [SerializeField] private TMP_InputField nameInput;

    // パスワード
    [SerializeField] private TMP_InputField passwordInput;

    // ログインボタン
    [SerializeField] private Button sendButton;

    // ログインボタンを押したとき
    private async void OnSendClicked()
    {
        // リクエストを送信し、待機する
        var name = nameInput.text;
        var pass = passwordInput.text;
        JObject result = await APIRequest.LoginAsync(name, pass);

        // 空の文字列 -> 失敗
        string userID = (string)result["user_id"];
        if (string.IsNullOrEmpty(userID))
        {
            PopupController.Show("エラー", "ログインできませんでした。");
        }
        else
        {
            PopupController.Show("成功!", "ログインできました!");
        }
    }

    // (省略)
  }

image.png

データベースを作成

まず、カードゲーム専用の「card_game」データベースを作成しましょう:

image.png

次に、ユーザーのテーブルを作成しましょう。必要なのは、ユーザーID番号、ユーザー名とパスワードなので、3のカラムで設計:

image.png

image.png

そして、いくつかのユーザーを作成してみてください

image.png

注意

平文(人間が読める)パスワードを保存するのは大変危険であるので、必ずソルト+ハッシュしてから保存してください。今回の練習のために平文で保存する。

 

パスワードセキュリティに関して、ここを読んでください。

PHPでAPIを作成(DBを使用)

login.php を編集し、データベースのサポートを追加しましょう。

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 接続 
	$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);

	// 失敗を想定する
	$json['user_id'] = "";

    // ユーザー名とパスワードを確認
    $user = htmlspecialchars($_POST['user']);
    $pass = htmlspecialchars($_POST['password']);

	// データベース検索
	$sql = sprintf("SELECT * FROM users WHERE user='%s' AND password='%s'", $user, $pass);
	$result = $dbh->query($sql);

	// 必ず1個を返さなきゃ
	if ($result->rowCount() == 1)
	{
		# ユーザーIDを返す
		$record = $result->fetch();
        $json['user_id'] = strval($record['id']); // 文字列として返す
	}
	
	// 返す
	print(json_encode($json));
?>

Unityを使い、確認してみてください。

ユーザーIDを保存、シーン遷移

ログインを成功したら、ユーザーIDを再利用できるようにし、TopMenu シーンに遷移しましょう。なお、IDはずっと変わらないので静的変数(static)として管理しても大きな問題がない。LoginController の先頭に:

// ログインを処理する
public class LoginController : MonoBehaviour
{
    // ログインしているユーザーID
    public static string UserID { get; private set; }

    // ...
}

そして、ボタンの処理では…

// ログインボタンを押したとき
private async void OnSendClicked()
{
    // リクエストを送信し、待機する
    var name = nameInput.text;
    var pass = passwordInput.text;
    JObject result = await APIRequest.LoginAsync(name, pass);

    // 空の文字列 -> 失敗
    UserID = (string)result["user_id"];
    if (string.IsNullOrEmpty(UserID))
    {
        PopupController.Show("エラー", "ログインできませんでした。");
    }
    else
    {
        //PopupController.Show("成功!", "ログインできました!");
        SceneManager.LoadScene("TopMenu");
    }
}

持っているカードを確認

カードの表示方法を理解する

Unityで「CardTest」シーンを開く。以下のような画面が表示される。

image.png

このシーンを使って、カード表示機能をテストできる。使用する値は次の通りである。

これらはすべて CardDisplay クラスによって処理される。特に、以下に示す SetData メソッドで扱われる。

// データを設定し、カードを更新
public void SetData(JObject data)
{
    // スプライトを読み込む
    cardRank.sprite = CardResources.GetRankSprite((int)data["rank"]);
    monsterPicture.sprite = CardResources.GetMonsterSprite((int)data["picture_id"]);
    
    // テキストを更新
    monsterName.text = (string)data["monster_name"];
    atkValue.text = (string)data["atk_value"];
    defValue.text = (string)data["def_value"];
    hpValue.text = (string)data["hp_value"];
}

データベースを作成する

全カードの一覧

この知識を使って、ゲーム内の全カードを保存するための新しいテーブルをデータベースに作成しよう。phpMyAdmin をもう一度開き、以下のように必要なカラムを持つ「cards」という名前のテーブルを作成しよう。

image.png

そして、データベースにいくつかカードを追加しよう。

image.png

以下のSQL文を使ってカードを挿入してもいいし、自分で作ってもよい。

INSERT INTO `cards` (`id`, `rank`, `picture_id`, `monster_name`, `attack`, `defense`, `hit_points`) VALUES
(1, 0, 0, '弱虫スケルトン', 3, 1, 10),
(2, 0, 1, '黒オニ', 4, 1, 8),
(3, 0, 0, 'ホネボネ', 3, 2, 15),
(4, 0, 1, '悪魔赤ちゃん', 5, 2, 10),
(5, 1, 4, 'ファイヤー猫', 8, 2, 12),
(6, 1, 3, 'スカルゴースト', 6, 5, 10),
(7, 1, 8, 'アルミ戦士', 4, 8, 10),
(8, 2, 5, 'カエルる', 10, 5, 20),
(9, 2, 6, 'メデゥーサ', 8, 8, 15),
(10, 3, 7, '木マン', 5, 15, 22),
(11, 3, 8, '闇ナイト', 12, 15, 12),
(12, 4, 9, 'ギャラクシー', 20, 12, 35);

プレイヤーが所有するカードの一覧

プレイヤーごとに持っているカードが異なるため、「users」テーブルと「cards」テーブルの間に関係を作る必要がある。これは、この2つを単純な形でつなぐ「user_cards」という新しいテーブルを作成することで実現できる。

image.png

見ての通り、このテーブルにはレコードID、ユーザーID、カードID、そのカードの所持枚数という4つのカラムしかない。テストユーザーとカードはすでに用意してあるので、それぞれのプレイヤーにカードを持たせよう。手動で追加してもいいし、以下のSQL文を使って手早く初期カードを用意してもよい。

image.png

INSERT INTO `user_cards` (`id`, `user_id`, `card_id`, `count`) VALUES
(1, 1, 1, 2), (2, 1, 2, 1), (3, 1, 5, 1),(4, 1, 8, 2),
(5, 1, 6, 1), (6, 2, 3, 5), (7, 2, 6, 3),(8, 2, 12, 1);

APIを実装する

テーブルとテストデータが揃った。次は、プレイヤーが所有するカードを返すPHPスクリプトを作る。Unity側のコードは詳細なカード情報を必要とするため、ランク、イラスト番号、モンスター名、各種数値などすべてを送る必要がある。ただし、データを重複させたくないので、カードの枚数も一緒に送ることにする。最終的なJSONは次のような形になる

{
  "card_list" : [
    {
      "card" : {
        "id" : 1,
        "rank" : 0, 
        "picture_id" : 0, 
        "monster_name" : "弱虫スケルトン", 
        "atk_value" : 3, 
        "def_value" : 1, 
        "hp_value" : 10
      },
      "count" : 2
    },
    ...
  ]
}

PHP スクリプト

まず、データベースにクエリを送る。パラメータとしてユーザーIDを受け取り、そのユーザーが持っているすべてのカードを検索する。カードを持っていない場合や、ユーザーが存在しない場合は、空の配列を返す。

テスト用として、ユーザーIDはGETパラメータで渡し、最終版ではPOSTに変更する。get_player_cards.php ファイルを作成し、以下のように実装しよう。

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 接続 
	$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);
	
	// ユーザーIDを取得
	$user_id = $_GET['user_id'];
	
	// そのユーザーを持っているカードを検索
	$sql = "SELECT * FROM user_cards WHERE user_id = " . $user_id;
	$result = $dbh->query($sql);
	
	// 返すデータを準備する
	$json["card_list"] = [];
	
	while ($record = $result->fetch())
	{
		// カード1枚に関するデータ
		$card_data['card']['id'] = $record['card_id'];
		$card_data['count']      = $record['count'];
		
		// 結果に追加
		array_push($json["card_list"], $card_data);
	}

	print(json_encode($json));
?>

ここでは、ユーザーIDを受け取り、そのユーザーが持っているすべてのカードを検索して返している。しかし、以下のテストからわかるように、

image.png

これはカードの詳細情報ではなく、カードのIDを返しているだけである。Unityで必要なデータを取得するには、2つのテーブルを結合して同時に検索する必要がある。これはJOIN操作と呼ばれる。まずは phpMyAdmin でテストしてみよう。以下のSQLクエリから始める。

SELECT * FROM user_cards WHERE user_id = 1;

image.png

これはPHPの結果と同じである。次に、cardsテーブルを結合し、card_idがcardsテーブルの同じidと一致するという条件を追加しよう。つまり、

SELECT * FROM user_cards, cards WHERE user_id = 1 AND user_cards.card_id = cards.id;

image.png

完璧である!これで、user_id = 1 が持つすべてのカードについて、詳細な情報が得られた。改良したSQLクエリを使うようにPHPスクリプトを修正し、必要な結果を返すようにしよう。

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 接続 
	$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);
	
	// ユーザーIDを取得
	$user_id = $_GET['user_id'];
	
	// そのユーザーを持っているカードを検索
	$sql = sprintf("SELECT * FROM user_cards, cards WHERE user_id = '%s' AND user_cards.card_id = cards.id", $user_id);
	$result = $dbh->query($sql);
	
	// 返すデータを準備する
	$json["card_list"] = [];
	
	while ($record = $result->fetch())
	{
		// カード1枚に関するデータ(UnityのJObjectに合わせる)
		$card_data['card']['id']           = $record['id'];
		$card_data['card']['rank']         = $record['rank'];
		$card_data['card']['picture_id']   = $record['picture_id'];
		$card_data['card']['monster_name'] = $record['monster_name'];
		$card_data['card']['atk_value']    = $record['attack'];
		$card_data['card']['def_value']    = $record['defense'];
		$card_data['card']['hp_value']     = $record['hit_points'];
		
		$card_data['count'] = $record['count'];
		
		// 結果に追加
		array_push($json["card_list"], $card_data);
	}

	print(json_encode($json));
?>

もう一度テストを実行すると、以下のようになる。

image.png

これは、求めていたJSON形式である。あとはスクリプト内の $_GET を $_POST に変更すれば、次のステップに進む準備は完了である。

Unityを使い、APIとの接続

APIRequest クラスを更新し、get_player_cards.php にアクセスして、使える形に変換する新しいメソッドを追加しよう。

// プレーヤーが所有しているカードを求める
// user_id:ユーザーID
public static async Awaitable<JArray> GetPlayerCardsAsync(string userID)
{
    // POSTでデータを準備
    var postData = new WWWForm();
    postData.AddField("user_id", userID);

    // APIのURLへリクエストを送信
    const string url = ServerAPI + "/get_player_cards.php";
    var webRequest = UnityWebRequest.Post(url, postData);

    // 返事を待機する
    await webRequest.SendWebRequest();

    // 返事の文字列を取得
    var webResponse = webRequest.downloadHandler.text;

    // JSONからPlayerCardの配列へ変換
    JObject response = JObject.Parse(webResponse);

    // 返す
    return (JArray)response["card_list"];
}

ログイン中のユーザーで試してみる

ユーザーIDは LoginController.UserID に保存してあるので、サーバーに対してプレイヤーの全カードを取得するようリクエストできる。まずは CardListController を見てみよう。

// カード一覧を管理する
public class CardListController : MonoBehaviour
{
    // 画面を表示する
    public void Show()
    {
        screenSlide.Show();
        _ = LoadCardsAsync();
    }

    // 選択されたカードを削除
    private void DeleteSelectedCards()
    {
        // 未実装
    }
    
    // カードを読み込み、画面に追加し、表示する
    private async Awaitable LoadCardsAsync()
    {
        // すべてのカード一を削除する
        foreach (Transform child in cardsList.transform)
            Destroy(child.gameObject);
        
        // オンライン機能を実装する前、いくつかのカード手動に追加
        await Awaitable.NextFrameAsync();
        var cards = FakeCards();

        // 1枚ずつを追加
        foreach (var item in cards)
        {
            var count = (int)item["count"]; 
            for (var i = 0; i < count; i++)
            {
                var card = (JObject)item["card"];
                var copy = Instantiate(cardPrefab, cardsList.transform);
                copy.SetData(card);
                copy.SetSelectable(true);
            }
        }
    }

    // オフラインテスト用のカード
    private JArray FakeCards()
    {
        // (省略)
    }
}

これは非常にシンプルなスクリプトである。画面に表示されているカードをすべて消してから、テスト用のカードを読み込んでいる。「削除」ボタンは現時点では何もせず、「戻る」ボタンはトップメニューに戻る。

APIを使うには、オフラインのテストカードを置き換えて、サーバーにカードをリクエストするだけでよい。APIRequest メソッドはすでに用意してあるので、ここで使ってみよう。

// カードを読み込み、画面に追加し、表示する
private async Awaitable LoadCardsAsync()
{
    // すべてのカード一を削除する
    foreach (Transform child in cardsList.transform)
        Destroy(child.gameObject);
    
    // サーバーにカードの一覧を要求
    var cards = await APIRequest.GetPlayerCardsAsync(LoginController.UserID);

    // 1枚ずつを追加
    foreach (var item in cards)
    {
        var count = (int)item["count"]; 
        for (var i = 0; i < count; i++)
        {
            var card = (JObject)item["card"];
            var copy = Instantiate(cardPrefab, cardsList.transform);
            copy.SetData(card);
            copy.SetSelectable(true);
        }
    }
}

もう一度ログインして、プレイヤーのカードを読み込んでみよう。

image.png

これは、データベースに保存しておいたカードである!

image.png

カード削除

カードを削除するには、PHPで新しいAPIを作る必要がある。このAPIは以下のことを行う。

SQLを使ってカードを削除する

まず、SQLでカウントを減らす方法を見てみよう。例えば、user_id = 1、card_id = 1のカードを2枚減らしたいとする。このユーザーはcard_id = 1のカードを2枚持っているので、これで全部なくなることになる。MySQLに「count」という項目から2を引くよう指示すれば、カウントを減らせる。以下のようにする

UPDATE user_cards SET count = count - 2 WHERE user_id = 1 and card_id = 1;

これは、user_id = 1かつcard_id = 1の行だけ、「count」という列から2を引くようMySQLに指示している。実行すると、以下のようになる。

image.png

カウントが0になったことが確認できる。次にやるべきことは、countが0以下になったレコードを全部削除することだけである。これはDELETEを使えば簡単にできる。

DELETE FROM user_cards WHERE count <= 0;

この2つの命令をPHPで使えば、プレーヤーからカードを削除できる。それでは、APIを設計していこう。

PHP スクリプト

始める前に、データベースを元に戻しておこう。phpMyAdmin で以下のSQLコマンドを実行してください。

REPLACE INTO `user_cards` (`id`, `user_id`, `card_id`, `count`) VALUES
(1, 1, 1, 2), (2, 1, 2, 1), (3, 1, 5, 1),(4, 1, 8, 2),
(5, 1, 6, 1), (6, 2, 3, 5), (7, 2, 6, 3),(8, 2, 12, 1);

では、スクリプトを設計していこう。delete_player_cards.php という新しいファイルを作ってください。このAPIでは、クライアント側からユーザーIDと、カードIDごとの削除枚数を送ってもらう。削除するカードの枚数は毎回変わるので、削除したいカードを以下のようなJSONオブジェクトとして送ってもらうことにする。

{
	"user_id" : "1",
	"cards" : [
		{ 
			"id" : "1",
			"count" : 2
		},
		{ 
			"id" : "8",
			"count" : 1
		},		
	]
}

この例では、ユーザーID「1」のカードID「1」を2枚、カードID「8」を1枚削除したいということになる。まだデータを送る仕組みがないので、最初はスクリプトの中に直接書き込んでおこう。Unityからデータを送る部分は、後でPOSTを使って作る。

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 接続 
	$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);
	
	// テスト中にデータを固定
	$data = '{
		"user_id" : "1",
		"cards" : [
			{ "id" : "1", "count" : 2 },
			{ "id" : "8", "count" : 1 }
		]
	}';
	
	// JSONに変換
	$json = json_decode($data, true);
	
	// ユーザーIDと削除したいデータの分ける
	$user_id   = $json['user_id'];
	$card_list = $json['cards'];
?>

次に、forループを使って、配列の中の削除リクエストを1つずつ処理しながらSQL文を組み立てていく。

// SQL文を構築
foreach ($card_list as $card)
{		
    $sql = "UPDATE user_cards SET count = count - " . $card['count']
         . " WHERE user_id = " . $user_id
         . " AND card_id = " . $card['id'];
         
    // 確認しましょう
    print($sql . "\n");
}	

実行すると、以下のような結果になるはずである。

image.png

これはまさに、カードを削除するために必要なものである!SQL文を実行して、データベースからカードを削除するところまで作って、スクリプトを完成させよう。

// SQL文を構築
foreach ($card_list as $card)
{		
    $sql = "UPDATE user_cards SET count = count - " . $card['count']
         . " WHERE user_id = " . $user_id
         . " AND card_id = " . $card['id'];
         
    $dbh->exec($sql);
}		

// 最後に、countが0以下になったカードを削除
$dbh->exec("DELETE FROM user_cards WHERE count <= 0");

// 空のJSONを返す
print("{}")	

コードを実行する前の状態を確認してみよう。

image.png

そして、実行した後はこうなる。

image.png

あとは、テスト用のJSONを実際のPOST入力に置き換えるだけである。以下が最終的なスクリプトになる。

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 接続 
	$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);
	
	// POSTデータを取得
	$data = $_POST['data'];
	
	// JSONに変換
	$json = json_decode($data, true);
	
	// ユーザーIDと削除したいデータの分ける
	$user_id   = $json['user_id'];
	$card_list = $json['cards'];
	
	// SQL文を構築
	foreach ($card_list as $card)
	{		
		$sql = "UPDATE user_cards SET count = count - " . $card['count']
  		     . " WHERE user_id = " . $user_id
			 . " AND card_id = " . $card['id'];
			 
		$dbh->exec($sql);
	}		
	
	// 最後に、countが0以下になったカードを削除
	$dbh->exec("DELETE FROM user_cards WHERE count <= 0");
	
	// 空のJSONを返す
	print("{}")	
?>

Unityを使い、APIとの接続

次のステップは、Unity側を実装することである。PHPスクリプトが期待しているのと同じデータ構造を作る必要がある。APIRequest には、ユーザーIDと、削除したいカードIDのリストが必要になる。

// プレーヤーのカードを削除する
// userID:ユーザーID
// cards: 削除したい「カードID」→「数」のディクショナリー
public static async Awaitable DeleteCardsAsync(string userID, Dictionary<string, int> cards)
{
    // JSONを構築
    JArray list = new JArray();
    foreach (var (id, count) in cards)
    {
        JObject card  = new JObject();
        card["id"]    = id;
        card["count"] = count;
        list.Add(card);
    }

    JObject json    = new JObject();
    json["user_id"] = userID;
    json["cards"]   = list;
    
    // POSTでデータを準備
    var postData = new WWWForm();
    postData.AddField("data", json.ToString());

    // APIのURLへリクエストを送信
    const string url = ServerAPI + "/delete_player_cards.php";
    var webRequest = UnityWebRequest.Post(url, postData);

    // 返事を待機し、終わり(何も返さない)
    await webRequest.SendWebRequest();
}

最後のステップは、削除ボタンが押されたときに必要なカード情報を集められるよう、ゲームのUIを修正することである。リクエストには少し時間がかかることがあるので、非同期メソッドとして書く必要がある。

// 選択されたカードを削除
private void DeleteSelectedCards()
{
    _ = DeleteCardsAsync();
}

// 選択されたカードを削除(非同期)
private async Awaitable DeleteCardsAsync()
{
    // 削除ボタンを無効にする
    deleteButton.interactable = false;
    
    // 表示中のカードを巡り、選択されたものを処理
    var cards = new Dictionary<string, int>();
    foreach (Transform child in cardsList.transform)
    {
        // 選択されていない?
        var card = child.GetComponent<CardDisplay>();
        if (!card.IsSelected)
            continue;

        // 同じIDだったら、数える
        cards.TryAdd(card.CardID, 0);
        cards[card.CardID]++;
    }
    
    // カードがなければ、終わり
    if (cards.Count == 0) return;
    
    // サーバーに依頼し…
    await APIRequest.DeleteCardsAsync(LoginController.UserID, cards);
    
    // …再読み込み依頼
    await LoadCardsAsync();
    
    // 削除ボタンを有効にする
    deleteButton.interactable = true;
}

あとは、カードをいくつか選んで、データベースが更新されているか確認するだけである。

image.png

image.png

ガチャを引く

カードを一覧表示して、削除もできるようになった。次は、ガチャゲームを作ってカードを追加していこう。このゲームには、次の機能が必要になる。

具体的には、各レア度の確率は次の通りである。

Rank SS S A B C
Rarity (%) 5% 10% 20% 30% 35%

データベース

まず、各プレイヤーが持つガチャの回数を保存するために、データベースに新しいテーブルを作る。必要なのは、ユーザーIDと回数だけである。

image.png

まず、各プレイヤーが持つガチャの回数を保存するために、データベースに新しいテーブルを作る。必要なのは、ユーザーIDと回数だけである。

INSERT INTO `gacha` (`user_id`, `count`) VALUES ('1', '3');

image.png

PHP スクリプト

ガチャを引く回数を求める

新しいAPIが2つ必要になる。1つはガチャの残り回数を確認するもの、もう1つは実際にガチャを引くものである。まずは簡単な方から始めよう。get_gacha_count.phpという新しいスクリプトを作る。これは単純な SELECT 文で、プレイヤーの残り回数を調べるだけである。テーブルにレコードが無い場合は、0を返す。

いつも通り、まずは GET パラメータでテストしてみよう。調べたい user_id を引数として渡す。

<?php
	// JSONを返すべき
	header("Content-Type: application/json; charset=utf-8");

	// 接続 
	$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);
	
	// ユーザーIDを取得
	$userID = $_GET'user_id'];
	
	// SQL文を構築
	$sql = "SELECT * FROM gacha WHERE user_id = " . $userID;
	
	// デフォルトとして 0 を返す
	$json['count'] = 0;
	
	// 見つけたら、回数を取得
	$result = $dbh->query($sql);
	if ($result->rowCount() == 1)
	{
		$record = $result->fetch();
		$json['count'] = $record['count'];
	}
	
	// 返す
	print(json_encode($json));
?>

user_id=1とuser_id=3でテストすると、次のような結果が得られる。

image.png

image.png

これが求めていた正しい出力である。次のステップに進む前に、$_GET$_POST に変更するのを忘れないでください。

ガチャを引く

次のスクリプトは、ガチャを実際に引く処理を行う。いくつかの処理が必要になるので、少し複雑である。

この処理は、4つの関数に分けて順番に呼び出すことができる。まずは、ランダムにランクを決める処理から始めよう。gacha_pull.php という新しいスクリプトを作ってください。

確率でランダムにカードのランクを決める

// 確率によりランダムランクを作成
// 0:C ~ 4:SS
function createRandomRank()
{
	// 0~100までの乱数を生成
	$chance = rand(0, 100)
	
	// 5% 以下だったら、SSランク(4)のカードになる
	if ($chance < 5) 
		return 4; 
	
	// 15% 以下だったら、Sランク(3)のカードになる
	if ($chance < 15)
		return 3; 

	// 35% 以下だったら、Aランク(2)のカードになる
	if ($chance < 35)
		return 2; 

	// 65% 以下だったら、Bランク(1)のカードになる
	if ($chance < 65)
		return 1; 

	// 残りは C(0)
	return 0;
}

このスクリプトは、0から100までのランダムな数を生成し、累積確率を順番にチェックしながら、0(C) から 4(SS) までのランクを1つ見つけて返す。

ランクを指定してデータベースからランダムにカードを選ぶ

次のスクリプトは、0から4までのランク値を受け取り、データベースを検索して、その中からランダムに1枚のカードのレコードを返す。

// ランクを用いて、ランダムのカードを返す
// $dbh:  データベースを検索するためのPDO
// $rank: カードのランク
function getCardWithRank($dbh, $rank)
{
    // SQL文を構築
    $sql = "SELECT * FROM cards WHERE rank = " . $rank;
    
    // すべてのレコードを取得
    $result = $dbh->query($sql);
    $records = $result->fetchAll();
    
    // ランダムのを選択
    $count = sizeof($records);
    $idx   = rand(0, $count-1);
    return $records[$idx];
}

プレイヤーにカードを追加する

このステップでは、user_cards テーブルを更新して、プレイヤーの持っているカードを反映させる。もしプレイヤーがすでにそのカードを持っていたら、count に1を足す必要がある。逆に、まだ持っていなければ、count を1として新しいレコードを追加する必要がある。幸い、SQL には INSERT ... ON DUPLICATE という特別な文がある。読むのは少し複雑だが、だいたい次のような形になる。

INSERT INTO table (カラム1, カラム2, ...) VALUES (値1, 値2, ..) 
ON DUPLICATE KEY UPDATE カラム1 = 値1, カラム2 = 値2, ...

この場合、重複するレコードが見つかったら、新しく追加する代わりに更新される。ただし、そのためには、重複してはいけないフィールドを「UNIQUE」として指定しておく必要がある。続ける前に、phpMyAdmin を開いて user_cards テーブルを編集してみよう。

image.png

user_idcard_id の両方を選択して、「UNIQUE」に設定してください。こうすることで、user_idcard_id の組み合わせが重複するレコードは存在できないと、データベースに伝えることができる。例えば、

user_id = 3, card_id = 1

は、

user_id = 3, card_id = 2

user_id = 2, card_id = 1

と共存できる。少なくともどちらかのフィールドが違うからである。しかし、user_id = 3, card_id = 1というレコードをもう一度追加しようとすると、その組み合わせは一意でなければならないため、エラーになりデータベースには追加されない。この2つの列の組み合わせが一意であることは、次の画面で確認できる。

image.png

一意なフィールドができたので、特別な INSERT ... ON DUPLICATE 文を使って、ユーザーとカードの組み合わせが存在するかどうかによって、追加または更新を行うことができる。だいたい次のような形になる。

INSERT INTO user_cards (user_id, card_id, count) VALUES ($userID, $cardID, 1) 
ON DUPLICATE KEY UPDATE count = count + 1

この文は、新しいuser_id / card_idの組み合わせを、count = 1として追加しようとする。もし重複していたら、代わりにcountを1増やす。これで、次のようなPHP関数が書けるようになる。

// ユーザーにカードを追加する
// $dbh:    データベースを検索するためのPDO
// $userID: ユーザーID
// $cardID: カードID
function addCardToPlayer($dbh, $userID, $cardID)
{
    // SQL文を構築
    $sql = "INSERT INTO user_cards (user_id, card_id, count) "
         . "VALUES ($userID, $cardID, 1) "
         . "ON DUPLICATE KEY UPDATE count = count + 1";
         
    // 実行
    $result = $dbh->exec($sql);
}

ガチャの回数を減らす

カードをプレイヤーに追加できたので、次はガチャの残り回数を減らす。これは、単純な UPDATE 文で実現できる。

// ガチャの数を減らす
// $dbh:    データベースを検索するためのPDO
// $userID: ユーザーID
function decreaseGachaPullCount($dbh, $userID)
{
    // SQL文を構築
    $sql = "UPDATE gacha SET count = count - 1 "
         . "WHERE user_id = " . $userID;
         
    // 実行
    $result = $dbh->exec($sql);
}

まとめる

あとは、これまで作った関数をすべて実行して、手に入れたカードの情報をJSONでプレイヤーに返すだけである。

// JSONを返すべき
header("Content-Type: application/json; charset=utf-8");

// 接続 
$dbh = new PDO('mysql:host=localhost; dbname=card_game', "root", "", [PDO::ATTR_PERSISTENT => true]);

// ユーザーID
$userID = $_POST['user_id'];

$rank = createRandomRank();
$card = getCardWithRank($dbh, $rank);
addCardToPlayer($dbh, $userID, $card['id']);
decreaseGachaPullCount($dbh, $userID);

// カード情報を返す
$json['card']['id']           = $card['id'];
$json['card']['rank']         = $card['rank'];
$json['card']['picture_id']   = $card['picture_id'];
$json['card']['monster_name'] = $card['monster_name'];
$json['card']['atk_value']    = $card['attack'];
$json['card']['def_value']    = $card['defense'];
$json['card']['hp_value']     = $card['hit_points'];	

print(json_encode($json));

スクリプト全体はこちらである。

Unityを使い、APIとの接続

次は、Unity側の作業に移る。まず、それぞれのPHP関数に対応するAPIリクエストを2つ作ろう。

APIRequest

ガチャの回数取得

// ガチャの回数を返す
// userID:ユーザーID
public static async Awaitable<int> GetGachaCountAsync(string userID)
{
    // POSTでデータを準備
    var postData = new WWWForm();
    postData.AddField("user_id", userID);

    // APIのURLへリクエストを送信
    const string url = ServerAPI + "/get_gacha_count.php";
    var webRequest = UnityWebRequest.Post(url, postData);

    // 返事を待機する
    await webRequest.SendWebRequest();

    // 返事の文字列を取得
    var webResponse = webRequest.downloadHandler.text;

    // JSONからPlayerCardの配列へ変換
    JObject response = JObject.Parse(webResponse);

    // 返す
    return (int)response["count"];
}

ガチャを引く

// ガチャを引く
// userID:ユーザーID
public static async Awaitable<JObject> GachaPullAsync(string userID)
{
    // POSTでデータを準備
    var postData = new WWWForm();
    postData.AddField("user_id", userID);

    // APIのURLへリクエストを送信
    const string url = ServerAPI + "/gacha_pull.php";
    var webRequest = UnityWebRequest.Post(url, postData);

    // 返事を待機する
    await webRequest.SendWebRequest();

    // 返事の文字列を取得
    var webResponse = webRequest.downloadHandler.text;

    // JSONからPlayerCardの配列へ変換
    JObject response = JObject.Parse(webResponse);

    // 返す
    return (JObject)response["card"];
}

GachaController

ガチャの回数

次に、GachaController.cs を更新する必要がある。このスクリプトは今のところアニメーションを再生するだけで、サーバーとはつながっていない。APIを使って残り回数を確認し、その回数分だけガチャを引けるように更新していこう。

UpdateGachaCountAsync() の中にある、次の行を置き換えてください。

// 未実装:サーバーに依頼し、数を取得
await Awaitable.NextFrameAsync();
int count = 1; // とりあえず固定

APIの呼び出しに置き換える。

// サーバーに依頼し、数を取得
int count = await APIRequest.GetGachaCountAsync(LoginController.UserID);

Unity上で試してみて、データベースで設定した通り「3」回が正しく取得できるか確認してください。

ガチャを引く

最後に、ガチャを引くAPIを GachaController とつなげて、結果をカード表示に反映させる必要がある。APIが返すカード情報は、CardDisplay が期待している形式とちょうど同じなので、わりと簡単につなげることができる。

PullGachaAsync() の中にある、次の行を置き換えてください。

// 未実装:サーバーに依頼し、ガチャを引き、
//        カードを更新する
await Awaitable.NextFrameAsync();

次のように置き換える。

//サーバーに依頼し、ガチャを引き、カードを更新する
JObject data = await APIRequest.GachaPullAsync(LoginController.UserID);
card.SetData(data);

あとは、実際にガチャゲームを遊ぶだけである!回数が無くなるまで3回引いてみて、それからカード一覧に戻って、新しいカードが増えているか確認してください!