This shows you the differences between two versions of the page.
|
pjv:laboratoare:2025:a02 [2025/10/05 11:56] alexandru.gradinaru |
pjv:laboratoare:2025:a02 [2025/10/23 12:06] (current) andrei.lapusteanu |
||
|---|---|---|---|
| Line 29: | Line 29: | ||
| https://unity.com/how-to/architect-game-code-scriptable-objects#:~:text=ScriptableObject%20is%20a%20serializable%20Unity,to%20manage%20changes%20and%20debugging. | https://unity.com/how-to/architect-game-code-scriptable-objects#:~:text=ScriptableObject%20is%20a%20serializable%20Unity,to%20manage%20changes%20and%20debugging. | ||
| + | |||
| + | === Resurse Andrei L === | ||
| + | |||
| + | * Explicatii pattern-uri in Unity (Factory, Pooling, State, Command, Observer, MVC & MVP) [[https://learn.unity.com/tutorial/65e0df08edbc2a2447bf0b98?uv=2022.3&projectId=65de084fedbc2a0699d68bfb#|aici]] | ||
| + | * PDF cu toate pattern-urile (Unity): {{:pjv:laboratoare:2025:unity_patterns.pdf|}} | ||
| + | * PDF despre Scriptable Objects si arhitectura modulara cu ele (Unity): {{:pjv:laboratoare:2025:unity_modular_architecture.pdf|}} | ||
| + | * PDF despre guideline-uri de programare C# (Unity): {{:pjv:laboratoare:2025:unity_cleaner_code.pdf|}} | ||
| + | * Repo cu exemple de pattern-uri (Unity): [[https://github.com/Unity-Technologies/game-programming-patterns-demo|aici]] | ||
| </hidden> | </hidden> | ||
| Line 40: | Line 48: | ||
| * un mass damage | * un mass damage | ||
| * attack simplu | * attack simplu | ||
| + | * Abilitatile au efecte grafice (particule, text cu -10dmg etc) la aplicare | ||
| * Evenimentele actualizează UI și efectele vizuale prin pooling. | * Evenimentele actualizează UI și efectele vizuale prin pooling. | ||
| * Fiecare personaj se poate deplasa, pentru a evita atacul simplu | * Fiecare personaj se poate deplasa, pentru a evita atacul simplu | ||
| Line 56: | Line 65: | ||
| * un mass damage: cooldown 3 ture | * un mass damage: cooldown 3 ture | ||
| * attack simplu: cooldown 1 tura | * attack simplu: cooldown 1 tura | ||
| - | * Abilitatile au efecte grafice (particule, text cu -10dmg etc) la aplicare | ||
| * Elemente de design imbunatatit: scena/personaje/animatii etc | * Elemente de design imbunatatit: scena/personaje/animatii etc | ||
| * Grid system | * Grid system | ||
| Line 74: | Line 82: | ||
| * Command | * Command | ||
| * Observer | * Observer | ||
| + | * Cu evenimente C# sau ''UnityEvent'' | ||
| * Object Pooling | * Object Pooling | ||
| - | * MVC (cu ScriptableObjects) | + | * MVC |
| + | * Cu scriptable objects sau Event Bus | ||
| Line 606: | Line 616: | ||
| </code> | </code> | ||
| + | ===== Event Bus ===== | ||
| + | Prezentam un pattern pentru decuplare si mai "loosely coupled" decat cele anterioare (un fel de "final boss" 🙂), intrucat folosind aceasta metoda puteti comunica fara nicio referinta (nici macar indirecta, precum in cazul comunicarii via ''ScriptableObjects''. | ||
| + | <note important> | ||
| + | Reiteram faptul ca fiecare pattern are valoare intrinseca si ceea ce face unul sa fie mai "bun" decat altul depinde foarte mult de context. | ||
| + | * Referentiarea directa este OK pentru prototipare radpida si proiecte mici | ||
| + | * Event Bus-ul este recomandat pentru proiecte la scara larga | ||
| + | De asemenea, este important de mentionat faptul ca pe masura ce decuplam sisteme, debugging-ul si urmarirea flow-ului pot avea de suferit (ceea ce face codul este mai obfuscat). | ||
| + | </note> | ||
| + | **Event Bus-ul** reprezinta un sistem centralizat care face management de mesaje (ex. evenimente) si este un middle-man intre diversele componente ale unui proiect. Conceptul, este similar cu cel bazat pe ''ScriptableObjects'' prezentat anterior, dar nu mai necesita existenta unui obiect concret (''ScriptableObject''-ul) si poate defini tipuri de date arbitrare care sunt "pasate" intre componente. | ||
| + | |||
| + | La cel mai de baza nivel un **event bus** trebuie sa suporte | ||
| + | * O metoda de **abonare (subscrice)** | ||
| + | * O metoda de **dezabonare (unsubscribe)** | ||
| + | * O metoda de **semnalare eveniment (broadcast/publish)** | ||
| + | |||
| + | Asadar, o componenta A se va abona la un eveninment (asculta), iar componenta B va face broadcast/publish - moment in care A va executa codul aferent. In acest fel event bus-ul este un **router de mesaje**. | ||
| + | |||
| + | Decuplarea reise din faptul ca atat A si B trebuie sa fie "de acord" ce date transfera, dar cine si cum face comunicarea intre ele (event bus-ul) nu este de interes pentru niciuna din componentele aflate in schimbul de mesaje. | ||
| + | |||
| + | Inainte de a introduce un exemplu, clarificam pe scurt cateva elemente folosite in constructia event bus-ului. | ||
| + | |||
| + | ==== Generics (T) ==== | ||
| + | |||
| + | In C# puteti folosit tipuri de date generice prin type parameter-ul ''T'', care este in esenta un placeholder pentru orice tip de date. | ||
| + | |||
| + | <code c#> | ||
| + | public class GenericList<T> | ||
| + | { | ||
| + | private List<T> items = new List<T>(); | ||
| + | | ||
| + | public void Add(T item) => items.Add(item); | ||
| + | } | ||
| + | |||
| + | public class Program | ||
| + | { | ||
| + | public void Example() | ||
| + | { | ||
| + | var intList = new GenericList<int>(); | ||
| + | intList.Add(10); | ||
| + | |||
| + | var stringList = new GenericList<string>(); | ||
| + | stringList.Add("PAJV"); | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | ==== Records (C# 9+) ==== | ||
| + | |||
| + | Un **record** este un tip special de clasa, folosita in principal pentru structuri de data imutabile (immutable data container). Se pot scrie/declara foarte concis. Implementeaza in mod automat | ||
| + | * Constructor | ||
| + | * Proprietati | ||
| + | * Check-uri de egalitate | ||
| + | * ''ToString()'' | ||
| + | |||
| + | In esenta, cand este nevoie de un tip de date care poate fi referentiat si este imutabil, un **record** este o alternativa buna. | ||
| + | |||
| + | De exemplu: | ||
| + | |||
| + | <code c#> | ||
| + | public record PlayerHealthChangedEvent(int Current, int Max); | ||
| + | </code> | ||
| + | |||
| + | creeaza o clasa completa cu tot cu constructor, 2 proprietati (''Current'', ''Max''), equality check. | ||
| + | |||
| + | ==== Singleton ==== | ||
| + | |||
| + | Event bus-ul propus este un **Singleton** (adeseori considerat de fapt un [[https://www.geeksforgeeks.org/system-design/why-is-singleton-design-pattern-is-considered-an-anti-pattern|anti-pattern]]), anume astfel ne asiguram de faptul ca event bus-ul are o singura instanta si este accesibil in mod global. Detaliile de implmenetare ale acestuia sunt omise in laborator. | ||
| + | |||
| + | |||
| + | ===== Exemplu implementare Event Bus ===== | ||
| + | |||
| + | Event bus-ul: | ||
| + | |||
| + | <code c#> | ||
| + | |||
| + | public record OnPlayerDiedSoundEvent(AudioClip SoundToPlay); | ||
| + | public record PlayerHealthChangedEvent(int CurrentHealth, int MaxHealth); | ||
| + | // ... add more records as neeed ... | ||
| + | |||
| + | public class EventBus : Singleton<EventBus> | ||
| + | { | ||
| + | private readonly Dictionary<Type, List<object>> _subscribers = new(); | ||
| + | | ||
| + | // Abonare. | ||
| + | public void Subscribe<T>(Action<T> listener) where T : class | ||
| + | { | ||
| + | var eventType = typeof(T); | ||
| + | | ||
| + | if (!_subscribers.ContainsKey(eventType)) | ||
| + | { | ||
| + | _subscribers[eventType] = new List<object>(); | ||
| + | } | ||
| + | _subscribers[eventType].Add(listener); | ||
| + | } | ||
| + | | ||
| + | // Dezabonare. | ||
| + | public void Unsubscribe<T>(Action<T> listener) where T : class | ||
| + | { | ||
| + | var eventType = typeof(T); | ||
| + | | ||
| + | if (_subscribers.TryGetValue(eventType, out var subscriber)) | ||
| + | { | ||
| + | subscriber.Remove(listener); | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | // Semnalare mesaj. | ||
| + | public void Broadcast<T>(T eventData) where T : class | ||
| + | { | ||
| + | var eventType = typeof(T); | ||
| + | | ||
| + | if (_subscribers.TryGetValue(eventType, out var subscribers)) | ||
| + | { | ||
| + | foreach (var subscriber in subscribers) | ||
| + | { | ||
| + | (subscriber as Action<T>)?.Invoke(eventData); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Exemplu clasa ''Player'' care face ''Broadcast'': | ||
| + | |||
| + | <code c#> | ||
| + | public class Player : MonoBehaviour | ||
| + | { | ||
| + | // Omitted other implementation details... | ||
| + | | ||
| + | private void PlayerDamaged() => EventBus.Instance.Broadcast(new PlayerHealthChangedEvent(health, maxHealth)); | ||
| + | |||
| + | private void Die() => EventBus.Instance.Broadcast(new OnPlayerDiedSoundEvent(deathSound)); | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Exemplu de abonat: | ||
| + | |||
| + | <code c#> | ||
| + | |||
| + | public class PlayerUI : MonoBehaviour | ||
| + | { | ||
| + | [SerializeField] private TextMeshProUGUI text; | ||
| + | | ||
| + | private void OnEnable() => EventBus.Instance.Subscribe<PlayerHealthChangedEvent>(OnPlayerHealthChanged); | ||
| + | | ||
| + | private void OnDisable() => EventBus.Instance.Unsubscribe<PlayerHealthChangedEvent>(OnPlayerHealthChanged); | ||
| + | | ||
| + | private void OnPlayerHealthChanged(PlayerHealthChangedEvent @event) => text.text = $"Health: {@event.CurrentHealth} / {@event.MaxHealth}"; | ||
| + | } | ||
| + | |||
| + | </code> | ||