使用ライブラリ
UniTask
事前準備
スプレッドシートの共有設定をリンクを知っている全員にする。
↓みたいな感じに変数名とかを合わせる。
[Serializable]
public class AttackData
{
public string attackKey;
public int power;
public Attribute attribute;
}
JsonUtilityではなくJsonConvertを使ってるので、SerializeFieldを使えない。
泣く泣くpublic変数にします。
大文字小文字も多分合わせないといけないはず。
コード
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace Takenokohal.Utility
{
public static class CsvConverter
{
public static string CsvToJson(string csv)
{
var v = JsonConvert.SerializeObject(CsvToDictionary(csv));
return v;
}
private static IReadOnlyList<IReadOnlyDictionary<string, object>> CsvToDictionary(string csv)
{
const string splitKey = @"""";
var rows = csv.Split(new[] { splitKey + "\n" }, StringSplitOptions.None);
var dataList = rows
.Select(SplitRowToData).ToList();
var cleanedData = CleanUp(dataList).ToList();
var header = cleanedData[0];
header.RemoveAll(string.IsNullOrEmpty);
var dicList = new List<Dictionary<string, object>>();
foreach (var currentRow in cleanedData.Skip(1))
{
var dic = new Dictionary<string, object>();
for (int i = 0; i < header.Count; i++)
{
var dataString = currentRow[i];
var key = header[i];
if (int.TryParse(dataString, out var result))
{
dic.Add(key, result);
continue;
}
dic.Add(header[i], dataString);
}
dicList.Add(dic);
}
return dicList;
}
private static string[] SplitRowToData(string row)
{
return row.Split(new[] { ',' }, StringSplitOptions.None);
}
private static List<List<string>> CleanUp(IReadOnlyList<IReadOnlyList<string>> origin)
{
var clone = new List<List<string>>();
foreach (var data in origin.Where(value => !string.IsNullOrEmpty(value[0])))
{
var list = data.Select(s => s.Trim(new[]
{
'"'
})).ToList();
clone.Add(list);
}
return clone;
}
}
}
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine.Networking;
namespace Takenokohal.Utility
{
public static class SpreadSheetLoader
{
public static async UniTask<IReadOnlyList<T>> LoadData<T>(string url)
{
var req = UnityWebRequest.Get(url);
var result = await req.SendWebRequest();
var csv = result.downloadHandler.text;
var json = CsvConverter.CsvToJson(csv);
var target = new List<T>();
JsonConvert.PopulateObject(json, target, new JsonSerializerSettings()
{
ObjectCreationHandling = ObjectCreationHandling.Replace
});
return target;
}
}
}
解説
SpreadSheetLoader
csv読み込み
urlを引数に、好きなデータのデータのリストを持ってきます。
var req = UnityWebRequest.Get(url);
var result = await req.SendWebRequest();
var csv = result.downloadHandler.text;
この部分でスプレッドシートをstringとして読み込んでる。
URLの書き方は、
スプレッドシートの編集画面のURLが
https://docs.google.com/spreadsheets/d/hogehoge/edit?gid=0#gid=0
となっているはずなので、このhogehoge部分をコピペして、
"https://docs.google.com/spreadsheets/d/hogehoge/gviz/tq?tqx=out:csv&sheet=";
みたいに書く。
sheet=
の後にシート名を書く。
Jsonにコンバート
CsvConverter(後述)を使ってJsonにしてもらう。
var json = CsvConverter.CsvToJson(csv);
好きなデータに変換
JsonConvert.PopulateObjectで変換。
UnityのJsonUtilityは使いにくかったのでNewtonsoft.JsonのJsonConvertを使いました。
var target = new List<T>();
JsonConvert.PopulateObject(json, target, new JsonSerializerSettings()
{
ObjectCreationHandling = ObjectCreationHandling.Replace
});
return target;
CsvConverter
考え方
Jsonだとクラスの中身とDictionaryの中身が同じ形になることを利用して、まずDictionaryに変換してからそれをJsonにすることで好きなデータとして拾ってます。
csvをListのListに
const string splitKey = @"""";
var rows = csv.Split(new[] { splitKey + "\n" }, StringSplitOptions.None);
var dataList = rows
.Select(SplitRowToData).ToList();
private static string[] SplitRowToData(string row)
{
return row.Split(new[] { ',' }, StringSplitOptions.None);
}
\nで切ってしまうと、スプレッドシート内で改行ができなくなってしまうので、""\nで区切ってます。
ダウンロードしてきたcsvデータに毎回ゴミ列が含まれていたのでそれを利用しました。
データを整理
var cleanedData = CleanUp(dataList).ToList();
private static List<List<string>> CleanUp(IReadOnlyList<IReadOnlyList<string>> origin)
{
var clone = new List<List<string>>();
foreach (var data in origin.Where(value => !string.IsNullOrEmpty(value[0])))
{
var list = data.Select(s => s.Trim(new[]
{
'"'
})).ToList();
clone.Add(list);
}
return clone;
}
1列目が空になっている行を削除。
1列目以外はわざと空欄にしてるかもしれないので消さない。
csvの生のデータを読んだ時に「"」がすごい邪魔そうな気がしたのでTrimしたが、要らないかも。
変数名を取得
var header = cleanedData[0];
header.RemoveAll(string.IsNullOrEmpty);
1行目は絶対こんな感じのヘッダーになっていると思うのでそれを取得。
空欄を削除。
Dictionaryに変換
var dicList = new List<Dictionary<string, object>>();
foreach (var currentRow in cleanedData.Skip(1))
{
var dic = new Dictionary<string, object>();
for (int i = 0; i < header.Count; i++)
{
var dataString = currentRow[i];
var key = header[i];
if (int.TryParse(dataString, out var result))
{
dic.Add(key, result);
continue;
}
dic.Add(header[i], dataString);
}
dicList.Add(dic);
}
return dicList;
データの1行目はさっきのヘッダーになっているのでスキップ。
要素を取っていって、intにパースできそうならパースする。改造すればfloatにもできるかも。
実際に使っているコード
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Sirenix.OdinInspector;
using SpellProject.Utility;
using Takenokohal.Utility;
using UnityEditor;
using UnityEngine;
namespace SpellProject.Data.Database
{
public abstract class DatabaseBase<T> : SerializedScriptableObject
where T : IKeyHoldingData
{
protected abstract DatabaseURL.SheetName GetSheetName();
[SerializeField] private List<T> data;
public IReadOnlyList<T> DataList => data;
public T FindByKey(string key) => data.First(value => value.Key == key);
public T FindByIndex(int index) => data[index];
public int GetIndex(string key) => data.FindIndex(value => value.Key == key);
#if UNITY_EDITOR
public async UniTask UpdateAsync()
{
data.Clear();
var v = await SpreadSheetLoader.LoadData<T>(DatabaseURL.GetDataURL(GetSheetName()));
data = v.ToList();
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
}
#endif
}
}
using SpellProject.Utility;
using UnityEngine;
namespace SpellProject.Data.Database
{
[CreateAssetMenu(menuName = "Create AttackDatabase", fileName = "AttackDatabase", order = 0)]
public class AttackDatabase : DatabaseBase<AttackData>
{
protected override DatabaseURL.SheetName GetSheetName()
{
return DatabaseURL.SheetName.Attack;
}
}
}
実際に自作ゲームで使ってるコード。
UpdateAsynceを呼んだらデータベースが更新されるようにしてる。