Byn's Research Note

AI based Mixed Reality, Human-Computer Interaction

↓ My Web CV & Portfolio 자세히보기

카테고리 없음

Unity Programming [5] : 유니티 소프트웨어 설계 (Attribute, Gizmos)

JaehyeonByun 2024. 12. 27. 13:55

Attribute

 

Unity의 Attribute는 스크립트의 동작이나 Inspector의 표시 방식을 제어하기 위해 제공되는 메타데이터이다. 이들은 클래스, 필드, 메서드에 부여되어 Unity 에디터와 런타임에서 특정 동작을 수행하거나 사용자 경험을 개선하는 데 사용된다. 이를 통해 스크립트의 실행 순서를 조정하거나, Inspector에서 필드를 숨기거나 표시하고, 데이터 직렬화를 제어하거나, 개발 도구를 확장할 수 있다. 아래는 주요 Unity Attribute를 코드 예제와 함께 정리한 것이다.

 

[ExecuteAlways]

 

[ExecuteAlways]는 스크립트를 Play 모드와 에디터 모드 모두에서 실행되도록 설정한다. 이를 통해 Unity 에디터에서 작업 자동화, 실시간 업데이트, 또는 상태 동기화 기능을 구현할 수 있다.

using UnityEngine;

[ExecuteAlways]
public class ExampleExecuteAlways : MonoBehaviour
{
    void Update()
    {
        Debug.Log("This runs in both Edit and Play mode!");
    }
}

 

더보기

1. 오브젝트 자동 정렬

 

[ExecuteAlways]를 사용하면 에디터 모드에서 GameObject를 자동으로 정렬할 수 있다. 예를 들어, 자식 오브젝트를 특정 간격으로 배치하거나 정렬 상태를 유지하는 데 사용할 수 있다.

using UnityEngine;

[ExecuteAlways]
public class AutoAlignChildren : MonoBehaviour
{
    [SerializeField] private float spacing = 1.0f;

    void Update()
    {
        // 모든 자식 오브젝트를 일정한 간격으로 정렬
        for (int i = 0; i < transform.childCount; i++)
        {
            Transform child = transform.GetChild(i);
            child.localPosition = new Vector3(0, i * spacing, 0);
        }
    }
}

 

이 스크립트를 빈 GameObject에 추가하면, 자식 오브젝트들이 에디터 모드에서도 자동으로 정렬된다.

 

2. 씬에서 실시간 데이터 업데이트

 

UI 텍스트나 특정 값을 에디터 모드에서도 실시간으로 갱신하여 확인해야 할 때 사용할 수 있다.

using UnityEngine;
using UnityEngine.UI;

[ExecuteAlways]
public class DisplayGameObjectCount : MonoBehaviour
{
    [SerializeField] private Text objectCountText;

    void Update()
    {
        if (objectCountText != null)
        {
            // 씬에 존재하는 GameObject의 수를 표시
            objectCountText.text = $"GameObjects in Scene: {FindObjectsOfType<GameObject>().Length}";
        }
    }
}

 

씬에 존재하는 GameObject의 수를 실시간으로 텍스트 UI에 표시하여, 에디터 작업 중에도 씬의 상태를 확인할 수 있다.

 

3. 컬러 변화를 실시간으로 미리보기

 

[ExecuteAlways]를 사용하여 Material의 색상 변경 효과를 에디터에서 바로 확인할 수 있다.

 

using UnityEngine;

[ExecuteAlways]
public class ColorCycler : MonoBehaviour
{
    [SerializeField] private Renderer targetRenderer;
    [SerializeField] private Color baseColor = Color.white;
    [SerializeField] private float cycleSpeed = 1.0f;

    void Update()
    {
        if (targetRenderer != null && targetRenderer.material != null)
        {
            float t = Mathf.PingPong(Time.time * cycleSpeed, 1.0f);
            targetRenderer.material.color = Color.Lerp(baseColor, Color.red, t);
        }
    }
}

 

: 이 스크립트를 적용하면 에디터에서도 Material의 색상이 시간에 따라 변화하는 모습을 실시간으로 확인할 수 있다.

 

