ローカルのデータベースを使ってUnityのログイン認証を作った話

# はじめに
Qiitaを書くのは学生時代の時以来ですね。
今回はIT企業に入社し、データベースとサーバー関連の技術を身に着けたので、それをUnityのログイン認証として実装してみたお話です。
データベースを運用する上でのお話なので、その他のものでデータベースを使用したい　という際にもご活用いただけるかと思います。

一応ですがこの記事はまだ触って歴も浅い自分が学んだことを記す目的の意味合いが強いので、間違っているところもあるかと思います。
ご了承ください。

GitHubURL
https://github.com/komugikoShimizu/UnityLoginDemo

# 使用する環境
Unity(多分バージョンは何でもいいはず)
XAMPP
・普段Unityばかり触っていて知らないよ　という方は下記からダウンロードできます。
https://www.apachefriends.org/jp/index.html

言語
C#(Unity)
PHP(クエリ書き込み用)

### XAMPPについて軽くご説明
XMAPPはMySQLとApacheを内包した便利なパッケージです。
とても扱いやすいのでデータベースを使い始めたい　という方にお勧めです。

※MySQL：とてもメジャーなデータベース。大体これでいいと思う。詳しく知りたい人はRDBMSで調べてね
※Apache：サーバーに使用するアプリケーション。Apacheが提供するURLに使いたい情報を紐づけると、ApacheのURLからアクセスできるようになる。すごい
※クエリ：データベースに対する命令みたいなもの。「INSERT」や「SELECT」などを使用してデータベースに対して処理を行うもの　って認識で大丈夫だと思う

# 実践
それでは記述していきましょう。

まずはUnityを触ってきた方向けに少々サーバーやデータベースの話を踏まえて知っておくべきことについて。
ApacheなどはローカルのWebアプリケーションとして作成されます。
それらWebアプリケーションにアクセスを行うには「UnityWebRequest」を使用します。
これにアクセスしたいURLを渡すことでアクセスすることができます。
そしてもうひとつ。
データベースへのアクセス手段は大きく2つあり、それが「Get」と「Post」です。
Getはその名の通りデータベースから情報を取得するときに使用します。
Postはデータベースに登録するときなど...
今後も出てくるので頭に入れておきましょう。

