📝 구조체란?                                                                                                       

: 데이터와 관련된 기능을 캡슐화할 수 있는 값(Value) 형식

: struct 키워드를 사용해서 정의

 

📝 구조체의 특징                                                                                                     

  • 1. 작은 크기의 데이터에 적합 (16바이트 이하)
  • 2. 복사 시 전체 데이터가 복사됨 (깊은 복사)
    • - 클래스는 복사 시 참조만 복사됨 (얕은 복사)
  • 3. 구조체 내부에 참조 타입의 필드가 있으면 힙 할당 발생
  • 4. 함수에 큰 구조체를 전달하면 저하가 될 수도 있다
    • - ref 매개변수로 값 타입을 참조타입처럼 넘겨줄 수 있음 

 

📝 구조체와 클래스의 차이                                                                                                         

  구조체 (struct) 클래스 (class)
형식 값 형식 (Value Type) 참조 형식 (Reference Type)
메모리 스택 영역 힙 영역
상속 불가능 가능

 

: 클래스를 인스턴스 화 하면 힙에 할당된다.

힙에 할당된 메모리는 가비지 컬렉터가 사용하지 않는 메모리를 정리해 주는데

클래스를 너무 많이 할당하는 등의 처리야이 많으면 시스템 부하가 올 수 있다.

 

그래서 굳이 힙에 할당하지 않아도 되는 데이터는 스택 메모리에 할당되는 구조체를 사용하는 것이 성능적으로 좋아 보인다.

 

📝 구조체와 클래스, 무엇을 선택할까?                                                                                             

https://forum.dotnetdev.kr/t/c-structure/762

 

C# Structure(구조체)는 언제 사용하면 좋은가요?

안녕하세요. 😄 Structure에 대해 질문 드리고자 합니다. 저는 구조체를 단 한번도 사용해 본 적이 없는데요. 책이나 관련 글을 읽더라도 언제 사용해야 하면 좋을지 제대로 이해하지 못했습니다.

forum.dotnetdev.kr

구조체는 임시 객체에 사용하면 좋을 것 같다는 이야기

ex) for문에서 반복적으로 객체를 생성해야 할 때

'용어정리' 카테고리의 다른 글

