본문 바로가기

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

유니티(11) F키로 Flash OnOff, 미니맵 만들기, 123으로 총기 교체 만들기

03월29일 수업 진행 내용입니다.

 

먼저 F키를 누르면 flash Light를 On/Off 되게 만들 예정입니다.

 

먼저 Spot Light를 만들어서 FirstPersonCharacter 하위 항목으로 달아주겠습니다..

이렇게 만들면 됩니다.

 

총기쪽으로 position값을 수정해주고 컴포넌트는 아래와 같이 설정합니다.

그리고, 맵 전체에 비추는 빛의 세기를 감소시켜 기본값을 어둡게 해야 플래시라이트를 쓰는 의미가 있겠죠

Directional Light 의 Intensity를 0.3으로 설정해서 어둡게 만들었습니다.

 

게임 씬에서 플래시라이트를 비추는 모습

 

 

FireCtrl 클래스에서변수를 추가합니다.

public Light flashLight;

컴포넌트가 생기는걸 확인, 늘 해오던것과 같이 Spot Light 오브젝트를 끌어다 주겠습니다.

불을 켜고 끌 때 효과음도 넣어 주겠습니다.

오디오 소스는 기존 총기 발사사운드를 적용할 때 컴포넌트에 추가되어 있기 때문에 오디오 클립만 추가합니다.

사운드 관련 변수만 따로 몰아서 헤더로 어트리뷰트 하겠습니다. [SerializeField]처럼 관찰하기위해 사용하는것을 어트리뷰트라고 배웠었죠

 

    [Header("Sound관련변수")] //어트리뷰트
    public AudioSource source;
    public AudioClip fireSound;
    public AudioClip flashSound;

변수 선언 부에 위와 같이 적어 두면 컴포넌트에서 아래와 같이 확인이 됩니다.

Flash Sound에 해당하는 AudioClip도 찾아 넣어주고, 실행문은 아래와 같이 만들어서 Update 문 안에 넣어 주었습니다.

오디오 클립 짤막 설명

if (Input.GetKeyDown(KeyCode.F))
        {
            flashLight.enabled = !flashLight.enabled;
            source.PlayOneShot(flashSound, 0.5f);
        }

F를 누를때마다, Light의 enabled 상태가 반전되게 됩니다.

잘 작동하는 FlashLight 기능

이렇게 플래시라이트 기능을 간단하게 구현해 보았습니다.

 

 


 

 

다음은 미니맵을 만들어 보았습니다.

Project 창에서 03.Image 폴더에 텍스쳐를 하나 생성하겠습니다.

 

위 사진과 같이 선택하면 폴더에 Render Texture 가 생성됩니다. 이름을 아래와 같이 변경 했습니다.

Inspector창에서 따로 건드릴 것은 없었습니다.

 

FPSController 하위 항목으로 Camera를 하나 추가 했습니다. 이름은 아래와 같이 추가 했고요.

카메라의컴포넌트 중에서 AudioListener는 체크 해제합니다.

 

카메라의 컴포넌트 정보입니다. 플레이어의 위에서 내려다 보는 형식으로 Transform 정보는 아래와 같이 설정합니다.

카메라의 Depth값은 카메라를 랜더링 하는 순서입니다.

Depth 값이 작은 카메라는 먼저 렌더링 되며 값이 큰 카메라는 나중에 랜더링 됩니다.

더 나중에 렌더링 될 수록 겹쳤을때 플레이어에게 보여지는 화면이 됩니다. 샌드위치 아래부분이라고 생각하면 될것 같네요.

카메라의 기본 Depth 값은 0인데, 지금FirstPersonCharacter 오브젝트 자체가 Camera 컴포넌트를 가지고 있거든요.

Depth 값으로 0을 가지고 있습니다.

지금 추가된 카메라가  같은 Depth 값을 가지고 있는 것은 의도된 결과가 아닙니다. 지금 미니맵 카메라와 플레이어 시점 화면이 겹쳤을 때는 시점이 우선적으로 출력되게 해야죠

따라서 카메라 Depth값을 -1 로 변경해서 먼저 렌더링 하도록 해 주었습니다.

Target Texture는 방금 생성한 Render Texture 를 설정 해 주었습니다.

 

 

미니맵을 올려놓을 Panel을 생성 합니다. 저번시간과 동일하게 Shift + Alt 활용해서 Middle Left로 맞춰주고 미니맵이 자리할 장소를 만들어 주었어요 이때는 정확한 위치는 중요하지 않아요 두고 싶은 곳이 미니맵의 위치가 되는거예요.

 

 

다음에는 Panel의 하위 항목으로 RawImage를 생성 해 주었습니다.

 

 

