Check Sudoku - Unity Tutorial - 7. Hint legend, State persistence, Localization
In the previous step we implemented hints. In this one we will add a legend for the hints, state persistence, and translation of the application to other languages (localization):
Implementation
Let's start from localization. Following this tutorial we install Localization package, add locale for languages: English (default), German, and Polish.
We fix the names of all application strings, and add their translations.
We also add the option to change language in our settings panel. To do that we add a toggle group, containing:
- HorizontalLayoutGroup - to automatically place the toggles
- ToggleGroup - to make sure only toggles are exclusive
Each toggle is a Toggle element, with a text field replaced by an image, with icon downloaded from pixabay.com (english-flag, german-flag, polish-flag)
We add a script with
localeName property and (so far empty) click handler to the language toggle (LanguageToggle):public class LanguageToggle : MonoBehaviour
{
public string localeName;
...
public void Check()
{
GetComponent<Toggle>().SetIsOnWithoutNotify(true);
}
public void OnValueChanged(bool value)
{
}
}In the editor, we set correct
localeName and click handler (On Value Changed) for each toggle:We add the script to language toggle group, which gets and sets the selected language (
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;
}
}
}
}Finally, we implement language toggle click handler (
LanguageToggle):public class LanguageToggle : MonoBehaviour
{
public void OnValueChanged(bool value)
{
if (value)
{
LanguageToggleGroup.SetLocaleByName(localeName);
}
}
}We move on to the persistence of game state. For each element, whose state we want to persist, i.e.:
- the board (
BoardImage/BoardModel) - audio settings (dodajemy
MusicToggle/MusicVolumeSlider) - language settings (
LanguageToogleGroup)
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);
}
}The board loads its state upon creation. For all other elements, the methods for loading their state is called by the game manager (
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();
}
...
}The last step is to display the legend of the hint. We add the following to the hint (
HintImage):- a text field (
HintText) to display the hint content (e.g. "1 is the only value") - the legend for the hint (e.g.. "
in the column")
Because our application is localized, we need to define each text value in three languages, and add them as properties of the hint script (
HintImage). We also add methods for displaying and hiding the legend of the hint: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);
}
}These methods will be called by selected cell (
CellImage) together with the work cell (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();
}
}
}That's how we get the app from the introduction.
GitHub
Commits related to this step are here:
Next steps
The app is almost ready to be published. It allows us to solve a sudoku. Now we need to figure out the way to input a sudoku board to our application. That's what we'll do in the next step.
Polski | Angielski













Comments
Post a Comment