Unity

Dialogue System

psb08 2024. 11. 21. 15:01
728x90
반응형

제가 공부하며 만든 Dialogue 즉 대화 시스템입니다.

 

0

Gif가 좀 이상하게 나왔지만, 완성본입니다. (클릭 시 Gif가 진행 되네요;;)

SpaceBar로 대화를 진행합니다.

오토도 잘 진행되고, 스킵으로 넘기는 것도 잘 되네요.

 

(이땐 까먹었지만 스킵으로 끝내면 배경색이 바뀌지 않네요;;)

 

시작하기전, 저는 KoreanTyper라는 에셋을 사용하였습니다. 따라 만드실 경우 꼭 다운로드 받아 주세요.


 

코드와 상관없이 먼저 이렇게 세팅해주세요. Capsule은 대화가 끝났는지 확인하기 위한 테스트 오브젝트입니다.

 

그럼 가장 중요한 DialogueManager를 만들기 위해 필요한 CharacterImage, CharacterManager코드를 생성 해줍니다.

DialogueManager와 DialogueGameManager도 미리 만들어 주세요.

 


CharacterImage.cs

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

[System.Serializable]
public class CharacterImage : MonoBehaviour
{
    public string characterName;
    public Image image;
}

CharacterImage 코드 입니다.

 

이 클래스는 캐릭터의 이름과 이미지를 저장하는 데이터 구조입니다.
characterName: 캐릭터의 이름을 저장합니다.
image: 캐릭터의 UI 이미지 객체입니다.

 

 

CharacterManager.cs

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

public class CharacterManager : MonoBehaviour
{
    [SerializeField] private List<CharacterImage> characterImages;

    private void Start()
    {
        foreach (CharacterImage characterImage in characterImages)
        {
            characterImage.image.color = new Color(characterImage.image.color.r,
                characterImage.image.color.g, characterImage.image.color.b, 0.25f);  //시작하면 어둡게 만들기
        }
    }
    public void UpdateCharacterImages(string speakingCharacterName)
    {
        foreach (CharacterImage characterImage in characterImages)
        {
            if (characterImage.characterName == speakingCharacterName)
            {
                characterImage.image.color = new Color(characterImage.image.color.r, 
                    characterImage.image.color.g, characterImage.image.color.b, 1f); // 밝게 설정
            }
            else
            {
                characterImage.image.color = new Color(characterImage.image.color.r,
                    characterImage.image.color.g, characterImage.image.color.b, 0.25f); // 어둡게 설정
            }
        }
    }


}

CharacterManager 코드 입니다.

 

이 클래스는 각 캐릭터의 이미지를 관리합니다.

필드:
characterImages: 캐릭터의 이름과 이미지를 포함하는 리스트입니다.


메서드:
Start(): 게임 시작 시 모든 캐릭터 이미지를 어둡게 초기화합니다.
UpdateCharacterImages(): 현재 대화 중인 캐릭터의 이미지를 밝게 하고, 나머지 캐릭터들은 어둡게 설정합니다.

 

이제 가장 중요한 DialogueManager를 확인해봅시다.

 

DialogueManager.cs

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

public class DialogueManager : MonoBehaviour
{
    [Header("SerializeField")]
    [SerializeField] private TextMeshProUGUI textLocated;
    [SerializeField] private List<string> dialogues;
    [SerializeField] private List<string> characterNames;
    [SerializeField] private GameObject cap;
    [SerializeField] private Button autoBtn;
    [SerializeField] private Camera main;
    [Header("Private UI")]
    private Image buttonImage;
    [Header("Private Value")]
    private int currentDialogueIndex = 0;
    private bool isTyping;
    private bool isAutoDialogue = false;
    private bool isFinished = false;
    private bool isTalking = true;
    [Header("Manager and Coroutine")]
    private CharacterManager characterManager;
    private Coroutine typingCoroutine;
    private Coroutine autoDialogueCoroutine;

    private void Start()
    {
        isAutoDialogue = false;
        isFinished = false;
        isTalking = true;
        textLocated.text = "스페이스 바로 대화 시작.";
        cap.SetActive(false);
    }

    private void Awake()
    {
        characterManager = GetComponent<CharacterManager>();
        buttonImage = autoBtn.GetComponent<Image>();
    }