4. 간단한 에디터 Gizmo 표시

 

씬 뷰에서 특정 오브젝트의 상태를 시각적으로 표시하기 위해 OnDrawGizmos를 사용할 수 있다. [ExecuteAlways]와 함께 사용하면 오브젝트의 상태가 에디터 모드에서도 실시간으로 업데이트된다.

 
using UnityEngine;

[ExecuteAlways]
public class DrawGizmoForRadius : MonoBehaviour
{
    [SerializeField] private float radius = 5.0f;

    void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, radius);
    }
}

 

에디터에서 특정 오브젝트의 영향을 미치는 영역(예: 스킬 범위나 충돌 반경)을 확인할 수 있다.

 

5. 프로퍼티 자동 보정

게임 개발 중 특정 값이 일정 범위를 벗어나지 않도록 자동으로 보정할 수 있다.

using UnityEngine;

[ExecuteAlways]
public class PropertyClamper : MonoBehaviour
{
    [SerializeField] private Vector3 scaleLimits = new Vector3(10, 10, 10);

    void Update()
    {
        // 스케일 값을 제한
        transform.localScale = Vector3.Min(transform.localScale, scaleLimits);
    }
}

 

에디터에서 GameObject의 크기를 조정할 때 자동으로 제한 범위 내로 조정되어 값이 과도하게 설정되는 것을 방지할 수 있다.

 

[HideInInspector]

 

[HideInInspector]는 필드를 Inspector에서 숨기지만, Unity의 직렬화 시스템에는 그대로 포함된다. 불필요한 정보를 숨기고 코드의 가독성을 높이는 데 유용하다.

using UnityEngine;

public class ExampleHideInInspector : MonoBehaviour
{
    [HideInInspector]
    public int hiddenValue = 42;
}
더보기

HideInInspector는 디자이너와의 협업에서 자주 사용된다.

using UnityEngine;

public class DebugExample : MonoBehaviour
{
    public float speed = 10f; // 디자이너가 설정

    [HideInInspector]
    public Vector3 debugPosition; // 디버깅 용도로 사용

    private void Update()
    {
        debugPosition = transform.position + Vector3.forward * speed * Time.deltaTime;
    }
}

 

예를 들어, 디자이너는 speed만 설정할 수 있으며, debugPosition은 Inspector에 표시되지 않지만 코드에서 디버깅에 활용된다

 

[SerializeField]

 

[SerializeField]는 private 또는 protected 필드를 Inspector에 노출하고 싶을 때 사용한다. 이를 통해 캡슐화를 유지하면서도 Inspector에서 값 조정을 할 수 있다.

 

using UnityEngine;

public class ExampleSerializeField : MonoBehaviour
{
    [SerializeField]
    private int serializedValue = 100;
}

 

[RequireComponent]

 

[RequireComponent]는 스크립트가 특정 컴포넌트에 의존할 때, 해당 컴포넌트를 자동으로 추가하도록 강제한다. 이는 컴포넌트 간의 의존성을 보장하고, 런타임 오류를 방지한다.

 

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class ExampleRequireComponent : MonoBehaviour
{
}

 

더보기

1. 이동 스크립트에서 Rigidbody 강제

 
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : MonoBehaviour
{
    private Rigidbody rb;
    public float speed = 5f;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        float moveX = Input.GetAxis("Horizontal") * speed;
        float moveZ = Input.GetAxis("Vertical") * speed;
        rb.velocity = new Vector3(moveX, rb.velocity.y, moveZ);
    }
}

 

2. 오디오 관리에서 AudioSource 강제

using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class SoundManager : MonoBehaviour
{
    private AudioSource audioSource;

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();
        audioSource.playOnAwake = false;
    }

    public void PlaySound(AudioClip clip)
    {
        audioSource.clip = clip;
        audioSource.Play();
    }
}

 

[AddComponentMenu]

 

[AddComponentMenu]는 스크립트를 Add Component 메뉴에 특정 경로로 등록하여 사용자 정의 스크립트를 더 쉽게 관리할 수 있도록 한다. 프로젝트가 커질수록 사용자 정의 스크립트가 많아지는데, 이를 체계적으로 관리할 수 있다.

 

