Comments
Description
Transcript
Editor拡張マニアクス2014
Editor拡張マニアクス2014 チュートリアル ハンズオン 安藤 圭吾! フィールドエンジニア! @kyusyukeigo 県人会議 Unity部 ドキュメント 執筆 この資料について • 発表した内容を全て文字にしたものです! • Unite Japan後の見直し用としてどうぞ 今日の流れ • Editor拡張を作る側の目線が多めです! • 本当にタメになるのか なものまで紹介します! • でもそういう話大好き! • 使う側の人も楽しめるようにはしてるつもりです! ! • 私が事前に作ったアセットを中心に紹介&解説 ( link )! • Unity4.5やUnity5についても少しだけ紹介! • あくまでEditor拡張の部分として 謝辞 • Facebookグループ「日本Androidの会 Unity部」! • ここで今回のネタを募集しました! • 協力してくださった皆様ありがとうございました! Unity部 unity package preview unity packageにプレビュー画像をつける • プレビュー画像! • 普段はアセットストアにある、 パッケージをインポートする時 に使用される部分! • なのでEditor APIでプレビューを 付けることは考えられていない unity packageにプレビュー画像をつける • パッケージは配布目的が多い! • アセットストアに出ていない(出せない)SDKの配布目的! • 広告とかバックエンドサービスとか色々 unity packageにプレビュー画像をつける • パッケージは解凍出来る! • 解凍: tar -zxpf unitychan.unitypackage -C unitychan! • 圧縮: tar -zcpf ../unitychan.unitypackage .! ! • プレビュー画像! • .icon.png! • 128 x 128 GlobalGameManager GlobalGameManager • Project Settingsと同じ扱い方をしたい! • でもそれは難しい! • 「Project Settings」フォルダとのやりとりが複雑! • Editor上でアクセスする時にどうする?! • ビルドする時にどうする? GlobalGameManager • Resourcesフォルダ内に作成! • ScriptableObjectを作成する! • Editor上でもビルド後でも簡単 にアクセスできる! • あとはどこまで自動化出来るか GlobalGameManager • ScriptテンプレートからGameManagerの作成 • JS, C# , Booと同じテンプレート! • Assets/ScriptTemplatesフォルダ! • ここに作成すること! • フォーマットは?! • Unity.app/Contents/Resources/ ScriptTemplatesを参照 GlobalGameManager • 自動化 : マネージャースクリプトの存在チェック [InitializeOnLoad] public class Creator { static Creator () { foreach (var monoScript in MonoImporter.GetAllRuntimeMonoScripts()) { var type = monoScript.GetClass(); ! if (type == null) { continue; } if (type.IsSubclassOf(typeof (GlobalGameManagerObject)) == false) ) { continue; } ! ! if (ExistGlobalGameManager(type) == false) { GlobalGameManager(type); } if (ExistGameManagerInspector(type) == false) { GlobalGameManager • 自動化 : マネージャースクリプトの存在チェック! • スクリプトに何らかの変化があったらコンパイルされる! • コンパイル後にはInitializeOnLoadが呼び出される! • その時にMonoImporter.GetAllRuntimeMonoScripts()! • Assembly-CSharp.dll にあるMonoScriptを全て取得! • これでマネージャークラスが存在するかチェックする GlobalGameManager • 自動化 : CustomEditor スクリプトの作成 private static void GlobalGameManagerInspector(Type type) { #if UNITY_EDITOR var script = new StringBuilder(); script.AppendLine("using UnityEditor;"); script.AppendLine("using UnityEngine;"); script.AppendLine("using System.Collections;"); script.AppendLine(""); script.AppendLine("[CustomEditor(typeof(#NAME#))]"); script.AppendLine("public class #NAME#Inspector : GlobalGameManagerInspector"); script.AppendLine("{"); script.AppendLine(" [MenuItem(\"Edit/Project Settings/#NAME#\")]"); script.AppendLine(" private static void ShowManagerSettings()"); script.AppendLine(" {"); script.AppendLine(" Show<#NAME#>();"); script.AppendLine(" }"); script.Append("}"); ! } var content = Regex.Replace(script.ToString(), "#NAME#", type.Name); var path = GetGlobalGameManagerInspectorPath(type); File.WriteAllText(path, content); AssetDatabase.ImportAsset(path); #endif GlobalGameManager • 自動化 : CustomEditor スクリプトの作成! • スクリプトはテキストベースのアセット! • File.WriteAllText(path, contents) で書き込む! • 作成した後のインポート処理が走らない! • AssetDatabase.ImportAsset( path ) を使用する! • ImportしないとLoadAssetAtPathでNullになる! • AssetDatabase.CreateAssetなら自動でインポート処理が走る MultiSpriteEditor MultiSpriteEditor • 複数のスプライトのスライスを一度に行う! • スプライトエディターは複数のスライスが出来ない! ! • 既にスプライトのサイズなんて仕様で決まってる! • とにかく一度にスライスしたい MultiSpriteEditor • ウィンドウはScriptableWizard! • Automaticは無しでGridのみ! • offsetとpaddingも対応! • たぶんうまく動く MultiSpriteEditor • 何かを作成する時はScriptableWizard! • 条件を満たさない時は作成できない MultiSpriteEditor • 自前でスライスする public Rect[] GenerateGridSpriteRectangles(Texture2D texture) { var rects = new List<Rect>(); for (var y = (int)offset.y; y + (int)pixelSize.y <= texture.height; y += (int)pixelSize.y) { for (var x = (int)offset.x; x + (int)pixelSize.x <= texture.width; x += (int)pixelSize.x) { ! var rect = new Rect(x, texture.height - y - pixelSize.y, pixelSize.x, pixelSize.y); ! if (IsAlphaTexture(texture, rect) == false) { rects.Add(rect); } ! x += (int)padding.x; } ! y += (int)padding.y; } ! return rects.ToArray(); } ! MultiSpriteEditor • 入力した値でRectを作成する! • これが実際に切り分ける部分! • 切り分ける時に何も考えないでやるとアルファのみのスプライ トが作成される! • 「このRect内はアルファのみ」というのをどう判断するか?! • Texture2D.GetPixelsを使用する! • でもGetPixelはTextureのisReadableが有効じゃないといけない! • ではどうするか MultiSpriteEditor • isReadableを強制的に有効にして、すぐ戻す public void DoSlicing() { var _textures = m_selectedTextures; for (var i = 0; i < _textures.Length; i++) { var tex = _textures[i]; var path = AssetDatabase.GetAssetPath(tex); var importer = (TextureImporter)AssetImporter.GetAtPath(path); var isReadable = importer.isReadable; importer.isReadable = true; EditorUtility.SetDirty(importer); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); var rects = GenerateGridSpriteRectangles(tex); EditorUtility.DisplayProgressBar(tex.name, string.Format("{0}/{1}", i, _textures.Length), (float)i / (float)_textures.Length); ! importer.isReadable = isReadable; ! importer.spritesheet = rects.Select((t, j) => new SpriteMetaData { alignment = (int)pivot, MultiSpriteEditor • isReadableを強制的に有効にして、すぐ戻す! • スライスするときだけisReadableを有効にする! • AssetDatabase.ImportAsset で ImportAssetOptions.ForceUpdate! • 強制的に再インポートを行うもの! • スライスが終わったらisReadableを元の設定に戻す! • インポート処理が走り多少時間はかかる! • でも普段のスプライトエディターでポチポチやるよりは良い Overwriter Scroll View Scroll View • GUIエレメントが多い場合はスクロールビューを使って EditorWindow内に全てGUIエレメントを表示する事が多い! • 例えば3000∼10000個のエレメントを同時に表示させるとどうな るか! • GUILayoutで表示させると重い。めっちゃ重い。! • スクロールして見えない部分があるとしても描画はされている! • よく、ビューワー系がこの現象に陥る! • 私のプロジェクトでは3000のテクスチャがあるんだ! Scroll View • 見えないところは描画しない! GUILayout.Space • GUILayout.Spaceで穴埋め! • かなり速度が違う GUILayout.Space Sync Overwriter Camera Sync Camera • シーンビューのカメラとゲームのカメラを同期させる [InitializeOnLoad] public class SyncCamera { ! static int selected; static Rect windowRect = new Rect (10, 20, 100, 24); ! static SyncCamera () { SceneView.onSceneGUIDelegate += (sceneView) => { ! var cameras = Object.FindObjectsOfType<Camera> (); ! Handles.BeginGUI (); ! GUI.skin = EditorGUIUtility.GetBuiltinSkin (EditorSkin.Inspector); ! int windowID = EditorGUIUtility.GetControlID (FocusType.Passive, windowRect); ! windowRect = GUILayout.Window (windowID, windowRect, (id) => { ! string[] displayNames = new string[]{"None",""} ; ArrayUtility.AddRange(ref displayNames,cameras.Select<Camera, string> (c => c.name).ToArray()); selected = EditorGUILayout.Popup (selected, displayNames); Sync Camera • シーンビューのカメラとゲームのカメラを同期させる! • 非推奨のSceneViewクラスを使用する! • onSceneGUIDelegateというデリゲートを使う! ! • 同期の方法は簡単! • シーンビューのカメラを取得して位置と角度を同期してる Overwriter Overwriter • あるある話! • 同じ名前のアセットをインポートする時、別のものとして判断! • 名前の末端に数字がついてしまう! • なので! • 元のアセットを削除してインポート _人人人人人人人人_ Missing > <  ̄Y^Y^Y^Y^Y^Y^Y ̄ Overwriter • あるある話! • Missingを起こさないために! • インポート設定や参照を維持するためにエクスプローラー上 で上書きを行う • こうすることで設定が維持 される Overwriter • あるある話! • エクスプローラー上での操作に慣れた人たちはフォルダ名の変 更もやってしまう • そのフォルダ以下のアセッ トが別物としてインポート される _人人人人人人人人_ > 大量のMissing <  ̄Y^Y^Y^Y^Y^Y^Y ̄ Overwriter • 現状Unityでファイルの上書きはサポートしていない! • でも何とかしたい public class AssetPostprocessor : UnityEditor.AssetPostprocessor { static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] mov { var overwriter = Preference.GetOverwriter(); foreach (var assetPath in importedAssets) { var extension = Path.GetExtension(assetPath); if (!overwriter.all && !overwriter.extensions .Where(ex => ex.enabled) .Select(ex => ex.extension) .Contains(extension)) continue; ! const string pattern = "\\s[\\d]+\\.(.*)$"; ! if (!Regex.IsMatch(assetPath, pattern)) continue; ! var _assetPath = Regex.Replace(assetPath, pattern, ".$1"); File.Copy(assetPath, _assetPath, true); ! Overwriter • Unityの管理下で操作を行うと全部リネームされてしまう! • 「Hoge」が「Hoge 1」「Hoge 2」「Hoge 3」というように! • これはどうしようもない! • なので! • リネームされたものをUnity管理外のFile.Copyを使って元のア セットへ上書きコピーする! • そのあと強制的にインポート! • こうすることでインポート設定や参照はそのままになる Prefab Overwriter Extension Prefab Extension • プレハブを少しだけ扱いやすくするための拡張 // こっちはプレハブ public GameObject player; ! // こっちはゲームオブジェクト public GameObject enemy; Prefab Extension • コードだけ見るとどっちなのかがわからない! • シーン上のゲームオブジェクトなの?! • アセットのプレハブなの?! • 今回はPrefabクラス作って解決してみた public class Example : MonoBehaviour { public Prefab prefab; } [System.Serializable] public class Prefab { public GameObject gameObject; } Prefab Extension • Prefabクラスを作るのは簡単だけど問題はインスペクター! • 見た目をゲームオブジェクトと同じに! • プレハブだけを受け付けるようにしなければいけない そのまま 見た目変更 Prefab Extension • 見た目を変えるのにPropertyDrawerを使う [CustomPropertyDrawer(typeof(Prefab))] public class PrefabDrawer : PropertyDrawer { public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { Object obj = null; ! var gameObjectProperty = property.FindPropertyRelative ("gameObject"); ! if (gameObjectProperty != null) { obj = gameObjectProperty.objectReferenceValue; } ! EditorGUI.BeginChangeCheck (); ! var prefab = EditorGUI.ObjectField (position, label, obj, typeof(GameObject), true) as Gam ! if (EditorGUI.EndChangeCheck ()) { ! Prefab Extension • 見た目を変えるのにPropertyDrawerを使う! • 特定のフィールドをカスタマイズするだけなら断然こっち! • PropertyDrawerの説明は割愛。ドキュメント見てね ( link )! • 見た目のGUIはEditorGUI.ObjectField! • Prefabかどうかの判断! • PrefabUtility.GetPrefabType! • プレハブならSerializedPropery.objectReferenceValueに格納 Prefab Extension • 複数のプレハブをまとめてApplyする public static void ApplyPrefabState (this GameObject prefabInstance, bool force = true) { var prefab = PrefabUtility.GetPrefabParent (prefabInstance) as GameObject; ! if (prefab == null) { Debug.LogException (new System.NullReferenceException ()); return; } ! bool updatePrefab = true; ! if (force == false) { updatePrefab = EditorUtility.DisplayDialog ("Apply", "Do you want to change the Prefab?", "Yes", "No"); } ! if (updatePrefab) { PrefabUtility.ReplacePrefab (prefabInstance, prefab, ReplacePrefabOptions.ConnectToPrefab); } } Prefab Extension • 複数のプレハブをまとめてApplyする! • Unityではインスペクター上にゲームオブジェクトからPrefabへ 更新を反映させるためのボタンがある! • 複数の更新には対応していない。めんどい! • 自分で更新をする仕組みを作ってみる。どうするか Prefab Extension • 複数のプレハブをまとめてApplyする! • プレハブのApplyの仕組み! • PrefabUtility.ReplacePrefabしてるだけ! • なのであとはメニューから実行できるようにしてあげるだけ [MenuItem("PrefabExtension/Apply Prefab State")] static void ApplyPrefabState () { Selection.gameObjects.ApplyPrefabState (false); } ! [MenuItem("PrefabExtension/Apply Prefab State", true)] static bool ValidateApplyPrefabState () { return Selection.gameObjects.All (prefabInstance => prefabInstance.IsPrefabInstance ()); } Reference Overwriter Viewer Reference Viewer • アセットの参照関係を見るためのビューワー! • 「Find References In Scene」の Project 版! ! • このプレハブはどこで使われてる?! • このプレハブには何が使われてる?! • このシーン内には何のアセットが使われてる? Reference Viewer • ひたすら参照チェック! • 自分で書いたスクリプトやコンポーネントを取得してチェック! • 結構単純作業! • ただし、特定のアセットは異なる処理を書かなければいけない! • プレハブ! • シーンファイル! • アニメーションコントローラー! • マテリアル Reference Viewer • プレハブの参照チェック! • AssetDatabase.LoadAssetAtPathで取得したプレハブ (GameObject)でGetComponentは動かない! • 1度シーン上に生成したゲームオブジェクトでチェックを行う var prefab = AssetDatabase.LoadAssetAtPath(assetPath, typeof(GameObject));! var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab);! ! ∼∼∼ 何か処理 ∼∼∼! ! Object.DestroyImmediate(go);! Reference Viewer • シーンの参照チェック! • シーン上の全てのゲームオブジェクトの参照チェック! • EditorApplication.OpenSceneでシーンを開く! • Object.FindObjectsOfType(typeof(GameObject))! • 全ゲームオブジェクトを取得 Reference Viewer • アニメーションコントローラーの参照チェック! • AnimationControllerをいじるAPIはUnityEditorInternalにある! • Unity5.0からはUnityEditor.Animationsに移動! ! • 以下を考慮して参照チェックしなければいけない! • レイヤー! • ステートマシン! • ブレンドツリー Reference Viewer • マテリアルの参照チェック! • マテリアルは大きく2つの参照チェックが必要! • シェーダー / テクスチャ! • テクスチャの取得! • プロパティ名からテクスチャ取得! • Material.GetTexture ( propertyName );! • シェーダーのプロパティ名を知る! • ShaderUtil.GetPropertyName ( shader , index ); ReorderableList Overwriter ReorderableList • 配列の順番を楽に変更できる仕組み! • これはリフレクションを使っているためかなり非推奨なもの! • でも便利! • Unity5からUnityEditorInternalに入ってる! • 少しだけ扱いやすくなる! ! • リフレクションを使うが、かなり便利なものはある! • ようこそ闇の世界へ Overwriter Sprite Field Sprite Field • SpriteのためのObjectField! • ObjectFieldはTexture系に対応しているがSpriteはしていない! • EditorGUILayout.ObjectField(null,typeof(Sprite),false) 理想 現実 Sprite Field • GUIを作る! • GUIを作るのに必要なものはいくつかある! • GUIStyle.Draw ー> GUIの描画部分! • コントロールID! • 様々なものにコントロールIDが割り振られている! • コントロールIDを正確に扱うことによってGUI操作が可能 Sprite Field • コントロールID! ! var id = GUIUtility.GetControlID(FocusType.Keyboard, rect); ! • この場合はキーボード操作を受け付けるコントロールIDを取得 している! • コントロールIDと描画部分の紐付け! EditorStyles.objectFieldThumb.Draw(rect, content, id, false); ! • これにより、フォーカスが当たった時の描画が行われる Sprite Field • コントロールID! • キーボードのコントロールIDで描画するとTabキーで移動する! • 移動によって現在フォーカスされているコントロールID! • EditorGUIUtility.keyboardControl! ! • キーボードのコントロールIDとキー入力を組み合わせる! • そのGUIに対してのみのアクションが出来る! • これがテキストフィールドやオブジェクトフィールドの仕組み AssetBundles Overwriter AssetBundles • Unity5で新しくなるアセットバンドルの仕組み! • 今できること! • アセットのラベルみたいにバンドル名を付けることが出来る! • BuildPipeline.BuildAssetBundlesでサクッと作れる! • 依存関係勝手に解決してくれる! • マニフェストファイルが作られる! • この中にいろんな情報が詰まってる! AssetBundles • 自動的にバンドル名を付ける . static void OnPostprocessAllAssets (string[] importedAssets, string[] deletedAssets, string[] movedAssets, st { string[] results = Directory.GetDirectories ("Assets", "AssetBundles", SearchOption.AllDirectories).Where ( foreach (var assetBundlesFolder in results) { foreach (var assetPath in AssetDatabase.FindAssets("", new []{assetBundlesFolder}).Select<string,string> if (Directory.Exists (assetPath)) continue; ! var assetBundleName = Path.GetDirectoryName (assetPath).Replace (assetBundlesFolder, ""); if (string.IsNullOrEmpty (assetBundleName)) assetBundleName = "___root___"; assetBundleName = Regex.Replace (assetBundleName, "^/", ""); AssetImporter.GetAtPath (assetPath).assetBundleName = assetBundleName; } } AssetDatabase.RemoveUnusedAssetBundleNames (); } AssetBundles • 自動的にバンドル名を付ける! • AssetBundlesフォルダ以下のアセットに自動で付ける! • インポート時にバンドル名を付ける! • スラッシュ(” / ”)で階層になる AssetBundles • アセットバンドルの設定! • ビルドしたいものにチェックを付ける! • 手動でアセットバンドルのビルド出来る! • プレイヤーをビルドした後にも自動ビルド出来る! • UnityEditor.Callbacks.PostProcessBuildを使用する! • プレイヤーのビルド後に呼び出される