    private void Update()
    {
        if (isTalking == false) return;
        if (isAutoDialogue == true) return;
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (isTyping)
            {
                StopCoroutine(typingCoroutine);
                DisplayCurrentDialogue();
            }
            else
            {
                ShowNextDialogue();
            }
        }

    }

    /// <summary>
    /// 지금 대화 보여주는 메서드
    /// </summary>
    private void DisplayCurrentDialogue()
    {
        if (isTalking == false) return;
        if (currentDialogueIndex < dialogues.Count)
        {
            string characterName = characterNames[currentDialogueIndex - 1];
            textLocated.text = $"{characterName} : {dialogues[currentDialogueIndex - 1]}";
            isTyping = false;
        }
        if (currentDialogueIndex == dialogues.Count)
        {
            string characterName = characterNames[currentDialogueIndex - 1];
            textLocated.text = $"{characterName} : {dialogues[currentDialogueIndex - 1]}";
            isTyping = false;
            cap.SetActive(true);
            main.backgroundColor = Color.gray;
        }
    }

    /// <summary>
    /// 다음 대화 내용 보여주는 메서드
    /// </summary>
    private void ShowNextDialogue()
    {
        if (isTalking == false) return;
        if (currentDialogueIndex < dialogues.Count)
        {
            string characterName = characterNames[currentDialogueIndex];
            characterManager.UpdateCharacterImages(characterName);
            typingCoroutine = StartCoroutine(Typing(characterName, dialogues[currentDialogueIndex]));
            currentDialogueIndex++;
        }
        else
        {
            FinishedTalking();
        }
    }

    /// <summary>
    /// 대화 타이핑하는 메서드
    /// </summary>
    /// <param name="characterName"></param>
    /// <param name="dialogue"></param>
    /// <returns></returns>
    private IEnumerator Typing(string characterName, string dialogue)
    {
        string fullText = $"{characterName} : {dialogue}";
        textLocated.text = "";
        isTyping = true;

        int typingLength = fullText.GetTypingLength(); 

        for (int i = 0; i <= typingLength; i++)
        {
            textLocated.text = fullText.Typing(i);
            yield return new WaitForSeconds(0.05f);
        }

        isTyping = false;
    }

    /// <summary>
    /// 대화 스킵 메서드
    /// </summary>
    public void SkipDialogue()
    {
        if (currentDialogueIndex < dialogues.Count)
        {
            FinishedTalking();
        }
    }

    /// <summary>
    /// 대화가 끝났는지 확인하는 메서드
    /// </summary>
    /// <param name="finished"></param>
    /// <returns></returns>
    public bool CheckFinishDialogue(bool finished)
    {
        return isFinished;
    }

    /// <summary>
    /// 이야기 중인지 체크하는 메서드
    /// </summary>
    /// <param name="talking"></param>
    /// <returns></returns>
    public bool CheckTalking(bool talking)
    {
        return isTalking;
    }

    /// <summary>
    /// 자동모드로 변경하는 메서드
    /// </summary>
    public void ToggleAutoDialogue()
    {
        Color origin = Color.white;
        Color changed = Color.yellow;
        isAutoDialogue = !isAutoDialogue;

        if (isAutoDialogue)
        {
            buttonImage.color = changed;
            if (autoDialogueCoroutine == null)
            {
                autoDialogueCoroutine = StartCoroutine(AutoDialogueCoroutine());
            }
        }
        else
        {
            isAutoDialogue = false;
            buttonImage.color = origin;
            if (autoDialogueCoroutine != null)
            {
                StopCoroutine(autoDialogueCoroutine);
                autoDialogueCoroutine = null;
            }
        }

    }

    /// <summary>
    /// 자동 대화 코루틴
    /// </summary>
    /// <returns></returns>
    private IEnumerator AutoDialogueCoroutine()
    {
        for (int i = currentDialogueIndex; i < dialogues.Count; i++)
        {
            while (isTyping)
            {
                yield return null; 
            }
            yield return new WaitForSeconds(1f);
            ShowNextDialogue();
            yield return new WaitForSeconds(1f);
        }
        yield return new WaitForSeconds(2f);
        FinishedTalking();

    }

    /// <summary>
    /// 이야기 끝나면 할 것들
    /// </summary>
    private void FinishedTalking()
    {
        isFinished = true;
        isTalking = false;
        textLocated.text = "";
        cap.SetActive(true);
    }

}

진짜 기네요. summary도 있어서 그런것 같습니다.

이 클래스는 대화의 흐름을 관리하며, 대사가 어떻게 표시되는지를 처리합니다.

 

필드:
textLocated: 대사가 표시될 UI 텍스트 객체입니다.
dialogues: 대사의 내용이 저장된 리스트입니다.
characterNames: 대사를 말하는 캐릭터의 이름이 저장된 리스트입니다.
cap: 대화가 끝났을 때 보여줄 캡슐 오브젝트입니다.

autoBtn: 자동 대화 버튼입니다.
main: 주 카메라입니다.
여러 상태 변수(isTyping, isAutoDialogue, isFinished, isTalking)가 대화 진행 상황을 추적합니다.

 

메서드:
Start(): 초기 설정을 수행하며, 대화 시작 안내 메시지를 표시합니다.
Awake(): CharacterManager와 autoBtn의 이미지를 가져옵니다.
Update(): 스페이스 키 입력을 통해 대화를 진행합니다.
DisplayCurrentDialogue(): 현재 대사를 화면에 표시합니다.
ShowNextDialogue(): 다음 대사를 준비하고 보여줍니다.
Typing(): 대사를 타이핑하는 효과를 주는 코루틴입니다.
SkipDialogue(): 대화를 스킵하는 메서드입니다.
CheckFinishDialogue(): 대화가 끝났는지 확인합니다.
CheckTalking(): 현재 대화 중인지 확인합니다.
ToggleAutoDialogue(): 자동 대화 모드를 켜거나 끕니다.
AutoDialogueCoroutine(): 자동 대화 진행을 위한 코루틴입니다.
FinishedTalking(): 대화가 끝났을 때의 처리를 수행합니다.

 

