본문 바로가기

Unity3D/C#

유니티 토글(Toggle) 켜기/끄기 · 토글 그룹 하나만 고르기

반응형

TOGGLE ToggleGroup

 

유니티 Toggle 은 isOn 이 true 면 켜짐, false 면 꺼짐입니다. 켜고 끌 때마다 무언가를 하려면 onValueChanged 에 bool 을 받는 함수를 붙이고, 여러 토글 중 하나만 켜지게 하려면 ToggleGroup 으로 묶습니다.

 

toggle_sound.onValueChanged.AddListener(On_Changed);   // void On_Changed(bool is_on)
toggle.group = toggle_group;                            // 같은 그룹이면 하나만 켜짐

 

게임 설정 화면을 보면 사운드 켜기/끄기처럼 혼자 켜고 끄는 토글이 있고, 난이도 Easy · Normal · Hard 처럼 셋 중 하나만 고르는 토글이 있습니다. 이 블로그의 예전 글 UI Button / Toggle에 기능 할당과 삭제 in Script 는 토글에 AddListener 로 함수를 붙이는 데까지였습니다. 이번 글에서는 저장해 둔 값을 이벤트 없이 보여 주는 SetIsOnWithoutNotify, 하나만 고르게 하는 ToggleGroup 과 allowSwitchOff, 그리고 그룹 안에서 onValueChanged 가 어떤 순서로 불리는지를 사운드 토글과 난이도 토글 예제로 정리하겠습니다.

 

C# (UNITY 3D)

 

토글 웹 데모 — 그룹에서 Hard 를 누르면 Normal(false) 다음 Hard(true), 켜진 Hard 를 다시 누르면 true 가 또 불리고, SetIsOnWithoutNotify 는 이벤트가 없다

▲ 아래 SoundToggle · DifficultyToggles 와 Toggle · ToggleGroup 의 동작을 옮긴 웹 데모입니다(Unity 화면 아님). 오른쪽 이벤트 순서는 Unity 에서 확인한 순서와 같습니다.

 

Normal 이 켜진 상태에서 Hard 를 누르면 Normal 의 onValueChanged(false) 가 먼저, Hard 의 onValueChanged(true) 가 다음에 불립니다. 이미 켜진 Hard 를 다시 누르면 꺼지지 않고 true 가 한 번 더 옵니다. SetIsOnWithoutNotify 로 Easy 를 켜면 표시만 바뀌고 이벤트는 없습니다.

 

1. 준비 — 토글 하나와 토글 셋

 

Hierarchy 에서 UI (Canvas) > Toggle(예전 버전 메뉴는 UI > Toggle)로 Sound 토글을 만들고 SoundToggle 을 그 토글에 직접 붙입니다. 난이도는 Canvas 아래에 빈 오브젝트 Difficulty 를 만들어 DifficultyToggles 를 붙이면 ToggleGroup 이 함께 붙고, 그 아래에 Toggle 셋을 Easy · Normal · Hard 이름으로 만들어 toggles_difficulty 에 순서대로 할당합니다. text_result 에는 UI (Canvas) > Text - TextMeshPro 글자를 넣습니다.

 

using UnityEngine;
using UnityEngine.UI;
using TMPro;

 

Toggle 과 ToggleGroup 을 쓰기 위해 UnityEngine.UI 를, 결과 글자 TMP_Text 를 쓰기 위해 TMPro 를 사용합니다.

 

2. 켜기/끄기 — 사운드 토글

 

using UnityEngine;
using UnityEngine.UI;

//CodeFinder 코드파인더
//From https://codefinder.janndk.com/
[RequireComponent(typeof(Toggle))]
public class SoundToggle : MonoBehaviour
{
    const string KEY_SOUND = "sound";

    Toggle toggle_sound;

    void Awake()
    {
        toggle_sound = GetComponent<Toggle>();
    }

    void Start()
    {
        bool is_on = PlayerPrefs.GetInt(KEY_SOUND, 1) == 1;
        toggle_sound.SetIsOnWithoutNotify(is_on);     // 저장된 값을 보여 주기만 함
        Apply_Sound(is_on);

        toggle_sound.onValueChanged.RemoveAllListeners();
        toggle_sound.onValueChanged.AddListener(On_Changed);
    }

