インディーズゲームを作った時に便利だったUnity拡張の紹介②　SQLiteを用いたセーブデータ編


# 概要
別名義で活動しているのでタイトルは伏せさせていただきますが、個人でインディーズゲームを作成しておりました
その際に作って便利だったUnityの拡張などの機能や仕組みの紹介をします
今回はSQLiteを用いたセーブデータ編となります
基本的には生成物や生成前のデータ、最低限のロジックなどは説明しますがコードは記載しません
自身で作成する際のヒントになればと思います


# 生成前のXML
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370747/83f763d5-4bb0-92b8-d297-0667667c0fc2.png)

# 生成されるC#コード

```c#
///Don't write
///This is Generated Code

using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using SQLite;
using UniRx;

[Table("GameData")]
public class GameDataEntity : EntityBase
{
    /// <summary>
    /// ユーザーID
    /// <summary>
    [PrimaryKey]
    [Column("user_id")]
    public int user_id { get; set; }
    /// <summary>
    /// 所持金
    /// <summary>
    [Column("money")]
    public int money { get; set; }

}

public class GameDataModel
{
    private static List<GameDataModel> items = null;

    public static IList<GameDataModel> Items
    {
        get
        {
            if(items == null)
            {
                GetAll();
            }
            return items;
        }
    }

    private GameDataEntity entity = new GameDataEntity();
    private CompositeDisposable disposable = new CompositeDisposable();
    
    public int UserId => entity.user_id;
    private ReactiveProperty<int> money = new ReactiveProperty<int>();
    public IReadOnlyReactiveProperty<int> Money => money;

    


    public static async UniTask<GameDataModel> Get()
    {  
        if(items == null)
        {
            await GetAll();
        }
        
        if(items.Exists(i => i.UserId == 0))
        {
            return items.First(i => i.UserId == 0);
        }

        var entity = new GameDataEntity();
        entity.user_id = 0;
        entity.money = default;

        await SaveDataCore.InsertData(entity);
        var model = new GameDataModel(entity);
        items.Add(model);
        return model;
    }

    private static async UniTask GetAll()
    {
        if (items == null)
        {
            var datas = await SaveDataCore.GetAllData<GameDataEntity>();
            var models = new List<GameDataModel>();
            foreach (var data in datas)
            {
                models.Add(new GameDataModel(data));
            }
            items = models;
        }
    }

    private GameDataModel(GameDataEntity _entity)
    {
        entity = _entity;
        money.Value = entity.money;
        money.Do(v => entity.money = v).Subscribe(_ => UpdateData()).AddTo(disposable);

    }

    private async UniTask UpdateData()
    {
        await SaveDataCore.UpdateData(entity);
    }
     
    public void UpdateMoney(int _money)
    {
        money.Value = _money;
    }

    
    ~GameDataModel()
    {
        disposable.Dispose();
    }
}
```

## 使用技術
- XML
- C#
- System.Xml.Linq
- SQLite-net
- UniRX
  - いずれR3に置き換える
となっております

こちらはC#のロジックにより作られているため、UniRX以外の部分はgodotエンジンに移植しても動作しました

↑のコード上ではSQLite-netがかなりラップされた状態のコードになってます
基本的にはTemprate用のTextファイルを用意して、文字を置き換えるような形で対応してます


```mermaid
sequenceDiagram

participant temp as Template
participant gen as Generate
participant xml as XML

gen ->> xml : xmlの取得
gen ->> gen : 解析
gen ->> temp : テキストデータの読込み
gen ->> temp : テキストの特定のキーワードをxmlから取得した名に置き換える
gen ->> temp : ファイル名を書き換えて別名で保存

```
自動生成の流れはこんな感じです

## コンセプト
基本的にはXMLでセーブデータに関する機能が全部自動生成されるようにしたい
セーブデータを書き換えた場合は即保存したい
セーブデータの実データはDBのように扱いたい（デバックしやすくするため
これらの理由からSQLiteを採用しつつ↑のようなコードを自動生成する形となりました。


## その他
↑の仕組みでは
マイグレーションについて触れてませんが、SQLite-netがかなりいい感じにやってくれます

SQLite-netが実際便利なので、SQLite側の機能はかなり甘えた仕組みになってます