본문 바로가기

게임프로그래밍 개발 수업/유니티 수업

게임 프로그래밍 05 - 유니티 총알 프리팹 만들기, 애니메이션 컨트롤러 맛보기

BulletImpactMetalEffect.prefab
0.11MB

 

 

03월 21일 화요일에 진행한 수업입니다.

 

 

 

 

어제03월20일에 진행 했던 프로젝트는 그대로 이고,

해당 파일만 추가로 필요합니다.

 

어제 UnityTechnologies 폴더에서 MuzzleFlash01 이라는 총기 발사 화염을 찾았었죠

위 파일을 같은 경로 안에 넣어줍니다.

 

오늘 해 볼 것은 총알을 발사하는 것입니다.

 

Hierachy 상에 Bullet이라고 이름 붙인 Sphere 오브젝트를 하나 추가 했습니다.

총알 역할을 해줄 구(Sphere) 오브젝트 입니다.

Trasform > Scale을 통해서 크기를 X,Y,Z 각각 0.03으로 줄여 보았습니다.

선택한 채로, 상단 메뉴바에서 Component > Effect에 보시면 Trail Renderer라는 항목이 있어요.

선택해서 Inspecter 상에 컴포넌트를 추가 해줍니다.

그냥 맨 아래서 Add Component 이후 검색창에 Trail이라고 검색해도 나오더라구요

 

가장 처음에 보이는 Time 컴포넌트는 잔상이 남아있는 시간을 의미합니다.

30 정도로 설정 해두고 Bullet Object의 좌표를 이리 저리 움직여 봅니다.

 

움직임을 주면 갑자기 엄청나게 큰 면이 나오는데요, 

Trail Renderer 를 수정합니다.

 

그래프를 더블클릭하면 변곡점이 생기는데 잘 조절해서 아래와 같이 만들면

 

총알이 날아가는 모양새 같나요?

 

여기 Material을 입혀줄 예정입니다.

03.Images 폴더에 Create Material 해주고, 이름을 Trail이라 짓습니다.

우측 Inspector에서 Shader를 아래와 같이 설정 합니다.

Mobile > Particles > Additive

 

Texture 를 flame이라고 검색해서 선택, 가져와도 되고

Textures_Survival 폴더에서 찾아도 됩니다.

 

방금 만든 'Trail' Material을

Trail Renderer 하위 항목의 Material로 가져갑니다.

Material 적용 후 Gizmos를 사용해서 좌표를 옮겨보면 그럴듯한 총알 잔상을 확인할 수 있습니다.

 

충돌 효과 적용을 위해 'Bullet' Object에 Rigidbody 컴포넌트를 추가하고 Use Gravity 항목은 체크 해제 합니다.

탄도학을 적용하려면 Use Gravity 항목을 사용해야 겠죠

30으로 설정 했던 Time 컴포넌트를 5 정도로 설정하고 나면 잔상이 사라지는 것도 볼 수 있죠,

대충 총알 구실을 할 수 있게 된것 같습니다.

 

 

스크립트를 생성 해보겠습니다.

이름을 BulletCtrl이라고 합니다. 02.Script 폴더에 생성합니다.

using UnityEngine;

public class BulletCtrl : MonoBehaviour
{
    public float speed = 100f;
    public Rigidbody rbody;


    void Start()
    {
        rbody.AddForce(transform.forward * speed, ForceMode.Impulse);
        //Vector3를 사용하면 Global기준으로 적용 되어 항상 같은 방향으로 총알이 나간다.
        //로컬 방향으로 물체의 '앞쪽'으로 인식하는 방향으로 발사 된다.
        //Forcemode.Impulse로 하면 속도가 아주 빨라진다.
       Destroy(this.gameObject, 3.0f);
        //3초후에 소멸 된다. 자기 자신
    }

}

총알은 불러 옴과 동시에 작동하며 짧은 시간 동안 생성 되었다가 사라지기 때문에, Start 함수로 구현하였습니다.

 

주의 하여야 할 점은 총알과 같은 오브젝트에 Vector3를 사용하시면 안된다는 점입니다.

