MonoBehaviour

GameObject의 컴포넌트로 script를 추가하려면, MonoBehaviour를 상속받고 있는 클래스만 붙일 수 있다. MonoBehaviour는 Object, Component 등등이 상속된 형태의 클래스로 이를 통해 유니티의 컴포넌트로 활용될 수 있는 것. 또한 이는 new를 통해 instance가 생성될 수 없다. 해당 링크의 내용에 따르면 유니티의 내부 구조 상 MonoBehaviour 스크립트나 컴포넌트를 게임 오브젝트에 부착하면 컴포넌트의 인스턴스가 생성된다. 각 컴포넌트는 자체 객체이자 자체 인스턴스이다.

 

 

일반적인 C# 클래스로 사용하려면 MonoBehaviour를 상속에서 제외하면 된다.

  • 하지만 Manager 같은 클래스를 독립적으로 사용하려면, 결국 유니티의 라이프사이클 안에 들어가는 것이 편한 경우가 많은데, 이럴 때는 빈 게임오브젝트를 하나 만든 뒤에 MonoBehaviour를 상속받은 Managers클래스를 컴포넌트로 집어넣는 것이 편한 것 같기도 하고, 일반적인 경우처럼 보인다.
  • DontDestroyOnLoad를 붙일 것은 컨벤션 상 @를 붙인다.

 

Materials

  • Materials = Texture + shader
  • 빛을 어떻게 반사시킬 것인지 정함. 이를 통해 인간이 눈으로 볼 수 있듯이.

 

Animation

  • 애니메이터가 만들어준 파일들을 호출해서 각 모션들을 사용할 수 있게 된다.

 

Local position, World Position

좌표계는 월드 좌표와 로컬 좌표 2가지가 있다.

월드 좌표 vs 로컬 좌표
씬 뷰의 상단에서 확인 가능. 좌표계 변환 단축키는 'x'이다. 로

  • local space는 오브젝트를 기준으로 정의된다. 원점은 게임 오브젝트 중심이고 축은 오브젝트의 오른쪽, 위쪽, 앞쪽이다. 
  • world space는 씬, 월드를 기준으로 정의된다. 원점은 월드의 중심(의미를 부여하는 것은 사용자에게 달려있음)에 있고, 축은 오른쪽, 위쪽, 앞쪽이며 보통 동쪽, 위쪽, 북쪽으로 매핑된다. 

유니티에는 local space, world space, camera space, screen space, viewport space등 많은 공간이 있다. 한 공간에 표현된 위치는 변환을 사용하여 다른 공간으로 표현할 수 있다. 이는 종종 행렬 곱셈이나 동등한 헬퍼 함수를 통해 수행된다.

 

그런 다음, 전체 게임은 한 공간에서 다른 공간으로의 올바른 변환을 아는 것으로 구성된다. 예를 들어 로컬 공간에서 월드 공간으로 표현된 위치를 알고 싶다면 localToWorldMatrix를 사용하면 된다. 다른 방향으로 알고 싶다면 worldToLocalMatrix(역행렬)을 사용하면 된다.

 

그리고 오브젝트의 로컬 좌표계는 모델러들이 정한다. 그래서 로컬과 월드 좌표계는 완전히 분리되어 있다.

  • Position은 객체가 월드를 기준으로 할 지 부모를 기준으로 할 지에 따라 나뉜다.
  • 예를 들어, 월드 좌표에서 부모 오브젝트의 위치가 (3,3,3)이면 자식오브젝트의 위치도 (3,3,3)이 되는데 자식 오브젝트의 로컬 좌표는 부모 좌표기준으로는 그대로니 (0,0,0)의 좌표를 가지게 된다.

 

 

오브젝트 이동

위 localToWorldMatrix와 같은 행렬곱 연산이 아닌, TransformDirection() 또는 InverseTransformDirection()을 사용해도 된다. 각각 함수의 의미는 로컬 공간에서 월드 공간으로 변환하거나 월드 공간에서 로컬 공간으로 변환하는 것을 의미한다.

 

