SQLite4Unity3d データベースにデータ保存

ゲームではよく進捗や設定などを保存したりしますよね。
Unityでは情報の保存にPlayerPrefsというものがあります。PlayerPrefsは使いやすく、コード数行で簡単にデータを保存できるので重宝していました。
しかしウェブサーバーなどから受け取った情報を保存したり、複雑な情報などを保存しておきたい時にはデータベースに保存する方法が良いと聞き、SQLiteをUnityで扱ってみようと思いました。

SQLiteを使うために色々な方法がありますが、SQLite4Unity3dというプラグインを使ってみようと思います
私自身も初心者ですので、この記事ではSQLite4Unity3dの簡単な使い方について紹介したいと重ます。

Unityスキルアップ、
始めるなら今

パズル、脱出、RPG...目標のゲームを完成させよう!

人気のUnity講座はこちら

Udemy講座

SQLite4Unity3dの導入とセットアップ

プラグインのダウンロードとインポート

それでは早速こちらのページからSQLite4Unity3dをダウンロードします。
zipファイルをダウンロードしたらUnityプロジェクトを立ち上げ、Assetsディレクトリに中身を入れていきましょう。
新規にPluginsというフォルダを作成しzipファイルを解凍して、ドラッグアンドドロップでインポートします。

これでインポートは完了です。

データベースの作成

事前準備

次にデータベースを作成していきましょう。
Assetsフォルダ内にStreamingAssetsを作成します。この中にデータベースのファイルを作成していくことになります。
StreamingAssetsフォルダにファイルを置くことで、ビルド時にそのままの形でアプリケーションに組み込まれ、実行時にアクセス可能となります。
プラットフォームに依存しない方法でデータベースファイルや他のリソースファイルをアプリケーションに含めたい場合に特に便利だそうです。
またC#スクリプトを作成していきますのでScriptsフォルダも用意しておきましょう。
このような感じになりました。

データベース作成メソッド

続いてC#スクリプトを作成します。Scriptsフォルダ内にDataServiceというファイルを作成しました。
中身は以下の通りです。


using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public static class DataService
{
    private static SQLiteConnection _database;

    public static void InitDatabase(string databaseName)
    {
        string DBPath = $"Assets/StreamingAssets/{databaseName}";
        _database = new SQLiteConnection(DBPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
    }
}

using

ネームスペースにSQLite4Unity3dを使用するために必要なので宣言します。

DataServiceクラス

アプリケーション全体で簡単にアクセスできるようにするためstaticクラスにします。

_databaseフィールド

SQLiteConnection型のフィールドを定義します。データベースとの接続を保持するための静的フィールドです

InitDatabaseメソッド

データベースのファイルパスを構築し、そのパスを使用してSQLiteConnectionの新しいインスタンスを作成します。
データベース名を引数で受け取りDBPathという変数に格納します。
次にSQLiteConnectionのインスタンスを作成します。
第一引数にデータベース名を受け取ります。
第二引数ではオプションを設定します。
今回の例は以下の通りです。
SQLiteOpenFlags.ReadWriteデータベースの読み書きできるモードを指定しています。
SQLiteOpenFlags.Createもしファイルが存在しなければファイルを作成するというオプションです。

これでデータベース自体を作るメソッドは完成しました。

テーブル作成メソッド

次にデータベースにテーブルを作って行きます。
今回はプレイヤーのID、名前、点数を保存したいと思います。
まずテーブルの構造を決めていきましょう。
DataServiceクラスとは別にPlayerクラスを作成し定義します。

using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

 /* 略 */
public class Player
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }
}

このようなクラスになりました。

Playerクラス