자세한 설명은 다음글에 적는것이 좋을 것 같습니다.

링크 : https://psb08.tistory.com/98

 

마지막으로 DialogueGameManager입니다

 

DialogueGameManager.cs

using System.Collections;
using UnityEngine;

public class DialogueGameManager : MonoBehaviour
{
    [SerializeField] private DialogueManager dialogueManager;
    [SerializeField] private GameObject panel;
    [SerializeField] private GameObject capsule;


    private void Start()
    {
        capsule.SetActive(false);
    }

    private void Update()
    {
        if (dialogueManager.CheckFinishDialogue(true))
        {
            panel.SetActive(false);
        }

    }

    public void SkipBtn()
    {
        if (dialogueManager.CheckTalking(true))
        {
            dialogueManager.SkipDialogue();
            capsule.SetActive(true);
        }
    }

    private IEnumerator ShowEffect()
    {
        yield return new WaitForSeconds(0.1f);
    }

    public void AutoBtn()
    {
        if (dialogueManager.CheckTalking(true))
        {
            dialogueManager.ToggleAutoDialogue();
        }
    }


}

이 클래스는 대화 관리와 UI 패널의 활성화를 처리합니다.

 

필드:
dialogueManager: 대화 시스템을 관리하는 DialogueManager 객체입니다.
panel: 대화 UI 패널입니다.
capsule: 대화가 끝났을 때 표시할 캡슐 오브젝트입니다.


메서드:
Start(): 캡슐을 비활성화합니다.
Update(): 대화가 끝났을 때 패널을 비활성화합니다.
SkipBtn(): 대화를 스킵하고 캡슐을 활성화합니다.
ShowEffect(): 특정 효과를 보여주기 위한 코루틴입니다 (현재 사용되지 않음).
AutoBtn(): 자동 대화 모드를 토글합니다.

 

자세한 설명은 다음글에 적도록 하겠습니다.

링크 : https://psb08.tistory.com/99


이제 유니티에서 세팅을 하도록 하겠습니다.

 

먼저 DialogueManager에 DialogueManager와 CharacterManager를 넣어주고

2d 오브젝트 아무거나 2개를 만든 후 색을 각각 다른 색으로 지정해주세요.

오브젝트에 각각 CharacterImage 코드를 넣어준 뒤, 이름을 정하고 Image를 넣어주세요.

이 이름은 DialogueManager에서 사용해야 하니 꼭 오타가 나지 않도록 복사해주세요.

 

DialogueGameManager도 만들어 주세요. 코드도 넣어주시고요

여기에는 DialogueManager를 넣어준 뒤, 전체적인 UI (저는 Panel로 만들었습니다)를 넣어준 뒤

테스트로 사용할 Capsule 오브젝트를 넣어주세요

 

 

TMP를 하나 생성해주시고 Text가 표시될 위치를 정해주세요.

 

버튼 2개를 만들어 주시고, 하나는 AutoBtn 다른 하나는 SkipBtn이라고 지정해줍시다.

각각 버튼 색은 아무 상관 없으나, Auto 모드가 활성화 될 시에 버튼 색을 노란색으로 바꾸도록

코드를 만들었으니 노란색은 피해주세요.

SkipBtn의 OnClick
AutoBtn의 OnClick

SkipBtn에 DialogueGameManager 코드가 들어있는 오브젝트를 끌어 당긴 뒤, SkipBtn 메서드를 OnClick 이벤트로

넣어주세요

AutoBtn에는 AutoBtn메서드를 넣으면 됩니다.

 

CharacterManager에는 List를 추가하여 각각의 오브젝트를 넣어주세요.

 

이제 가장 중요한 DialogueManager를 세팅할 차례입니다.

 

조금 정리가 안되어 있지만 TextLocated에는 아까 생성한 TMP를 넣어주세요.

Cap에는 Capsule 오브젝트, AutoBtn에는 AutoBtn을, Main에는 MainCamera를 넣어주세요.

카메라는 Space Bar로 클릭하여 끝이 났을 시, 배경 색을 바꾸어 줄겁니다. (스킵, 오토는 X)

 

Dialogues를 CharacterNames와 같이 설명 드리겠습니다.

 

Dialogues는 말 그대로 대화 내용을 적으시면 됩니다.

그리고 대화를 할 주체자는 CharacterNames에 아까 설정한 오브젝트의 코드에 있는 이름을 적으시면 됩니다.

지금은 이렇게 하나하나 쳐야 하지만 실력이 좀 더 올라온다면 알아서 설정 되도록 만들어 보고 싶네요.

 

이렇게 설정 하시면 아마 잘 실행 될겁니다.

 

부족한 내용이 있다면 언제든지 댓글로 적어주세요.

비밀 댓글은 삼가해주시면 감사하겠습니다. 모두가 봐야 하는 문제가 있을지도 모르니까요.

 

감사합니다.