Vector3는 글로벌 좌표계를 사용하고 있기 때문에, 발사 하는 방향이 절대적으로 고정된다면 총구 방향이 아닌 엉뚱한 방향으로 총알이 발사되게 됩니다.

 

Unity에서 Rigidbody.AddForce() 메소드는 물리 기반 게임 오브젝트에 힘을 가하는 데 사용됩니다. 이 메소드는 게임 오브젝트에 연결된 Rigidbody 컴포넌트에 대해 호출되며, 힘을 가해 움직임이나 회전을 일으키는데 사용됩니다.

AddForce(Vector3 force, ForceMode mode = ForceMode.Force);

ForceMode에는 다음 네 가지 옵션이 있습니다:

  1. ForceMode.Force: 힘을 가해지는 오브젝트의 질량을 고려하여 가속도를 적용합니다. 일반적인 경우에 사용됩니다.
  2. ForceMode.Acceleration: 힘을 가해지는 오브젝트의 질량을 무시하고 가속도를 직접 적용합니다.
  3. ForceMode.Impulse: 짧은 시간 동안 큰 힘을 적용하여 가속도를 즉시 변경합니다. 질량을 고려합니다.
  4. ForceMode.VelocityChange: 짧은 시간 동안 큰 힘을 적용하여 가속도를 즉시 변경합니다. 질량을 무시합니다.

 

프리팹을 Hierachy로 옮기고, 스크립트를 적용한 채로 게임을 실행해보면 앞으로 빠르게 나가는 총알을 볼 수 있었습니다.

 

Destroy(GameObject target, float delay);

이 스크립트에서 Destroy 함수는 인수로 자기 자신을 받고 있으며, 3.0f는 3초가 지나면 사라진다는 의미 입니다.
여기서 3.0f는 선택 인수로서, 값을 넣지 않으면 즉시 사라지게 됩니다.
게임 오브젝트를 제거 할 때 사용되는 메소드이며, 유니티가 기본적으로 제공하는 빌트인 툴입니다.

지금은 총알이 그냥 독립적인 개체로 있지만, 나중엔 이게 누가 쏜 총알인지, 데미지는 몇인지 복잡한 계산이 들어가게 될거라고 예상을 해봅니다. 지금은 그냥 모양만 나와도 재밌고 그러네요

 

 

 

총알에 적용되는 이펙트를 매핑 했으며 움직임을 스크립트로 구현하고 게임 씬에서 총알을 직접 날려 보았습니다.
AddForce, Destroy 메소드에 대해 잠깐 알아보았습니다.

 

어렵게 만든 총알이죠, 프리팹으로 만들어 주겠습니다.

07.Prefabs 폴더에 그대로 Drag&Drop해서 프리팹이 되었습니다.

Hierachy 상에서는 Bullet을 지워 주었어요. 발사하는 순간 생성 되어야 하기 때문이죠.

이렇게 씬에 존재하지 않다가 필요한 순간 불러와서 사용하는 것을 동적할당 이라고 합니다.

동적 할당을 하면 훨씬 게임이 가벼워 지겠죠.

 

강사님 말씀 : 프리팹을 사용하는 이유에 2가지가 있다고 합니다.
1. 많은 오브젝트를 한번에 수정할 수 있는점. 유지보수의 편리함.
2. 위에 설명한 오브젝트 동적할당의 유용함. 관리해야할 자원이 훨씬 적어진다.

 


총알 개체를 만들었으니, 발사 애니메이션에 총알을 추가 해볼 예정입니다.

FireCtrl 스크립트로 넘어옵니다.

 

총알을 발사하는 스크립트가 될 예정인데, 일단 총알 발사 위치와 총알 프리팹이 어떤 오브젝트인지 Assign 해줄 예정입니다.

지역 변수 선언 부분에 아래 코드 두줄을 추가합니다.

    public GameObject bulletPrefab; //총알 프리팹
    public Transform firePos;//총알 발사 위치

바로 이전시간에 만든 스크립트를 수정하자면 이렇습니다.

(FireCtrl 의 1차 수정)

using UnityEngine;

