Sprawdzam Sudoku - Kurs Unity - 4. Ustawianie wartości

W poprzednim kroku stworzyliśmy szkielet aplikacji. W tym kroku zaimplementujemy mechanizm ustawiania wartości w komórkach planszy:

Implementacja

Aby wypełnić planszę do gry kwadratami 3x3 (potrzebujemy ich 9) oraz pojedynczymi komórkami (potrzebujemy 81), tworzymy odpowiednie prefabrykaty i korzystamy z GridLayout aby je wyświetlić. Tworzymy GroupRect dla każdej grupy 3x3 oraz CellImage dla każdej komórki:


Podobnie robimy z roboczą komórką, gdzie wyświetlamy możliwe wartości. Tworzymy PossibilityRect do wyświetlenia możliwej wartości komórki:


Po kliknięciu na komórkę planszy (CellImage), chcemy, aby coś (???) zaznaczyło wybraną komórkę ramką, a robocza komórka (WorkCellImage) pokazała aktualną wartość danej komórki. Po kliknięciu na inną komórkę planszy, coś powinno odznaczyć poprzednią komórkę, zaznaczyć nową i pokazać nową aktualną wartość w roboczej komórce.

Pytanie 1:
Który element powinien pamiętać aktualnie wybraną komórkę?

Możliwości:
  • Menadżer gry (GameManager)
  • Plansza (BoardImage)
  • Robocza komórka (WorkCellImage)
Decyzja:
Ponieważ robocza komórka i tak musi wykonać jakąś pracę przy zmianie wybranej komórki (pokazać aktualną wartość), najprościej będzie jeśli to ona będzie ją pamiętała.

Pytanie2:
Jak wywołać z komórek planszy (jest ich 81) metodę w roboczej komórce?

Możliwości:
  • Zrobienie z roboczej komórki "Singleton'a", wraz z wszystkimi zaletami i wadami tego rozwiązania opisanymi tutaj.
  • Dodanie publicznego pola do komórki i ręczne przypisanie roboczej komórki do każdej komórki w edytorze.
Decyzja:
Ponieważ nie spodziewamy się, aby kiedykolwiek w grze pojawiły się dwie robocze komórki jednocześnie, wybieramy rozwiązanie korzystającego ze wzorca "Singleton" dla roboczej komórki.

public class WorkCellImage : MonoBehaviour
{
    public static WorkCellImage Instance { get; private set; }
    
    private void Awake()
    {
      if (Instance != null && Instance != this)
      {
        Destroy(this);
      }
      else
      {
        Instance = this;
      }
    }
    
    ...
}
Pytanie 3:
Jak zaimplementować zaznaczanie komórki?

Możliwości:
  • Dodanie nowego UI elementu SelectionImage do każdej komórki oraz dodanie metod "Select"/"Unselect" do aktywowania/dezaktywowania go.
  • Dodanie nowego UI elementu SelectionImage do planszy, który robocza komórka będzie pokazywała oraz przemieszczała nad wybraną komórkę.
Decyzja:
Oddelegowanie zaznaczenia do każdej komórki wydaje się łatwiejsze, gdyż nie trzeba zmieniać pozycji żadnego elementu UI (a jedynie niektóre elementy aktywować lub dezaktywować). 

Dodajemy zatem do prefabrykatu CellImage element SelectionImage pokazujący ramkę w kolorze #004D40 oraz pole "Selection Image", które w edytorze powiązujemy z obiektem SelectionImage.


W skrypcie implementujemy metody "Select" oraz "Unselect" oraz dodajemy obsługę "OnClick", która mówi roboczej komórce o zmianie wybranej komórki:

public class CellImage : MonoBehaviour
{
    public GameObject selectionImage;
    
    ...
    
    public void Select()
    {
        selectionImage.SetActive(true);
    }

    public void Unselect()
    {
        selectionImage.SetActive(false);
    }

    public void OnClick()
    {
        WorkCellImage.Instance.SelectCell(this);
    }
}
W metodzie "SelectCell" roboczej komórki odznaczamy poprzednio wybraną komórkę, a zaznaczamy nową:

