파일을 통해 데이터를 저장하여 불러오거나 해도 되지만

유니티에는 스크립터블 오브젝트라는 형태로 데이터 저장 및 사용이 가능합니다.

 

using UnityEngine;

[CreateAssetMenu(fileName = "MyEditorData", menuName = "Custom ScriptableObject/MyEditorData")]
public class MyEditorData : ScriptableObject
{
    public string filePath;
}

 

우선 string형 하나만 입력받도록 설정해줍니다. 클래스는 ScriptableObject 를 상속 받기만 하면 됩니다.

생성을 위해 CreateAssetMenu 를 설정해주겠습니다.

fileName : 생성될 파일명

munuName : 메뉴에 나타날 이름입니다.

 

CreateAssetMenu를 통해 해당 이름으로 파일 생성이 가능해졌습니다.

 

 

해당 파일을 클릭하면 Inspector에서 보기 및 편집이 가능합니다.

 

유니티를 껏다켜도 해당 정보는 저장되어 있게 됩니다.

이제 이것을 통해 윈도우 에디터 값들을 저장하며 만들 예정입니다!

using UnityEngine;
using UnityEditor;

public class MyEditor : EditorWindow
{
    private static EditorWindow window;
    private static float width = 600;
    private static float height = 600;

    [MenuItem("CustomWindow/MyEditor")] //상단 메뉴에 추가됩니다.
    public static void Open ()
    {
        window = GetWindow<MyEditor>();

        Rect main = EditorGUIUtility.GetMainWindowPosition(); //전체 화면 크기를 갖고 옵니다.

        window.minSize = window.maxSize = new Vector2(width, height); //시작 사이즈를 width, height로 만들어 주고

        window.minSize = new Vector2(200, 200);
        window.maxSize = new Vector2(main.width, main.height); //제한을 풀어줍니다.

        //window.position에 x, y 좌표와 width, height가 들어있습니다.
        window.position = new Rect((main.width - width) * 0.5f, (main.height - height) * 0.5f, width, height);
    }

    private void OnGUI()
    {
        GUILayout.Label("Hello!");
    }
}

 

[MenuItem("CustomWindow/MyEditor")] 를 통해 메뉴가 상단에 추가 된 모습

 

윈도우 창이 실행 된 모습. 가운데에 위치하게 됩니다. (전체화면으로 한 경우만!)

유니티 에디터에 대해 독학 중인데 엄청난 뻘짓들을 하고 있습니다..

가끔씩 이상한데 꽂혀서 시간을 많이 쓰네요.. ㅠ_ㅠ

 

찾아보면 기능들이 다양하고 많네요 ' - '@

시간이 되면 만져 본 것들을 하나씩 정리해보기!

커스텀 에디터를 만들 때 가장 중요한 항목은 사용자의 확인, 취소에 대한 입력을 받는 것입니다.

그리고 실수로 잘 못 된 버튼을 누르는 것을 방지하거나 무언가 오류가 생길시 알림을 주는 기능도 할 수 있습니다.

 

C#으로 치면 MessageBox 기능이고

유니티에서는 UnityEditor.EditorUtility.DisplayDialog 를 통해 메시지 출력이 가능합니다.

 

에디터 모드에서도 가능하지만 실시간 게임 중에도 출력이 가능하기에 우선

플레이 버튼 시 메시지가 뜨도록 MonoBehaviour에 작성하여 실행시켜보았습니다.

 

파라미터 개수에 따라 확인만 가능하거나 취소까지 가능하며

해당 결과를 bool 값으로 받을 수 있습니다.

using UnityEngine;
using UnityEditor;

public class ShowMessage : MonoBehaviour
{
    void Start()
    {
        bool result = EditorUtility.DisplayDialog("제목", "메시지", "확인", "취소");

        if (result)
        {
            EditorUtility.DisplayDialog("클릭 결과", "확인 메시지가 클릭되었습니다.", "확인");
        }
        else
        {
            EditorUtility.DisplayDialog("클릭 결과", "취소 메시지가 클릭되었습니다.", "확인");
        }
    }
}

 

초기 메시지

 

카메라에 붙여서 사용할 수 있는 코드입니다.

카메라가 보는 방향으로 이동 할 수 있게 만들었고

( transform.forward * vertical + transform.right * horizontal )

 