또한 Transform.Translate를 사용하는 방법도 존재한다.

public void Translate(Vector3 translation) => this.Translate(translation, Space.Self);
public void Translate(Vector3 translation, [DefaultValue("Space.Self")] Space relativeTo)
{
    if (relativeTo == Space.World)
      this.position += translation;
    else
      this.position += this.TransformDirection(translation);
}

해당 함수 매개변수 relativeTo의 기본값은 Space.Self로 되어 있어, 별다른 설정 없이 사용하면 바로 로컬 좌표를 기준으로 연산하게 된다.

 

방향

$$상대방의 \space 위치 - 나의 \space 위치$$ 해당 식을 통해 방향 벡터 구할 수 있다. 방향 벡터는 방향과 크기를 둘 다 가진다.

 

벡터의 종류

  1. 위치 벡터
  2. 방향 벡터
    1. 거리(크기) - magnitude 이용
    2. 실제 방향
TestVector to = new TestVector(10.0f, 0.0f, 0.0f);
TestVector from = new TestVector(5.0f, 0.0f, 0.0f);
TestVector dir = to - from;

dir = dir.normalized;

 

 

 

회전

transform.rotation은 Quaternion을 반환하기에 사용하기 불편한 감이 있다. 그래서 Vector3를 통해 월드 공간에서 회전을 조작하려면 transform.eulerAngles을 사용하면 된다. 값이 게임오브젝트의 인스펙터에서 다르게 보일 수 있느넫, 인스펙터에서는 local rotation이 표시되기 떄문에, 이를 조작하려면 Transform.localEulerAngles를 참고해야 한다.

 

EulerAngle을 통해 쿼터니언의 회전을 설정하는 것이며, 이 속성을 사용하여 회전을 설정할 때는 X, Y, Z 모든 회전 값을 제공해야 한다. 그리고 이 값은 회전에 저장되지 않는다.

 

.eulerAngle 프로퍼티를 읽을 때 유니티는 내부의 쿼터니언 값을 오일러 각도로 변환한다. 오일러 각을 사용하여 주어진 회전을 표현하는 방식은 여러 가지가 있으므로 다시 읽어온 값은 지정한 값과 상당히 다를 수 있다. 이는 애니메이션을 생성하기 위해 값을 점진적으로 증가시키려는 경우 혼란이 발생할 수 있다.

 

그래서 점진적으로 회전이 증가하는 애니메이션을 생성하려 시도할 때 .eulerAngle을 읽을 때 결과에 의존하지 않는 것이 좋다. 더 좋은 방법은 쿼터니언의 '*' 연산자를 사용해보는 것도 좋다.

float rotationSpeed = 45;
Vector3 currentEulerAngles;
float x;
float y;
float z;

void Update()
{
    if (Input.GetKeyDown(KeyCode.X)) x = 1 - x;
    if (Input.GetKeyDown(KeyCode.Y)) y = 1 - y;
    if (Input.GetKeyDown(KeyCode.Z)) z = 1 - z;

    //modifying the Vector3, based on input multiplied by speed and time
    currentEulerAngles += new Vector3(x, y, z) * Time.deltaTime * rotationSpeed;

    //게임 오브젝트에 회전 변화 적용
    transform.eulerAngles = currentEulerAngles;
}

그래서 공식 문서에서는, Quanternion.eulerAngle을 절대 읽는 방식으로 의존하지 않는다. 모든 회전 변경을 currentEulerAngles에서만 발생시키며 이를 통해 eulerAngle값을 변경시킨다. 이 방식은 위에서 언급한 문제를 피할 수 있다.

 

transform.Rotate()

사실 회전을 할 때는 eulerAngles을 쓰는 것보다, Rotate()함수를 쓰는 것이 좋다.

transform.Rotate(new Vector3(0.0f, Time.deltaTime * 100.0f, 0.0f));

회전은 오일러의 양만큼 이루어진다. 즉, 기존 오브젝트가 가진 회전값을 기준으로 계속해서 연산이 되는 형태.

 

 

transform.rotation

transform.rotation = Quaternion.Euler(new Vector3(0.0f, _yAngle, 0.0f));

