Sprawdzam Sudoku - Kurs Unity - 7. Legenda podpowiedzi, Zapisywanie stanu, Tłumaczenie

W poprzednim kroku zaimplementowaliśmy podpowiedzi. W tym kroku dodamy legendę wyjaśniającą podpowiedzi, dodamy zapisywanie stanu oraz tłumaczenie na inne języki:


Implementacja

Zacznijmy od tłumaczenia na inne języki. Zgodnie z tym tutorialem instalujemy pakiet Localization, dodajemy locale dla języków: angielskiego (domyślny), niemieckiego oraz polskiego.


Poprawiamy nazwy wszystkich napisów w aplikacji, oraz dodajemy ich tłumaczenia.


Następnie do każdego napisu dodajemy "Localize" i wybieramy odpowiednie tłumaczenie.


Dodajemy też opcję w ustawieniach, aby można było ręcznie zmienić język. W tym celu dodajemy grupę przełączników, która zawiera:
  • HorizontalLayoutGroup - aby automatycznie rozmieścić przełączniki języków
  • ToggleGroup - aby jednocześnie mógł być aktywny tylko jeden język

Każdy przełącznik, to zwykły Toggle, w którym pole tekstowe zastąpiliśmy obrazkiem, z ikoną pobraną z pixabay.com (english-flag, german-flag, polish-flag)

Dodajemy skrypt z właściwością localeName oraz (na razie pustą) obsługą kliknięcia do przełącznika (LanguageToggle):

public class LanguageToggle : MonoBehaviour
{
    public string localeName;

    ...

    public void Check()
    {
        GetComponent<Toggle>().SetIsOnWithoutNotify(true);
    }

    public void OnValueChanged(bool value)
    {        
    }
}
W edytorze dla każdego przełącznika ustawiamy poprawny localeName oraz obsługę kliknięcia (On Value Changed):

Dodajemy skrypt do grupy języków który odczytuje i zapisuje wybrany język (LanguageToggleGroup):
public class LanguageToggleGroup : MonoBehaviour
{
    ...

    private void OnEnable()
    {
        foreach (LanguageToggle languageToggle in GetComponentsInChildren<LanguageToggle>())
        {
            if (languageToggle.localeName == LocalizationSettings.SelectedLocale.name)
            {
                languageToggle.Check();
                break;
            }
        }
    }

    public static void SetLocaleByName(string name)
    {
        foreach (Locale locale in LocalizationSettings.AvailableLocales.Locales)
        {
            if (name == locale.name)
            {
                LocalizationSettings.SelectedLocale = locale;
                break;
            }
        }
    }
}
Na koniec implementujemy obsługę kliknięcia przełącznika (LanguageToggle):

public class LanguageToggle : MonoBehaviour
{
    public void OnValueChanged(bool value)
    {
        if (value)
        {
            LanguageToggleGroup.SetLocaleByName(localeName);
        }
    }
}
Przechodzimy do zapisywania stanu. Do każdego elementu, którego stan chcemy przechować, a więc:
  • planszy (BoardImage/BoardModel)
  • ustawień audio (dodajemy MusicToggle/MusicVolumeSlider)
  • ustawień języka (LanguageToogleGroup)
...dodajemy metody zapisujące i odczytujące stan. Dla planszy, przy pierwszym uruchomieniu aplikacji (a więc gdy nie jest zapisany żaden stan) chcemy, aby pokazało się domyślne sudoku:

public class BoardImage : MonoBehaviour
{
    ...

    private void Awake()
    {
        ...
        model = new BoardModel(cellImages);

        if (model.IsStoredInPrefs())
        {
            model.LoadFromPrefs();
        } else {
            // Set the initial board.
            //   9 2 |       | 7 5
            // 7     | 2   5 |     8
            // 4     | 8   9 |     1
            // ---------------------
            //   2 8 |       | 1 6
            //       |       |
            //   1 6 |       | 5 3
            // ---------------------
            // 1     | 5   6 |     2
            // 2     | 9   4 |     3
            //   4 3 |       | 8 9 
            model.SetCellValue(0, 1, 9);
            ...
            model.SetCellValue(8, 7, 9);
        }
    }

    ...

    public void OnDestroy()
    {
        SavePrefs();
    }

