2022. 7. 21. 03:20ㆍ유니티 unity
이번에는 아이템 에디터를 만들어봅시다
UI Builder로 UI들을 꾸며봅시다
https://docs.unity3d.com/kr/2021.3/Manual/UIE-HowTo-CreateEditorWindow.html
아이콘 에셋은 무료 에셋 사용했습니다
https://assetstore.unity.com/packages/2d/gui/icons/rpg-inventory-icons-56687
이번엔 저장할 데이터는 스크립터블 오브젝트로 하겠습니다.
스크립트 하나를 만들어봅시다
using System;
using UnityEngine;
public enum ItemType
{
Weapon,
Armor,
Portion,
Food,
}
[CreateAssetMenu(fileName = "ItemInfo", menuName = "ScriptableObject/ItemInfo",order = 1)]
public class ItemInfo : ScriptableObject
{
public string id = Guid.NewGuid().ToString().ToUpper();
//고유 아이디
public string name;
//이름
public Sprite icon;
//아이콘
public ItemType itemType;
//타입
public string info;
//정보
public int gold;
//돈
}
이제 스크립터블 오브젝트 하나를 만들어봅니다.
Data 폴더에 Item 폴더를 만들고 거기에 생성시켰습니다.
스크립터블오브젝트를 만들었으면 다시 UIBuilder로 돌아가서 바인딩을 할 것이기 때문에 적어줍니다. 변수 이름이랑 똑같이 적어야 합니다
https://medium.com/pocs/%EB%B0%94%EC%9D%B8%EB%94%A9-binding-4a4a2f641b27
요소들의 이름도 하나하나 적어줍시다.
이제 에디터를 화면에 보이도록 해봅니다.
스크립트 하나를 만드세요
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
public class ItemEditor : EditorWindow
{
[MenuItem("Tools/ItemEditor")]
public static void ShowExample()
{
ItemEditor wnd = GetWindow<ItemEditor>();
//불러옵니다.
wnd.titleContent = new GUIContent("ItemEditor");
Vector2 size = new Vector2(1000f,400f);
wnd.minSize = size;
wnd.maxSize = size;
//사이즈 설정
}
private void CreateGUI()
{
VisualElement root = rootVisualElement;
//최상의 화면을 변수 root로 설정합니다
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
("Assets/Editor/ItemEditor.uxml");
rootVisualElement.Add(visualTree.Instantiate());
//uxml를 불러옵니다.
}
}
이제 요소들 상호작용 하나하나 추가만 하면 끝납니다.
이제 아이템 리스트를 불러와서 하나씩 채워야 하는데
저는 UGUI에서 구현을 하려면 UI프리 팹을 만들어서 생성한 뒤 넣는 방법을 사용했었는데
똑같은 방법으로 구현해보겠습니다.
UIBuilder를 만들어서 새로운 uxml를 만들어봅니다.
이제 한번 테스트를 해봐야 하니 원래 uxml를 불러온 뒤
Library 창에서 우리가 아까 만든 uxml를 불러와 봅니다
우리가 넣을 곳에 한번 넣어봅시다
잘 나옵니다.
원하는 그림이 나올 때까지 수정을 한 뒤 다시 지워줍시다
이제 이걸 동적으로 생성시켜주면 됩니다.
아이템 수가 많을 수도 있으니 스크롤을 이용해서 넣는 게 좋을 거 같습니다.
UIElements에서는 ListView라는 게 있는데 스크롤 뷰에 있는 목록들을 바인딩해주는 뷰인데 우리도 데이터들을 불러와서 보여줘야 하기 때문에 ListView를 쓰면 될 거 같습니다.
https://docs.unity3d.com/ScriptReference/UIElements.ListView.html
그러면 스크립트로 ListView를 만들어 봅시다
스크립터블 오브젝트 스크립트 이름인 ItemInfo들을 불러와야 하기 때문에 리스트 변수를 하나 만들어주고
우리가 프리팹을 만든 uxml를 불러와야 하기 때문에 그 uxml 경로 변수를 만들어 줘야 합니다.
public static List<ItemInfo> itemInfoList=new List<ItemInfo>();
//데이터 담을 리스트
private VisualTreeAsset itemPrefab;
//프리팹uxml
private void CreateGUI()
{
//......
itemPrefab = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/itemprefab.uxml");
//경로 설정
}
우리가 리스트뷰를 생성시켜야 할 곳은
아이템 리스트 밑이기 때문에
저곳에 생성을 시켜줘야 하기 때문에
itemList라는 요소를 불러와서 그 자식으로 생성시키면 됩니다.
private ListView itemViewList;
//리스트뷰
private VisualElement itemList;
//아이템 리스트
private Sprite basicItemIcon;
//기본아이콘
private readonly float itemHeight = 40;
//아이템 하나마다 리스트뷰 높이가 커져야하기 때문
private void CreateGUI()
{
//....
itemList = rootVisualElement.Q<VisualElement>("ItemList");
//이름인 ItemList를 가져옵니다.
basicItemIcon = (Sprite)AssetDatabase.LoadAssetAtPath(
"Assets/RPG_inventory_icons/f.png", typeof(Sprite));
CreateListView();
//기본 아이콘을 설정해줍니다.
}
void CreateListView()
{
}
리스트뷰에 대해서 알아보면
itemsSource는 리스트이고
itemHeight는 높이
makeItem는 visualElement 즉 우리가 만든 uxml를 넣으면 될 거 같고
bindItem는 바인딩할 목록을 넣어주면 될꺼같습니다.
참고자료
https://forum.unity.com/threads/listview-problem.978783/
값이 변경되어야 할 곳은 두 가지가 있는데 아이콘과 이름입니다.
함수를 작성해 봅시다.
private void CreateListView()
{
Func<VisualElement> makeItem = () => itemPrefab.CloneTree();
//우리가 프리팹으로 만든 uxml
Action<VisualElement, int> bindItem = (e, i) =>
//바인딩 할 목록은 아이콘과 이름만 필요
{
e.Q<VisualElement>("Icon").style.backgroundImage =
itemInfoList[i] == null ? basicItemIcon.texture : itemInfoList[i].icon.texture;
//만약에 아이콘이 null이라면 기본 아이콘으로 설정하고 아니면 해당 아이콘으로 설정한다.
e.Q<Label>("Name").text = itemInfoList[i].name;
//이름을 설정한다.
};
itemViewList = new ListView(itemInfoList, 40, makeItem, bindItem);
//ListView를 만듭니다.
itemViewList.selectionType = SelectionType.Single;
//선택은 하나만 되게 설정.
itemViewList.style.height = itemInfoList.Count * itemHeight;
//리스트뷰의 높이를 아이템갯수에 맞춰서 높이를 설정합니다.
itemList.Add(itemViewList);
}
자 이제 리스트뷰를 생성시켰으니
이제는 우리가 아까 만든 스크립터블 오브젝트를 불러와야 하는데 불러오는 함수를 만들어봅시다
불러올 때는 Directory.GetFiles를 이용해서 배열로 경로를 가져옵니다.
https://docs.microsoft.com/ko-kr/dotnet/api/system.io.directory.getfiles?view=net-6.0
private void CreateGUI()
{
//....
itemPrefab = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/itemprefab.uxml");
LoadData();
//로드해주는 함수 추가
itemList = rootVisualElement.Q<VisualElement>("ItemList");
basicItemIcon = (Sprite)AssetDatabase.LoadAssetAtPath(
"Assets/RPG_inventory_icons/f.png", typeof(Sprite));
CreateListView();
}
private void LoadData()
{
itemInfoList.Clear();
//리스트 정리
string[] Items = Directory.GetFiles("Assets/Data/Item", "*.asset",
SearchOption.AllDirectories);
//모든 경로를 가져옵니다.
foreach (string item in Items)
{
string cleanedPath = item.Replace("\\", "/");
// \\를 / 로 바꿔줍니다. Assets\\Data 가 아닌 Assets/Data 로 쓰기때문에
itemInfoList.Add((ItemInfo)AssetDatabase.LoadAssetAtPath(cleanedPath,
typeof(ItemInfo)));
//리스트안에 넣어줍니다. ItemInfo는 우리가 만든 스크립터블 오브젝트 입니다.
}
}
잘 나옵니다.
크기를 살짝 줄여보겠습니다.
텍스트 아이템 리스트 Padding 조절하고
프리팹 uxml에서
name은 Flex Grow 0 -> 1
Padding 5 2 0 0
조절하고
나머지 보면서 조절했습니다.
이제 클릭 시 오른쪽에 있는 정보들을 보여주게 해야 하는데
클릭하기 전에는 숨겼다가
클릭하면 보여줘야 합니다.
클릭하면 이벤트를 반응해야 하는데
https://docs.unity3d.com/ScriptReference/UIElements.BaseVerticalCollectionView.html
에 보시면
onSelectedIndicesChange
onSelectionChange
onSelectionChanged
가 있는데
우리는 선택한 아이템이 어떤 건지 알아야 하기 때문에
onSelectionChange를 쓰겠습니다.
public event Action<IEnumerable<object>> onSelectionChange;
우리가 위에서 Biding Path에 넣은걸 이제 바인딩해줘야 합니다.
private ScrollView itemScrollView;
//Infos 스크롤뷰
private VisualElement ItemIcon;
//큰 아이콘
private ItemInfo currentItemInfo;
//현재 선택한 아이템데이터
private void CreateGUI()
{
//...
itemScrollView = rootVisualElement.Q<ScrollView>("Infos");
//스크롤뷰를 가져옵니다.
itemScrollView.style.visibility = Visibility.Hidden;
//맨처음에는 정보를 숨겨야합니다.
ItemIcon = itemScrollView.Q<VisualElement>("Icon");
//아이템 아이콘을 가져옵니다.
}
private void CreateListView()
{
//....
itemViewList.onSelectionChange += SelecFunc;
//선택시 SelecFunc 가 실행합니다.
}
private void SelecFunc(IEnumerable<object> selectedItems)
{
currentItemInfo = (ItemInfo)selectedItems.First();
//리스트뷰는 여러개 선택이 가능하지만 우리는 싱글로 선택하게 했습니다.
//선택된것중에 첫번째를 가져옵니다.
SerializedObject so = new SerializedObject(currentItemInfo);
itemScrollView.Bind(so);
//바인딩해줍니다.
if (currentItemInfo.icon !=null)
{
ItemIcon.style.backgroundImage = currentItemInfo.icon.texture;
//아이콘 설정해줍니다.
}
itemScrollView.style.visibility = Visibility.Visible;
//이제 보여줘야합니다.
}
이제 맵 에디터에서 수정을 해도 바로바로 연동이 됩니다.
이제 New 버튼과 Del 버튼만 함수를 추가하면 끝입니다.
private void CreateGUI()
{
//.....
rootVisualElement.Q<Button>("NewBtn").clicked += NewBtn_Click;
rootVisualElement.Q<Button>("DelBtn").clicked += DelBtn_Click;
}
void NewBtn_Click()
{
}
void DelBtn_Click()
{
}
새로운 아이템은 AssetDatabase.CreateAsset를 이용해서 새로운 파일을 만들어주면 됩니다.
public static extern void CreateAsset([NotNull("ArgumentNullException")] UnityEngine.Object asset, string path);
void NewBtn_Click()
{
ItemInfo Item = CreateInstance<ItemInfo>();
Item.name = "ItemInfo";
//기본 이름
Item.icon = basicItemIcon;
//기본 아이콘
AssetDatabase.CreateAsset(Item,$"Assets/Data/Item/{Item.id}.asset");
//생성시킵니다.
itemInfoList.Add(Item);
//리스트안에 넣습니다.
itemViewList.Rebuild();
//갱신시켜줍니다. 화면에 보이게
itemViewList.style.height = itemInfoList.Count * itemHeight;
//크기를 조절합니다.
}
삭제 버튼은 선택된 아이템의 경로를 가져오고 삭제시키면 됩니다.
AssetDatabase.GetAssetPath
AssetDatabase.DeleteAsset
public static extern string GetAssetPath(UnityEngine.Object assetObject);
public static extern bool DeleteAsset(string path);
void DelBtn_Click()
{
if (currentItemInfo==null)
{
return;
}
string path = AssetDatabase.GetAssetPath(currentItemInfo);
//현재 선택된 아이템 경로를 가져옵니다.
AssetDatabase.DeleteAsset(path);
//삭제
itemInfoList.Remove(currentItemInfo);
//리스트도 삭제
itemViewList.Rebuild();
//리스트 갱신. 화면에 보이게
itemScrollView.style.visibility = Visibility.Hidden;
//정보창은 안보이게 꺼짐.
currentItemInfo = null;
}
버튼들이 작동이 잘 됩니다.
이제 정보창에서 이름을 바꾸면 데이터들은 바뀌지만 왼쪽 리스트 이름들은 그대로입니다.
우리가 따로 수정을 해주지 않았기 때문에 바꿔줘야 합니다.
1번 글에서 썼던 RegisterValueChangedCallback를 이용해서 바로 바꾸게 합시다.
public void CreateGUI()
{
itemScrollView.Q<TextField>("Name")
.RegisterValueChangedCallback(evt =>
{
currentItemInfo.name = evt.newValue;
//이름 변경
itemViewList.Rebuild();
//갱신
});
itemScrollView.Q<ObjectField>("IconFiled")
.RegisterValueChangedCallback(evt =>
{
Sprite newSprite = evt.newValue as Sprite;
//아이콘을 가져옵니다.
currentItemInfo.icon = newSprite == null ? basicItemIcon : newSprite;
//아무것도없다면 기본으로 아니라면 아이콘을 수정합니다.
ItemIcon.style.backgroundImage =
newSprite == null ? basicItemIcon.texture : newSprite.texture;
itemViewList.Rebuild();
//갱신
});
}
이제 여러 가지 기능들을 이용해서 UI들을 꾸미거나
원하는 기능들을 넣어서 맵 에디터를 꾸미시면 됩니다.
디버그 창을 열어서 동적으로 생성한 요소들을 직접 확인해가면서 스크립트로 작성한 것들을 디버그가 가능합니다.
예시는 2번 글에서 만들었던 에디터입니다.
저런 식으로 동적으로 만들었던 버튼들의 정보를 직접 확인하면서 스크립트로 넣었던 게 어떤 문제가 있는지 바로바로 파악이 가능합니다.
스크립트 보기
ItemEditor.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
public class ItemEditor : EditorWindow
{
public static List<ItemInfo> itemInfoList=new List<ItemInfo>();
//데이터 담을 리스트
private VisualTreeAsset itemPrefab;
//프리팹uxml
private ListView itemViewList;
//리스트뷰
private VisualElement itemList;
//아이템 리스트
private Sprite basicItemIcon;
//기본아이콘
private readonly float itemHeight = 40;
//아이템 하나마다 리스트뷰 높이가 커져야하기 때문
private ScrollView itemScrollView;
//Infos 스크롤뷰
private VisualElement ItemIcon;
//큰 아이콘
private ItemInfo currentItemInfo;
//현재 선택한 아이템데이터
[MenuItem("Tools/ItemEditor")]
public static void ShowExample()
{
ItemEditor wnd = GetWindow<ItemEditor>();
//불러옵니다.
wnd.titleContent = new GUIContent("ItemEditor");
Vector2 size = new Vector2(1000f,400f);
wnd.minSize = size;
wnd.maxSize = size;
//사이즈 설정
}
private void CreateGUI()
{
VisualElement root = rootVisualElement;
//최상의 화면을 변수 root로 설정합니다
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>
("Assets/Editor/ItemEditor.uxml");
rootVisualElement.Add(visualTree.Instantiate());
//uxml를 불러옵니다.
itemPrefab = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/itemprefab.uxml");
LoadData();
itemList = rootVisualElement.Q<VisualElement>("ItemList");
basicItemIcon = (Sprite)AssetDatabase.LoadAssetAtPath(
"Assets/RPG_inventory_icons/f.png", typeof(Sprite));
CreateListView();
itemScrollView = rootVisualElement.Q<ScrollView>("Infos");
//스크롤뷰를 가져옵니다.
itemScrollView.style.visibility = Visibility.Hidden;
//맨처음에는 정보를 숨겨야합니다.
ItemIcon = itemScrollView.Q<VisualElement>("Icon");
//아이템 아이콘을 가져옵니다.
rootVisualElement.Q<Button>("NewBtn").clicked += NewBtn_Click;
rootVisualElement.Q<Button>("DelBtn").clicked += DelBtn_Click;
itemScrollView.Q<TextField>("Name")
.RegisterValueChangedCallback(evt =>
{
currentItemInfo.name = evt.newValue;
//이름 변경
itemViewList.Rebuild();
//갱신
});
itemScrollView.Q<ObjectField>("IconFiled")
.RegisterValueChangedCallback(evt =>
{
Sprite newSprite = evt.newValue as Sprite;
//아이콘을 가져옵니다.
currentItemInfo.icon = newSprite == null ? basicItemIcon : newSprite;
//아무것도없다면 기본으로 아니라면 아이콘을 수정합니다.
ItemIcon.style.backgroundImage =
newSprite == null ? basicItemIcon.texture : newSprite.texture;
itemViewList.Rebuild();
//갱신
});
}
private void CreateListView()
{
Func<VisualElement> makeItem = () => itemPrefab.CloneTree();
//우리가 프리팹으로 만든 uxml
Action<VisualElement, int> bindItem = (e, i) =>
//바인딩 할목록은 아이콘과 이름만 필요
{
e.Q<VisualElement>("Icon").style.backgroundImage =
itemInfoList[i] == null ? basicItemIcon.texture : itemInfoList[i].icon.texture;
//만약에 아이콘이 null이라면 기본 아이콘으로 설정하고 아니면 해당 아이콘으로 설정한다.
e.Q<Label>("Name").text = itemInfoList[i].name;
//이름을 설정한다.
};
itemViewList = new ListView(itemInfoList, 40, makeItem, bindItem);
//ListView를 만듭니다.
itemViewList.selectionType = SelectionType.Single;
//선택은 하나만 되게 설정.
itemViewList.style.height = itemInfoList.Count * itemHeight;
//리스트뷰의 높이를 아이템갯수에 맞춰서 높이를 설정합니다.
itemList.Add(itemViewList);
itemViewList.onSelectionChange += SelecFunc;
}
void NewBtn_Click()
{
ItemInfo Item = CreateInstance<ItemInfo>();
Item.name = "ItemInfo";
//기본 이름
Item.icon = basicItemIcon;
//기본 아이콘
AssetDatabase.CreateAsset(Item,$"Assets/Data/Item/{Item.id}.asset");
//생성시킵니다.
itemInfoList.Add(Item);
//리스트안에 넣습니다.
itemViewList.Rebuild();
//갱신시켜줍니다. 화면에 보이게
itemViewList.style.height = itemInfoList.Count * itemHeight;
//크기를 조절합니다.
}
void DelBtn_Click()
{
if (currentItemInfo==null)
{
return;
}
string path = AssetDatabase.GetAssetPath(currentItemInfo);
//현재 선택된 아이템 경로를 가져옵니다.
AssetDatabase.DeleteAsset(path);
//삭제
itemInfoList.Remove(currentItemInfo);
//리스트도 삭제
itemViewList.Rebuild();
//리스트 갱신. 화면에 보이게
itemScrollView.style.visibility = Visibility.Hidden;
//정보창은 안보이게 꺼짐.
currentItemInfo = null;
}
private void SelecFunc(IEnumerable<object> selectedItems)
{
currentItemInfo = (ItemInfo)selectedItems.First();
//리스트뷰는 여러개 선택이 가능하지만 우리는 싱글로 선택하게 했습니다.
//선택된것중에 첫번째를 가져옵니다.
SerializedObject so = new SerializedObject(currentItemInfo);
itemScrollView.Bind(so);
//바인딩해줍니다.
if (currentItemInfo.icon !=null)
{
ItemIcon.style.backgroundImage = currentItemInfo.icon.texture;
//아이콘 설정해줍니다.
}
itemScrollView.style.visibility = Visibility.Visible;
//이제 보여줘야합니다.
}
private void LoadData()
{
itemInfoList.Clear();
//리스트 정리
string[] Items = Directory.GetFiles("Assets/Data/Item", "*.asset",
SearchOption.AllDirectories);
//모든 경로를 가져옵니다.
foreach (string item in Items)
{
string cleanedPath = item.Replace("\\", "/");
// \\를 / 로 바꿔줍니다. Assets\\Data 가 아닌 Assets/Data 로 쓰기때문에
itemInfoList.Add((ItemInfo)AssetDatabase.LoadAssetAtPath(cleanedPath,
typeof(ItemInfo)));
//리스트안에 넣어줍니다. ItemInfo는 우리가 만든 스크립터블 오브젝트 입니다.
}
}
}
ItemInfo.cs
using System;
using UnityEngine;
public enum ItemType
{
Weapon,
Armor,
Portion,
Food,
}
[CreateAssetMenu(fileName = "ItemInfo", menuName = "ScriptableObject/ItemInfo",order = 1)]
public class ItemInfo : ScriptableObject
{
public string id = Guid.NewGuid().ToString().ToUpper();
//고유 아이디
public string name;
//이름
public Sprite icon;
//아이콘
public ItemType itemType;
//타입
public string info;
//정보
public int gold;
//돈
}
깃허브 주소
'유니티 unity' 카테고리의 다른 글
Unity로 node.js WebSocket 통신하기 (socket.io,Mysql 이용해서 오목게임 만들기)-3 (0) | 2022.07.25 |
---|---|
Unity로 node.js WebSocket 통신하기 (socket.io를 이용해서 룸형식 채팅방 구현)-2 (4) | 2022.07.23 |
unity ToolKit 와 UI Builder 이용해서 커스텀에디터만들기(응용편 소코반 맵 에디터만들기)-2 (0) | 2022.07.21 |
unity ToolKit 와 UI Builder 이용해서 커스텀에디터만들기-1 (0) | 2022.07.20 |
Unity로 node.js WebSocket 통신하기 -1 (0) | 2022.07.19 |