오일러값을 쿼터니언으로 반환하여 rotation값에 직접 값을 대입하여 변환할 수도 있다.

 

 

원하는 특정 방향을 쳐다보게 만들기

`transform.rotation`을 살펴보면, Quaternion 타입을 반환하는 것을 볼 수 있으며, 월드 공간에서 transform의 회전을 저장한다고 주석에 작성되어 있다. (A Quaternion that stores the rotation of the Transform in world space.) 이렇게 Vector3가 아닌 Quaternion으로 저장하는 이유는 짐벌록 현상 때문이다. 

 

Quaternion.LookRotation()

사용 예시

transform.rotation = Quaternion.LookRotation(Vector3.forward);

 

 

Quaternion.Slerp()

사용 예시

transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.5f);

시작 회전 값부터 목표 회전 값까지 lerp를 통해 회전

 

 

Prefab

특정 게임 오브젝트의 상태(컴포넌트의 값과 상태까지)를 저장해놓은 것으로 재사용 가능한 에셋 의미한다. 프리팹 에셋은 템플릿 역할을 하며 이를 통해 씬에 새로운 프리팹 인스턴스를 만드는 것이 가능하다. 프리팹의 장점은 프리팹 에셋을 수정하면 프리팹 인스턴스에 모두 일괄 적용이 가능하다.

 

프리팹의 수정

프리팹의 수정은 Project탭에 있는 프리팹 에셋 파일을 더블 클릭하여 프리팹 편집창으로 진입할 수 있게 된다.

유니티 버전 2022.3.9, 씬과 합쳐진 프리팹 편집창

 

또는, Hierachy에서 프리팹 인스턴스 오른쪽 끝에 있는 화살표 버튼을 클릭하여 씬과 합쳐진 프리팹 편집창으로 진입할 수 있다.

 

 

Prefab의 Override

프리팹의 수정은 모두 한 번에 통일 되는데, 각자 다르게 설정을 하고 싶을 수도 있다. 이는 프리팹 인스턴스의 값을 단순히 수정하기만 해도 오버라이딩이 적용된다.

원본 프리팹 vs 오버라이딩된 프리팹

오버라이딩 프리팹은 수정된 부분의 프로퍼티가 굵게 적혀져있고, 왼쪽에 파란색 막대기가 붙어있다. 또 다르게 식별 할 수 있는 방법은 오버라이드 버튼을 클릭해보는 것인데, 아래처럼 프리팹에 대한 오버라이딩 히스토리를 확인할 수 있으며, 이 세팅을 전체 프리팹에 적용하는 'Apply All' 버튼과 해당 프리팹을 프리팹의 원본처럼 복구시키는 'Revert All' 버튼이 존재한다.

오버라이딩 히스토리 이미지

 

 

Nested Prefab

중첩해서 프리팹을 사용하는 방식이다. 유니티 2018.3부터 프리팹 배리언트와 함께 추가된 기능이라고 한다(공식 문서 마지막 확인)

왼쪽과 같이 빈 오브젝트인 PlayerInTank를 만든 뒤 Prefab화 시키면 nested prefab이 된다.

위처럼 Nested prefab처럼 만든 뒤, Tank 프리팹의 컴포넌트값을 수정하면 오버라이드가 떠야할텐데, 유니티 2022의 버그인지 뜨지 않는다. 추후 확인 필요.

 

 

Prefab Variant

기존 프리팹을 다시 prefab화 시키는 작업을 거치면 prefab variant를 만들 수 있는 선택지가 뜬다. 이런 prefab variant는 원본 프리팹의 변경 사항이 생기면 똑같이 적용을 받는다(상속과 비슷).

원본 프리팹의 PlayerController 컴포넌트의 speed 변경 시 PrefabVariant도 변경된다.

물론 FastTank값을 따로 override하는 경우도 가능한데, FastTank의 PlayerController 값을 변경하게 되면 원본 프리팹의 해당하는 컴포넌트 값을 변경해도 오버라이딩된 프리팹 배리언트의 컴포넌트 값은 바뀌지 않는다

 

