Unity 2D RPG - 6. 대화 애니메이션 느낌있게 만들기

2023. 8. 16. 07:33C# & Unity 공부

대화창 이펙트

 

캔버스 아래에 Talkset (대화창)에다가 넣을 애니메이션 컨트롤러를 하나 만들고 집어 넣은 뒤에 ,

빈 상태를 하나 만들자.

그리고 보여줄 때와 숨길 때 차이를 두기 위하여 'isShow'라는 bool형 Parameters를 하나 만들자.

 

 

애니메이션 2가지를 만들자.

대화창을 보여주는 'TalkShow'와 대화창을 숨기는 'TalkHide'를 만들자.

보여주는 게 먼저고, 다음 단계가 숨기는 거다. 이것을 반복하기 때문에

Make Transition을 통한 화살표는 아래와 같다.

 

 

애니메이션 때 항상 하는 거지만 Has Exit Time을 꺼주고, TransitionDuration을 0으로 만들어 준다.

그리고 Conditions에 만들어준 Parameter 변수에 맞게 넣어준다.

 

 

이제 애니메이션을 만들어보도록 하겠다.

여태까지 대화창을 껐다가 켰으면, 이제는 대화Ui를 아래로 내려주자. 

 

 

Add Property를 눌러 Anchored Position을 눌러 추가해주자.

 

 

Position Y를 설정하여 올라오도록 애니메이션 만들어주자.

TalkShow : -500 => 20 정도

TalkHIde : 20 => -500 정도

 

 

이제 플래그를 관리하는 코드를 작성하여 보자.

전에 GameObject를 불러와서 SetActive()로 껐다가 켰다 했다면,

이번에는 Animator를 불러오자.

 

//public GameObject talkPanel;
public Animator talkPanel;

 

SetActive()도 SetBool로 바꿔주자.

 

//Visible Talk for Action
//talkPanel.SetActive(isAction);
talkPanel.SetBool("isShow", isAction);

 

마지막으로 Talk Panel에다가 TalkSet을 넣어주면 된다.

 

 

초상화 이펙트

 

이번에는 초상화가 위아래로 살짝씩 흔들리는 이펙트를 구성해보겠다.

우선 Portrait의 Image에 넣을 Animator와 Animation을 하나씩 만들고 넣자.

그리고 Make Transion으로 올바르게 연결하여 준다.

마지막으로 Trigger 형태의 Parameter 변수 'doEffect'를 만들어주자. 

 

 

전과 다른 점이 있다면 갔다가 다시 오는 과정에서 Has Exit Time을 켜둔다.

이유는 Trigger 형태이기 때문에 Conditions에 별다른 매개변수가 없어도 알아서 빠져나오게 된다.

 

 

이제 Animation을 만들 차례이다.

Add Property > Anchored Postion을 선택한 후, Key Frame을 추가하여 총 3개를 만든다.

Y값이 150에서 140으로 약간 내려갔다가 다시 150으로 올라오면 된다.

 

 

그럼 이제 Trigger을 관리하는 코드를 짜보자.

우선 GameManager.cs에 Animator 변수를 만들어주자.

 

public Animator portraitAnim;

 

이제 Trigger을 발동시킬 코드를 짜야하는데 그전에 언제 발동시킬지를 정해야 한다.

우리는 Npc와 대화할 때, Npc의 얼굴 표정이 달라질 경우 때 트리거를 당기겠다.

 

우선 이전 얼굴 표정을 저장할 변수를 만들어보자.

 

public Sprite prevPortrait;

 

그러면 이전 초상화 Sprite가 현재 초상화 portraitImg.sprite와 다르다면,

트리거를 발동시키고 이전 초상화 변수에다가 현재 초상화의 sprite로 업데이트(초기화)해주는 코드가 필요하다.

 

//Animation Portrait
if(prevPortrait!=portraitImg.sprite)
{
     portraitAnim.SetTrigger("doEffect");
     prevPortrait=portraitImg.sprite;
}

 