public class FireCtrl : MonoBehaviour
{
    public Animation animation; //애니메이션 컴포넌트
    public ParticleSystem muzzleFlash; //파티클 시스템 컴포넌트
    public AudioSource source; // 오디오 소스 컴포넌트그
    public AudioClip fireSound; //오디오 클립 컴포넌트
    public GameObject bulletPrefab; //총알 프리팹
    public Transform firePos;//총알 발사 위치

    private void Start()
    {
        muzzleFlash.Stop();  // 파티클이 그대로 두면 무한 반복 되기 때문에 시작과 동시에 멈춘다.
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0)) // 마우스를 좌클릭하면
        {
            animation.Play("fire"); //발사 애니메이션이 동작하게 된다.
            muzzleFlash.Play(); // 파티클 시스템으로 컴포넌트에 적용한 파티클 효과가 재생된다.
            source.PlayOneShot(fireSound, 0.5f); // 설정한 오디오 소스와 클립이 재생되며, 1.0f의 볼륨 크기를 가진다. (너무 크면 조절 가능)
        }
        else if (Input.GetMouseButtonUp(0)) // 발사 키를 떼면
        {
            muzzleFlash.Stop(); // 파티클 효과가 꺼진다.
        }


    }
}

 

FPSController 에 새로운 컴포넌트가 생겼습니다, 총알 프리팹을 폴더에서 찾아 넣고, 총알의 발사위치는

발사이펙트가 있는 FirePos 오브젝트로 설정합니다.

컴포넌트가 주렁주렁 달렸습니다.

Bullet Prefeb과 Fire Pos를 Assign 해줍니다.

 

위는 발사 효과를 담당하는 스크립트 이므로 발사에 필요한것들을 최대한 떠올려 적용한 것이다.

총을 발사하는데는 무엇이 필요한지 생각해보자

1. 발사 동작에 나오는 애니메이션

2. 파티클 효과(이펙트)

3. 총을 발사할 때 나는 소리(Audio Source)

4. 발사되는 총알

5. 발사되는 위치

이런것들이 위의 컴포넌트 들이며 public 접근 제한자를 가지고 class영역에 선언된 전역변수들입니다.

 

(FireCtrl 의 2차 수정)

using UnityEngine;

public class FireCtrl2 : MonoBehaviour
{
    public Animation animation; //애니메이션 컴포넌트
    public ParticleSystem muzzleFlash; //파티클 시스템 컴포넌트
    public AudioSource source; // 오디오 소스 컴포넌트그
    public AudioClip fireSound; //오디오 클립 컴포넌트
    public GameObject bulletPrefab; //총알 프리팹
    public Transform firePos;//총알 발사 위치

    private void Start()
    {
        muzzleFlash.Stop();  // 파티클이 그대로 두면 무한 반복 되기 때문에 시작과 동시에 멈춘다.
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0)) // 마우스를 좌클릭하면
        {
            animation.Play("fire"); //발사 애니메이션이 동작하게 된다.
            muzzleFlash.Play(); // 파티클 시스템으로 컴포넌트에 적용한 파티클 효과가 재생된다.
            source.PlayOneShot(fireSound, 0.5f); // 설정한 오디오 소스와 클립이 재생되며, 1.0f의 볼륨 크기를 가진다. (너무 크면 조절 가능)
            Instantiate(bulletPrefab, firePos.position, firePos.rotation);
            //프리팹 생성 함수 Instantiate(무엇What을? 어디Where에? 어떻How rotate게?)
        }
        else if (Input.GetMouseButtonUp(0)) // 발사 키를 떼면
        {
            muzzleFlash.Stop(); // 파티클 효과가 꺼진다.
        }


    }
}

Update 메소드의 If 문을 잘 보면 발사 할 때 동작하는 구간이 있습니다.

Instantiate(bulletPrefab, firePos.position, firePos.rotation);

위 문장을 추가 해 주었는데요.

위 문장에서 쓰인 Instantiate 함수의 매개 변수는 다음과 같습니다.

Instantiate(Object original, Vector3 position, Quaternion rotation);

 


메소드를 추출하는 방법

 

위에 완성한 코드를 살짝 수정해서 간단하게 바꿔 보겠습니다.

 

