ItemInfoUI 매커니즘의 적용 방식을 변경했다. 왜냐하면 아이템에 해당 컴포넌트를 붙이고, 컴포넌트 설정하는 과정이 불편하다고 느꼈기 때문이다. 따라서 해당 매커니즘을 플레이어에게 붙여서 플레이어가 아이템의 상호작용 여부를 판정하는 것으로 변경되었다.
void Update()
{
if (GameManager.Instance.IsPaused || GameManager.Instance.IsCleared || GameManager.Instance.IsGameOver)
return;
DetectInteractableObjectByRay();
DisplayInteractableObjectUI();
if (m_detectedObject != null && GameManager.Instance.Input.InteractionKeyPressed)
{
InteractWithObject();
}
if (GameManager.Instance.Input.DropKeyPressed)
{
DropItem();
}
}
// UI를 띄우는 기능
private void DisplayInteractableObjectUI()
{
if (m_detectedObject == null)
{
m_panel.SetActive(false);
return;
}
var interactionKey = PlayerPrefs.GetString("Interaction");
if (m_detectedObject.CompareTag("Item"))
{
m_popupText.text = $"아이템을 얻으려면 [{interactionKey}]를 누르세요.";
m_panel.SetActive(true);
}
else if (m_detectedObject.CompareTag("InteractableObject"))
{
m_popupText.text = $"상호작용을 하려면 [{interactionKey}]를 누르세요.";
m_panel.SetActive(true);
}
}
// 아이템과 상호작용
void InteractWithObject()
{
IInteractable interactableObject = m_detectedObject.gameObject.GetComponent<IInteractable>();
if (interactableObject == null)
return;
if (m_interactionCoroutine == null)
{
interactableObject.Interact();
m_interactionCoroutine = StartCoroutine(InteractionDelay());
}
}
// 상호작용 가능한 아이템을 탐지
void DetectInteractableObjectByRay()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Debug.DrawRay(ray.origin, ray.direction * m_raycastDistance, Color.red, 0.1f);
if (Physics.Raycast(ray, out RaycastHit hitInfo))
{
if (hitInfo.distance > m_raycastDistance)
{
m_detectedObject = null;
return;
}
m_detectedObject = hitInfo.collider.gameObject;
}
}
UI를 띄우는 기능을 플레이어가 가지고 있는 점이 다소 어울리지 않기는 하지만, 이 부분에 대해서는 우선 기능 구현에 초점을 두고 리팩토링을 추후에 진행하기로 했다.
팀장님의 요청으로 상호작용 가능한 오브젝트에 아웃라인을 뜨게 하여 오브젝트를 더 잘 보이게 하는 기능을 만들자는 의견을 받았다. 이 부분은 UI를 담당하는 내가 맡는 게 좋겠다는 의견을 받아, 아웃라인을 그리는 방법을 찾아보기 시작했다.
아웃라인을 그리는 방법에는 Shader 기능이 들어가야 한다는 것을 알게 되었다. 셰이더 기능은 아예 배워본 적이 없어서 우선은 아래 링크의 글을 참고하여 제작하였다.
지금 단계에선 Shader 기능을 직접 구현하기는 어려울 것 같고 해당 기능의 이해만을 바탕으로 코드 내용만 조금 변경하여 사용하였다.
셰이더부터 차근차근 만드는 과정을 알려주는 글이 별로 없어서 초반부터 곤혹을 치뤘다.
우선 셰이더 스크립트를 열기 위해선 C# 스크립트를 만드는 것이 아니라 Shader를 만들어주어야 한다.
(셰이더에 작성한 스크립트는 사실 위 글에서 참고한 내용에서 거의 바꾸지 않았다.)
셰이더에 대해서는 아직 잘 모르지만, 우선은 코드를 참고하여 아웃라인 셰이더를 만들었다.
Shader "Custom/OutlineShader"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_OutLineColor("OutLine Color", Color) = (0,0,0,0)
_OutLineWidth("OutLine Width", Range(0.001, 0.5)) = 0.5
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent"}
LOD 200
// 여기서 1차적으로 아웃라인의 렌더링을 진행
cull front
zwrite off
CGPROGRAM
#pragma surface surf NoLight vertex:vert noshadow noambient
#pragma target 3.0
float4 _OutLineColor;
float _OutLineWidth;
void vert(inout appdata_full v) {
v.vertex.xyz += v.normal.xyz * _OutLineWidth;
}
struct Input
{
float4 color;
};
void surf (Input IN, inout SurfaceOutput o)
{
}
float4 LightingNoLight(SurfaceOutput s, float3 lightDir, float atten) {
return float4(_OutLineColor.rgb, 1);
}
ENDCG
// 여기서 2차적으로 대상의 원래 재질을 랜더링함
cull back
zwrite on
CGPROGRAM
#pragma surface surf Lambert
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
이와 같이 아웃라인 셰이더를 만든 후 Material을 만들어준다.
Standard에서 설정된 Shader를 변경하기 위해 Custom으로 들어가면, 내가 작성한 스크립트를 찾을 수 있다.
이제 이와 같이 아웃라인 세부설정을 세팅하여 적용시켜 보자.
이와 같이 아웃라인의 색이 정상적으로 들어온 것을 확인할 수 있다.
이 부분에서 아무래도 오늘 작업시간 중 싱당시간을 고민했던 부분이다. 이상하게도 원하는대로 잘 되지 않았기 때문이다.
첫 번째로 생각했던 방법은 Materials를 하나를 추가로 붙여 상호작용시 Elemental을 변환시키는 것이었다.
이와 같이 재질을 겹쳐서 두고, 해당 변화를 스크립트를 통해서 변경하도록 했다.
하지만 코드 상으로는 문제가 없는데, 아무리 변경해 봐도 재질 변경이 되지 않는 현상을 발견했다.
한참을 공부한 끝에, 유니티 자체가 서브메쉬를 지원하지 않는다는 점과 덮어씌워진 재질을 변경하는 것이 불가능하다는 것을 알게 되었다.
그러므로 방법을 바꿔야 하는 수밖에 없었다.
- 기본 재질은 하나만 설정하며, 아웃라인이 없는 재질로 설정한다
- 변경할 재질을 SerializedField로 등록한다. 원본과 아웃라인이 들어간 두 가지 모두 등록해야 한다.
- 조건에 따라 해당 재질이 변경 가능하도록 설정한다.
이와 같이 착안하여 아래와 같이 코드를 작성하였다.
using UnityEngine;
public class DeskDrawerController : MonoBehaviour, IInteractable
{
private Animator m_animator;
private Material[] mat = new Material[2];
private bool isDetected = false;
[SerializeField] private Material m_material1;
[SerializeField] private Material m_material2;
void Awake()
{
m_animator = GetComponent<Animator>();
mat[0] = m_material1;
mat[1] = m_material2;
}
private void Update()
{
ChangeMat(isDetected);
}
public void Interact()
{
DrawerAnimator(m_animator);
}
private void DrawerAnimator(Animator animator)
{
bool isOpen = animator.GetBool("IsOpen");
isOpen = !isOpen;
animator.SetBool("IsOpen", isOpen);
}
private void ChangeMat(bool isDetected)
{
if(isDetected)
{
gameObject.GetComponent<MeshRenderer>().material = mat[1];
}
else
{
gameObject.GetComponent<MeshRenderer>().material = mat[0];
}
}
public bool OutlineOn()
{
isDetected = true;
return isDetected;
}
public bool OutlineOff()
{
isDetected = false;
return false;
}
}
인스펙터는 다음과 같이 세팅한다.
이와 같이 만든 후에 테스트를 진행해보자.
이 부분은 생각보다 복잡한 문제가 될 수 있기 때문에 결국 당일 해결하지 못하고 회의를 거쳐 진행할 예정인 부분이다.
이 부분을 플레이어에게 바로 반영하기에는 문제점이 생긴 부분이, 아래와 같은 두 가지 이유 때문이다.
- 셰이더를 붙일 상호작용 아이템이 서랍만 있는 것이 아니기에, 1차적으로 어떤 아이템과 상호작용 중인지 판별이 필요함.
- 상호작용이 해제되었을 때 탐지된 물체 값이 null이 되어 버리기 때문에 아웃라인을 해제할 방법이 없음.
(해당 두 가지 내용은 위의 플레이어 스크립트를 참고)
이와 같은 문제 때문에 우선은 상호작용 연결은 그대로 두기로 했다.
열쇠로 잠긴 문을 구현해보기로 했다. 이를 위해서는 게임매니저의 싱글톤으로 선언되어 있는 인벤토리에 열쇠가 있는지 여부를 판정하고, 열쇠가 있으면 해당 열쇠를 제거하면서 문을 열 수 있게 하고, 아니면 문을 열지 못하게 한다.
이를 위해서 문의 열림 여부를 판정할 스크립트를 분리해서 제작하기로 했다.
먼저, 문의 열림 여부를 판정하는 클래스를 다음과 같이 작성하였다.
using UnityEngine;
public class IsLockedDoor : MonoBehaviour
{
// 초기에는 잠긴 문으로 설정
private bool isLocked = true;
public bool IsLocked(bool isPlayer = false)
{
// 잠긴 문이 잠금이 풀렸으면 판정을 진행하지 않고 바로 반환(연산을 줄이기 위함)
if(isLocked == false)
{
return isLocked;
}
// 접촉한 상대가 플레이어면 잠금 해제를 시도
if(isPlayer == true)
TryOpen();
return isLocked; // 해당 결과를 반환
}
// 잠금 해제를 시도
private void TryOpen()
{
// RemoveKey()를 가져와서 이게 True면 열리고, 아니면 안열림
if (GameManager.Instance.Inventory.RemoveKey())
{
isLocked = false;
}
}
}
다음으로 인벤토리에서 열쇠가 있는지 여부를 판정하고, 열쇠를 사용했으면 지워준다.
// 인벤토리 쪽 코드 - 팀장님이 추가해주셨다.
public bool RemoveKey()
{
for(int i = 0; i < items.Length; i++)
{
if (items[i] == null)
continue;
if (items[i].ItemName == "Key")
{
OnDropOrUseItem?.Invoke(i);
Destroy(items[i].gameObject);
items[i] = null;
m_itemCount--;
return true;
}
}
return false;
}
마지막으로 문이 열렸는지 여부를 DoorController에서 판정해준다.
// 몬스터 상호작용(다가가서 알아서 문 열기)
public void MonsterInteract()
{
// 잠겨있지 않은 문이거나 잠금이 풀렸을 때
if (m_isLockedDoor == null || !m_isLockedDoor.IsLocked())
{
m_isClosed = m_doorAnimator.GetBool("Close");
if (m_doortrigger1.MonsterDetected() && m_isClosed)
{
OpenDoorCounterClockwise();
}
else if (m_doortrigger2.MonsterDetected() && m_isClosed)
{
OpenDoorClockwise();
}
else if (m_doortrigger1.MonsterDetected() || m_doortrigger2.MonsterDetected())
{
return;
}
}
}
// 캐릭터 상호작용
public void Interact()
{
// 잠겨있지 않은 문이거나 잠금이 풀렸을 때
if (m_isLockedDoor == null || !m_isLockedDoor.IsLocked(true))
{
if (m_close == true && m_doortrigger1.PlayerDetected())
{
OpenDoorCounterClockwise();
}
else if (m_close == true && m_doortrigger2.PlayerDetected())
{
OpenDoorClockwise();
}
else if (m_close == false)
{
CloseDoor();
}
else
{
return;
}
}
}
이와 같이 작성한 후 테스트를 해 보자. 열쇠가 아직 구현되지 않은 상태라 임시로 박스를 열쇠로 설정하여 진행했다.