    void On_Changed(bool is_on)
    {
        PlayerPrefs.SetInt(KEY_SOUND, is_on ? 1 : 0);
        Apply_Sound(is_on);
    }

    void Apply_Sound(bool is_on)
    {
        AudioListener.volume = is_on ? 1f : 0f;
    }
}

 

On_Changed 의 is_on 은 바뀐 뒤의 값이라 toggle_sound.isOn 을 다시 읽을 필요가 없습니다. isOn 에 넣으면 onValueChanged 가 불리므로, 리스너를 붙인 뒤에 값을 맞추거나 Inspector 에 함수를 붙여 두었다면 저장 코드가 한 번 더 돕니다. 그래서 처음 한 번은 SetIsOnWithoutNotify 로 화면만 맞추고 Apply_Sound 를 직접 부릅니다.

 

3. 하나만 고르기 — ToggleGroup 난이도

 

using UnityEngine;
using UnityEngine.UI;
using TMPro;

//CodeFinder 코드파인더
//From https://codefinder.janndk.com/
[RequireComponent(typeof(ToggleGroup))]
public class DifficultyToggles : MonoBehaviour
{
    public Toggle[] toggles_difficulty;   // Easy · Normal · Hard 순서로 할당
    public TMP_Text text_result;          // 고른 난이도를 보여 줄 글자(없어도 됨)

    const string KEY_DIFFICULTY = "difficulty";

    ToggleGroup toggle_group;

    void Awake()
    {
        toggle_group = GetComponent<ToggleGroup>();
    }

    void Start()
    {
        if (toggles_difficulty == null || toggles_difficulty.Length == 0)
            return;                                   // 할당한 토글이 없으면 아무것도 안 함

        toggle_group.allowSwitchOff = false;          // 켜진 것을 다시 눌러도 꺼지지 않음

        foreach (Toggle toggle in toggles_difficulty)
            toggle.group = toggle_group;              // Inspector 의 Group 칸을 코드로 채움

        for (int i = 0; i < toggles_difficulty.Length; i++)
        {
            int index = i;                            // 람다가 i 대신 이 값을 기억하도록 복사
            toggles_difficulty[i].onValueChanged.RemoveAllListeners();
            toggles_difficulty[i].onValueChanged.AddListener(is_on => On_Changed(index, is_on));
        }

        Load_Difficulty();
    }

    void Load_Difficulty()
    {
        int index = Mathf.Clamp(PlayerPrefs.GetInt(KEY_DIFFICULTY, 1), 0, toggles_difficulty.Length - 1);
        toggles_difficulty[index].SetIsOnWithoutNotify(true);   // 나머지도 이벤트 없이 꺼짐
        Show_Result(index);
    }

    void On_Changed(int index, bool is_on)
    {
        if (!is_on)
            return;                                   // 꺼지는 토글도 불리므로 켜진 쪽만 처리

        PlayerPrefs.SetInt(KEY_DIFFICULTY, index);
        Show_Result(index);
    }

    void Show_Result(int index)
    {
        if (text_result == null)
            return;

        string name_toggle = toggles_difficulty[index].name;    // 토글 오브젝트 이름
        text_result.text = string.Format("difficulty {0} : {1}", index, name_toggle);
    }
}

 

Play 하면 세 토글이 하나의 그룹으로 묶이고, 저장된 값이 없으면 Normal(1)이 켜진 채로 시작합니다. 메뉴로 만든 토글은 Is On 이 켜진 채로 생기지만 그룹에 들어가면서 하나만 남고, 이어서 SetIsOnWithoutNotify 가 저장된 칸을 켭니다. 다른 칸을 누르면 그 번호가 PlayerPrefs 에 저장되고 text_result 에 「difficulty 2 : Hard」 처럼 보입니다.

두 스크립트는 Unity 6000.6.3f1(ugui 2.6.0)에서 컴파일 오류 없이 들어갔고, 배치 모드에서 토글 셋을 만들어 클릭(OnPointerClick)을 보내 봤을 때 위 데모와 같은 순서로 이벤트가 불리고 저장 값도 2 로 바뀌었습니다. Game 창에서 마우스로 눌러 본 것은 아닙니다.

 

변수(variable)
toggles_difficulty(Toggle[]) : 난이도 토글 셋, 배열 번호가 저장되는 값
text_result(TMP_Text) : 고른 번호와 이름을 보여 줄 글자
toggle_group(ToggleGroup) : 같은 오브젝트의 토글 그룹

 