[Unity] 코루틴  (0) 2025.03.10
[Unity] Monobehavior  (0) 2025.03.07
[c#] Action,Func,Delegate 차이점  (0) 2025.02.28
[c#] call by value, call by reference  (0) 2025.02.27
[c#]static 정적 변수, 정적 메서드 , 정적 클래스  (0) 2025.02.24

해당 글은 Csv를 Json으로 변환하는 툴에 대한 가이드입니다.

자세한 코드 내용은 아래의 블로그를 참고해 주세요 

2025.02.25 - [Unity] - [Unity] Csv To Json 변환 툴

 

[Unity] Csv To Json 변환 툴

내일배움캠프-레벨업세션에서 엑셀 파일을 Json으로 변환하는 기능을 봤는데 이번 프로젝트에서 그대로 사용하기에는 맞지않다고 느껴서유니티 내부에서 csv파일을 Json으로 저장하는 기능을 만

youcheachae.tistory.com

 


Csv 변환 방법                                                                                       

  1. wrapper 클래스를 작성합니다
    1. 클래스는 반드시 ICsvParsable 인터페이스를 상속받아 구현해야 합니다.
    2. Parse(string [] values) 메서드 구현
      1. CSV 파일의 각 행은 values 배열로 전달됩니다(string 타입).
      2. 클래스의 필드명에 맞춰서 형변환이 필요합니다
        1. 구체적인 예시는 ItemWrapper의 Parse메서드 참고해 주세요

- 형 변환 방식

// enum 변환 방법
itemType = (변환할 Enum)Enum.Parse(typeof(변환할 Enum), 문자열);

// ex
ItemType itemType = (ItemType)Enum.Parse(typeof(ItemType), values[인덱스]);

// 숫자 변환 방법
//숫자 변환: 
int.Parse(values[인덱스];
float.Parse(values[인덱스])

 

- 각 행은 values 배열로 전달됩니다.

 

- 구체적인 예시

[System.Serializable]
public class ItemWrapper : ICsvParsable
{
    [SerializeField] private int itemNum;
    [SerializeField] private ItemType itemType;
    [SerializeField] private string itemName;
    [SerializeField] private string itemToopTip;
    [SerializeField] private float attackSpeed;
    [SerializeField] private float attackDamage;
    [SerializeField] private float durationTime;

    public void Parse(string[] values)
    {
        // [0] item num
        // [1] item type
        // [2] name
        // [3] tooltip
        // [4] attackSpeed
        // [5] attackDamage
        // [6] durationTime
        // [7] PlayerState

        itemNum = int.Parse(values[0]);
        itemType = (ItemType)Enum.Parse(typeof(ItemType), values[1]);
        itemName = values[2];
        itemToopTip = values[3];
        attackSpeed = float.Parse(values[4]);
        attackDamage = float.Parse(values[5]);
        durationTime = float.Parse(values[6]);
    }
}

 

 

2. Resources 폴더에 CSV 파일 추가

  • CSV 파일명은 wrapper 클래스명과 정확히 동일해야 합니다.
  • 예시 csv 파일

ItemWrapper.csv
0.00MB

 

 

3. 변환 실행

  1. 씬의 Converter 오브젝트에서 CsvToJsonConverter 컴포넌트의 className 배열에 클래스명(=CSV 파일명)을 입력합니다.
  2. Inspector에서 [Csv to Json] 버튼을 클릭합니다.
  3. 변환이 완료되면 콘솔에 성공 메시지가 표시됩니다.
  4. JSON 파일은 Application.persistentDataPath 경로에 저장됩니다.
    1. 구제적인 경로
C:\Users\사용자이름\AppData\LocalLow\DefaultCompany\프로젝트이름

 

저장된 Json 불러오기                                                                                     

  • 저장된 클래스를 역직렬화, 리스트로 가져오기
// 저장된 클래스를 역직렬화
List<클래스명> 변수이름 = JsonSerialized.Deserialization<클래스명>("클래스명")

// 사용 예시 
List<ItemWrapper> itemWrapperList = JsonSerialized.Deserialization<ItemWrapper>("ItemWrapper");

 

!!!  주의사항 !!!                                                                                                 

  • CSV 파일, wrapper 클래스, className 배열에 입력한 이름이 모두 일치해야 합니다.
  • CSV 파일은 반드시 Resources 폴더 하위에 위치해야 합니다.
  • CSV 파일이 비어있거나 형식이 잘못된 경우 오류가 발생합니다.

깃허브

https://github.com/kimYouChae/UnityCsvToJsonConverter

 

GitHub - kimYouChae/UnityCsvToJsonConverter

Contribute to kimYouChae/UnityCsvToJsonConverter development by creating an account on GitHub.

github.com

 

코루틴은 동기적인가 비동기적인가 ? 

 

유니티 코루틴 공식문서에서 : 
하지만 코루틴은 스레드가 아니라는 점을 명심해야 합니다. 코루틴의 동기 작업은 여전히 메인 스레드에서 실행됩니다. 메인 스레드에 소요되는 CPU 시간을 줄이려면 다른 스크립트 코드에서와 마찬가지로 코루틴의 작업 차단을 방지하는 것이 중요합니다. Unity 내에서 다중 스레드 코드를 사용하려면 
C# 잡 시스템
을 고려하십시오.

 

코루틴은 "동기적(synchronous)"이면서도 "비동기적인 실행 패턴"을 제공하는 특별한 케이스 이다.

 

동기와 비동기의 의미는 ?

  1. 동기 Synchronous
    1. 말 그래도 동시에 일어난다는 뜻
    2. 요청을 하면 바로 결과가 주어져야함
    3. 여러가지 요청을 동시에 할 수 없다
  2. 비동기 Asynchronous
    1. 말그대로 동시에 일어나지 않는
    2. 요청을 하고 결과과 오지 않아도 대기시간동안 다른 요청을 할 수 있다
    3. 여러개의 요청을 동시에 처리 가능

 

MS의 공식문서에는 아침식사 차리기를 예시로 든다

https://learn.microsoft.com/ko-kr/dotnet/csharp/asynchronous-programming/

 

C#의 비동기 프로그래밍 - C#

async, await 및 Task를 사용하여 비동기 프로그래밍을 지원하는 C# 언어에 대해 간략히 설명합니다.

learn.microsoft.com

 

코루틴의 한계

코루틴은 가비지 컬렉터를 많이 잡아먹는다

 

쓰레드 단일/멀티쓰레드?

유니티에서 스크립트를 생성하면, Monobehavior을 기본적으로 상속받고 있다.

이러한 Monobehavior 클래스는 어떤 역할을 하는 것일까?

 

📝 Monobehavior                                                                                                         

유니티 공식 docs에서는 아래와 같이 설명하고 있다.

MonoBehaviour 클래스는 대규모 이벤트 메시지 컬렉션에 대한 액세스를 제공하며, 이를 통해 현재 프로젝트에서 발생하는 상황에 따라 코드를 실행할 수 있습니다.

 

🔖 Monobehavior의 역할

 

1. 모든 유니티 스크립트가 파생되는 기본 클래스

 

2. 에디터와 스크립트 파일을 연결시켜주는 역할

클래스를 에디터에서 읽고, 컴포넌트로 추가하는 등의 동작을 하기 위해서는 유니티 에디터가 클래스에 대한 정보를 알아야 한다. 이때 Monobehavior이 그 역할을 해준다.

 

3. 시작 및 업데이트 등과 같은 대한 연결을 제공 

Start(), Update(), OnEnable() 등의 콜백 함수를 의미한다.

 

4. 이러한 이벤트 함수들의 생명주기 관리

 

🔖 Monobehavior의 특징

1. 게임오브젝트의 Component로 등록해야 사용가능

반대로, Monobehavior을 상속받지 않는 클래스는 게임오브젝트의 컴포넌트로 등록할 수 없다.

 

2. Monobehavior을 상속받은 클래스를 사용하기 위해서는 GetComponent로 클래스를 가져와야 함

 

3. new로 동적 생성이 불가능 

 

🔖 콜백 함수(생명주기 메서드)는 어떻게 호출되는 것인가?

유니티에는 Messaging System이라는 시스템이 있는데 특정 시점에 유저가 정의한 함수가 동작할 수 있게 해 준다.

Monobehavior 스크립트가 처음으로 이용될 때, 스크립트 내 이러한 함수들이 있다면

이 스크립트는 각 함수와 관련된 List에 저장된다. 

예를 들어서 어떤 스크립트에 Update 함수가 정의되어 있다면 이 스크립트는 Update와 관련된 List에 저장되고,

이 List는 매 프레임마다 Update가 호출되어야 할 시점에 사용된다.

 

사용하지 않는 " 자주 호출되는 메서드(특히 Update, FixedUpdate, LateUpdate) 관련된 함수들은 사용하지 않으면 지우는 것이 성능에 좋다"라는 이야기를 들어보았을 것이다.

이것은 Monobehavior가 이용될 때 해당 메서드를 사용하던 안 하던 실행하기 때문이다.

 

자세한 내용은 공식문서를 참고하자! 

https://unity.com/kr/blog/engine-platform/10000-update-calls

 

10000번의 Update() 호출

void Update() { transform.Translate(0, 0, Time.deltaTime); }하지만 숙련된 개발자는 위의 코드에서 몇몇 의문이 들 수 있습니다. 이 함수는 도대체 언제 호출되지? 만약, 여러개의 스크립트가 있고 그 스크립

unity.com

 

 

내부적으로는 IL2CPP와 같은 Unity의 스크립팅 백엔드에서 MonoBehaviour가 어떻게 처리되는지 알 수 있다.

 

🔖 Monobehavior이 콜백함수를 실행하는 순서, 생명주기

Awake() → OnEnable() → Start() → Update() → LateUpdate() → OnDisable() → OnDestroy() 등의 순서로 호출된다.

자세한 내용은 유니티 생명주기 공식문서를 참고하자

 


유니티 라이프사이클

https://docs.unity3d.com/kr/2019.4/Manual/ExecutionOrder.html

 

이벤트 함수의 실행 순서 - Unity 매뉴얼

Unity 스크립트를 실행하면 사전에 지정한 순서대로 여러 개의 이벤트 함수가 실행됩니다. 이 페이지에서는 이러한 이벤트 함수를 소개하고 실행 시퀀스에 어떻게 포함되는지 설명합니다.

docs.unity3d.com

 

 

유니티 중요 클래스 - Monobehavior

https://docs.unity3d.com/kr/2020.3/Manual/class-MonoBehaviour.html

 

중요 클래스 - MonoBehaviour - Unity 매뉴얼

MonoBehaviour 클래스는 기본적으로 모든 Unity 스크립트가 파생되는 기본 클래스입니다. Unity의 프로젝트 창에서 C# 스크립트를 생성하면 MonoBehaviour에서 자동으로 상속되며, 템플릿 스크립트를 제공

docs.unity3d.com

 

 

1인칭 플레이어 움직임을 만들 때 근본적인 문제점이 발생했다. 

플레이어가 어떻게 자연스럽게 경사를 올라가게 할 것인가? 

일반적인 애니메이션과 콜라이더를 사용하여 경사를 올라가면, 발이 다른 오브젝트의 매쉬를 뚫거나 발이 공중에 떠있게 될 것이다

실제로 경사를 올라갈 때 앞에 있는 발은 매쉬를 뚫고 들어가고, 뒤에 있는 발은 공중에 뜨게된다.

 

이를 해결하려면, 발에서 Ray를 쏴서 바닥과 발 사이의 각도를 구한 다음 발을 회전시켜야 할까?라고 생각하고 검색을 해보던 도중에, IK 기능을 발견했다. 

 

유튜브에 올라와 있는 강의를 참고해서 작성하였다

https://www.youtube.com/watch?v=rGB1ipH6DrM&t=133s

 


📝IK?                                                                                                           

 

일단. IK가 무엇인지 알아야 한다. 유니티 공식문서에 따르면 , 대부분의 애니메이션은 스켈레톤의 조인트 각도를 미리 정해진 값으로 회전하여 만든다. 자식 뼈대 (조인트)는 부모 뼈대 (조인트)의 회전에 따라 변하고, 이런 방식을 순 운동학 (FK)라고 한다.

 

하지만 이런 방법을 반대로 바라본다면 더욱 유용하다. 오브젝트를 기준으로 뼈대 (조인트) 값을 계산하여 적용하는 것이다. 예를 들어 플레이어가 다른 오브젝트를 건드리거나, 울퉁불퉁한 표면 위에 캐릭터의 두 발이 자연스럽게 밀착해 있도록 할 수 있다. 이러한 접근법을 역운동학 (IK)라고 한다. 

 

유니티 공식문서에 있는 IK 관련 이미지

 

IK를 적용하기 위해서는 아바타의 타입이 휴머노이드여야 한다.  


📝 사전준비                                                                                                           

1. 플레이어의 Animator의 Layer의 설정에서 IK Pass 옵션을 체크해준다 

2. 플레이어가 걸어갈 발판/경사로 등의 레이어를 Walkable로 지정해 준다 

 


📝구현                                                                                                            

PlayerAnimator.cs

public class PlayerAnimator : MonoBehaviour
{
    [Header("===Animator===")]
    [SerializeField] private Animator animator;

    [Range(0, 1f)]
    public float distanceToGround;

    public LayerMask layerMask;
    
    void Start()
    {
        animator = GetComponent<Animator>();
    }
    
    private void OnAnimatorIK(int layerIndex)
    {
    
    }
}

  void OnAnimatorIK(int layerIndex)

             애니메이션 IK(역운동학)을 설정하기 위한 콜백

              Animator Component가 내부 IK 시스템을 업데이트하기 직전에 호출

             콜백 안에 IK 목표 위치와 , 가중치를 설정하는 데 사용한다

 

private void OnAnimatorIK(int layerIndex)
    {
        // 애니메이터가 실행되는 매 프레임 실행
       if(animator != null) 
        {
            animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
            animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1f);
            animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1f);
            animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1f);

animator.SetIKPositionWeight()

            공식문서에서는, IK 목표의 변환 가중치를 설정한다. ( 0 = IK전 원래 애니메이션에서 1 = 목표에서 )

            즉 가중치를 설정하여 원래 애니메이션과 , IK 조정 사이의 혼합정도를 결정한다.

  animator.SetIKRotationWeight()

            SetIKPositionWeight과 같이 회전에 대한 가중치를 설정한다.

🔖 가중치
가중치가 0일 때 : 원래 애니메이션이 100% 적용된다. IK는 전혀 영향을 주지 않는다
가중치가 1일 때 : 조정이 100% 적용된다. 원래 애니메이션은 무시된다
가중치가 0 ~ 1 사이일 때 : 원래 애니메이션과 IK 조정값이 해당 비율로 혼합된다

 

🔖  여기서 AvaterIkGoal은 Enum으로,
LeftFoot과 RightFoot은 Animator에 적용된 아바타의 
Left Leg의 Foot과 , Right Leg의 Foot를 의미한다. 

 

 

 

 // left foot 
 RaycastHit hit;
 Ray ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + Vector3.up, Vector3.down);
 
 Debug.DrawRay(ray.origin, ray.direction * (distanceToGround + 1f), Color.red);

 Ray 생성

             시작지점 :  아바타의 LeftFoot의 위치에서 1f 올라간 곳, 

             방향 : 시작지점에서 아래쪽 방향

 

  DrawRay로 레이를 그리면, 주황색 동그라미에서 시작하여 아래쪽으로 ray를 쏘고 있는 것을 확인할 수 있다. 

 

if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer)) 
{

    if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt) 
    {
    	// 추가예정
    	// 플레이어 발 위치 , 회전 적용
    }
}

  Raycast

             플레이어를 제외한 오브젝트를 감지해야 하기 때문에 PlayerLayer을 제외한 레이어만 검사

             레이캐스트가 성공적으로 충돌체를 감지하면 , if문 실행 

 

