Unity 2D RPG - 4. 대화 시스템 구현하기

2023. 8. 13. 06:21C# & Unity 공부

저번에는 대상을 인식하여 대화창에 띄우는 것까지 했다면, 

이번 시간에는 대상이 대사를 할 수 있도록 해보겠다.

오브젝트 관리

 

어느 사물을 정했을 때, 그 사물이 NPC인지 아니면 그냥 일반 사물인지

혹은 ID는 어떻게 되는지 그것을 정해주어야 한다.

 

우선 C# Script를 만들어보자.

이름은 'ObjData"라고 해두자.

 

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

public class ObjData : MonoBehaviour
{
    public int id;
    public bool isNpc;
}

 

우선 위처럼 코드를 작성한 뒤 저장하자.

그리고 작성한 스크립트들을 오브젝트들에게 넣어주자.

 

 

[예시]

NPC A : Id => 1000, isNpC => 체크

NPC B : Id => 2000, isNpC => 체크

Box : Id => 100

Desk : Id => 200

 

대화 시스템

 

이번에는 대화 데이터를 관리해줄 매니저를 생성해 보겠다.

 

우선 Create Empty를 통해 새로운 오브젝트를 만들어주고,

C# Script를 만들어 'TalkManager'이라고 이름을 바꿔주자.

 

Dictionary<int, string[]> talkData;
//데이터 구조가 키와 키에 연결된 밸류가 들어간다.(타입 두개 필요)

void Awake()
{
    talkData = new Dictionary<int, string[]>();
    GenerateData();
}