Model prefab instance

이런 모델 파일들을 씬 하이어라키에 배치하면 위와 같은 아이콘으로 보인다. 이는 프리팹처럼 보이지만, 엄밀히 정의하면 모델 프리팹 인스턴스이고 별도의 모델인 에셋이다. 그래서 모델 프리팹 인스턴스의 내용을 변경했을 때는 원래 프리팹처럼 모든 프리팹의 값이 한 번에 변한다거나 하지 않기에, 위 모델 파일을 다시 프리팹으로 만들어서 사용하는 것이 좋다.

 

 

 

Resource Manager

사실 코드를 제외한 리소스와 같은 것들은 대부분 Resources 디렉토리 밑에서 관리한다. 이를 통해 코드에서 동적으로 리소스를 불러와 사용할 수 있게 된다.

public class PrefabTest : MonoBehaviour
{
    private GameObject prefab;

    GameObject tank;
    void Start()
    {
        prefab = Resources.Load<GameObject>("Prefabs/Tank");
        tank = Instantiate(prefab);

        Destroy(tank, 3.0f);
    }
}

 

그리고 Input도 한 곳에서 관리해서 이벤트를 뿌리는 것처럼, 리소스도 리소스 매니저를 통해 관리하는 것이 좋다. 

 

Collision

Rigidbody, Collider

유니티에서는 리지드바디 컴포넌트를 붙여야 물리적 상호작용이 가능하다.

Mass는 질량이고, 단위는 kg이다. 보통 Mass, Use gravity, Is Kinematic, Constraints를 많이 사용한다.

 

리지드 바디는 물리적인 시뮬레이션을 통해 물체의 위치를 제어한다. 리지드바디 컴포넌트가 오브젝트에 추가되면 오브젝트의 모션 이 유니티의 물리 엔진 아래에서 동작하게 된다. 아무런 코드를 사용하지 않더라도, 리지드바디 오브젝트는 중력으로 인해 아래로 끌려가고 콜라이더가 컴포넌트가 있는 경우 만나게 되는 오브젝트와 충돌 반응이 존재한다. 물론 충돌하는 다른 오브젝트도 콜라이더 컴포넌트를 가지고 있어야 한다.

 

보통 메시를 기준으로 삼각형마다 충돌을 검사하게 되면, 성능이 너무 안좋을 것이다. 그래서 보통 콜라이더를 통해 러프하게 사용한다.

 

스크립트에서 힘을 가하고 리지드바디 설정을 변경하는 곳은 FixedUpdate가 좋다 (대부분의 다른 프레임 업데이트 작업에 사용되는 Update와 달리). 그 이유는 Physix update가 프레임 업데이트와 일치하지 않는 시간 단계로 수행되기 때문이다.

 

FixedUpdate는 각 Physics update 전에 즉시 호출되므로 변경된 사항은 바로 처리된다.

 

그리고 리지드바디는 실제로 모델에 사용된 스케일에 따라 계산이 되는데, 기본 중력 설정은 하나의 월드 unit이 1미터의 거리에 해당한다고 가정한다. 비물리 게임에서는 길이가 모두 100단위라도 상관없지만, 물리를 사용할 때는 매우 큰 오브젝트로 취급되므로 작은 물체에 큰 스케일을 사용하면 물리 엔진은 매우 큰 물체가 매우 먼 거리에서 떨어지는 것으로 간주하여 매우 느리게 떨어지는 것처럼 보인다. 이를 염두에 두고 물체를 실제 크기와 비슷하게 유지해야 한다.

 

 

물리 기반 움직임이 있는 리지드바디 게임 오브젝트