해당 충돌체의 레이어가 'Walkable'이면?

             플레이어 발의 위치와 회전을 변경


(+) 레이어에 관해서 

layer을 검사할 땐 layerMask의 인덱스 값으로 검사해야 하기 때문에 GetMask를 사용해 준다

해당코드는 어디에 있던지 상관없다

[SerializeField] private LayerMask ignorePlayerLayer;   // 플레이어 레이어를 제외한 레이어
[SerializeField] private int walkableLayerInt;

void Start()
{
    walkableLayerInt = LayerMask.NameToLayer("Walkable");
}

PlayerLayer을 뺀 LayerMask 정보는 인스펙터 창에서 PlayerLayer을 제외한 다른 레이어들을 체크해주면 된다.


 

if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer)) 
{
    if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt) 
    {
        Vector3 footPosition = hit.point;
        footPosition.y += distanceToGround;
        animator.SetIKPosition(AvatarIKGoal.LeftFoot, footPosition);
        animator.SetIKRotation(AvatarIKGoal.LeftFoot, Quaternion.LookRotation(transform.forward, hit.normal));
    }
}

 해당 충돌체의 레이어가 'Walkable'이면?

            발의 위치

                        hit.point(레이가 충돌한 정확한 지점)를 footPosition으로 사용

            footPosition.y += distanceToGround

                       발이 지면에 완전히 묻히지 않고 자연스럽게 위치하도록 약간의 오프셋을 주기 위함

                       현재 distanceToGround는 0.06f 정도로 설정되어있다

             발의 회전

                         Quaternion.LookRotation을 사용해서 플레이어가 바라보고 있는 방향으로,

                         hit의 법선 벡터와 일치하도록 hit.normal을 사용해서 회전값을 줌

 

