[Unity,Editor] SOManagementWindow 제작 일기 Part.4

2025. 5. 20. 09:49·Editor
목차
  1. 만든 거
  2. 구현부
  3. Uxml
  4. USS
  5. Script
  6. 전체 코드
  7. 결과물

만든 거

 

이게 계속 보다보니까 짜치는 점을 발견했다.

이게 생성하는 Window인데 너무 단출하고 생성 위치도 직접 못 정해준다. 그래서

짜잔, 파일을 선택에서 원하는 위치에 생성할 수 있게 만들었다. 

그리고 파일을 선택한다면 그 파일의 이름, 크기, 가지고 있는 파일이 오른쪽에 화면에 뜨게 된다.

 

구현부

Uxml

먼저 Uxml과 USS를 만들어야 한다.

이건 Uxml이다. 이런 식으로 꾸며주고

.unity-two-pane-split-view__dragline, .unity-two-pane-split-view__dragline-anchor

 

SplitView의 Dragline을 없애기 위해서 두개의 USSClass Extract를 해준뒤 Display 탭에 Display를 꺼준다. DragLine이 보이지 않게 된다.

 

사용자 정의 tab의 크기를 바꾸는 DragLine은 필요없기 때문에 꺼주었다.

그리고 원래 이렇게 밋밋하게 생긴 Foldout을

이렇게 바꿔주었다.

 

이렇게 바꾸기 위해서

.unity-foldout__checkmark, Foldout > Toggle:checked .unity-foldout__checkmark
의 BackGround의 Sprite를 바꿔주었다.

열렸을 때 효과도 있다.

 

USS

.custom-label {
    font-size: 20px;
    -unity-font-style: bold;
    color: rgb(68, 138, 255);
}

.unity-two-pane-split-view__dragline {
    display: none;
}

.unity-two-pane-split-view__dragline-anchor {
    display: none;
}

.file-icon {
    flex-grow: 0;
    width: 20px;
    background-image: url("project://database/Assets/Layer%20Lab/GUI-TheStone/ResourcesData/Sprites/Components/UI_Etc/Toggle_Switch_On.Png?fileID=2800000&guid=2ff96032cccaa4cac810220fca1ff7a5&type=3#Toggle_Switch_On");
    height: 20px;
    bottom: -2px;
}

.file-name-label {
    flex-grow: 1;
    flex-direction: row;
    white-space: pre-wrap;
    -unity-text-align: middle-left;
}

.unity-scroll-view__content-container {
    flex-grow: 0;
}

.file-visual {
    flex-grow: 1;
    height: 25px;
    border-top-width: 0;
    border-right-width: 0;
    border-bottom-width: 1px;
    border-left-width: 0;
    border-left-color: rgb(20, 20, 20);
    border-right-color: rgb(20, 20, 20);
    border-top-color: rgb(20, 20, 20);
    border-bottom-color: rgb(20, 20, 20);
    white-space: nowrap;
    flex-direction: row;
    width: auto;
}

.unity-foldout__checkmark {
    background-image: url("project://database/Assets/SOManagementWindow/Asset/Folder%20Icon.png?fileID=2800000&guid=37f7954445b090d4aa6ac9a1679cf50e&type=3#Folder Icon");
    background-repeat: no-repeat no-repeat;
    -unity-background-scale-mode: stretch-to-fill;
    -unity-slice-left: 30;
    -unity-slice-top: 30;
    bottom: 1px;
}

Foldout > Toggle:checked .unity-foldout__checkmark {
    background-image: url("project://database/Assets/SOManagementWindow/Asset/FolderOpened%20Icon.png?fileID=21300000&guid=4d80dfc7319226c4d9d31aad7691d8e1&type=3#FolderOpened Icon");
    bottom: 1px;
    -unity-slice-left: 30;
}

.ljs-label {
    font-size: 15px;
    border-left-color: rgb(32, 32, 32);
    border-right-color: rgb(32, 32, 32);
    border-top-color: rgb(32, 32, 32);
    border-bottom-color: rgb(32, 32, 32);
    border-bottom-width: 1px;
    -unity-font-definition: url("project://database/Assets/SOManagementWindow/Font/DNFForgedBlade-Light.ttf?fileID=12800000&guid=1a54e8b4cbd09f74986c9f5fe2166f82&type=3#DNFForgedBlade-Light");
}