즉, Rigidbody를 사용하게 되면 Transform 프로퍼티 대신 시뮬레이션된 물리 힘돠 토크를 사용하여 게임 오브젝트를 이동하고 물리 엔진이 결과를 계산하도록 할 수 있다. 대부분의 경우 게임 오브젝트에 리지드바디가 있는 경우, 게임 오브젝트를 이동하려면 트랜스폼 프로퍼티 대신 Rigidbody 프로퍼티를 사용해야 한다. Rigidtbody 프로퍼티는 물리 시스템의 힘과 토크를 적용하여 게임 오브젝트의 트랜스폼을 변경하는데, 트랜스폼을 직접 변경하면 Unity가 물리 시뮬레이션을 올바르게 계산하지 못하여 원치 않는 동작이 발생할 수 있다. 특히  조인트가 있는 경우 에러가 더 쉽게 발생한다. 

 

스크립트를 통해 리지드바디를 제어하기 위한 주요 클래스는 `AddForce`와 `AddTorque`이다.

 

물리 기반 움직임이 없는 리지드바디 게임 오브젝트

물리 시스템이 게임 오브젝트를 감지하되 제어하지 않기를 원하는 경우가 있다. 예를 들어 콜라이더가 게임 오브젝트를 감지하도록 하되, Transform을 통해 해당 게임 오브젝트의 움직임과 포지션을 제어하려고 할 수 있다.

 

유니티에서 물리 기반이 아닌 움직임을 키네마틱 모션이라고 한다. Rigidbody 컴포넌트에는 is Kinematic 프로퍼티가 존재하는데, 이 프로퍼티를 활성화하면 연결된 게임 오브젝트를 물리 기반이 아닌 것으로 정의하고 물리 엔진의 제어에서 제거한다. 이렇게 하면 unity의 물리 시뮬레이션 계산이 변경 사항을 오버라이드하지 않고도 트랜스폼을 통해 운동학적으로 움직일 수 있다.

 

키네마틱 리지드바디는 물리 기반 리지드바디 게임 오브젝트에 물리 기반 힘을 가할 수 있다. 그러나 물리 기반 힘을 받지는 않는다. 즉, 키네마틱 리지드바디는 밀 수는 있지만, 밀려나진 않는다. 조인트를 사용하여 키네마틱 리지드바디를 일반 리지드바디에 연결하는 경우 조인트는 히믕 가하여 키네마틱 리지드바디를 움직일 수 없다.

 

리지드바디는 물리 시스템이 아닌 트랜스폼 포지션을 통해 움직이는 정적 콜라이더(즉, 리지드바디가 없는 콜라이더)의 움직임과 충돌에 반응하여 깨어나지 못할 수 있다. 특히 물리 시스템이 더 이상 정적 콜라이더를 감지할 수 없는 경우 이러한 문제가 발생할 가능성이 높다. 이 경우 Rigidbody.WakeUp을 사용하여 잠자고 있는 리지드바디를 깨울 수 있다. 

 

Rigidbody와 Collider의 정리

리지드 바디 없이 콜라이더만 가진 두 오브젝트가 만나면 물리적 상호작용은 발생하지 않는다.

 

그래서 고정적으로 캐릭터를 조종하는 상황이고, 캐릭터에게 충돌 판정을 주고 싶으면, 캐릭터는 리지드바디 컴포넌트(isKinematic은 off)와 콜라이더 컴포넌트를 가지고 있어야 하고 충돌되는 물체는 리지드바디는 없더라도 콜라이더 컴포넌트는 무조건 필요하다. 여기서 충돌되는 물체에도 리지드 바디를 붙여주면 서로 밀거나 밀릴 수 있게 된다. Collider의 isTrigger옵션도 마찬가지로 리지드바디가 필요하므로 주의해야 한다.

 

OnCollisionEnter

  1. 나 혹은 상대한테 RigidBody가 있어야 한다. (IsKinematic: off)
  2. 나한테 Collider가 있어야 한다. (IsTrigger: off)
  3. 상대한테 Collider가 있어야 한다. (IsTrigger: off)

 

Physics body collider, Dynamic Collider 설명

 

Collider 타입 간의 interaction (collision detection messages)

 

 

Trigger

보통 둘 중 하나만 Trigger만 켜져있어도 Trigger가 발동. 그리고 둘 중 하나는 최소 리지드바디를 하나는 들고 있어야 한다.

  1. 둘 다 콜라이더가 있어야 한다.
  2. 둘 중 하나는 IsTriggerOn.
  3. 둘 중 하나는 리지드바디가 있어야 한다