전체코드

public class PlayerAnimator : MonoBehaviour
{
    [Header("===Animator===")]
    [SerializeField] private Animator animator;

    [Range(0, 1f)]
    public float distanceToGround;	// 0.06f

    void Start()
    {
        animator = GetComponent<Animator>();
    }

    #region Foot Ik
    

    private void OnAnimatorIK(int layerIndex)
    {
        // 애니메이터가 실행되는 매 프레임 실행

        if(animator != null) 
        {
            animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
            animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1f);
            animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1f);
            animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1f);

            // left foot 
            RaycastHit hit;
            Ray ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + Vector3.up / 2, Vector3.down);

            Debug.DrawRay(ray.origin, ray.direction * (distanceToGround + 1f), Color.red);

            if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer)) 
            {
                if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt) 
                {
                    Vector3 footPosition = hit.point;
                    footPosition.y += distanceToGround;
                    animator.SetIKPosition(AvatarIKGoal.LeftFoot, footPosition);
                    animator.SetIKRotation(AvatarIKGoal.LeftFoot, Quaternion.LookRotation(transform.forward, hit.normal));
                }
            }

            // 오른발
            ray = new Ray(animator.GetIKPosition(AvatarIKGoal.RightFoot) + Vector3.up, Vector3.down);

            if (Physics.Raycast(ray, out hit, distanceToGround + 1f, LayerManager.Instance.IgnorePlayerLayer))
            {              
                if (hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt)
                {

                    Vector3 footPosition = hit.point;
                    footPosition.y += distanceToGround;
                    animator.SetIKPosition(AvatarIKGoal.RightFoot, footPosition);
                    animator.SetIKRotation(AvatarIKGoal.RightFoot, Quaternion.LookRotation(transform.forward, hit.normal));
                }
            }
        }
    }
    
    #endregion

 