타이핑 이펙트

 

글씨에 나타나는 효과이다.

이것은 이펙트라 애니메이션으로 효과를 만드는 것이라 생각하겠지만,

스크립트에 코드를 작성하여 만드는 효과이다.

 

'TypeEffect'라는 스크립트를 만들고, 글씨 효과이므로 Text 오브젝트에 넣어주자.

 

우선 표시할 원래 대화 문자열을 저장해야 할 것이다.

변수를 만들어주자.

 

string targetMsg;

 

그럼 이제 이 변수를 초기화 할 코드와 애니메이션 재생을 시작하는 코드가 있어야 한다.

애니메이션 재생은 크게 3가지 단계로 나뉜다.

시작- 재생 -종료로 나뉘어진다.

우선 targetMSg를 세팅해주는 함수를 구현하자.

 

public void SetMsg(string msg)
{
    targetMsg = msg;
    EffectStart();
}

 

애니메이션 이펙트를 시작하는 함수부터 구현해보자.

우선 텍스트는 공백으로 시작될 것이고, 한 글자씩 추가될 것이기 떄문에

인덱스가 필요할 것이다. 그리고 그 인덱스는 0부터 시작할 것이다.

또한 애니메이션 재생 함수를 시작할 텐데, 한 글자씩 효과를 줄 것이기 때문에

Invoke 함수를 이용할 것이고, 1/CharPerSeconds의 시간이 지나고 실행될 것이다.

그리고 글자가 다 나온 뒤에 EndCursor가 나와야 한다.

그러나 그 뒤에는 꺼져야 하는데 애니메이션 재생을 시작할 때 끄기로 하자.

 

//Text 변수 생성, 초기화 후, 시작함수에서 공백 처리
Text msgText;

//글자 재생 속도를 위한 변수 생성
public int CharPerSeconds;

//글자 한개 한개 올리기 위하여 index가 필요
int index;

//End Curser을 담을 변수 생성
public GameObject EndCursor;

//msgText를 private로 선언했기 때문에 필요
private void Awake()
{
    msgText = GetComponent<Text>();
}

//애니메이션 시작 함수
void EffectStart()
{
    msgText.text = "";
    index = 0;
    EndCursor.SetActive(false);

    //시간차 반복 호출을 위한 invoke 함수를 사용
    // 1초 / CPS = 1초 동안 전체 메시지 중 1글자가 나오는 시간
    Invoke("Effecting", 1 / CharPerSeconds);
}

 

다음은 이펙트 재생중인 함수이다.

Text 오브젝트의 내용물이 우리가 미리 저장해둔 문장과 같아진다면 이펙트 종료 함수로 넘어가야 한다.

그 전에는 msgText 안의 내용물에 targetMsg 문자열의 index번쨰 char 값을 하나씩 더해간다.

index를 하나 늘린 이후에는 재귀를 사용하여 Invoke()함수를 한 번 더 실행해 모든 문자가 나오게 한다.

 

void Effecting()
{
     if(msgText.text==targetMsg)
     {
        EffectEnd();
        return;
     }

     //문자열도 배열처럼 char값에 접근 가능
     msgText.text += targetMsg[index];
     index++;

     Invoke("Effecting", 1 / CharPerSeconds);
}

 

마지막은 이펙트 종료 함수이다.

EndCursor을 활성화 시키면된다.

 

 void EffectEnd()
{
    //End Curser 키게 하는 코드
    EndCursor.SetActive(true);
}

 

이제 SetMsg() 함수 안에 EffectStart()함수가 있으니, GameManager.cs에 들어가 SetMsg() 함수만 써주면 된다.

GameManager.cs에 Text변수였던 talkText를 TypeEffect 변수 talk로 바꾼 뒤 코드를 수정해주자.

 

public TypeEffect talk;

//Setting Talkdata
talk.SetMsg(talkData.Split(':')[0]);


