12일차 요약
스택(Stack)
- LIFO형식의 자료구조이다.
- 활용 예시
- 웹 브라우저 뒤로 가기 : 가장 나중에 열린 페이지부터 뒤로 가기를 실행합니다.
- 문서작업에서 Ctrl+Z : 가장 나중에 수정한 내역부터 되돌립니다.
- 후위 표기법 계산
- 재귀적 알고리즘
Unity Editor Customizing
Unity Editor는 커스터 마이징하여 편리하게 사용하는 것이 가능하다.
Menu Item을 만드는 방법
[MenuItem("Window/Scope Checker")]
- Attribute를 이용하여 Menu에 추가한다.
- OnGUI라는 Unity Event함수를 통해 화면을 어떻게 구성할지 설정한다.
Inspector를 설정하는 법
- Script를 작성할 때, Attribute를 설정하여 Editor에서 표시될 방식을 설정할 수 있다.
- Editor를 상속받은 클래스에서 원하는 타입의 Component가 어떻게 Editor에 표시될지 재정의할 수 있다.
스택(Stack)
스택은 Last In First Out (LIFO) 원칙을 따르는 자료구조이다.
스택 구현
public class StackNode<T>
{
public StackNode(T d)
{
data = d;
}
public T data;
public StackNode<T> prev;
}
public class StackCustom<T> where T : new()
{
public StackNode<T> top;
public void Push(T data)
{
var stackNode = new StackNode<T>(data);
stackNode.prev = top;
top = stackNode;
}
public T Pop()
{
if (top == null) return default(T);
var result = top.data;
top = top.prev;
return result;
}
public T Peek()
{
if (top == null) return default(T);
return top.data;
}
}
스택 사용 예제
오버워치의 트레이서처럼 자신이 n초전에 있던 위치로 되돌가 가게 하는 기능을 구현할 수 있다.
자신의 위치를 스택에 담아 저장하다 특정 키가 눌렸을 때 되돌아가는 기능이다.
public class StackExample : MonoBehaviour
{
public float Speed = 3.0f;
[SerializeField] private Stack<Vector3> positionStack = new Stack<Vector3>();
void Start()
{
}
private void Update()
{
Vector3 movePos = Vector3.zero;
if (Input.GetKey(KeyCode.W))
{
movePos += transform.forward;
}
if (Input.GetKey(KeyCode.S))
{
movePos -= transform.forward;
}
if (Input.GetKey(KeyCode.A))
{
movePos += transform.right;
}
if (Input.GetKey(KeyCode.D))
{
movePos -= transform.right;
}
if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.D))
{
movePos = Vector3.zero;
positionStack.Push(transform.position);
}
if (Input.GetKeyDown(KeyCode.Space))
{
if (positionStack.Count != 0)
{
transform.position = positionStack.Pop();
}
}
transform.position += movePos.normalized * Speed * Time.deltaTime;
}
}
자신의 위치를 기록하는 경우는 이동키가 눌렸을 때 기록한다.
스택 활용
괄호쌍이 올바른지 확인하는 과정에서 스택을 활용할 수 있다.
기본적인 규칙은 여는 괄호가 나오면 스택에 추가하고 닫는 괄호가 나오면 스택의 top을 제거한다.
만약, 닫는 괄호가 나왔을 때 스택이 비어있다면 올바른 괄호쌍이 올바르지 못한 것이다.
public bool AreBracketsBalanced(string expression)
{
Stack<char> stack = new Stack<char>();
foreach (char c in expression)
{
if (c == '(' || c == '[' || c == '{')
{
stack.Push(c);
}
else if (c == ')' || c == ']' || c == '}')
{
if (stack.Count == 0)
return false;
char top = stack.Pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{'))
{
return false;
}
}
}
return stack.Count == 0;
}
Unity Customizing
C# Script를 통해 Editor를 커스터마이징 할 수 있다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class ScopeChecker : EditorWindow
{
private string Text;
[MenuItem("Window/Scope Checker")]
public static void ShowWindow()
{
GetWindow<ScopeChecker>("Scope Checker");
}
private void OnGUI()
{
Text = EditorGUILayout.TextArea(Text, GUILayout.Height(300));
if (GUILayout.Button("Check Scope"))
{
if (AreBracketsBalanced(Text))
{
EditorUtility.DisplayDialog("Scrope Checker", "Scope Check Success", "OK");
}
else
{
EditorUtility.DisplayDialog("Scrope Checker", "Scope Check Fail", "OK");
}
}
}
public bool AreBracketsBalanced(string expression)
{
Stack<char> stack = new Stack<char>();
foreach (char c in expression)
{
if (c == '(' || c == '[' || c == '{')
{
stack.Push(c);
}
else if (c == ')' || c == ']' || c == '}')
{
if (stack.Count == 0)
return false;
char top = stack.Pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{'))
{
return false;
}
}
}
return stack.Count == 0;
}
}
EditorWindow를 상속받아서 구현한다.
ShowWindow는 창을 띄우는 함수이다. 객체가 존재하지 않더라도 실행되어야 하는 함수이기 때문에 static으로 선언한다.
ShowWindow위에 다음과 같은 표현을 PropertyAttribute라고 한다.
[MenuItem("Window/Scope Checker")]
MenuItem은 에디터의 메뉴 버튼을 만드는 Attribute라고 생각하면 된다.
ShowWindow에서는 GetWindow를 통해 ScopeChecker 타입의 윈도우를 받아와 화면에 띄워준다.
화면에 띄워질 화면을 구성하는 함수가 OnGUI이다.
OnGUI에서는 검사할 TextArea와 검사를 실행할 Button을 추가하는 것을 볼 수 있다.
만약 버튼이 눌린다면 if문 안으로 들어와 코드가 실행된다.
완성된 모습은 다음과 같다.
추가로 다음과 같은 명령어로 Layout을 조절할 수 있다.
EditorGUILayout.BeginHorizontal();
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginVertical();
EditorGUILayout.EndVertical();
Attribute
유용한 Attribute를 정리해 보자.
[Header("Layout")]
변수의 Header를 지정하여 Inspector에서 구분되도록 만들 수 있다.
[Tooltip("설명")]
변수에 마우스를 호버했을 때 출력되는 툴팁을 설정할 수 있다.
[Range(min, max)]
Inspector상에서 min~max로 범위를 지정하는 Slider가 출력되도록 한다.
이외에도 많은 Attribute가 있다.
https://docs.unity3d.com/kr/530/Manual/Attributes.html
Editor Custom
이미 만들어진 컴포넌트에 대해 에디터를 변경하고 싶을 때가 있다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(LayoutComp))]
public class LayoutCompEditor : Editor
{
private SerializedProperty data1Property;
private SerializedProperty data2Property;
private SerializedProperty data3Property;
private SerializedProperty data4Property;
private SerializedProperty data5Property;
private SerializedProperty data6Property;
private SerializedProperty data7Property;
private bool foldState = false;
private void OnEnable()
{
data1Property = serializedObject.FindProperty("data1");
data2Property = serializedObject.FindProperty("data2");
data3Property = serializedObject.FindProperty("data3");
data4Property = serializedObject.FindProperty("data4");
data5Property = serializedObject.FindProperty("data5");
data6Property = serializedObject.FindProperty("data6");
data7Property = serializedObject.FindProperty("data7");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
foldState = EditorGUILayout.Foldout(foldState, "Layout");
if (foldState)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(data1Property);
data2Property.stringValue = EditorGUILayout.TextField("Data 2", data2Property.stringValue);
EditorGUILayout.PropertyField(data3Property);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(data4Property);
EditorGUI.indentLevel--;
EditorGUILayout.PropertyField(data5Property);
EditorGUILayout.PropertyField(data6Property);
EditorGUILayout.PropertyField(data7Property);
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
// base.OnInspectorGUI();
}
}
Editor를 상속받은 클래스를 하나 만든다.
이때, Attribute로 CustomEditor를 설정하면 해당하는 type의 Component의 Inspector를 변경할 수 있다.
해당 Component에서 어떠한 변수를 사용하고 있는지 모를 때 이미 직렬화된 Property를 통해 자동으로 가져오도록 할 수 있다.
SerializedProperty라는 타입으로 변수를 만들면 된다.
이는 에디터에 띄우고 싶은 것을 결정하는 작업이라고 보면 된다.
이후, Editor클래스에 있는 OnInspectorGUI함수를 오버라이딩해 Inspector상에서 어떻게 보여줄지 재정의한다.
이때, 기존 Inspector를 사용하고 싶은 경우 base.OnInspectorGUI함수를 호출하면 된다.
Inspector에 보여주는 방법은 두 가지 이다.
- EditorGUILayout.PropertyField(data1Property);
- 해당 Property의 타입을 자동으로 계산하여 Inspector에 표시
- 변경할 Component에서 설정한 Attribute들이 그대로 적용됨
- data2Property.stringValue = EditorGUILayout.TextField("Data 2", data2Property.stringValue);
- 원하는 방식으로 지정하여 Inspector에 표시
- Property의 Value와 표시될 GUI의 타입이 일치하지 않으면 오류가 발생한다.
부가적인 기능으로 Foldout을 통해 그룹화하여 접을 수 있다.
또한, indentLevel을 이용하여 들여 쓰기 같은 효과도 줄 수 있다.
마지막으로 새로 설정한 요소들이 업데이트될 수 있도록 업데이트해 주는 구문이 필요하다.
serializedObject.ApplyModifiedProperties();