.unity-base-field__label {
    -unity-font-definition: url("project://database/Assets/SOManagementWindow/Font/DNFForgedBlade-Light.ttf?fileID=12800000&guid=1a54e8b4cbd09f74986c9f5fe2166f82&type=3#DNFForgedBlade-Light");
}

 

Script

(중요한 부분만 서술함)

 

먼저 기본적인 정보들을 초기화 해줌

생성할 Type와 끝났을 때 발행할 Action을 초기화 한뒤 Label의 이름을 변경해준다.

 

그 다음 CreateGUI 함수로 넘어가서 RootFoldout을 가져오는 부분이다.

그 뒤 값이 변경되었을 때 발행할 이벤트, text, FolderInfo를 변경한 뒤 분류를 위해 Dict에 넣는다.

FolderInfo는 크기, 자식 file들, 경로 정보가 담겨있다.

 

 

저기서 폴더의 크기를 구하는 방법은 다음과 같다.

현재 디렉토리의 모든 파일들을 가지고 와서 파일의 크기를 모두 구한다. file의 길이를 가져온다.(이게 크기임) 그 다음 하위 디렉토리 사이즈도 모두 구한다.

 

 

그 다음 특별한 연산을 사용하여 byte, kb, mb, gb를 분류해준다.

byte라면 그냥 반환, KB라면 2의10승, MB라면 2의20승, GB라면 2승의30승 단위로 나누어 줘서 String으로 바꿔준다.

 

여튼 이렇게 해서 FolderInfo의 값을 모두 넣어준 뒤 버튼 클릭 이벤트 구독해준다.

 

그 다음에 FoldOut 값 변경시 실행되는 함수를 보자.

Value가 True이고 FoldOut이라면

현재 FoldOut를 사용하여 FolderInfo를 가지고 온다.

그다음 folderInfo.directory를 계속 참조할 수 없으니 캐싱 작업을 한뒤 _currentDirectory를 변경한다. (생성할 위치라고 보면 된다.)

 

만약 자식이 없다면 (이미 만들어진 자식이 있나?)

자식의 디렉토리를 들고 와서

위에서 했던 Foldout 생성 방식과 동일하게 생성하고 Dict에 넣어준다.

사실 저 조건문은 안 넣어도 된다.

그리고 현재 Foldout의 자식으로 넣어준다.

끝났다면 File Visual 생성

이거임

만약 Value가 False라면 폴더를 닫아야 하니

자신을 포함한 자식을 모두 열림 상태에서 닫힘 상태도 바꿔준다.

 

그 다음은 FileVisual 생성 부분이다.

File 정보 Array, folder 이름, folder 크기가 매개변수로 들어온다.

ChangeDisplay함수

현재 VisualFileList(File들이 생성 되는 위치에 뭔가 있다면 DisPlay를 꺼줌(여러번 생성 안할려고 이렇게 만들었음))

그 다음 만약 생성된 FileVisualList중에 이 Folder이름을 가진 VisualElement가 있다면 DisplayType만 변경하고 folderNameLabel의 text를 변경하고 return 해준다.(아래 코드는 생성 코드라서 이 조건문이 true인 순간 필요없음.)

folderNameLabel임

 

사실 생성 부분은 다 이해하기 쉽기 때문에 중요한 몇가지만 설명하겠다.

fileInfo의 확장자 명이 .meta라면(Unity에서 기본적으로 파일을 만들때 같이 생성하는 파일임 따라서 이것도 생성하면 2배로 생성이 되기 때문에 안 만들었음) 굳이 생성하지 않는다.

만약 확장자 명이 iconDict안에 있다면 배경화면을 바꿔주고 없다면 기본 Icon으로 바꿔준다.

ChangeBackGround함수

 

Make 버튼 함수와 Cancel은 다른 생성 기능과 저번과 로직은 같아서 전체 코드로 남기고 설명은 안하겠다.

 

전체 코드

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public struct FolderInfo
{
    public string size;
    public FileInfo[] fileArray;
    public DirectoryInfo directory;
}
public class FileTree : EditorWindow
{
    [SerializeField]
    private VisualTreeAsset m_VisualTreeAsset = default;

    #region IconDict
    
