Edited at

【Unity】セリフやステータスなど大事な情報をGoogleスプレッドシートだけで管理する【GAS】

More than 1 year has passed since last update.


記事執筆時の動作環境

Windows10Pro(64bit)

unity2017.1、2017.2、2017.3


完成版

ボタン一つでスプレッドシートのマスタをScriptableObjectとして流し込みます。

afda7c12b435c764cf792fb432111d8d.gif

ダウンロードはこちら


はじめに

 ゲームを作っていると、たくさんのデータをやり取りする必要が出てきます。

SpreadSheet.png

RPGだと村人のセリフや武器のステータス、道具の効果など。

RPGでなくても、敵のステータスやプレイヤーのステータス、各ステージの情報など、様々なまとまったデータを保持する必要が出てきます。いわゆるマスタ管理です。

 そこで、そういったまとまったデータをなるべく楽に管理できるシステムを作ってみました。


なぜGoogleスプレッドシートで管理するのか

 楽だからです。

例えば敵のステータスを管理するとします。ゲームが小規模なうちは、敵の種類ごとにPrefabを作って、Inspectorでステータスを決めていくこともあるでしょう。

 しかし、Prefabでステータス管理をすると、他の敵ステータスとの比較が面倒です。おまけに、仕様が大きくなるにつれてInspectorがどんどん煩雑になります。

また、Prefabの変更はGitと相性が悪いです。ゲームジャムでもない限りお勧めできない方法だと思います。

EnemyControllerStatus.png

※昔エターナったRPGで使っていたEnemyController。今読むと訳が分かりません。

 ほかにも、よく記事でも見かける方法として、Excelなどの外部テキストファイルを用意し、Resourcesなどから読み込むという方法もあります。しかし、わたしはExcelには過去に散々苦しめられてきたので、もうアプリを開きたくもありません。おまけに、管理方法によってはCSVに変換しなきゃいけなかったり、名前を付けて保存しなければなりません。面倒です。

 Googleスプレッドシートなら!!

名前を付けて保存する必要はありません。自動で保存されます。また、表形式でデータを記述できるので、データごとの比較は一目瞭然です。文字コードの心配もいりません。

たぶんこれが一番楽だと思います。


どうやってスプレッドシートとUnityを連携させるのか

 流れとしては、

1. スプレッドシートでマスタを用意する

2. GoogleAppScript(以下GAS)でスプレッドシートの内容をJson形式で返す関数を作る

3. Unity側から上記の関数にアクセスし、返ってきたJsonをScriptableObjectに流し込む

というものです。順を追って説明していきます。


必要なもの


  • UniRx:言わずと知れた最強アセット。今回はすこしマニアック(?)な用途です。まだ持ってない方はこちら

  • JsonHelper:Json配列をJsonUtilityで扱えるようにします。こちらからいただきました。


GASでスプレッドシートの内容を返す関数を作る

※2018年2月執筆時点のバージョンでの解説をしています。解説と食い違う箇所は他の記事などを参考にしてください

 まずはスプレッドシート側の準備から行います。こんな感じでマスタを用意しましょう。

spreadsheet_master.png


  • 一行目に変数の定義

  • シート名にマスタ名

の二点だけ注意してください。

 次にこのマスタをUnity側で受け取れるコードを書きます。スプレッドシート上部のメニューバーの「ツール」→「スクリプトエディタ」を選択して開きます。

 エディタを開いたらコードを書きます。以下サンプルです。


コード.gs

function doGet(e) {

var sheetName = e.parameter.sheetName;
 // URL部分に自分で作ったスプレッドシートのURLを入れます
var ss = SpreadsheetApp.openByUrl('URL');
var sheet = ss.getSheetByName(sheetName);
if(sheet != null){
var json = ConvertSheetToJson(sheet);
return ContentService.createTextOutput(JSON.stringify(json))
.setMimeType(ContentService.MimeType.JSON);
}
else
{
Logger.log("NoSheets here");
Browser.msgBox(Logger.getLog());
}
}