📝 결과                                                                                                              

IK를 적용하기 전 모습

IK를 적용한 후

 


 

 OnAnimatorIK

https://docs.unity3d.com/kr/530/ScriptReference/MonoBehaviour.OnAnimatorIK.html

 

MonoBehaviour-OnAnimatorIK(int) - Unity 스크립팅 API

Callback for setting up animation IK (inverse kinematics).

docs.unity3d.com

 

SetIKPositionWeight

https://docs.unity3d.com/kr/530/ScriptReference/Animator.SetIKPositionWeight.html

 

Animator-SetIKPositionWeight - Unity 스크립팅 API

Sets the translative weight of an IK goal (0 = at the original animation before IK, 1 = at the goal).

docs.unity3d.com

 

역운동학 (IK)

https://docs.unity3d.com/kr/530/Manual/InverseKinematics.html

 

역운동학(IK) - Unity 매뉴얼

대부분의 애니메이션은 스켈레톤의 조인트 각도를 미리 정해진 값으로 회전하여 만듭니다. 자식 조인트의 포지션은 부모의 회전에 따라 변하므로 조인트 체인의 끝 점은 체인에 포함된 각 조인

docs.unity3d.com

 


깃허브

https://github.com/kimYouChae/Sparta_OnlyUp

 

 

New Input System과 기본적인 플레이어 움직임에 관한 글은 아래 블로그를 참고

2025.03.04 - [Unity] - [Unity] New Input System 으로 플레이어 움직이기

 

[Unity] New Input System 으로 플레이어 움직이기

1. New Input System2. New Input System 사용방법3. PlayerController  스크립트4. PlayerInput 컴포넌트📝 New Input System이란?                                                                          Unit

youcheachae.tistory.com

 


📝 플레이어 Jump                                                                                    

 

상단의 링크에서 작성한 PlayerMove 스크립트에서 추가로 작성해주었다.

    // ....
    #region 플레이어 점프

    public void OnJump(InputAction.CallbackContext context) 
    { 
        // 한번 눌리면 
        if(context.phase == InputActionPhase.Started) 
        {
            playerRb.velocity = Vector3.zero;
            playerRb.AddForce(Vector3.up * 5f , ForceMode.Impulse);
        }
    }
    #endregion
    // ....

  OnJump()

             PlayInput컴포넌트에서 입력 발생 시 리턴되는 콜백을 받아오기 위한 함수 작성

             매개변수를 InputAction.CallbackContext로 콜백을 받아온다

             입력이 눌렸을 때, 플레이어의 Vector3.Up방향(0,1,0)으로 , Addforce 해준다. 

 

  컴포넌트의 Event → Player  Jump Aciont에 (+) 버튼으로 추가해 준다

  Player 오브젝트를 드래그해서 넣고, PlayerController스크립트의 OnJump메서드를 추가한다 

 