void GenerateData()
{
    talkData.Add(1000, new string[] { "안녕", "이 곳에 처음 왔구나?" });

    talkData.Add(100, new string[] { "평범한 나무상자다." });
    talkData.Add(200, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
}

public string GetTalk(int id, int talkIndex)
{
    return talkData[id][talkIndex];
}

 

우선 Dictionary 변수를 하나 만들어준다.

Dictionary는 키와 그 키에 연결된 값이 같이 저장할 수 있는 자료구조이다.

 

그래서 이 자료구조를 사용하기 위해선 자료형이 2가지가 필요하다.

 

암튼 new 키워드를 통하여 공간을 만들어주고 Generate()함수를 실행한다.

(Dictionary 뒤에 ()가 오는 이유는 클래스라서)

 

Generate()함수는 Dictionary 구조에다가 데이터를 넣는 과정이다.

Add를 통하여 키를 정해주고 그 키에 연결된 string 배열에 알맞은 대사를 넣은 것이다.

 

그리고 GetTalk()함수는 매개변수로 id와 talkIndex를 통하여,

GenerateData()를 통해 지정한 id를 입력하여 그 id 안의 string 배열에서

인덱스 값을 통해 원하는 대사를 출력하는 함수이다.

 

이제 GameManager.cs에서 코드를 수정해보자.

 

public class GameManager : MonoBehaviour
{
    public TalkManager talkManager;
    public GameObject talkPanel;
    public Text talkText;
    public GameObject scanObject;
    public bool isAction;
    public int talkIndex;

    public void Action(GameObject scanObj)
    {
        if(isAction)
        {
            isAction = false;
        }

        else
        {
            isAction = true;
            scanObject = scanObj;
            ObjData objData=scanObject.GetComponent<ObjData>();
            Talk(objData.id, objData.isNpc);
        }

        talkPanel.SetActive(isAction);
    }

    void Talk(int id, bool isNpc)
    {
        string talkData = talkManager.GetTalk(id, talkIndex);

        if(isNpc)
        {
            talkText.text = talkData;
        }

        else
        {
            talkText.text = talkData;
        }
    }
}

 

Action() 함수에서  ObjData 변수 objData를 만들고 스캔한 오브젝트의 ObjData정보를 불러올 것이다.

그리고 스캔한 ObjData의 멤버 변수들을 매개변수로 활용하여 Talk함수를 실행할것이다.

 

talkData 문자열은 앞에서 TalkManager 변수인 talkManager (추후에 에디터를 통해 넣을 것)에서

id와 talkIndex를 통하여 그 사물의 대사를 입력 받는다.

 

그리고 Npc이냐 아니냐에 따라서 분류해 놓았다.

 

우선 저장하고 알맞은 오브젝트들을 public 변수에 넣자. (에디터에서)

 

 

 

근데 우리가 Npc A한테 넣은 문장은 2문장인데 하나만 실행된다.

이것을 수정해볼 것이다.

 

public class GameManager : MonoBehaviour
{
    public TalkManager talkManager;
    public GameObject talkPanel;
    public Text talkText;
    public GameObject scanObject;
    public bool isAction;
    public int talkIndex;

    public void Action(GameObject scanObj)
    {
        scanObject = scanObj;
        ObjData objData = scanObject.GetComponent<ObjData>();
        Talk(objData.id, objData.isNpc);

        talkPanel.SetActive(isAction);
    }

    void Talk(int id, bool isNpc)
    {
        string talkData = talkManager.GetTalk(id, talkIndex);

        if(talkData==null)
        {
            isAction = false;
            talkIndex = 0;
            return;
        }

        if(isNpc)
        {
            talkText.text = talkData;
        }

        else
        {
            talkText.text = talkData;
        }

        isAction = true;
        talkIndex++;
    }
}

 

스페이스 바를 두 번 이상 눌러야 하기 때문에 Action() 함수를 고쳐야 한다.

isAction을 Talk()함수에서 관리하도록 만들었다.

 

스페이스바를 누를 때마다 Action()함수를 실행하는데 그에 따라서 Talk()함수를 실행하게 된다.

증감 연산자를 사용하여 Talk()가 실행될 때마다 talkIndex를 늘려주게 한다.

 

 public string GetTalk(int id, int talkIndex)		//TalkManager.cs에서
{
    if (talkIndex == talkData[id].Length)
        return null;
    else
        return talkData[id][talkIndex];
}

 

그러면 알맞는 대사를 잘 갖고 올 수 있다.

끝을 만들어주기 위해서는 talkIndex가 talkData의 해당 id의 string 배열의 길이와 같을 때 null을 출력하게 한다.

GetTalk()을 통하여 저장된 talkData가 비어있다면(null이라면) isAction을 false로 만들어 판넬을 끄게 하고

talkIndex를 0으로 초기화 하여 다른 사물들과 대화를 시작할 때 0부터 시작하게 하며

return으로 빠져나오게 하였다.

 

Index가 배열 길이(0,1) 2를 도달하여 null을 출력하여 판넬이 꺼지도록 함

 

다음 대화를 할 수 있게 0으로 초기화된 모습

 

초상화

 

우선 대화창 오브젝트 아래에 Image를 넣어주자.

그리고 캐릭터 이미지를 넣은 후, Set Native size를 통해 원래의 크기를 설정해주자.

(안 맞으면 Sprite Editor로 가서 잘라 주기)

그리고 Anchor와 여백을 이용하여 해상도가 바뀌어도 잘 나오게 조정하자.

 

 

그리고 이미지를 컨트롤 하기 위하여 게임 매니저에다가 Image Ui 변수를 만들어주자.

 

 public Image portraitImg;

 

그리고 할당해주면 된다.

 

 if(isNpc)
{
        talkText.text = talkData;

        portraitImg.color = new Color(1, 1, 1, 1);
}

else
{
        talkText.text = talkData;

        portraitImg.color = new Color(1, 1, 1, 0);
}

 

Npc이냐 아니냐에 따라서 Color값의 alpha 값을 조정하여 이미지가 완전히 나오거나 완전히 투명하게 해준다.

 

Sprite를 보면 같은 캐릭터도 종류가 여러 가지가 있다.

표정이 다 다르기 떄문이다.

그러면 '대화를 가져올 때 Sprite도 다 가져올 수 있지 않을까? '라고 생각할 수 있다.

 

Dictionary<int, Sprite> portraitData;

public Sprite[] portraitArr;

 

TalkManager에 Dictionary 변수 하나와 Sprite 배열을 하나 만든다.

 

그리고 저장을 한 후 Editor에서 portraitArr에 칸을 8개 만든 후 Sprite들을 다 집어 넣는다.

 

 

그리고 코드를 돌아가서 Add를 통해 Dictionary 자료구조에다가 각 초상화 데이터를 번호와 함께 넣는다.

 

 void GenerateData()
    {
        talkData.Add(1000, new string[] { "안녕", "이 곳에 처음 왔구나?" });
        talkData.Add(2000, new string[] { "여어.", "이 호수는 정말 아름답지??", "사실 이 호수에는 무언가의 비밀이 숨겨져 있다고 해" });
        talkData.Add(100, new string[] { "평범한 나무상자다." });
        talkData.Add(200, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });

        portraitData.Add(1000 + 0, portraitArr[0]);
        portraitData.Add(1000 + 1, portraitArr[1]);
        portraitData.Add(1000 + 2, portraitArr[2]);
        portraitData.Add(1000 + 3, portraitArr[3]);
        portraitData.Add(2000 + 0, portraitArr[4]);
        portraitData.Add(2000 + 1, portraitArr[5]);
        portraitData.Add(2000 + 2, portraitArr[6]);
        portraitData.Add(2000 + 3, portraitArr[7]);
    }

 

