はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】UIElementでモールス信号からC#スクリプトを生成してくれるエディタ拡張を作ってみた

はじめに

最近UnityのUIElementUIBuilderという新機能を試してみるために、お遊びでこんなものを作ってみました。

これについて少し紹介をしたいと思います。

コード

使い方を書いていきたいと思ったのですが、おそらく使いたい人は誰一人いないと察したのでメインのコードをべたっと貼って終わりたいと思います。

程よく手を抜くことも人生肝心ですよね。

あとこのエディタ拡張はUIBuilderを用いて作ったのですが、全部スタイルを.uxmlにべた書きになってしまっています。.ussが空白になってしまっていますが、まあいいでしょう。

MorseCode.uxml
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    <ui:VisualElement style="flex-direction: row; width: 397px;">
        <Style src="MorseCode.uss" />
        <ui:VisualElement style="flex-direction: column; min-width: 70%;">
            <ui:VisualElement style="display: flex;">
                <ui:ScrollView>
                    <ui:TextField picking-mode="Ignore" label="Script Name" value="filler text" text="Name" name="ScriptName" />
                    <ui:Label text="-" name="MorseText" style="white-space: normal; -unity-font-style: bold; font-size: 15px; border-left-width: 1px; border-right-width: 1px; border-top-width: 1px; border-bottom-width: 1px; border-top-left-radius: 3px; border-bottom-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-left-color: rgba(0, 0, 0, 255); border-right-color: rgba(0, 0, 0, 255); border-top-color: rgba(0, 0, 0, 255); border-bottom-color: rgba(0, 0, 0, 255); margin-left: 3px; margin-right: 3px; margin-top: 3px; margin-bottom: 3px;" />
                </ui:ScrollView>
                <ui:Button text="・" name="TonButton" style="-unity-font-style: bold; font-size: 50px; flex-direction: column; position: relative; top: auto; right: auto; left: auto; bottom: auto; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
                <ui:Button text="-" name="TuButton" style="-unity-font-style: bold; font-size: 50px; flex-direction: column-reverse; position: relative; top: auto; right: auto; left: auto; bottom: auto; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
            </ui:VisualElement>
            <ui:VisualElement style="margin-top: 5%; margin-bottom: 0;">
                <ui:Button text="SPACE" name="SpaceButton" style="font-size: 30px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
                <ui:Button text="lowercase character" name="CharacterButton" focusable="false" style="font-size: 20px; display: flex; visibility: visible; opacity: 1; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
                <ui:Button text="DELETE" name="DeleteButton" style="font-size: 20px; margin-top: 10px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
            </ui:VisualElement>
            <ui:VisualElement style="margin-top: 5%; margin-bottom: 0;">
                <ui:Button text="DEBUG" name="DebugButton" style="font-size: 20px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
                <ui:Button text="SAVE" name="SaveButton" style="font-size: 20px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
                <ui:Button text="CLEAR" name="ClearButton" style="font-size: 20px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0);" />
            </ui:VisualElement>
        </ui:VisualElement>
        <ui:VisualElement style="min-width: 32%;">
            <ui:ScrollView name="MorseList" style="margin-right: 0; border-left-color: rgba(0, 0, 0, 255); border-right-color: rgba(0, 0, 0, 255); border-top-color: rgba(0, 0, 0, 255); border-bottom-color: rgba(0, 0, 0, 255); border-right-width: 0; border-left-width: 1px; margin-left: 0;" />
        </ui:VisualElement>
    </ui:VisualElement>
</ui:UXML>
MorseCodeEditor.cs
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using System.Text.RegularExpressions;
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;

namespace hanachiru
{

    public class MorseCodeEditor : EditorWindow
    {
        public static MorseCodeList MorseCode { get; private set; }

        private static MorseCodeContainer _container;

        private string TopDirectoryPath
            => Directory.GetDirectories("Assets", "*", System.IO.SearchOption.AllDirectories).FirstOrDefault(path => System.IO.Path.GetFileName(path) == "MorseCodeEditor");


        private const string MAKE_SCRIPT = @"
using UnityEngine;
public class [ClassName] : MonoBehaviour
{
    private void Start()
    {
        [Script]    
    }
}
";

        [MenuItem("Tools/MorseCoding")]
        public static void Create()
        {
            _container = new MorseCodeContainer();
            MorseCode = null;
            EditorWindow.GetWindow<MorseCodeEditor>();
        }