using UnityEngine;

[AddComponentMenu("Custom/Example Script")]
public class ExampleAddComponentMenu : MonoBehaviour
{
}

 

더보기

카테고리 계층화

using UnityEngine;

[AddComponentMenu("Game Mechanics/Movement/Player Controller")]
public class PlayerController : MonoBehaviour
{
    public float moveSpeed = 5f;

    private void Update()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        Vector3 movement = new Vector3(horizontal, 0, vertical) * moveSpeed * Time.deltaTime;
        transform.Translate(movement);
    }
}

 

PlayerController 스크립트는 "Add Component" 메뉴의 Game Mechanics > Movement > Player Controller 경로에 등록된다. 여러 카테고리를 생성하여 논리적으로 스크립트를 정리할 수 있다.

 

 

 

동일한 이름의 스크립트 분리

using UnityEngine;

[AddComponentMenu("Physics/Basic Rigidbody Controller")]
public class BasicRigidbodyController : MonoBehaviour
{
    public Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
    }
}

 

 

같은 이름의 스크립트를 서로 다른 카테고리에 분류하여 충돌을 방지한다. Add Component 메뉴의 Physics 카테고리에서 Basic Rigidbody Controller를 선택할 수 있다.

 

 

숨겨진 스크립트 만들기

using UnityEngine;

[AddComponentMenu("")]
public class HiddenScript : MonoBehaviour
{
    public int hiddenValue = 42;
}

 

 

HiddenScript는 "Add Component" 메뉴에 표시되지 않는다. 디자이너가 사용할 필요가 없거나, 내부적으로만 사용되는 스크립트를 숨길 때 유용하다.

 

 

[ContextMenu]

 

[ContextMenu]는 Inspector에서 특정 메서드를 호출할 수 있는 메뉴 항목을 추가한다. 디버깅이나 반복 작업을 자동화하는 데 유용하다.

using UnityEngine;

public class ExampleContextMenu : MonoBehaviour
{
    [ContextMenu("Print Message")]
    void PrintMessage()
    {
        Debug.Log("Context menu method called!");
    }
}

 

[Tooltip]

 

[Tooltip]은 Inspector에서 필드에 대한 설명을 제공하는 툴팁을 추가한다. 이를 통해 필드의 의미나 사용 방법을 명확히 전달할 수 있다.

using UnityEngine;

public class ExampleTooltip : MonoBehaviour
{
    [Tooltip("This is a tooltip for the health value.")]
    public int health = 100;
}

 

[Range]

[Range]는 필드 값의 범위를 슬라이더 형태로 Inspector에 표시한다. 이를 통해 데이터 범위를 제한하고 직관적으로 값을 설정할 수 있다.

using UnityEngine;

public class ExampleRange : MonoBehaviour
{
    [Range(0, 100)]
    public int sliderValue = 50;
}

 

[Header]

[Header]는 필드 그룹 위에 제목을 추가하여 Inspector를 시각적으로 정리하고 가독성을 높인다.

using UnityEngine;

public class ExampleHeader : MonoBehaviour
{
    [Header("Character Stats")]
    public int health = 100;
    public int mana = 50;
}

 

[Space]

 

[Space]는 Inspector에서 필드 사이에 여백을 추가하여 필드를 논리적으로 그룹화하거나 구분할 때 사용된다.

using UnityEngine;

public class ExampleSpace : MonoBehaviour
{
    public int firstValue;
    [Space]
    public int secondValue;
}

 

 

[DisallowMultipleComponent]

 

[DisallowMultipleComponent]는 동일한 스크립트를 같은 GameObject에 여러 개 추가하는 것을 방지한다. 이를 통해 중복 추가로 인한 런타임 오류를 방지할 수 있다.

using UnityEngine;

[DisallowMultipleComponent]
public class ExampleDisallowMultipleComponent : MonoBehaviour
{
}

 

[DefaultExecutionOrder]

 