추출할 부분을 길게 블럭 설정 합니다.

이 부분은 발사시에 수행되는 동작으로 묶을 수가 있습니다.

 

 

마우스 우클릭 후 빠른 작업 및 리팩터링

 

메서드 추출 선택

 

 

 

메소드 이름을 Fire로 해주었습니다.

 

이렇게 긴 if문을 조건으로 하여 하나의 동작(메소드)이 완성되었습니다.

긴 스크립트를 줄여서 표현하는 방법이 여기 있었네요

 

잠깐 알아보는 짧은 상식
[ 메소드 ⊂ 함수 ]
함수(function)는 메소드(method)를 포함하는 말입니다. 함수가 더 큰 개념이죠.
가장 큰 차이점은 독립적으로 존재할 수 있는지, 클래스내에서 정의해주어야 사용 가능한지 입니다.
전자가 함수, 후자가 메소드이며

함수는 따로 우리가 추출하거나 선언하지 않아도 사용가능합니다.
예를 들면위에서 강조해서 설명한 Destroy, Instantiate, AddForce 같은 것들이 함수인거죠.
메소드는 우리가 클래스내에서 정의해주어야 함수처럼 동작합니다.

그런데, 많은 사람들이 메소드와 함수를 혼동해서 사용한다고 합니다.
사실 저도 정리해놓고도 말하다 보면 헷갈릴 때가 많은데, 개떡같이 말해도 찰떡같이 알아듣는 사람이 되도록 해요.

 

 


Fire 메소드를 바탕으로 연사기능을 만들어 보도록 합니다.

 

(FireCtrl 의 3차 수정)

using UnityEngine;

public class FireCtrl : MonoBehaviour
{
    public Animation animation; //애니메이션 컴포넌트
    public ParticleSystem muzzleFlash; //파티클 시스템 컴포넌트
    public AudioSource source; // 오디오 소스 컴포넌트그
    public AudioClip fireSound; //오디오 클립 컴포넌트
    public GameObject bulletPrefab; //총알 프리팹
    public Transform firePos;//총알 발사 위치


    private float TimePrev; //이전 시간
    private float fireRate = 0.15f;//발사 간격;

    private void Start()
    {
        muzzleFlash.Stop();  // 파티클이 그대로 두면 무한 반복 되기 때문에 시작과 동시에 멈춘다.
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0)) // 마우스를 좌클릭하면
            Fire();//아래의 Fire 메소드를 실행
        else if(Input.GetMouseButton(0))// 또는 마우스를 꾹 누르고 있을 때면
        {
            if (Time.time - TimePrev > fireRate)//현재시간에서 TimePrev를 초기화 한 시간을 뺐을 때가 fireRate를 초과 하는 경우
            //다시 말하자면 TimePrev를 초기화 한 후로 부터 fireRate 보다 오랜 시간이 경과 한 경우
            {
                Fire();
                TimePrev = Time.time;
                Debug.Log("TimePrev 발사 시간 초기화");
                //여기서 발사하자마자 초기화를 해주면 발사시간으로 부터 fireRate 보다 오래 지나야 발사 가능한 로직이 완성
            }
        }
        else if (Input.GetMouseButtonUp(0)) // 발사 키를 떼면
        {
            muzzleFlash.Stop(); // 파티클 효과가 꺼진다.
        }


    }

    private void Fire()
    {
        animation.Play("fire"); //발사 애니메이션이 동작하게 된다.
        muzzleFlash.Play(); // 파티클 시스템으로 컴포넌트에 적용한 파티클 효과가 재생된다.
        source.PlayOneShot(fireSound, 0.5f); // 설정한 오디오 소스와 클립이 재생되며, 1.0f의 볼륨 크기를 가진다. (너무 크면 조절 가능)
        Instantiate(bulletPrefab, firePos.position, firePos.rotation);
        //프리팹 생성 함수 Instantiate(무엇What을? 어디Where에? 어떻How rotate게?)
    }
}

연사기능이 완성 되었습니다. 테스트 해 봅니다.

연사 기능 테스트


달리는 도중 총기 발사 막기

 