Raw Image의 Texture 로 처음 생성했던 Render Texture 이미지를 넣었습니다.

 

미니맵이 잘 작동하는것을 볼 수 있습니다.

 

다음으로 플레이어의 위치를 표시하는 기능을 넣겠습니다.

Raw Image 하위 항목으로 Panel을 또 생성합니다.

 

컴포넌트 설정을 아래와 같이 설정해주면 빨간 점이 됩니다.

미니맵의 가운데에 빨간점이 생성되고 그게 곧 플레이어의 위치가 됩니다.

스크립트를 하나 생성해서 빨간점을 점멸되도록 하였습니다.

방금 생성한 Panel에 추가 했습니다.

 

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

public class ImageOnOff : MonoBehaviour
{
    Image this_Image;
    float timePrev;
    void Start()
    {
        this_Image = GetComponent<Image>();
        timePrev = Time.time;
    }

    // Update is called once per frame
    void Update()
    {
        if(Time.time - timePrev>0.5f)
        {
            this_Image.enabled = !this_Image.enabled;
            timePrev = Time.time;
        }
    }
}

0.5초 간격으로 점멸되는 미니맵이 완성되었습니다.

 

 


 

다음은 무기교체기능을 만들어 보았습니다.

 

무기 교체를 사용하기 위해 아래 에셋을 Import 하였습니다.

 

Modern Weapons Pack | 3D Guns | Unity Asset Store

 

Modern Weapons Pack | 3D 총기 | Unity Asset Store

Elevate your workflow with the Modern Weapons Pack asset from 7XF Design. Find this & other 총기 options on the Unity Asset Store.

assetstore.unity.com

FBX 파일 프리팹을 총기모델 하위항목에 넣어 주었습니다.

 

FBX파일을 뜯어보면 몸통, 탄창, 조준경 오브젝트로 나누어져 있네요

각 오브젝트들은 Transform, Mesh Filter, Mesh Renderer, Material을 가지고 있는것을 볼 수 있었습니다.(아래 사진)

모델의 Position값을 변경해서 SPAS-12모델을 들고있는것과 동일하게 파지한 모양을 만들어 줍니다.

겹치게 만들고(배경 Directional Light가 어두워서 잘 안보임..)

 

SPAS12의 Skinned Mesh Renderer 를 체크 해제 하면

AK-47만 남아서 보이게 해 주었습니다!

소총을 조준한 자세가 얼추 완성 되었습니다.

M4A1모델도 동일한 방법으로 자세를 만들어 주었습니다.

모델 크기가 작은것 같아서  조금 키워 주었네요

 

 

 

 

FPSController 오브젝트로 이동합니다.

 

 

HandAni 클래스에 총을 변경하는 스크립트를 추가 해 보겠습니다.

 

    public SkinnedMeshRenderer SPAS12;	//SPAS12가 가지고 있는 메쉬 렌더러
    public MeshRenderer[] Ak47;	//AK47의 메쉬렌더러 오브젝트가 3개라서 배열로 만듬
    public MeshRenderer[] M4A1;	// 위와 동일, 배열로 선언 함 배열 구성원 모두 메쉬렌더러를 가지고 있음
    public static bool IsHaveM4A1 = false;	//M4A1을 들고 있을때를 구분하기 위해 만들어진 bool자료형의 변수

 

클래스 변수로 4가지 변수를 추가 했습니다. 

컴포넌트로 가보면 배열구성원이 비어있습니다.

 

구성원을 채워 주었습니다.

 

 

업데이트 문에 아래 스크립트를 추가했습니다.

123번 키를 누르면 draw 애니메이션과 함께 총을 변경하게 됩니다.

 

 

 

 

점점 Update 함수에 if문이 바글바글하게 쌓여가는데, 이게 별로 좋아보이지는 않네요.

매프레임 마다 이거 조건 맞니? 안맞니? 계속 확인하고 있으니까요. 자원낭비가 되겠네요.

 