Trigger는 스킬과 같은 부분에서 사용하거나 검에 부착하여 판정을 주는 방법, 바닥에 도트 장판을 까는 등(서버는 수학적 계산으로 하겠지만). 응용 경우의 수는 많다.

 

 

Raycasting

Raycasting은 레이저를 쐈을 때 레이저에 부딪히는 물체가 있는지 없는지 판단하는 기술이다. Raycasting을 사용할 때에도 상대에게 Collider가 필요하다.

 

플레이어 캐릭터의 정면을 기준으로 Raycast하는 코드

void Update()
{
    Vector3 look = transform.TransformDirection(Vector3.forward);
    Debug.DrawRay(transform.position + Vector3.up, look * 10, Color.red);

    RaycastHit hit;
    if (Physics.Raycast(transform.position, look, out hit, 10))
    {
        Debug.Log($"Raycast {hit.collider.gameObject.name} !");
    }
}

 

 

만약 레이캐스트가 모든 물체를 통과하도록 만들고 싶으면 `Physics.RaycastAll()`을 사용하면 된다.

void Update()
{
    Vector3 look = transform.TransformDirection(Vector3.forward);
    Debug.DrawRay(transform.position + Vector3.up, look * 10, Color.red);

    RaycastHit[] hits;
    hits = Physics.RaycastAll(transform.position + Vector3.up, look, 10);

    foreach (RaycastHit hit in hits)
    {
        Debug.Log($"Raycast {hit.collider.gameObject.name}!");
    }
}

 

 

투영의 개념

Local <-> World <-> Viewport <-> Screen

 

Screen coordinate

Viewport, Screen 좌표는 비슷해서 뭉뚱그려 보기도 하지만, 따로따로 확인해보자.

void Update()
{
    Debug.Log(Input.mousePosition);
}

Screen coordinate (pixel coordinate)를 출력한 이미지

마우스 포지션에 따 Screen coordinate를 위처럼 확인할 수 있다. 왼쪽 아래는 (0,0)이고 오른쪽 위는 화면크기의 최대값이다. z축은 사용하지 않는다.

 

Viewport coordinate

Viewport는 카메라와 연관이 있다. Screen coordinate를 0과 1사이로 정규화 시킨 값이다. 그래서 왼쪽 아래는 (0,0)이고 오른쪽 위는 (1,1)이다. Screen coordinate 똑같이 z축은 사용하지 않는다.

 

카메라

그러면 특정 스크린 좌표를 알았을 때 어떻게 월드 좌표를 알아낼 수 있을까? 그 말은 즉, 2D 좌표와 3D 좌표를 서로 변환할 수 있어야 한다는 의미이다. 이를 이해하기 위해선 카메라를 우선 알아야 한다.

카메라를 통해 촬영을 할 때 절두체 바깥 영역은 컬링이 되어 유니티엔진에서 연산도 하지 않고 그리지 않는다. 

그리고 절두체 안에 있다고 모든 물체가 다 렌더링되는 것은 아니고 카메라 기준으로는 맨 앞의 물체가 찍히게 될 것인데, 플레이어 앞에 엄청 작은 박스를 놨다고 가정하면 플레이어가 그 박스를 가리고 있기에 그 박스도 그리지 않는다. 그리고 원근법으로 인해 같은 크기의 물체라도 다르게 렌더링할 것이다.

 

즉, 이 모든것의 의미는 3D 화면에서 2D화면으로 넘어올 때는 좌표가 하나 없어진다.

3D에서 보고 있는 부분을 사진을 찍어서 해당 평면에 오려 붙인다고 생각하면 3D처럼 보일 것이다(카메라처럼). 이를 투영이라고 한다. 즉 전체 절두체 안에서 보고 있는 영역을 위 이미지 평면에 달라붙을 정도 한 축을 없애게 된다.

 

 

클릭한 Screen좌표를 World 좌표로 변환하는 법

