본문 바로가기
UNITY/유니티게임스쿨

[유니티게임스쿨 TIL] 유니티 기본 :: 프리팹과 오브젝트, 캐릭터 이동, 점프 기능 추가하기

by 램플릿 2024. 6. 3.

 

프리팹과 오브젝트


프리팹과 오브젝트의 차이점은?

Hierarchy 창의 오브젝트들이 인스턴스라면, Project 창의 Prefab은 클래스라고 할 수 있다.

즉, 프로젝트 창의 Prefab을 끌어다 하이어라키 창에 놓으면 인스턴스화 되며 오브젝트가 생성된다.

 

  • 마우스 오른쪽 클릭후 Create Empty Parent 를 누르면 부모 오브젝트가 생성된다.

 

 

  • 오브젝트에서 Select를 누르면 원래의 프리팹이 위치한 장소로 이동하여 Project창에 띄워준다.
  • 오브젝트에서 기존 프리팹과 달라진 변경사항이 있으면 Overrides에 Apply All이 활성화되고, 이를 클릭하면 프리팹과 다른 인스턴스들에도 변경사항이 동일하게 적용된다.

 

 

 

🎓 새로운 지식 :: 메인 카메라 설정
카메라 오브젝트의 Tag를 MainCamera로 설정하면 해당 카메라가 게임 플레이 화면의 메인 카메라로 사용된다.

 

 

 


 

캐릭터 이동 기능 추가하기


  • 먼저, 멤버변수로 이동속도, 회전속도, 점프력, 움직일캐릭터 등을 선언한다.
public float speed = 20; //이동속력
public float rotSpeed; //회전속력
public float jumpPower; //점프력
public GameObject CharacterObject; //캐릭터

private bool isGrounded = true; // 점프시 땅을 밟았는가?를 판단하기 위해 선언.

Vector3 direction = Vector3.zero; //위치를 저장하는 멤버변수. 벡터를 (0,0,0)으로 초기화
Vector3 rotDirection = Vector3.zero; //회전각도를 저장하는 벡터

 

 

  • public으로 멤버변수 작성 시 ⇒ 에디터에 해당 항목들이 표시됨

  •  GameObject 칸에는 원하는 오브젝트를 끌어다 바인딩 하면 된다.

 

 void Update()
 {
     direction = Vector3.zero;
     rotDirection = Vector3.zero;
     // 매 업데이트 마다 direction, rotDirection을 초기화 해주어 그 다음 인풋이 적용되도록 한다.

     if (Input.GetKey(KeyCode.W))		...

     if (Input.GetKey(KeyCode.S))		...

     if (Input.GetKey(KeyCode.A)) 		...

     if (Input.GetKey(KeyCode.D))		...

	//벡터의 정규화
     direction.Normalize();
     rotDirection.Normalize();
 }

 

키보드 키 입력 받기

if (Input.GetKey(KeyCode.W))
{
    direction += CharacterObject.transform.forward;
}

transform.forward : 오브젝트가 바라보고 있는 방향을 기준으로 앞 방향(Blue axis = Z축 방향)

 

 

벡터의 정규화 (normalize)

     벡터의 길이를 정규화 하여 길이가 1인 벡터를 만드는 로직.

     예를 들어 x방향으로 1, y방향으로 1이 주어졌을 때 정규화를 하지 않으면 대각선 방향으로 나아갈 때 1보더 더 먼 거리를 이동하게 된다. 정규화를 거치면 대각선으로 이동할 때에도 1만큼만 나아가게 할 수 있다.

Vector3 direction = Vector3.zero;
...
direction.Normalize();

 

 

캐릭터 이동하기

void FixedUpdate()
{
    // current, target까지 speed * Time.fixedDeltaTime만큼 이동한 벡터의 위치를 반환한다.
    Vector3 nextPosition = Vector3.MoveTowards(transform.position,
     transform.position + direction * 1000, 
     speed * Time.fixedDeltaTime);

    // nextPosition으로 물리이동한다.
    GetComponent<Rigidbody>().MovePosition(nextPosition);
}

현재 위치 = transform.position

목표 위치 = transform.position + direction*1000

이동 거리 = speed * Time.fixedDeltaTime (속력*시간)

 

 

🎓 새로운 지식 :: Update()와 FixedUpdate()의 차이점
Update()와 Time.deltaTime
Update()는 매 프레임마다 한번 씩 실행된다. 따라서, fps가 높아질수록 더 많이 실행하게 된다.
deltaTime은 한 프레임과 그 다음 프레임이 출력되는 사이의 시간을 의미한다. 
    deltaTime = 1/frameRate
30fps일 때 Update()는 1초동안 30번 실행되고, 이 때 deltaTime은 1/30초가 된다.  60fps라면 Update()는 1초동안 60회 실행, deltaTime은 1/60초다.

FixedUpdate()와 Time.fixedDeltaTime
FixedUpdate()는 정해진 물리 시간마다 한 번 실행되며, Time.fixedDeltaTime은 그 기준이 되는 물리 시간이다.
따라서 Update()와는 달리 FixedUpdate()는 fps와 관계없이 상수시간마다 동작한다.

Edit > ProjectSetting > Time 탭에서 fixed Timestep을 0.02로 설정하였다면 Time.fixedDeltaTime은 현실 시간의 0.02초이며, FixedUpdate() 또한 0.02초마다 한 번씩 실행된다.

 

 