    private readonly (string, string)[] _iconNameList 
        = new (string, string)[24]
    {
        (".cs", "cs Script Icon"),
        (".shader", "Shader Icon"),
        (".mat", "Material Icon"),
        (".asset", "TextAsset Icon"),
        (".prefab", "Prefab Icon"),
        (".unity", "SceneAsset Icon"),
        (".anim", "AnimationClip Icon"),
        (".fbx", "ModelImporter Icon"),
        (".png", "RawImage Icon"),
        (".vfx", "VisualEffect Icon"),
        (".compute", "ComputeShader Icon"),
        (".mask", "AvatarMask Icon"),
        (".avatar", "Avatar Icon"),
        (".overrideController", "AnimatorOverrideController Icon"),
        (".renderTexture", "RenderTexture Icon"),
        (".lighting", "Light Icon"),
        (".timeline", "TimelineAsset Icon"),
        (".signal", "SignalAsset Icon"),
        (".preset", "Preset Icon"),
        (".uxml", "UxmlScript Icon"),
        (".uss", "UssScript Icon"),
        (".asmdef", "AssemblyDefinitionAsset Icon"),
        (".asmref", "AssemblyDefinitionReferenceAsset Icon"),
        (".unitypackage", "UnityLogo"),
    };

    private Dictionary<string, Texture> _iconDict;

    #endregion

    #region USSClass

    private readonly string _fileVisual = "file-visual";
    private readonly string _fileNameLabel = "file-name-label";
    private readonly string _fileIconLabel = "file-icon";

    #endregion

    #region MyElement

    private Foldout _rootFoldout;
    private TextField _nameField;
    private VisualElement _fileCreateList;
    private VisualElement _currentVisualFileList;
    private Label _folderNameLabel;
    private Label _typeNameLabel;
    
    #endregion

    /// <summary>
    /// Foldout에 따라 Directory를 반환하는 Dictionary
    /// </summary>
    private Dictionary<Foldout, FolderInfo> _directoryInfoDict;
    /// <summary>
    /// 현재 Directory
    /// </summary>
    private DirectoryInfo _currentDirectory;

    /// <summary>
    /// 만들어진 FileVisual Element를 저장하는 Dict
    /// </summary>
    private Dictionary<string, VisualElement> _savedCreateElementDict;
    
    private Type _currentType;
    private Action<ScriptableObject> _endCallback;
    private StringBuilder sb = new StringBuilder();

    /// <summary>
    /// 기본 Info 설정
    /// </summary>
    /// <param name="type"></param>
    /// <param name="endCallback"></param>
    public void SetInfo(Type type, Action<ScriptableObject> endCallback)
    {
        _currentType = type;
        _endCallback = endCallback;

        sb.Clear();
        sb.Append($"Current Type : {_currentType.Name}");
        _typeNameLabel.text = sb.ToString();
    }

    /// <summary>
    /// 생성 함수
    /// </summary>
    public void CreateGUI()
    {
        _iconDict = new();
        _directoryInfoDict = new();
        _savedCreateElementDict = new();
        foreach (var item in _iconNameList)
        {
            _iconDict.Add(item.Item1, EditorGUIUtility.IconContent(item.Item2).image);
        }
        
        VisualElement root = rootVisualElement;
        root.style.flexGrow = 1;
        
        VisualElement labelFromUXML = m_VisualTreeAsset.Instantiate();
        root.Add(labelFromUXML);

        
        _rootFoldout =  root.Q<Foldout>("RootFoldout");
        _nameField = root.Q<TextField>("FileNameField");
        _fileCreateList = root.Q<VisualElement>("FileVisualList");
        _folderNameLabel = root.Q<Label>("FolderNameLabel");
        _typeNameLabel = root.Q<Label>("TypeNameLabel");

        _rootFoldout.RegisterValueChangedCallback(HandleValueChangedFoldoutEvent);
        _rootFoldout.text = "Asset";

        FolderInfo folderInfo = new FolderInfo();
        DirectoryInfo rootDirectory = new DirectoryInfo(Application.dataPath);
        folderInfo.size = GetFileSize(FolderSize(rootDirectory));
        folderInfo.fileArray = rootDirectory.GetFiles();
        folderInfo.directory = rootDirectory;
        _directoryInfoDict.Add(_rootFoldout, folderInfo);

        root.Q<Button>("CreateBtn").clicked += HandleCreateBtnClickEvent;
        root.Q<Button>("CancelBtn").clicked += HandleCancelBtnClickEvent;
    }
    
    /// <summary>
    /// 폴더 사이즈 구하는 함수
    /// 하위 디렉토리 다 돌면서 크기를 구합니다.
    /// </summary>
    /// <param name="d">디렉토리</param>
    /// <returns></returns>
    private long FolderSize(DirectoryInfo directory)
    {
        long size = 0;
        // 파일 사이즈.
        FileInfo[] fis = directory.GetFiles();
        foreach (FileInfo fi in fis)
        {
            size += fi.Length;
        }
        // 하위 디렉토리 사이즈.
        DirectoryInfo[] dis = directory.GetDirectories();
        foreach (DirectoryInfo di in dis)
        {
            size += FolderSize(di);
        }
        return size;
    }