마우스 오른쪽 클릭을 통해 회전을 할 수 있는 코드입니다.

회전은 마우스 가로 움직임이 세로축에 영향을 주고

마우스 세로 움직임이 가로축에 영향을 줍니다.

MouseX => rotation.y에 영향

MouseY => rotation.x에 영향

( Quaternion.Euler(transform.rotation.x - mouseY, transform.rotation.y + mouseX, 0.0f) )

 

private new Transform transform; 를 선언하고 transform = GetComponent<Transform> 를 해주었는데

transform을 캐싱하여 성능을 높일 수 있다고 해서 사용했습니다! ㅎㅎ

using UnityEngine;

public class CameraController : MonoBehaviour
{
    public float rotateSpeed = 5.0f;
    public float moveSpeed = 3.5f;
    public float limitAngle = 70.0f;

    private new Transform transform;
    private bool isRotate;
    private float mouseX;
    private float mouseY;

    private void Start()
    {
        transform = GetComponent<Transform>();

        mouseX = transform.rotation.eulerAngles.y;  //마우스 가로(x)는 세로축(y) 이 중심
        mouseY = -transform.rotation.eulerAngles.x; //마우스 세로(y)는 가로축(-x) 이 중심
    }

    private void Update()
    {
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        //카메라를 기준으로 앞, 옆으로 이동시킵니다.
        Vector3 movement = transform.forward * vertical + transform.right * horizontal;
        movement = movement.normalized * (Time.deltaTime * moveSpeed);
        transform.position += movement;

        //마우스 오른쪽 클릭 시 회전시킴
        if (Input.GetMouseButtonDown(1))
        {
            isRotate = true;
        }
        if (Input.GetMouseButtonUp(1))
        {
            isRotate = false;
        }

        if (isRotate)
        {
            Rotation();
        }
    }

    public void Rotation()
    {
        mouseX += Input.GetAxis("Mouse X") * rotateSpeed; // AxisX = Mouse Y
        mouseY = Mathf.Clamp(mouseY + Input.GetAxis("Mouse Y") * rotateSpeed, -limitAngle, limitAngle);

        //mouseX (가로 움직임) 은 Y축에 영향을 줌
        //mouseY (세로 움직임) 은 X축에 영향을 줌
        transform.rotation = Quaternion.Euler(transform.rotation.x - mouseY, transform.rotation.y + mouseX, 0.0f);
    }
}

 

1. Box Collider

박스 콜라이더는 위와 같이 x좌표, y좌표가 박스 내부에 있는지 판별을 하게 됩니다.

x좌표와 y좌표 모두 내부에 속해있으면 충돌로 판정하게 됩니다.

(만약 3차원이라면 z좌표까지도 내부인지 확인해주면 되겠습니다.)

public bool IsBoxInner ()
{
    float x = clickPosition.x;
    float y = clickPosition.y;

    return xMin <= x && x <= xMax &&
            yMin <= y && y <= yMax;
}

 

2. Circle Collider

원형 콜라이더는 거리를 구해서 반지름 값보다 같거나 작은지를 확인하면 됩니다.

간단한 계산 덕분에 가장 빠른 충돌체 처리로 알려져 있습니다.

피타고라스 정리를 이용하여 거리를 계산합니다.

가로^2 + 세로^2 = 빗변^2

위와 같이 빗변을 구하고 루트를 씌워주면 원하는 빗변값을 구할 수 있고,

빗변이 원의 반지름 이내라면 원과 접촉했다고 판별할 수 있습니다.

최적화는 빗변에 루트를 씌우지 않고 반지름을 제곱해주어 판별하면 되겠습니다.

//방법1
public bool IsCircleInner ()
{
    float distance = 
    Mathf.Sqrt((clickPosition.x - colliderPosition.x) * (clickPosition.x - colliderPosition.x)
             + (clickPosition.y - colliderPosition.y) * (clickPosition.y - colliderPosition.y));
    
    //float distance = Vector2.Distance(colliderPosition, clickPosition);

    return distance <= circleRadius;
}

//방법2
public bool IsCircleInner ()
{
    float distance = 
               (clickPosition.x - colliderPosition.x) * (clickPosition.x - colliderPosition.x)
             + (clickPosition.y - colliderPosition.y) * (clickPosition.y - colliderPosition.y);
    
    return distance <= circleRadius * circleRadius;
}

 

