본문 바로가기

IT/유니티

11. 유니티 교육 (Sebastian Lague 강의1)

1. 벡터

벡터를 모르고 게임을 만든다고 하면, 그건 거짓말이라고 할수 있다.

물론 단순한 게임은 만들수 있겠지만, 

아직가지 세계를 구성하는 힘이 무엇인지는 정확히 알수 없지만,

게임에서 벡터란 모든 힘의 근원이다. 

결국 이걸 모르면 안것도 안되는것이다.

무생물을 생물로 바꿔줄수 있는 마법과 같은 힘! 바로 벡터이다.


학교때 배운 백터는 단순히 x와 y좌표를 가지고 있는거 처럼 보였다.

하지만 단순히 좌표만을 의미하지 않는다.

위치와 힘을 가질수 있다. 밑에 그림을 보면

플레이어와 적의 위치를 표현했다.


게임에서 당연히 구현되는것이 적이 플레이어를 향해서 공격하는 것이다.

그럼 과연 적은 플레이어가 어디에 있는지 알고, 어느 방향으로 쏴야 하는지 알수 있을까?


결국 벡터를 이용하면 된다.

플레이어의 위치에서 적의 위치를 빼면, 방향이 나온다.

그리고 그 방향은 노말라이징 시킨다. 

(노말라이징이란 그 벡터의 힘이 1이 되도록 한것이다.)

그 노말라이징 시킨 값에다가 총알이나 발사체의 속도계수를 더하고, 업데이트를 한다면,

그 객체는 플레이어를 향해서 날라갈 것이다.


단순히 의미없이 정석에서 배울때와 달리, 수학을 공부해서 필요한곳에 적용해보면,

그 성취감은 이루 말할수 없다.

내가 원하는대로 다 해주니까... 공식몇개만 알면....





Tip. deltaTime

델타타임은 전프레임에서 현재 프레임사이의 시간입니다.

그럼 왜 이 델타타임이 필요할까?

왜냐하면 컴퓨터는 게임기와 다르게, 컴퓨터 마다 연산속도가다르기 때문입니다.

게임이란 개발해보시면 알겠지만, 결국 update문이 실행될때마다 데이타의 연산이 실행됩니다.

그럼 캐릭터가 이동한다고 생각하면, 

빠른 컴퓨터는 1초에 10번 업데이트를 실행하고

느린 컴퓨터는 1초에 2번 업데이트를 실행한다면,

과연 이 두캐릭터는 1초에 같은 거리는 이동할까요?

아니겠죠. 빠른 컴퓨터의 캐릭터가 훨씬 더 많이 움직입니다.


가끔식 옛날 향수에 고전게임을 해보신적이 있나요?

예전 지관유라고 대만 회사 게임을 오랜만에 해봤더니,캐릭터들이 너무 빨리 움직여서

게임을 할수가 없었습니다.


왜그럴까요? 역시나 중국스타일의 날림 코딩이여서 그런지 걍 업데이트에 캐릭터 이동속도를 

박아 놓은거죠.


이러하듯이 거의 모든 객체의 이동에는 반드시 컴퓨터 성능을 고려한 코딩이 필요합니다.

그 코딩이 바로 델타타임을 이용한것입니다.


1초에 10번 업데이트한다면, 0.1

1초에 2번 업데이트한다면, 0,5로 두고

이 값들이 이동치에 곱한다면 결국 같은 시간이 같은 거리는 움직일수 있게 됩니다.


기본이지만, 모르고 넘어가는 분들이 많은거 같습니다.





*만약에 vector3 구조체가 없다면, 우리는 코딩할때 항상 불편할것이다.

매번 x,y,z을 선언해주어야 하니까.

그리고 vector3 구조체는 기본적인 산술식을 다 적용할수 있다.

예를 들어 더하기 빼기 곱하기 나누기




*Invoke("SetNewRandomTime", roundStartDelaytime);

간단히 Invoke에 대해서 설명하고 넘어간다.

Invoke는 불러내다 라는 뜻인데, 첫번째 인자는 불러낼 함수의 이름이고,

두번재 인자는 몇초뒤에 불러내는가 이다.

적절히 사용하면 도움이 될것이다.



2. 날잡아봐라


세바스찬의 강의에서 첫번째 게임은 단수히 체이스게임이다.

밑의 그림처럼 구가 큐브를 따라다닌다.


백터를 이해하고 있다면, 아주 간단하게 구현가능하다. 

public class Player : MonoBehaviour {


    public float speed = 10;


// Use this for initialization

void Start () {

        

}

// Update is called once per frame

void Update () {

        Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

        Vector3 direction = input.normalized;

        Vector3 velocity = direction * speed;

        Vector3 moveAmount = velocity * Time.deltaTime;


        //transform.position += moveAmount;

        transform.Translate(moveAmount);

}

}

Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

위의 코드는 키보드 이동키 입력을 받기 위해서 넣은것이다.

좌우는 x좌표, 위아래는 z좌표


Vector3 direction = input.normalized;

        Vector3 velocity = direction * speed;

        Vector3 moveAmount = velocity * Time.deltaTime;

키보드에서 입력받은 값을 기준으로 방향을 정하고, 그 방향에 스피드를 곱하면 속도가 정해진다.

그리고 그 속도를 컴퓨터에 속도에 상관없이 시간에 일정하게 움직이게하기위해 deltatime을 곱한다.






3. Space, Parenting, Rotation


전부터 항상 애매하게 느끼고 있었던 전역과 로컬 그리고 부모와 자식의 관계에 대해서 알아보자.

많은 강의를 들어봤지만, 어느 누구도 명확히 설명해주지 않았는데, 이번에 조금 이해할수 있게 되었다.