💡질문 :: 사용하려는 변수가 지역변수와 이름이 같은 경우 어떻게 해야하나요?
⇒ this.~ 를 사용해줍니다.
public int name;

void Func(int name)
{
    this.name = name;
    
    name = 1; //지역변수에 1이 들어간다.
    this.name = 1; //public int name에 1이 들어간다.
}

+) 강사님 코멘트 : 다만, 같은 이름의 변수들을 사용하는 것은 권장하지 않습니다. 규칙을 정하여 _name처럼 사용하는 등 다른 방식을 취하는 것이 좋습니다.

 예시 : public int name;

    void Func2(int _name)
    {        name = _name;     }

 


 

캐릭터 회전 기능 추가하기


 

키보드 키 입력받기

void Update(){
	...
    
    if (Input.GetKey(KeyCode.A))
    {
 	   rotDirection += -CharacterObject.transform.right;
	}
    if (Input.GetKey(KeyCode.A))
    {
 	   rotDirection += CharacterObject.transform.right;
	}

}

transform.right : 오브젝트가 바라보고 있는 방향을 기준으로 오른쪽(Red axis = X축 방향)

 

캐릭터 회전하기

void FixedUpdate(){
	...
    if (rotDirection.sqrMagnitude > 0.001f) //sqrMagnitude가 유효한지(0이 아닌지) 확인
    {
        // newRot을 계산한다.
        Quaternion newRot = Quaternion.RotateTowards(CharacterObject.transform.rotation,
          Quaternion.LookRotation(rotDirection),
          Time.fixedDeltaTime * rotSpeed);

        CharacterObject.transform.rotation = newRot;
    }
}

Quaternion.RotateTowars : From -> To 로 회전할 값을 정함.

현재 내가 보고 있는 방향(From) : CharacterObject.transform.rotation

내가 바라볼 방향(To) : Quaternion.LookRotation(direction)

회전하는 양 : Time.FixedDeltaTime*rotSpeed

 

💡질문 :: Quaternion이 뭔가요?
⇒ 인스펙터 창에는 3축의 회전을 직관적으로 이해할 수 있도록 오일러각(x,y,z)이 쓰여있지만 실제로 이를 계산하기 위해서 유니티에서는 쿼터니언을 사용합니다. 쿼터니언은 4개의 요소(x,y,z,w)를 통해 회전을 표현하며 이에 대해서는 게임 수학을 다룰 때 다시 설명하겠습니다.

 

 

점프 기능 추가하기


땅에 Ground 태그 추가하기

  • 아무 오브젝트나 클릭하여 Inspector 창 > Tag > Add Tag 를 눌러 새 태그를 추가한다.

 

  • 태그 리스트에 Ground 라는 이름의 태그를 추가해준다.

 

  • Collider 컴포넌트를 가진 오브젝트의 태그를 Ground로 설정해준다.

 

키 입력받기 & 점프 한번만 뛰게 만들기

private bool isGrounded = true;
...

void Update(){
	...
    if (Input.GetKeyDown(KeyCode.Space) && isGrounded) //땅에 닿아있을때만 점프
    {
        GetComponent<Rigidbody>().AddForce(Vector3.up * jumpPower, ForceMode.Impulse);

        isGrounded = false; //땅에서 떨어지면 false
    }
    ...
}

스페이스바가 입력되었으며 땅에 착지한 상태일때, Rigidbody에 AddForce()함수를 통해 물리력을 가한다.

 

리지드바디에 Vector3.up * jumpPower만큼 ForceMode.Impulse효과를 준다.

ForceMode 힘이 적용되는 방식 중 Impulse는 정지 상태서 이동을 시작하려 할 때 순간적으로 힘을 가하여 캐릭터가 점프하는 모습을 만들어낸다.

 

점프한 상태에서는 다시 점프할 수 없도록 isGrounded를 false로 바꾸어준다.

참고하면 좋은 자료👍 :: ForceMode
https://blog.naver.com/gold_metal/221486016593

 

 

땅과 충돌하면 착지 상태로 인식하기

     오브젝트가 무언가와 부딪히면 collision이 발생하여 이를 인식할 수 있다. 캐릭터가 점프했다가 내려와 Ground 태그를 가진 땅에 닿게 되면 이를 인식하여 isGrounded를 다시 true로 바꾸어주면 된다.

  • OnCollisionEnter(Collision collision) : 이 오브젝트가 다른 콜라이더와 충돌을 일으켰을 때, 충돌 판정이 일어나며 이 오브젝트와 충돌된 객체가 collision에 담겨져서 반환된다.
void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.CompareTag("Ground")) //충돌을 일으킨 콜라이더가 Ground인지?
    {
        isGrounded = true; //충돌 : Ground에 닿았으면 true.
    }
}

+ 강사님 코멘트 : collision.gameObject.CompareTag("Ground")) 와 collision.gameObject.tag == "Ground" 는 동일하지만, 개인적으로 명확한 구분을 위해 CompareTag()를 활용하는 것을 더 추천합니다.

 

 

🎓 새로운 지식
GetComponent<Rigidbody>() ... 를 너무 자주 사용하는 것 같을 때에는 변수화를 통해 이를 최적화 할 수 있습니다.
Start(){ Rigidbody rigidbody = GetComponent<Rigidbody>(); }
Update() { rigidbody.AddForce.... } 와 같이 활용할 수 있습니다.
이러한 방법을 캐싱이라고 하며 최적화(캐싱, 메모리콜 ..등) 에 대해서는 다음에 최적화를 다룰 때 설명하겠습니다.