4 분 소요

QuestGiver

각각의 Quest를 작성할 때 enum으로된 NPCName을 입력하는 변수가 있다.
NPC들은 QuestGiver를 가지고 있으며 마찬가지로 어떤 NPC인지 enum 타입으로 가지게 된다.
그렇다면 QuestManager에 등록된 퀘스트중 NPCName을 가져와보도록 하자.

public enum NPCName
{
    Mother,
    Manager,
    President
}

public class QuestGiver : MonoBehaviour
{   
    public NPCName _npcName;
    public List<Quest> _myQuests = new List<Quest>();   // 이 NPC가 가진 퀘스트

    // 각 NPC의 Quest 가져오기
    private void DistributeQuests()
    {
        // QuestManager에서 NPCName이 같은 모든 퀘스트 가져오기
        QuestManager questMgr = QuestManager._Instance;
        _myQuests = questMgr._questContainer.FindAll(x => x._npcName.GetHashCode() == _npcName.GetHashCode());
    }
}

그냥 string으로 비교하는 것보다 HashCode로 비교하는 것이 훨씬 빠르다고하여 이렇게 하였다.

우리는 NPC가 주는 퀘스트를 골라서 받는 기능은 없다. Player가 NPC와 상호작용 한다면 NPC가 가진 첫 번째 퀘스트부터 순차적으로 받을 것이다. 그렇기 때문에 현재 퀘스트를 나타내는 변수를 선언한다.

public Quest _CurrentQuest
{
    get
    {
        // 더이상 퀘스트가 없을때
        if (_currentQuestIdx >= _myQuests.Count)
        {
            return null;
        }
                    
        return _myQuests[_currentQuestIdx];
    }
}

NPCMakerUI

각각의 NPC가 가진 퀘스트에 따라 머리위에 마크를 달 것이다.
조건과 모양은 다음과 같다.

  • 퀘스트가 없거나 수락 불가 : 마크 없음
  • 수락 가능 : ?
  • 진행중 : 톱니바퀴
  • 완료 가능 : ★

NPC를 배열로 저장하고 퀘스트가 완료될때 마다 이벤트로 갱신시켜주겠다.

public class NPCMarkerUI : MonoBehaviour
{
    [SerializeField] private QuestGiver[] _npcs;

    public QuestGiver[] _Npcs => _npcs;

    // (int 퀘스트제공자, QuestState 퀘스트 진행상태)
    public void SettingByQuestState(int idx, QuestState questState)
    {
        OnMarker(idx, questState);
    }

    private void OnMarker(int idx, QuestState state)
    {
        QuestGiver giver = _npcs[idx];
        int intState = (int)state - 1;  // 0은 수락 불가능 상태이므로 1부터 시작

        for(int i = 0; i < giver._Markers.Length; i++)
        {
            // 수락 불가 상태나 완료된 상태라면 모든 마커 종료
            if (state == QuestState.Unvaliable || state == QuestState.Done)
            {
                giver._Markers[i].gameObject.SetActive(false);
                continue;
            }

            if (intState == i)
            {
                giver._Markers[i].gameObject.SetActive(true);
            }
            else 
            {
                giver._Markers[i].gameObject.SetActive(false);
            }
        }
    }
}

이벤트들은 QuestGiver에서 퀘스트를 Manager에서 가져올때 같이 추가시켜준다. Quest에서 먼저 이벤트를 선언해준다.

Quest class

// QuestGiver의 현재 퀘스트 상태에 따른 Marker 업데이트
public System.Action<int, QuestState> _onNPCMarker; 

QuestGiver class

private void DistributeQuests()
{
    // QuestManager에서 NPCName이 같은 것 가져오기
    QuestManager questMgr = QuestManager._Instance;
    _myQuests = questMgr._questContainer.FindAll(x => x._npcName.GetHashCode() == _npcName.GetHashCode());

    if (_myQuests.Count <= 0) { return; }

    // 이벤트 등록
    foreach (var quest in _myQuests)
    {
        quest._onNPCMarker += UIManager._Instance._NPCMarkerUI.SettingByQuestState;
    }
}

NPCMarker

ObjectInteraction과 InteractionUI

NPC와 대화를 하기 위한 상호작용을 만들어야 한다. 캐릭터에서 정면으로 Ray를 쏘고 충돌체가 Layermask를 이용해 NPC인 것을 골라내자.
또한, 상호작용 할 다른 오브젝트를 고려하여 enum으로 정의 하였다. (확장성 고려)

public enum InteractionObjectType
{
    Default,
    NPC,
    Item
}

public class ObjectInteraction : MonoBehaviour
{
    [SerializeField] private LayerMask _mask;
    private RaycastHit _hit;
    private InteractionObjectType _interObjType;
    private int _npc;

    private void Awake()
    {
        _npc = LayerMask.NameToLayer("NPC");
    }

    private void Update()
    {
        OnInterection();
    }

    private void OnInterection()
    {
        Debug.DrawRay(transform.position + Vector3.up, transform.forward, Color.blue, 1f);
        if (Physics.Raycast(transform.position + Vector3.up, transform.forward, out _hit, 1f, _mask))
        {
            // hit.Layer가 NPC 일때 (다른 타입이 추가 될경우 else if또는 switch로 추가)
            if (_hit.collider.gameObject.layer == _npc)
            {
                _interObjType = InteractionObjectType.NPC;
            }
        }
    }
}

Player에 이 스크립트를 붙여주고 Layer를 설정하자.
ObjectInteraction DrawRay