プロパティそれぞれがテーブルのカラムに相当します。
今回の例では以下のようにプロパティが定義されています。
ID(int型)プレイヤーを識別するIDです。
[PrimaryKey, AutoIncrement]が付与されています。意味は以下の通りです。
PrimaryKeyIdというカラムがこのテーブルの主キーであると示しています。
AutoIcrementデータが追加されるたびにIdが自動的に1ずつ増えていくことを意味しています。
これによってデータが追加される度にユニークなIDが自動的に割り当てられます。
Name(string型)プレイヤー名です。
Score(int型)プレイヤーのスコアです。
このようにPlayerクラスを定義することで、前述のCreateTableメソッド使用する際にSQLite4Unity3Dは自動的にデータベース内にPlayerテーブルを作成してくれます。

テーブル作成メソッド

では最後にPlayerクラスを基にPlayerテーブルを作成するメソッドを定義しましょう。
DataServiceクラス内に定義していきます。

using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public static class DataService
{
 /* 略 */
     
     public static void CreateTable()
    {
        _database.CreateTable<Player>();
    }
}

CreateTableメソッド

CreateTableメソッドではSQLite4Unity3DライブラリのCreateTable<T>()メソッドを実行しています。
<T>には具体的な型としてPlayerクラスが指定されます。
こういうのをジェネリックと言います。ジェネリックを使用することで任意のクラス型を引数として渡すことが可能になります。
これによってPlayerクラスの構造に基づいたPlayerテーブルがデータベース内に作成されます。
これでPlayerテーブルにデータを保存したり取得したりする準備が整いました。

ここまでのソースコード全体を載せておきます。

using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public static class DataService
{
    private static SQLiteConnection _database;

    public static void InitDatabase(string databaseName)
    {
        string DBPath = $"Assets/StreamingAssets/{databaseName}";
        _database = new SQLiteConnection(DBPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
    }

    public static void CreateTable()
    {
        _database.CreateTable<Player>();
    }
}

public class Player
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }
}


データ挿入と取得

それでは早速データベースにPlayer情報を登録してみましょう。
この段階ではテストとしてダミーデータを作成し、データベースに登録してみます。

DataManager作成

前段で作ったDataServiceを使うためにDataManagerというゲームオブジェクトとC#スクリプトを作ります。
ゲームオブジェクトにC#スクリプトをアタッチしておきましょう。
エディターではこのような感じになりました。

続いてDataManagerクラスを作っていきましょう。

dummyPlayers

データベースに登録するPlayer型のダミーデータを作成します。
複数登録したいので今回はListで作成しました。

using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public class DataManager : MonoBehaviour
{
    List<Player> dummyPlayers = new List<Player>()
    {
        new Player {Name = "Player1",Score = 100},
        new Player {Name = "Player2",Score = 50},
        new Player {Name = "Player3",Score = 48},
        new Player {Name = "Player4",Score = 19},
    };
}


Awake

次にAwakeメソッドでDataServiceクラスにアクセスし、データベースの作成とテーブルの作成を行います。
データベース名は”SampleDb”としておきましょうか。

using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public class DataManager : MonoBehaviour
{
    /* 略 */
    private void Awake()
    {
        DataService.InitDatabase("SampleDb");
        DataService.CreateTable();
    }
}

InsertDummyDataメソッド

続いてデータを登録(Insert)するメソッドを作っていきます。
dummyPlayersをループで回し一件ずつ登録していきます。

using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public class DataManager : MonoBehaviour
{
    /* 略 */

    public void InsertDummyData()
    {
        foreach(Player player in dummyPlayers)
        {
            DataService.InsertPlayer(player);
        }
    }
}

InsertPlayerメソッド

この段階ではDataServiceクラスにInsetPlayerメソッドはありませんので作っておきます。
SQLite4Unity3dの機能でInsertというメソッドがあります。
これによってデータベースに情報を登録する事ができます。
今回はそれを使ってみましょう。


using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;

public static class DataService
{
    /* 略 */
    public static void InsertPlayer(Player player) 
    {
        _database.Insert(player);
    }
}

今回は一件ずつPlayer型の情報を受け取って登録していますが、InsertAllというメソッドもあります。
これはまとめて複数の情報を登録することができますので、大量に情報を登録したい場合はInsertAllを使うのも良いでしょう。