## Unity側の実装
```C#:MySQLDatabaseManager.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System;

public class MySQLDatabaseManager : MonoBehaviour
{
    // XAMPP内のPHPファイルへのURL
    private string testUrl = "http://127.0.0.1//get_user.php";

    // インスタンスを一つに維持するための静的変数
    public static MySQLDatabaseManager instance { get; private set;}

    void Awake()
    {
        if (instance == null) // インスタンスが存在していなければ
        {
            instance = this; // 自身をインスタンスに設定

            // 値取得用コルーチンを実行
            Get(testUrl, DebugLog);
        }
        else // インスタンスが存在していれば
        {
            Destroy(this.gameObject); // オブジェクトを削除してアクセスできなくする
        }
    }

    // デバッグ用コールバック関数
    private void DebugLog()
    {
        Debug.Log("アクセス完了");
    }

    // Getにアクセスしてデータベース内すべての情報を取得
    public void Get(string url, Action endAction)
    {
        StartCoroutine(GetDataFromServer(url, endAction));
    }

    // idをもとに抜き出すクエリを実行して、一致する情報を取得
    public void GetSelectUser(string url, int id, Action<UserData> act)
    {
        StartCoroutine(GetSelectUserServer(url, id, act));
    }

    // 指定された情報をもとに登録
    public void Post(string url, string name, int state, string text, Action<int> act)
    {
        StartCoroutine(PostDataToServer(url, name, state, text, act));
    }

    // PHPアクセスを行って値を取得する
    IEnumerator GetDataFromServer(string url, Action endAction)
    {
        // WebRequestを使用してPHPにアクセス
        UnityWebRequest www = UnityWebRequest.Get(url);

        // 値が送信されるまで処理を停止
        yield return www.SendWebRequest();

        // 返却データの内容が成功ではないなら
        if (www.result != UnityWebRequest.Result.Success)
        {
            // 返却データから例外文を表示
            Debug.LogError("Error: " + www.error);
        }
        else    // 返却データ内容が成功なら
        {
            // 取得されたJson内容を出力する
            Debug.Log("Received: " + www.downloadHandler.text);

            // 取得したJsonをもとにオブジェクトに変換する関数を実行
            ProcessJsonData(www.downloadHandler.text);

            endAction();
        }
    }

    // Jsonをもとにオブジェクトに変換する関数を実行
    void ProcessJsonData(string jsonData)
    {
        // Json変換クラスを通じてJson内容を配列オブジェクトに変換
        var dataArray = JsonHelper.FromJson<UserData>(jsonData);

        // Jsonを変換した配列オブジェクトをforeachで実行
        foreach (var data in dataArray)
        {
            // 各ステータスをログに出力
            Debug.Log("ID: " + data.id + ", name: " + data.name + ", state: " + data.state + ", text: " + data.text);
        }
    }

    // データベース登録コルーチン   
    IEnumerator PostDataToServer(string url, string name, int state, string text, Action<int> act)
    {
        // PHPに渡す情報を作成
        WWWForm form = new WWWForm();
        form.AddField("name", name);
        form.AddField("state", state);
        form.AddField("text", text);

        UnityWebRequest www = UnityWebRequest.Post(url, form);  // URLにPostする
        yield return www.SendWebRequest();  // 値の返却があるまで処理を停止

        if (www.result != UnityWebRequest.Result.Success) // 返却されたデータに成功以外のデータが格納されている場合
        {
            Debug.LogError("Error: " + www.error); // ログにエラー文を出力                                                              
        }
        else
        {
            string jsonResponse = www.downloadHandler.text; // レスポンス内容を取得
            Debug.Log(jsonResponse); // レスポンス内容を出力

            act(int.Parse(jsonResponse)); // 登録されていたコールバックに返却されたデータを使用して実行
            
            Debug.Log("Received: " + www.downloadHandler.text);
        }
    }

    // 指定したユーザーを取り出すコルーチン
    IEnumerator GetSelectUserServer(string url, int id, Action<UserData> act)
    {
        // 送信する内容を作成
        WWWForm form = new WWWForm();
        form.AddField("id", id);

        url += "/?id=" + id;    // URLパラメータを追加

        UnityWebRequest www = UnityWebRequest.Get(url);  // URLにリクエストを送信
        yield return www.SendWebRequest();  // 値の返却があるまで処理を停止
        
        // 返却データの内容が成功ではないなら
        if (www.result != UnityWebRequest.Result.Success)
        {
            // 返却データから例外文を表示
            Debug.LogError("Error: " + www.error);
        }
        else    // 返却データ内容が成功なら
        {
            // 取得されたJson内容を出力する
            Debug.Log("Received: " + www.downloadHandler.text);

            string[] userDataColumn = www.downloadHandler.text.Split(":"); // 取得したデータをSplitで配列化する

            // 取得した情報からユーザーデータを初期化する
            UserData user = new UserData();
            user.id = int.Parse(userDataColumn[0]);
            user.name = userDataColumn[1];
            user.state = int.Parse(userDataColumn[2]);
            user.text = userDataColumn[2];

            act(user); // コールバックに作成したユーザー情報を渡して実行
        }
    }
}
```
```C# JsonHelper.cs
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public static class JsonHelper
{
    // Jsonファイルをオブジェクトに変換する関数
    public static T[] FromJson<T>(string json)
    {
        // Jsonファイルを配列として扱えるようにする
        // Itemsなのはラッパークラスの変数名がItemsであるため
        string newJson = "{\"Items\":" + json + "}";

        // ラッパークラスにJsonの内容を記述する
        Wrapper<T> wrapper = JsonUtility.FromJson<Wrapper<T>>(newJson);

        // ラッパークラスの変数を返却する
        return wrapper.Items;
    }

    // ジェネリックを配列化するためのクラス
    [System.Serializable]
    private class Wrapper<T>
    {
        public T[] Items;
    }
}
```