if (Input.GetMouseButton(0))
{
    Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));
    Vector3 dir = mousePos - Camera.main.transform.position; // 방향벡터
    dir = dir.normalized;

    Debug.DrawRay(Camera.main.transform.position, dir * 100.0f, Color.red, 1.0f);

    RaycastHit hit;
    if (Physics.Raycast(Camera.main.transform.position, dir, out hit, 100.0f))
    {
        Debug.Log($"Raycast Camera @ {hit.collider.gameObject.name}!");
    }
}

 

`Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));` 카메라의 clipPlane을 온전히 가져오려면, Camera컴포넌트의 Clipping plane의 near까지 이동해야 하기에 z값을 해당 코드처럼 사용한다. 그 다음 마우스의 위치와 카메라의 위치를 빼서 방향벡터를 구한 뒤 이를 기반으로 레이캐스팅을 한다.

 

그리고 아래 코드를 Ray를 사용해서 한 줄로 줄일 수 있다.

Vector3 mousePos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane));
Vector3 dir = mousePos - Camera.main.transform.position; // 방향벡터
dir = dir.normalized;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
void Update()
{
    //Debug.Log(Input.mousePosition); // Screen
    //Debug.Log(Camera.main.ScreenToViewportPoint(Input.mousePosition)); // Viewport

    if (Input.GetMouseButton(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f))
        {
            Debug.Log($"Raycast Camera @ {hit.collider.gameObject.name}!");
        }
    }
}

 

 

LayerMask

레이캐스팅의 성능과 최적화에 다루기 위해 이 내용을 다룬다. 레이 캐스팅은 정말 무거운 작업이고 신중히 써야 한다. 박스 콜라이더와 같은 경우는 연산이 그래도 괜찮겠지만, 메시 자체를 충돌범위로 인식한다던가 하는 상황에선 연산부하가 엄청 커지게 된다. 일반적으로 충돌을 구현할 때는 충돌전용 메시를 따로 만든다. 그리고 안보이는 상태로 중복배치를 해서 연산에 활용한다. 그렇다해서 모든 문제를 해결할 수 있는 건아니고, 몇 천개의 물체가 있다면 레이캐스팅 연산 부하가 심해진다. 레이 캐스팅은 용도에 따라서 여러 가지 객체로 구분되는 경우가 많다. 예를 들어 레이 캐스팅을 활용한 카메라 줌인 아웃 기능은 주변 환경만 계산하면 되고, 스킬을 쓰기 위해 상대 몬스터를 클릭할 때는 몬스터나 플레이어 대상으로만 레이 캐스팅을 쓰면 된다.

 

그래서 Layer를 이용해서 연산하고 싶은 것만 골라서 레이 캐스팅을 연산할 수 있게 된다.

Layer mask 설정

void Update()
{
    //Debug.Log(Input.mousePosition); // Screen
    //Debug.Log(Camera.main.ScreenToViewportPoint(Input.mousePosition)); // Viewport

    if (Input.GetMouseButton(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

        LayerMask mask = LayerMask.GetMask("Monster") | LayerMask.GetMask("Wall");
        // int mask = (1 << 6) | (1 << 7); // 이렇게도 사용가능
        Debug.Log($"mask: {mask}");

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, mask))
        {
            Debug.Log($"Raycast Camera @ {hit.collider.gameObject.name}!");
        }
    }
}

 

부하를 많이 주는 작업이기에 이처럼 당연히 최적화를 해야 한다. 

 

 

Camera

Camera의 Culling mask 설정을 통해 해당 Layer의 객체들을 렌더링되지 않게할 수 있다.

Camera 컴포넌트의 Culling mask 설정

 

public class CameraController : MonoBehaviour
{
    [SerializeField]
    Define.CameraMode _mode = Define.CameraMode.QuarterView;

    [SerializeField]
    Vector3 _delta; // 방향 벡터

    [SerializeField]
    GameObject _player; // 나중에는 매니저에서 들고올 수 있겠지만, 우선은 구조가 정해지지 않았으니 이렇게.

    void Start()
    {
        
    }

