게임 엔진/Unity

Unity 11일차 - 3D게임-2(3D게임 스크립트, 파티클, 스크린 좌표)

FakeZero 2022. 2. 8. 03:34

  저번 시간에 만들던 것을 완성시켜 봅시다.


6. 표적과 발사체

  배경을 만들었으니 이제 본격적으로 게임을 만들어봅시다. 우선은 표적을 만들어봅시다.

  target 에셋을 Scene 창에 배치하고 0, 0, 10 위치로 옮겨 줍시다.

 

  표적에는 충돌의 판정이 필요하므로 콜라이더를 쓰도록 하겠습니다.

  Box Collider 컴포넌트를 추가해주고 위와 같이 설정해줍시다. 명심하세요. 2D가 붙지 않은 Box Collider 컴포넌트를 추가해야 합니다. 2D 게임이 아니기 때문입니다.

 

  표적을 배치했으니 표적을 맞출 발사체도 추가합시다.

  bamsongi 에셋을 Scene 창에 배치하고 0, 5, -9의 위치에 배치하도록 합시다.

 

  충돌 판정을 위해선 표적과 발사체 어느 한쪽에는 Rigidbody 컴포넌트가 필요하다는 것을 기억하실 것입니다. 또한 Rigidbody 컴포넌트가 적용된 오브젝트에게는 물리엔진이 적용된다는 것도 기억하실 것입니다. 그렇기 때문에 자연히 Rigidbody 컴포넌트가 들어가야할 오브젝트는 발사체 오브젝트입니다.

  bamsongi 오브젝트에 Rigidbody 컴포넌트(이것도 2D가 붙지 않은 컴포넌트여야 합니다.)와 Sphere Collider 컴포넌트를 추가하도록 합시다. 구형 콜라이더의 경우 지름이 너무 크므로 Radius를 0.35으로 내려주도록 합시다.

 

  이제 밤송이는 중력의 영향을 받아 게임을 실행하면 땅으로 낙하할 것입니다. 한 번 실행해봐서 확인해봅시다.

 

  밤송이는 발사체이므로 발사체를 날릴 스크립트를 작성하도록 합시다. 새로운 C# 스크립트를 만들어서 BamsongiController라는 이름으로 바꿔주겠습니다. 코드는 아래와 같이 짭니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BamsongiController : MonoBehaviour
{
    public void Shoot(Vector3 dir)
    {
        GetComponent<Rigidbody>().AddForce(dir);
    }

    void Start()
    {
        Shoot(new Vector3(0, 200, 2000));    
    }
}

  일단은 쏘기만 하므로 Update 메서드를 없애고 스크립트의 확장성을 고려해 Shoot이라는 새로운 메서드를 만들었습니다. y축으로도 200만큼 힘을 주었는데 이것은 밤송이가 날아가자마자 중력의 힘으로 바닥에 떨어지는 것을 방지하기 위함입니다.

 

  이제 스크립트를 적용하고 확인해봅시다.

 

  시작하자마자 밤송이가 날아가는 것을 볼 수 있습니다.

 

  밤송이는 원래 표적에 맞고 나서 떨어지는 것이 아니라 그대로 박혀 있어야 합니다. 원리는 간단합니다. 굳이 물리적인 연산을 할 것이 아니라 충돌하는 순간 밤송이가 물리 엔진을 무시하게 만들면 됩니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BamsongiController : MonoBehaviour
{
    public void Shoot(Vector3 dir)
    {
        GetComponent<Rigidbody>().AddForce(dir);
    }

    void OnCollisionEnter(Collision other)
    {
        GetComponent<Rigidbody>().isKinematic = true;
    }

    void Start()
    {
        Shoot(new Vector3(0, 200, 2000));
    }
}

(12~15번 줄이 추가되었습니다.)

isKinematic 키워드를 이용하면 Rigidbody의 Is Kinematic 설정을 컨트롤 할 수 있습니다.

  isKinematice 키워드를 이용해서 밤송이가 물체에 닿는 순간 물리 엔진의 영향을 안받게 만들었습니다.

 

  이제 밤송이가 표적에 닿아도 떨어지지 않습니다.


