기록 보관소

[Unity/유니티] 기초-탑다운 2D RPG: 대화 애니메이션 느낌있게 만들기[B25] 본문

유니티 프로젝트/탑다운 2D RPG

[Unity/유니티] 기초-탑다운 2D RPG: 대화 애니메이션 느낌있게 만들기[B25]

JongHoon 2022. 2. 21. 23:31

개요

유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.

현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

유니티 강좌 기초 채널 Basic

유니티 개발을 처음 시작하시는 입문자 분들을 위한 기초 채널. [ 프로젝트 ] B00 ~ B12 (BE1) : 유니티 필수 기초 B13 ~ B19 (BE2) : 2D 플랫포머 B20 ~ B26 (BE3) : 2D 탑다운 대화형 RPG B27 ~ B37 (BE4) : 2D 종스크롤

www.youtube.com


탑다운 2D RPG: 대화 애니메이션 느낌있게 만들기[B25]

1. 대화창 이펙트

Animator Controller 생성
Animator 컴포넌트와 생성했던 Controller 추가
Animator에서 Empty 생성
Bool형 매개변수 isShow 생성
사용할 애니메이션 2개 생성. 이후 Animator에 넣어준다
Transition 설정. isShow 매개변수도 설정해준다.

  • isShow 매개변수는 애니메이션 이름에서 예상되는 것처럼 설정했다.
    • Empty->TalkShow : isShow true
    • TalkShow->TalkHide : isShow false
    • TalkHide->TalkShow : isShow true

 

 

 

 

이번 파트의 목적은 대화창을 더이상 활성화/비활성화 하는것이 아닌, 카메라 아래로 내리고, 올리는 것으로 바꾸는 것이다.
지난 시간 대화창 커서 애니메이션을 만들었던 것처럼 Anchored Position을 활용
TalkShow는 Y축 값이 -500에서 20으로 증가한다. 즉, 떠오르는 애니메이션.
TalkHide는 반대로 20에서 -500으로 내려가는 애니메이션이다

//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
    public TalkManager talkManager;
    public QuestManager questManager;
    public Animator talkPanel;  //Object에서 Animator로 변경.
    public Image portraitImg;
    public Text talkText;
    public GameObject scanObject;
    public bool isAction;   //대화창 활성화 여부 체크
    public int talkIndex;

    void Start() {
        Debug.Log(questManager.CheckQuest());
    }

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

        //대화창 활성화, 비활성화
        talkPanel.SetBool("isShow", isAction);
	}

    void Talk(int id, bool isNPC) {
        //대화 데이터 불러오기
        int questTalkIndex = questManager.GetQuestTalkIndex(id);
        string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
        
        //대화가 끝나면 멈추기
        if (talkData == null) {
            isAction = false;
            talkIndex = 0;
            Debug.Log(questManager.CheckQuest(id));
            return;
        }

        //대화 전 체크하기
        if (isNPC) {    //NPC라면
            talkText.text = talkData.Split(':')[0];

            portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
            portraitImg.color = new Color(1, 1, 1, 1);  //초상화 보이기
        }
        else {  //NPC가 아니라면
            talkText.text = talkData;

            portraitImg.color = new Color(1, 1, 1, 0);  //초상화 투명화
        }

        //대화창 띄우기 및 대화 인덱스 증가
        isAction = true;
        talkIndex++;
    }
}

Talk Panel의 변수타입이 바뀌었으므로 재할당
올라오는 모습
잘 적용되었다


2. 초상화 이펙트

초상화 Image에 Animator와 컨트롤러 추가
초상화 Animator 설정. 앞의 과정과 마찬가지로 Animation을 생성하고, Trigger형 매개변수 doEffect 추가.
Empty-&gt;PortraitEffect 설정
PortraitEffect-&gt;Empty 설정

  • PortraitEffect->Empty는 이전과 다르게 Has Exit Time을 체크하고, 매개변수 또한 넣지 않는다. 굳이 매개변수를 넣지 않더라도 PortraitEffect 애니메이션이 발동한 후, 자동으로 Empty로 빠져나오게 된다.

마찬가지로 Anchored Position을 활용
PortraitEffect는 위 아래로 잠깐 움직이는 애니메이션. Y축 값을 150-&gt;140-&gt;150으로 설정한다.

