TAKOYAKING’s blog

たこ焼き系

Unityでゲームを作っていたらまたPythonでつまらないものを作ってしまった

またUnityから脱線して、つまらないものをPythonで作ってしまいました。


経緯
UnityでDigital Puppet - デジタル パペットというゲームのステージを作っていたところ、ステージの並び順が気に食わなくなってしまいました。
↓こんなゲーム
f:id:TAKOYAKING:20160106232945p:plain


難易度順に並び替えようと思ったものの、並び替えるにはセーブデータ、ステージ情報データ、ステージ画像データの3つを並び替えないといけないことに気づき(面倒くさい・・・)、もっと直感的なGUIベースの並び替えがしたいと思って、気づいたら、つまらないものをPythonで作ってしまっていました。


完成品f:id:TAKOYAKING:20160110061131p:plain
並び替えたいステージを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
f:id:TAKOYAKING:20160110101302p:plain

親クラスでは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):
        # path
        filename = "*-*{ext}".format(ext=self.get_ext())
        file_glob_path = os.path.join(
            self.get_parent_dir_path(),
            filename)
        # glob
        filepaths = glob.glob(file_glob_path)
        # sorted
        return sorted(filepaths, key=self._to_order)

    def _to_order(self, path):
        basename = os.path.basename(path)  # 1-3
        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を外して完了

クラス設計
- メイン
f:id:TAKOYAKING:20160110101912p:plain
- Mix in
f:id:TAKOYAKING:20160110101959p:plain

このクラスも同様に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.meta_paths = self.instantiate_glob_file().get_meta_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)
        # if self.meta_paths:
        #     self._sort(self.meta_paths)

    def _sort(self, paths):
        # temp
        for dstIdx, srcIdx in enumerate(self.sort_result):
            self._mv_temp(paths, int(srcIdx), int(dstIdx))

        # temp ext to normal ext
        self._mv_back_to_normal(paths)

    def _mv_temp(self, paths, srcIdx, dstIdx):
        ''''move to temp place
        ex .json(.meta) to .json_(.meta_)'''
        # src
        src = paths[srcIdx]
        src_basename = os.path.basename(src)
        src_splitted_basename = src_basename.split(".")
        src_ext = ".".join(src_splitted_basename[1:])

        #dst
        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)

        #exec
        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 (tmp path)
            src = path + StaticVars.EXT_TEMP_SUFFIX  # .json -> .json_

            #dst (org path)
            dst = path

            #exec
            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)

実行ソースコード

########## Exec ###########
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
    sort_result = read_sort_result_json()

    # sort
    SortStage(sort_result).sort()
    SortClearData(sort_result).sort()
    SortStagePic(sort_result).sort()

if __name__ == "__main__":
    main()


感想
2日かかったので手作業で並び替えをやったほうが速かったのは内緒です・・・
(2回目以降の並び替えはコレを利用してかなり楽になりました!)
なぜコレを作ったかというとpythonをさわりたかったから・・・

またpythonでつまらないものをつくってしまいました・・・orz


以上

Good bye baseball