📝 발생한 문제                                                                                    

열심히 스페이스바를 누르고 있지만 플레이어에 Addforce가 적용되지 않는 모습이다.

 

🔖 해결시도

1. Mass를 줄이기 / 늘리기 

            Mass는 오브젝트 질량 이기 때문에 질량을 줄이면 느리게 떨어지고, 늘리면 빨리 떨어질 거라 생각했다.

:

하지만 Mass를 변경해도 떨어지는 속도가 변하지 않았다. 

 

2. 중력 Scale을 수정 

             Edit → Project Setting Physics의 Gravity를 -24 정도까지 줄여봤다. 

: 하지만 이것 또한 플레이어가 떨어지는 속도가 변하지 않았다. 

 

3. 테스트로 만들어놓은 큐브와 컴포넌트 하나하나 비교해 보기....

: 테스트용 큐브와 플레이어의 차이는 PlayerController밖에 없는데,

혹시 여기서 문제인가...?! 싶어서 코드를 다시 뜯어봤다.

 

 

- 그 당시 PlayerMove() 코드 

private void PlayerMove() 
{
    Vector3 dir = transform.forward * moveVector.y + transform.right * moveVector.x;
    dir *= speed;
    playerRb.velocity = dir;
}

: 해당 코드에서 , y값의 속력을 지정해 주는 부분이 없다!!

그래서 y의 속력이 항상 0으로 설정되고 , 그래서 Gravity값과 Mass를 아무리 수정해도 변화가 없었던 것.............

 

- 해결 방법

🔖 dir의 y는 현재 물리엔진에서 계산되고 있는 중력이나 점프 등으로 발생한 y 방향의 속도를 유지하기 위해서 playerRB.velocity.y로 설정해 주면 된다.

    dir.y = playerRB.velocity.y;

 

y 속도를 생각하지 못한 내 실수였던 것....


: 상단의 링크에서 작성한 코드 PlayerMove()에 대해 추가적인 설명을 하고 넘어가려 한다.

전체적인 코드는 상단의 링크를 참고!

private void Move() 
{
    Vector3 dir = transform.forward * curMoveInput.y + transform.right * curMoveInput.x;
    dir *= _moveSpeed;
    dir.y = playerRB.velocity.y;    

    playerRB.velocity = dir;
}

  curMoveInput은 Vector2 타입으로, x와 y 밖에 없다.

 Vector2에서 움직일 때, 플레이어 기준 앞 / 뒤로 움직이면 y 값이 변화한다. 좌 / 우로 움직이면 x 값이 변화한다.

              y가 양수면 앞쪽으로 이동
              y가 음수면 뒤쪽으로 이동
              x가 양수면 오른쪽으로 이동
              x가 음수면 왼쪽으로 이동

 Vector3에서 움직일 때, 플레이어 기준 앞 / 뒤로 움직이면 z 값이 변화한다. 좌 / 우로 움직이면 x 값이 변화한다.

 

위에서 바라봤을 때 Vector2와 Vector3의 축 변화

 

  플레이어 기준 앞/뒤 움직임이 발생하면 플레이어 앞쪽(forward)과 y값을 곱하고, 좌/우 움직임이 발생하면 플레이어의 오른쪽 (right)과 x 값을 곱해서 가야 할 방향을 나타낸다.

  y는 위에서 설명한 대로, 현재 물리엔진에서 계산되고 있는 중력이나 점프 등으로 발생한 y 방향의 속도를 유지하기 위해서 playerRB.velocity.y로 설정해 주면 된다.

 

📝 마우스에 따른 플레이어 회전 / 카메라 회전                                                                               

접근방식

마우스를 회전하면, 플레이어의 x회전과 y 회전을 변경시켜야 된다고 생각했다. 

하지만 플레이어의 x값 회전을 하면 플레이어의 콜라이더가 땅에 박히거나 충돌 나는 문제가 발생할 것 같았다. 

: 콜라이더가 땅에 박힌다....


즉 , 마우스를 회전하면 

1. 플레이어의 y 회전 값 (좌, 우)이 변경되어야 한다.

2. 카메라의 x 회전 값 (상, 하)이 변경되어야 한다.

 

 

- 구체적인 코드

[Header("===Rotate===")]
[SerializeField] private Vector2 mouseDelta;        // 마우스 움직임 델타
[SerializeField] private float currentY;            // 현재 회전 상태 Y
[SerializeField] private float rotationBoundary = 80f;
[SerializeField] private float sensitivity;

private void LateUpdate()
{
    RotateCamera();
}

private void RotateCamera()
{
    // 플레이어는 rotation의 y값만 바껴야한다
    // 카메라는 rotation의 x값만 바껴야 한다

    currentY += mouseDelta.y * sensitivity;

    float newY = Mathf.Clamp(currentY, -rotationBoundary , rotationBoundary) ;

    // 카메라 회전 
    cameraTrs.localEulerAngles = new Vector3( -newY, 0, 0);

    // 플레이어 회전 
    transform.eulerAngles += new Vector3(0, mouseDelta.x * sensitivity, 0);
}