HandAni 클래스 중 Update함수 내

        if(Input.GetKeyDown(KeyCode.Alpha1))    //숫자 1을 누른다면
        {
            IsHaveM4A1 = false; //M4A1 장착 여부 반환            
            animation.Play("draw"); //총 변경 애니메이션 출력
            for(int i =0; i<Ak47.Length; i++)
            {
                Ak47[i].enabled = false;    //반복문으로 AK-47 모델의 Mesh Renderer 전부 OFF
            }            
            for (int i = 0; i < M4A1.Length; i++)   //반복문으로 M4A1 모델의 Mesh Renderer 전부 OFF
            {
                M4A1[i].enabled = false;
            }
            SPAS12.enabled = true;  //SPAS12 ON
        }
        else if(Input.GetKeyDown(KeyCode.Alpha2))   //숫자 2를 누른다면
        {
            IsHaveM4A1 = false; //M4A1 장착 여부 반환
            animation.Play("draw"); //총 변경 애니메이션 출력
            for (int i = 0; i < Ak47.Length; i++)
            {
                Ak47[i].enabled = true; //반복문으로 AK-47 모델의 Mesh Renderer 전부 ON
            }
            for (int i = 0; i < M4A1.Length; i++)
            {
                M4A1[i].enabled = false;    //반복문으로 M4A1 모델의 Mesh Renderer 전부 OFF
            }
            SPAS12.enabled = false; //SPAS12 OFF
        }
        else if(Input.GetKeyDown(KeyCode.Alpha3))
        {
            IsHaveM4A1 = true;
            Debug.Log(IsHaveM4A1);
            animation.Play("draw"); //총 변경 애니메이션 출력
            for (int i = 0; i < Ak47.Length; i++)   //반복문으로 AK47 모델의 Mesh Renderer 전부 OFF
            {
                Ak47[i].enabled = false;
            }
            for (int i = 0; i < M4A1.Length; i++)   //반복문으로 M4A1 모델의 Mesh Renderer 전부 ON
            {
                M4A1[i].enabled = true;
            }
            SPAS12.enabled = false; //SPAS12 OFF
        }

총기 변경까지 구현했습니다.

 

 

 

위에서 총기 변경을 구현 할 때, M4A1 을 구분하기 위해 bool 함수를 추가 했었죠 그 변수를 활용해서

 

M4A1총기를 사용 할 때 점사가 발사되도록 구현 해 보았습니다.

 

(FireCtrl 클래스 중 Update 함수 내)

        if (Input.GetMouseButtonDown(0))
        {
            if (handAni.isRun == false)
                Fire();
            if (handAni.isRun == false && HandAni.IsHaveM4A1 == true)
            {
                Debug.Log("발사엠포");
                //StartCoroutine 함수의 처음 사용
                StartCoroutine(M4A1Shot());
            }
        }

 

StartCoroutine(M4A1Shot())

StartCoroutine은 오늘 처음 배운 함수 인데요  코루틴을 실행하기위해 사용되는 함수라고 합니다.

 

메뉴얼은 링크에 준비 되어있는데

코루틴 - Unity 매뉴얼 (unity3d.com)

 

코루틴 - Unity 매뉴얼

코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있습니다. Unity에서 코루틴은 실행을 일시 정지하고 제어를 Unity에 반환하지만 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드입니

docs.unity3d.com

기본적으로 번역체이기도 하고 배경지식이 없는 상태로는 메뉴얼도 이해하기가 벅차네요.

 

 

루틴이라는 말은 일상 생활에서 많이들 들어 보셨을거라고 생각 합니다. 운동을 할때, 아침에 일어날때, 어떤 훈련이 반복적으로 필요 할 때, 루틴이라는 말을 사용하는것 같네요.

 

반복된 움직임이 필요 할 때, 일반적으로 Update 함수안에 움직임을 구현하곤 했었는데, 이렇게 만들면 매 프레임 단위로 원하지 않는 때에도 반복적으로 업데이트문이 실행되게 됩니다.

 

코루틴을 사용하면 필요한 순간에만 StartCoroutine 을 사용해서 루틴을 반복하고, 사용하지 않을 때는 자원을 세이브 할 수 있다고 합니다.

 

메뉴얼에 나와있는 시간 지연 예시를 보니 좀 알것같은데 자세한건 내일 수업시간에 배울 예정입니다.

완전히 정리가 되면 한번더 정리 해보겠습니다.

 

 

 

IEnumerator 함수로, 0.2초당 한발씩 발사되는 점사 방식을 구현 했습니다.

(FireCtrl 클래스 중 함수 추가)

    IEnumerator M4A1Shot()
    {
        Fire();
        muzzleFlash.Stop();
        yield return new WaitForSeconds(0.2f);
        Fire();
        muzzleFlash.Stop();
        yield return new WaitForSeconds(0.2f);
        Fire();
        muzzleFlash.Stop();
    }

 

 

 

 

최종 스크립트 완성본은 접어 두겠습니다.