함수(function)
Load_Difficulty : 저장된 번호의 토글을 이벤트 없이 켜고 글자로 보여 줍니다.
On_Changed : 토글이 바뀔 때마다 불려, 켜진 토글의 번호만 저장합니다.
Show_Result : 번호와 토글 이름을 글자로 보여 줍니다.

 

4. 그룹 안에서 onValueChanged 가 불리는 순서

 

ToggleGroup 안에서 onValueChanged 가 불리는 순서 — 꺼지는 토글의 false 가 먼저, 켜진 토글 다시 클릭은 true 한 번 더, SetIsOnWithoutNotify · SetAllTogglesOff(false) 는 이벤트 없음

 

그룹 안에서 하나를 켜면 꺼지는 토글의 onValueChanged(false) 도 불립니다. 토글마다 같은 함수를 붙였다면 한 번 누를 때 두 번 불리는 셈이라, On_Changed 처음에 is_on 이 false 면 바로 돌아가게 했습니다. 켜진 토글을 다시 눌렀을 때 true 가 또 오는 것도 기억해 둘 만합니다. 저장 정도는 상관없지만, 효과음이나 화면 전환처럼 두 번 하면 안 되는 일이라면 지금 번호와 같은지 먼저 봅니다.

 

5. 아무것도 안 고를 수 있게 — allowSwitchOff

 

toggle_group.allowSwitchOff = true;                      // 켜진 것을 다시 누르면 꺼짐(아무것도 안 고름 허용)

Toggle toggle_on = toggle_group.GetFirstActiveToggle();  // 켜진 토글, 없으면 null
bool is_any_on = toggle_group.AnyTogglesOn();
int count_on = toggle_group.ActiveToggles().Count();     // using System.Linq 필요

toggle_group.SetAllTogglesOff();                         // 모두 끔(켜져 있던 토글의 onValueChanged(false) 불림)
toggle_group.SetAllTogglesOff(false);                    // 모두 끔(이벤트 없음)

 

allowSwitchOff 를 켜면 켜진 토글을 다시 눌렀을 때 false 로 꺼지고 그룹에 켜진 토글이 하나도 없게 됩니다. 「필터 없음」 처럼 안 고른 상태가 뜻이 있을 때 씁니다. 그때 GetFirstActiveToggle 은 null 이므로 null 인지 먼저 봐야 합니다. SetAllTogglesOff 는 allowSwitchOff 와 상관없이 모두 끕니다.

 

6. 토글이 이상할 때 — 자주 하는 실수

 

ToggleGroup 을 붙였는데 둘 다 켜짐
ToggleGroup 컴포넌트만 붙여서는 묶이지 않습니다. 각 Toggle 의 Group 칸에 그룹 오브젝트를 넣거나, 위 코드처럼 toggle.group 에 넣어야 합니다. 그룹 없는 토글 둘을 차례로 누르면 둘 다 켜진 채로 남습니다.

 

어느 토글을 눌러도 번호가 3 으로 들어감
for 문 안의 람다에서 i 를 바로 쓰면 모든 람다가 for 문의 i 하나를 함께 봅니다. 클릭할 때는 반복이 끝난 뒤라 i 가 토글 개수(3)이고, 그 번호로 배열을 읽으면 IndexOutOfRangeException 이 납니다. int index = i; 처럼 반복마다 새 변수에 담아 넘깁니다.

 

Inspector 에서 붙인 함수가 그대로 불림
RemoveAllListeners 는 코드로 붙인 리스너만 지웁니다. Inspector 의 On Value Changed (Boolean) 칸에 넣어 둔 함수는 남아 있으니 코드와 같은 함수가 들어 있다면 한쪽을 뺍니다.

 

함께 보기: Button 의 onClick 과 Toggle 의 onValueChanged 에 함수를 붙이고 지우는 기본은 예전 글 UI Button / Toggle에 기능 할당과 삭제 in Script 에 있습니다.

 

Toggle · ToggleGroup 동작은 Unity 공식 문서 Toggle · ToggleGroup 을 기준으로 적었습니다.

 

 

반응형

Google Play 및 Google Play 로고는 Google LLC의 상표입니다.