7. 파티클

  게임에 있어서 이펙트란 눈의 즐거움을 늘려주는 좋은 장치 중 하나입니다. 그 이펙트의 종류 중 가장 널리, 그리고 자주 쓰이는 것이 바로 파티클입니다.

 

  파티클을 게임에 대량의 입자를 발사해 입자의 움직임, 색, 크기를 제어해서 물, 연기, 불꽃 등 다양한 표현을 만들 수 있습니다. 때문에 파티클의 미세한 조정이 해당 파티클이 어떤 효과를 주는지 결정합니다. 다르게 말하면 아주 조금의 변화에도 많은 것이 변화했다고 느낄 수 있는 아주 세심한 주의를 필요로 하는 이펙트이기도 합니다.

 

  파티클 오브젝트를 바로 생성해도 되겠지만 오늘은 밤송이 오브젝트 자체에 파티클을 붙여 봅시다. 파티클도 컴포넌트에 의해 제어되기 때문에 가능합니다.

  Particle System 컴포넌트를 추가하면 바로 분홍색 정사각형의 입자들이 분출되기 시작합니다. 저것이 파티클이고 저것을 조정하므로써 멋진 이펙트를 만들어 낼 수 있습니다.

 

  세세한 조정이 필요한 컴포넌트인 만큼 변수의 숫자도 다른 컴포넌트와는 차원을 달리 합니다. 저 많은 수의 탭을 전부 설명할 수는 없지만 자주 쓰이는 변수에 대해서만 설명드리도록 하겠습니다.

이름 설명 이름 설명
Duration 파티클 방출 시간 Start Color 파티클 초기 색
Looping 파티클 무한 방출 여부 Gravity Modifier 파티클에 가하는 중력
Start Delay 파티클을 방출하기
시작하는 데 걸리는 시간
Max Particles 최대 파티클 수
Start Lifetime 파티클 표시 시간(수명) Emission탭의 Rate over Time 시간 단위당 생성되는
파티클 수
Start Speed 파티클 방출 속도 Emission탭의 Bursts 지정 시간에
생성되는 파티클 수
Start Size 파티클 크기 Shape탭의 Shape 파티클 방출 형태

 

  파티클의 변수들을 알아보았으니 이제 파티클을 다뤄봅시다. 일단 저 분홍색 직사각형부터 해봅시다. 게임에서 분홍색의 무언가는 머테리얼을 불러오지 못한다는 뜻으로 많이 쓰입니다. 따라서 파티클의 머테리얼을 정해줘 봅시다.

  위와 같이 머테리얼을 선택하면 더 이상 분홍색 정사각형이 아니라 선택한 머테리얼이 나오게 됩니다.

  이제 다른 요소들을 건들여 봅시다. 일단은 방출 형태부터 건들이겠습니다.

  Shape를 Corn에서 Sphere로 바꿔줍시다. Corn형태의 방출 형태는 무언가를 분사한다는 느낌이 강하게 듭니다. 분사형의 파티클을 이번 게임에선 맞지 않습니다. 구형으로 바꿔주겠습니다. 반지름(Radius)도 0.01로 바꿔주겠습니다.

 

  다음은 생성 패턴을 조정해 봅시다. 지금은 계속해서 나오고 있지만 이번 이펙트는 간헐적으로 나와야 합니다. 따라서 간헐적으로 나올 수 있도록 바꿔야 합니다.

  Emission 탭의 Rate over Time을 0으로 바꾸고 Bursts 표의 +를 누르고 위와 같이 설정해 줍시다. 이렇게 설정하면 5초에 한 번씩 파티클이 한 번에 분사되게 됩니다.

 

  이제 확 퍼지는 이펙트가 되었지만 파티클이 사라질 때까지 시간이 너무 오래 걸리는 것이 문제입니다.

  Duration과 Start Lifetime을 1로 바꾸면 좀 더 빠르게 파티클이 재생됩니다.

 

  파티클이 사라질 때 한 번에 확 사라집니다. 좀 더 부드럽게 사라지도록 연출하겠습니다. 그러기 위해선 파티클의 크기를 서서히 줄이거나 투명도를 천천히 내리면 되죠. 여기선 서서히 크기를 줄여보도록 합시다.

  이렇게 하면 파티클이 점점 작아지면서 사라지는 것을 볼 수 있을 것입니다.

 

  상당히 괜찮은 이펙트가 되었죠? 이펙트가 완성이 되었으니 이제 재생 여부에 관한 설정과 스크립트를 짜 봅시다.

  이 이펙트는 반복 재생하지 않으므로 Looping을 해제하고 오브젝트가 나타났을 때 재생하는 것이 아니라 과녁에 맞았을 때 재생되어야 하므로 Play On Awake도 해제해 줍시다.

 

  이제 밤송이가 타켓에 맞았을 때 재생 될 수 있도록 코드를 짜 줍시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BamsongiController : MonoBehaviour
{
    public void Shoot(Vector3 dir)
    {
        GetComponent<Rigidbody>().AddForce(dir);
    }

    void OnCollisionEnter(Collision other)
    {
        GetComponent<Rigidbody>().isKinematic = true;
        GetComponent<ParticleSystem>().Play();
    }

    void Start()
    {
        Shoot(new Vector3(0, 200, 2000));
    }
}

