일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 멋쟁이사자처럼
- 드로잉
- 서포터즈
- 노하우
- 에이세프라이트
- 연습
- menu
- photoshop
- 포토샵
- TOOL
- layer
- 반환원정대
- COSMO
- 스마일게이트
- 개발
- 장학팀
- 도트공부
- 픽셀 아트
- pixel art
- 애니메이션
- 도트
- 픽셀아트
- 인디게임 개발
- 드로잉 연습
- 채색
- Aseprite
- 모작
- 자원순환보증금관리센터
- 기초
- Pixelart
- Today
- Total
소소한 나의 하루들
탑다운 2d RPG(6) - 대화 애니메이션 느낌있게 만들기 본문
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
우리는 대화 시스템, 퀘스트 시스템을 다 만들었다. 이제 여기에 대화 애니메이션을 느낌있게 만들어본다.
#1. 대화창 이펙트
우선 대화창 애니메이션부터 만들어본다.
Canvas UI 오브젝트 아래, 대화창 이미지(Panel UI) 오브젝트에 Animator 컴포넌트를 추가하고, Assets>Animation>Controller 폴더 안에 Panel Animation Controller 파일을 생성하여 컴포넌트에 드래그 적용시킨다.
그리고 빈 State를 Animator 창에서 생성한다. 그리고 애니메이션을 보여줄지 여부를 결정할 수 있도록 bool형 매개변수를 생성한다. (IsShow) Animation 폴더 안에서 애니메이션 파일을 2개(Talk Show/Talk Hide) 생성해서 Animator에 드래그한다. : 빈 State 1개, Show State, Hide State (총 State 3개) [보여주기 ↔ 숨기기]
맨 처음 IsShow 매개변수가 true가 되면서 Talk Show 애니메이션이 실행되고, IsShow가 false가 되면 Talk Hide 애니메이션으로 전환되고, true가 되면 다시 Talk Show 애니메이션으로 전환되도록 한다.
그리고 대화 UI를 화면 아래로 내려서 안보이게 설정한다. (꼼수, y축으로 -500 이동) 그리고 Animation창에서 Add Property해서 Talk Show에서 Rect Transform>Anchored Position 해서 y -500에서 y 20(원래위치)로 오도록 한다.
: 두 거리를 오고 가는 애니메이션 설정 (Talk Hide 애니메이션은 반대로 설정)
//Game Manager
public class GameManager : MonoBehaviour
{
public TalkManager talkManager;
public QuestManager questManager;
//public GameObject talkSpace;
public Animator talkSpace;
public TextMeshProUGUI talkText;
public Image portraitImg;
public GameObject scanObject;
public bool isAction;
public int talkIndex;
private void Awake()
{
//talkSpace.SetActive(false);
talkSpace.SetActive("IsShow", false);
}
...
public void Action(GameObject scanObj)
{
//Get Current Object
scanObject = scanObj;
ObjectData objData = scanObject.GetComponent<ObjectData>();
Talk(objData.id, objData.isNPC);
//Visible Talk for Action
//talkSpace.SetActive(isAction);
talkSpace.SetBool("IsShow", isAction);
}
...
}
이렇게 애니메이션을 만들었고, 이제 스크립트 코드에서 매개변수를 관리해준다.
지금까지는 Object 레이어를 가진 NPC 혹은 오브젝트와 대화를 시작하면 GameObject 변수로 가져온 talkSpace UI 오브젝트를 활성화시켰다. 논리값을 가진 bool형 변수 isAction을 넣어서 SetActive() 함수를 통해 조절.
그런데 이제는 해당 코드들을 지우고, 매개변수를 통해 애니메이션을 활성화시키도록 한다.
변수 타입을 GameObject에서 Animator로 교체했으므로 Type mismatch가 생겼을텐데, 다시 Animator 컴포넌트를 갖고있는 Panel UI 오브젝트를 드래그하여 재할당해준다.
#2. 초상화 이펙트
NPC마다 초상화가 4가지 있는데, 초상화가 바뀌면, 초상화가 아래로 살짝 내려갔다 올라가는 애니메이션을 만들어본다.
Portrait UI에 Animator 컴포넌트를 추가하고, Animation 폴더 내에 Portrait Animator Controller를 생성하여 컴포넌트에 적용시킨다.
그리고 Animation Clip 파일(PortraitEffect)을 생성하고 Animator창에 드래그하여 적용시키고, Empty State와 상호 연결한다.
처음 한번만 실행하면 되니까 매개변수는 Trigger형으로 생성한다. Empty에서 PortraitEffect로 전환할 때는 Has Exit TIme을 끄고, 반대 전환일 때는 키도록 한다. (Has Exit Time을 키면 매개변수 조건(Condition)을 넣지 않아도 된다)
그러면 애니메이션이 한바퀴 돌고 알아서 Empty로 빠져나온다.
그리고 초상화가 살짝 아래로 내려갔다 올라갈 수 있게 y축으로 -10 정도만 내려갔다 다시 원위치로 올라가도록 프레임을 구성한다.
//GameManager
public class GameManager : MonoBehaviour
{
public TalkManager talkManager;
public QuestManager questManager;
public Animator talkSpace;
public Animator portraitAnime;
public Sprite prePortrait;
public TextMeshProUGUI talkText;
public Image portraitImg;
public GameObject scanObject;
public bool isAction;
public int talkIndex;
...
void Talk(int id, bool isNPC)
{
//Set Talk Data
int questTalkIndex = questManager.GetQuestTalkIndex(id);
string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
//End Talk
if (talkData == null)
{
isAction = false;
talkIndex = 0;
questManager.CheckQuest(id);
Debug.Log(questManager.CheckQuest(id));
return;
}
//Continue Talk
if (isNPC)
{
talkText.text = talkData.Split(':')[0];
//Show Portrait
portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
portraitImg.color = new Color(1, 1, 1, 1);
if(prePortrait != portraitImg.sprite)
{
portraitAnime.SetTrigger("DoEffect");
prePortrait = portraitImg.sprite;
}
}
else
{
talkText.text = talkData;
portraitImg.color = new Color(1, 1, 1, 0);
}
isAction = true;
talkIndex++;
}
...
}
설정은 다 됐고, 스크립트에서 Animator의 매개변수를 조절해준다.
초상화 애니메이션(PortraitEffect)는 언제 실행할지 생각해보면, 이전 초상화와 모양이 다를 때 실행될 수 있도록 하고싶다.
그렇게 하려면 과거 초상화 sprite를 Sprite 변수(prePotrait)에 저장해두고, 비교 후 다르다면 애니메이션을 실행해야한다.
#3. 타이핑 이펙트
이건 애니메이션으로 하는게 아니라, 스크립트로 구현을 해야한다.
타이핑 이펙트를 구현하기위해 TypeEffect 스크립트를 생성하고, Text UI에 드래그 적용시킨다.
타이핑 이펙트에 필요한 것은 원래 대사이다. 따라서 표시할 대화 문자열을 따로 변수로 저장한다. (targetMsg)
그리고 글자가 하나씩 입력되어야하는데, '1초에 몇글자가 입력되어야하는가' Character per Seconds(CPS)라는 속도에 대한 것도 고려해야한다. 따라서 글자 재생 속도를 위한 int형 변수(CharPerSeconds)를 생성한다.
대화 문자열 변수(targetMsg)를 받는 함수를 생성한다. (SetMsg())
그리고 애니메이션 재생을 위한 시작 - 재생 - 종료 3개의 함수를 생성한다. (EffectStart() / Effecting() / EffectEnd())
*이렇게 애니메이션을 코드로 처리할 때는 시작 - 재생 - 종료 3가지 함수를 생성하는 것이 좋다.
시작할 때는 텍스트를 싹 비워놔야한다. 'Text UI가 가지고 있는 텍스트를 가지고 와서 비워야겠다'
따라서 Text UI 변수 생성 및 초기화 후, 시작함수에서 공백처리한다.
인덱스도 하나 필요하므로 인덱스 변수를 만들어둔다. (글자를 한 글자씩 보여줄 수 있게) 그래서 인덱스 변수도 처음 시작함수에서는 0으로 설정한다.
재생함수에서는 인덱스 변수가 가산되면서 한 글자씩 보여줄 수 있도록 시간차를 두고 반복적으로 실행해줘야한다.
: 시간차 반복 호출을 위해 Invoke() 함수나 코루틴을 사용
Invoke("Effecting", 1/CharPerSeconds);
1초/CPS = 1글자가 나오는 딜레이
문자열도 배열처럼 인덱스를 사용해서 char 값에 접근 가능하다. 문자열[인덱스]
이전에 추가한 Cursor UI는 대사가 다 출력된 후에 나와줘야한다. 따라서 처음에는 꺼져있어야한다.
public class TypeEffect : MonoBehaviour
{
public int CharPerSeconds;
string targetMsg;
TextMeshProUGUI msgText;
public GameObject EndCursor;
int index;
public void SetMsg(string msg)
{
targetMsg = msg;
EffectStart();
}
void EffectStart()
{
msgText.text = "";
index = 0;
EndCursor.SetActive(false);
Invoke("Effecting", 1 / CharPerSeconds);
}
void Effecting()
{
if(msgText.text == targetMsg)
{
EffectEnd();
return;
}
msgText.text += targetMsg[index];
index++;
Invoke("Effecting", 1 / CharPerSeconds);
}
그러면 이제 GameManager 스크립트에 SetMsg() 함수만 작성하면 될 것이다.
//GameManager
//public TextMeshProUGUI talkText;
public TypeEffect talk;
void Talk(int id, bool isNPC)
{
//Set Talk Data
int questTalkIndex = questManager.GetQuestTalkIndex(id);
string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
...
if (isNPC)
{
//talkText.text = talkData.Split(':')[0];
talk.SetMsg(talkData.Split(':')[0]);
...
}
else
{
talkText.text = talkData;
talk.SetMsg(talkData);
...
}
}
전에는 talkText.text에 직접 대사를 넣어줬다. 이제는 직접 붙이지 않고, talkText UI에 자체적으로 talkEffect 스크립트가 들어있으니 TypeEffect의 SetMsg() 함수를 통해서 글자를 붙여넣어본다.
따라서 기존에 사용하던 TextMeshPorUGUI로 선언한 talkText 변수를 TypeEffect (이펙트 스크립트)로 바꿔서 선언한다.
이렇게 코드를 조금씩 단계적으로 업그레이드 해나가면 된다.
//TypeEffect
public class TypeEffect : MonoBehaviour
{
public int CharPerSeconds;
string targetMsg;
TextMeshProUGUI msgText;
public GameObject EndCursor;
int index;
float interval;
private void Awake()
{
msgText = GetComponent<TextMeshProUGUI>();
}
public void SetMsg(string msg)
{
targetMsg = msg;
EffectStart();
}
void EffectStart()
{
msgText.text = "";
index = 0;
EndCursor.SetActive(false);
//Start Animation
interval = 1.0f / CharPerSeconds;
Debug.Log(interval);
Invoke("Effecting", interval);
}
void Effecting()
{
if(msgText.text == targetMsg)
{
EffectEnd();
return;
}
msgText.text += targetMsg[index];
index++;
Invoke("Effecting", interval);
}
실행시켜보면, CharPerSeconds를 10으로 줬더니, 타이핑되는 속도가 너무 빠르다.
: 1 / CharPerSeconds로 했더니 0으로 계산되어서 바로바로 타이핑됐다.
그래서 속도를 줄이기 위해 float형 변수 interval(시간간격)을 주었다. 확실한 소수 값을 얻기 위해 분자 1.0f를 CharPerSeconds로 나눴다.
그리고 확인해보기위해 Debug.Log()로 출력해본다. : 0.1나옴
Invoke()에 두번째 변수(시간초)를 입력할 때는 바로 값을 집어넣기보다 확실하게 계산을 해서 변수를 만들고 집어넣는 것이 문제를 줄일 수 있다. 사실 그냥 1.0f / CharPerSeconds로 바로 집어넣어줘도 괜찮았을 것이다. (계산하는 인자의 타입으로 발생한 문제니까)
#4. 타이핑 사운드 이펙트
이전에 활용했던 사운드 에셋을 다시 import한다.
https://assetstore.unity.com/packages/audio/sound-fx/free-casual-game-sfx-pack-54116
Text UI에 Audio Source 컴포넌트를 추가하고, AudioClip에 적당한 음원 에셋파일을 적용시킨다. Play On Awake는 체크해제한다.
//TypeEffect
public class TypeEffect : MonoBehaviour
{
public int CharPerSeconds;
public GameObject EndCursor;
string targetMsg;
TextMeshProUGUI msgText;
AudioSource audioSource;
int index;
float interval;
private void Awake()
{
msgText = GetComponent<TextMeshProUGUI>();
audioSource = GetComponent<AudioSource>();
}
public void SetMsg(string msg)
{
targetMsg = msg;
EffectStart();
}
void EffectStart()
{
msgText.text = "";
index = 0;
EndCursor.SetActive(false);
//Start Animation
interval = 1.0f / CharPerSeconds;
Debug.Log(interval);
Invoke("Effecting", interval);
}
void Effecting()
{
if(msgText.text == targetMsg)
{
EffectEnd();
return;
}
msgText.text += targetMsg[index];
//Sounding
if (targetMsg[index] != ' ' || targetMsg[index] != '.')
audioSource.Play();
index++;
Invoke("Effecting", interval);
}
TypeEffect 스크립트에서 AudioSource 변수를 생성하고 초기화한 뒤, 재생함수에서 Play()한다.
※그런데, 여기서 공백, 특수기호(마침표)는 사운드를 실행시키고 싶지 않다. 순수하게 글자만 나올 때 사운드를 실행시키고 싶다. [공백, 마침표는 사운드 재생 제외]
이제 실행시키면 대사가 타이핑되면서 사운드까지 출력된다. 이렇게 사운드 시스템까지 완성되었다.
타이핑 사운드는 자극적이지 않은 계속 들어도 편안한 사운드가 되어야한다.
#5. 대사 타이핑 중 애니메이션 스킵
대사 타이핑이 되고있는 도중에 스페이스바(Jump키)를 누르면 다음 대사로 넘어가거나 그냥 대사창이 내려가버린다.
보통 대사가 타이핑되는 도중에 yes키를 누르면 대사가 넘어가거나, 대사창이 내려가서 끝나는게 아니라 대사를 다 끝내줘야한다. (대사를 다 채우고 Cursor 이미지까지 나와야한다)
//TypeEffect
public void SetMsg(string msg)
{
if (isAnime)
{
CancelInvoke();
msgText.text = targetMsg;
EffectEnd();
}
else
{
targetMsg = msg;
EffectStart();
}
}
'이펙트가 실행되고 있는 도중' 이라는 것을 알아야하기 때문에 애니메이션 실행 판단을 위한 플래그 변수(isAnime)를 생성한다.
: SetMsg()를 isAnime가 true인 상태(=텍스트 타이핑 애니메이션 진행중)에서 날렸다면...
플래그 변수를 이용하여 분기점 로직 작성
Invoke("Effecting", interval);도 종료시켜야하고, EffectEnd()를 실행시켜야한다. 그런데 글자가 아직 덜 입력됐을텐데 바로 대사를 전부 입력시킨다.
※이제 SetMsg()에서는 (isAnime 변수를 통해) 타이핑 도중을 판단하여 Invoke()를 종료시키고, 대사를 전부 화면에 띄우고, EffectEnd()를 실행시키는 것까지 자체적으로 처리할 수 있다.
그리고 스페이스바(Jump키)를 눌렀을 때 PlayerAction 스크립트에서 GameManager의 Action() 함수를 호출하고, Action()함수는 다시 (대화창/대화내용, 초상화를 띄우는 역할을하는) Talk()함수를 실행시킨다.
void Talk(int id, bool isNPC)
{
//Set Talk Data
int questTalkIndex = questManager.GetQuestTalkIndex(id);
string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
여기서 문제는 GameManager에서 GetQuestTalkIndex를 실행시켜서 그 다음 코너의 대사로 넘어가버린다. 이걸 실행하면 안된다.
//GameManager
void Talk(int id, bool isNPC)
{
int questTalkIndex = 0;
string talkData = "";
//Set Talk Data
if (talk.isAnime)
{
talk.SetMsg("");
return;
}
else
{
questTalkIndex = questManager.GetQuestTalkIndex(id);
talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
}
//End Talk
if (talkData == null)
{
isAction = false;
talkIndex = 0;
questManager.CheckQuest(id);
Debug.Log(questManager.CheckQuest());
return;
}
//Continue Talk
if (isNPC)
{
talk.SetMsg(talkData.Split(':')[0]);
//Show Portrait
portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
portraitImg.color = new Color(1, 1, 1, 1);
if(prePortrait != portraitImg.sprite)
{
portraitAnime.SetTrigger("DoEffect");
prePortrait = portraitImg.sprite;
}
}
else
{
talk.SetMsg(talkData);
portraitImg.color = new Color(1, 1, 1, 0);
}
isAction = true;
talkIndex++;
}
TypeEffect의 isAnime를 public으로 선언하고, GameManager의 Talk()에서 다음 코너의 대사로 넘기기 전에 분기점을 작성한다. [스페이스바를 눌렀을 때 다음 대사가 실행되는것이 아니라, 타이핑 애니메이션이 skip된 입력이 다 된 현재 대사가 나와야 하므로]
문제상황 - 피드백
//TypeEffect
public class TypeEffect : MonoBehaviour
{
public int CharPerSeconds;
string targetMsg;
TextMeshProUGUI msgText;
public GameObject EndCursor;
int index;
private void Awake()
{
msgText = GetComponent<TextMeshProUGUI>();
}
...
}
반드시 public으로 에디터에서 직접 드래그 적용하여 초기화시키지 않은 외부 클래스 변수들은 코드 내에서라도 초기화해주었는지 확인해줘야한다. (초기화가 제대로 이루어지지 않으면 참조 에러(Null Reference)가 발생한다)
'개발 > 유니티' 카테고리의 다른 글
탑다운 2d RPG(8) - 모바일 UI & 안드로이드 빌드 (0) | 2024.02.05 |
---|---|
탑다운 2d RPG(7) - 서브 메뉴와 저장기능 만들기 (0) | 2024.02.04 |
탑다운 2d RPG(5) - 퀘스트 시스템 구현하기 (0) | 2024.02.01 |
탑다운 2d RPG(4) - 대화 시스템 구현하기 (0) | 2024.01.31 |
탑다운 2d RPG(3) - 대화창 UI 구축하기 (0) | 2024.01.30 |