본문 바로가기

GAME

[Unity2D] 대화창 구현

728x90

 

[유니티 기초 - B23] RPG 대화 시스템 구현하기 

를 보고 작성했습니다.

 

 

오브젝트 관리

Layer가 Object인 오브젝트들에게 id를 부여하고, 해당 오브젝트가 NPC인지 판별이 가능하도록 변수를 주도록 합시다

ObjData.cs를 생성하고 다음과 같이 변수를 생성합니다.

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

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

 

그리고 이를 Object 레이어인 모든 오브젝트들에게 적용하고 

NPC들에게는 1000번대의 id를 부여하고

isNPC에 체크하고,

 

NPC가 아닌 오브젝트에는 100번대의 id를 부여하고,

isNPC를 해제합니다.

 

 

 

대화시스템

대화를 관리하는 매니저를 생성합시다

빈 오브젝트(TalkManager)를 생성합니다

그후 TalkManager.cs를 생성한 후 , 빈 오브젝트에 넣어줍니다.

 

TalkManager.cs

모든 오브젝트의 대사를 저장합니다.

어떤 오브젝트인지 id와 대사인 string (여러대사가 있을 수 있으므로 배열로 선언) 한 Dictionary로 관리됩니다. 

 

 

대사를 저장(생성)하는 함수 

GenerateData() 에 오브젝트의 id, 대사를 저장합니다.

이를 한줄 씩 반환하는 함수인 GetTalk를 생성해줍니다.  -> 대화창에 보여주어야 함 

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

public class TalkManager : MonoBehaviour
{
    Dictionary<int, string[]> talkData;

    // Start is called before the first frame update
    void Awake()
    {
        talkData = new Dictionary<int, string[]>();
        GenerateData();
    }

    void GenerateData()
    {
    	//id = 1000 : Talia
        talkData.Add(1000, new string[]{"안녕","이 곳에 처음 왔구나?"});
        //id = 100 : 탈리아가 갇혀있는 Prision
        talkData.Add(100,new string[]{"쇠로 만들어진 감옥이다.","열쇠없이는 열 수 없는 것 같다."});
        //id = 200 : 다음 스테이지로 넘어갈 수 있는 문 
        talkData.Add(200,new string[]{"평범한 문이다. \n들어갈 수 있을 것 같다"});
    }

    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {
        return talkData[id][talkIndex]; //해당 아이디의 해당
    }
}

* 두개의 쌍으로 이루어진 Dcitionary는

ditionary_name[first_var][second_var] 으로 반환합니다. 

 

이제 실제 UI 대화창에 이를 띄우기 위하여 

GameManager.cs

public TalkManager talkManager;
public int talkIndex;
 
 
public void Action(GameObject scanObj)
    {
        if(isAction) // 실행중 아닐때 ->대화창 없애기 
        {
            isAction=false;

        }
        else //실행중 -> 대화창 띄우기 
        {
            isAction = true;
            scanObject = scanObj;
            
            //스캔한 오브젝트의 id와 isNPC정보를 가져와야 하기 때문에 objData script가 필요
            ObjData objData = scanObject.GetComponent<ObjData>();
            //objData의 id와 NPC인지 정보를 매개변수로 넘김 
            Talk(objData.id,objData.isNPC);
        }

        talkPanel.SetActive(isAction); //대화창 활성화 상태에 따라 대화창 활성화 변경
    }
    
//실제 대사들을 UI에 출력하는 함수     
void Talk(int id, bool isNPC){

		//talkManager에 생성한 대사를 반환하는 함수를 이용하여 대사 한줄을 입력받음 
        string talkData = talkManager.GetTalk(id, talkIndex);

		//입력받은 대사를 이용하여 출력 
        if(isNPC){ 
            UITalkText.text = talkData;
        }else{
            UITalkText.text = talkData;
        }
    }

일반 오브젝트들은 거의 한줄의 대사만을 반환하지만,

NPC들은 여러 대사를 하므로 둘을 if문으로 분리하여 작성합니다 (일단은 이렇게)

 

 

TalkManager를 드래그하여 초기화 해줍니다. 

 

한 문장 까지만 출력이 가능 

 