    #region Event Method

    /// <summary>
    /// 만들기 버튼을 눌렀다면 실행되는 Event
    /// </summary>
    private void HandleCreateBtnClickEvent()
    {
        ScriptableObject newItem = CreateInstance(_currentType);
        Guid typeGuid = Guid.NewGuid();
        if (_nameField.text.Length == 0)
        {
            newItem.name = typeGuid.ToString();
            Debug.Log($"a random GUID was assigned due to the absence of input.");
            Debug.Log($"random GUID : {typeGuid}");
        }
        else
        {
            newItem.name = _nameField.text;
        }
        
        string path = _currentDirectory.FullName;
        path = path.Substring(path.IndexOf("Assets", StringComparison.Ordinal));
        AssetDatabase.CreateAsset(newItem,
            $"{path}/{newItem.name}.asset");
            
        Debug.
            Log($"Success Create SO, Name : {newItem.name} Path : {path}/{newItem.name}.asset");
            
        AssetDatabase.SaveAssets();
        _endCallback?.Invoke(newItem);
        Close();
    }
    
    private void HandleCancelBtnClickEvent()
    {
        Close();
    }

    /// <summary>
    /// Foldout의 Value가 변경 되었을 때 실행 되는 이벤트
    /// 만약 자식이 이미 만들어졌다면 (Directory 캐싱) _currentDirectory, Foldout 만 변경시켜준다.
    /// </summary>
    /// <param name="evt"></param>
    private void HandleValueChangedFoldoutEvent(ChangeEvent<bool> evt)
    {
        if (evt.newValue)
        {
            if (evt.target is Foldout foldout)
            {
                FolderInfo folderInfo = _directoryInfoDict[foldout];
                DirectoryInfo directoryInfo = folderInfo.directory;
                _currentDirectory = directoryInfo;
            
                // Create Child
                if (foldout.childCount <= 0)
                {
                    foreach (var directory in directoryInfo.GetDirectories())
                    {
                        Foldout childFoldout = new Foldout();
                        childFoldout.value = false;
                    
                        childFoldout.text = directory.Name;

                        FolderInfo newFolderInfo = new FolderInfo();
                        newFolderInfo.size = GetFileSize(FolderSize(directory));
                        newFolderInfo.fileArray = directory.GetFiles();
                        newFolderInfo.directory = directory;
                    
                        _directoryInfoDict.Add(childFoldout, newFolderInfo);

                        if (directory.GetFiles().Length <= 0)
                        {
                            childFoldout.toggleOnLabelClick = false;
                        }
                    
                        foldout.Add(childFoldout);
                    }
                }
            
                MakeFileVisual(folderInfo.fileArray, folderInfo.directory.Name, folderInfo.size.ToString());
            }
        }
        else
        {
            if (evt.target is Foldout foldout)
            {
                foreach (var child in foldout.Children())
                {
                    if (child is Foldout childFoldout)
                    {
                        childFoldout.value = false;
                    }
                } 
            }
        }
    }


    #endregion
    
