Czasami zdarza się, że coś dzieje się inaczej, niż się tego spodziewamy. W takich sytuacjach często konieczne jest dokładne zbadanie przyczyn i przebiegu działania naszego kodu. Zwykle pierwszym krokiem jest dodanie gdzieś Debug.Log w miejscu, w którym spodziewamy się wystąpienia problemu. Co jednak, gdy to nie wystarczy?
Kolejny parametr w Debug.Log
Problem: Masz wiele obiektów o tej samej nazwie, które trudno odróżnić.
Rozwiązanie: Pierwszym krokiem do wzmocnienia komunikatu w logu jest dodanie dodatkowego parametru do wywołania. Zgodnie z dokumentacją:
public static void Log(object message, Object context);
Jeśli przekażesz GameObject lub Component jako opcjonalny argument context, Unity podświetli ten obiekt w oknie Hierarchy, gdy klikniesz komunikat w konsoli.
Co to oznacza w praktyce? Załóżmy, że nasz debug wygląda tak:
public class FindMe : MonoBehaviour { void Start() { Debug.Log("ヘ(・ω| Gdzie jestem?", this); } }
Wystarczy kliknąć w komunikat, aby odpowiadający mu obiekt został podświetlony. W tym przykładzie przekazaliśmy Component
(this odnosi się do klasy dziedziczącej po Component). Podobnie możemy przekazać dowolny GameObject
.
Z oficjalnego opisu może wynikać, że działają tylko obiekty widoczne w Hierarchy. Jednak nagłówek metody wskazuje, że możemy użyć dowolnego UnityEngine.Object
. Dzięki temu możemy zlokalizować cokolwiek, co ma Instance ID – m.in. Material
, ScriptableObject
czy Mesh
.
Bonus: Możemy też użyć EditorGUIUtility.PingObject
, aby skorzystać z tej funkcji bez zapisywania logów. Link
Spraw, żeby logi się wyróżniały
Problem: Musisz logować wiele rzeczy i szybko je kategoryzować. Wyszukiwarka i zwijanie logów to za mało – kolejność wiadomości jest ważna, a wyszukiwany tekst może być trudno zauważalny.
Rozwiązanie: Uatrakcyjnij logi wizualnie. Komunikaty mogą zawierać tagi Rich Text. Wyróżnianie kolorem jest szybsze niż czytanie każdej linii. W przykładzie poniżej, gdy wartość przekroczy próg, zmienia kolor, co od razu przyciąga wzrok.
Przerwanie wykonywania
Problem: Specjalny przypadek oznaczony kolorem nie jest wystarczająco zrozumiały po jego wystąpieniu. Musisz zbadać scenę lub przyjrzeć się dokładnie wykonaniu kodu.
Rozwiązanie: Najpierw znana metoda – breakpoint. Unity i Visual Studio dobrze współpracują w tym zakresie. Po stronie Visual Studio podstawowy debug opisuje oficjalna dokumentacja. Aby to działało w Unity:
- Kliknij na lewym marginesie wiersza, w którym chcesz breakpoint.
- Załącz debugger do Unity.
- Wciśnij Play.
- Breakpoint zostaje trafiony. W Visual Studio możesz sprawdzić wartości lokalne, stos wywołań i inne narzędzia debugowania.
Gdy Visual Studio przejmuje kontrolę, edytor przestaje odpowiadać. Jeśli potrzebujesz zbadać scenę, przydatne jest Debug.Break
– działa jak naciśnięcie przycisku pauzy dokładnie w wybranym miejscu. Możesz też użyć Debug.LogError
przy włączonej opcji pauzy przy błędzie.
Dostosowywanie okna Konsoli
Problem: Różne typy danych mogą nie mieścić się w ograniczonej przestrzeni wpisu logu.
Rozwiązanie: Przejrzyj opcje okna Konsoli i włącz lub wyłącz potrzebne elementy. Możesz też pokazać lub ukryć znaczniki czasu.
Co właśnie zbudowałem?
Problem: Komunikat Build completed with a result of ‚Succeeded’ nie informuje, co dokładnie się wydarzyło. Zwłaszcza gdy chcesz zmniejszyć rozmiar aplikacji.
Rozwiązanie: W opcjach okna Konsoli wybierz „Open Editor Log”. Otworzy się plik tekstowy, w którym od sekcji Build Report znajdziesz szczegółowy wykaz assetów i ich rozmiar w malejącej kolejności.
Logowanie z użyciem dyrektyw preprocesora
Problem: Masz krytyczny system, który za każdym razem wywołuje zestaw kluczowych komunikatów debug. Jest ich za dużo, żeby aktywować je i dezaktywować ręcznie.
Rozwiązanie: Utwórz klasę statyczną, aktywną przy pomocy własnej dyrektywy. Wywoła ona odpowiednią metodę logującą z niestandardowym prefiksem. Aby szybko to wyłączyć, dodaj klasę o tej samej nazwie metod, ale aktywną przy negacji dyrektywy. W Edit > Project Settings > Player > Other Settings > Scripting Define Symbols
umieść symbol filtrujący logger. Przykład dla modułu sieciowego:
using UnityEngine; #if NET_LOGGER && (UNITY_EDITOR || DEVELOPMENT_BUILD) public static class DebugNet { public static void Log(string message) { Debug.Log($"[NET] {message}"); } public static void LogWarning(string message) { Debug.LogWarning($"[NET] {message}"); } public static void LogError(string message) { Debug.LogError($"[NET] {message}"); } } #else public static class DebugNet { public static void Log(string message) { } public static void LogWarning(string message) { } public static void LogError(string message) { } } #endif
Aby debugowe komunikaty nie trafiły do finalnej wersji, uzależnij ich wyświetlanie od wersji deweloperskiej lub edytora. Więcej o dyrektywach Unity: dokumentacja.
Liczby to za mało?
Problem: Jest dużo danych, które trudno zrozumieć w postaci samych liczb. Może to być wektor, odległość, tekst lub inna wartość – ważne, by od razu wiedzieć, jaką ma wartość i do którego obiektu należy.
Rozwiązanie: W zależności od potrzeb, można narysować różne elementy na ekranie.
Klasa Debug
Najprostszym sposobem na narysowanie linii jest użycie klasy Debug. Z dokumentacji (DrawLine, DrawRay):
public static void DrawLine(Vector3 start, Vector3 end, Color color = Color.white, float duration = 0.0f, bool depthTest = true);
public static void DrawRay(Vector3 start, Vector3 dir, Color color = Color.white, float duration = 0.0f, bool depthTest = true);
Obie metody przydają się np. do debugowania raycastów. Przykład prezentuje, jak wizualizować promień widzenia obiektu:
[SerializeField] float hitDist = 1000; void Update() { Ray ray = new Ray(transform.position, transform.forward); RaycastHit hit; if (Physics.Raycast(ray, out hit, hitDist)) { Debug.DrawLine(ray.origin, hit.point, Color.green); // obsługa trafienia } else { Debug.DrawLine(ray.origin, ray.GetPoint(hitDist), Color.red); } }
Gizmos i Handles
Jeśli potrzebujesz bardziej złożonych kształtów, skorzystaj z Gizmos. Możesz rysować podstawowe bryły lub własne siatki. Poniższy przykład wizualizuje obszar w stylu BoxCollider:
public class BoundingBox : MonoBehaviour { [SerializeField] Bounds bounds; [SerializeField] bool filled; void OnDrawGizmosSelected() { Gizmos.matrix = transform.localToWorldMatrix; if (filled) { Gizmos.DrawCube(bounds.center, bounds.size); } Gizmos.DrawWireCube(bounds.center, bounds.size); } }
Aby rysunki uwzględniały transformację obiektu, ustawiamy macierz lokalnego przekształcenia. Dla etykiet przydaje się klasa Handles, pozwalająca na wyświetlanie tekstu:
public static void Label(Vector3 position, string text, GUIStyle style);
void OnDrawGizmos() { Handles.Label(transform.position, $"{gameObject.name} {transform.position}", EditorStyles.miniButton); }
Uwaga: metody Gizmos działają tylko w funkcjach OnDrawGizmos
i OnDrawGizmosSelected
. Jeśli chcesz rysować z poziomu okien edytora, użyj Handles.
Podsumowanie…
Na koniec warto wspomnieć, że czasami do wsparcia kodu debugowania przydają się narzędzia lub skrypty firm trzecich. Należy jednak zachować ostrożność przy ich dodawaniu, ponieważ mogą oferować funkcje zbędne w projekcie, a nawet go utrudniać. Mimo to mam nadzieję, że poznałeś techniki, które możesz wykorzystać przy usuwaniu nowych błędów lub pogłębieniu zrozumienia systemu. Pamiętaj jednak, że rozwiązanie powinno być zawsze dopasowane do problemu, a korzystanie z błyszczącej, nowej metody nie zawsze jest najlepszym wyborem. Zawsze warto zrobić krok wstecz i zastanowić się, co naprawdę jest potrzebne.