이러면 일단 NPC에 한해서만 충돌 할 것이다. 그러면 Talk 라는 메세지를 띄워 보도록 하자.

InteractionUI

마찬가지로 확장성을 고려해 출력 되어질 text에 상호작용되는 물체에따라 string이 대입 될 것이다.
현재는 NPC만 있으니 itemText는 직렬화를 안 시켜 놓을 것이다.

public class InteractionUI : MonoBehaviour
{
    [SerializeField] private string _talkText;  // 인스펙터에서 텍스트 입력
    private string _itemText;
    public Text _text;

    public void OnInterText()
    {
        gameObject.SetActive(true);
    }

    public void OffInterText()
    {
        gameObject.SetActive(false);
    }

    public void SetText(InteractionObjectType interObj)
    {
        switch (interObj)
        {
            case InteractionObjectType.NPC:
                _text.text = _talkText;
                break;
            case InteractionObjectType.Item:
                _text.text = _itemText;
                break;
            default:
                break;
        }
    }
}

이제 물체 충돌시 나타낼 텍스트를 만들었으니 다시 ObjectInteraction 스크립트로 돌아가자 대화가 가능한 조건을 만들어 준다. 다음은 Talk가 가능한 조건이다.

  1. interObjType == NPC
  2. NPC의 currentQuest가 null이 아닐 것
  3. NPC의 currentQuest가 수락 가능한 상태 (퀘스트 수락을 위한 대화)
  4. NPC의 currentQuest가 완료 가능한 상태 (퀘스트 완료를 위한 대화)

위 조건을 OnInterection()에서 작성해준다.

private void OnInterection()
{
    //Debug.DrawRay(transform.position + Vector3.up, transform.forward, Color.blue, 1f);
    if (Physics.Raycast(transform.position + Vector3.up, transform.forward, out _hit, 1f, _mask))
    {
        _canInteract = true;

        // hit.Layer가 NPC 일때 (다른 타입이 추가 될경우 else if또는 switch로 추가)
        if (_hit.collider.gameObject.layer == _npc)
        {
            _interObjType = InteractionObjectType.NPC;
        }

        // 상호작용 UI 표시
        if (!UIManager._Instance._InterUI.gameObject.activeSelf)
        {
            if (_interObjType == InteractionObjectType.NPC && 
                !UIManager._Instance._DialogUI.gameObject.activeSelf &&
                _hit.collider.GetComponent<QuestGiver>()._CurrentQuest != null &&
                (_hit.collider.GetComponent<QuestGiver>()._CurrentQuest._questState == QuestState.Avaliable ||
                _hit.collider.GetComponent<QuestGiver>()._CurrentQuest._questState == QuestState.Completed))
            { 
                UIManager._Instance._InterUI.OnInterText();
            }
        }
        UIManager._Instance._InterUI.SetText(_interObjType);
    }
}

이제 NPC 앞으로 가면 UI가 나타날것이다. 하지만 이렇게만하고 끝내면 영원히 꺼지지 않는다.
이것은 Update 문에서 돌기 때문에 On/Off되는 조건을 걸어주자.

[SerializeField] private bool _canInteract;

private void OnInterection()
{
    //Debug.DrawRay(transform.position + Vector3.up, transform.forward, Color.blue, 1f);
    if (Physics.Raycast(transform.position + Vector3.up, transform.forward, out _hit, 1f, _mask))
    {
        _canInteract = true;

        // hit.Layer가 NPC 일때 (다른 타입이 추가 될경우 else if또는 switch로 추가)
        if (_hit.collider.gameObject.layer == _npc)
        {
            _interObjType = InteractionObjectType.NPC;
        }

        // 상호작용 UI 표시
        if (!UIManager._Instance._InterUI.gameObject.activeSelf)
        {
            if (_interObjType == InteractionObjectType.NPC && 
                !UIManager._Instance._DialogUI.gameObject.activeSelf &&
                _hit.collider.GetComponent<QuestGiver>()._CurrentQuest != null &&
                (_hit.collider.GetComponent<QuestGiver>()._CurrentQuest._questState == QuestState.Avaliable ||
                _hit.collider.GetComponent<QuestGiver>()._CurrentQuest._questState == QuestState.Completed))
            { 
                UIManager._Instance._InterUI.OnInterText();
            }
        }
        UIManager._Instance._InterUI.SetText(_interObjType);
    }
    else
    {
        if (_canInteract) { _canInteract = false; }

        if (UIManager._Instance._InterUI.gameObject.activeSelf)
        {
            UIManager._Instance._InterUI.OffInterText();
        }
    }
}

bool 타입 _canInteract를 선언하여 On/Off하는 제동을 걸어줬다.
이제 UI가 켜져있는데 켜지고 꺼져있는데 꺼지는 반복 호출을 하지 않을 것이다.

자, 그럼 이 canInteract를 이용하여 실제 상호작용하는 메서드를 만들어보자.
확장성을 고려해 switch문을 사용한다.

// 오브젝트와 상호작용
private void Update()
{
    OnInterection();
    InteractObject();
}

private void InteractObject()
{
    if (Input.GetKeyDown(KeyCode.F) && _canInteract)
    {
        switch (_interObjType)
        {
            case InteractionObjectType.NPC:        
                // 대화시작
                break;
            case InteractionObjectType.Item:
                // 실행
                break;
            default:
                break;
        }
    }
}

InteractionUI

다음은 DialogUI를 할 차례다. 다음편에서 계속~~