[Unity 공포게임] 1인칭 손전등 라이트 구현

2023. 4. 12. 21:25프로젝트/Unity로 공포게임 개발

 

[Unity 공포게임] 1인칭 손전등 라이트 구현

 

<목차>

1. 손전등 라이트 구현
2. 손전등 ON/OFF 구현
3. 아이템 습득 구현

 


 

1. 손전등 라이트 구현

 

 

주변이 어두워 아직은 답단한 게임에 드디어 손전등을 구현할 차례이다. 지난번 책상 위 램프의 불빛은 Point Light를 사용하였지만 손전등은 Spot Light를 이용해 만들어볼 생각이다.

 

 

(라이팅 관련 게시글)

https://zheldajdajd.tistory.com/11

 

[Unity 공포게임] 맵 제작(2) - 오브젝트 배치 & 라이팅

오브젝트 배치 & 라이팅 ProBuilder로 만든 더미모델 대신에 본격적으로 에셋을 배치해보았다. 3D 작업물을 위한 포트폴리오가 아니기 때문에 맵은 되도록 무료 에셋을 다운받아 제작할 생각이었지

zheldajdajd.tistory.com

 

 

 

 

SpotLight는 위 그림처럼 지정한 포인트에서 원뿔 형태로 빛이 뿜어져 나온다. 때문에 우리가 사용하는 손전등의 불빛 형태와 굉장히 유사하다고 볼 수 있다. 

 

 

 

우선 1인칭 시점에서 라이트를 확인하기 위해 씬뷰와 게임뷰를 나란히 둔다. 그리고 Player를 선택한 상태로 Hierarchy창에서 Light->Spotlight를 하나 만들어준다. 이렇게 만들어진 Spotlight는 플레이어가 있는 위치에 생성되어 플레이어를 비추고 있을 것이다. 

 

 

 

빛이 플레이어가 아닌 정면을 비추도록 로테이션을 조절한 후, 라이트 컴포넌트의 설정을 위와 같이 조정해준다.

 

 

Range 변경
Spot Angle 변경

 

 

Range는 빛의 영향을 받는 거리)를 의미한다. Range를 높게 설정할수록 더욱 먼 거리까지 빛이 나아가게 된다.

Spot Angle은 빛이 닿는 범위를 설정할 수 있으며 값이 높을수록 빛이 닿는 면이 더욱 커지게 된다. 게임뷰를 확인하며 두 옵션의 값을 적절하게 조절해주자. 

 

 

 

이제 Spotlight를 플레이어의 자식 오브젝트로 넣어준다. 

 

 

https://zheldajdajd.tistory.com/14

 

[Unity 공포게임] 1인칭 플레이어 이동, 마우스로 카메라 시점 조작(3) - 마우스로 카메라 시점 조작

1인칭 플레이어 이동, 마우스로 카메라 시점 조작(3) - 마우스로 카메라 시점 조작하기 1. 키보드로 오브젝트 이동하기 1-1. transform.position을 이용하기 1-2. transform.Translate을 이용하기 1-3. Inptut.GetAxis

zheldajdajd.tistory.com

 

 

또, 위 게시글에서 시점의 상하 이동은 플레이어가 아닌 카메라가 회전하도록 구현하였는데 그렇게 하니 플레이어가 위 또는 아래를 바라볼때 손전등의 불빛이 따라서 움직이지 않는다. (좌우 회전시에만 플레이어가 회전하도록 구현했던 이유는 플레이어가 바라보는 방향으로 이동을 시키기 위해서였다.) 그래서 위나 아래를 바라볼때에도 플레이어가 회전할 수 있도록 상하 회전 스크립트를 카메라 스크립트에서 플레이어 스크립트로 이동시켜주었다. 즉, 이제 카메라 스크립트는 지금으로썬 필요가 없게되었다.

 

 


 

2. 손전등 ON/OFF 구현

 

 

게임을 플레이 하다보면 손전등을 꺼야하는 상황도 발생한다. 특히나 공포게임에선 빛에 반응하는 에너미가 자주 등장하는 편이니 손전등의 ON/OFF 기능은 반드시 필요하다고 볼 수 있다.

 

 

앞서 만든 Spotlight(손전등 불빛)에 C#스크립트를 하나 만들어 붙여준다. 스크립트의 이름은 FlashLight로 설정해주었다.

 

 

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


public class FlashLight : MonoBehaviour
{
	bool PlayerGetLight; //true일 경우 손전등on
	Light myLight; //light 컴포넌트를 담는 변수

	void Start()
	{
		PlayerGetLight = false; //초기에는 손전등의 불빛이 꺼진 상태
		myLight = this.GetComponent<Light>(); //오브젝트가 가진 light 컴포넌트를 가져옴.
	}