주의!

헷갈리면 안된다.

해당 오브젝트의 ID 뒤에 + 숫자를 한 이유는

대사를 넣을 때에는 배열 한 개에 sting을 여러 개 저장하면 끝나기 때문에 같은 ID를 구분할 상황이 오지 않았지만,

초상화는 같은 대상이 여러 개의 표정 데이터를 갖기 때문에 헷갈리지 않게 구분할 필요가 있다.

뭐 1000번 키에 0번 인덱스의 정보를, 1001번 키에는 1번 인덱스의 정보를 넣은 건 맞으나.

나중에 ID + 표정 인덱스 꼴로 넣기 위해 저렇게 표시한 것이다.

 

그리고 지정된 스프라이트를 반환할 함수를 구현해주면 된다.

 

 public Sprite GetPortrait(int id, int portraitIndex)
{
    return portraitData[id + portraitIndex];
}

 

이제 대사마다 표정(Sprite)가 다르기 때문에 대사에다가 직접 표정을 정해줄 것이다.

 

talkData.Add(1000, new string[] { "안녕:0", "이 곳에 처음 왔구나?:1" });
talkData.Add(2000, new string[] { "여어.:1", "이 호수는 정말 아름답지??:0", "사실 이 호수에는 무언가의 비밀이 숨겨져 있다고 해:1" });

 

이렇게 문장 뒤에 기호와 그리고 표정 인덱스를 같이 넣어주면 된다.

기호는 꼭 ' : '이 아니어도 된다.

 

 

그 이후에는 대사와 숫자만 나눠주면 된다.

Split() 함수로 대사였던 문자열을 배열로 나눌 것이다.

 

void Talk(int id, bool isNpc)
{
    string talkData = talkManager.GetTalk(id, talkIndex);

    if(talkData==null)
    {
        isAction = false;
        talkIndex = 0;
        return;
    }

    if(isNpc)
    {
         talkText.text = talkData.Split(':')[0];

         portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
         portraitImg.color = new Color(1, 1, 1, 1);
    }

    else
    {
         talkText.text = talkData;

         portraitImg.color = new Color(1, 1, 1, 0);
    }

    isAction = true;
    talkIndex++;
}

 

Split() : 문자열을 지정한 문자 기준으로 배열로 나누는 것.

 

당연히 배열의 인덱스 0번째는 대사일 거고, 1번째는 우리가 나타낼 표정인 portraitIndex가 될 것이다.

 

Parse()를 쓴 이유는 안의 매개변수가 string이기 때문에 형변환을 해줘야 한다.

그래서 형변환 해주는 함수인 Parse()를 사용한 것이다.

 

이후 저장한 후에, Editor에서 GameManager 오브젝트에 Portrait Img에다가 Portrait 오브젝트를 넣자.

 

 

 

잘 실행되는 것을 확인할 수 있다.