일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 드로잉
- COSMO
- Pixelart
- 반환원정대
- 서포터즈
- 장학팀
- 스마일게이트
- TOOL
- 도트
- 포토샵
- 드로잉 연습
- 기초
- 노하우
- 픽셀아트
- photoshop
- 모작
- 자원순환보증금관리센터
- layer
- 픽셀 아트
- 인디게임 개발
- menu
- 애니메이션
- pixel art
- 에이세프라이트
- 연습
- Aseprite
- 도트공부
- 멋쟁이사자처럼
- 채색
- 개발
- Today
- Total
소소한 나의 하루들
탑다운 2d RPG(4) - 대화 시스템 구현하기 본문
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
대화창에 띄울 대화 내용을 체계적으로 관리하는 '대화 시스템'을 구현해본다.
지난 시간까지 한 것은 Action 버튼(Jump키) 누르면, 오브젝트의 이름을 가져와서 대화창에서 나타내는 것이다. 단순하게 이름만 불러왔다. 이제는 오브젝트가 어떤 것인지 인식을 하고, 데이터로 정해놓은 대사를 대화창에 띄워본다.
#1. 오브젝트 관리
우선 인지한 사물이 NPC인지, 일반 사물인지, 오브젝트의 ID는 어떻게 되는지 정의해준다.
우선 오브젝트를 관리하는 ObjectData 스크립트를 만들어주고, 오브젝트 ID와 NPC 여부를 판단하는 변수를 생성한다.
그리고 맵에 만들어놨던 오브젝트들(NPC 포함)에게 ObjectData를 드래그 적용시켜주고, 각 오브젝트마다 고유한 ID를 지정해준다.
NPC들에게는 ID 1000 이상을 부여하고, Is NPC를 체크한다. NPC가 아닌 오브젝트들에게는 100번대의 ID를 부여하고 Is NPC를 체크하지 않는다. (NPC A: 1000 NPC B: 2000 / House: 100 Tree: 200 Desk: 300 Box: 400)
#2. 대화 시스템
대화 내용을 만들어주고 대화 데이터를 관리해주는 Manager를 만들어준다. Project 창에서 TalkManager 오브젝트를 만들어준다. Hierarchy 창에서 Talk Manager 오브젝트도 만들어주고, 스크립트를 적용시켜준다.
이 TalkManager 스크립트에 들어갈 것을 생각해보면
1) 어떤 대사가 들어갈지 저장해야한다. (Dictionary<>)
Dictionary<key 데이터타입,value 데이터타입> 변수명; //선언
변수명 = new Dictionary<key 데이터타입, value 데이터타입>(); //초기화
Dictionary는 데이터 구조가 key와 key에 연결되는 value가 들어간다. 따라서 타입을 꼭 2개 작성해줘야한다.
그리고 데이터를 만들어주는 함수 GenerateData();를 만들어준다. Dictionary에는 Add()를 사용해서 대화 데이터를 추가해준다.
대화 하나에는 여러 문장이 들어있으므로 string[] 배열을 사용한다.
보통 대화할 수 없는 오브젝트들은 플레이어의 독백이 들어간다.
이렇게 대사를 지정했으면, 지정된 대화 문장을 반환하는 함수를 하나 생성한다. 대사를 갖고오기 위해 필요한 인수는 id와, 대사가 문자열을 '배열'로 사용했기 때문에 첫번째 문장을 가져올 것인지 두번째 문장을 가지고 올 것인지 지정해줘야 할 것이다. (id와 문자열(대사) 인덱스)
public class TalkManager : MonoBehaviour
{
Dictionary<int, string[]> talkData;
void Awake()
{
talkData = new Dictionary<int, string[]>();
GenerateData();
}
void GenerateData()
{
talkData.Add(1000, new string[] { "안녕?", "이곳에 처음 왔구나?" });
talkData.Add(400, new string[] { "평범한 나무상자다." });
talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
}
public string GetTalk(int id, int talkIntdex)
{
return talkData[id][talkIntdex];
}
Dictionary 가져올 때는 2차원 배열과 똑같다. 먼저 key값인 id를 통해 대화를 가져오고, value인 talkIndex를 통해 대화 중 한 문장(대사)를 가져온다.
이제 이것을 GameManager에서 활용한다. TalkManager를 변수로 선언하고, 함수를 사용하여 대사를 관리한다.
오브젝트마다 ObjectData라는 스크립트를 적용해서 id와 IsNPC를 지정해줬는데, 이걸 가지고 와야한다. (스캔한 오브젝트에 있는 'ObjectData 스크립트'의 정보를 받아와야함)
public class GameManager : MonoBehaviour
{
public TalkManager talkManager;
public GameObject talkSpace;
public TextMeshProUGUI talkText;
public GameObject scanObject;
public bool isAction;
public int talkIndex;
private void Awake()
{
talkSpace.SetActive(false);
}
public void Action(GameObject scanObj)
{
if (isAction) //Exit Action
{
isAction = false;
}
else //Enter Action
{
isAction = true;
scanObject = scanObj;
ObjectData objData = scanObject.GetComponent<ObjectData>();
Talk(objData.id, objData.isNPC);
}
talkSpace.SetActive(isAction);
}
void Talk(int id, bool isNPC)
{
string talkData = talkManager.GetTalk(id, talkIndex);
if (isNPC)
{
talkText.text = talkData;
}
else
{
talkText.text = talkData;
}
}
}
스캔한 오브젝트의 (ObjectData에 입력되어있는) id와 IsNPC 정보를 인수로 받도록 하고, TalkManager의 GetTalk()는 id에 있는 talkIndex에 위치한 대사 문자열을 반환하니까, 이 대사를 저장할 변수를 지정한다.
그리고 이제 NPC인지 여부를 가려주는 분기점을 만든다.
※플레이어가 인지할 수 있는 Object 레이어를 가진 오브젝트들은 모두 id를 가지고 있다. 따라서 실행시키기 전, id를 갖고있는 모든 (인지할 수 있는) 오브젝트들에 대한 대사를 설정해줘야. NUll Reference 에러가 나타나지 않는다.
void GenerateData()
{
talkData.Add(1000, new string[] { "안녕?", "이곳에 처음 왔구나?" });
talkData.Add(2000, new string[] { "좋은아침?", "밥은 먹었니?" });
talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
talkData.Add(200, new string[] { "평범한 나무다." });
talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
talkData.Add(400, new string[] { "평범한 나무상자다." });
talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });
}
그런데 문제가 생긴다. 분명 NPC의 경우에는 대사를 2개 설정했는데, "안녕?"과 "좋은아침?" 첫번째 대사만 볼 수 있었다.
이전까지는 플레이어가 대화 중에는 액션을 할 수 없도록 했는데, 이제는 NPC와의 좀 더 긴 추가적인 대화를 위해 해당 코드를 수정해주도록 한다.
: 대화가 모두 끝나야 액션이 끝나도록 설정을 바꾼다.
이제 GameManager의 Action()에서 플레이어의 액션을 컨트롤하지는 않고, Talk()에서 작성해주겠다.
NPC A와 NPC B의 대사는 모두 인덱스 0, 1에서 끝난다. 그래서 talkIndex가 1까지 오게되면, 더 이상 대사가 없기 때문에 거기에서 isAction을 false로 놓는다.
talkIndex와 대화의 문장 개수(talkData[id].Length)를 비교하여 끝인지 확인.
//GameManager
void Talk(int id, bool isNPC)
{
string talkData = talkManager.GetTalk(id, talkIndex);
if (talkData == null)
{
isAction = false;
return;
}
if (isNPC)
{
talkText.text = talkData;
}
else
{
talkText.text = talkData;
}
isAction = true;
talkIndex++;
}
//TalkManager
public string GetTalk(int id, int talkIndex)
{
if (talkIndex == talkData[id].Length)
return null;
else
return talkData[id][talkIndex];
}
void로 선언한 함수도 return은 가능하지만, 뒤에 아무것도 작성하지 않아야 한다. (강제종료 역할)
첫번째 대사를 뽑아냈으면 그 다음 대사도 가져오기 위해 talkIndex를 가산시켜준다.
※이렇게 실행시키면 생기는 문제는, 처음 대사는 정상적으로 출력하지만 그 다음 다른 오브젝트 앞에가서 대사를 출력하려고 하면 에러가 발생하는데, 그 이유는 가산된 talkIndex는 2인 상황이고, 새로운 오브젝트로부터 출력해야할 대사는 인덱스 0인 상황이라 참조 에러가 발생하는 것이다.
void Talk(int id, bool isNPC)
{
string talkData = talkManager.GetTalk(id, talkIndex);
if (talkData == null)
{
isAction = false;
talkIndex = 0;
return;
}
따라서 대화가 끝난 뒤에는 talkIndex를 0으로 초기화시켜줘야한다.
#3. 초상화
그렇다면, 왜 IsNPC로 NPC 여부를 구분해준걸까?
보통 쯔꾸르 게임에서는 NPC의 초상화가 대화를 나눌 때 대화창에 같이 나오게 된다.
학습 자료로 제공된 에셋 초상화의 경우 크기를 좀 크게하여 Pixel per Units을 24로 하고, Sprite Editor에서 Slice를 x34 y46 padding x1 y1로 하고 slice했다. (설정 후 Apply 필수)
그리고 대화창 Panel 안에 Image UI 오브젝트(Portrait)를 하나 더 만들고, 거기에 초상화를 넣는다. Anchors를 설정하고 크기가 안맞으면 Set Native Size를 클릭한다.
나머지 텍스트 UI도 초상화 길이에 맞춰서 배치해준다.
(나중에 NPC 대화창 vs 사물 오브젝트 대화창 UI 배치 구분해줄 예정)
GameManager에서 Image UI(Portrait) 접근을 위해 변수를 생성하고, 할당해준다. 그리고 Talk()에서 NPC일 때(IsNPC가 true)만 Image UI가 보이도록 작성한다.
그런데, NPC도 가만히 있을 때 / 이야기할 때 / 웃을 때 / 슬퍼할 때 표정이 다 다르게 있다. 그러면 각 표정의 sprite도 갖고와야한다. Dictionary 클래스를 선언해서 대사와 마찬가지로 portrait을 관리한다.
표정이 4개 있다. 어떻게 해야하나? Dictionary의 key값 뒤에 숫자를 더한다. (key1000은 표정을 4개 갖는다)
Sprite는 어떻게 가져올까? Sprite는 바로 못갖고온다. 따라서 미리 변수에 저장해야한다. (public으로 선언)
*해당 오브젝트의 id가 무엇인지, portrait 표정과 맞는지 판단해서 적용한다.
void GenerateData()
{
talkData.Add(1000, new string[] { "안녕?:1", "이곳에 처음 왔구나?:2" });
talkData.Add(2000, new string[] { "좋은아침?:4", "밥은 먹었니?:6" });
talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
talkData.Add(200, new string[] { "평범한 나무다." });
talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
talkData.Add(400, new string[] { "평범한 나무상자다." });
talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });
portraitData.Add(1000 + 0, portraitArray[0]);
portraitData.Add(1000 + 1, portraitArray[1]);
portraitData.Add(1000 + 2, portraitArray[2]);
portraitData.Add(1000 + 3, portraitArray[3]);
portraitData.Add(2000 + 4, portraitArray[4]);
portraitData.Add(2000 + 5, portraitArray[5]);
portraitData.Add(2000 + 6, portraitArray[6]);
portraitData.Add(2000 + 7, portraitArray[7]);
}
이제 지정된 초상화 sprite를 관리해줄 함수를 생성한다.
NPC의 표정은 대사마다 다를 수 있으므로 문장과 1:1매칭시킨다. 따라서 대사 뒤에 구분자(슬래시, 콜론 등 아무 기호)를 넣고 portraitIndex 값을 넣는다. 이번에는 구분자로 콜론 : 을 입력했다.
이 상태라면 대화창에 텍스트가 출력될 때 안녕?:1 같은 식으로 구분자와 index가 모두 출력될 것 같다.
그래서 portrait을 사용하는 NPC같은 경우 코드를 추가 작업해줘야한다.
Split() : 구분자를 통하여 배열로 나눠주는 문자열 함수
*매개변수로 문자열이 들어간다.
※구분을 지어서 나누게 되면, 그것은 더이상 문자열이 아니고, 문자열의 배열이 된다. 따라서 나눠지게된 배열에서 갖고올 것을 배열처럼 인덱스로 갖고와서 원하는 곳에 대입해주면 된다.
그런데 이렇게 나눠지게된 구분자 뒤의 인덱스 값은 이제 string 타입이 되었다. 따라서 타입.Parse() 함수를 사용한다.
타입.Parse() : 문자열을 해당 타입으로 변환해주는 함수
*순수하게 숫자만 들어있는 문제열은 숫자타입.Pase(문자열)이라고 하면 숫자타입으로 변환되지만, 문자열 내용이 변환하려는 타입과 맞지 않으면 오류가 발생한다.
문제상황 및 피드백
//TalkManager
portraitData.Add(1000 + 0, portraitArray[0]);
portraitData.Add(1000 + 1, portraitArray[1]);
portraitData.Add(1000 + 2, portraitArray[2]);
portraitData.Add(1000 + 3, portraitArray[3]);
portraitData.Add(2000 + 0, portraitArray[4]); //잘못된 코드
portraitData.Add(2000 + 1, portraitArray[5]); //잘못된 코드
portraitData.Add(2000 + 2, portraitArray[6]); //잘못된 코드
portraitData.Add(2000 + 3, portraitArray[7]); //잘못된 코드
public Sprite GetPortrait(int id, int portraitIndex)
{
return portraitData[id + portraitIndex];
}
※ 각 key(id)에 대하여 표정이 4가지 있어서 key에다가 0 / 1 / 2 / 3을 더해주었는데 이렇게 하면 안되고, 더해주는 값은 아래처럼 portraitArray[]의 인덱스와 값을 갖게 설정해줘야한다.
portraitData.Add(1000 + 0, portraitArray[0]);
portraitData.Add(1000 + 1, portraitArray[1]);
portraitData.Add(1000 + 2, portraitArray[2]);
portraitData.Add(1000 + 3, portraitArray[3]);
portraitData.Add(2000 + 4, portraitArray[4]);
portraitData.Add(2000 + 5, portraitArray[5]);
portraitData.Add(2000 + 6, portraitArray[6]);
portraitData.Add(2000 + 7, portraitArray[7]);
https://blog.naver.com/gold_metal/221663095416
이번 강의에서는 이해를 위해 스크립트 안에서 데이터를 만들었지만, 실무에서는 Excel, CSV, Json, Xml 파일로 만들어 관리를 한다는데, 이 부분도 추가적으로 알아봐야겠다.
'개발 > 유니티' 카테고리의 다른 글
탑다운 2d RPG(6) - 대화 애니메이션 느낌있게 만들기 (0) | 2024.02.02 |
---|---|
탑다운 2d RPG(5) - 퀘스트 시스템 구현하기 (0) | 2024.02.01 |
탑다운 2d RPG(3) - 대화창 UI 구축하기 (0) | 2024.01.30 |
탑다운 2d RPG(2) - 쯔꾸르식 액션 구현하기 (0) | 2024.01.30 |
탑다운 2d RPG(1) - 도트 타일맵으로 쉽게 준비하기 (0) | 2024.01.29 |