public void OnRotateCamera(InputAction.CallbackContext context)
{
    // Delta값 :
    // 화면의 중앙을 (0, 0) 기준으로
    // 마우스를 빠르게 움직일수록 절대값이 커짐
    //      마우스를 오른쪽으로 빠르게 이동: (15, 0)
    //      마우스를 왼쪽으로 천천히 이동: (-2, 0)
    mouseDelta = context.ReadValue<Vector2>();
    // Debug.Log($"Mouse Delta: {mouseDelta}");
}

  OnRotateCamera()

             context.ReadValue <Vector2>()로 마우스 움직임에 관한 Vector2 값을 mouseDelta 변수에 저장한다. 

  RotateCamera()

            카메라 회전을 계산하기 위해서 입력받은 mouseDelta값의 y에 sensitivity값을 곱해서 저장한다.

            카메라 회전 (위, 아래)는 너무 큰 값이 들어가면 360도 돌 수 있기 때문에 임시로 -80 ~ 80까지 값으로 설정했다.

            localEulerAngles를 사용해서 회전값을 적용한다. 

                      □ 카메라는 현재 Player 하위에 있기 때문에 local 회전값을 변경시켜 준다 

                       카메라의 회전값을 변경하면 (-) 값으로 들어가서 코드상에서 (- ) 값을 넣어준다

            플레이어 회전 (좌, 우)는 mouseDelta값의 x로 설정한다.

 


📝 플레이어 움직임/점프/카메라 회전 결과 

원하던 움직임이 잘 나타나는 것을 확인할 수 있다. 


https://github.com/kimYouChae/Sparta_OnlyUp

 

 

 

 

1. New Input System
2. New Input System 사용방법
3. PlayerController  스크립트
4. PlayerInput 컴포넌트

📝 New Input System이란?                                                                          

Unity에서 2019 버전부터 새로 만든 Input 시스템

 

🔖 기존 Input System과 다른 점

기존의 Input시스템에는 Axes를 추가하고, 스크립트를 작성할 때 Update에서 계속 GetKeyDown으로 검사해줘야 한다.

또한 플랫폼별 입력 대응이 어렵다는 한계가 있다. 

 

🔖 New Input System의 장점

1. 키보드, 컨트롤러 등의 여러 플랫폼에 쉽게 대응할 수 있다.

2. 하나의 코드를 통해 다양한 플랫폼에서의 입력 처리 가능

3. 입력 Action에 대한 함수를 직관적이고 간편하게 연결할 수 있다

 

기존 방식의 한계 

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        Jump();
    }
}

 

New Input System의 함수 연결 

void OnJump(InputAction.CallbackContext context)
{
    Jump();
}

  코드 가독성이 향상됨 

  입력 관리를 효율적으로 관리할 수 있다


📝 New Input System 사용                                                                                             

 

1. Package Manger의 InputSystem을 다운로드한다

 

 

2. Edit → Project Setting Player ActiveInputHandiling에서 사용할 Input System을 설정할 수 있다.

Input System Package(New)를 클릭하면 경고창이 뜨는데 Apply를 누른 후 유니티프로젝트를 재실행한다.

경고창 , Apply를 누른다

 

 

3. Create → Input Action을 생성한다. 이름은 PlayerInput으로 설정한다. 

 

 

4. 생성 후 Inspector 창에서 Edit Asset을 클릭한다

 

 

5. InputAction 창이 뜨면 왼쪽 상단에 Scema를 설정해 준다.

 

 

🔖 Control Scheme (컨트롤 스키마)
: 특정 플랫폼에서 사용할 입력 설정의 그룹
: 예시 
   : 키보드 + 게임패드 스키마
   : 키보드 + 마우스 스키마
: 플랫폼별 입력 대응을 할 수 있는 부분 

🔖 Action Maps (액션 맵)
: 사용할 키 입력에 대한 Action들을 그룹화

🔖 Actions (액션)
: 특정 입력에 대한 입력 동작 
: Started , Performed, Cancled등 다양한 입력상태 처리 가능 

 

 

 

6. Actions 추가 

플레이어 움직임 입력을 위한 Action을 추가한다.

Action Type은 Value / ControlType은 Vector2로 설정한다 

🔖 Action Type 
버튼 : 단일성으로 누르고 떼는 용도
Value : 지속적으로 값을 변화하고자 할 때

 

6-1. Move Action옆의 + 버튼을 누른 후 "Add Up ~ Composite"를 클릭한다

Up / Down / Left / Right에 해당하는 키를 바인딩해준다. 각각 w s a d로 바인딩해주었다. 

 