이제 대화를 전부 끝마쳐야만 isAction이 false가 되도록 함수를 변경하여봅시다.

그렇다면 더이상 Action함수에서 관할할 일이 아니므로 함수를 다음과 같이 변경합니다.

    public void Action(GameObject scanObj)
    {

            scanObject = scanObj;
            //UITalkText.text = "이것은 "+scanObject.name+"이다.";
            ObjData objData = scanObject.GetComponent<ObjData>();
            Talk(objData.id,objData.isNPC);
        

        talkPanel.SetActive(isAction); //대화창 활성화 상태에 따라 대화창 활성화 변경
    }

 

 

일단 TalkManager에서 대사를 반환하는 GetTalk함수가 

대사가 남아있을 때는 다음의 대사를, 대사가 남아있지 않을 때는 null을 반환하도록 만듭시다.

 

TalkManager.cs

    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {
        if(talkIndex==talkData[id].Length) //해당 id를 가지는 string배열의 길이와 같음 
            return null;
        else
            return talkData[id][talkIndex]; //해당 아이디의 해당하는 대사를 반환 
    }

 

이제 GameManager.cs의 반환받은 대사를 실제 UI에 출력하는 함수인 Talk에서

null을 반환받으면 isAction을 false로 변경하고, talkIndex를++ 시켜 

스페이스를 누르면(Action함수 실행) 다음 index의 대사를 받아올 수 있도록 변경합시다.

    void Talk(int id, bool isNPC){

        string talkData = talkManager.GetTalk(id, talkIndex);

        if(talkData == null) //반환된 것이 null이면 더이상 남은 대사가 없으므로 action상태변수를 false로 설정 
        {
            isAction = false;
            talkIndex=0; //talk인덱스는 다음에 또 사용되므로 초기화해야함 
            return; //void에서의 return 함수 강제종료 (밑의 코드는 실행되지 않음)
        }

        if(isNPC){
            UITalkText.text = talkData;
        }else{
            UITalkText.text = talkData;
        }

        //다음 문장을 가져오기 위해 talkData의 인덱스를 늘림
        isAction=true; //대사가 남아있으므로 계속 진행되어야함 
        talkIndex++;
    }

 

 

스페이스를 누르면 다음 대사로 넘어감

 

 

 

+추가) 

평범한 NPC를 가졌다면 상관 없지만, 우리의 NPC는 감옥에 갇혀서 raycast에 잡히지 않는다.. 

이런일은 여기서 밖에 발생하지 않으니까 수동으로 연동해주자 

 

 

초상화

 

Talia초상화 사용 귀찮아서 표정같은건 없다...

그대신 나는 두명이 번갈아가면서 대화해야해서 두개를 넣었다 

 

 

이미지를 이용해 UI를 추가해주고 앵커로 적절하게 위치를 조절해준다

 

 

GameManager.cs에 UI변수를 추가해줍니다.

 

TalkManager.cs에 두개의 변수를 추가해준다.

초상화를 저장할 배열 / id당 초상화데이터를 저장할 Dictionary

    Dictionary<int, Sprite> portraitData;
    public Sprite[] portraitArr;

 

초상화 배열을 사용할 초상화를 드래그하여 초기화해준다 (난 2개추가)

 

    void GenerateData()
    {
        //대사 생성 
        //생략



        //초상화 생성
        portraitData.Add(1000 + 0, portraitArr[0]); //0번 인덱스에 저장된 초상화를 id = 1000과 mapping
        portraitData.Add(1000 + 1,portraitArr[1]); //1번 인덱스에 저장된 초상화를 id = 1001과 mapping
    }

여러가지 표정이 있으시다면 1000+n을 이용해 표정을 전부 넣으시면됩니다. 

 

TalkManager.cs에서 초상화를 반환하는 함수를 만들어줍니다.

    public sprite GetPortrait(int id, int portraitIndex)
    {
        //id는 NPC넘버 , portraitIndex : 표정번호(?)
        return portraitData[id+portraitIndex];  
    }

portraitIndex는 감이 오실탠데.. 해당 캐릭터의 다른 표정이나 등등이 있으면 더해주면됩니다.