これで一通りデータを登録する処理が完成しました。
最後にDataManagerのStartでInsertDummyDataを実行するようにしておきます。
DataManagerのソースコード全体は以下の通りです。


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

public class DataManager : MonoBehaviour
{
    List dummyPlayers = new List$lt;Player>()
    {
        new Player {Name = "Player1",Score = 100},
        new Player {Name = "Player2",Score = 50},
        new Player {Name = "Player3",Score = 48},
        new Player {Name = "Player4",Score = 19},
    };
    private void Awake()
    {
        DataService.InitDatabase("SampleDb");
        DataService.CreateTable();
    }

    private void Start()
    {
        InsertDummyData();
    }

    public void InsertDummyData()
    {
        foreach(Player player in dummyPlayers)
        {
            DataService.InsertPlayer(player);
        }
    }
}

データの表示

続いて登録したデータをログで確認してみましょう。

GetAllPlayers

まずDataServiceクラス側にPlayerテーブルのデータをすべて取得するGetPlayersメソッドを作成します。
Player型のListを返却するようにしてみます。
SQLite4Unity3dを使ってTable<T>とするとテーブルの内容を取得できますので、その結果をListにします。
Linqも使いますのでusingに追加しておきましょう。


using System.Collections;
using System.Collections.Generic;
using SQLite4Unity3d;
using System.Linq;

public static class DataService
{
  /*略*/
    
    public static List<Player> GetAllPlayers()
    {
        return _database.Table<Player>().ToList();
    }
}

ShowPlayers

続いてDataManagerクラスにPlayer情報を表示するShowPlayersメソッドを作成します。
今回はシンプルにログに表示させてみましょう。


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

public class DataManager : MonoBehaviour
{
  /* 略 */

    private void ShowPlayers()
    {
        foreach(Player player in DataService.GetAllPlayers())
        {
            Debug.Log($"ID:{player.Id} / NAME : {player.Name} / SCORE : {player.Score}\n");
        }
    }
}


このShwoPlayersメソッドをInsertDummyDataの後に呼び出してみます。


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

public class DataManager : MonoBehaviour
{
    /* 略 */

    private void Start()
    {
        InsertDummyData();//データ登録
        ShowPlayers();//データ表示
    }

    /* 略 */


}

実行するとログにダミーデータが表示されました。

そして今まで空っぽだったStreamingAssetsフォルダにもSampleDbというデータベースファイルが作られたのが確認できます。

データ操作とUIの統合

さて、ここまででデータベースを作成し、データを登録、表示までできるようになりました。
このままではあまり面白くありませんので、UIから情報を受け取ってデータベースに登録して行く処理を実行したいと思います。

UIの作成

UnityエディターでUIを配置していきます。
必要なのはInputField、Button、あとはそれらを包括するPanelとラベル用のTextを置いてみました。
このような感じです。

続いてUIを管理するC#スクリプト、UIManagerを作成します。
このスクリプトではInputFieldの文字、Buttonのクリック時の挙動などを管理したいと思います。
スクリプトを作成したらCanvasにアタッチしておきます。
では早速中身を書いていきましょう。

登録処理


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIManager : MonoBehaviour
{
    [SerializeField] Button addButton;
    [SerializeField] TMP_InputField nameInputField;
    [SerializeField] DataManager dataManager;
    private void Start()
    {
        addButton.onClick.AddListener(()=>  SendPlayerData() );
        nameInputField.characterLimit = 10;
    }

    public void SendPlayerData()
    {
        string inputName = nameInputField.text;
        if(string.IsNullOrEmpty(inputName))
        {
            return;
        }
        int score = Random.Range(1,200); //スコアをランダムで生成
        dataManager.InsertPlayer( new Player { Name = inputName, Score = score });
    }
}


