【Unity】月別のデータを反映する

ようやく体重管理アプリが完成しました。
時間掛かり過ぎなんですが… (;´・ω・)

月別のデータ反映で悩み…
グラフ化の際、歯抜けのデータ処理で悩み…
あれもこれも追加…
最後はバグ発生…

ようやくって感じでっす(ノ_<。)うっうっうっ

運用開始4日目ですが、今の所問題もなく動いております。

入力10日目と30日目に処理を挟んでいるので、
そこでバグが出ない事を祈っているのですがどうなるでしょう(;^_^A

この話はこれくらいで、
体重管理アプリを作っている時に調べた事や作った物を
少しの間、記事にしていこうと思います。

まず、月別のデータを読み込んで表示させる所をまとめてみます。
今回、データはCSV形式で保存・読込みを行いました。

以前にも記事にしたので、
またかと言われそうですが…
そんな声は無視するとして (;^o^) \(ToT )あんたほんとにそれでいいの


まず、以前のデータの扱いは表示月のみって事でした。
CSVファイルを一つに絞れたので、簡単だったのですが…

今回は、カレンダーの月が変えられるので、
月別に表示する必要があります。

なので、全データを読み込んで、月別に分類しておく必要があります。

この分類方法なんですが、
・一つのCSVファイルに月毎のデータを行単位で分類する
・月毎のファイルを作成して別々に分類する

二つの方法を思いつきました。

しかし、どちらも行であれファイルであれ
月を把握する事が重要になります。

順番に読み込んで行って、当月から逆算すれば
何番目が何月と読み込む事ができそうなんですが…

この時、歯抜けの月が出来たら逆算ができないと気付いて、
歯抜けの月をどうやって判別するか悩みました。

行単位で分類すると判別しにくいのでパス!
ファイル単位なら歯抜け月のファイルが無いだけなので、
分類も楽にできるだろうと思って、ファイル分類を選択する事に。

まず、分類するのにファイル名を日付にしてしまえば問題ないので、
読込み年月をファイル名にしてみました。

例えば、この記事を書いているのが、2021/1/26なので、
この日に読込みを行う場合、年月の部分20211をファイル名にしておけば、
読み込む事ができるかと思います。


この辺りを踏まえて、少し前に記事にした
【Unity】カレンダーを作り直してみた
このカレンダーに組み込んでみたいと思います。

まず、読込み用のオブジェクト・スクリプトを追加します。
追加したものをDataBaseにリネームして、スクリプトをアタッチしておきます。
月別読込み⑥.jpg
CSVファイルはResourcesに保存して、DataBaseから読み取りに行かせます。

準備ができたらCSVファイルの読込みから作っていくのですが…
読込みって、ファイル名を指定しないと読込めないんですよね~ ヽ(´~`;)ウーン

Resourcesの中に、何年何月のファイルがあるか取得する必要があるので、
ファイル名を取得するメソッドを作成します。

using System.IO;

private List<string> fileName = new List<string>(); //ファイル名リスト
private const string extension = ".csv"; //拡張子
private const string folderRoot = "/Resources"; //Resourcesのルート

//フォルダ内のファイル名を取得する
private void GetFileName()
{
string filePath = Application.dataPath + folderRoot;

//フォルダ内のcsvが付いたファイル名をピックアップする
string[] temp = Directory.GetFiles(filePath, "*" + extension);

//ピックアップしたファイル名をList変数に格納する
foreach (var s in temp)
{
fileName.Add(Path.GetFileName(s));
}
}

こんな感じで取得すれば、ファイル名をピックアップできます。
しかし、このメソッドでは、ファイルが無い場合、
nullエラーで止まってしまうんですよね~ (~ヘ~;)ウーン

ファイルを用意している場合は、問題ないのですが、
アプリ内でファイルを作成する場合、最初はファイルが存在しないので、
読込みエラーが発生します。

なので、ファイルの存在確認が必要になってきます。

ファイルの存在はFile.Existsで取得できるのですが…
ファイル名を指定しないとダメなので、ファイル名が無いと話にならない orz

そこで、Google先生に泣きつく事に (T-T) センセイ

コガネブログさん
【C#】フォルダが空かどうか調べる関数
有益な情報ありがとうございます。
この場を借りてお礼申し上げますm(_ _)m

この関数を使えば、フォルダ内が空か確認できるので導入します。
DataBase

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

/// <summary>
/// フォルダ内が空か確認する拡張クラス
/// </summary>
public static class DirectoryUtils
{
public static bool IsEmptyDirectory(string path)
{
if (!Directory.Exists(path)) return false;

try
{
var entries = Directory.GetFileSystemEntries(path);
return entries == null || entries.Length == 0;
}
catch
{
return false;
}
}
}


/// <summary>
/// DataBaseクラス
/// </summary>
public class DataBase : MonoBehaviour
{

あとはコガネブログさんが使い方を紹介されてるので、
ファイル名取得のメソッドに組み込みます。
DataBase

//フォルダ内のファイル名を取得する
private void GetFileName()
{
string filePath = Application.dataPath + folderRoot;

//指定フォルダ内にファイルが無ければロードしない
if (DirectoryUtils.IsEmptyDirectory(filePath))
{
Debug.Log("ファイル無いよ");
return;
}

//フォルダ内のcsvが付いたファイル名をピックアップする
string[] temp = Directory.GetFiles(filePath, "*" + extension);

//ピックアップしたファイル名をList変数に格納する
foreach (var s in temp)
{
fileName.Add(Path.GetFileName(s));
}
}

これでPLAYすると無事に”ファイル無いよ”がコンソールに表示されました。

これでファイル内が空でもエラーが掛からないので、
データロードを組んでいきます。
DataBase

private List<string[]> loadText = new List<string[]>(); //CSVデータ読み取り用

// Start is called before the first frame update
void Start()
{
GetFileName();
}

//フォルダ内のファイル名を取得する
private void GetFileName()
{
string filePath = Application.dataPath + folderRoot;

//指定フォルダ内にファイルが無ければロードしない
if (DirectoryUtils.IsEmptyDirectory(filePath))
{
return;
}

//フォルダ内のcsvが付いたファイル名をピックアップする
string[] temp = Directory.GetFiles(filePath, "*" + extension);

//ピックアップしたファイル名をList変数に格納する
foreach (var s in temp)
{
fileName.Add(Path.GetFileName(s));
}

//ファイル名取得後にデータロードを呼び出す
StartCoroutine(LoadCSV());
}

//ファイルの読込み
IEnumerator LoadCSV()
{
for (int i = 0; i < fileName.Count; i++)
{
//ファイル名から拡張子を外す
string reName = fileName[i].Replace(extension, "");

//リネームしたファイル名をTextAssetとして取り込む
TextAsset ta = Resources.Load(reName) as TextAsset;

//取得したTextをstringに変換してカンマ毎に切り分けloadTextに格納する
StringReader sr = new StringReader(ta.text);
string line = sr.ReadLine();

loadText.Add(line.Split(','));

yield return null;
}
}

こんな感じで存在するCSVデータをピックアップします。

注意点としては、TextAssetで読み込む場合、拡張子が付いてると読込めません。
なので、fileName[i].Replace(extension, "")で拡張子を外しています。

中身の説明は、スクリプト内のコメント通りです。

ファイルの読込みが完成したので、
UIとCalendarManagerの修正をしていきます。

まずUIからDayPanelのプレハブを修正します。
月別読込み①.jpg
DayPanelプレハブをFrameにアタッチして編集できるようにします。
月別読込み⑤.jpg
UIのテキストを追加してDisplayとリネームします。
このテキストがロードしたデータを表示する部分になります。

DayテキストとDisplayテキストの位置と大きさを調整します。
私はスクショの通りに調整しました。

DayPanelの変更はこれだけです。
あとは、元のDayPanelプレハブを削除して、修正後のDayPanelをプレハブ化します。
プレハブ化したあとのFrameに残っているDayPanelの削除をお忘れなく。
月別読込み④.jpg
プレハブのリンクが切れているので、CalendarManagerにアタッチし直します。

UIの準備ができたので、CalendarManagerを修正して行くのですが…

その前にお詫びを
CalendarManagerの中でCalendarDateを取得するのに
CalendarManager

using UnityEngine.UI;

[Header("カレンダーの年月テキスト")]
public GameObject calendarDate;

private Text calendarDateText = null; //カレンダー月表示テキスト

// Start is called before the first frame update
void Start()
{
//カレンダーの年月を表示する
calendarDateText = calendarDate.GetComponent<Text>();
calendarDateText.text = firstDay.ToString("yyyy年M月");
}

//表示月を変える処理
public void ChangeMonth(int month)
{
calendarDateText.text = firstDay.ToString("yyyy年M月");
}

無駄なGetComponentを使っているので訂正します。
訂正後は、
CalendarManager

using UnityEngine.UI;

[Header("カレンダーの年月テキスト")]
public Text calendarDate;

// Start is called before the first frame update
void Start()
{
//カレンダーの年月を表示する
calendarDate.text = firstDay.ToString("yyyy年M月");
}

//表示月を変える処理
public void ChangeMonth(int month)
{
calendarDate.text = firstDay.ToString("yyyy年M月");
}

今までホント申し訳ないです。_(._.)_ ユルシテ

変数を書き換えた場合、リンクが切れるのでアタッチをお忘れなく。

本題に戻って、データ表示を組んでいきます。
CalendarManager

public class CalendarManager : MonoBehaviour
{
[Header("DataBase本体")]
public GameObject dataBaseObj;

private DataBase dataBase = null; //DataBaseスクリプト


/// <summary>
/// メソッド
/// </summary>

// Start is called before the first frame update
void Start()
{
//DataBaseスクリプト取得
dataBase = dataBaseObj.GetComponent<DataBase>();


GetTheDate();

//カレンダーの年月を表示する
calendarDate.text = firstDay.ToString("yyyy年M月");


CreateDayPanel();
}

CalendarManagerからアクセスできるように
DataBaseスクリプトを取得しておきます。

続いて、DayPanel生成のメソッドにDisplayの取得を追加します。
CalendarManager

private Text[] displayText = new Text[42]; //データ表示テキスト

//DayPanelの生成
private void CreateDayPanel()
{
for (int i = 0; i < 42; i++)
{
GameObject createPanel = (GameObject)Instantiate(dayPanel);
createPanel.transform.SetParent(frame.transform, false);

//DayPanel内のコンポーネントを取得する
dayPanelColor[i] = createPanel.GetComponent<Image>();
dayText[i] = createPanel.transform.GetChild(0).GetComponent<Text>();
displayText[i] = createPanel.transform.GetChild(1).GetComponent<Text>();
}
}

Display用の変数を追加して、生成メソッドの一番下でコンポーネントを
取得させるようにしました。

最後は、データの取得なのですが、
・データロードが終わってから取得させる
・DataBaseは表示月のデータを渡す
・非表示(グレー)のDayPanelには表示しない

この3点が必要なので、データロード完了後にデータを渡せるように
DataBaseに追加をします。
DataBase

private bool readComplete = false; //読込みフラグ

//フォルダ内のファイル名を取得する
private void GetFileName()
{
string filePath = Application.dataPath + folderRoot;

//指定フォルダ内にファイルが無ければロードしない
if (DirectoryUtils.IsEmptyDirectory(filePath))
{
readComplete = true;
return;
}

(途中省略)

//ファイル名取得後にデータロードを呼び出す
StartCoroutine(LoadCSV());
}

//ファイルの読込み
IEnumerator LoadCSV()
{
for (int i = 0; i < fileName.Count; i++)
{

(途中省略)

yield return null;
}
//読込み完了フラグ
readComplete = true;
}

/// <summary>
/// CalendarManagerに公開する変数
/// </summary>

//ロード完了フラグ
public bool DataOnLoad
{
get { return readComplete; }
}

//データを渡す
public string DataPassing(string date, int dayNum)
{
string dataStr = string.Empty;
int listNum = fileName.IndexOf(date + extension);

if (listNum >= 0) dataStr = loadText[listNum][dayNum];

return dataStr;
}

フラグを追加して、ロード後にtrueにします。
ファイルが無い場合、フラグが立たないので、フォルダ内検索の関数にも
trueを仕込んでおきます。

データを渡すメソッドは、表示月とDayPanelのナンバーを引数にして、
それぞれのloadTextからデータをリターンできるようにしました。

fileName.IndexOfで取得しているファイル名が何番に格納されているのか分かります。
無い場合は、listNumに-1が代入されるので、0以上ならloadTextにアクセスして、
返り値として設定します。

データが無い場合は、空のstringがリターンされます。

フラグの設定とデータ渡しができるようになったので、
フラグ判別とデータ取得をCalendarManagerに追加します。
CalendarManager

private bool firstContact = false; //起動時のData取得フラグ

// Update is called once per frame
void Update()
{
if (dataBase.DataOnLoad && firstContact == false)
{
SetCalendarDate();
firstContact = true;
}
}

//カレンダーの日付をセットする
private void SetCalendarDate()
{
DayOfWeek firstWeek = firstDay.DayOfWeek; //月初の曜日を取得
int diff = 0; //1枠目との差を取得する変数

//月初の曜日から1枠目との差を割出す
if (firstWeek == DayOfWeek.Sunday) diff = (int)WEEK.sunday;
if (firstWeek == DayOfWeek.Monday) diff = (int)WEEK.monday;
if (firstWeek == DayOfWeek.Tuesday) diff = (int)WEEK.tuesday;
if (firstWeek == DayOfWeek.Wednesday) diff = (int)WEEK.wednesday;
if (firstWeek == DayOfWeek.Thursday) diff = (int)WEEK.thursday;
if (firstWeek == DayOfWeek.Friday) diff = (int)WEEK.friday;
if (firstWeek == DayOfWeek.Saturday) diff = (int)WEEK.saturday;
//差分を計算する
firstPoint = firstDay.AddDays(diff);



ここを追加
string tempDate = firstDay.Year.ToString() + firstDay.Month.ToString();

//各日付テキストに代入
for (int i = 0; i < 42; i++)
{
DateTime temp = firstPoint.AddDays(i);

//前月・次月は表示しない、日付パネルをグレーにする
if (temp < firstDay || temp >= nextMonth)
{
dayText[i].text = null;
dayPanelColor[i].color = gray;

ここを追加
displayText[i].text = string.Empty;
}
else
{
dayText[i].text = temp.Day.ToString();
dayPanelColor[i].color = Color.white;

ここを追加
displayText[i].text = dataBase.DataPassing(tempDate, i);
}

//曜日で文字色を変更する
switch (temp.DayOfWeek)
{
case DayOfWeek.Sunday:
dayText[i].color = Color.red;
break;

case DayOfWeek.Saturday:
dayText[i].color = Color.blue;
break;

default:
dayText[i].color = green;
break;
}
}
}

//表示月を変える処理
public void ChangeMonth(int month)
{
ここを追加
if (firstContact == false) return;

//表示月と次月の月初日を計算する
firstDay = firstDay.AddMonths(month);
nextMonth = firstDay.AddMonths(1);

//カレンダーの年月を表示する
calendarDate.text = firstDay.ToString("yyyy年M月");
//各テキストに日付を代入する
SetCalendarDate();
}

説明なんですが、
DayPanelの表示・非表示をSetCalendarDate()内で行っているので、
Display表示も連動するようにしてみました。

ただ、SetCalendarDate()は、Startで呼び出していたので、
データロードとは関係なく表示されてしまうので、
修正する事にしました。

Updateからフラグを監視して、データの読込みが完了した所で、
SetCalendarDate()を呼び出すようにしています。

また、カレンダーの切り替え操作(月変更)をロード完了前に
操作できると問題なので、firstContactフラグで初回データ取得までは、
操作できないようにしました。

このフラグを利用して、Update内のSetCalendarDate()も一度しか
呼び出されないように設定しています。

スクリプトが完成したので、ファイルを用意してPLAYしてみます。

ResourcesにCSVファイルを用意します。
月別読込み⑫.jpg
5つのファイルを用意してみました。
・201911.csv
・201912.csv
・202011.csv
・202012.csv
・20211.csv

上手く読み込めるか確認したいので、連続月と大きく離れた月を用意してみました。

ファイルの中身は、
月別読込み⑪.jpg
それぞれ、ファイル名にある年月をカンマ区切りで、1行42個並べています。
保存の際、UTF-8で保存すると読み取る事ができます。

PLAYの動画でっす。

画面が小さいので分かりにくいと思いますが、
無事、読込みから反映まで出来たので問題ないと思います。

最後に完成版のスクリプトを
CalendarManager

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

public class CalendarManager : MonoBehaviour
{
/// <summary>
/// オブジェクト・コンポーネント
/// </summary>

//パブリック
[Header("カレンダー枠")]
public GameObject frame;
[Header("カレンダーの年月テキスト")]
public Text calendarDate;
[Header("日付プレハブ")]
public GameObject dayPanel;
[Header("DataBase本体")]
public GameObject dataBaseObj;

//プライベート
private Image[] dayPanelColor = new Image[42]; //日付パネルのイメージ
private Text[] dayText = new Text[42]; //日付テキスト
private Text[] displayText = new Text[42]; //データ表示テキスト
private DataBase dataBase = null; //DataBaseスクリプト
private bool firstContact = false; //起動時のData取得フラグ


// 各種変数
private DateTime firstDay = DateTime.MinValue; //表示月の月初取得用
private DateTime nextMonth = DateTime.MinValue; //次月取得用
private DateTime firstPoint = DateTime.MinValue; //1枠目の日付取得用


//曜日ごとの補正値を設定しておく
private enum WEEK
{
sunday = 0,
monday = -1,
tuesday = -2,
wednesday = -3,
thursday = -4,
friday = -5,
saturday = -6,
}

//カラーコード設定
private Color green = new Color(0f, 0.4f, 0f, 1f);
private Color gray = new Color(0.9f, 0.9f, 0.9f, 1f);


/// <summary>
/// メソッド
/// </summary>

// Start is called before the first frame update
void Start()
{
dataBase = dataBaseObj.GetComponent<DataBase>();

GetTheDate();

//カレンダーの年月を表示する
calendarDate.text = firstDay.ToString("yyyy年M月");

CreateDayPanel();
}

// Update is called once per frame
void Update()
{
if (dataBase.DataOnLoad && firstContact == false)
{
SetCalendarDate();
firstContact = true;
}
}

//起動日から表示月・表示次月を取得する
private void GetTheDate()
{
DateTime temp = DateTime.Now.Date;
firstDay = new DateTime(temp.Year, temp.Month, 1);
nextMonth = firstDay.AddMonths(1);
}


//DayPanelの生成
private void CreateDayPanel()
{
for (int i = 0; i < 42; i++)
{
GameObject createPanel = (GameObject)Instantiate(dayPanel);
createPanel.transform.SetParent(frame.transform, false);

//DayPanel内のコンポーネントを取得する
dayPanelColor[i] = createPanel.GetComponent<Image>();
dayText[i] = createPanel.transform.GetChild(0).GetComponent<Text>();
displayText[i] = createPanel.transform.GetChild(1).GetComponent<Text>();
}
}

//カレンダーの日付をセットする
private void SetCalendarDate()
{
DayOfWeek firstWeek = firstDay.DayOfWeek; //月初の曜日を取得
int diff = 0; //1枠目との差を取得する変数

//月初の曜日から1枠目との差を割出す
if (firstWeek == DayOfWeek.Sunday) diff = (int)WEEK.sunday;
if (firstWeek == DayOfWeek.Monday) diff = (int)WEEK.monday;
if (firstWeek == DayOfWeek.Tuesday) diff = (int)WEEK.tuesday;
if (firstWeek == DayOfWeek.Wednesday) diff = (int)WEEK.wednesday;
if (firstWeek == DayOfWeek.Thursday) diff = (int)WEEK.thursday;
if (firstWeek == DayOfWeek.Friday) diff = (int)WEEK.friday;
if (firstWeek == DayOfWeek.Saturday) diff = (int)WEEK.saturday;
//差分を計算する
firstPoint = firstDay.AddDays(diff);

string tempDate = firstDay.Year.ToString() + firstDay.Month.ToString();

//各日付テキストに代入
for (int i = 0; i < 42; i++)
{
DateTime temp = firstPoint.AddDays(i);

//前月・次月は表示しない、日付パネルをグレーにする
if (temp < firstDay || temp >= nextMonth)
{
dayText[i].text = null;
dayPanelColor[i].color = gray;
displayText[i].text = string.Empty;
}
else
{
dayText[i].text = temp.Day.ToString();
dayPanelColor[i].color = Color.white;
displayText[i].text = dataBase.DataPassing(tempDate, i);
}

//曜日で文字色を変更する
switch (temp.DayOfWeek)
{
case DayOfWeek.Sunday:
dayText[i].color = Color.red;
break;

case DayOfWeek.Saturday:
dayText[i].color = Color.blue;
break;

default:
dayText[i].color = green;
break;
}
}
}

//表示月を変える処理
public void ChangeMonth(int month)
{
//初回データ読込みまでは操作できなくする
if (firstContact == false) return;

firstDay = firstDay.AddMonths(month);
nextMonth = firstDay.AddMonths(1);
calendarDate.text = firstDay.ToString("yyyy年M月");
SetCalendarDate();
}
}


DataBase

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

/// <summary>
/// フォルダ内が空か確認する拡張クラス
/// </summary>
public static class DirectoryUtils
{
public static bool IsEmptyDirectory(string path)
{
if (!Directory.Exists(path)) return false;

try
{
var entries = Directory.GetFileSystemEntries(path);
return entries == null || entries.Length == 0;
}
catch
{
return false;
}
}
}


/// <summary>
/// DataBaseクラス
/// </summary>
public class DataBase : MonoBehaviour
{
//各種変数
private List<string> fileName = new List<string>(); //ファイル名リスト
private List<string[]> loadText = new List<string[]>(); //CSVデータ読み取り用
private const string extension = ".csv"; //拡張子
private const string folderRoot = "/Resources"; //Resourcesのルート
private bool readComplete = false; //読込みフラグ


/// <summary>
/// メソッド
/// </summary>

void Start()
{
GetFileName();
}

//フォルダ内のファイル名を取得する
private void GetFileName()
{
string filePath = Application.dataPath + folderRoot;

//指定フォルダ内にファイルが無ければロードしない
if (DirectoryUtils.IsEmptyDirectory(filePath))
{
readComplete = true;
return;
}

//フォルダ内のcsvが付いたファイル名をピックアップする
string[] temp = Directory.GetFiles(filePath, "*" + extension);

//ピックアップしたファイル名をList変数に格納する
foreach (var s in temp)
{
fileName.Add(Path.GetFileName(s));
}
StartCoroutine(LoadCSV());
}

//ファイルの読込み
IEnumerator LoadCSV()
{
for (int i = 0; i < fileName.Count; i++)
{
//ファイル名から拡張子を外す
string reName = fileName[i].Replace(extension, "");

//リネームしたファイル名をTextAssetとして取り込む
TextAsset ta = Resources.Load(reName) as TextAsset;

//取得したTextをstringに変換してカンマ毎に切り分けloadTextに格納する
StringReader sr = new StringReader(ta.text);
string line = sr.ReadLine();

loadText.Add(line.Split(','));

yield return null;
}
readComplete = true;
}


/// <summary>
/// CalendarManagerに公開する変数
/// </summary>

//ロード完了フラグ
public bool DataOnLoad
{
get { return readComplete; }
}

//データを渡す
public string DataPassing(string date, int dayNum)
{
string dataStr = string.Empty;
int listNum = fileName.IndexOf(date + extension);

if (listNum >= 0) dataStr = loadText[listNum][dayNum];

return dataStr;
}
}

月別のデータ反映は以上となります。

かなり長い記事になったので、上手くまとめられたか心配ですが、
なにかの参考になれば幸いでっす。(^ё^) ♪♪

また、内容がおかしいなどあれば気軽にコメントして頂くと助かります。

それでは、今回はこの辺で( ^ 0 ^ )/~~~~see you again

この記事へのコメント