본문 바로가기

IT/유니티

14. 유니티 교육 (Sebastian Lague 강의-3DShooter #1)

1. 총쌈겜 만들기 시작


지난번에 2D로 단순한 피하기 게임을 만들었다.

이번에는 3D를 이용한 총싸움 게임을 만들어 보자.

다음 이미지와 같은 식으로~~




2. 플레이어


우리의 주인공을 만들어 보자.

어떻게 보면 이겜은 좀비박멸겜과 같을수 있을것이다.

다만 메트리얼이나 모델링이 없어서 단순해 보일수도 있지만..


플레이어는 간단히 캡슐로 만들어 놓는다.

그리고 플레이어에 관한  스크립트 2개를 만들어 보자.


Player.cs

using UnityEngine;

using System.Collections;


//이 스립트를 오브젝트에 붙이면 playerController스립트도 같이 가서 붙는다.

//이 스크립트가 지우지 않는한 playerController는 지울수 없다.

[RequireComponent (typeof(PlayerController))]


public class Player : MonoBehaviour {


public float moveSpeed = 5;


Camera viewCamera;

PlayerController controller;


// Use this for initialization

void Start () {

controller = GetComponent<PlayerController> ();

viewCamera = Camera.main;

}

// Update is called once per frame

void Update () {

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

Vector3 moveVelocity = moveInput.normalized * moveSpeed;

controller.Move (moveVelocity);


Ray ray = viewCamera.ScreenPointToRay (Input.mousePosition);

Plane groundPlane = new Plane (Vector3.up, Vector3.zero);

float rayDistance;


if(groundPlane.Raycast(ray, out rayDistance)){

Vector3 point = ray.GetPoint (rayDistance);

//Debug.DrawLine (ray.origin, point, Color.red);

controller.LookAt(point);

}

}

}

위 스크립트에서 새롭게 배운것은 RequireComponent 이다.

설명은 주석으로 대신한다.

기획을 잘해놓고 RequireComponent 을 잘 사용한다면 아주 편하게 사용할수 있을거 같다.


그리고 수업이 끝난후에도 이해가 정확히는 되지 않는 부분이


Ray ray = viewCamera.ScreenPointToRay (Input.mousePosition);

카메라에서부터 마우스의 위치를 향해서 빛을 쏜다.


Plane groundPlane = new Plane (Vector3.up, Vector3.zero);

float rayDistance;


if(groundPlane.Raycast(ray, out rayDistance)){

Vector3 point = ray.GetPoint (rayDistance);

//Debug.DrawLine (ray.origin, point, Color.red);

controller.LookAt(point);

}








PlayerController.cs

using UnityEngine;

using System.Collections;


[RequireComponent (typeof (Rigidbody))]

public class PlayerController : MonoBehaviour {


Vector3 velocity;

Rigidbody myRigidBody;


// Use this for initialization

void Start () {

myRigidBody = GetComponent<Rigidbody> ();

}

// Update is called once per frame

void FixedUpdate () {

myRigidBody.MovePosition (myRigidBody.position + velocity * Time.deltaTime);

}


public void LookAt(Vector3 lookPoint){

Vector3 heightCorrentedPoint = new Vector3 (lookPoint.x, transform.position.y, lookPoint.z);

transform.LookAt(heightCorrentedPoint);

}



public void Move(Vector3 _velocity){

velocity = _velocity;

}

}



스크립트들의 구조에 대해서 이야기 해보자.
이건 프로그래밍이라기 보다는 기획쪽이라고 생각하는데, 정말 너무나도 중요하다고 생각한다.
현재는 플레이어와 플레이어컨트롤러를 만들었다.
그리고 이위에 총과 총컨트롤러 그리고 총알을 만들것이다.


플레이어에는 플레이어컨트롤러와 총 컨트롤러를 포함한다.

예전에 DB기획하거나 클래스 정의 할때 배웠던 것과 일맥상통한다.

객체를 정의하는것처럼 의미가 있고 관계가 있는것들은 서로간의 구조를 가지고 있어야 한다.


총도 플레이어의 움직임도 결국 플레이어의 밑에 존재하게 된다.

결국 플레이어는 입력을 받으면 그 입력을 각각의 컨트롤러에게 전달하는 역활을 한다.





3. 총을 만들자

위에서 플레이어를 만들었다.

우리는 총쌈겜을 만들어야 하니, 무엇을 만들어야 할까?

바로 총이다.


총은 전에 지미의 강의에서 했던것을 생각하면 어렵지 않게 할수있다.


Gun.cs

using UnityEngine;

using System.Collections;


public class Gun : MonoBehaviour {

    public Transform muzzle;

    public Bullet projectile;

    public float msBetweenShots = 1000;

    public float muzzleVelocity = 35;


    float nextShotTime;


    public void Shoot()

    {

        if (Time.time > nextShotTime)

        {

            nextShotTime = Time.time + msBetweenShots / 1000;

            Bullet newProjectile = Instantiate(projectile, muzzle.position, muzzle.rotation) as Bullet;

            newProjectile.SetSpeed(muzzleVelocity);

        }

        

    }

}

위의 소스는 총 스크립트이다.

머즐은 총입구라는 뜻으로 퍼블릭으로 만들어놓고, 씬에서 드래그해 놓는다.

msBetweenShots는 변수명 그대로 연사속도를 의미한다.

muzzleVelocity은 총알 스피드를 의미한다.

결국 이 총 스크립트는 컨트롤러에게 불려서 사용될것이다.

(prefab으로 만든 총에 이 스크립트를 드레그해놓아야 한다. 이거 안해서 또 한 30분 헤맸다.)



GunController.cs

using UnityEngine;

using System.Collections;


public class GunController : MonoBehaviour {


public Transform weaponHold;

public Gun startingGun;

Gun equipedGun;


  //시작총이 널이 아니면, 시작총을 장착한다.

void Start(){

if(startingGun != null){

EquipGun (startingGun);

}

}


public void EquipGun(Gun gunToEquip){

if(equipedGun != null){

Destroy (equipedGun.gameObject);

}

equipedGun = Instantiate (gunToEquip, weaponHold.position, weaponHold.rotation) as Gun;

equipedGun.transform.parent = weaponHold;

}


    public void Shoot()

    {

        if (equipedGun != null)

        {

            equipedGun.Shoot();

        }

    }

}


weaponHold는 플레이어 바로 앞의 위치정보이다.

startingGun은 미리만들어 놓은 prefab총을 넣어놓는다.

equipedGun는 현재 장착된 총.

위에서 언급했지만, 건 컨트롤러는 결국 플레이어에게 불려질것이다.



그리고 총을 발사하기 위해서 플레이어에 다음을 추가한다.


//Weapon Input

if (Input.GetMouseButton(0)){

gunController.Shoot();

}

마우스 버튼을 누르면 총을 발사한다.

하지만! 총을 발사는 하지만, 총알이 없으니까 아무것도 나가지 않을것이다.

그럼 총알을 한번 만들어 보자.


bullet.cs

using UnityEngine;

using System.Collections;


public class Bullet : MonoBehaviour {


    public LayerMask collisionMask;

    float speed = 10;


    public void SetSpeed(float newSpeed)

    {

        speed = newSpeed;

    }


// Update is called once per frame

void Update () {

        float moveDistance = speed * Time.deltaTime;

        checkCollisions(moveDistance);

        transform.Translate(Vector3.forward * Time.deltaTime * speed);

}


    void checkCollisions(float moveDistance)

    {

        Ray ray = new Ray(transform.position, transform.forward);

        RaycastHit hit;


        if (Physics.Raycast(ray, out hit, moveDistance, collisionMask, QueryTriggerInteraction.Collide))

        {

            OnHitObject(hit);

        }

    }


    void OnHitObject(RaycastHit hit)

    {

        GameObject.Destroy(gameObject);

    }


}


총알에서 처음배운 것은 public LayerMask collisionMask이다.

그냥 레이어라고 생각하면 된다. 이것은 raycast에서 사용될것인데, 충돌체크를 감시할때 

같은 레이어에 있는 것들만 체크하게 할수 있다.


다음을 보자.

Physics.Raycast(ray, out hit, moveDistance, collisionMask, QueryTriggerInteraction.Collide)

레이의 방향을 앞이고, 이동거리는 시간당 속도로 한다.

그리고 같은 레이어에 있는 것들과 충돌체크를 한다.

ontriggerEnter보다 이것을 쓰는 이유는 밑에 팁에 설명해 놓았다.




4. 적을 만들어 보자


이제 총도 쏠수 있게 되었다. 

그럼 적을 한번 만들어 보자.

걍 캡슐 하나를 만들고 색깔을 입힌다.

적이 해야할 행동은 무엇인가?

바로 플레이어를 쫓아 오는 것이다.

이것은 특별히 코딩으로 처리 하지않고, 유니티의 navigation의 nav mesh agent를 이용한다.


우선 바닥객체(plane)을 선택하고 navigation 창을 열고

navigation static을 체크한다.  그리고 navigation area를 walkable로 두고 아래의 bake버튼을 누르면

바닥에 파란 영역이 생긴다.


이영역이 즉 이동할수 있는 영역으로 자리 잡는다.


그리고 방해물로 큐브를 하나 만들고, 똑같이 navigation 창에서 not walkable로 선택후 bake를 다시하면

파란 영역에서 그 방행물 근처가 지워진다.


그리고 다음과 같은 적 스크립트를 만든다.


using UnityEngine;

using System.Collections;


[RequireComponent (typeof(NavMeshAgent))]

public class Enemy : MonoBehaviour {


    NavMeshAgent pathfinder;

    Transform target;


// Use this for initialization

void Start () {

        pathfinder = GetComponent<NavMeshAgent>();

        target = GameObject.FindGameObjectWithTag("Player").transform;


        StartCoroutine(UpdatePath());

}

// Update is called once per frame

void Update () {

        

}


    IEnumerator UpdatePath()

    {

        float refreshRate = .25f;

        while (target != null)

        {

            Vector3 targetPosition = new Vector3(target.position.x, 0, target.position.z);

            pathfinder.SetDestination(target.position);

            yield return new WaitForSeconds(refreshRate);

        }

    }

}


이 스크립트에서 중요한것은 새로운 NavMeshAgent도 있지만,

UpdatePath()함수라고 할수 있다.

pathfinder.SetDestination(target.position); 이 라인이 결국 네비게이션 영역에서 타겟을 찾는 것인데,

흔히 말해서 코스트가 높은 연산이다.

그러므로, 매 업데이트마다 실행하는 것이 아니라.

정해진 시간마다 업데이트하게 만들면 코스트를 절약할수 있기때문에, 

다음과 같은 waitForSeconds와 while문을 사용해서 만들었다.








Tip. OnTriggerEnter 를 레이캐스트 대신에 사용하지 않는 이유가 있나?

 발사체의 속도가 매우 빠르기에 OnTriggerEnter가 호출되지 않을 수 있다.

전에는 언급했듯이 프레임사이에 이미 객체를 넘어가버리면 충돌되지 않는 것으로 처리하기때문에

하지만 레이캐스팅으로 발사체의 속도에 상관없이 충돌 감지를 할 수 있다.



Tip. 방향이던 색깔조정이던 부드럽게 넘어가기


color.lerp(color.white, color.clear, Time.time);

transform.position = Vector3(Mathf.Lerp(minimum, maximum, Time.time), 0, 0);


Tip. 무료이미지

www.kenney.nl/ 강추!



Tip. 애셋관리

이미지나, 사운드등의 애셋은 탐색기에서 유니티에 드래그해서 그냥 옮기지 말자.

스프라이트정보는 없어지고 단순이미지 정보만 이동할경우가 있기때문에, 버그의 소지가 될수 있다.