StartでaddButtonのクリック時の挙動を設定し、nameInputFieldで文字数制限をしています。
SendPlayerdataメソッドではnameInputFieldで入力された文字を取得し0文字なら処理を中断しています。
文字が入力されていたらランダムでスコアを生成しています。
最後にDataManagerのInsertPlayerメソッドを呼び出しています。
この段階ではDataManagerにそのメソッドはありませんので早速作っておきましょう。


public class DataManager : MonoBehaviour
{
    /* 略 */
    public void InsertPlayer(Player player)
    {
        DataService.InsertPlayer(player);
    }
    /* 略 */

これでUIからデータベースに情報を登録することができました。
次に登録されたデータを表示する仕組みを作っていきます。

スコア表示用UI配置

今回はデータを送信したらTextでスコア一覧を表示させるといったシンプルな実装にします。
まずUIを作っておきましょう。

ScorePanelというものを新たに作り、その中にラベル用テキスト、データ一覧を表示するテキストを配置しただけです。
本来はScrollViewなどを使って作ったほうが良いのでしょうが、今回の主旨はデータベースの扱いなので割愛します。
それではスクリプトを書いていきましょう。

スコア表示用スクリプト

UIManagerのSendPlayerDataを実行後にShowAllPlayersというメソッドを呼び出しますので以下のように修正しました。


public class UIManager: MonoBehaviour
{
    /* 略 */
    [SerializeField] GameObject scorePanel;
    [SerializeField] TextMeshProUGUI playersText;
    /* 略 */
    public void SendPlayerData()
    {
        string inputName = nameInputField.text;
        if(string.IsNullOrEmpty(inputName))
        {
            return;
        }
        int score = Random.Range(1,200); //スコアをランダムで生成
        dataManager.InsertPlayer( new Player { Name = inputName, Score = score });
        ShowAllPlayers();
    }

    public void ShowAllPlayers()
    {

        string playerScores = "";
        foreach(Player player in dataManager.GetAllPlayers())
        {
            playerScores += $"{player.Name} / {player.Score} \n";
        }
        playersText.text = playerScores;
        scorePanel.SetActive(true);
    }
}

Unityエディターの方で忘れずにSerializeFieldで追加した項目をアタッチしておきましょう。
さて次にDataManagerの方にもGetAllPlayersメソッドを追加していきます。


public class DataManager: MonoBehaviour
{
    /* 略 */
    public List GetAllPlayers()
    {
        return DataService.GetAllPlayers();
    }
    /* 略 */
    
}

これでデータを表示させることができます。
実行してみましょう。

おっと、ダミーデータが残っていましたね。
これを消去しておきたいと思います。
せっかくなのでデータのリセット処理を実装しておきましょうか。

テーブルの中身をリセットする

テーブルの中身を空っぽにするために削除ボタンを作ってみます。

UIManagerの方にはSerializeFieldでリセットボタンを追加しアタッチしておきました。
続いてデータベースのPlayerテーブルの中身を空っぽにする処理を実装していきます。
まずはDataServiceクラスに以下の処理を追記します。


public static class DataService
{
    /* 略 */
    public static void DeletePlayers()
    {
        _database.DeleteAll<Player>();
    }
    /* 略 */
    
}

DeleteAll<T>()で該当のテーブルの中身を空にします。
テーブル自体は削除されずデータのみのが削除されます。

次にDataManagerです。


public class DataManager : MonoBehaviour
{
    /* 略 */
    public void DeletePlayers()
    {
        DataService.DeletePlayers();
    }
    /* 略 */
    
}

最後にUIManagerです。


public class UIManager: MonoBehaviour
{
    private void Start()
    {
        addButton.onClick.AddListener(()=>  SendPlayerData() );
        resetButton.onClick.AddListener(() => DeletePlayers()); //追加
        nameInputField.characterLimit = 10;
        scorePanel.SetActive(false);
    }
    /* 略 */
    public void DeletePlayers()
    {
        dataManager.DeletePlayers();
    }
    /* 略 */
    
}

これでPlayerテーブルのデータはresetButtonをクリックした時点で削除されます。
一度削除されたデータは元に戻せないので、削除時は注意を促すアラートを出すなどした方が良いと思います。
今回はテストですので、いきなり消してしまいます。
ではデータを削除した後に改めてデータを登録してみましょう。

このようにPlayerテーブルからデータはすべて削除され、新たに追加したPlayerの情報が表示されました。
これで終了でも良いのですが、本来であればスコアが高い順に並ぶのが良いと思います。
しかし現在、Playerのデータはこのような感じに並んでいます。

これはデータを登録した順番(Id順)で情報を取得しています。
それをスコアの高い順で取得する処理を実装してみましょう。

データのソート

DataServiceクラスのGetAllPlayersメソッドを修正していきます。
と言っても簡単で以下のようになります。