    /// <summary>
    /// 실제 File의 비주얼 만들어 주는 함수
    /// Dict에 없다면 만들어주고 있다면 그걸 띄워줌
    /// </summary>
    /// <param name="fileInfoArray">파일 정보 Array</param>
    /// <param name="folderName">폴더 이름</param>
    /// <param name="folderSize">폴더 크기</param>
    private void MakeFileVisual(FileInfo[] fileInfoArray, string folderName, string folderSize)
    {
        if(_currentVisualFileList != null)
            ChangeDisplayType(_currentVisualFileList, DisplayStyle.None);
        
        if (_savedCreateElementDict.TryGetValue(folderName, out var element))
        {
            ChangeDisplayType(element, DisplayStyle.Flex);
            
            _currentVisualFileList = element;
            sb.Clear();
            sb.Append(folderName);
            sb.Append("- size : ");
            sb.Append(folderSize);
            _folderNameLabel.text = sb.ToString();
            return;
        }
        
        VisualElement rootElement = new VisualElement();
        rootElement.name = folderName;
        
        foreach (FileInfo fileInfo in fileInfoArray)
        {
            if(fileInfo.Extension == ".meta") continue;
            
            VisualElement fileVisual = new VisualElement();
            fileVisual.AddToClassList(_fileVisual);
            Label fileNameLabel = new Label();
            fileNameLabel.AddToClassList(_fileNameLabel);
            fileNameLabel.text = fileInfo.Name;
            VisualElement icon = new VisualElement();
            icon.AddToClassList(_fileIconLabel);

            string extension = fileInfo.Extension.ToLower();
            if (_iconDict.TryGetValue(extension, out var value))
            {
                ChangeBackGroundImage(icon, value);
            }
            else
            {
                ChangeBackGroundImage(icon, EditorGUIUtility.IconContent("TextAsset Icon").image);
            }
            
            fileVisual.Add(icon);
            fileVisual.Add(fileNameLabel);
            
            rootElement.Add(fileVisual);
        }
        _fileCreateList.Add(rootElement);
        _savedCreateElementDict.Add(folderName, rootElement);
        _currentVisualFileList = rootElement;

        sb.Clear();
        sb.Append(folderName);
        sb.Append("- size : ");
        sb.Append(folderSize);
        _folderNameLabel.text = sb.ToString();
    }
    
    /// <summary>
    /// Display 변경해주는 편의성 함수
    /// </summary>
    /// <param name="element">target</param>
    /// <param name="displayStyle">Flex = true, None = false</param>
    private void ChangeDisplayType(VisualElement element, DisplayStyle displayStyle){
        var display = element.style.display;
        display.value = displayStyle;
        element.style.display = display;
    }

    /// <summary>
    /// 백그라운드 변경해주는 편의성 함수
    /// </summary>
    /// <param name="element">target</param>
    /// <param name="texture">image</param>
    private void ChangeBackGroundImage(VisualElement element, Texture texture)
    {
        var background = element.style.backgroundImage.value;
        background.texture = texture as Texture2D;
        element.style.backgroundImage = background;
    }
    
    /// <summary>
    /// 파일 사이즈 구하기
    /// </summary>
    /// <param name="byteCount"></param>
    /// <returns></returns>
    private string GetFileSize(double byteCount)
    {
        string size = "0 Bytes";

        sb.Clear();
        if (byteCount >= 1073741824.0)
        {
            sb.Append(String.Format("{0:##.##}", byteCount / 1073741824.0));
            sb.Append(" GB");
            
        }
        else if (byteCount >= 1048576.0)
        {
            sb.Append(String.Format("{0:##.##}", byteCount / 1048576.0));
            sb.Append(" MB");
        }
        else if (byteCount >= 1024.0)
        {
            sb.Append(String.Format("{0:##.##}", byteCount / 1024.0));
            sb.Append(" KB");
        }
        else if (byteCount > 0 && byteCount < 1024.0)
        {
            sb.Append(byteCount.ToString());
            sb.Append(" Bytes");
        }
        
        size = sb.ToString();
        return size;
    }
}

 

이렇게 한 뒤

SOManagementWindow의 MakeBtn의 clicked를 구독 받는 함수에 다음 코드만 짜주면 된다. 그럼 끝!

 

결과물

너무 잘된다 ㅠㅠ

 

'Editor' 카테고리의 다른 글

[Unity, Editor] SOManagementWindow 제작 일기 Part.3  (0) 2025.05.15
[Unity, Editor] SOManagementWindow 제작 일기 Part.2  (0) 2025.04.25
[Unity, Editor] SOManagementWindow 제작 일기 Part.1  (0) 2025.04.25
[졸업 작품, Unity]Item EditorWindow로 관리하기  (0) 2025.03.18
[Editor] 적 생성 Editor Window 코드 부분 제작  (0) 2024.10.10
  1. 만든 거
  2. 구현부
  3. Uxml
  4. USS
  5. Script
  6. 전체 코드
  7. 결과물
'Editor' 카테고리의 다른 글
  • [Unity, Editor] SOManagementWindow 제작 일기 Part.3
  • [Unity, Editor] SOManagementWindow 제작 일기 Part.2
  • [Unity, Editor] SOManagementWindow 제작 일기 Part.1
  • [졸업 작품, Unity]Item EditorWindow로 관리하기
HK1206
HK1206
고3 게임 개발자의 개발 일지
  • HK1206
    GGM-LJS
    HK1206
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • Unity (16)
      • Shader (1)
      • Editor (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
HK1206
[Unity,Editor] SOManagementWindow 제작 일기 Part.4

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.