(15번 줄이 추가되었습니다.)

  언제부턴가 딱히 자세하게 새로운 코드를 설명하지 않고 있습니다. 그럼에도 만약 여러분들이 이것이 어떤 코드인지 알게되었다면 여러분들은 Unity가 어떤 구조로 프로그램 되어있고 어떤 구조로 컴포넌트가 구성되어 있는지 이해할 수 있게 된 것입니다.

 

  이번에도 간단하게만 설명하겠습니다. 표적에 밤송이가 맞았을 때, 파티클이 재생되도록 해 주었습니다.

 

  이렇게 만들어지면 잘 된 것입니다.

 

  파티클을 다뤄보니 어떻습니까? 세세한 곳을 전부 조정해야해서 귀찮지 않았습니까? 또는 무엇이 변화했는지 잘 모르겠습니까? 파티클은 세세한 조정이 핵심인 시스템입니다.

 

  파티클의 힘은 아직 이런 것이 아닙니다. 한 입자가 스프라이트 애니메이션처럼 재생되며 나오는 것도 가능하고 굳이 퍼져나가는 것이 아니라 재생된 장소에서만 머무는 파티클도 있습니다.

 

  필자도 게임을 하거나 만들면서 '이 효과가 파티클이었어?'라고 생각하게 되는 파티클 효과들이 매우 많이 있었습니다. 그만큼 파티클은 잘 사용하면 상상을 초월하는 이펙트들을 만들 수 있습니다. 유니티의 에셋 스토어에 무료로 배포되고 있는 고퀄리티의 파티클들이 많이 있습니다. 한 번 자신의 프로젝트에 임포트 해 보시고 여러가지 시험해 보시길 바랍니다. 파티클은 연구해 두어서 게임 개발에 나쁠 일은 절대 없습니다. 단언컨데 파티클은 현재 우리가 만들 수 있는 가장 간단하고 효과적이며 효율적인 이펙트 중 하나입니다. 꼭 혼자서라도 다양한 기능을 연구하시길 바랍니다.


8. 스크린 좌표

  자, 이제 발사될 밤송이가 준비되었습니다. 이걸 여러발 발사해야하니 bamsongiPrefab이란 이름의 프리팹으로 만들어 줍니다....라고 하려고 했더니 bamsongi 오브젝트는 이미 프리팹 상태가 되어있습니다. 이것은 fbx파일은 오브젝트로 불러낼때에 이미 프리팹으로서 불려나오기 때문입니다.

 

  그 이유는 다름아닌 이전에 설명했던 3D 모델의 3요소 때문입니다. 3D모델, 머트리얼, 쉐이더(머테리얼에 포함). 이 3요소가 전부 포함되어 3D 오브젝트가 되어는 것입니다.

 

  어쨌든 중요한 것은 bamsongi 오브젝트가 이미 프리팹이라는 것입니다. 그렇기 때문에 유니티는 다음과 같이 물어봅니다.

    Prefab Variant는 프리팹으로 만드려는 오브젝트까지 생성된 프리팹으로 바꿔버립니다. 우리는 새로운 프리팹을 만들려는 것이므로 Original Prefab을 선택하도록 합시다. 프리팹을 제대로 만들었다면 원래의 bamsongi 오브젝트는 필요 없으니 지워줍시다.

 

  이제 화면을 클릭하면 밤송이가 만들어지는 스크립트를 만들어봅시다. BamsongiGenerator라는 새로운 C# 스크립트를 만들어 주고 다음과 같이 코딩합시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BamsongiGenerator : MonoBehaviour
{
    public GameObject bamsongiPrefab;

    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            GameObject bamsongi = Instantiate(bamsongiPrefab) as GameObject;
            bamsongi.GetComponent<BamsongiController>().Shoot(new Vector3(0, 200, 2000));
        }
    }
}

  bamsongiPrefab이란 변수는 그저 이름뿐이고 아직 진짜 우리가 만든 프리팹을 말하는 것이 아니니 public을 이용해서 나중에 게임 엔진쪽에서 bamsongiPrefab 변수에 프리팹을 넣을 수 있게 해줍시다.

  Update 메서드에서는 마우스 클릭이 감지되면 밤송이 프리팹의 인스턴스를 반환하고 해당 인스턴스에 적용되어있는 스크립트 컴포넌트 중 BamsongiController에서 Shoot 메서드를 불러와서 실행합니다. 즉, 생성된 밤송이에 힘을 가하는 코드가 BamsongiGenerator 스크립트에서 실행되었단 뜻입니다.

  (이해가 잘 되지 않는다면 7일차 강좌를 참고하도록 합시다.)

 

  따라서(BamsongiController 스크립트를 엽시다.)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BamsongiController : MonoBehaviour
{
    public void Shoot(Vector3 dir)
    {
        GetComponent<Rigidbody>().AddForce(dir);
    }

    void OnCollisionEnter(Collision other)
    {
        GetComponent<Rigidbody>().isKinematic = true;
        GetComponent<ParticleSystem>().Play();
    }

    void Start()
    {
        //Shoot(new Vector3(0, 200, 2000));
    }
}