3. Triangle Collider

예전에 프로그래밍 고수형이 넥슨 회사 면접 볼 때 나왔던 문제라고 저한테 풀어보라고 했었습니다.

점이 삼각형 내부(충돌)인지 아닌지 아닌지 어떻게 판별을 해야될까요?

처음엔 삼각형의 기울기를 생각했었는데 선형대수학에 나오는 벡터의 외적을 이용하면 쉽게 풀 수 있습니다.

2차원 좌표이기에 외적을 구해보면 z값에 대해서만 변화가 있게 됩니다.

이 z값이 양수인지 음수인지를 따져보면 현재 위치에서 방향을 알 수 있게 됩니다.

먼저 상대적인 좌표로 만들어줍니다. 기준점 (-2, -2)으로부터의 거리로 만들어주고

두 점의 외적을 구해봅니다. 노란 화살표가 기준이 되고, 보라색 화살표가 왼쪽인지 오른쪽인지 판별을 합니다.

이렇게 3개의 꼭지점을 기준으로 위치를 판별해줍니다.

결과는 왼쪽, 오른쪽, 왼쪽 이 나왔고 점은 밖에 있다는 것이 판별되었습니다.

삼각형 내부에 있는 경우는 모두 동일한 방향에 있는 경우입니다.

아래는 이미 포지션 값을 정해놓고, 시뮬레이션 단계마다 판별을 하는 코드입니다.

전체 코드는 깃허브에 유니티 프로젝트와 함께 올려두었습니다.

private void DeterminePosition ()
{
    Vector3[] positions = triangle_clickPoint.GetTrianglePositions();

    Vector3 startPosition = positions[simulationStep - 1]; //기준 위치
    Vector3 endPosition = positions[simulationStep];       //기준 라인
    Vector3 basePoint = endPosition - startPosition;       //1번 화살표 구하기
        
    endPosition = triangle_clickPoint.GetClickPosition();  //(노란점)클릭 위치
    Vector3 clickPoint = endPosition - startPosition;      //2번 화살표 구하기

    //벡터의 외적을 통해 z값을 구합니다. 아래코드와 동일합니다.
    //float cross = Vector3.Cross(basePoint, clickPoint).z;
    
    float cross = basePoint.x * clickPoint.y - basePoint.y * clickPoint.x;
    
    if (cross >= 0)
    {
        //Is Left
    }
    else
    {
        //Is Right
    }
}

전체 코드와 유니티 프로젝트는 깃허브에 따로 올려두겠습니다!

https://github.com/dlaehdeod/Practice

 

GitHub - dlaehdeod/Practice: Play File and Sources

Play File and Sources. Contribute to dlaehdeod/Practice development by creating an account on GitHub.

github.com

 

버튼을 누르면 무언가 동작을 하게 해봅시다.

2가지 방법으로 연결이 가능합니다.

 

1. 코드로 연결하기

using UnityEngine;
using UnityEngine.UI;

public class UI_Script : MonoBehaviour
{
    public Button testButton;

    private void Start()
    {
        //방법1. 무명 메소드 람다식을 통해 적용
        testButton.onClick.AddListener(() => TestButtonDown());
        
        //방법2. delegate를 이용해 적용
        testButton.onClick.AddListener(delegate { TestButtonDown(); });
        
        //방법3. (방법1 에서 UnityAction으로 함수를 갖고 있는 방법입니다.)
        UnityEngine.Events.UnityAction action = () => TestButtonDown();
        testButton.onClick.AddListener(action);
        
        //방법4. (방법2 에서 UnityAction으로 함수를 갖고 있는 방법입니다.)
        UnityEngine.Events.UnityAction action2 = (delegate { TestButtonDown(); });
        testButton.onClick.AddListener(action2);
    }

    public void TestButtonDown ()
    {
        print("Button Down!");
    }
}

방법1~4 중에 편한 방법으로 써주면 되겠습니당. (모두 동일한 기능을 합니다.)

버튼이 많거나 동적으로 생성해서 사용해줄 때 AddListener를 이용하여 메소드를 등록해줄 수 있습니다.

(또는 버튼을 누른 다음에 다른 메소드로 변경하는데도 사용될 수 있겠죠?)

 