    /* 略 */
    public static List GetAllPlayers()
    {
        return _database.Table<Player>().OrderByDescending(p => p.Score).ToList();
    }
    /* 略 */

OrderByDescendingはLinqのメソッドです。指定されたキーを軸に降順(大きい順)に並べ替えるためのメソッドです。
p => p.Scoreはラムダ式となっていて、各Playerオブジェクトpに対してp.Scoreをキーにして使用するという意味です。
Linqは便利な機能がたくさんありますので興味がある方は調べて見てください。
こちらのUdemyの教材でもLinqについてわかりやすく解説してくれています。

それではちゃんとスコアが高い順で並んでいるか確認します。

このように並び替えられていました。

同じPlayer名だった場合の処理

記事が長くなりましたが、最後に同じPlayer名だった場合の処理を考えてみたいと思います。
今回はテストなのでPlayer名が同じ=同じプレイヤーと仮定して作ってみます。
Playerのスコアはランダムで決められるので、もし同じプレイヤーが過去のスコアよりも上回った場合、そのPlayerの情報を更新するという処理を実装してみます。

DataServiceにメソッド追加

まずDataServiceに必要なメソッドを2つ追加します。
PlayerのNameで情報を取得するメソッドと、Playerテーブルを更新するメソッドです。


    /* 略 */
    public static Player GetPlayerByName(string playerName)
    {
        return _database.Table<Player>().FirstOrDefault(p => p.Name == playerName);
    }

    public static void UpdatePlayerData(Player player)
    {
        _database.Update(player);
    }
    /* 略 */

GetPlayerByNameメソッドではPlayerテーブルからplayerNameが同じ情報を取得し返却する処理を行っています。
UpdatePlayerDataメソッドではPlayerテーブルの情報を更新しています。SQLite4Unity3dではUpdate時に渡されるオブジェクトの型を元にテーブルを推察してくれるのでテーブル名を指定する必要はありません。
また渡されたオブジェクトのPrimaryKeyの値を使用して該当のレコードを更新します。
今回はPlayerはIdがPrimaryKeyなのでそれを使って更新すると言った感じです。

DataManagerのInsertPlayerを修正

次にUIから受け取ったPlayerデータを基にDataManagerのInsertPlayerメソッドを修正していきましょう。