[DefaultExecutionOrder]는 스크립트의 실행 순서를 설정한다. 이를 통해 스크립트 간의 실행 우선순위를 명확히 정의할 수 있다.

using UnityEngine;

[DefaultExecutionOrder(-10)]
public class ExampleDefaultExecutionOrder : MonoBehaviour
{
    void Start()
    {
        Debug.Log("This script runs earlier!");
    }
}

 

더보기

1. 입력 처리와 카메라 제어

 

입력 처리 스크립트가 캐릭터 이동을 처리한 뒤, 카메라가 그 이동에 따라 위치를 업데이트해야 한다면, 실행 순서를 조정해야 한다.

[DefaultExecutionOrder(-50)]
public class InputHandler : MonoBehaviour
{
    public Vector3 playerPosition;

    void Update()
    {
        playerPosition += new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        Debug.Log($"Player moved to: {playerPosition}");
    }
}

[DefaultExecutionOrder(50)]
public class CameraController : MonoBehaviour
{
    public Transform player;

    void LateUpdate()
    {
        transform.position = player.position + new Vector3(0, 10, -10);
        Debug.Log("Camera updated");
    }
}

 

InputHandler가 먼저 실행되어 플레이어의 위치를 업데이트한 뒤, CameraController가 해당 위치를 기반으로 카메라를 움직인다.

 

2. 복잡한 시스템 간의 상호작용

 

다수의 스크립트가 서로 종속적인 경우 [DefaultExecutionOrder]로 순서를 명시적으로 지정한다.

[DefaultExecutionOrder(-100)]
public class SystemA : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("System A initialized");
    }
}

[DefaultExecutionOrder(-50)]
public class SystemB : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("System B initialized");
    }
}

[DefaultExecutionOrder(0)]
public class SystemC : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("System C initialized");
    }
}

 

SystemA -> SystemB -> SystemC 순서로 Awake() 호출.

 

 

[System.Serializable]

 

[System.Serializable]는 클래스나 구조체를 Unity의 직렬화 시스템에 등록하여 Inspector에서 편집 가능하게 한다. 이를 통해 데이터를 그룹화하거나 구조화된 방식으로 관리할 수 있다.

using UnityEngine;

[System.Serializable]
public class CustomData
{
    public string name;
    public int value;
}

public class ExampleSerializable : MonoBehaviour
{
    public CustomData data;
}

 

[NonSerialized]

 

[NonSerialized]는 직렬화를 방지하여 Inspector에 필드가 나타나지 않도록 설정한다. 런타임에서만 사용하는 데이터나 직렬화가 필요 없는 데이터를 제외할 때 사용된다.

using UnityEngine;

public class ExampleNonSerialized : MonoBehaviour
{
    [NonSerialized]
    public int runtimeOnlyValue;
}

 

더보기

유니티에서 직렬화(Serialization)란 객체 데이터를 디스크에 저장하거나 네트워크를 통해 전송할 수 있도록, 혹은 유니티 에디터에서 사용자가 설정한 데이터를 유지하고 관리할 수 있도록 데이터 구조를 일련의 바이트로 변환하는 과정을 의미한다. 이 과정은 데이터를 저장하거나 로드할 때, 그리고 Unity Inspector에서 데이터를 편집하거나 볼 때 유용하다.

 

반대로 역직렬화(Deserialization)란 직렬화된 데이터를 원래의 객체 형태로 복원하는 과정을 의미한다. 즉, 저장되거나 전송된 데이터를 다시 객체로 변환하여 Unity 내에서 사용할 수 있게 만드는 것이다. 직렬화된 데이터를 파일, 네트워크, 또는 메모리에서 불러온 후 역직렬화를 통해 게임에서 활용 가능한 형태로 복원한다.

 

[CanEditMultipleObjects]

 

[CanEditMultipleObjects]는 커스텀 에디터에서 여러 GameObject의 속성을 동시에 편집할 수 있도록 설정한다. 이를 통해 작업 효율성을 크게 높일 수 있다.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects]
[CustomEditor(typeof(ExampleCanEditMultipleObjects))]
public class ExampleEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();
    }
}
public class ExampleCanEditMultipleObjects : MonoBehaviour
{
    public int value;
}

 