이때, potraitIndex는 모든 문장 하나하나와 mapping이 되어야 하므로,

NPC가 가진 대사 뒤에 :를 이용하여 mapping해줍니다. 

 

저는 표정이 아닌 대화형식으로 할 계획이고

0 : 탈리아

1 : 백설공주

로 계획해서 대사를 수정해 주도록 하겠습니다.

 talkData.Add(1000, new string[]{"엥? 이런곳에 사람이?!?!:1","저기요! \n저 좀 도와주세요:0","왜 여기 갇혀 계세요?:1","저도 잘 모르겠어요. \n저희 아버지가 좀 이상해서요:0","제가 어떻게 해드리면 될까요?:1","몬스터들이 열쇠를 가지고 있어요. \n그것을 구해다 주시면 제가 좋은 기술을 알려드릴게요!:0","네! 잠시만 기다리세요:1"});

이렇게 수정했습니다.

 

이제 문장은 "구분자" ":"를 기준으로 2개로 나누어 집니다.

이를 구분하면 배열로 들어가게 되는데 무조건 2개이므로, 길이가 2인 배열로 나누어져

index0은 대사 , index 1은 표정번호(위에서 portraitIndex)가 됩니다.

    void Talk(int id, bool isNPC){

        string talkData = talkManager.GetTalk(id, talkIndex);

        if(talkData == null) //반환된 것이 null이면 더이상 남은 대사가 없으므로 action상태변수를 false로 설정 
        {
            isAction = false;
            talkIndex=0; //talk인덱스는 다음에 또 사용되므로 초기화해야함 
            return; //void에서의 return 함수 강제종료 (밑의 코드는 실행되지 않음)
        }

        if(isNPC){
            UITalkText.text = talkData.Split(':')[0]; //구분자로 문장을 나눠줌  0: 대사 1:portraitIndex
            portraitImg.sprite = talkManager.GetPortrait(id,int.Parse(talkData.Split(':')[1]));
            
            //초상화를 보이게함 (투명도 1)
            portraitImg.color = new Color(1,1,1,1);

        }else{
            UITalkText.text = talkData; //별도의 구분자가 없으므로 그냥 출력가능 
            
            //초상화를 안보이게함(투명도 0) 
            portraitImg.color = new Color(1,1,1,0);
        }

        //다음 문장을 가져오기 위해 talkData의 인덱스를 늘림
        isAction=true; //대사가 남아있으므로 계속 진행되어야함 
        talkIndex++;
    }

다음과 같이 표현할 수 있겠네요 

 

 

 

다음과 같이 변경되었습니다.

원래는 여기까진데, 나는 필요한게 더 있어서 일단 코드 전문을 올린 후 해결해 보도록 하겠다.

 

 

ObjData.cs

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

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

 

TalkManager.cs

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

public class TalkManager : MonoBehaviour
{
    Dictionary<int, string[]> talkData;
    Dictionary<int, Sprite> portraitData;
    public Sprite[] portraitArr;

    // Start is called before the first frame update
    void Awake()
    {
        talkData = new Dictionary<int, string[]>();
        portraitData = new Dictionary<int, Sprite>();
        GenerateData();
    }

    void GenerateData()
    {
        //대사 생성 
        talkData.Add(1000, new string[]{"엥? 이런곳에 사람이?!?!:1","저기요! \n저 좀 도와주세요:0","왜 여기 갇혀 계세요?:1","저도 잘 모르겠어요. \n저희 아버지가 좀 이상해서요:0","제가 어떻게 해드리면 될까요?:1","몬스터들이 열쇠를 가지고 있어요. \n그것을 구해다 주시면 제가 좋은 기술을 알려드릴게요!:0","네! 잠시만 기다리세요:1"});
        talkData.Add(100,new string[]{"쇠로 만들어진 감옥이다.","열쇠없이는 열 수 없는 것 같다."});
        talkData.Add(200,new string[]{"평범한 문이다. \n들어갈 수 있을 것 같다"});



        //초상화 생성
        portraitData.Add(1000 + 0, portraitArr[0]); //0번 인덱스에 저장된 초상화를 id = 1000과 mapping
        portraitData.Add(1000 + 1,portraitArr[1]); //1번 인덱스에 저장된 초상화를 id = 1001과 mapping
    }

    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {
        if(talkIndex==talkData[id].Length)
            return null;
        else
            return talkData[id][talkIndex]; //해당 아이디의 해당
    }