public class WorkCellImage : MonoBehaviour
{
    private CellImage selectedCell = null;
    
    ...
    
    public void SelectCell(CellImage cell)
    {
        if (selectedCell != null)
        {
            selectedCell.Unselect();
        }
        selectedCell = cell;
        if (selectedCell)
        {
            selectedCell.Select();
        }
    }
}
W taki sam sposób implementujemy wybieranie aktualnie wybranej wartości w roboczej komórce. Po kliknięciu na możliwą wartość (PossibilityRect), chcemy, aby robocza komórka (WorkCellImage) zaznaczyła wybraną możliwą wartość ramką, a komórka (CellImage) pokazała swoją wartość. Po kliknięciu na inną możliwą wartość, robocza komórka powinna odznaczyć poprzednią możliwą wartość, zaznaczyć nową i pokazać nową wartość w wybranej komórce.

Do prefabrykatu PossibilityRect dodajemy UI element SelectionImage pokazujący ramkę w kolorze #004D40 oraz pole "Selection Image", które w edytorze powiązujemy z tym obiektem.


W skrypcie implementujemy metody "Select" oraz "Unselect". Dodajemy również obsługę "OnClick", która mówi roboczej komórce o zmianie wartości wybranej komórki:

public class PossibilityRect : MonoBehaviour
{
    public GameObject selectionImage;
    
    ...
    
    public void Select()
    {
        selectionImage.SetActive(true);
    }

    public void Unselect()
    {
        selectionImage.SetActive(false);
    }

    public void OnClick()
    {
        WorkCellImage.Instance.SelectPossibility(this);
    }
}
W metodzie "SelectPossibility" roboczej komórki odznaczamy poprzednio wybraną wartość, a zaznaczamy nową:

public class WorkCellImage : MonoBehaviour
{
    private PossibilityRect selectedPossibility = null;
    
    ...
    
    public void SelectPossibility(PossibilityRect possibility)
    {
        if (selectedPossibility != null)
        {
            selectedPossibility.Unselect();
        }
        selectedPossibility = possibility;
        if (selectedPossibility != null)
        {
            selectedPossibility.Select();
        }
        if (selectedCell != null)
        {
            selectedCell.SetValue(possibility.Value);
        }
    }
}
Jak widać, aby to zadziałało komórka (CellImage) musi mieć metodę "SetValue", a aktualna wartość (PossibilityRect) własność "Value". Dodajemy więc te własności, oraz w kodzie upewniamy się, że pola tekstowe wyświetlają ich zawartość:

public class CellImage : MonoBehaviour
{
    public int Value { get; private set; }
    public TextMeshProUGUI valueText;
    
    ...
    
    // Start is called before the first frame update
    void Start()
    {
        SetValue(0);
    }
    
    ...
    
    public void SetValue(int value)
    {
        Value = value;
        valueText.SetText(value != 0 ? value.ToString() : "");
    }
}
public class PossibilityRect : MonoBehaviour { [field: SerializeField] public int Value { get; private set; } = 0; public TextMeshProUGUI valueText; ... // Start is called before the first frame update void Start() { valueText.SetText(Value.ToString()); } ... }
Na koniec, modyfikujemy metodę "SelectCell" obiektu WorkCellImage aby przy zmianie wybranej komórki robocza komórka pokazywała jej aktualną wartość:

public class WorkCellImage : MonoBehaviour
{
    ...
    
    public void SelectCell(CellImage cell)
    {
        ...
        selectedPossibility = null;
        foreach(PossibilityRect possibility in GetComponentsInChildren<PossibilityRect>())
        {
            if (selectedCell != null && possibility.Value == selectedCell.Value)
            {
                possibility.Select();
                selectedPossibility = possibility;
            }
            else
            {
                possibility.Unselect();
            }
        }
    }
    
    ...
}
Otrzymujemy aplikację pokazaną we wstępie.

GitHub

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

Kolejne kroki

W następnym kroku dodamy sprawdzanie zasad Sudoku, aby nie dało się "wyklikać" niepoprawnej planszy.

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ł