	void Update()
	{	
	    if (Input.GetKeyDown(KeyCode.R))
	    {
		   PlayerGetLight = PlayerGetLight ? false : true; //r키를 눌러 손전등의 불빛을 on/off
	    }
            
	    if (PlayerGetLight == false)
	    {
		   myLight.intensity = 0; //손전등 off
	    }
        
        
	    if (PlayerGetLight == true)
	    {
		   myLight.intensity = 10; //손전등 on
	    }
        
	}	
}

 

 

 

if문에 사용한 PlayerGetLight=PlayerGetLight?false:true;는 false일 때  true를, true일때 false를 반환해주는 코드이다. 이게 무슨말이며 왜 필요하냐? 손전등은 R키 하나만을 사용해 on/off를 반복한다. 만약 위 if문에 PlayerGetLight=true;라는 코드를 넣었다고 생각해보자. PlayerGetLight가 true가 되었으니 손전등의 전원은 켜질것이다.

 

하지만 손전등의 전원을 끄기 위해 한 번더 R키를 누르면 PlayerGetLight=true; 코드로 인해 PlayerGetLight는 계속해서 true값을 가지고 손전등의 전원은 off상태가 되지 못 할 것이다. 이러한 문제를 해결하기 위해 false일 때 true를, true일 때 false를 반환하는 코드를 사용한 것이다. 

 

A?B:C;
A가 참이면 B를, 거짓이면 C를 리턴한다.

 

이제 PlayerGetLight의 ture, false를 이용해 myLight의 intensity(빛의 세기)값을 바꿔준다. (처음에는 SetActive를 사용해보았지만 Spotlight가 사라지자 스크립트가 제대로 동작하지 않았다.)

 

ON/OFF가 적용된 모습

 

 


 

3. 아이템 습득 구현

 

 

이제 아이템 습득을 구현해보자. 아이템 습득은 Raycast를 이용해 구현할 것이다.

 

Raycast란?

Raycast는 bool타입을 가진 광선(우리가 생각하는 그 레이저가 맞다)을 생성해 해당 광선과 collider가 충돌하면 true값을 반환한다. 

 

ray는 우리 눈에 보이지 않는 광선이다. (설정을 통해 보이게 할 수는 있다!) 이 광선에 특정한 collider가 충돌한다면 ray는 해당 collider를 감지할 수 있다. 대표적인 예시로는 다가온 플레이어를 감지하여 공격하는 에너미가 있다. 에너미가 쏘는 ray에 플레이어의 collider가 충돌한다면 에너미는 플레이어가 주변에 있음을 감지하고 공격을 하는 것이다.

 

이것을 이용하여 ray에 아이템 collider가 감지된다면 그것을 습득할 수 있도록 구현해보겠다.

 

 

 

우선 아이템(손전등)이 만들어질 포인트를 생성한다. 아이템은 스크립트를 이용해 등장시킬 예정이다.

 

 

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

public class ObjectPoint : MonoBehaviour
{
    public GameObject FlashLight;

    private void Awake()
    {
        FlashMake();
    }

    void FlashMake() {
        Instantiate(FlashLight, transform.position, transform.rotation);
    }
}

 

 

생성한 포인트에 c#스크립트를 하나 붙여준다. Instantiate는 지정한 위치에 객체를 생성해주는 함수이다. 

Awake를 이용해 Instantiate를 호출해 아이템을 만들어주자. 

 

Awake는 start함수보다 먼저 실행되는 메소드로 스크립트가 비활성화된 상태에도 실행된다는 특징이 있다. 

 

 

 

 

다음으로 아이템 오브젝트에 collider를 붙여준 후, 감도를 높이기 위해 collider의 크기를 오브젝트보다 큰 사이즈로 잡아준다.

그리고 C#스크립트를 하나 만들어 Player 오브젝트에 넣어준다.

 

 

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

public class Player : MonoBehaviour
{

    private RaycastHit hit; //ray의 충돌정보를 저장하는 구조체
    private Ray ray;

    void Update()
    {
        ObjectHit();
    }

    void ObjectHit() {
        ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); //ray생성

        if (Physics.Raycast(ray, out hit))
        {
            if (hit.collider.gameObject.tag == "FlashLight")
            {
                Debug.Log("손전등입니다."); //오브젝트의 태그가 FlashLight일 경우           
            }

        }     
    }
  
}

 

Ray 생성

Ray ray = new Ray(Ray의 시작점, Ray의 방향);
ex) Ray ray=new Ray(transform.position, transform.forward);

 

ViewPointToRay는 카메라의 뷰포트에서 ray를 생성하겠다는 의미이다. 벡터의 x,y값에 0.5를 넣어주면 카메라의 중앙에서 ray를 생성할 수 있다.우리는 1인칭 시점의 메인카메라를 사용하고 있으니

 

 

 

이정도 부분에서 앞으로 뻗는 ray가 생성되는 것이다. 이제 ray가 생성되었으니 ray와 충돌하는 collider를 감지해야 한다. ray와 collider의 충돌여부는 Raycast 함수를 통해 감지할 수 있다.

 

