【Unity】複数解像度スクリーンショットを一括で撮影する拡張機能

写真を撮影する Unity
スポンサーリンク

アプリを開発していると申請用のスクリーンショットを用意するのが大変ですよね。
なので今回は複数解像度を連続撮影できる拡張機能をご紹介します。
手間だなぁと感じている方はぜひご利用いただければ幸いです。

はじめに

今回紹介するのは「アプリ申請に必要なスクリーンショットを一括で撮影するUnity拡張機能」です。

設定内容は保存されるようになっていて、スクリプトひとつで導入できるので簡単に利用できるかと思います。

とても便利だったので公開することにしました!

 

更新内容

  • 2020/09/23
    解像度変更後に待ち時間を追加

 

 

導入方法

スクリプト

ダウンロードはこちら
【Unity拡張】一括スクショ撮影スクリプト

TedLab_CaptureScreenshotEditor.cs

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

namespace TedLab
{
    public class CaptureScreenShotWindow : EditorWindow
    {
        // iOS用の解像度リスト
        static CaptureResolution[] resolutions_IOS =
        {
            new CaptureResolution(1242, 2208),
            new CaptureResolution(1242, 2688),
            new CaptureResolution(2048, 2732),
        };

        [MenuItem("TedLab Editor/スクリーンショットキャプチャ")]
        static void OpenWindow()
        {
            GetWindow<CaptureScreenShotWindow>(true, "[TedLab]スクリーンショットキャプチャ");
        }

        [Serializable]
        class CaptureResolution{
            public CaptureResolution(int _width, int _height){
                width = _width;
                height = _height;
            }
            public int width = 1920;
            public int height = 1080;
        }

        static readonly string KeyPrefix = "TedLab_CaptureScreenShot_";
        static readonly string PathKey = KeyPrefix + "Path";
        static readonly string ReverseKey = KeyPrefix + "Reverse";
        static readonly string StopTimeKey = KeyPrefix + "StopTime";
        static readonly string ResoCountKey = KeyPrefix + "ResoCount";
        static readonly string ResoWidthBaseKey = KeyPrefix + "ResoW_";
        static readonly string ResoHeightBaseKey = KeyPrefix + "ResoH_";

        string m_path = "";
        bool m_started = false;
        bool m_reverse = false;
        bool m_stopTime = true;

        [SerializeField] List<CaptureResolution> captureResolutions = null;
        SerializedObject resolutionSerializedObj;
        SerializedProperty resolutionProp;

        EditorCoroutine m_coroutine = null;
        int m_selectedIndexOld = -1;
        GameViewSizeGroupType m_currentGroupType;

        private void OnEnable()
        {
            captureResolutions = new List<CaptureResolution>(32);

            // 読み込み
            m_path = EditorPrefs.GetString( PathKey, "" );
            m_reverse = EditorPrefs.GetBool( ReverseKey, false );
            m_stopTime = EditorPrefs.GetBool( StopTimeKey, true );

            int reso_count = EditorPrefs.GetInt( ResoCountKey, 0 );
            for( int i=0; i<reso_count; ++i )
            {
                string indexText = i.ToString("D2");
                int width = EditorPrefs.GetInt( ResoWidthBaseKey + indexText, 0 );
                int height = EditorPrefs.GetInt( ResoHeightBaseKey + indexText, 0);
                if( width > 0 && height > 0 ){
                    captureResolutions.Add( new CaptureResolution(width, height));
                }
            }

            resolutionSerializedObj = new SerializedObject(this);
            resolutionProp= resolutionSerializedObj.FindProperty("captureResolutions");
        }

        private void OnDisable()
        {
            if( m_coroutine != null )
            {
                m_coroutine.Stop();
                m_coroutine = null;
            }

            if( m_timeScaleOld >= 0f ){
                Time.timeScale = m_timeScaleOld;
                m_timeScaleOld = -1f;
            }

            // 保存
            EditorPrefs.SetString( PathKey, m_path );
            EditorPrefs.SetBool( ReverseKey, m_reverse );
            EditorPrefs.SetBool( StopTimeKey, m_stopTime );

            int reso_count = captureResolutions.Count;
            EditorPrefs.SetInt( ResoCountKey, reso_count );
            for( int i=0; i<reso_count; ++i )
            {
                var reso = captureResolutions[i];
                string indexText = i.ToString("D2");
                EditorPrefs.SetInt( ResoWidthBaseKey + indexText, reso.width );
                EditorPrefs.SetInt( ResoHeightBaseKey + indexText, reso.height );
            }
            
        }