else
{
        talk.SetMsg(talkData);

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

 

이제는 오디오 소스를 넣어보자.

Text에 Add Component를 통하여 Audio Source를 집어넣어준다.

그리고 원하는 사운드를 넣어준다. ( 21번 오디오 소스 사용했음)

Play On Awake()를 해제시켜준다.

 

코드로 넘어가서 다음과 같이 작성해준다.

 

//오디오 재생을 위한 변수 선언
AudioSource audioSource;

//Awake() 함수에다 사용할 것
audioSource = GetComponent<AudioSource>();

//Effecting() 함수안에 index++ 위에 써줄것
// 공백 혹은 .이 아니라면 오디오 소스를 재생
if (targetMsg[index]!=' '|| targetMsg[index] != '.')
    audioSource.Play();

 

그러나 문제가 발생한다.

대화를 하는 도중에 spacebar을 누르면 그냥 넘어가버리면서 사운드도 계속 난다.

 

이것을 수정해 보겠다.

 

우선 현재 타이핑 이펙트가 되고 있는지 상태를 나타내는 bool 변수를 하나 선언해준다.

 

//타이핑 애니메이션이 진행되고 있는지
public bool isAnim;

 

그리고 이 bool 변수는 당연하게도 EffectStart() 함수에서 true가 될 것이고,

EffectEnd()에서 false가 될 것이다.

 

//EffectStart()에 추가
isAnim = true;

//EffectEnd()에 추가
isAnim = false;

 

SetMsg()함수를 수정해주자.

도중에 Spacebar을 눌러서 다음 대사를 진행한다면, 현재 text를 저장해둔 targetMsg로 바꿔준다.

그리고 실행중인 Invoke를 CancelInvoke()를 통하여 끊어주고,

EffectEnd()를 실행시킨다.

 

public void SetMsg(string msg)
{
    if(isAnim)  //Interrupt
    {
        msgText.text = targetMsg;
        CancelInvoke();
        EffectEnd();
    }
    else
    {
        targetMsg = msg;
        EffectStart();
    }
}

 

그러나 GameManager.cs도 수정해주어야 한다.

왜냐하면 타이핑 이펙트가 실행되는 도중에 space bar을 누르면,

Action > Talk로 실행되면서, 다음 문장을 받아올 것이다.

그리고  밑에 SetMsg가 실행이 되면서, isAnim가 True이기 때문에

현재의 Text는 저장해둔 targetMsg (한 글자씩 뽑던 그 문장)이 될 것이다.

 

결론적으로 말하면 한 문장이 씹힐 것이다.

 

그래서 isAnim의 상태에 따라 Talk를 수행해야 한다.

 

void Talk(int id, bool isNpc)
{
     //Set Talk Data
     int questTalkIndex = 0;
     string talkData = "";

     if(talk.isAnim)
     {
        talk.SetMsg("");
        return;
     }

     else
     {
        questTalkIndex = questManager.GetQuestTalkIndex(id);
        talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
     }

     //End Talk
     if(talkData==null)
     {
        isAction = false;
        talkIndex = 0;
        Debug.Log(questManager.CheckQuest(id));
        return;
     }

     //Continue Talk
     if(isNpc)
     {
        //Setting Talkdata
        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);
            
        //Animation Portrait
        if(prevPortrait!=portraitImg.sprite)
        {
            portraitAnim.SetTrigger("doEffect");
            prevPortrait=portraitImg.sprite;
        }
     }

     else
     {
        talk.SetMsg(talkData);

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

     isAction = true;
     talkIndex++;
}

 

이러면 씹히지 않고 씹혀야 할 문장이 ""으로 바뀌면서 씹히지 않게 되고,

isAction은 아직 true이기 떄문에, 이펙트 진행중이던 문장이 완성되면서 Invoke는 캔슬되고,

isAction이 false가 되면서 talk에서 다음 문장을 정상적으로 받아올 수 있는 것이다.