    public void SavePrefs()
    {
        model.SaveToPrefs();
    }
}

public class BoardModel
{
    ...

    public bool IsStoredInPrefs()
    {
        bool result = true;
        for (int row = 0; row < NUM_ROWS; ++row)
        {
            for (int col = 0; col < NUM_COLS; ++col)
            {
                if (!PlayerPrefs.HasKey("Board" + row + col))
                {
                    result = false;
                    break;
                }
            }
            if (!result)
            {
                break;
            }
        }
        return result;
    }

    public void LoadFromPrefs()
    {
        for (int row = 0; row < NUM_ROWS; ++row)
        {
            for (int col = 0; col < NUM_COLS; ++col)
            {
                SetCellValue(row, col, PlayerPrefs.GetInt("Board" + row + col));
            }
        }
    }

    public void SaveToPrefs()
    {
        for (int row = 0; row < NUM_ROWS; ++row)
        {
            for (int col = 0; col < NUM_COLS; ++col)
            {
                PlayerPrefs.SetInt("Board" + row + col, cellImageGrid[row, col].GetValue());
            }
        }
    }
}

public class MusicToggle : MonoBehaviour
{
    public AudioSource audioSource;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void LoadPrefs()
    {
        GetComponent<Toggle>().isOn = PlayerPrefs.GetInt("MusicEnabled", 0) == 1;
    }

    public void SavePrefs()
    {
        PlayerPrefs.SetInt("MusicEnabled", audioSource.enabled ? 1 : 0);
    }
}

public class MusicVolumeSlider : MonoBehaviour
{
    public AudioSource audioSource;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void LoadPrefs()
    {
        GetComponent<Slider>().value = PlayerPrefs.GetFloat("MusicVolume", 0.25f);
    }

    public void SavePrefs()
    {
        PlayerPrefs.SetFloat("MusicVolume", audioSource.volume);
    }
}

public class LanguageToggleGroup : MonoBehaviour
{
    ...

    public static void SetLocaleByName(string name)
    {
        ...
        SavePrefs();
    }

    public void LoadPrefs()
    {
        SetLocaleByName(PlayerPrefs.GetString("LocaleName", LocalizationSettings.SelectedLocale.name));
    }

    private static void SavePrefs()
    {
        PlayerPrefs.SetString("LocaleName", LocalizationSettings.SelectedLocale.name);
    }
}
Plansza sama załadowuje swój stan przy pierwszym wyświetleniu. Dla pozostałych elementów metody wczytujące stan zawołamy w menedżerze gry (GameManager):
public class GameManager : MonoBehaviour
{
    public LanguageToggleGroup languageToggleGroup;
    public MusicToggle musicToggle;
    public MusicVolumeSlider musicVolumeSlider;

    // Start is called before the first frame update
    IEnumerator Start()
    {
        musicToggle.LoadPrefs();
        musicVolumeSlider.LoadPrefs();

        // Wait for the localization system to initialize, loading Locales, preloading etc.
        yield return LocalizationSettings.InitializationOperation;
        languageToggleGroup.LoadPrefs();
    }

    ...
}
Pozostaje nam wyświetlenie legendy podpowiedzi. Do obiektu legendy (HintImage) dodajemy:
  • pole tekstowe (HintText), które będzie wyświetlało podpowiedź (np. "1 to jedyna wartość")
  • legendy podpowiedzi (np. "w kolumnie")


Ponieważ mamy już w aplikacji tłumaczenia, dla tego każdy napis musimy zdefiniować w trzech językach i dodać jako właściwość skryptu (HintImage). Do skryptu dodajemy również metody do pokazywania i ukrywania legendy:

using TMPro;
using UnityEngine.Localization;

public class HintImage : MonoBehaviour
{
    public static HintImage Instance { get; private set; }