//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
    public TalkManager talkManager;
    public QuestManager questManager;
    public Animator talkPanel;
    public Image portraitImg;
    public Animator portraitAnim;
    public Sprite prevPortrait;
    public Text talkText;
    public GameObject scanObject;
    public bool isAction;   //대화창 활성화 여부 체크
    public int talkIndex;

    void Start() {
        Debug.Log(questManager.CheckQuest());
    }

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

        //대화창 활성화, 비활성화
        talkPanel.SetBool("isShow", isAction);
	}

    void Talk(int id, bool isNPC) {
        //대화 데이터 불러오기
        int questTalkIndex = questManager.GetQuestTalkIndex(id);
        string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
        
        //대화가 끝나면 멈추기
        if (talkData == null) {
            isAction = false;
            talkIndex = 0;
            Debug.Log(questManager.CheckQuest(id));
            return;
        }

        //대화 전 체크하기
        if (isNPC) {    //NPC라면
            talkText.text = talkData.Split(':')[0];

            //초상화 보이기
            portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
            portraitImg.color = new Color(1, 1, 1, 1);
            //초상화 애니메이션
            if (prevPortrait != portraitImg.sprite) {
                portraitAnim.SetTrigger("doEffect");
                prevPortrait = portraitImg.sprite;
            }
        }
        else {  //NPC가 아니라면
            talkText.text = talkData;

            portraitImg.color = new Color(1, 1, 1, 0);  //초상화 투명화
        }

        //대화창 띄우기 및 대화 인덱스 증가
        isAction = true;
        talkIndex++;
    }
}

Portrait Anim에 초상화 Animator추가
표정이 바뀌니 초상화가 살짝 내려갔다가
다시 올라왔다


3. 타이핑 이펙트

스크립트 생성

//TypeEffect 스크립트 파일

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

public class TypeEffect : MonoBehaviour {
    public GameObject EndCursor;    //대화창 끝 화살표
    Text msgText;   //대화 텍스트
    
    public int CharPerSeconds;  //글자 재생 속도 변수
    string targetMsg;    //표시할 대화 문자열 변수
    int index;	//문자열 인덱스
    float interval;	//CPS 보조 변수

    void Awake() {
        msgText = GetComponent<Text>();
    }

    public void SetMsg(string msg) {
        targetMsg = msg;
        EffectStart();
	}
	
	void EffectStart () {
        msgText.text = "";  //시작시 공백 처리
        index = 0;
        EndCursor.SetActive(false);

        interval = 1.0f / CharPerSeconds;   // 1 / CharPerSeconds : 1글자가 나오는 딜레이
        Invoke("Effecting", interval);
    }

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

        msgText.text += targetMsg[index];   //string도 배열처럼 인덱스로 char 값에 접근 가능
        index++;

        Invoke("Effecting", interval);
    }

    void EffectEnd() {
        EndCursor.SetActive(true);
    }
}
//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
    public TalkManager talkManager;
    public QuestManager questManager;
    public Animator talkPanel;
    public Image portraitImg;
    public Animator portraitAnim;
    public Sprite prevPortrait;
    public TypeEffect talk;
    public GameObject scanObject;
    public bool isAction;   //대화창 활성화 여부 체크
    public int talkIndex;

    void Start() {
        Debug.Log(questManager.CheckQuest());
    }

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

        //대화창 활성화, 비활성화
        talkPanel.SetBool("isShow", isAction);
	}

    void Talk(int id, bool isNPC) {
        //대화 데이터 불러오기
        int questTalkIndex = questManager.GetQuestTalkIndex(id);
        string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
        
        //대화가 끝나면 멈추기
        if (talkData == null) {
            isAction = false;
            talkIndex = 0;
            Debug.Log(questManager.CheckQuest(id));
            return;
        }

        //대화 전 체크하기
        if (isNPC) {    //NPC라면
            talk.SetMsg(talkData.Split(':')[0]);

            //초상화 보이기
            portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
            portraitImg.color = new Color(1, 1, 1, 1);
            //초상화 애니메이션
            if (prevPortrait != portraitImg.sprite) {
                portraitAnim.SetTrigger("doEffect");
                prevPortrait = portraitImg.sprite;
            }
        }
        else {  //NPC가 아니라면
            talk.SetMsg(talkData);

            portraitImg.color = new Color(1, 1, 1, 0);  //초상화 투명화
        }

        //대화창 띄우기 및 대화 인덱스 증가
        isAction = true;
        talkIndex++;
    }
}

대화창 Text에 스크립트와 End Cursor 추가
GameManager의 Talk에 Text 추가
한글자씩 출력된다
완전히 다 출력되니 오른쪽에 EndCursor가 생겼다.

  • 참고로 대화창의 글자가 한글자씩 나오므로 모든 글자가 출력된 뒤, EndCursor(대화창 끝에 있는 화살표)를 보이도록 하기위해서 End Cursor 스프라이트의 체크를 빼서 비활성화 상태로 둬야한다. 이제 사운드를 추가할 차례다.

사용할 사운드 에셋. 이전 프로젝트들에서도 사용했던 것이다.
대화창 Text에 AudioSource 추가 및 설정

using UnityEngine.UI;

public class TypeEffect : MonoBehaviour {
    public GameObject EndCursor;    //대화창 끝 화살표
    Text msgText;   //대화 텍스트
    AudioSource audioSource;    //사운드
    
    public int CharPerSeconds;  //글자 재생 속도 변수
    string targetMsg;    //표시할 대화 문자열 변수
    int index;  //문자열 인덱스
    float interval; //CPS 보조 변수

