내일배움캠프 Unity 22일차 TIL - Unity 게임 개발 입문 1주차

Wooooo·2023년 11월 28일
0

내일배움캠프Unity

목록 보기
24/94

오늘의 키워드

오늘도 개인 과제를 진행했다. 필수 요구 사항은 모두 구현했고, 선택 요구 사항도 거의 다 구현했다. 오늘은 NPC 대화를 구현하는 데 많은 시간을 쓴 것 같다. 인터페이스와 이벤트를 둘 다 사용해보려고 노력했다.


1. 준비 - NPC 클래스

인터페이스로 NPC들의 기능을 세세하게 분리하고 보니까, 깡통 NPC 클래스는 되게 단순해졌다.

using UnityEngine;

public class NPC : MonoBehaviour
{
    public string npcName;
    [SerializeField] NameText nameText;

    protected virtual void Start()
    {
        nameText.SetName(npcName);
    }
}

자기 이름이랑 그 이름을 머리 위에 띄워주는 기능이 전부다!!


2. 설계 - NPC의 각 행동을 구현하는 인터페이스

NPC가 어떤 행동을 할 수 있을 지 생각해보고, 그 기능들을 다 인터페이스로 빼봤다.
인터페이스에 어떤 이벤트와 메서드가 들어가야할지는 금방 생각해냈는데, 메서드에 어떤 매개변수를 지정해줘야할지를 오래 고민했던 것 같다.

2-1. 말할 수 있는 NPC 인터페이스

using System;

public interface IConversableNPC
{
    public event Action<string> OnConversationEntered;
    public event Action<string> OnConversationLeaved;

    public void OnConversationEnter(string script);
    public void OnConversationLeave(string script);
}

말할 수 있는 NPC는 IConversableNPC를 상속받아서 구현한다!
대화가 시작되면 할 말은 Enter, 대화가 끝나면 할 말은 Leave에서 호출하도록 설계해봤다.

2-2. 주변 범위를 탐지하는 NPC 인터페이스

using System;
using UnityEngine;

public interface IRangeDetectableNPC
{
    public event Action<Collider2D> OnRangeEntered;
    public event Action<Collider2D> OnRangeExited;
    public event Action<Collider2D> OnRangeStayed;

    public void OnRangeEnter(Collider2D col);
    public void OnRangeExit(Collider2D col);
    public void OnRangeStay(Collider2D col);
}

자기 주변 일정 범위 내에 플레이어가 들어왔는지, 나갔는지 탐지하는 NPC는 IRangeDetectableNPC를 상속받아서 구현한다!
범위 내에 플레이어가 들어왔을 때, 들어와 있을 때, 나갔을 때 호출될 Enter, Stay, Exit 메서드들을 구현해야한다.
아무래도 OnTriggerEnter2D(Collider2D)로 탐지를 할 것이기 때문에, 일단은 매개변수도 Collider2D로 해줬다.

2-3. 때릴 수 있는 NPC

using System;

public interface IHitableNPC
{
    public event Action OnHited;

    public void OnHit();
}

아직은 만들어만 두고 구현하는 NPC를 추가하진 못했지만, 때릴 수 있는 NPC도 있으면 재밌을 것 같아서 만들어놨다.


3. 구현 - 일정 거리에 들어오면 말을 거는 NPC

IConversableNPCIRangeDetectableNPC 인터페이스를 구현하고, NPC 클래스를 상속받는 NPC_Lizard 클래스를 구현했다.

3-1. 전체 코드

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

public class NPC_Lizard : NPC, IRangeDetectableNPC, IConversableNPC
{
    [SerializeField] SpeechBubble speechBubble;

    List<string> RangeDetectEnteredScripts = new() 
    {
        @"growl",
        @"!",
        @"DateTime : {DateTime}",
    };
    List<string> RangeDetectExitedScripts = new() 
    {
        @"bye {PlayerName}",
        @"...",
    };

    public event Action<Collider2D> OnRangeEntered;
    public event Action<Collider2D> OnRangeExited;
    public event Action<Collider2D> OnRangeStayed;
    public event Action<string> OnConversationEntered;
    public event Action<string> OnConversationLeaved;

    protected override void Start()
    {
        base.Start();
        OnRangeEntered += OnConversationEnter;
        OnRangeExited += OnConversationLeave;
        OnConversationEntered += Speech;
        OnConversationLeaved += Speech;
    }

    public void Speech(string script)
    {
        script = ScriptConverter.Convert(script);
        speechBubble.Enable(script);
    }

    public void OnConversationEnter(string script)
    {
        OnConversationEntered?.Invoke(script);
    }
    public void OnConversationEnter(Collider2D col)
    {
        OnConversationEnter(RandomScriptInList(RangeDetectEnteredScripts));
    }

    public void OnConversationLeave(string script)
    {
        OnConversationEntered?.Invoke(script);
    }
    public void OnConversationLeave(Collider2D col)
    {
        OnConversationLeave(RandomScriptInList(RangeDetectExitedScripts));
    }

    public void OnRangeEnter(Collider2D col)
    {
        OnRangeEntered?.Invoke(col);
    }

    public void OnRangeExit(Collider2D col)
    {
        OnRangeExited?.Invoke(col);
    }

    public void OnRangeStay(Collider2D col)
    {
        //OnTriggerStayed?.Invoke(col);
    }