(20번 줄이 수정되었습니다.)

  BamsongiController 스크립트에서 힘을 가할 필요가 없다는 뜻입니다. 따라서 주석 처리하도록 합시다.

 

  BamsongiController의 Shoot메서드가 public으로 선언되어있는 것을 알 수 있습니다. public으로 선언되어 있었기에 BamsongiGenerator 스크립트에서 불러올 수 있었던 것입니다.

  Hierarchy창에 빈 오브젝트를 만들어서 프리팹을 지속적으로 생성할 공장으로 만들어 줍시다.

  빈 오브젝트(BamsongiGenerator 오브젝트)에 BamsongiGenerator 스크립트를 적용하고 bamsongiPrefab을 할당해 줍시다.

 

  참고로 이런식으로 스크립트에서 선언한 변수에 오브젝트 실체를 대입할 때 사용하는 이 방식을 아웃렛 접속이라고 합니다.

 

  이제 밤송이를 날릴 수는 있게 되었지만 이래서는 같은 위치에서만 발사하기 때문에 거의 같은 결과만 나오게 됩니다. 따라서 스크린의 어딘가를 누르면 해당 위치에서 발사되도록 만들어서 조금 게임답게 만들도록 하겠습니다.

 

  이전에 5일차 강좌에서 마우스가 클릭한 좌표를 알 수 있는 메서드를 쓴 적이 있습니다. 하지만 그것은 2D에서 였습니다. 3D는 차원이 하나 더 추가되었기 때문에 상황이 달라집니다.

 

  마우스가 클릭한 좌표는 스크린 좌표계가 되고 실제 오브젝트가 생성되는 곳은 월드 좌표계입니다. 이전의 2D에서는 스크린 좌표계가 곧 월드 좌표계가 되었지만 3D에서는 상황이 달라집니다. 따라서 스크린 좌표계를 월드 좌표계로 전환하는 기능이 필요합니다.

 

  정말 고맙게도 유니티에는 해당 방법이 준비되어 있습니다. ScreenPointToRay 메서드입니다. 이 메서드는 스크린 좌표를 전달하면 '카메라'에서 '스크린 좌표'로 향하는 월드 좌표계로 벡터를 구할 수 있습니다.

 

  바로 BamsongiGenerator 스크립트를 열어서 써 봅시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BamsongiGenerator : MonoBehaviour
{
    public GameObject bamsongiPrefab;

    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            GameObject bamsongi = Instantiate(bamsongiPrefab) as GameObject;

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            Vector3 worldDir = ray.direction;
            bamsongi.GetComponent<BamsongiController>().Shoot(worldDir.normalized * 2000);
        }
    }
}

(15~17번 줄이 추가되고 변경되었습니다.)

  Ray라는 새로운 클래스가 나타났습니다. 이 클래스는 말 그대로 광선이며 광원의 좌표(origin)와 광선의 방향(direction)을 멤버 변수로 갖습니다. 또한 Ray는 콜라이더가 적용된 오브젝트와 충돌을 감지하는 특징이 있습니다. Ray의 사용 방법은 다음 게임에서 설명할테니 지금은 Ray라는 클래스가 있다는 것을 알아둡시다.

 

  아무튼 이미 게임에 배치되어 있는 Main Camera 오브젝트가 향하는 방향을 direction값으로 받을 수 있습니다. 정확히는 화면을 클릭했을 때, 해당 좌표에서 향하는 방향인 것입니다. 예를 들어 정중앙을 클릭했다면 +Z방향 곧대로 가겠지만 정중앙이 아니라면 정중앙에서 조금 틀어진 방향이 될 것입니다.

 

  Shoot메서드에서 쓰인 normalized 키워드는 벡터가 가진 변수를 단위 벡터로 만듭니다. 따라서 값이 1이 되었으므로 해당 변수에 2000을 곱해 오브젝트에 힘을 가하는 것입니다.

 

  직접 해보았다면 위에서 했던

  [정확히는 화면을 클릭했을 때, 해당 좌표에서 향하는 방향인 것입니다. 예를 들어 정중앙을 클릭했다면 +Z방향 곧대로 가겠지만 정중앙이 아니라면 정중앙에서 조금 틀어진 방향이 될 것입니다.]

  라는 말이 어떤 의미인지 알 수 있을 것이라 생각합니다.

 

  이렇게 해서 이번 게임은 완성이 되었습니다.


  이번 게임은 많이 간단하게 끝났습니다. 다음 게임이 기타무라 미나미 저자의 [유니티 교과서]로 하는 마지막 강좌가 됩니다.

 

  긴 강좌 읽고 따라하시느라 정말 수고하셨습니다.