    public Sprite GetPortrait(int id, int portraitIndex)
    {
        //id는 NPC넘버 , portraitIndex : 표정번호(?)
        return portraitData[id+portraitIndex];  
    }
}

 

GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{

    //스크립트 
    public PlayerMove player;
    public TalkManager talkManager;

    //점수와 스테이지 이동관리하는 오브젝트(클래스)

    public int totalPoint;
    public int stagePoint;
    public int stageIndex;
    public int health;
    public GameObject[] Stages; //스테이지를 오브젝트로 만들었기 떄문에 오브젝트 배열로 관리가능 


    //UI 변수
    public Image[] UIhealth; //이미지는 3개이므로 배열 
    public Text UIPoint;
    public Text UIStage;
    public GameObject UIRestartBtn;



    //대화창 
    public GameObject talkPanel;
    public Text UITalkText;
    public Image portraitImg;
    public GameObject scanObject;
    public bool isAction; //대화창 활성화 상태 
    public int talkIndex;


    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        UIPoint.text = (totalPoint + stagePoint).ToString();
    }


    public void NextStage(){

        if (stageIndex < Stages.Length-1){   // 마지막 스테이지 아닌 경우 -> 다음스테이지로 

            Stages[stageIndex].SetActive(false);
            stageIndex++; //스테이지 증가 
            Stages[stageIndex].SetActive(true); //다음 스테이지 활성화

            PlayerReposition(); //시작위치에서 플레이어를 태어나게?하는 함수 


            UIStage.text = "STAGE "+(stageIndex + 1);

        }
        else{ //마지막 스테이지인 경우 ->게임끝 

            //플레이어 컨트롤 막기 
            Time.timeScale = 0; //플레이어가 이동되지 않게 함 

            //결과출력 
            Debug.Log("게임 클리어");

            //UI 다시시작버튼 
            Text btnText = UIRestartBtn.GetComponentInChildren<Text>();
            btnText.text = "GameClear!";
            UIRestartBtn.SetActive(true);


        }


        //Calculate point
        totalPoint += stagePoint; // 얻은 지역포인트 전체점수에 포함시키기 
        stagePoint = 0; //지역 포인트 초기화
    }

    private void OnTriggerEnter2D(Collider2D other) {
        
        if(other.gameObject.tag == "Player"){

            //체력감소
            HealthDown(); //경계에 부딪힌게 플레이어면 health 감소


            if(health>1){
            //떨어진 위치에서 플레이어 재생성 -> 플레이어가 죽지 않았을 떄만 해야함 
                PlayerReposition();
            }
        } 
    }

    public void HealthDown(){

        if(health > 1) {//생명이 0보다 크면 단순 생명 감소
            health--;
            UIhealth[health].color = new Color(1,0,0,0.3f);
        }
        else{// 생명이 0이하면 죽음

            //플레이어가 죽는 모션(이패트)
            player.OnDie();

            //UI에 결과 출력
            Debug.Log("죽었습니다.");

            //다시 시작 버튼 
            UIRestartBtn.SetActive(true);

            //죽었을 경우 모든 UI가 사라지도록 해야함 -> All Health UI Off
            UIhealth[0].color = new Color(1,0,0,0.3f);

        }
    }

    void PlayerReposition(){
            
            player.VelocityZero();
            player.transform.position = new Vector3(-12,-2,-1); //플레이어의 시작위치로 되돌아오기
    }


    public void Restart(){ //재시작이므로 처음부터 다시시작이라 scene 0번 
        
        Time.timeScale = 1; //플레이어가 다시 움직일 수 있도록 함 
        SceneManager.LoadScene(0);
    }
    
    public void Action(GameObject scanObj)
    {

            scanObject = scanObj;
            //UITalkText.text = "이것은 "+scanObject.name+"이다.";
            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) //반환된 것이 null이면 더이상 남은 대사가 없으므로 action상태변수를 false로 설정 
        {
            isAction = false;
            talkIndex=0; //talk인덱스는 다음에 또 사용되므로 초기화해야함 
            return; //void에서의 return 함수 강제종료 (밑의 코드는 실행되지 않음)
        }

        if(isNPC){
            UITalkText.text = talkData.Split(':')[0]; //구분자로 문장을 나눠줌  0: 대사 1:portraitIndex
            portraitImg.sprite = talkManager.GetPortrait(id,int.Parse(talkData.Split(':')[1]));
            portraitImg.color = new Color(1,1,1,1);

        }else{
            UITalkText.text = talkData;
            
            portraitImg.color = new Color(1,1,1,0);
        }

        //다음 문장을 가져오기 위해 talkData의 인덱스를 늘림
        isAction=true; //대사가 남아있으므로 계속 진행되어야함 
        talkIndex++;
    }


}

 

 

 

