【備忘録】ScriptableObjectとCSVの連携・「転生女騎士、アパートに住む【放置ゲーム】」の作り方(3)【Unity】
2024/2/20リリースしました!
↓iOS版
↓GooglePlay版
↓YouTube(PV)
ScriptableObjectとCSVの連携
セリフのデータを格納するSarifuDataはScriptableObjectで作成しましたが、設定することが多く、セリフの数も多いため、いちいちインスペクタで設定するのは手間がかかりすぎます。そのためcsvファイルからデータを流し込めるようにしました。
using System;
using UnityEngine;
using Random = UnityEngine.Random;
[CreateAssetMenu(fileName = "セリフ.asset", menuName = "Serifu")]
public class SerifuData : ScriptableObject{
[System.Serializable]
public class Serifu{
[Header("表情")]
public string faceType;
[Header("セリフ音声")]
public AudioClip audioClip = default;
[Header("セリフテキスト")]
public string _string = default;
[Header("条件1・レベルがこれ以上ならOK")]
public int _minlevel = 0;
[Header("条件1レベルがこれ以下ならOK")]
public int _maxlevel = 20;
[Header("条件2を適用するか、適用した場合の条件は")]
public SecondJudge secondJudge;
[Header("条件2-1・この服の画像が表示されているならOK")]
public Clothes clothes;
[Header("条件2-2・この時刻以降ならOK")]
public int _minTime;
[Header("条件2-2・この時刻以前ならOK")]
public int _maxTime;
[Header("条件2-3,2-4・この月以降ならOK")]
public int _minMonth;
[Header("条件2-3,2-4・この月以前ならOK")]
public int _maxMonth;
[Header("条件2-4・この日以降ならOK、月も設定する必要あり")]
public int _minDay;
[Header("条件2-4・この日以前ならOK、月も設定する必要あり")]
public int _maxDay;
[Header("条件3・この食べ物を食べたならOK")]
public Food food;
}
public Serifu[] serifus; // Serifuのインスタンスの配列
public Serifu GetRandomSerifu(){
// serifus配列からランダムなSerifuを選択して返す
int randomIndex = Random.Range(0, serifus.Length);
return serifus[randomIndex];
}
}
イメージは以下の記事のとおりです。数値と文字列のみの登録であれば、記事のとおりで問題ないでしょう。
また以下の記事のとおり、enum型を読み込むこともできます。
なおcsvファイルを保存するときにファイルの種類は「CSV(コンマ区切り)」ではなく、「CSV UTF‐8(コンマ区切り)」で保存しないと、Unity側で読み取れません。
今回のScriptableObjectではAudioClipを指定する必要があります。スクリプトから直接ファイルを読み込む場合は、Resourcesフォルダにファイルを入れる必要があるのですが、それではResourcesフォルダに大量のファイルを入れることになり、ファイルの管理がしにくい状態になるので、以下の記事を参考にResourcesフォルダでないフォルダからもファイルを読み込めるようにします。
エディタ拡張クラス【NonResources】
NonResourcesの活用例
以上を参考にし、ChatGPTにより作成してもらったスクリプトが以下のとおりです。
CSVLoaderクラス
using UnityEngine;
using UnityEditor; // AssetDatabaseを使用するために必要
using System;
using System.Collections.Generic;
using System.IO;
public class CSVLoader : MonoBehaviour
{
public SerifuData serifuData;
[Header("Resourcesフォルダに置いてあるcsvファイル名(拡張子不要)")]
public string csvFilePath; // "Assets/Resources/" は含めない
[Header("AudioClipが格納されているディレクトリのパス (プロジェクト内のAssetsフォルダからの相対パス)")]
public string audioClipDirectoryPath; // "Assets/" から始まる相対パス
void Start()
{
// ボタンのクリックイベントにLoadCSVメソッドを登録
GetComponent<UnityEngine.UI.Button>().onClick.AddListener(LoadCSV);
}
public void LoadCSV()
{
// CSVファイルのフルパスを指定
string fullPath = Path.Combine(Application.dataPath, "Resources", csvFilePath + ".csv");
if (!File.Exists(fullPath))
{
Debug.LogError("CSV file not found at path: " + fullPath);
return;
}
// CSVファイルから全ての行を読み込む
string[] lines = File.ReadAllLines(fullPath);
List<SerifuData.Serifu> loadedSerifus = new List<SerifuData.Serifu>();
// 最初の行(ヘッダー)をスキップするために、インデックス1から開始
for (int i = 1; i < lines.Length; i++)
{
string line = lines[i];
string[] values = line.Split(',');
SerifuData.Serifu newSerifu = new SerifuData.Serifu
{
faceType = values[0],
// AudioClipのパスを生成してロード
audioClip = values[1] != "" ? AssetDatabase.LoadAssetAtPath<AudioClip>($"Assets/{audioClipDirectoryPath}/{values[1]}") : null,
_string = values[2].Replace("<br>", "\n"),
_minlevel = int.Parse(values[3]),
_maxlevel = int.Parse(values[4]),
secondJudge = (SecondJudge)Enum.Parse(typeof(SecondJudge), values[5]),
clothes = (Clothes)Enum.Parse(typeof(Clothes), values[6]),
_minTime = int.Parse(values[7]),
_maxTime = int.Parse(values[8]),
_minMonth = int.Parse(values[9]),
_maxMonth = int.Parse(values[10]),
_minDay = int.Parse(values[11]),
_maxDay = int.Parse(values[12]),
food = (Food)Enum.Parse(typeof(Food), values[13])
};
loadedSerifus.Add(newSerifu);
}
// 読み込んだデータをSerifuDataに設定
serifuData.serifus = loadedSerifus.ToArray();
}
}
注意点
このスクリプトは、Unityエディタでのみ動作します。AssetDatabaseはビルドには含まれないため、エディタ拡張機能として使用する必要があります。
csvFilePathはResourcesフォルダからの相対パスで、ファイル名のみを指定します(例: "myData/myCsvFile")。
audioClipDirectoryPathはAssetsフォルダからの相対パスで、AudioClipが格納されているディレクトリを指定します(例: "Audio/Clips")。
AudioClipのファイル名は、CSVファイル内で拡張子なしで指定し(例: "myAudioClip")、コード内で拡張子(例: ".wav")を追加しています。
つまり、取り込むファイルによって修正が必要です。
AudioClipを読み込む行では、AssetDatabase.LoadAssetAtPathを使用して、指定したパスからAudioClipを読み込んでいます。存在しない場合はnullが割り当てられます。
CSVLoader使用方法
(1) CSVLoaderを適当なボタンにアタッチする
このボタンはcsvデータ取り込み時にしか使用しないため、普段はSetActive=falseにしておき、Unityエディタ上で再生してから、SetActiveをtrueにしてクリックするか、「EditorOnly」タグをつけておきましょう。デバッグ用のボタンやオブジェクトも「EditorOnly」タグをつけると便利です。
自分は「EditorOnly」の空フォルダを作り、デバッグ用のボタンやCSVLoaderはこの中にすべて入れています。
(2)CSVデータを作成する
csvデータを作成します。
ポイントとしては、ボイスデータを1つのフォルダに格納し(今回は「Asset\05Audio\VoiceSE\雑談」に入れました。)、エクスプローラーで開きます。
取り込みたいボイスデータを全て選んで、Shiftキーを押しながら、右クリックし、パスのコピーを選択します。
COEIROINKで連続したボイスデータを出力すると、自動採番されるので便利です。
なお、ボイスデータを追加する場合は「Flexible Renamer」を使って、既存のボイスデータの次の番号から追加ボイスデータの採番するといいです。
パスのコピーをCSVファイルのaudioClipの列に貼り付けます(1行目は取り込まれないので、2行目以下に)。
D:\Unity_\Project\OnnaKishi_Houchi_\Assets\05Audio\VoiceSE\雑談\ まではいらないので、置換で消去します。
その他の列の情報を入力します。String(文字列)で改行を行いたい場合は<br>を入れると改行されます。
全て入力できたら、csvフォルダを保存し、閉じて、Resourcesフォルダに設置します。
(3)インスペクタ上で取り込み設定を行う
インスペクタに更新するScriptableObjectのSerifuData、Resourcesフォルダに設置したcsvファイル名、AudioClipを設置しているフォルダのパス(Assets\は含めない)を設定します。
(4)Unityエディタを再生し、CSVLoaderをアタッチしたボタンをクリックする
エディタを再生し、ボタンをクリックするとScriptableObjectにCSVファイルのデータが流し込まれます。
今作ではAudioClipを読み込んでいますが、ファイルのパスを使用しているので、Sprite(画像)等でも同じです。
注意事項
//以下のコードはビルド時はコメントアウトすること。
audioClip = values[1] != "" ? AssetDatabase.LoadAssetAtPath<AudioClip>($"Assets/{audioClipDirectoryPath}/{values[1]}") : null,
理由は、AssetDatabaseクラスはUnityのエディタスクリプトでのみ使用可能で、ビルドされたゲーム内では使用できないからです。
AssetDatabaseを使用するスクリプトは、Unityエディタの機能拡張として機能するため、Editorフォルダ内に配置する必要があります。しかし、MonoBehaviourを継承したクラス(例えば、ゲームオブジェクトにアタッチされるスクリプトなど)は、Editorフォルダ内に置くことはできません。
ですが、今回は「ボタンをクリックするとデータが更新される。」という使い方をしているため、MonoBehaviourを継承する必要があります。もっといい方法があるかもしれませんが、このやり方でcsvを利用する場合は、ビルド時にパスを流し込む部分をコメントアウトしてください。