본문 바로가기
Development/C#

[멋쟁이사자처럼 부트캠프 TIL 회고] Unity 게임 개발 3기 12일차

by Mobics 2024. 12. 4.

 

목차


    자료구조

    Stack

    : Last In First Out (LIFO) 원칙을 따르는 자료구조

     

    Stack의 연산

    • Push(x) : 주어진 요소 x를 Stack의 맨 위에 추가한다.
    • Pop() : 스택이 비어있지 않으면 맨 위에 있는 요소를 삭제하고 반환한다.
    • Peek() : 스택이 비어있지 않으면 맨 위에 있는 요소를 반환한다.

    https://www.devkuma.com/docs/data-structure/stack/

     

    Stack 구현해보기

     

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class StackNode<T>
    {
        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>();
    
            stackNode.data = data;
    
            stackNode.prev = top;
            top = stackNode;
        }
    
        public T Pop()
        {
            if (top == null)
                return new T();
    
            var result = top.data;
            top = top.prev;
    
            return result;
        }
    
        public T Peek()
        {
            if (top == null)
            {
                return new T();
            }
    
            return top.data;
        }
    }
    public class StackExample : MonoBehaviour
    {
        void Start()
        {
            StackCustom<int> stack = new StackCustom<int>();
            stack.Push(1);
            stack.Push(2);
            stack.Push(3);
            
            stack.Pop();
            
            Debug.Log(stack.Pop());
            Debug.Log(stack.Peek());
        }
    }

     

    ※ where T : new() --> T는 new() 를 사용할 것이라는 뜻

     

    Stack 활용 (이동과 Undo)

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class StackExample : MonoBehaviour
    {
        // [NonSerialized] public float Speed = 3.0f;
        [SerializeField] private float Speed2 = 3.0f;
    
        private Stack<Vector3> position_stack = new Stack<Vector3>();
    
        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.S) || 
                Input.GetKeyDown(KeyCode.A) ||
                Input.GetKeyDown(KeyCode.D))
            {
                // movePos = Vector3.zero; --> GetKeyUp으로 기록했을 때 작성했던 코드
                position_stack.Push(transform.position);
            }
            
            // 왔던 포지션으로 되돌아가는 코드
            if (Input.GetKeyDown(KeyCode.Space))
            {
                if (position_stack.Count > 0)
                    transform.position = position_stack.Pop();
                    //transform.Translate ((position_stack.Pop() - transform.position)); --> 더 부드럽게 돌아가기
            }
            
            transform.position += movePos.normalized * (Speed2 * Time.deltaTime);
        }
    }

     

    • [SerializeField] : private 일지라도 Inspector에 노출이 되며, 다른 C# Class에서는 접근이 불가능하다.
    • [NonSerializeField] : public 일지라도 Inspector에 노출은 안되나, 다른 C# Class에서는 접근이 가능하다.

    ※ 움직였던 정보를 GetKeyUp으로 기록할 때, movePos = Vector3.zero; 를 작성했던 이유

    : 이동을 멈추게 하기 위해서, 0에는 뭘 곱해도 0이듯이 --> 순서를 내려도 되지만 순서로 조작하는 것보다 코드로 제어하는 것이 더 낫다.

     

    Vector3 vs transform

    • Vector3 : 좌표 기준으로 이동 (월드 좌표), 계산된 값
    • transform : Object가 바라보는 방향으로 이동 (로컬 좌표), 상수값

     

    Stack을 활용하여 Undo와 Redo 만들기

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Runtime.Serialization;
    using UnityEngine;
    
    public class StackStudy : MonoBehaviour
    {
        [SerializeField] private float speed = 10.0f;
        
        private Stack<Vector3> stack_position_undo = new Stack<Vector3>();
        private Stack<Vector3> stack_position_redo = new Stack<Vector3>();
    
        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.S) ||
                Input.GetKeyDown(KeyCode.A) ||
                Input.GetKeyDown(KeyCode.D))
            {
                stack_position_undo.Push(transform.position);
            }
    
            if (Input.GetKeyDown(KeyCode.Space)) // undo
            {
                if (stack_position_undo.Count > 0)
                {
                    stack_position_redo.Push(transform.position);
                    transform.position = stack_position_undo.Pop();
                }
            }
    
            if (Input.GetKeyDown(KeyCode.LeftShift)) // redo
            {
                if (stack_position_redo.Count > 0)
                {
                    stack_position_undo.Push(transform.position);
                    transform.position = stack_position_redo.Pop();
                }
            }
    
            transform.position += speed * Time.deltaTime * movePos.normalized;
        }
    }

    Stack을 활용하여 괄호 검사 만들기

    : 열린 괄호는 Stack에 Push하고, 닫힌 괄호를 만나면 스택의 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;
        }

     

    EditorWindow

    : 툴 만들기

    Attributes

    : Script에서 Class, Property 또는 함수 위에 명시하여 특별한 동작을 나타낼 수 있는 마커

    >> [MenuItem("Window/Scope Checker")] : 'Window'에 'Scope Checker'라는 이름의 메뉴 항목 추가

    괄호 검사를 하기 위한 툴 만들기

    >> 전체 코드

    using System.Collections;
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;
    
    public class ScopeChecker : EditorWindow // EditorWindow에서 파생
    {
        private string _text;
        
        [MenuItem("Window/Scope Checker")]
        public static void ShowWindow()
        {
            GetWindow<ScopeChecker>("Scope Checker");
        }
    
        private void OnGUI()
        {
            //EditorGUILayout.BeginHorizontal(); // 가로로 정렬하는 방법
            _text = EditorGUILayout.TextArea(_text, GUILayout.Height(300));
    
            if (GUILayout.Button("Check Scope")) // Button을 누르면 True가 반환
            {
                if (AreBracketsBalanced(_text))
                {
                    EditorUtility.DisplayDialog("Scope Checker", "Scope Check Success", "OK");
                }
                else
                {
                    EditorUtility.DisplayDialog("Scope Checker", "Scope Check Fail", "OK");
                }
            }
            
            //EditorGUILayout.BeginHorizontal(); // 가로로 정렬하는 방법 끝
        }
        
        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;
        }
    }

    간단한 해석

    >> GetWindow

    [MenuItem("Window/Scope Checker")] // 'Window'에 'Scope Checker'라는 이름의 메뉴 항목 추가
    public static void ShowWindow()
    {
        GetWindow<ScopeChecker>("Scope Checker"); // 열린 기능 창의 이름
    }

     

    >> EditorGUILayout.TextArea

    _text = EditorGUILayout.TextArea(_text, GUILayout.Height(300)); // 기능창 내부 디자인

    _text = EditorGUILayout.TextField("Text", _text, GUILayout.Width(300), GUILayout.Height(300));
    // Width : 가로 길이 고정

     

    >> EditorGUILayout.TextField

    _text = EditorGUILayout.TextField("Text", _text, GUILayout.Height(300)); // 기능창 내부 디자인

     

    >> GUILayout.Button

    if (GUILayout.Button("Check Scope")) // Button에 표시될 메세지, Button을 누르면 True가 반환
    {
        if (AreBracketsBalanced(_text))
        {
            EditorUtility.DisplayDialog("Scope Checker", "Scope Check Success", "OK");
            // EditorUtility.DisplayDialog("눌렀을 때 제목", "눌렀을 때 메세지", "확인 버튼 메세지");
        }
        else
        {
            EditorUtility.DisplayDialog("Scope Checker", "Scope Check Fail", "OK");
        }
    }


    Component 꾸미기

    : 변수를 편하게 정리하는 방법

    방법 1. Attributes 활용

    >> 전체 코드

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class LayoutComponent : MonoBehaviour
    {
        [Header("Life"), Tooltip("인생에 관한 변수입니다.")]
        public string data1;
        [Tooltip("인생에 관한 변수2입니다.")]
        public string data2;
        public string data3;
        
        [Header("Love"), Tooltip("사랑에 관한 변수입니다.")]
        public string data4;
        public string data5;
        public string data6;
        
        [Header("Power"), Range(0.1f, 5f)]
        public float data7;
    }

    간단한 해석

    >> [Header(" ")]

    : Inspector 상에서 간단하게 구분 가능

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class LayoutComponent : MonoBehaviour
    {
        [Header("Life")]
        public string data1;
        public string data2;
        public string data3;
        [Header("Love")]
        public string data4;
        public string data5;
        public string data6;
        [Header("Power")]
        public string data7;
    }

     

    >> [Tooltip(" ")]

    : 변수 칸에 마우스를 올렸을 때, 나오는 툴팁

    ※ [Tooltip(" ")]를 적은 한 줄만 적용된다.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
     
    public class LayoutComponent : MonoBehaviour
    {
        [Header("Life"), Tooltip("인생에 관한 변수입니다.")]
        public string data1;
        [Tooltip("인생에 관한 변수2입니다.")]
        public string data2;
        public string data3;
        
        [Header("Love"), Tooltip("사랑에 관한 변수입니다.")]
        public string data4;
        public string data5;
        public string data6;
        
        [Header("Power"), Tooltip("힘에 관한 변수입니다.")]
        public string data7;
    }

     

    >> [Range(Min, Max)]

    ※ data 타입이 int or float 이어야 한다.

    [Header("Power"), Range(0.1f, 5f)]
    public float data7;

     

    방법 2. CustomEditor 사용

    : Inspector의 Default Layout을 사용자가 선택하는 Editor Control로 대체하는 별도의 Script

     

    >> 전체 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;
    
    [CustomEditor(typeof(LayoutComponent))] // 'LayoutComponent' Script 사용
    public class LayoutCompEditor : Editor // 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()
        {
            //base.OnInspectorGUI();
            
            serializedObject.Update();
    
            foldState = EditorGUILayout.Foldout(foldState, "Layout");
    
            if (foldState)
            {
                EditorGUI.indentLevel++; // 들여쓰기
                
                EditorGUILayout.LabelField("Life"); // 라벨 추가
                
                EditorGUI.indentLevel++;
                data1Property.stringValue = EditorGUILayout.TextField("data1Property", data1Property.stringValue);
                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--;
                
                EditorGUI.indentLevel--;
    
                if (GUILayout.Button("Check Scope")) // 버튼 추가
                {
                    
                }
            }
            
            serializedObject.ApplyModifiedProperties();
        }
    }

    간단한 해석

    >> SerializedProperty

    : SerializedObject의 속성에 대한 접근을 제공하는 클래스

     

    >> OnEnable()

    : Start()와 비슷하지만 Component가 켜졌을 경우에만 사용

     

    >> serializedObject.FindProperty(" ");

    : serializedObject는 Unity Editor에서 객체를 직렬화된 형태로 다룰 수 있도록 해주는 클래스

    : FindProperty()는 해당 속성이 존재하면 SerializedProperty 객체를 반환하고, 그렇지 않으면 null을 반환한다.

    --> serializedObject. FindProperty("propertyName") : SerializedObject에서 "propertyName"이라는 이름의 속성을 찾는다.

     

    >> base.OnInspectorGUI();

    : 실행하면 이 Script에 작성되는 내용은 기본값에 추가될 것이라는 것을 명시

    ※ 이를 명시하지 않으면 기존의 Inspector에 표시되는 내용은 모두 사라지고 이곳에 기록된 것만 보인다.

     

    >> serializedObject.Update();

    : 직렬화된 데이터를 최신 상태로 갱신한다.

     

    >> serializedObject.ApplyModifiedProperties();

    : SerializedObject에서 변경된 데이터를 실제 대상 객체에 반영한다.

    --> 주로 serializedObject.Updata()와 함께 사용되며, 직렬화된 데이터의 동기화를 위해 중요하다.

     

    >> EditorGUILayout.Foldout(foldState, "Layout");

    : 접었다/펼쳤다를 가능하게 만들어준다.

    --> EditorGUILayout.Foldout(bool type 변수, "이름");

     

    >> EditorGUI.indentLevel

    ++ : 들여쓰기

    -- : 원상복귀

     

    >> EditorGUILayout.LabelField(" ")

    : Label 띄우기 --> Label : 단순히 텍스트를 화면에 출력하기 위한 UI 요소

    ※ 여기선 헤더는 제자리에 두고 자식들만 들여쓰기 위해 사용

     

    >> Inspector에 띄우는 법 2가지

    1.

    data1Property.stringValue = EditorGUILayout.TextField("data1Property", data1Property.stringValue);

     

    2.

    EditorGUILayout.PropertyField(data3Property);

    ※ UnityDocument - EditorGUI

    https://docs.unity3d.com/ScriptReference/EditorGUI.html

     

    Unity - Scripting API: EditorGUI

    Success! Thank you for helping us improve the quality of Unity Documentation. Although we cannot accept all submissions, we do read each suggested change from our users and will make updates where applicable. Close

    docs.unity3d.com