        void OnGUI()
        {
            resolutionSerializedObj.Update();

            if( m_started )
            {
                GUILayout.Label("スクリーンショットを撮影中...");
            }
            else
            {
                GUILayout.Label("設定:");
                m_reverse = GUILayout.Toggle( m_reverse, "縦横反転" );
                m_stopTime = GUILayout.Toggle( m_stopTime, "撮影時に止める(TimeScale)" );

                GUILayout.Space(16f);

                GUILayout.Label("解像度リスト:");
                EditorGUILayout.PropertyField(resolutionProp, true);
                resolutionSerializedObj.ApplyModifiedProperties();

                if (GUILayout.Button("解像度をクリア")) {
                    captureResolutions.Clear();
                }

                if (GUILayout.Button("iOS解像度を追加")) {
                    AddResolutions( resolutions_IOS );
                }

                GUILayout.Space(16f);

                GUILayout.Label("保存フォルダ:");
                m_path = GUILayout.TextField(m_path);

                if (GUILayout.Button("フォルダを開く"))
                {
                    string filePath = m_path;
                    filePath = filePath.Replace( '/', Path.DirectorySeparatorChar );
                    EditorUtility.RevealInFinder(filePath);
                }

                if (GUILayout.Button("マニュアルを開く")) {
                    OpenManual();
                }
                if (GUILayout.Button("スクリーンショットを保存")) {
                    Capture();
                }
            }
        }

        void AddResolutions( CaptureResolution[] resolutions )
        {
            foreach( var reso in resolutions ){
                captureResolutions.Add( reso );
            }
        }

        void Capture()
        {
            if( m_started){
                Debug.Log( "Already Started Capture..." );
                return;
            }

            // 解像度指定がない場合はそのまま
            if( captureResolutions.Count <= 0 )
            {
                string dateString = DateTime.Now.ToString("yyyyMMddHHmmss");
                string filePath = string.Format( Path.Combine(m_path, "ScreenCapture_{0}.png"), dateString );
                Debug.Log(string.Format("Saved a new screenshot as {0}", filePath));
                ScreenCapture.CaptureScreenshot(filePath);
            }
            // 複数キャプチャ
            else
            {
                m_coroutine = EditorCoroutine.Start( MultipleCaptureScreen() );
            }
        }

        void OpenManual()
        {
            string url = "https://tedenglish.site/unity-ex-multi-screenshots/";
            Application.OpenURL( url );
        }

        
        float m_timeScaleOld = -1f;

        IEnumerator MultipleCaptureScreen()
        {
            m_started = true;

            string TmpLabel = "TedLabScreenCaptureResoTemp";

            m_timeScaleOld = Time.timeScale;
            if( m_stopTime ){
                Time.timeScale = 0f;
            }

            foreach( CaptureResolution reso in captureResolutions )
            {
                int width = m_reverse ? reso.height : reso.width;
                int height = m_reverse ? reso.width : reso.height;

                string dateString = DateTime.Now.ToString("yyyyMMddHHmmss");
                string filePath = string.Format( Path.Combine(m_path, "ScreenCapture_{0}_{1}.png"), dateString, width.ToString() + "x" +  height.ToString() );
                Debug.Log( string.Format("Save a new screenshot as {0}", filePath) );

                StartCustomSize( TmpLabel, width, height );
                yield return new WaitForSeconds(0.5f);

                // キャプチャ&待つ
                ScreenCapture.CaptureScreenshot(string.Format(filePath));

                while( !IsSaved( filePath ) ){
                    yield return new WaitForSeconds(0.1f);
                }

                EndCustomSize( TmpLabel, width, height );
            }

            Debug.Log( "Saved All Captures!!" );

            if( m_stopTime ){
                Time.timeScale = m_timeScaleOld;
                m_timeScaleOld = -1f;
            }

            m_started = false;
            m_coroutine = null;

            yield return null;
        }

        // UnityEditor.GameViewSizeTypeと合わせる
        public enum GameViewSizeType
        {
            AspectRatio = 0,
            FixedResolution = 1,
        }

