2022/12/16 : 初稿
Unity : 2021.3.15f1
作ってるゲームのマスターデータをスプレッドシートで管理したいけれど、
スプレッドシートをGAS使って解析するのはしんどいし
2分以内に終わらないと打ち切られてしまう。
ならば、エクセルとしてダウンロードしてから、
ローカルでNPOIとか使って解析すればいいじゃん的な。
やりたいこと
https://drive.google.com/drive/folders/うんたらかんたら
にあるスプレッドシートを全て
Unityのプロジェクトフォルダ直下にあるExcels
フォルダに
ダウンロードしたいとします。
スプレッドシートをエクセルとしてダウンロードするUnity側のスクリプト
こいつをAssets/Editorとかに置きます。
また、Unityのプロジェクト直下にExcels
フォルダを掘ります。
///
/// @file MasterDownloader.cs
/// @author KYukimoto
/// @date
///
/// @brief スプレッドシートをエクセルとしてダウンロード
///
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Google.Apis.Download;
using Google.Apis.Drive.v3;
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
namespace Utils
{
// サンプル
class SampleMasterDownloader : MasterDownloader
{
// GoogleAPI : Google Developer Consoleで作成したプロジェクト名
protected override string GoogleApplicationName { get { return "ExcelDownloader"; } }
// マスター置き場のフォルダId : GoogleDriveをブラウザで開いた時のURL https://drive.google.com/drive/folders/うんたらかんたら のウンタラカンタラ部分
protected override string FolderId { get { return "XXXXXXXXXXXXXXXXXXXXXXXXXX"; } }
// ダウンロードエントリポイント
[MenuItem("Utils/マスターデータ/スプレッドシート/エクセルとしてダウンロード")]
static void DownloadExcels()
{
SampleMasterDownloader instance = new SampleMasterDownloader();
instance.Download();
}
// アップロードエントリポイント
[MenuItem("Utils/マスターデータ/スプレッドシート/エクセルをアップロード")]
static void UploadExcels()
{
SampleMasterDownloader instance = new SampleMasterDownloader();
instance.Upload();
}
}
// MasterDownloader
abstract class MasterDownloader
{
// GoogleAPI : Google Developer Consoleで作成したプロジェクト名
protected abstract string GoogleApplicationName { get; }
// マスター置き場のフォルダId : GoogleDriveをブラウザで開いた時のURL https://drive.google.com/drive/folders/うんたらかんたら のウンタラカンタラ部分
protected abstract string FolderId { get; }
// エクセル置き場
protected virtual string ExcelsPath { get { return "Excels"; } }
protected virtual string TimeStampsPath { get { return "SpreadSheets"; } }
// client_secret.jsonの置き場所
protected virtual string ClientSecretJsonPath
{
get
{
// デフォルトはこのソースファイル(MasterDownloader.cs)と同じフォルダ
var stackFrame = new System.Diagnostics.StackFrame(true);
var thisFilePath = stackFrame.GetFileName();
var thisDir = Path.GetDirectoryName(thisFilePath);
var client_secret_path = Path.Combine(thisDir, "client_secret.json");
return client_secret_path;
}
}
// 認証結果保存場所
protected virtual string StoreCredentialsPath
{
get
{
// デフォルトはこのソースファイル(MasterDownloader.cs)と同じフォルダ
var stackFrame = new System.Diagnostics.StackFrame(true);
var thisFilePath = stackFrame.GetFileName();
var thisDir = Path.GetDirectoryName(thisFilePath);
var credPath = Path.Combine(thisDir, "credentials");
return credPath;
}
}
// スプレッドシートをエクセルとしてダウンロード
(string desiredPath, string savedPath) DownloadExcel(DriveService service, string excelsPath, string googleDriveFileID, string fileName)
{
// ダウンロードファイル名
var desiredPath = excelsPath + "/" + fileName + ".xlsx";
var savedPath = excelsPath + "/__" + fileName + ".xlsx";
// スプレッドシートをエクセルに変換
var fileId = googleDriveFileID;
var request = service.Files.Export(fileId, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
// ダウンロード
var stream = new MemoryStream();
request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) => {
switch (progress.Status) {
case DownloadStatus.Downloading:
Debug.Log(progress.BytesDownloaded + "byteダウンロードしました...");
break;
case DownloadStatus.Completed:
File.Delete(savedPath);
var fileStream = new FileStream(savedPath, FileMode.Create);
stream.WriteTo(fileStream);
fileStream.Close();
Debug.Log("スプレッドシートのダウンロード完了.");
break;
case DownloadStatus.Failed:
Debug.Log("スプレッドシートのダウンロードに失敗しました.");
break;
}
};
var result = request.DownloadWithStatus(stream);
// 成否を返す
return (result.Status == DownloadStatus.Completed) ? (desiredPath, savedPath) : (null, null);
}
// ダウンロード処理エントリポイント
protected void Download()
{
// 確認
var ret = EditorUtility.DisplayDialogComplex("確認", "GoogleDriveからスプレッドシートをダウンロードします", "差分のみ", "キャンセル", "フル");
if (ret == 1) {
return;
}
var full = ret == 2;
// 処理中...
EditorUtility.DisplayProgressBar("処理中", "GoogleAPIを設定しています...", 0);
// 処理
var downloadedPaths = new Dictionary<string, string>();
try {
DownloadCore(downloadedPaths, full);
} catch (Exception e) {
Debug.LogError("エラー:" + e.ToString());
EditorUtility.DisplayDialog("失敗", "失敗しました", "OK");
}
foreach (var downloadedPath in downloadedPaths) {
var savedPath = downloadedPath.Key;
File.Delete(savedPath);
}
// 終わり
EditorUtility.ClearProgressBar();
}
// ダウンロードした日付
[Serializable]
class TimeStamp
{
public string FileName;
public string ModifiedTime;
}
// ダウンロードした日付リスト
[Serializable]
class TimeStamps
{
public List<TimeStamp> Stamps;
}
// ダウンロード実体
bool DownloadCore(Dictionary<string, string> downloadedPaths, bool full)
{
// DriveAPIサービス取得
var service = SetupServie();
// ダウンロードしたファイルの置き場所
string excelsPath = Path.GetDirectoryName(Application.dataPath) + "/" + ExcelsPath;
// そのフォルダがなければ生成
if (!Directory.Exists(excelsPath)) {
Directory.CreateDirectory(excelsPath);
full = true;
}
// タイムスタンプ
var stampsDir = Path.GetDirectoryName(Application.dataPath) + "/" + TimeStampsPath;
var stampsPath = stampsDir + "/TimeStamps.json";
TimeStamps savedStamps = null;
if (!full && File.Exists(stampsPath)) {
try {
var json = File.ReadAllText(stampsPath);
if (json != null) {
savedStamps = JsonUtility.FromJson<TimeStamps>(json);
}
} catch {
Debug.LogWarning("ダウンロード履歴がありません");
}
}
// format
var format = "yyyy-MM-ddTHH:mm:ss zzz";
// フォルダ内のファイル一覧取得
FilesResource.ListRequest listRequest = service.Files.List();
listRequest.PageSize = 1000;
listRequest.Fields = "nextPageToken, files(id, name, modifiedTime)";
listRequest.Q = string.Format("'{0}' in parents and trashed=false and mimeType='application/vnd.google-apps.spreadsheet'", FolderId);
var files = listRequest.Execute().Files;
var saveStamps = new TimeStamps();
saveStamps.Stamps = new List<TimeStamp>();
if (files != null && files.Count > 0) {
for (int i = 0; i < files.Count; i++) {
var file = files[i];
var saveStamp = new TimeStamp();
saveStamp.FileName = file.Name;
saveStamp.ModifiedTime = file.ModifiedTime.Value.ToString(format);
saveStamps.Stamps.Add(saveStamp);
if (savedStamps != null && savedStamps.Stamps != null) {
var savedStamp = savedStamps.Stamps.Find((s) => s.FileName == file.Name);
if (savedStamp != null && savedStamp.ModifiedTime != null) {
var savedDT = DateTime.ParseExact(savedStamp.ModifiedTime, format, System.Globalization.CultureInfo.CurrentCulture);
var saveDT = DateTime.ParseExact(saveStamp.ModifiedTime, format, System.Globalization.CultureInfo.CurrentCulture);
if (savedDT >= saveDT) {
continue;
}
}
}
EditorUtility.DisplayProgressBar("処理中", file.Name + "をダウンロードしています...", (float)i / files.Count);
Debug.Log("スプレッドシートをダウンロードします:" + file.Name + " id:" + file.Id);
var (desiredPath, savedPath) = DownloadExcel(service, excelsPath, file.Id, file.Name);
if (savedPath == null) {
Debug.LogError("ダウンロード失敗");
return false;
}
downloadedPaths.Add(savedPath, desiredPath);
Debug.Log("Add key:" + savedPath + " value:" + desiredPath);
}
}
Debug.Log("ダウンロード成功。ファイル数:" + downloadedPaths.Count);
// ファイルをリネーム
foreach (var downloadedPath in downloadedPaths) {
var savedPath = downloadedPath.Key;
var desiredPath = downloadedPath.Value;
File.Copy(savedPath, desiredPath, true);
}
// タイムスタンプを保存
var saveStampJson = JsonUtility.ToJson(saveStamps);
try {
if (!Directory.Exists(stampsDir)) {
Directory.CreateDirectory(stampsDir);
}
File.WriteAllText(stampsPath, saveStampJson, System.Text.Encoding.UTF8);
} catch {
Debug.LogWarning("ダウンロード履歴の保存に失敗しました");
}
// 成功
return true;
}
// Upload
protected void Upload()
{
// 確認
if (!EditorUtility.DisplayDialog("確認", "GoogleDriveにスプレッドシートをアップロードします", "Yes", "No")) {
return;
}
if (!EditorUtility.DisplayDialog("確認", "GoogleDriveにある既存のスプレッドシートは削除され、新しいファイルに差し変わりますがよろしいですか?", "Yes", "No")) {
return;
}
// 処理中...
EditorUtility.DisplayProgressBar("処理中", "GoogleAPIを設定しています...", 0);
// 処理
try {
// ダウンロードしたファイルの置き場所
string excelsPath = Path.GetDirectoryName(Application.dataPath) + "/" + ExcelsPath;
// エクセルリスト取得
var excelPaths = Directory.GetFiles(excelsPath, "*.xlsx");
// サービス取得
var service = SetupServie();
// 同名ファイルを削除
DeleteCore(service, excelPaths);
// アップロード
UploadCore(service, excelPaths);
} catch {
EditorUtility.DisplayDialog("失敗", "失敗しました", "OK");
}
// 終わり
EditorUtility.ClearProgressBar();
}
// GoogleDriveサービスのセットアップ
DriveService SetupServie()
{
UserCredential credential;
// GoogleAPI設定
string[] Scopes = { DriveService.Scope.Drive, };
using (var stream0 = new FileStream(ClientSecretJsonPath, FileMode.Open, FileAccess.Read)) {
var credPath = Path.Combine(StoreCredentialsPath, "credentials");
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.FromStream(stream0).Secrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(credPath, true)).Result;
Debug.Log("Credential file saved to: " + credPath);
}
// DriveAPIサービス取得
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = GoogleApplicationName,
});
// サービスを返す
return service;
}
// アップロード実体
bool UploadCore(DriveService service, string[] excelPaths)
{
// アップロード
for (int i = 0; i < excelPaths.Length; i++) {
var excelPath = excelPaths[i];
// アップロード
{
Google.Apis.Upload.IUploadProgress prog;
FileStream fsu = new FileStream(excelPath, FileMode.Open);
try {
Google.Apis.Drive.v3.Data.File meta = new Google.Apis.Drive.v3.Data.File();
meta.Name = Path.GetFileNameWithoutExtension(excelPath);
meta.MimeType = "application/vnd.google-apps.spreadsheet";
meta.Parents = new List<string>() { FolderId, };
EditorUtility.DisplayProgressBar("処理中", meta.Name + "をアップロードしています...", (float)i / excelPaths.Length);
FilesResource.CreateMediaUpload req = service.Files.Create(meta, fsu, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
req.Fields = "id, name";
prog = req.Upload();
} finally {
fsu.Close();
}
}
}
// 成功
return true;
}
// Delete
bool DeleteCore(DriveService service, string[] excelPaths)
{
// フォルダ内のファイル一覧取得
FilesResource.ListRequest listRequest = service.Files.List();
listRequest.PageSize = 1000;
listRequest.Fields = "nextPageToken, files(id, name)";
listRequest.Q = string.Format("'{0}' in parents and trashed=false and mimeType='application/vnd.google-apps.spreadsheet'", FolderId);
var files = listRequest.Execute().Files;
if (files != null && files.Count > 0) {
for (int i = 0; i < files.Count; i++) {
var file = files[i];
bool shouldDelete = false;
foreach (var excelPath in excelPaths) {
if (Path.GetFileNameWithoutExtension(excelPath) == file.Name) {
shouldDelete = true;
break;
}
}
if (!shouldDelete) {
continue;
}
EditorUtility.DisplayProgressBar("処理中", "同名ファイル" + file.Name + "を削除しています...", (float)i / files.Count);
Debug.Log("スプレッドシートを削除します:" + file.Name + " id:" + file.Id);
var request = service.Files.Delete(file.Id);
request.Execute();
}
}
// 成功
return true;
}
}
}
あとはSampleMasterDownloader
クラスの
GoogleApplicationName
とFolderId
を
自分の環境に合わせて書き換えます。
これでUnityのメニューに「Utils」が追加され、
実行できてめでたしめでたし...とは行かない。
その前にアクセス認証などの事前準備を行う必要があります。
事前準備リスト
- GoogleDriveにプログラムからアクセス可能にする
- UnityにGoogleAPI呼び出しdllをインポート
GoogleDriveにプログラムからアクセス可能にする
-
ブラウザでGoogle Developer Consoleにアクセス
-
プロジェクトを作成。
下記ではプロジェクト名を
SampleMasterDownloader.GoogleApplicationName
に合わせてExcelDownloader
としていま...スペルミスしてるぅ
-
OAuth同意画面作成
- 最初の画面、UserTypeは外部。
- 二番目の画面
- アプリ名はここでは
ExcelDownloader
- ユーザーサポートメールとデベロッパーの連絡先は自分のメアド
- その他は空欄のまま
- アプリ名はここでは
- 三番目の画面(スコープ)、何もいじらず次へ。
- 四番目の画面(テストユーザー)、何もいじらず次へ。
- 最後の画面で一番下にある
ダッシュボードへ戻る
をクリック。
-
DriveAPIとSheetsAPIを有効化
この辺の画面から+APIとサービスの有効化
をクリックし、
その後の画面でDrive
とSheets
を検索して有効化します。 -
認証設定をしてclient_secret.jsonをダウンロードし、MasterDownloader.csと同じフォルダに配置
+認証情報を作成
をクリックしてOAuthクライアントIDを作成。
その後に出てくる画面は適当に。
アプリケーションの種類は「デスクトップアプリ」でいけそう。
作成したらjsonをダウンロードするボタンがどっかにあるのでクリック。
client_secretウンタラカンタラ.jsonみたいなファイルがダウンロードされるので、
これをclient_secret.json
にリネームして、MasterDownloader.csと同じ場所に置く。
UnityにGoogleAPI呼び出しdllをインポート
これがまた面倒。
要は.dll
が欲しいだけで、パッケージのインストールとかはしない。
以下はMacのVisualStudio for Mac Communityでの手順です。
-
ひとまず、UnityエディタからVisualStudioを立ち上げる。
-
VSのメニューから
プロジェクト
>Manage NuGet Packages
をクリック。
出てくるダイアログからGoogle.Apis
Google.Apis.Auth
Google.Apis.Drive.V3
を追加。
どのプロジェクトに追加するか聞かれたら
「Assembly.CSharp.Editor」が良さげ。
-
追加したら、Unityのプロジェクト直下の
Packages
フォルダに
それぞれのフォルダができてるので、
その中にある.dll
を全てMasterDownloader.cs
と同じフォルダか
その下にフォルダ掘るかしてコピー。
.dll
全部コピーし終わったら
もうPackages
フォルダの中に追加されたものは不要なので削除。
以上。これでスプレッドシートをエクセルとしてダウンロードできるはず。
なお、ユーザーが初めてアクセスする際、
ブラウザが立ち上がって認証許可を求められるので
許可
を選んでください。結果はcredentials
に保存されるので、
二回目からは確認なしでダウンロード&アップロードできます。
また、このcredentialsは開発チームで共有する必要ないので、
Unityプロジェクト直下にできるSpreadSheets
フォルダと共に
.gitignore
に突っ込んでおきます。