function ConvertSheetToJson(sheet)
{
// 抽出したい列の数。最初の列は定義なのでスキップ。
var recordSize = sheet.getLastRow()-1;
// 抽出したい行の数
var columnSize = sheet.getLastColumn();

// シートの中身を2列目1行目から文字列で抽出。
var json = sheet.getRange(2, 1, recordSize, columnSize).getValues();

// 出力するJSON。一つ一つの値に定義を埋め込んでいく。
var jsonArray = [];
for(var row = 0; row < recordSize; row++)
{
var line = json[row];
var obj = new Object();
// 変数の定義部分を取得
var valueColumns = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
for(var column = 0; column < columnSize; column++)
{
// 定義をJsonの要素に注入
obj[valueColumns[column]] = line[column];
}
jsonArray.push(obj);
}
return jsonArray;
}


function doGet(e)というのは、GASのテンプレ関数だそうです。

今回は、作成したシートを引数にして、そのシート内のデータをJson形式にまとめたものを返してもらいます。

function ConvertSheetToJson(sheet)は自作の関数です。シートの任意の範囲のデータを取得して、一つずつJson配列にデータを入れていっています。

ミソは

  // シートの中身を2列目1行目から文字列で抽出。

var json = sheet.getRange(2, 1, recordSize, columnSize).getValues();

の部分です。

一行目はJsonに入れるデータではなく、値の定義なので、一行スキップしています。

この定義の部分は、

    // 変数の定義部分を取得

var valueColumns = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];

で使用しています。定義部分と対応が取れるように各データを取得するためです。

なので定義を日本語にしてはいけません。

コードが書けたら、コードを公開し、外部からアクセスできるようにします。

公開の仕方についてはこちらなどが参考になるかと思います。

GASの解説は面倒複雑になるので、本稿では割愛いたします。上記記事はかなりこの記事の参考にしたので、とても参考になるかと思います。


Unity上でGASの関数にアクセスする

 さて、いよいよUnityとGoogleスプレッドシートとのつなぎ込みです。

まずはスプレッドシートで作ったマスタのデータに対応するクラスを作ります。

[System.Serializable]属性を付与することで、Jsonからクラスに流し込めるようにします。


Enemy

/// <summary>

/// マスタ管理するデータの一単位。
/// 変数は必ずスプレッドシートのマスタと同じにする
/// </summary>
[System.Serializable]
public class Temp {
public int id;
public string tempText;
}

次に、スプレッドシートのデータを注入するコードを用意します。

コードは以下のような感じです。(あくまで一部です)


MasterLoader.cs

using UnityEditor;

using UniRx;

namespace MasterLoader
{

  public enum MasterType
{
Temp,
}

[InitializeOnLoad]
public class MasterLoader : Editor
{

const string URL = "URL";

/// <summary>
/// スプレッドシートからマスタを取得する
/// </summary>
public static void LoadMaster(MasterType masterType)
{
var sheetName = GetSheetName(masterType);
var url = URL + "?sheetName=" + sheetName;
       // UniRxの関数。後述。
ObservableWWW.GetWWW(url)
.Subscribe(www =>
{
var Json = JsonHelper.ListFromJson<TempMaster>(www.text);
});
}
}

    /// <summary>
/// マスタのタイプからシート名を返す。シート名はスプレッドシートのシート名を入れる。
/// </summary>
/// <param name="masterType">マスタの種類</param>
/// <returns>シート名</returns>
static string GetSheetName(MasterType masterType)
{
switch (masterType)
{
case MasterType.Temp:
return "TempMaster";
// TODO: ここにマスタを追加したときにそのシート名を追記していく
default:
return string.Empty;
}
}
}


 これでスプレッドシートの情報をUnity側で取得できるようになりました。

Editorを継承しているのは、この後Editor拡張したいからです。

 ObservableWWW.GetWWW(url)についてですが、

これはUniRxで用意されている関数です。

UniRxは、Monobehaviourの機能を強力に拡張してくれますが、Monobehaviourを継承しなくてもコルーチンのような振る舞いをしてくれるという特長もあります。

このコードでは、その特長を生かし、Editor拡張コードでもコルーチンのような振る舞いを行っています。

すなわち、スプレッドシートのマスタにアクセスし、値が返るまで待ち、値が返ってきたら var Json に注入しています。

 さて、次はこの値をUnity上でいつでも参照できるように保持します。


ScriptableObjectを用いてマスタをUnity内に保持する

 本稿では、マスタの値の保持にScriptableObjectを採用しました。