Raycast 함수 사용

Physics.Raycast(Ray의 시작점, Ray의 방향, out RaycastHit);

 

Ray의 시작점과 방향은 앞서 만들어둔 ray를 사용하였다. ray가 감지한 충돌 정보는 RaycastHit에 저장한다. (위 스크립트에서는 hit을 사용하였다.)

 

 

 

ray에 손전등이 닿자 if문이 실행되며 콘솔에 로그가 찍히는 모습이다. Raycast가 정상적으로 작동하는 것을 확인했으니 이습득하는 것을 구현해보자. 아이템습득을 위한 키는 E키로 지정하였다. 

 

아이템 습득을 구현하기 위해서는 Player, FlashLight 두 개의 스크립트를 수정해주어야 한다.

 

 

<Player 스크립트>

 

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

public class PlayerTest : MonoBehaviour
{
    private RaycastHit hit;
    private Ray ray;

    void Update()
    {
        ObjectHit();
    }

    void ObjectHit()
    {
        ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));

        if (Physics.Raycast(ray, out hit))
        {
            if (hit.collider.gameObject.tag == "FlashLight")
            {
                Debug.Log("손전등입니다.");
                FlashLight.GetLight(); //FlashLight스크립트의 GetLight함수 실행

            }
        }
    }

}

 

 

<FlashLight 스크립트>

 

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


public class FlashLight : MonoBehaviour
{
	bool PlayerGetLight;
	static bool useLight; //손전등 획득 여부를 확인할 변수
	Light myLight;
	// Start is called before the first frame update
	void Start()
	{
		PlayerGetLight = false;
		useLight = false;
		myLight = this.GetComponent<Light>();
	}

	// Update is called once per frame
	void Update()
	{
		lightOnOFF();
		if (PlayerGetLight == false)
		{
			myLight.intensity = 0;
		}
		else if (PlayerGetLight == true)
		{
			myLight.intensity = 10;
		}
	}

	static internal void GetLight()
	{
		if (Input.GetKeyDown(KeyCode.E))
		{
			useLight = true; //손전등을 획득했음을 알림
			Destroy(GameObject.FindGameObjectWithTag("FlashLight")); //맵에 놓인 손전등 삭제          
		}
	}

	void lightOnOFF() {
		if (useLight == true) //손전등을 획득했을 경우에만 실행되도록
		{
			if (Input.GetKeyDown(KeyCode.R))
			{
			PlayerGetLight = PlayerGetLight ? false : true;
			}
		}		
	}
}

 

Player스크립트에는 if문에 FlashLight.GetLight함수를 실행하라는 코드만 추가하였다. 본격적인 작동은 FlashLight스크립트에서 구현하였다.

 

FlashLight스크립트의 GetLight함수는 static으로 선언되었다. static이란 정적 속성을 의미하며 클래스로부터 객체를 생성하지 않고도 변수나 함수를 호출할 수 있게 해준다. Player스크립트에서 GetLight함수에 바로 접근할 수 있도록 static으로 선언해주었다.

 

static은 정적인 속성을 가지고있다. 이 말은 즉, static으로 선언된 요소들은 변하지 않는다는 것이다. 일반적인 변수나 함수는 프로그램이 실행되면 메모리 공간을 할당하지만 정적인 속성을 가진 것은 프로그램이 실행되기 전, static으로 선언한 순간부터 메모리 공간을 할당하게 된다. 

때문에 static은 static끼리만 접근 가능하며 static함수에서 일반적인 멤버 변수를 선언할 수는 없다. static함수가 데이터를 저장하는 시점에서 일반 멤버 변수는 만들어지지 않았기 때문이다. (일반 멤버 변수는 프로그램이 실행되면 메모리 공간을 할당한다!)

 

internal은 접근지정자 중 하나로 public과 비슷하지만 조금 다르다. 외부 클래스에서 접근이 가능한 것은 public과 동일하지만 internal은 같은 에셈블리, 그러니까 동일한 프로젝트 내에서만 접근이 가능하다. 위 GetLight함수는 같은 에셈블리에 포함된 Player클래스에서의 접근만 필요하므로 public이 아닌 internal로 선언해주었다.

 

 

그 외, 손전등의 on/off가 손전등을 획득했을때만 작동하도록 if문과 useLight변수를 이용해 코드를 수정하였고 Update문이 너무 길어지지 않도록 따로 함수로 구현해주었다.

 

 

 

아이템 획득이 무사히 구현된 모습이다.

 

 

 

하지만 ray가 오브젝트에 닿았는지 알 수 없어 아이템을 습득하는 것이 불편하다.

다음 게시글에서는 오브젝트 위에 UI를 생성해 ray가 오브젝트에 충돌했는지 여부를 게임 화면에서 확인해보도록 하겠다.