    /* 略 */
    public void InsertPlayer(Player player)
    {
        Player existingPlayer = DataService.GetPlayerByName(player.Name);
        if(existingPlayer != null)
        {
            if(player.Score > existingPlayer.Score)
            {
                existingPlayer.Score = player.Score;
                DataService.UpdatePlayerData(existingPlayer);
            }
        }
        else
        {
            DataService.InsertPlayer(player);
        }
    }
    /* 略 */

ここではまずUIから渡されたPlayer情報を受け取っていますが、そのNameをキーとしてDataServiceからPlayer情報を取得し、extistingPlayerに格納しています。
次にextistingPlayerがnullでなければ、既に登録済みPlayerと判断し、UIから送られてきたPlayerとのScoreを比較しています。
もしScoreが上回っていたらextstingPlayerのScoreを更新しています。
この時なぜplayerのScoreをわざわざextistingPlayerのScoreに代入するかと言うと、先程Update時にはPrimaryKeyを使ってレコードを更新すると書きました。
UIから送られてきたPlayerデータにはPrimaryKey(今回はId)が含まれていません。
なのでPrimaryKeyの情報が含まれているextistingPlayerの情報を上書きし、DataServiceのUpdatePlayerDataメソッドで渡しています。
またextistingPlayerがnullの場合は新しいPlayerと判断し情報を新たに登録しています。
それでは先程登録したYou Unityという名前で改めて登録してみます。

最初はYou Unityは47点だったのが69点に上書きされていました。
このようにしてPlayerデータを上書きすることができました。

例外を追加

データベースの処理を行うに当たってエラーが発生することがあります。
例えば、データベースファイルが見つからない、データベースロック、不正なクエリ、など様々な理由で発生する可能性があります。
今回で言うとDataServiceクラスのメソッドに可能性がありますので、最後にtry-catchを実装していきましょう。
以下のようになります。


using System;
using System.Collections.Generic;
using SQLite4Unity3d;
using System.Linq;
using UnityEngine;

public static class DataService
{
    private static SQLiteConnection _database;

    public static void InitDatabase(string databaseName)
    {
        try
        {
            string DBPath = $"Assets/StreamingAssets/{databaseName}";
            _database = new SQLiteConnection(DBPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to initialize database: {e.Message}");
        }
    }

    public static void CreateTable()
    {
        try
        {
            _database.CreateTable<Player>();
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to create table: {e.Message}");
        }
    }

    public static void InsertPlayer(Player player) 
    {
        try
        {
            _database.Insert(player);
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to insert player: {e.Message}");
        }
    }

    public static List GetAllPlayers()
    {
        try
        {
            return _database.Table<Player>().OrderByDescending(p => p.Score).ToList();
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to get all players: {e.Message}");
            return new List(); // エラーが発生した場合は空のリストを返す
        }
    }

    public static void DeletePlayers()
    {
        try
        {
            _database.DeleteAll();
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to delete players: {e.Message}");
        }
    }

    public static Player GetPlayerByName(string playerName)
    {
        try
        {
            return _database.Table<Player>().FirstOrDefault(p => p.Name == playerName);
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to get player by name: {e.Message}");
            return null; // エラーが発生した場合はnullを返す
        }
    }

    public static void UpdatePlayerData(Player player)
    {
        try
        {
            _database.Update(player);
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to update player data: {e.Message}");
        }
    }
}

それぞれのデータベース操作でエラーが発生した場合はログを表示し、戻り値があるメソッドに関してはnullや空のListをreturnしています。
以上簡単ではありますが、データベースの処理になります。

まとめ

データベースとLinqの機能は他にも色々な使い方があります。
この記事で興味が湧きましたら是非ご自身でも色々調べてみてください。

注意点

今回はわかりやすいようにパフォーマンス等を考えず記事を書いていますが、実際にゲームに組み込む場合は以下の点を注意してください。

大量のデータがある場合

扱うデータが非常に多い場合、全件取得するとパフォーマンスに悪影響を及ぼす可能性があります。データの量に応じて、ページングやデータの遅延読み込みなど、クエリの実行方法を工夫してみてください。

ToListに関して

LinqのToListは結果セットをメモリにロードするため非常に便利ですが、結果が大きい場合はメモリ使用量が増大し、アプリケーションのパフォーマンスに影響を与えることがあります。データの量が多い場合は、必要なデータのみを抽出するなど、クエリを最適化してみてください。

今回の内容はツッコミどころも多いと思いますが、とりあえずこれで終わります。
また今回のプロジェクトはgithubにアップしておきますので、よければ参考にしてみてください。
SQLiteTest
以上です。ありがとうございました。

関連記事

最後までご覧頂いてありがとうございました。

セール中!
C#プログラミング入門

もっと早く教えてほしかった!

Unity C#プログラミング入門