달려가는 동안 총을 발사할 수 없도록 하겠습니다.

그런데 기존 지식으로는 해결할 수 없는 문제가 생겼습니다.

 

저는 지금 발사 관련된 작업을 FireCtrl 이라는 스크립트에서 진행중인데

달리는 도중인지 알려면 Animation을 참조해야만 한다는 말이죠

저번시간에 발사와 달리기, 재장전등을 구현한 HandAni 라는 스크립트를 만든 바가 있었죠

아래는 HandAni 입니다.

using UnityEngine;

public class HandAni : MonoBehaviour
{
    public Animation animation;


    void Start()
    {

    }


    void Update()
    {
        //LShift + W = 달리기
        if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.W))
            animation.Play("running");
        //LShift 떼면 멈추기
        else if (Input.GetKeyUp(KeyCode.LeftShift))
            animation.Play("runStop");
        //마우스 클릭시 발사 애니메이션
        else if (Input.GetKeyDown(KeyCode.Mouse0))
            animation.Play("fire");
        //R누르면 재장전 애니메이션    
        else if (Input.GetKeyDown(KeyCode.R))
            animation.Play("pump3");
    }

}

 

여기 우리가 참조해야할 애니메이션은 Run과 RunStop 입니다.

Run일때는 발사 불가능, RunStop이 되는 타이밍엔 발사가 가능하도록 만들겁니다.

 

이제 여기서 부터 중요합니다.

다른 클래스의 변수를 가져올거예요. 아래는 수정된 HandAni 스크립트 입니다.

 

using UnityEngine;

public class HandAni : MonoBehaviour
{
    public Animation animation;
    //뛰는중인지 판단
    public bool isRun=false;

    void Start()
    {

    }


    void Update()
    {
        if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.W))
        {
            animation.Play("running");
            isRun = true;
        }
        else if (Input.GetKeyUp(KeyCode.LeftShift))
        {
            animation.Play("runStop");
            isRun=false;
        }
            

        else if (Input.GetKeyDown(KeyCode.Mouse0))
            animation.Play("fire");

        else if (Input.GetKeyDown(KeyCode.R))
            animation.Play("pump3");
    }

}

public 접근 제한자를 통해 bool 자료형을 가진 변수를 하나 만들었습니다.

변수 이름은 IsRun 말그대로 뛰는지 안뛰는지 상태를 알려주는 역할을 합니다.

달리고 있으면 true 값을 계속 줘요.

 

 


(FireCtrl 의 4차 수정)

 

using UnityEngine;

public class FireCtrl : MonoBehaviour
{
    public Animation animation; //애니메이션 컴포넌트
    public ParticleSystem muzzleFlash; //파티클 시스템 컴포넌트
    public AudioSource source; // 오디오 소스 컴포넌트그
    public AudioClip fireSound; //오디오 클립 컴포넌트
    public GameObject bulletPrefab; //총알 프리팹
    public Transform firePos;//총알 발사 위치


    private float TimePrev;
    private float fireRate = 0.15f;
    private HandAni handAni;//클래스명을 자료형으로 하여 handAni변수를 가져옴. 선언.


    private void Start()
    {
        handAni = this.gameObject.GetComponent<HandAni>(); //위에서 선언한 변수를 초기화
        TimePrev = Time.time; // 현재 시를 대입한다.
        muzzleFlash.Stop(); 
    }

        void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if(handAni.isRun == false)//handAni에서 isRun변수를 가져 왔습니다.
                Fire();
        }
        else if (Input.GetMouseButton(0)) // 연사
        {
            if(Time.time - TimePrev > fireRate) 
                //지금 시간 - 초기화된 시간값 = 초기화 이후 지금까지의 시간 
            {
                if (handAni.isRun == false)//달리고 있지 않다면 발사.
                    Fire();
                TimePrev = Time.time;
                //시간값 초기화
                Debug.Log("TimePrev 초기화");
            }
            
        }
        else if (Input.GetMouseButtonUp(0))//마우스 버튼 떼면
        {
            muzzleFlash.Stop();//파티클 효과가 꺼진다.
        }
        
        
    }

   private void Fire()
    {
        animation.Play("fire"); //발사 애니메이션이 동작하게 된다.
        muzzleFlash.Play(); // 파티클 시스템으로 컴포넌트에 적용한 파티클 효과가 재생된다.
        source.PlayOneShot(fireSound, 0.5f); // 설정한 오디오 소스와 클립이 재생되며, 1.0f의 볼륨 크기를 가진다. (너무 크면 조절 가능)
        Instantiate(bulletPrefab, firePos.position, firePos.rotation);
        //프리팹 생성 함수 Instantiate(무엇What을? 어디Where에? 어떻How rotate게?)
    }
}