```C# UserData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class UserData
{
    public int id;
    public string name;
    public int state;
    public string text;
}
```
```C# SaveDataManager.cs
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Unity.VisualScripting;
using UnityEngine;

public class SavedataManager : MonoBehaviour
{
    private const string url = "http://127.0.0.1/add_user.php";

    private const string selectUrl = "http://127.0.0.1/get_user_select.php";

    [SerializeField]
    private string username = "user";

    [SerializeField]
    private string text = "text";

    [SerializeField]
    private TextAsset savefile;

    public static UserData USER {get; private set;}

    // Start is called before the first frame update
    void Start()
    {
        if (savefile.text == "")
        { 
            Debug.Log("NULL");
            MySQLDatabaseManager.instance.Post(url, username, 1, text, Method);
        }
        else
        {
            Debug.Log("NOT NULL");
            StreamReader reader = new StreamReader("Assets/Json/" + savefile.name + ".json");
            int id = int.Parse(reader.ReadLine());
            MySQLDatabaseManager.instance.GetSelectUser(selectUrl, id, Mehtod);
        }
    }

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

    private void Method(int id)
    {
        UserData user = new UserData();
        user.id = id;
        user.name = username;
        user.state = 1;
        user.text = text;
        string str = JsonUtility.ToJson(user);
        USER = user;
        Debug.Log(str);
        StreamWriter writer = new StreamWriter("Assets/Json/" + savefile.name + ".json");
        writer.WriteLine(id);
        writer.Close();
    }

    private void Mehtod(UserData user)
    {
        Debug.Log(user.id);
        USER = user;
    }
}

```
今回作成した「MySQLDatabaseManager」には3つの機能を備えさせています。
1.データベースからすべての行を抜き出す
2.データベースから指定のidを持つ行を抜き出す
3.データベースに新たに行を追加する

とはいえ役割は「UnityWebRequest」を使用したデータベースからのデータ取得とそのコールバックです。
これらを実際に処理しているのがPHPとなります。

## XAMPP側の実装
実際に処理を記述する前に注意点。
これらはXAMPPに対しPHPにフォルダを保存して、そこにURLを介して値を取得しています。
保存する場所は
「C:\xampp\htdocs」ここです。
xmapp直下ではないので気を付けて。

今回は指定ユーザー抜き出しPHPと行追加PHPについて記述していきます。
(まあ全体取得も書いてもいいんだけどちょっとクエリ調整するだけなので割愛)
まず指定ユーザー抜き出しのところについて
```PHP get_user_select.php
<?php
// データベース接続の設定
$servername = "localhost";
$username = "root";
$password = "pass";
$dbname = "testDatabase";

// 指定されたIDを取得
$id = $_GET['id']; // URLパラメータからIDを取得

// データベース接続の作成
$conn = new mysqli($servername, $username, $password, $dbname);

// 接続の確認
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

// SQLクエリの作成
$sql = "SELECT * FROM userData WHERE id = $id";
$result = $conn->query($sql);

// 結果の取得と表示
if ($result->num_rows > 0) {
    // 結果が存在する場合
    while($row = $result->fetch_assoc()) {
        echo "$id:{$row["name"]}:{$row["state"]}:{$row["text"]}";
    }
} else {
    echo "0 results";
}

// 接続の終了
$conn->close();
?>
```
このPHPは「MySQLDatabaseManager」の「GetSelectUserServer」でURLとして渡されていたPHPファイル。
内容としてはURLパラメータとして指定されていた「id」をもとにデータベースに登録されているユーザーをクエリで抜き出すというもの。
そしてこのデータを取得しようとしているC#プロジェクトに、Splitを使うことですべての情報が取れるようになっているという感じ。

次に行追加PHPについて
```PHP add_user.php
<?php
$servername = "localhost";
$username = "root";
$password = "pass";
$dbname = "testDatabase";

$conn = new mysqli($servername, $username, $password, $dbname);

if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

$name = $_POST['name'];
$state = $_POST['state'];
$text = $_POST['text'];

$sql = "INSERT INTO userData (name, state, text) VALUES ('$name', $state, '$text');";
// echo $sql;
if ($conn->query($sql) === TRUE) {
    $last_id = $conn->insert_id;
    echo $last_id;
} else {
    echo "Error: " . $sql . "<br>" . $conn->error;
}

$conn->close();
?>
```
このPHPは値を送信側から受け取って、そのデータをもとにINSERTクエリを実行するというもの。
「PostDataToServer」から呼び出していて、送信データをformにまとめてPOSTの引数に追加することでPHPで送信側のデータを扱えるようにしている。
ちなみにこの手段はGETだと使えないからそこだけは注意。

# 応用
今回はユーザーデータ関連で使用しましたが、モンスターデータやフィールドデータなんかもデータベースで扱うことができるようになります。
自分の使いやすいような形にカスタマイズしていただければと思います。
今回のような指定行抜き出しや行追加程度なら簡単なクエリで書けると思う。

# 終わりに
今回はXAMPPを使用したローカルのデータベースを使用した簡単な認証機能作成を紹介しました。
のちのちは外部キー制約を利用した複雑なデータベースの構造や、AWSなどを利用したクラウド系のサービスの利用とかもできたらなーって思っています。
また投稿するかもなので気が向いたら見てください。