    void Awake() {
        msgText = GetComponent<Text>();
        audioSource = GetComponent<AudioSource>();
    }

    public void SetMsg(string msg) {
        targetMsg = msg;
        EffectStart();
	}
	
	void EffectStart () {
        msgText.text = "";
        index = 0;
        EndCursor.SetActive(false);

        //이펙트 시작
        interval = 1.0f / CharPerSeconds;
        Invoke("Effecting", interval);
    }

    void Effecting() {
        //이펙트 끝내기
        if (msgText.text == targetMsg) {
            EffectEnd();
            return;
        }

        msgText.text += targetMsg[index];

        //사운드 출력
        if (targetMsg[index] != ' ' || targetMsg[index] != '.')
            audioSource.Play();

        index++;

        //재귀
        Invoke("Effecting", interval);
    }

    void EffectEnd() {
        EndCursor.SetActive(true);
    }
}
  • 대사 중 공백과 마침표가 출력될때는 사운드가 출력되지 않도록 설정해두었다.
  • 다만 대사가 출력되는 중 스페이스바를 누르면 바로 다음 대사로 넘어가는 문제가 있다. 다음 대사로 넘어가는 것 대신, 남은 대사가 완전히 출력되게 하도록 수정할 것이다.
//TypeEffect 스크립트 파일

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

public class TypeEffect : MonoBehaviour {
    public GameObject EndCursor;    //대화창 끝 화살표
    Text msgText;   //대화 텍스트
    AudioSource audioSource;    //사운드
    
    public int CharPerSeconds;  //글자 재생 속도 변수
    public bool isAnim;   //애니메이션 실행 판단
    string targetMsg;    //표시할 대화 문자열 변수
    int index;  //문자열 인덱스
    float interval; //CPS 보조 변수

    void Awake() {
        msgText = GetComponent<Text>();
        audioSource = GetComponent<AudioSource>();
    }

    public void SetMsg(string msg) {
        if (isAnim) {   //애니메이션 실행 중일때. 즉, 대사 중에 스페이스바를 눌렀을 때
            msgText.text = targetMsg;  //대사 문자열 전부 출력
            CancelInvoke();
            EffectEnd();
        }
        else {
            targetMsg = msg;
            EffectStart();
        }
	}
	
	void EffectStart () {
        msgText.text = "";
        index = 0;
        EndCursor.SetActive(false);

        //이펙트 시작
        isAnim = true;
        interval = 1.0f / CharPerSeconds;
        Invoke("Effecting", interval);
    }

    void Effecting() {
        //이펙트 끝내기
        if (msgText.text == targetMsg) {
            EffectEnd();
            return;
        }

        msgText.text += targetMsg[index];

        //사운드 출력
        if (targetMsg[index] != ' ' || targetMsg[index] != '.')
            audioSource.Play();

        index++;

        //재귀
        Invoke("Effecting", interval);
    }

    void EffectEnd() {
        isAnim = false;
        EndCursor.SetActive(true);
    }
}
//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
    public TalkManager talkManager;
    public QuestManager questManager;
    public Animator talkPanel;
    public Image portraitImg;
    public Animator portraitAnim;
    public Sprite prevPortrait;
    public TypeEffect talk;
    public GameObject scanObject;
    public bool isAction;   //대화창 활성화 여부 체크
    public int talkIndex;

    void Start() {
        Debug.Log(questManager.CheckQuest());
    }

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

        //대화창 활성화, 비활성화
        talkPanel.SetBool("isShow", isAction);
	}

    void Talk(int id, bool isNPC) {
        int questTalkIndex = 0;
        string talkData = "";

        //대화 데이터 불러오기
        if (talk.isAnim) {  //대사 출력 중일때
            talk.SetMsg("");
            return;
        }
        else {
            questTalkIndex = questManager.GetQuestTalkIndex(id);
            talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);
        }
        
        //대화가 끝나면 멈추기
        if (talkData == null) {
            isAction = false;
            talkIndex = 0;
            Debug.Log(questManager.CheckQuest(id));
            return;
        }

        //대화 전 체크하기
        if (isNPC) {    //NPC라면
            talk.SetMsg(talkData.Split(':')[0]);

            //초상화 보이기
            portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
            portraitImg.color = new Color(1, 1, 1, 1);
            //초상화 애니메이션
            if (prevPortrait != portraitImg.sprite) {
                portraitAnim.SetTrigger("doEffect");
                prevPortrait = portraitImg.sprite;
            }
        }
        else {  //NPC가 아니라면
            talk.SetMsg(talkData);

            portraitImg.color = new Color(1, 1, 1, 0);  //초상화 투명화
        }

        //대화창 띄우기 및 대화 인덱스 증가
        isAction = true;
        talkIndex++;
    }
}

대사 한글자씩 출력 중에 스페이스바를 누르니
다음 대사로 넘어가지 않고, 바로 남은 대사가 모두 출력되었다.