        void StartCustomSize( string name, int width, int height )
        {
            var asm = typeof(Editor).Assembly;
            Type gameViewType = asm.GetType("UnityEditor.GameView");
            Type gameViewSize = asm.GetType("UnityEditor.GameViewSize");
            Type gameViewSizes = asm.GetType("UnityEditor.GameViewSizes");
            Type gameViewSizeType = asm.GetType("UnityEditor.GameViewSizeType");
            Type gameViewSizeGroup = asm.GetType("UnityEditor.GameViewSizeGroup");

            EditorWindow gameView = EditorWindow.GetWindow(gameViewType, false, "Game", false);

            PropertyInfo currentSizeGroupType = gameViewType.GetProperty("currentSizeGroupType", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
            m_currentGroupType = (GameViewSizeGroupType)currentSizeGroupType.GetValue(gameView, null);

            MethodInfo getGroup = gameViewSizes.GetMethod("GetGroup");
            Type scriptableSingleton = typeof(ScriptableSingleton<>).MakeGenericType(gameViewSizes);
            PropertyInfo scriptableSingletonInstance = scriptableSingleton.GetProperty("instance");
            object gameViewSizesInstance = scriptableSingletonInstance.GetValue(null, null);
            object group = getGroup.Invoke(gameViewSizesInstance, new object[] { m_currentGroupType });

            Type[] paramTypes = new Type[] { gameViewSize };
            MethodInfo addCustomSize = gameViewSizeGroup.GetMethod("AddCustomSize", BindingFlags.Public | BindingFlags.Instance, null, paramTypes, null);
            ConstructorInfo constructor = gameViewSize.GetConstructor( new Type[] { gameViewSizeType, typeof(int), typeof(int), typeof(string) } );

            // インデックスを保持しておく
            PropertyInfo selectedSizeIndex = gameViewType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            m_selectedIndexOld = (int)selectedSizeIndex.GetValue( gameView, null );

            // 追加
            {
                object newSize = constructor.Invoke(new object[] { (int)GameViewSizeType.FixedResolution, width, height, name });
                addCustomSize.Invoke(group, new object[] { newSize });
            }

            // 追加したのをインデックス設定
            int index = FindSameResolution( group, name, width, height );
            if( index >= 0 ){
                selectedSizeIndex.SetValue( gameView, index, null);
            }
        }

        void EndCustomSize( string name, int width, int height )
        {
            var asm = typeof(Editor).Assembly;
            Type gameViewSize = asm.GetType("UnityEditor.GameViewSize");
            Type gameViewSizes = asm.GetType("UnityEditor.GameViewSizes");
            Type gameViewSizeType = asm.GetType("UnityEditor.GameViewSizeType");
            Type gameViewSizeGroup = asm.GetType("UnityEditor.GameViewSizeGroup");

            MethodInfo getGroup = gameViewSizes.GetMethod("GetGroup");
            Type scriptableSingleton = typeof(ScriptableSingleton<>).MakeGenericType(gameViewSizes);
            PropertyInfo scriptableSingletonInstance = scriptableSingleton.GetProperty("instance");
            object gameViewSizesInstance = scriptableSingletonInstance.GetValue(null, null);
            object group = getGroup.Invoke(gameViewSizesInstance, new object[] { m_currentGroupType });

            Type[] paramTypes = new Type[] { typeof(int) };
            MethodInfo removeCustomSize = gameViewSizeGroup.GetMethod("RemoveCustomSize", BindingFlags.Public | BindingFlags.Instance, null, paramTypes, null);

            // 同名のインデックスを探す
            int index = FindSameResolution( group, name, width, height );
            if( index >= 0 ){
                removeCustomSize.Invoke( group, new object[] { index } );
            }

            // 元に戻す
            Type gameViewType = asm.GetType("UnityEditor.GameView");
            PropertyInfo selectedSizeIndex = gameViewType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            EditorWindow gameView = EditorWindow.GetWindow(gameViewType, false, "Game", false);
            selectedSizeIndex.SetValue( gameView, m_selectedIndexOld, null);
        }

        // 同名のインデックスを探す
        int FindSameResolution( object group, string name, int width, int height )
        {
            int index = -1;
            var getDisplayTexts = group.GetType().GetMethod("GetDisplayTexts");
            var displayTexts = getDisplayTexts.Invoke(group, null) as string[];
            string targetName = string.Format("{0} ({1}x{2})", name, width, height);
            for(int i=0; i<displayTexts.Length; ++i)
            {
                string textTmp = displayTexts[i];
                if( textTmp == targetName ){
                    index = i;
                    break;
                }
            }
            return index;
        }

        // 保存されているか
        bool IsSaved( string path )
        {
            bool isExist = File.Exists( path );
            bool isLocked = false;
            FileStream stream = null;
            try{
                stream = new FileStream( path, FileMode.Open, FileAccess.ReadWrite, FileShare.None );
            }catch{
                isLocked = true;
            }finally{
                if( stream != null ){
                    stream.Close();
                }
            }
            return isExist && !isLocked;
        }

        // コルーチン用
        public class EditorCoroutine
        {
            public static EditorCoroutine Start( IEnumerator _routine )
	        {
		        EditorCoroutine coroutine = new EditorCoroutine(_routine);
		        coroutine.Start();
		        return coroutine;
	        }
            readonly IEnumerator routine;
		    EditorCoroutine( IEnumerator _routine ){
			    routine = _routine;
		    }

            void Start(){
			    EditorApplication.update += Update;
		    }
		    public void Stop(){
			    EditorApplication.update -= Update;
		    }
		    void Update()
            {
			    if (!routine.MoveNext()){
				    Stop();
			    }
		    }
        }
    }

} // namespace TedLab

 

追加場所

そしてダウンロードしていただいたファイルを「Assets→Editor」フォルダに配置します。
(Editorフォルダがない場合は追加します)

これで準備完了です。

すっごい簡単!

 

マニュアル

では使い方を紹介したいと思います。

エディタを開く

UnityEditorのメニューから「TedLab Editor→スクリーンショットキャプチャ」を選択します。

 

エディタ画面

メニューから選択すると下記のようなウィンドウが開きます。

ではここから各項目について説明したいと思います。

縦横反転

基本的にPortrait(縦置き)が基本となっています。
なのでLandScape(横置き)のアプリではここにチェックを入れてください。

 

撮影時に止める

撮影時にゲームを停止するかをチェックします。

※TimeScaleを用いているため撮影中にTimeScaleが変わるような操作は行わない場合にご利用ください

 

解像度リスト

「Capture Resolution」に撮影するスクリーンショットの数を設定します。

各要素は
Width・・・幅
Height・・・高さ

となっています。
注意点として縦置き状態での「幅、高さ」を設定してください(縦横反転が有効な場合、Heightが幅、Widthが高さとなります)。

 

解像度をクリア

登録した解像度リストを全削除します。

 

iOS解像度を追加

現在(2020/09/15)、AppStoreの申請で必要な解像度(Portrait)を一括で解像度に追加します。

 

保存フォルダ

ルートフォルダはAssets直下になっています。
フォルダを指定する場合はAssets直下にフォルダを追加して名前を入力してください。

 

フォルダを開く

指定しているフォルダの階層を開きます。

 

マニュアルを開く

本記事を開いてマニュアルを確認できます。

 

スクリーンショットを保存

登録した解像度でスクリーンショット連続撮影します。
撮影が完了すると登録したフォルダにスクリーンショットが出力されます。

※撮影中にゲームを操作すると停止しているTimeScaleがおかしくなる可能性があるので極力触らないようにしてください。

 

 

保存機能

ちなみに設定などは保存されるようになっていて
一度設定するとそのプロジェクトでは再設定は不要となります。

 

 

おすすめの使い方

iOS向けのスクリーンショットを追加して、Android用に解像度を一つ利用すると便利です。

一度で全プラットホームを撮影できるのはすごいラク!

 

 

利用したゲームのPR

今回この機能を実装する時に利用したゲームとなります。

「鳥フライト」 公式サイト
ゲームアプリ「鳥フライト」の公式サイトです。

拡張機能を使ってアプリのリリースできたので機能としては問題なさそうです!

 

 

最後に

いかがでしたでしょうか。

今まで手作業で撮影していたものが一括で利用できるととても便利ですよね!
ほかにも予約サイト、プレスリリースなどの広告用の画像もまとめて撮影できちゃうので一石二鳥以上かもしれません…!

もし不具合などありましたらお気軽にコメントください。
ではここまでお読みいただきありがとうございました。

 

 

コメント

  1. […] […]

  2. […] 【Unity】複数解像度スクリーンショットを一括で撮影する拡張機能 無料でUnityをバージョン管理する!ローカルsvn編 Firebaseを入れて分析してみよう! その1 プライバシーポリシー編 【英語の文法】スラッシュリーディングという英語学習方法 2020年 はじめての確定申告、白色は無料でできる! 断捨離でメルカリ、フリマアプリをおすすめする理由 […]

スポンサーリンク
タイトルとURLをコピーしました