더보기

1. 적 캐릭터 설정

 

여러 적 캐릭터의 속성을 동시에 변경하고 싶을 때.

 

using UnityEngine;
using UnityEditor;

public class Enemy : MonoBehaviour
{
    public int health = 100;
    public float speed = 5.0f;
}

[CustomEditor(typeof(Enemy))]
[CanEditMultipleObjects] // 여러 객체 편집 가능 설정
public class EnemyEditor : Editor
{
    SerializedProperty healthProp;
    SerializedProperty speedProp;

    void OnEnable()
    {
        healthProp = serializedObject.FindProperty("health");
        speedProp = serializedObject.FindProperty("speed");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.PropertyField(healthProp, new GUIContent("Health"));
        EditorGUILayout.PropertyField(speedProp, new GUIContent("Speed"));

        serializedObject.ApplyModifiedProperties();
    }
}

 

여러 적 캐릭터를 선택한 후, Inspector에서 HealthSpeed를 동시에 변경 가능.

 

2. 환경 설정

 

씬 내 여러 오브젝트의 색상, 크기 등 공통 속성을 일괄 편집할 때.

 

using UnityEngine;
using UnityEditor;

public class EnvironmentObject : MonoBehaviour
{
    public Color objectColor = Color.white;
    public Vector3 objectScale = Vector3.one;
}

[CustomEditor(typeof(EnvironmentObject))]
[CanEditMultipleObjects]
public class EnvironmentObjectEditor : Editor
{
    SerializedProperty colorProp;
    SerializedProperty scaleProp;

    void OnEnable()
    {
        colorProp = serializedObject.FindProperty("objectColor");
        scaleProp = serializedObject.FindProperty("objectScale");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.PropertyField(colorProp, new GUIContent("Object Color"));
        EditorGUILayout.PropertyField(scaleProp, new GUIContent("Object Scale"));

        serializedObject.ApplyModifiedProperties();
    }
}

 

여러 환경 오브젝트를 선택하고, Inspector에서 색상과 크기를 일괄적으로 조정 가능.

 

[SelectionBase]

 

[SelectionBase]는 씬 뷰에서 오브젝트를 선택할 때 기본 선택 대상으로 지정한다. 복잡한 계층 구조에서 부모 오브젝트를 쉽게 선택하도록 돕는다.

 

using UnityEngine;

[SelectionBase]
public class ExampleSelectionBase : MonoBehaviour
{
}

 

더보기

a. 캐릭터 루트 오브젝트 지정

캐릭터가 여러 자식 오브젝트(메쉬, 콜라이더, 부착된 무기 등)를 가지고 있는 경우, 부모 GameObject를 기본 선택 대상으로 설정한다.

using UnityEngine;

[SelectionBase]
public class Character : MonoBehaviour
{
    // 캐릭터의 루트 오브젝트
    public string characterName = "Hero";
}

 

계층 구조:

  • Hero (부모)
    • Body (자식)
    • Sword (자식)

동작:

  • 씬 뷰에서 Body나 Sword를 클릭하면 부모 오브젝트인 Hero가 선택된다.

b. UI 루트 오브젝트 지정

 

UI 계층 구조에서 특정 컨테이너(GameObject)를 기본 선택 대상으로 설정한다.

 
using UnityEngine;

[SelectionBase]
public class UIContainer : MonoBehaviour
{
    public string containerName = "Main Panel";
}

 

계층 구조:

  • Main Panel (부모)
    • Header (자식)
    • Button (자식)

동작:

  • 씬 뷰에서 Header나 Button을 클릭하면 부모 오브젝트인 Main Panel이 선택된다.

c. 건축물과 같은 복잡한 오브젝트의 루트 설정

 

using UnityEngine;

[SelectionBase]
public class Building : MonoBehaviour
{
    public string buildingName = "House";
}

 

씬 뷰에서 Roof, Walls, Door를 클릭해도 부모 오브젝트인 House가 선택된다.