HandAni 클래스 최종

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HandAni : MonoBehaviour
{
    public Animation animation;
    //뛰는중인지 판단
    public bool isRun=false;
    public SkinnedMeshRenderer SPAS12;
    public MeshRenderer[] Ak47;
    public MeshRenderer[] M4A1;
    public static bool IsHaveM4A1 = 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");

        if(Input.GetKeyDown(KeyCode.Alpha1))    //숫자 1을 누른다면
        {
            IsHaveM4A1 = false; //M4A1 장착 여부 반환            
            animation.Play("draw"); //총 변경 애니메이션 출력
            for(int i =0; i<Ak47.Length; i++)
            {
                Ak47[i].enabled = false;    //반복문으로 AK-47 모델의 Mesh Renderer 전부 OFF
            }            
            for (int i = 0; i < M4A1.Length; i++)   //반복문으로 M4A1 모델의 Mesh Renderer 전부 OFF
            {
                M4A1[i].enabled = false;
            }
            SPAS12.enabled = true;  //SPAS12 ON
        }
        else if(Input.GetKeyDown(KeyCode.Alpha2))   //숫자 2를 누른다면
        {
            IsHaveM4A1 = false; //M4A1 장착 여부 반환
            animation.Play("draw"); //총 변경 애니메이션 출력
            for (int i = 0; i < Ak47.Length; i++)
            {
                Ak47[i].enabled = true; //반복문으로 AK-47 모델의 Mesh Renderer 전부 ON
            }
            for (int i = 0; i < M4A1.Length; i++)
            {
                M4A1[i].enabled = false;    //반복문으로 M4A1 모델의 Mesh Renderer 전부 OFF
            }
            SPAS12.enabled = false; //SPAS12 OFF
        }
        else if(Input.GetKeyDown(KeyCode.Alpha3))
        {
            IsHaveM4A1 = true;
            Debug.Log(IsHaveM4A1);
            animation.Play("draw"); //총 변경 애니메이션 출력
            for (int i = 0; i < Ak47.Length; i++)   //반복문으로 AK47 모델의 Mesh Renderer 전부 OFF
            {
                Ak47[i].enabled = false;
            }
            for (int i = 0; i < M4A1.Length; i++)   //반복문으로 M4A1 모델의 Mesh Renderer 전부 ON
            {
                M4A1[i].enabled = true;
            }
            SPAS12.enabled = false; //SPAS12 OFF
        }
    }
 }

 

FireCtrl클래스 최종

더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class FireCtrl : MonoBehaviour
{
    public Animation animation;
    public ParticleSystem muzzleFlash;
    public GameObject bulletPrefab; //총알 프리팹
    public Transform firePos;//총알 발사 위치
    public ParticleSystem cartrige;
    public Light flashLight;
    [Header("Sound관련변수")] //어트리뷰트
    public AudioSource source;
    public AudioClip fireSound;
    public AudioClip flashSound;


    private float TimePrev;
    private float fireRate = 0.15f;
    private HandAni handAni;


    private void Start()
    {
        //본인 게임오브젝트 컴포넌트의 HandAni에 접근
        handAni = this.gameObject.GetComponent<HandAni>();
        TimePrev = Time.time; // 현재 시를 대입한다.
        muzzleFlash.Stop();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (handAni.isRun == false)
                Fire();
            if (handAni.isRun == false && HandAni.IsHaveM4A1 == true)
            {
                Debug.Log("발사엠포");
                //StartCoroutine 함수의 처음 사용
                StartCoroutine(M4A1Shot());
            }
        }
        #region 연사는 점사로 바꿀 것임, 연사로직



        //else if (Input.GetMouseButton(0)) // 연사
        //{
        //    if (Time.time - TimePrev > fireRate)
        //        현재시간 - 과거시간 = 흘러간 시간이 0.15초를 초과 할 경우
        //    {
        //        if (handAni.isRun == false)
        //            Fire();
        //        TimePrev = Time.time;
        //        Debug.Log("TimePrev 초기화");
        //    }

        //}
        #endregion
        else if (Input.GetMouseButtonUp(0))
        {
            muzzleFlash.Stop();
            cartrige.Stop();
        }
        if (Input.GetKeyDown(KeyCode.F))
        {
            flashLight.enabled = !flashLight.enabled;
            source.PlayOneShot(flashSound, 0.5f);
        }


    }

    private void Fire()
    {
        animation.Play("fire");
        muzzleFlash.Play();
        cartrige.Play();
        Instantiate(bulletPrefab, firePos.position, firePos.rotation);
        //프리팹 생성 함수 Instantiate(무엇What을? 어디Where에? 어떻How rotate게?)
        source.PlayOneShot(fireSound, 0.5f);
    }

    IEnumerator M4A1Shot()
    {
        Fire();
        muzzleFlash.Stop();
        yield return new WaitForSeconds(0.2f);
        Fire();
        muzzleFlash.Stop();
        yield return new WaitForSeconds(0.2f);
        Fire();
        muzzleFlash.Stop();
    }
}