내가 하고 싶은건, 

Object와 NPC text창을 나누기 / NPC일 때는 이름까지 표시하기

 

UI에 이름 표시

 

새로 Name이라는 UI Txt 오브젝트를 생성하고 크기를 조절한다

 

이에 맞춰 대사 txt도 크기를 조절 

 

    public Text UINameText;

드래그해서 초기화 필수 

 

public string name;    
    
    public void Action(GameObject scanObj)
    {

            scanObject = scanObj;
            name = scanObject.name;
            //UITalkText.text = "이것은 "+scanObject.name+"이다.";
            ObjData objData = scanObject.GetComponent<ObjData>();
            Talk(objData.id,objData.isNPC);
        

        talkPanel.SetActive(isAction); //대화창 활성화 상태에 따라 대화창 활성화 변경
    }

이름을 저장하는 name변수를 만들어

Action을 실행할 때 마다 scanObject의 name을 저장하게 한다

 

Talk함수의 NPC일 때의 부분을 다음과 같이 변경 

if(isNPC){
            UITalkText.text = talkData.Split(':')[0]; //구분자로 문장을 나눠줌  0: 대사 1:portraitIndex
            portraitImg.sprite = talkManager.GetPortrait(id,int.Parse(talkData.Split(':')[1]));

            if(int.Parse(talkData.Split(':')[1])==1) 
            {
                UINameText.text = "백설공주";//1인경우 백설공주 초상화가 저장되어 무조건 이거임...
            }
            else
            {
                UINameText.text = name; //나머지일 때는 이름 UI에 미리 저장해둔 name출력 
            }
         
         }

 

 

 

 

 

초상화를 넣으니

일반적인 Object는 조금 이상하다.

그냥 다른 텍스트UI를 사용하자

 

Info라는 일반 Object용 txt를 만든다

Left 15 / Pos Y 15로 변경 

 

변수를 만들고 드래그로 초기화 

    public Text UIInfoText; 

 

 

다른 용도로 쓰이는건 공백처리로 만든다

        if(isNPC){
            UIInfoText.text = "";
            UITalkText.text = talkData.Split(':')[0]; //구분자로 문장을 나눠줌  0: 대사 1:portraitIndex
            portraitImg.sprite = talkManager.GetPortrait(id,int.Parse(talkData.Split(':')[1]));

            if(int.Parse(talkData.Split(':')[1])==1)
            {
                UINameText.text = "백설공주";
            }else{
                UINameText.text = name;
            }


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

        }else{
            UINameText.text="";
            UITalkText.text ="";
            UIInfoText.text = talkData;
            
            portraitImg.color = new Color(1,1,1,0);
        }

 

해결 

 

 

 

창살에 들어있는 NPC를 인식하는건 나중으로 미룸

 

<참고영상>

youtu.be/qJjfYvEYKiE

 

 

728x90

'GAME' 카테고리의 다른 글

[Unity2D] 퀘스트 시스템 구현  (0) 2020.08.31
[Unity2D]조사액션 / 조사창 구현  (0) 2020.08.28
[Unity2D] 기초2D게임 만들기  (0) 2020.08.07
[Unity2D] 적 몬스터 구현하기  (1) 2020.08.01
[Unity2D] 타일맵 Platform 만들기  (0) 2020.08.01