        private void OnEnable()
        {
            // UXMLを取得
            var UXML = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(TopDirectoryPath + "/Editor/Resources/MorseCode.uxml").CloneTree();

            // rootに追加する
            rootVisualElement.Add(UXML);

            rootVisualElement.Query<Button>("TonButton").AtIndex(0).clicked += OnClickTonButton;

            rootVisualElement.Query<Button>("TuButton").AtIndex(0).clicked += OnClickTuButton;

            rootVisualElement.Query<Button>("SpaceButton").AtIndex(0).clicked += OnClickSpaceButton;

            rootVisualElement.Query<Button>("CharacterButton").AtIndex(0).clicked += OnClickCharacterButton;

            rootVisualElement.Query<Button>("DebugButton").AtIndex(0).clicked += () => Debug.Log(_container.ToString());

            rootVisualElement.Query<Button>("SaveButton").AtIndex(0).clicked += OnClickSaveButton;

            rootVisualElement.Query<Button>("ClearButton").AtIndex(0).clicked += OnClickClearButton;

            rootVisualElement.Query<Button>("DeleteButton").AtIndex(0).clicked += OnClickDeleteButton;

            rootVisualElement.Query<Label>("MorseText").AtIndex(0).text = " ";

            if (MorseCode == null)
            {
                // モールス信号の対応リストを読み込む
                var path = TopDirectoryPath + "/Editor/Resources/MorseCodeSetting.asset";
                MorseCode = AssetDatabase.LoadAssetAtPath<MorseCodeList>(path);
                if (MorseCode == null) Debug.LogError("モールス信号の対応リスト(MorseCodeSetting)を読み取れませんでした");

                // 一覧をウィンドウの右側に表示する
                var morseList = rootVisualElement.Query<ScrollView>("MorseList").AtIndex(0);
                foreach (var item in MorseCode.morseCodes)
                {
                    var newLabel = new Label(" " + item.LowercaseChar + " 「" + item.Key + "」 ");
                    morseList.Add(newLabel);
                }
            }
        }

        private void OnClickTonButton()
        {
            _container.Add("・");
            rootVisualElement.Query<Label>("MorseText").AtIndex(0).text = _container.CodeText;
        }

        private void OnClickTuButton()
        {
            _container.Add("-");
            rootVisualElement.Query<Label>("MorseText").AtIndex(0).text = _container.CodeText;
        }

        private void OnClickSpaceButton()
        {
            _container.Space();
            rootVisualElement.Query<Label>("MorseText").AtIndex(0).text = _container.CodeText;
        }

        private void OnClickCharacterButton()
        {
            var text = _container.ChangeCharacterType();
            rootVisualElement.Query<Button>("CharacterButton").AtIndex(0).text = text;
        }

        private void OnClickSaveButton()
        {
            var scriptName = rootVisualElement.Query<TextField>("ScriptName").AtIndex(0).text;
            if (String.IsNullOrEmpty(scriptName)) scriptName = "ScriptMadeByMorseCode";

            var filePath = $"{TopDirectoryPath}/{scriptName}.cs";
            var assetPath = AssetDatabase.GenerateUniqueAssetPath(filePath);

            var script = MAKE_SCRIPT.Replace("[ClassName]", scriptName)
                .Replace("[Script] ", _container.ToString());

            File.WriteAllText(assetPath, script);

            AssetDatabase.Refresh();
        }

        private void OnClickClearButton()
        {
            _container.Clear();
            rootVisualElement.Query<Label>("MorseText").AtIndex(0).text = " ";
        }

        private void OnClickDeleteButton()
        {
            _container.Delete();
            rootVisualElement.Query<Label>("MorseText").AtIndex(0).text = _container.CodeText;
        }
    }

    internal class MorseCodeContainer
    {
        private List<string> _codes;
        private string Code => string.Join("", _codes);
        public string CodeText => Regex.Replace(Code, "\\[[^\\]]*?\\]", String.Empty);

        private enum CharacterType
        {
            LowerCase,
            UpperCase
        }
        private CharacterType _characterType = CharacterType.LowerCase;

        public MorseCodeContainer()
        {
            _codes = new List<string>();
        }

        public void Add(string text)
        {
            if (_characterType == CharacterType.UpperCase && (_codes.Count == 0 || _codes[_codes.Count - 1] == "   "))
            {
                _codes.Add("[upper]" + text);
            }
            else
            {
                _codes.Add(text);
            }
        }

        public void Space()
        {
            if (_codes[_codes.Count - 1] == "   " || _codes[_codes.Count - 1] == "[upper]   ")
            {
                _codes.Add("[space]");
            }
            else
            {
                _codes.Add("   ");
            }
        }

        public string ChangeCharacterType()
        {
            if (_characterType == CharacterType.LowerCase)
            {
                _characterType = CharacterType.UpperCase;
                return "UPPERCASE CHARACTER";
            }
            else
            {
                _characterType = CharacterType.LowerCase;
                return "lowercase character";
            }
        }

        public void Delete()
            => _codes.RemoveAt(_codes.Count - 1);

        public void Clear()
            => _codes = new List<string>();

        public override string ToString()
        {
            string text = " " + Code + " ";

            // 大文字に変更
            foreach (var morse in MorseCodeEditor.MorseCode.morseCodes)
            {
                text = text.Replace(" [upper]" + morse.Key + " ", morse.UppercaseChar);
            }

            // 小文字に変更
            foreach (var morse in MorseCodeEditor.MorseCode.morseCodes)
            {
                text = text.Replace(" " + morse.Key + " ", morse.LowercaseChar);
            }

            // 空白の削除
            text = text.Replace(" ", String.Empty);

            // スペースの作成
            text = text.Replace("[space]", " ");

            // 余分な箇所を削除
            text = text.Replace("・", String.Empty).Replace("-", String.Empty);

            return text;
        }
    }
}

さいごに

一応GitHubにて.unitypackageを公開しておくので、もし使ってみたいという方がいれば是非。
github.com