이렇게 FireCtrl 스크립트에서 HandAni의 public 접근제한자 변수를 가져와서 작업 해보았다.

 

 

 

 


Monster.zip
3.08MB
ZomBie.zip
5.90MB

 

 

애니메이션 뜯어보기

 

위의 에셋을 받아 Import 했습니다.

 

Zombie > Z@walk 모델을Hierachy 상에 올리고 zombie라고 이름붙여 줍니다.

 

모델을 가져오고 나서 Rig항목에 Animation Type을 먼저 살펴 보았습니다.

 

4개의 항목이 보입니다.

Legacy는 옛날방식

Generic은 4족보행 방식

Humanoid는 2족 보행 방식입니다.

 

이중 Generic과 Humanoid Type을 합쳐서 메카님 방식(Mecanim Type)이라고 부릅니다.

Humanoid 방식이 조금 특별한데, 모델(캐릭터)이 다르더라도, 애니메이션 파일을 호환하여 사용할 수 있습니다.

 

상단 메뉴에서 Animator 창을 열 수 있습니다. 씬에 존재하는 좀비의 Animator 항목으로 이동해줍니다.

 

Monster > monster_Mecanim 의 하위항목을 보면 Zombie 와는 다른 캐릭터의 애니메이션이 보입니다.

휴머노이드 타입이기 때문에 캐릭터가 다르더라도 적용이 가능한데

이 애니메이션 파일을 Animator로 옮겨줍니다.

 

보고싶은 애니메이션 동작은 

Set as Layer Default State로 기본 동작으로 설정한 후 게임을 플레이하면 

게임씬안에서 움직이는 좀비를 볼 수 있습니다.

 

Monster > Monster > monster_Mecanim 폴더에 가면 애니메이션 파일들이 많이 있습니다.

Animator 창으로 끌어다 아래와 같이 편성합니다.

 

Layer와 Transition을 이렇게 연결 해줍니다.

 

 

 

애니메이션간 전환할 때 딜레이가 발생하게 되는 HasExitTime을 체크 해제 해줍니다.

 

또한 동작을 자연스럽게 만들기 위해 컴포넌트도 아래와 같이 형성합니다.

Loop Time은 동작을 연속해서 하게 해주는 컴포넌트로, die 애니메이션을 제외하고 전부 켜는게 좋겠습니다.

Base Upon은 전부 Original로, YPosition만 Feet로 설정, Bake Into Pose는 전부 체크, 

 

그리고, 애니메이션 동작을 결정하는 Parameter값을몇개 추가 합니다.

 

파라미터값 4개 추가

이름옆의 빈박스는 네모난 모양은 Bool이고, 동그란 모양은Trigger 라는 뜻입니다.

 

오늘 수업은 여기까지 완성하는 것이었네요.


오늘 배운것

Trail Renderer로 총알 오브젝트를 만들었다.

그래프로 총알의 꼬리를 구현 했다.

AddForce 오브젝트에 힘을 가하는 함수

Destroy 오브젝트의 파괴를 구현하는 함수

Instantiate 프리팹을 생성하는 함수(무엇을, 어디에, 어떻게Rotation)

함수에 대해 알게 되었다.

 

중괄호 내의 문장을 메소드 추출하여 한문장으로 요약하는 방법에 대해 알게 되었다.

Public 접근 제한자를 가진 다른 클래스내의 개체를 호출하여 사용할 수 있게 되었다.

메카님 방식 (Mecanim Type) 의 애니메이션