7. 다른 행동에 대한 Action을 추가해 준다

  마우스 움직임에 따른 카메라 회전 입력을 위해서 Look이라는 Action을 추가한다

             Action Type은 Value / ControlType은 Delta / Binding path는 Delta[Mouse]로 바인딩해준다 

  점프, 인벤토리, 상호작용 입력을 위한 Jump , Inventory, Interaction이라는 Action을 추가한다

             각각 Action Type은 Button / Binding path는 Backspace,  Tab , E [Keyboard]로 바인딩해준다

  공격키 입력을 위한 Attack이라는 Action을 추가한다

             Action Type은 Value / Binding path는 Left Button [Mouse]로 바인딩해준다 

 

📝PlayerController 스크립트                                                              

public class PlayerController : MonoBehaviour
{
    [Header("===Movement==")]
    private float _moveSpeed = 3f;
    private Vector2 curMoveInput;
    private void 

    FixedUpdate()
    {
        Move();
    }

    private void Move() 
    {
        Vector3 dir = transform.forward * curMoveInput.y + transform.right * curMoveInput.x;
        dir *= _moveSpeed;
        dir.y = playerRB.velocity.y;    

        playerRB.velocity = dir;
    }

    public void OnMove(InputAction.CallbackContext context) 
    {
        // 입력이 있으면 받아오기
        if (context.phase == InputActionPhase.Performed)
        {
            curMoveInput = context.ReadValue<Vector2>();
        }
        // 입력이 없으면 0,0 으로
        else if (context.phase == InputActionPhase.Canceled) 
        {
            curMoveInput = Vector2.zero;
        }
    }
}

   OnMove()

             PlayInput컴포넌트에서 입력 발생 시 리턴되는 콜백을 받아오기 위한 함수 작성

             매개변수를 InputAction.CallbackContext로 콜백을 받아온다

             현재 움직임에 대한 Vector2 타입의 curMoveInput 값을 들어온 콜백의 Vector2로 설정한다

 

   Move()

              받아온 curMoveInput을 사용하여 플레이어 움직임을 제어한다 

🔖InputActionPhase.Performed
: 콜백이 발생하는 동안 

🔖InputActionPhase.Started
: 콜백이 발생할 때, 1회

🔖InputActionPhase.Canceled
: 콜백이 종료될 때 , 1회 

 

📝 PlayerInput 컴포넌트                                                               

플레이어 오브젝트를 만든 후 PlayerController스크립트와 Player Input 컴포넌트를 추가해 준다 

Acions에 방금 만든 PlayerInput을 추가해 준다 

Behavior도 Invoke Unity Events로 설정한다

 

🔖 Invoke Unity Events
: 유저가 정의한 Action에 대한 키 입력이 들어오면 , 설정한 이벤트 (함수)가 실행된다 

 

컴포넌트의 Event → Player  Move Aciont에 (+) 버튼으로 추가해 준다

Player 오브젝트를 드래그해서 넣고, PlayerController스크립트의 OnMove메서드를 추가한다 

 

📝 결과                                                               

: wsad가 입력되면 플레이어가 움직이는 것을 확인할 수 있다. 

 


 

📝 사전작업 

: Window → PackageManager → TextMeshPro를 다운로드한다

 

📝  사용할 폰트를 Asset폴더에 추가한다

 

(+) 눈누 사이트에 가면 상업적으로 사용한 폰트를 찾을 수 있다

https://noonnu.cc/

 

눈누

상업용 무료 한글 폰트 사이트

noonnu.cc

 


📝  폰트 에셋 추가방법

  window → TextMeshPro → Font Asset Creator

 

 

각 맞게 입력 후 Generate Font Atlas 클릭, 우측에 폰트가 생성된 걸 볼 수 있다.

하단의 Save를 클릭해서 폰트 에셋을 저장 후 사용하자 

□ 프로퍼티 

             Source Font File -  Asset 폴더에 드래그한 폰트 파일

             Sampling Point Size - Auto Sizing

             Padding - 5 

             Packing Method : Fast

             Atlas Resourlution : 4096 , 4096 

             Character Set : Custom Range

 

             Character Sequence : 32-126,44032-55203,12593-12643,8200-9900 입력

            32-126 : 영어 (알파벳 범위)

            44032 - 55203 : 한글범위 / 받침이 있는 모든 글자 

            12593 - 12643 : 자음 모음이 하나씩 떨어지 있는 글자 ex) ㄱ ㄴ ㄷ ㅏ ㅖ

            8200 - 9900 : 특수문자 


Font Asset Creator 공식문서

 

https://docs.unity3d.com/kr/2022.3/Manual/UIE-font-creator-properties.html

 

Font Asset Creator 프로퍼티 레퍼런스 - Unity 매뉴얼

Font Asset Creator로 폰트 아틀라스를 생성하고 업데이트할 수 있습니다.폰트 아틀라스를 생성하고 업데이트하려면 폰트 에셋의 인스펙터(Inspector) 창에서 Update Atlas Texture를 선택합니다.

docs.unity3d.com

 

+ Recent posts