오늘도 개인 과제를 진행했다. 필수 요구 사항은 모두 구현했고, 선택 요구 사항도 거의 다 구현했다. 오늘은 NPC 대화를 구현하는 데 많은 시간을 쓴 것 같다. 인터페이스와 이벤트를 둘 다 사용해보려고 노력했다.
인터페이스로 NPC들의 기능을 세세하게 분리하고 보니까, 깡통 NPC
클래스는 되게 단순해졌다.
using UnityEngine;
public class NPC : MonoBehaviour
{
public string npcName;
[SerializeField] NameText nameText;
protected virtual void Start()
{
nameText.SetName(npcName);
}
}
자기 이름이랑 그 이름을 머리 위에 띄워주는 기능이 전부다!!
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에서 호출하도록 설계해봤다.
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로 해줬다.
using System;
public interface IHitableNPC
{
public event Action OnHited;
public void OnHit();
}
아직은 만들어만 두고 구현하는 NPC를 추가하진 못했지만, 때릴 수 있는 NPC도 있으면 재밌을 것 같아서 만들어놨다.
IConversableNPC
와 IRangeDetectableNPC
인터페이스를 구현하고, NPC
클래스를 상속받는 NPC_Lizard
클래스를 구현했다.
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)];
}
}
인터페이스 여러 개를 구현하다보니 메서드가 많아져서 조금 길다....
protected override void Start()
{
base.Start();
OnRangeEntered += OnConversationEnter;
OnRangeExited += OnConversationLeave;
OnConversationEntered += Speech;
OnConversationLeaved += Speech;
}
Start()
에서 각 이벤트에 상황에 맞는 메서드들을 구독시켜줬다. 나는 이게 가장 힘들었다.
나는 범위 내에 들어갔을 때 대화 시작 말풍선을 출력하고, 범위 밖으로 나갔을 때 대화 종료 말풍선을 출력하고 싶은데,
IRangeDetectableNPC
에서는 OnRangeEnterd
의 매개변수가 Collider2D
이고
IConversableNPC
의 OnConversableEnter
는 매개변수가 string
이다.
이걸 어떻게 해야하나 머리를 끙끙 싸매다가 결국, Collider2D
를 매개변수로 받는 OnConversableEnter/Leave()
메서드를 오버로드하기로 했다...
public void OnConversationEnter(string script)
{
OnConversationEntered?.Invoke(script);
}
public void OnConversationEnter(Collider2D col)
{
OnConversationEnter(RandomScriptInList(RangeDetectEnteredScripts));
}
이게 맞나,, 싶은 오버로드. 매개변수로 콜라이더 받아놓고 안써버리기
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
클래스를 통해 변환해서 출력한다.
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;
}
}
플레이어의 이름이나, 현재 시간등 특수한 문자열을 변환해주는 전역 클레스를 만들어봤다...
다행히 잘 동작하는 것 같다.
제대로 짠 것 같은데 계속 비벼도 말풍선이 안나왔다. 이벤트도 잘 바인딩해줬고 콜라이더도 설정해줬고 Trigger도 체크해줬고 RigidBody도 있는데 계속 안나왔다.
결국 Debug.Log()
하나하나 찍어가면서 메서드 호출이 어디서 끊기나 찾아봤는데 말풍선이 켜지는 것까지 잘만 호출되는 것이다. 이 때 깨달았다.
말풍선의 지속시간을 0초로 해뒀다는 사실을...
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도 추가해봐야겠다.