이해한것을 써보면 다음과 같다.


space는 local과 global이 있다.

local에서 회전시키면 x,y,z도 그 회전에 따라 방향이 바뀐다.

local은 부모가 누구인지는 상관하지 않고 자신를 기준으로 움직인다.


global에서는 local과 다른다. 회전을 아무리 시켜도 x,y,z의 이동방향은 글로벌기분으로 움직인다.

global은 자신의 부모을 기준으로 회전이나 이동을 하는 것이다.


아시는 분들은 당연하시겠지만, 나에게는 아주 큰 도움이 되었다.

결국 로컬이냐 글로벌이냐가 중요하기 보다는, 부모 자식의 관계가 중요하고,

그 관계에서 로컬과 글로벌이 갈리게 된다.


아래는 큐브를 중심으로 구가 회전하는 씬을 만들어 보았다.

 

public class E09Cube : MonoBehaviour {


    public Transform SphereTransfrom;



// Use this for initialization

void Start () {

        SphereTransfrom.parent = transform;

        SphereTransfrom.localScale = Vector3.one * 2;

}

// Update is called once per frame

void Update () {


        //오일러앵글은 기준이 스페이스를 기준으로 돈다.

        //transform.eulerAngles += new Vector3(0, 1 , 0) * Time.deltaTime;

        //transform.eulerAngles += Vector3.up * 100 * Time.deltaTime;


        //스페이스월드 인자를 빼면 로컬을 기준으로 돈다.

        transform.Rotate(Vector3.up*Time.deltaTime * 100, Space.World);

        transform.Translate(Vector3.forward*Time.deltaTime * 7, Space.World);


        if (Input.GetKeyDown(KeyCode.Space))

        {

            //글로벌 기준의 좌표

            //SphereTransfrom.position = Vector3.zero;


            //로컬 기준의 좌표

            SphereTransfrom.localPosition = Vector3.zero;

        }

    }

}


public Transform SphereTransfrom;

퍼블릭으로 선언했으므로, 인스펙터에서 구를 그래그해서 놓는다.


// Use this for initialization

void Start () {

        SphereTransfrom.parent = transform;

        SphereTransfrom.localScale = Vector3.one * 2;

}

실행되면 SphereTransfrom의 부모를 자신으로두고,  SphereTransfrom의 크기를 2배로 한다. 


나머지는 코드에 주석으로 써놓다.

글로벌 기준으로 도는지, 로컬로 도는지에 따라서 전혀 다른 결과가 나오므로 신경을 써야 할것이다.




4. Collision Detection


처음 게임만들때 가장 재미있었고, 마지막까지 힘들었던 것은 바로 충돌체크였다.

아직도 삼각형 충돌체크는 극복을 못했다......아...수학을 공부했어야 했어...

아무튼 유니티는 기본적으로 제공해준다.


예전에 겜만들었을대 기본 구 충돌체크는 완벽하다고 생각했는데, 공의 속도가 빨라지니까

충돌은 안하기 시작했다. 

곰곰히 생각해보니 공이 너무 빠르다면 업데이트 사이에 이미 통과해버리기때문에 충동자체를 체크 할수 없었다.

하지만 유니티는 엔진이지 않은가..

나처럼 대충하는 사람들이 아니니 해결 방법을 아래와 같이 제시했다.

그것은 바로 FixedUpdate를 사용하는 것이다.

아래 그림을 보면 알겠지만, 파란큐브는 가속도때문에 2번과 3번프레이 사이가 엄청 벌어져 있다.

그 벌어진 사이에 만약 충돌체크를 해야 한다면 아마도 충돌을 감지하지 못할것이다.

이럴때를 위해서 가속도와 상관없이 무조건 고정된 업데이트를 적용해서 충돌체크를 잡아낼수 있다.


쩝..대단하다....이거때문에 한 4년전에 거의 한달이상을 고생했는데...

그때에도 결국에 해결 못했는데, 이 그림 한장을 보니 바로 이해가 된다.


역시 난 재능이 없는거 같다....



이 충돌 체크를 이용해서 아래와같은 간단한 동전먹기 게임을 만들수 있다.

객체에 중력이나 물리 엔진을 적용하기위해서는 rigidBody컴포넌트를 추가하면 된다.

 (Is Kinematic 이라는 옵션은 충돌체크를 무시하라는 뜻)


소스코드는 다음과 같다.



using UnityEngine;

using UnityEngine.UI;

using System.Collections;


public class E10Player : MonoBehaviour {


    public float speed = 20;

    int coinCount;

    Vector3 velocity;

    Rigidbody myRigidBody;

    GameObject coinLB;


    // Use this for initialization

    void Start () {

        myRigidBody = GetComponent<Rigidbody>();


        coinLB = GameObject.FindGameObjectWithTag("coinCountText");

        coinLB.GetComponent<Text>().text = "Coin: 0";

}

// Update is called once per frame

void Update () {

        Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));

        Vector3 direction = input.normalized;

        velocity = direction * speed;

    }


    void FixedUpdate()

    {

        myRigidBody.position += velocity * Time.deltaTime;

    }


    void OnTriggerEnter(Collider triggerCollider)

    {

        if (triggerCollider.tag == "coin")

        {


            GetComponent<AudioSource>().Play();

            ++coinCount;

            coinLB.GetComponent<Text>().text = "Coin: " + coinCount;

            Destroy(triggerCollider.gameObject);

        }

    }

}


소스코드는 워낙 단순하기때문에 별다른 설명은 하지 않겠다.
세바스찬의 강의는 단순히 동전먹기에서 끝났지만,
먹은 동전개수 라벨과 동전먹을때 사운드 임펙트를 추가시켜보았다.