またUnityから脱線して、つまらないものをPythonで作ってしまいました。
経緯
UnityでDigital Puppet - デジタル パペットというゲームのステージを作っていたところ、ステージの並び順が気に食わなくなってしまいました。
↓こんなゲーム
難易度順に並び替えようと思ったものの、並び替えるにはセーブデータ、ステージ情報データ、ステージ画像データの3つを並び替えないといけないことに気づき(面倒くさい・・・)、もっと直感的なGUIベースの並び替えがしたいと思って、気づいたら、つまらないものをPythonで作ってしまっていました。
完成品
並び替えたいステージをDrag&Dropで並び変えれます。
必要物
・Python
・Html, CSS, jQueryUI
・Unityの対象ファイルを再ImportするEditor拡張
GUI部分はHTMLで、データ並び替えのスクリプトはPythonで処理しています。
処理の流れ
1 ブラウザー上でステージを並び替える
2 並び替えた結果をテキストに保存
3 Pythonでその結果を読み込む
4 Pythonでその結果をもとにクリアデータ ステージ画像 ステージ情報データを並び替える
5 画像の並び替えをしたのでUnity上で画像を再Import(Unityの画面に行くと自動で再Importしてくれるかもしれませんが、してくれない場合もあります。その場合強制再Importを実行する必要があります。)
ソースコード
1,2は割愛・・・
まず5から・・・
5の指定フォルダを再ImportするEditorは以下の通り
using UnityEngine;
using System.Collections.Generic;
using UnityEditor;
using System.Text.RegularExpressions;
using System.IO;
public class ReimportAfterSortStage {
[MenuItem( "Tools/Reimport/AfterSortingStages", false, 100 )]
public static void ReimportUI()
{
var pathFormat = "Assets/Resources/{0}/{1}";
string stageJsonDataPath = string.Format(
pathFormat, "StageJsonData", "Stages");
string stagePicPath = string.Format(
pathFormat, "Textures", "StageSelect/StagePics");
ReimportDirectory( stageJsonDataPath );
ReimportDirectory( stagePicPath );
EditorUtility.DisplayDialog("Reimport", "reimportが完了しました", "OK");
}
static void ReimportDirectory(string path )
{
if ( Directory.Exists( path ) ){
AssetDatabase.ImportAsset(
path, ImportAssetOptions.ForceUpdate| ImportAssetOptions.ImportRecursive| ImportAssetOptions.DontDownloadFromCacheServer );
}
else {
Debug.LogError( string.Format( "Directory not found {0}", path ) );
}
}
}
対象フォルダの中身を丸ごと再ImportしたかったのでImportAssetOptions.ImportRecursiveをつければ丸ごとImportしてくれました。
AssetDatabase.ImportAsset(
path, ImportAssetOptions.ForceUpdate|
ImportAssetOptions.ImportRecursive|
ImportAssetOptions.DontDownloadFromCacheServer );
5はいらないかもしれませんが、たまにimportしてくれないときがあるのでそうなったらこのEditor拡張が役に立ちます。
では次に3,4のPythonコード
1 ファイル取得
まず結果の並び替えの対象のファイルを取得するクラスを作成します。
対象ファイルの種類はクリアデータ、ステージ情報データ、ステージ画像の3つです。
クラス設計
親のベースクラスがあり、その派生クラスとしてクリアデータ、ステージ情報、ステージ画像のファイルパスを読み込むクラスがあります。
親
・BaseGlobFile
子
・StageDataGlobFile
・ClearDataGlobFile
・StagePicGlobFile
親クラスではoverrideしてほしい関数は全て親の関数でraise Exceptionが記述されています。
def get_parent_dir_path(self):
'''ex parentPath = "/Users/hogehoge
/Library/Application Support/DefaultCompany/takoyaki/storage"
'''
raise Exception("must be overrided")
Pythonではabcクラスを使ってabstractクラスを作成できるみたいですが、この例外を投げる記述でabstractのような振る舞いをさせるのが今のところ好きです。
ソースコード
import os
import glob
import subprocess
import json
class StaticVars:
MAX_WAVE = 10
EXT_TEMP_SUFFIX = "_"
EXT_JSON = ".json"
EXT_META = ".meta"
EXT_PNG = ".png"
class BaseGlobFile:
def __init__(self):
self.paths = self._glob_filepaths_sorted()
def get_files(self):
return self.paths
def get_parent_dir_path(self):
'''ex parentPath = "/Users/hogehoge
/Library/Application Support/DefaultCompany/takoyaki/storage"
'''
raise Exception("must be overrided")
def _glob_filepaths_sorted(self):
filename = "*-*{ext}".format(ext=self.get_ext())
file_glob_path = os.path.join(
self.get_parent_dir_path(),
filename)
filepaths = glob.glob(file_glob_path)
return sorted(filepaths, key=self._to_order)
def _to_order(self, path):
basename = os.path.basename(path)
level, wave = map(int, basename.replace(self.get_ext(), "").split("-"))
order = level * StaticVars.MAX_WAVE + wave
return order
def get_ext(self):
raise Exception("must be overrided")
class StageDataGlobFile(BaseGlobFile):
def get_parent_dir_path(self):
return "/Users/takoyaki\
/Projects/takoyaki/takoyaki/Assets/Resources/StageJsonData/Stages"
def get_ext(self):
return StaticVars.EXT_JSON
class ClearDataGlobFile(BaseGlobFile):
def get_parent_dir_path(self):
return "/Users/takoyaki/Projects/DigitalPuppetDB/storage/Stages"
def get_ext(self):
return StaticVars.EXT_JSON
class StagePicGlobFile(BaseGlobFile):
def get_parent_dir_path(self):
return "/Users/takoyaki/\
Projects/takoyaki/takoyaki/Assets/Resources/Textures/StageSelect/StagePics"
def get_ext(self):
return StaticVars.EXT_PNG
2 ファイル並び替え
ファイル取得を取得してきた後はそれをもとに並び替えを実行します。
- 処理の流れ
1) ファイル取得
2) ファイル名を新しいファイル名に変更後いったん拡張子に.tmpを入れて保持する
3) その後全てのファイル名が新しいファイル名 + .tmpになったら全てのファイルの.tmpを外して完了
クラス設計
- メイン
- Mix in
このクラスも同様にoverrideしてほしい関数はbaseクラスで例外を投げます。
最初に思ったのがファイルを移動させるコマンドがshellのmvとgit mvで振る舞いを買えないといけないと思っていてmix in的なクラスを作成しました。
base
def exec_mv_cmd(self, src, dst):
'''execute file move cmd
ex git mv or os.rename?'''
raise Exception("must be overrided")
mix in
class GitCmd:
'''Mix in'''
def mv(self, src, dst):
if not self.DEBUG:
cmd_str = "git mv {0} {1}".format(src, dst)
subprocess.call(cmd_str.split(" "))
else:
print("src: {} -> dst: {}".format(src, dst))
class ShellCmd:
'''Mix in'''
def mv(self, src, dst):
if not self.DEBUG:
os.rename(src, dst)
else:
print("src: {} -> dst: {}".format(src, dst))
継承例
class SortStage(BaseSort, ShellCmd):
def instantiate_glob_file(self):
return StageDataGlobFile()
def exec_mv_cmd(self, src, dst):
super().mv(src, dst)
結局shellのmvだけで大丈夫でした・・・
最近の自分の多重継承の仕方はMix in的な書き方にとどめておくのが良いのかなと思っています。コンストラクタが絡んでくるとややこしさが倍増して怖いです。
ソースコード
import os
import glob
import subprocess
import json
class BaseSort:
def __init__(self, sort_result):
self.paths = self.instantiate_glob_file().get_files()
self.sort_result = sort_result
self.DEBUG = True
def instantiate_glob_file(self):
raise Exception('Must be overrided')
def sort(self):
self._sort(self.paths)
def _sort(self, paths):
for dstIdx, srcIdx in enumerate(self.sort_result):
self._mv_temp(paths, int(srcIdx), int(dstIdx))
self._mv_back_to_normal(paths)
def _mv_temp(self, paths, srcIdx, dstIdx):
''''move to temp place
ex .json(.meta) to .json_(.meta_)'''
src = paths[srcIdx]
src_basename = os.path.basename(src)
src_splitted_basename = src_basename.split(".")
src_ext = ".".join(src_splitted_basename[1:])
dst_filename = self.to_temp_filename_by_order(dstIdx, src_ext)
dst_dirname = os.path.dirname(src)
dst = os.path.join(dst_dirname, dst_filename)
self.exec_mv_cmd(src, dst)
def to_temp_filename_by_order(self, order, ext):
level, wave = divmod(order, StaticVars.MAX_WAVE)
return "{}-{}.{}{}".format(
level + 1, wave + 1, ext, StaticVars.EXT_TEMP_SUFFIX)
def _mv_back_to_normal(self, paths):
'''move back to normal place
ex .json_(.meta_) to .json (.meta)'''
for i, v in enumerate(self.sort_result):
path = paths[i]
src = path + StaticVars.EXT_TEMP_SUFFIX
dst = path
self.exec_mv_cmd(src, dst)
def exec_mv_cmd(self, src, dst):
'''execute file move cmd
ex git mv or os.rename?'''
raise Exception("must be overrided")
class GitCmd:
'''Mix in'''
def mv(self, src, dst):
if not self.DEBUG:
cmd_str = "git mv {0} {1}".format(src, dst)
subprocess.call(cmd_str.split(" "))
else:
print("src: {} -> dst: {}".format(src, dst))
class ShellCmd:
'''Mix in'''
def mv(self, src, dst):
if not self.DEBUG:
os.rename(src, dst)
else:
print("src: {} -> dst: {}".format(src, dst))
class SortStage(BaseSort, ShellCmd):
def instantiate_glob_file(self):
return StageDataGlobFile()
def exec_mv_cmd(self, src, dst):
super().mv(src, dst)
class SortClearData(BaseSort, ShellCmd):
def instantiate_glob_file(self):
return ClearDataGlobFile()
def exec_mv_cmd(self, src, dst):
super().mv(src, dst)
class SortStagePic(BaseSort, ShellCmd):
def instantiate_glob_file(self):
return StagePicGlobFile()
def exec_mv_cmd(self, src, dst):
super().mv(src, dst)
実行ソースコード
def read_sort_result_json():
path = "/Users/takoyaki\
/Projects/takoyaki/Tools/sort/builda/sort_result.json"
with open(path, mode="r", encoding="UTF-8") as f:
sort_result = json.loads(f.read())
return sort_result
def main():
sort_result = read_sort_result_json()
SortStage(sort_result).sort()
SortClearData(sort_result).sort()
SortStagePic(sort_result).sort()
if __name__ == "__main__":
main()
感想
2日かかったので手作業で並び替えをやったほうが速かったのは内緒です・・・
(2回目以降の並び替えはコレを利用してかなり楽になりました!)
なぜコレを作ったかというとpythonをさわりたかったから・・・
またpythonでつまらないものをつくってしまいました・・・orz
以上
Good bye baseball