staticな値として保持しやすいですし、エディタ拡張によってコードから直接作成できたりと、取り回しがしやすいからです。

 この辺は好みだと思いますので、これ以降はお好きなやり方で実装してしまってもいいと思います。

 さて、ScriptableObjectで実装する場合は、まずはScriptableObjectにするためのクラスを用意します。クラス名は、マスタのシート名と同じにします。


TempMaster.cs

using UnityEngine;

public class TempMaster : ScriptableObject{
public List<Temp> TempList;
}

先ほどのMasterLoader.csも、以下のように追記します。


MasterLoader.cs


namespace MasterLoader
{
public enum MasterType
{
Temp,
}

/// <summary>
/// スプレッドシートからマスタを取得してScriptableObjectに流し込むクラス
/// </summary>
[InitializeOnLoad]
public class MasterLoader : Editor
{
/// <summary>
/// マスタのURLは不変なのでconstにして編集できないようにしておく
/// URLはスプレッドシートのコードを公開したときに表示されるものを入れる。
/// </summary>
const string URL = "GASのコード公開時に取得できるURL";
/// <summary>
/// マスタを配置するパス。ResoucesディレクトリとMasterディレクトリをあらかじめ作成しておく
/// </summary>
const string path = "Assets/MasterLoader/Resources/Master/";

/// <summary>
/// アプリ起動時に自動でScriptableObjectを更新する
/// </summary>
static MasterLoader()
{
LoadMaster(MasterType.Temp);

// ゲームプレビュー終了時に自動でScriptableObjectを更新する
EditorApplication.playModeStateChanged += (state) =>
{
if (state == PlayModeStateChange.EnteredEditMode)
{
// スプレッドシートのマスタのシート名を引数に入れる
LoadMaster(MasterType.Temp);
// TODO: 今後マスターが増えるたびにここに追記していくと幸せになれる
}
};
}

/// <summary>
/// スプレッドシートからマスタを取得する
/// </summary>
public static void LoadMaster(MasterType masterType)
{
var sheetName = GetSheetName(masterType);

var url = URL + "?sheetName=" + sheetName;
ObservableWWW.GetWWW(url)
.Subscribe(www =>
{
var Json = JsonHelper.ListFromJson<Temp>(www.text);
if (Json != null)
{
// すでにマスタが作成されているかを確認するために取得してみる
var master = AssetDatabase.LoadAssetAtPath<TempMaster>(path + sheetName + ".asset");
if (master == null)
{
// マスタが取得できなければマスタを新規作成する
master = CreateInstance<TempMaster>();
AssetDatabase.CreateAsset(master, path + sheetName + ".asset");
AssetDatabase.Refresh();
}
// マスタは不変の値なので、Unityでは編集できないようにする
master.hideFlags = HideFlags.NotEditable;
// Jsonの値をScriptableObjectに流し込む
master.TempList = Json;
Debug.Log(sheetName + " load has completed");
}
else
{
// Jsonの取得に失敗している
Debug.LogError(www.text);
}
});
}

/// <summary>
/// マスタのタイプからシート名を返す。シート名はスプレッドシートのシート名を入れる。
/// </summary>
/// <param name="masterType">マスタの種類</param>
/// <returns>シート名</returns>
static string GetSheetName(MasterType masterType)
{
switch (masterType)
{
case MasterType.Temp:
return "TempMaster";
// TODO: ここにマスタを追加したときにそのシート名を追記していく
default:
return string.Empty;
}
}
}
}


これでようやくつなぎ込みが完了しました。

しかし、まだ肝心の LoadMaster(masterType) を呼び出すイベントを作っていません。

 本稿では、エディタ拡張によって、いつでもUnityEditorからボタン一つでマスタを更新できるようにします。

MasterLoaderはそのためにEditorを継承しておきました。


いつでもUnityEditorでマスタ更新できるボタンを作る

 Editor拡張を行うために以下コードを作成します。


MasterWindow.cs

using UnityEditor;