    void LateUpdate()
    {
        transform.position = _player.transform.position + _delta;
        transform.LookAt(_player.transform);
    }
}

 

플레이어를 따라 다니는 카메라의 코드인데, 플레이어의 Input 연산이 수행된 후 카메라가 쫓아가도록 설정하는 것이 좋기에 LateUpdate()를 사용한다. 만약 두 코드 다 Update()에 있으면, 어떤 함수가 먼저 수행될지 모르기 때문에 덜덜 거리는 현상이 발생할 수 있다.

 

캐릭터 움직임의 Overshooting 방지

오버슈팅의 예시 그림

Overshooting이란, 움직이는 오브젝트가 도착지점에 완전히 도착하지 못하고 움직이는 거리가 목적지점에 딱 알맞지 않아서, 도착지점을 중간에 두고 지속적으로 움직이는 현상이다. 이것이 발생하면 목적지 주변에서 오브젝트가 떨리는 현상을 확인할 수 있다.

void Update()
{
    if (_moveToDest)
    {
        Vector3 dir = _destPos - transform.position;
        float moveDist = _speed * Time.deltaTime;
        transform.position += dir.normalized * moveDist;
        transform.LookAt(_destPos);
    }
}

위 로그값을 보면 목적지는 (1.04, 0.0, 0.55)이다. 움직이는 오브젝트에서 목적지에 까지의 거리는 0.00132 이지만, moveDist가 0.06749이며, 이에 dir.normalized(방향값만)을 곱하여 움직이면 목적 좌표를 넘어서게 된다.

 

void Update()
{
    if (_moveToDest)
    {
        Vector3 dir = _destPos - transform.position;
        float moveDist = _speed * Time.deltaTime;
        transform.position += dir.normalized * moveDist;
        transform.LookAt(_destPos);
        
        if (dir.magnitude < 0.0001f)
        {
            _moveToDest = false;
        }
    }
}

만약 이러한 방법으로 예외처리를 하게 되면, 부들거리는 현상이 언젠가는 끝날 수는 있다. dir.magnitude가 값이 작게 나오는 운이 좋은 타이밍에 움직임은 멈추겠지만, 움직이는 오브젝트가 어느 방향을 보게 될지는 모른다. 하지만 해당 예외처리는 꼭 필요한 이유가, float는 소수점 단위에서 ==으로 정확한 연산이 많이 힘들기 때문에 해당 방식으로 움직이는 상태를 종료시킬 필요는 있다.

 


Vector3의 magnitude를 최대값으로 설정 후, 이를 벗어나서 계산되지 않도록 하면 오브젝트의 오버슈팅 문제는 해결될 수 있다. 또한 transform.position == _destPos와 같이 비교 연산을 수행한 예외처리 방식은 사용하지 말아야 한다.

void Update()
{
    if (_moveToDest)
    {
        Vector3 dir = _destPos - transform.position;

        if (dir.magnitude < 0.0001f)
        {
            _moveToDest = false;
        }
        else
        {
            // moveDist는 반드시 dir.magnitude보단 작아야 한다.
            //float moveDist = _speed * Time.deltaTime; 
            //if (moveDist >= dir.magnitude)
            //    moveDist = dir.magnitude;
            float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;
            transform.LookAt(_destPos);
        }
    }
}

 

 

벽이 존재할 경우 카메라 위치 변경

// CameraController.cs

[SerializeField]
Vector3 _delta = new Vector3(0.0f, 6.0f, -5.0f); // 방향 벡터

void LateUpdate()
{
    if (_mode == Define.CameraMode.QuarterView)
    {
        RaycastHit hit;
        if (Physics.Raycast(_player.transform.position, _delta, out hit, _delta.magnitude, LayerMask.GetMask("Wall")))
        {
            Debug.Log($"_delta: {_delta}");
            float dist = (hit.point - _player.transform.position).magnitude * 0.8f;
            transform.position = _player.transform.position + _delta.normalized * dist;
        }
        else
        {
            transform.position = _player.transform.position + _delta;
            transform.LookAt(_player.transform);
        }
    }
}

 

 

 

참고자료