    public TextMeshProUGUI hintText;
    public LocalizedString selectCellString;
    public LocalizedString selectValueString;
    public LocalizedString valueIsOnlyString;
    public LocalizedString valueIsSelectedString;
    public LocalizedString valueIsSuggestedString;
    public GameObject hintColumn;
    public GameObject hintRow;
    public GameObject hintGroup;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            Instance = this;
        }
    }
    
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnEnable()
    {
        DisplaySelectCellHint();
    }

    public void ClearOnlyValueHint()
    {
        hintText.SetText("");
        hintColumn.SetActive(false);
        hintRow.SetActive(false);
        hintGroup.SetActive(false);
    }

    public void DisplayOnlyValueHint(int value, List<CellSetType> cellSetTypes)
    {
        Debug.Assert(cellSetTypes.Count > 0, "Cell set types cannot be empty");
        hintText.SetText(valueIsOnlyString.GetLocalizedString(), value);
        foreach (CellSetType cellSetType in cellSetTypes)
        {
            switch (cellSetType)
            {
                case CellSetType.COLUMN:
                    hintColumn.SetActive(true);
                    break;
                case CellSetType.ROW:
                    hintRow.SetActive(true);
                    break;
                case CellSetType.GROUP:
                    hintGroup.SetActive(true);
                    break;
            }
        }
    }

    public void DisplaySelectCellHint()
    {
        hintText.SetText(selectCellString.GetLocalizedString());
    }

    public void DisplaySelectValueHint()
    {
        hintText.SetText(selectValueString.GetLocalizedString());
    }

    public void DisplaySelectedValueHint(int value)
    {
        hintText.SetText(valueIsSelectedString.GetLocalizedString(), value);
    }

    public void DisplaySuggestedValueHint(int value)
    {
        hintText.SetText(valueIsSuggestedString.GetLocalizedString(), value);
    }
}
Wołać te metody będzie wybrana komórka (CellImage) wraz z roboczą komórką (WorkCellImage):

public class CellImage : MonoBehaviour
{
    ...

    public void ClearPossibilityOnlyValue(PossibilityRect possibility)
    {
        ...
        HintImage.Instance.ClearOnlyValueHint();
    }

    public void DisplayPossibilityOnlyValue(PossibilityRect possibility)
    {
        List<CellSetType> cellSetTypes = model.GetCellSetTypesWithOnlyValue(possibility.Value);
        if (cellSetTypes.Count > 0)
        {
            foreach (CellSetType cellSetType in cellSetTypes)
            {
                possibility.DisplayOnlyValueForType(cellSetType);
            }
            HintImage.Instance.DisplayOnlyValueHint(possibility.Value, cellSetTypes);
        }
    }

    ...

    public void DisplayHint()
    {
        if(!Selected)
        {
            HintImage.Instance.DisplaySelectCellHint();
            return;
        }
        int value = GetValue();
        if (value != 0)
        {
            HintImage.Instance.DisplaySelectedValueHint(value);
            return;
        }
        int suggestion = CalculateSuggestion();
        if (suggestion != 0)
        {
            HintImage.Instance.DisplaySuggestedValueHint(suggestion);
            return;
        }
        HintImage.Instance.DisplaySelectValueHint();
    }
}

public class WorkCellImage : MonoBehaviour
{
    ...

    public void SelectCell(CellImage cell)
    {
        ...
        if (selectedCell != null)
        {
            ...
            selectedCell.DisplayHint();
        }
        ...
        if (selectedCell == cell)
        {
            selectedCell = null;
        }
        else
        {
            ...
            selectedCell.Select();
            selectedCell.DisplayHint();
            ...
        }
    }

    public void SelectPossibility(PossibilityRect possibility)
    {
        ...
        if (selectedPossibility == possibility)
        {
            ...
            selectedCell.DisplayHint();
            selectedCell.DisplayPossibilityOnlyValue(possibility);
        }
        ...
        else
        {
            ...
            selectedCell.ClearPossibilityOnlyValue(selectedPossibility);
            selectedCell.DisplayHint();
        }
    }
}
Otrzymujemy aplikację pokazaną we wstępie.

GitHub

Zmiany związane z tym krokiem znajdują się tutaj:

Kolejne kroki

Istniejąca aplikacja jest już prawie gotowa do opublikowania. Pozwala nam rozwiązać sudoku. Pozostaje nam rozwiązać kwestię wprowadzania plansz do aplikacji. Tym zajmiemy się w kolejnym kroku.

Polski | Angielski

Komentarze

Popularne posty z tego bloga

Sprawdzam Sudoku - Kurs Unity - 2. Konfiguracja

Sprawdzam Sudoku - Kurs Unity - 3. Szkielet aplikacji

Sprawdzam Sudoku - Kurs Unity - 1. Pomysł