    string RandomScriptInList(List<string> scripts)
    {
        return scripts[UnityEngine.Random.Range(0, scripts.Count)];
    }
}

인터페이스 여러 개를 구현하다보니 메서드가 많아져서 조금 길다....

3-2. 이벤트 구독시키기

    protected override void Start()
    {
        base.Start();
        OnRangeEntered += OnConversationEnter;
        OnRangeExited += OnConversationLeave;
        OnConversationEntered += Speech;
        OnConversationLeaved += Speech;
    }

Start()에서 각 이벤트에 상황에 맞는 메서드들을 구독시켜줬다. 나는 이게 가장 힘들었다.
나는 범위 내에 들어갔을 때 대화 시작 말풍선을 출력하고, 범위 밖으로 나갔을 때 대화 종료 말풍선을 출력하고 싶은데,
IRangeDetectableNPC에서는 OnRangeEnterd의 매개변수가 Collider2D이고
IConversableNPCOnConversableEnter는 매개변수가 string이다.

이걸 어떻게 해야하나 머리를 끙끙 싸매다가 결국, Collider2D를 매개변수로 받는 OnConversableEnter/Leave() 메서드를 오버로드하기로 했다...

    public void OnConversationEnter(string script)
    {
        OnConversationEntered?.Invoke(script);
    }
    public void OnConversationEnter(Collider2D col)
    {
        OnConversationEnter(RandomScriptInList(RangeDetectEnteredScripts));
    }

이게 맞나,, 싶은 오버로드. 매개변수로 콜라이더 받아놓고 안써버리기

3-3. 상황에 맞는 말들 중에 랜덤으로 말하게 하기

    List<string> RangeDetectEnteredScripts = new() 
    {
        @"growl",
        @"!",
        @"DateTime : {DateTime}",
    };
    List<string> RangeDetectExitedScripts = new() 
    {
        @"bye {PlayerName}",
        @"...",
    };

상황에 맞는 말들을 리스트로 만들어놨다.

    string RandomScriptInList(List<string> scripts)
    {
        return scripts[UnityEngine.Random.Range(0, scripts.Count)];
    }

이 스크립트들은 Speech() 메서드로 넘어가기 전에 리스트에서 랜덤한 하나만 꺼낸다.

    public void Speech(string script)
    {
        script = ScriptConverter.Convert(script);
        speechBubble.Enable(script);
    }

말풍선을 띄워주는 메서드인 Speech()에서는 특수한 문자열를 ScriptConverter 클래스를 통해 변환해서 출력한다.

ScriptConverter.cs

using System;
using UnityEngine;

public class ScriptConverter
{
    public static string Convert(string script)
    {
        string res = script;
        res = res.Replace(@"{PlayerName}", DataManager.Instance.PlayerName);
        res = res.Replace(@"{DateTime}", DateTime.Now.ToString("HH:mm"));
        return res;
    }
}

플레이어의 이름이나, 현재 시간등 특수한 문자열을 변환해주는 전역 클레스를 만들어봤다...

3-4. 잘 동작하나 보기

다행히 잘 동작하는 것 같다.


4. 이런 뻘짓을 했어요

4-1. 말풍선 왜 안나와

제대로 짠 것 같은데 계속 비벼도 말풍선이 안나왔다. 이벤트도 잘 바인딩해줬고 콜라이더도 설정해줬고 Trigger도 체크해줬고 RigidBody도 있는데 계속 안나왔다.

결국 Debug.Log() 하나하나 찍어가면서 메서드 호출이 어디서 끊기나 찾아봤는데 말풍선이 켜지는 것까지 잘만 호출되는 것이다. 이 때 깨달았다.

말풍선의 지속시간을 0초로 해뒀다는 사실을...

4-2. 특수문자열 왜 Convert 안 돼

ScriptConverter 클래스를 딱 만들고 실행해봤는데, 특수문자열을 변경하지 않은 상태로 말풍선에 출력이 됐다. 이것도 Replace 값 하나하나 Debug.Log() 찍어보다가 깨달았다.

    public static string Convert(string script)
    {
        string res = script;
        res = script.Replace(@"{PlayerName}", DataManager.Instance.PlayerName);
        res = script.Replace(@"{DateTime}", DateTime.Now.ToString("HH:mm"));
        return res;
    }

이건 멍청했던 내가 짠 거고

    public static string Convert(string script)
    {
        string res = script;
        res = res.Replace(@"{PlayerName}", DataManager.Instance.PlayerName);
        res = res.Replace(@"{DateTime}", DateTime.Now.ToString("HH:mm"));
        return res;
    }

이건 머리 깨진 내가 고친 거다.

Replace 했으면 변경된 string에서 다음 변경을 진행해야하는데, 바보같이 Replace 해놓고 다시 원본값에서 다음 변경을 진행하니까 {PlayerName}이 변경이 될 리가 없었다.

똑똑한 IDE가 불필요한 값 할당했다고 알려줬는데 나는 '얘 왜 이래' 하고 넘겼었던 기억이 있다.


마치며

이제 내일 개인 과제 제출이다. 큼지막한 기능은 다 구현했으니 내일 점심 중으로 다른 NPC 추가하고, NPC에도 애니메이션 적용하고, 시간이 된다면 맞으면 말하는 NPC도 추가해봐야겠다.

profile
game developer

0개의 댓글