2. 화면에서 적용시켜주기.

  먼저 스크립트가 있어야 하고, 스크립트 내부에 public으로 선언 된 함수가 있어야 됩니다.

  UI_Script 이름의 스크립트를 생성해줍니다.

using UnityEngine;

public class UI_Script : MonoBehaviour
{
    public void TestButtonDown ()
    {
        print("Button Down!");
    }
}

 

 이제 게임오브젝트를 하나 생성해주고 UI_Script를 갖고 있게 해줍니다.

오브젝트 이름도 동일하게 UI_Script 로 해주겠습니다.

 

 

버튼을 생성해줍니다. 그러면 버튼에 옵션이 나오게 되고 스크립트를 On Click () 으로 넣어줍니다.

 

빨간 네모 부분을 눌러주면 메뉴들이 나옵니다.

UI_Script -> TestButtonDown

을 찾아서 연결해주면 버튼이 눌렸을 때 해당 함수가 호출이 됩니다.

 

Scriptable Object Instances


유니티에서 스크립터블 오브젝트를 통해 게임내의 필요한 것들을 관리할 수 있다.

위의 Scriptable_Skill은 아래와 같이 구성되어 있다.

 

using UnityEngine;

[CreateAssetMenu(fileName = "Scriptable_Skill", order = 1004)]
public class Scriptable_Skill : ScriptableObject
{
    public Skill skill;
    public Sprite sprite;
    public float hitPower;
    public float sensorDistance;
    public float sensorRadius;
}

CreateAssetMenu 를 해당 파일을 생성할 수 있게 만들 수 있다.

생성되는 파일명, 보여질 순서이다. (order를 생략하면 Folder 윗쪽으로 생기게 된다.)

프로젝트 뷰에서 마우스 오른쪽 클릭 후 스크립터블 오브젝트 생성이 가능하다.

 

충분히 편리하지만 이미지가 너무 작다는 것이 아쉽기에

인터넷 검색을 해보았고 결국 찾아냈다. 원하는 결과물은 다음과 같다.

 

Sprite = Source Image 와 같다.

 

새로운 스크립트를 작성해주어 Scriptable_Skill에 대한 에디터를 정의해주어야 된다. 최종적으로

2개의 스크립트를 작성해준다.

using UnityEngine;

[CreateAssetMenu(fileName = "Scriptable_Skill", order = 1004)]
public class Scriptable_Skill : ScriptableObject
{
    public Skill skill;
    public float hitPower;
    public float sensorDistance;
    public float sensorRadius;
    public Sprite sprite;
}
using UnityEngine;
using UnityEditor; //Editor 상속 받기위해 써줌

[CustomEditor(typeof(Scriptable_Skill))] //해당 타입에 대해 적용을 해준다. (반드시 써줘야됨)
public class Scriptable_SkillEditor : Editor //에디터를 상속 받는다. OnInspectorGUI 재정의 가능
{
    private Scriptable_Skill scriptable_Skill;
    private Sprite sprite;
    private GUILayoutOption[] options;

    private void OnEnable()
    {
        //GUI 옵션 설정. 크기를 위해 설정해줌
        options = new GUILayoutOption[] { GUILayout.Width(128), GUILayout.Height(128) };
        
        //Editor에선 serializedObject 를 통해 해당 스크립트에 접근이 가능하다.
        //serializedObject.target의 형 변환을 통해 해당 값에 접근이 가능해진다.
        scriptable_Skill = serializedObject.targetObject as Scriptable_Skill;
        
        //기존 sprite를 기본으로 해줌.
        sprite = scriptable_Skill.sprite;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        EditorGUILayout.BeginHorizontal(); //줄 맞춤을 위해 설정
        
        GUILayout.Label("Source Image"); //라벨 설정. 이 후 값들이 오른쪽으로 정렬 된다.
        //EditorGUILayout.PrefixLabel("Source Image"); //라벨 설정. 왼쪽으로 정렬 된다.
        
        //EditorGUILayout.ObjectField 를 통해 이미지를 보이게 할 수 있다.
        //(Object, type, allowSceneObject (수정 가능 여부), options)
        EditorGUILayout.ObjectField(sprite, typeof(Sprite), false, options);
        
        EditorGUILayout.EndHorizontal();  //줄 마무리
    }
}

 

 

+ Recent posts