namespace MasterLoader
{
public class MasterLoadWindow : EditorWindow
{
[MenuItem("Window/MasterLoader")]
static void Open()
{
GetWindow<MasterLoadWindow>();
}

void OnGUI()
{
EditorGUILayout.Space();

if (GUILayout.Button("Temp", GUILayout.Width(80.0f)))
{
// Tempボタンを押したときにTempマスタを更新する
MasterLoader.LoadMaster(MasterType.Temp);
}

EditorGUILayout.Space();
}
}
}


これにより、UnityEditorのツールバーのWindow内に「MasterLoader」という項目が追加されます。

項目をクリックすると、ボタンの置かれたウィンドウが現れます。そのボタンを押すと、

MasterLoader.LoadMasterが実行され、マスタの更新が行われます。

afda7c12b435c764cf792fb432111d8d.gif

これでマスタ管理の全ての準備が整いました!


おまけ:もっと便利に運用しよう

 さて、上記までの内容により、ボタン一つでスプレッドシート内の任意のマスタを更新するシステムが完成しました。

 しかし、たぶんもっと楽になれると思います。

このシステムの問題点は、スプレッドシートの更新がUnity側からは通知されないということです。

うっかりマスタ更新を忘れて、古いバージョンのマスタを使い続けてしまった…などのリスクが考えられます。

 とはいえ、スプレッドシートの更新をUnity側に通知しようとするのは面倒くさそうです。楽したいです。

ということで、定期的に自動でUnity側でマスタ更新するという方法を採りました。

たぶんこれが一番楽だと思います。

まずは、MasterLoader.csのclassのところに[InitializeOnLoad]属性を追加します。

[InitializeOnLoad]

public class MasterLoader : Editor

これにより、Unityエディタ起動時にマスタ更新を行います。

さらに、MasterLoader.csにまたまた以下関数を追記します

/// <summary>

/// アプリ起動時に自動でScriptableObjectを更新する
/// </summary>
static MasterLoader()
{
LoadMaster(MasterType.Temp);

// ゲームプレビュー終了時に自動でScriptableObjectを更新する
EditorApplication.playModeStateChanged += (state) =>
{
if (state == PlayModeStateChange.EnteredEditMode)
{
// スプレッドシートのマスタのシート名を引数に入れる
LoadMaster(MasterType.Temp);
// TODO: 今後マスターが増えるたびにここに追記していくと幸せになれる
}
};
}

書いてある通りですが、EditorApplicationを利用することによって、UnityEditor内の様々なイベントに処理を滑り込ませることができます。

本稿では、エディタ上でゲームを再生し終わったときにLoadMasterを実行するようにしました。

これなら頻繁に更新されます。

 ゲーム実行開始時にしなかった理由は、マスタの更新は非同期処理なので、マスタの更新が未完了のままプレビューが開始してしまうことを回避するためです。(こういったケースがあるかはわかりませんが、マスタが大きくなるとありえそうです)


まとめ

 GASとエディタ拡張を活用することにより、個人的には満足のいくマスタ管理システムが完成しました。

導入には何かと大変かと思いますが、その後レベルデザインなどでちょくちょく値をいじる際にはかなり楽なものになったかと思います。

 新たなマスタを作る場合は、以下の手順を踏みます。

1. スプレッドシートに新しいマスタのシートを用意する

2. シート名をそのマスタの名前にする

3. Unity側で、マスタの1単位分のクラスとScriptableObject用のクラスを作る(例:Enemy.csとEnemyMaster.cs)

4. MasterLoader.csに、新たなマスタに対応する処理を追加する

5. MasterWindow.csに、新たなマスタに対応するボタン処理を追加する

6. (optional)MasterLoader.csに、エディタプレビュー時に自動実行する処理を追加する

…まだ楽できそうですね。のんびりアプデしていこうと思います。

さて、満足いくシステムではあるのですが、長所短所はあります。

最後にそれらをまとめて本稿を締めようと思います。

MasterLoaderでできること


  • スプレッドシートを用いたマスタ管理

  • ボタン一つの簡単更新

  • エディタの再生に紐づいた自動更新

MasterLoaderでできないこと


  • null許容型(int?とかそういうやつ)の変数の定義

  • class, List<>型などの変数の定義

MasterLoaderの苦手そうなこと(未検証)


  • 膨大な量のデータのやり取り

  • 膨大な種類のマスタのやり取り(1マスタごとに通信処理をやり直しているため)