Differences

This shows you the differences between two versions of the page.

Link to this comparison view

pjv:laboratoare:2024:a02 [2024/10/21 11:29]
alexandru.gradinaru [Sisteme turn based]
pjv:laboratoare:2024:a02 [2024/10/23 08:45] (current)
maria_anca.balutoiu [Sisteme de evenimente]
Line 47: Line 47:
     * Inamicii realizeaza actiuni random     * Inamicii realizeaza actiuni random
  
-Pentru simplitate, toate interactiunile se pot realiza prin butoane de comanda.+Pentru simplitate, toate interactiunile se pot realiza prin butoane de comanda, dar este necesar sa folositi un sistem de evenimente pentru decuplarea responsabilitatilor (la alegere).
  
 Bonusuri: Bonusuri:
Line 75: Line 75:
  
 === Gestiune harta === === Gestiune harta ===
 +
 +Desi se poate gestiona o harta turn based si fara un sistem grid, cele mai adesea se folosesc sisteme grid de tip matrice sau hexagonal, ca si in imaginile exemplificate mai sus.
 +
 +Daca in cazul unui sistem de tip matrice reprezentarea in memorie este simpla, folosind direct o matrice, in cazul unui sistem hexagonal, lucrurile sunt ceva mai complicate.
 +
 +Cateva resurse in acest sens: 
 +  * [[https://​www.redblobgames.com/​grids/​parts/​]]
  
 === Gestiune personaje === === Gestiune personaje ===
 +
 +Pentru gestiunea personajelor sunt importante 3 elemente:
 +  * gestiunea pe harta: miscare, constrangeri,​ efecte, pozitionare,​ pathfinding etc
 +  * gestiunea prin interfata grafica: abilitati, skilltree, actiuni etc
 +  * gestiunea in memorie
 +
 +
 +Intrucat sunt foarte multe elemente de gestionat, recomandarea este sa se incerce o decuplare a acestor componente, din punct de vedere logic.
 +
 +Pentru selectia personajelor cel mai adesea se folosesc RayCast-uri.
 +Pentru selectie multipla (mai multe personaje/​unitati odata) o varianta poate fi:
 +  * Obține raycast a poziției în care jucătorul a început să tragă ​
 +  * Obțineți raycast a poziției în care jucătorul a terminat de tras
 +  * Din aceste două Vector3 puteți construi un cub 3D.
 +  * Tratați-l ca pe un plan 2D (adică ignorați înălțimea cubului);
 +  * Pentru fiecare unitate: dacă unitatea se află în planul 2D, selectați-o.
  
 === Gestiune comenzi === === Gestiune comenzi ===
Line 106: Line 129:
 </​code>​ </​code>​
  
 +În acest caz, fiecare acțiune de joc va aplica interfața ICommand (puteți implementa aceasta și cu o clasă abstractă).
  
 +Fiecare obiect de comandă va fi responsabil pentru propriile sale metode Execute și Undo. Prin urmare, adăugarea mai multor comenzi în joc nu le va afecta pe cele existente.
 +
 +Clasa CommandInvoker este apoi responsabilă pentru executarea și anularea comenzilor. În plus față de metodele ExecuteCommand și UndoCommand,​ are o stivă de anulare pentru a păstra secvența de obiecte de comandă.
 +
 +O opțiune simplă pentru a schimba poziția jucătorului este crearea unui PlayerMover.
 +
 +Pentru a face acest lucru, va trebui să treceți un Vector3 în metoda Mutare pentru a ghida jucătorul de-a lungul celor patru direcții ale busolei. De asemenea, puteți utiliza un raycast pentru a detecta pereții în LayerMask corespunzătoare. Desigur, implementarea a ceea ce doriți să aplicați command pattern este separată de modelul în sine.
 +
 +
 +
 +Pentru a urma command pattern, capturați metoda de mutare a PlayerMover ca obiect. În loc să apelați direct Move, creați o nouă clasă, MoveCommand,​ care implementează interfața ICommand.
 +
 +<code c#>
 +public class MoveCommand : ICommand
 +{
 +PlayerMover playerMover;​
 +Vector3 movement;
 +public MoveCommand(PlayerMover player, Vector3 moveVector)
 +{
 +this.playerMover = player;
 +this.movement = moveVector;
 +}
 +public void Execute()
 +{
 +playerMover.Move(movement);​
 +}
 +public void Undo()
 +{
 +playerMover.Move(-movement);​
 +}
 +}
 +</​code>​
 +
 +Indiferent de logică pe care doriți să o realizați, intră aici, așa că invocați Move cu vectorul de mișcare.
 +
 +De asemenea, ICommand are nevoie de o metodă Undo pentru a restabili scena la starea anterioară. În acest caz, logica Undo scade vectorul de mișcare, împingând în esență jucătorul în direcția opusă.
 +
 +MoveCommand stochează toți parametrii necesari pentru a-i executa. Setați-le cu un constructor. În acest caz, salvați componenta PlayerMover corespunzătoare și vectorul de mișcare.
 +
 +Odată ce creați obiectul de comandă și salvați parametrii necesari acestuia, utilizați metodele statice ExecuteCommand și UndoCommand ale CommandInvoker pentru a transmite MoveCommand. Aceasta rulează Execute sau Undo din MoveCommand și urmărește obiectul de comandă din stiva de anulare.
 +
 +InputManager nu apelează direct metoda PlayerMover'​s Move. În schimb, adăugați o metodă suplimentară,​ RunMoveCommand,​ pentru a crea o nouă MoveCommand și a o trimite către CommandInvoker.
 +
 +Apoi, configurați diferitele evenimente onClick ale butoanelor UI pentru a apela RunPlayerCommand cu cei patru vectori de mișcare.
 +
 +Mai multe detalii si exemple aici: [[https://​unity.com/​how-to/​use-command-pattern-flexible-and-extensible-game-systems]]
  
 ==== Sisteme de evenimente ==== ==== Sisteme de evenimente ====
 +
 +In Unity, un motor de jocuri bazat pe **componente**,​ un aspect negresit pe care-l veti intalni pentru orice tip de joc sau aplicatie este **comunicarea intre diversele componente ale acestuia**.
 +
 +Exista diverse modele de comunicare, in capitolul asta vom sublinia cateva ce tin de **evenimente**.
 +
 +In esenta, un **eveniment** descrie o relatie de tip **broadcaster-listener**. In aceasta relatie avem, in general, un obiect care defineste evenimentul si-l **invoca** (face broadcast) si N alte obiect care **asculta** acest eveniment. Avantajul principal pentru utilizarea acestui mecanism este **decuplarea** sistemelor in cod - in special pe masura ce sistemele din joc devin mai numeroase si complexe.
 +
 +=== Referinte directe ===
 +
 +Cea mai simpla modalitate de comunicare este cea prin **referinte directe**. Scriptul A are o referinta la scriptul B - iar scriptul A **apeleaza in mod direct** metode ale scriptului B. In cazul acesta avem **tight coupling** intrucat A trebuie sa cunoasca detaliile lui B pentru a realiza comunicarea.
 +
 +Acest model de comunicare nu este gresit (in esenta nici unul din cele prezentate nu e "cel mai bun", ci depinde de context), dar se preteaza doar in cazul unor sisteme de dimensiune redusa, care se presupun ca nu vor suferi multe schimbari pe viitor.
 +
 +Imaginati-va ca aveti scriptul de ''​Player'',​ precum si N alte sub-sisteme ale acestuia - ''​PlayerUIManager'',​ ''​PlayerSoundManager'',​ etc. - scriptul de ''​Playe''​r va necesita o referinta catre fiecare dintre cele N subsisteme pentru a putea comunica. Vice-versa in cazul in care celelalte sisteme trebuie sa comunice cu Player-ul.
 +
 +Un exemplu minimal este prezentat in urmatorul snippet:
 +
 +<code c#>
 +public class Player : MonoBehaviour
 +{
 +   ​[SerializeField] private PlayerUIManager uiManager;
 +   ​[SerializeField] private PlayerSoundManager soundManager;​
 +   
 +   ​private void UpdateSubsystems()
 +   {
 +       // Direct references - tight coupling.
 +       ​uiManager.UpdateUI();​
 +       ​soundManager.PlaySomeSound();​
 +   }
 +}
 +</​code>​
 +
 +=== Evenimente C# (Action) ===
 +
 +Acestea reprezinta un mecanism care ne ajuta in a **decupla aceste componente**. Aderam astfel intr-un mod mai activ la [[https://​stackify.com/​solid-design-principles/​|Single-responsibility principle]] ​ - in acest mod, script-ul Player nu mai are nevoie de referinte catre subsistemele sale, ci doar **implementeaza un eveniment** la care va face broadcast.
 +
 +In esenta, pe Player nu-l intereseaza cine asculta - acesta doar va **emite acest mesaj** si este responsabilitatea celorlalte subsisteme sa **asculte** acest eveniment si sa **raspunda** in mod corespunzator.
 +
 +Totusi, in acest caz, subsistemele trebuie sa aiba o referinta catre player. Dar per total comunicarea este **loosely coupled**. Daca implementam cu grija si expunem via specificatorii de acces (public, protected, private) doar ce este de interes (in acest exemplu doar evenimentul),​ componenetele vor stii doar cat au nevoie din implementarea celorlalte componente.
 +
 +Folosind acest sistem avem broadcaster-ul,​ care va emite / invoca evenimentul si ascultatorii,​ care trebuie sa se **aboneze** la acesta. Nu uitati sa va si **dezabonati** in mod explicit! ​
 +
 +In snippet-ul urmator este prezentat un exemplu sumar:
 +
 +<code c#>
 +public class Player : MonoBehaviour
 +{
 +   // This is the broadcaster. It does not care who else is listening to this.
 +   // Note that adding the `event` keyword enforces stricter rules for the `Action`:
 +   // ​ - This can only be invoked in the defined class (this one, `Player`)
 +   ​public event Action<​int>​ OnPlayerHealthChanged;​
 +
 +   ​private void NotifyPlayerHealthChanged(int health)
 +   {
 +       ​OnPlayerHealthChanged?​.Invoke(health);​
 +   }
 +}
 +
 +~~~~~
 +
 +public class PlayerUIManager : MonoBehaviour
 +{
 +   // Reference to Player
 +   ​[SerializeField] private Player player;
 +   
 +   // Subscribe to event (listen).
 +   ​private void OnEnable() => player.OnPlayerHealthChanged += HandlePlayerHealthChanged;​
 +  ​
 +   // Unsubscribe from the event (stop listening).
 +   ​private void OnDisable() => player.OnPlayerHealthChanged -= HandlePlayerHealthChanged;​
 +   
 +   // Method which is called on event invocation
 +   ​private void HandlePlayerHealthChanged(int healthValue)
 +   {
 +       // Do something with `healthValue` ​ ...
 +   }
 +}
 +</​code>​
 +
 +=== UnityEvents ===
 +
 +Acesta este mecanismul specific Unity pentru implementarea comunicarii bazate pe evenimente, este in esenta un **wrapper peste metodologia C#** prezentata anterior.
 +Avantajul folosirii acesteia este ca putem lega sisteme in cod intr-un mod si mai loosely coupled, intrucat ne putem baza pe **adaugarea de referinte intre componenete direct in inspectorul din Unity**, neavand neaparat nevoie de referinte in cod.
 +
 +Tineti totusi minte ca C# events sunt in general mai performante,​ intrucat UnityEvents introduc un mic overhead - acest aspect devine relevant atunci cand aveti foarte multi broadcasteri si listeneri, asadar va recomandam sa folositi oricare in functie de contextul necesar.
 +
 +<code c#>
 +public class Player : MonoBehaviour
 +{
 +   // This is the broadcaster. It does not care who else is listening to this.
 +   ​public UnityEvent<​int>​ onPlayerHealthChanged;​
 +   
 +   ​private void NotifyPlayerHealthChanged(int health)
 +   {
 +       ​onPlayerHealthChanged?​.Invoke(health);​
 +   }
 +}
 +
 +~~~~~
 +
 +public class PlayerUIManager : MonoBehaviour
 +{
 +   // No need to reference the Player anymore in code! But make sure to assign this in the inspector.
 +  ​
 +   // Method which is called on event invocation.
 +   ​public void HandlePlayerHealthChanged(int healthValue)
 +   {
 +       // Do something with `healthValue` ​ ...
 +   }
 +}
 +</​code>​
 +
 +{{ :​pjv:​laboratoare:​2024:​pajv_l2_eventsinspector.png?​500 |}}
 +
 +=== Pattern de decuplare via UnityEvents si Scriptable Objects ===
 +
 +Imaginati-va un joc in care aveti Player-ul si UI-ul acestuia sub forma unui prefab. Daca, spre exemplu, trebuie sa instantiati prefab-ul de UI dupa ce deja ati instantiat Player-ul, nu puteti, intr-un mod usor, sa foloseti metodele descrise anterior - cum iau referinta catre Player? Cum setez in inspector referinta de care am nevoie?
 +
 +Un raspuns ar fi folosind ''​FindObjectOfType'',​ dar in general nu este o metoda recomandata,​ in primul rand datorita performance hit-ului (trebuie iterat prin toate obiectele scenei). In plus, imaginati-va un context multiplayer,​ in care aveti N game objects care au atasat script-ul Player - pe care-l referentiez?​
 +
 +O modalitate pentru a rezolva problema referentierii in scopul comunicarii intre obiecte si pentru a mentine conditia decuplarii intre acestea este sa folosesc un **Scriptable Object ca un middleman** intre diverse subsisteme.
 +
 +Cum ar functiona pe scurt:
 +  * Scriptul A apeleaza o metoda implementata pe Scriptable Object
 +  * Scriptable Object-ul va invoca la acest moment un eveniment (event X sa zicem)
 +  * Script-ul B asculta evenimentul X
 +
 +<note tip>
 +  * Gasiti [[https://​unity.com/​how-to/​architect-game-code-scriptable-objects|aici]] documentatie legata de acest pattern
 +  * Gasiti [[https://​www.youtube.com/​watch?​v=qUYpQ8ySkLU&​|aici]] un video ce intra in mai multe detalii legate de implementare
 +</​note>​
 +
 +Astfel, **decuplam aproape complet** script-ul A de B via acest Scriptable Object. Putem acum sa atasam prefab-ului de Player cat si celui de UI ca referinta acest Scriptable Object si indiferent de momentul instantierii,​ cele doua sisteme **vor putea comunica** intr-un mod decuplat - nici unul nu trebuie sa stie de existenta celuilalt. Un exemplu minimal este prezentat in urmatorul snippet:
 +
 +<code c#>
 +[CreateAssetMenu(
 +   ​fileName = "​PlayerEventSO",​
 +   ​menuName = "​Player/​PlayerEventSO"​)]
 +public class PlayerEventSO : ScriptableObject
 +{
 +   // Unity event.
 +   ​[NonSerialized] public UnityEvent<​int>​ OnPlayerHealthChanged;​
 +   
 +   // Event is initialized. `??=` is called the null-coalescing operator.
 +   // Scripts can listen to this.
 +   ​private void OnEnable() => OnPlayerHealthChanged ??= new UnityEvent<​int>​();​
 +  ​
 +   // Method called from scripts which will raise / invoke `OnPlayerHealthChanged` event.
 +   ​public void NotifyPlayerHealthChanged(int playerHealth) => OnPlayerHealthChanged.Invoke(playerHealth);​
 +}
 +
 +~~~~~
 +
 +public class Player : MonoBehaviour
 +{
 +   // Reference to Scriptable Object. Assigned in inspector.
 +   ​[SerializeField] private PlayerEventSO playerEventSO;​
 +   ​public void UpdatePlayerHealth(int health)
 +   {
 +       // Call Scriptable Object method.
 +       ​playerEventSO.NotifyPlayerHealthChanged(health);​
 +   }
 +}
 +
 +~~~~~
 +
 +public class PlayerUIManager : MonoBehaviour
 +{
 +   // Reference to Scriptable Object. Assigned in inspector.
 +   ​[SerializeField] private PlayerEventSO playerEventSO;​
 +   
 +   // Subscribe to event (listen).
 +   ​private void OnEnable() => playerEventSO.OnPlayerHealthChanged.AddListener(HandlePlayerHealthChan
 +ged);
 +   
 +   // Unsubscribe from the event (stop listening).
 +   ​private void OnDisable() => playerEventSO.OnPlayerHealthChanged.RemoveListener(HandlePlayerHealthChanged)
 +   
 +   // Method which is called on event invocation
 +   ​private void HandlePlayerHealthChanged(int healthValue)
 +   {
 +       // Do something with `healthValue` ​ ...
 +   }
 +}
 +</​code>​
 +
 +=== Callback-uri === 
 +
 +Ne putem folosi de ''​Action''​ pentru a defeni **callback-uri** - mecanism util in cazul in care trebuie sa notificam “inapoi” un anumit script, fara a fi nevoiti sa adaugam o referinta in plus. Spre exemplu, script-ul A ruleaza ceva pe script-ul B, dar A vrea sa stie cand B a terminat. In loc sa adaugam o referinta de la B catre A, **putem printr-un callback sa notificam** A. Un exemplu este prezentat in continuare:
 +
 +<code c#>
 +public class Player : MonoBehaviour
 +{
 +   ​[SerializeField] private OtherSystem otherSystem;​
 +   
 +   ​public void DoSomething()
 +   {
 +       // () => { } is a lambda method - simply, a function within a function
 +       // which gets executed when the `callback` is invoked.
 +       ​otherSystem.DoSomeWork(() =>
 +       {
 +           // `otherSystem` just notified me!
 +       });
 +   }
 +}
 +
 +~~~~~
 +
 +public class OtherSystem : MonoBehaviour
 +{
 +   ​public void DoSomeWork(Action callback)
 +   {
 +       // ... doing some work.
 +      ​
 +       // Let's notify the calling script that I'm done!
 +       ​callback?​.Invoke();​
 +   }
 +}
 +</​code>​
 +
 +
 +
 +
 +
 +
 +
  
pjv/laboratoare/2024/a02.1729499343.txt.gz · Last modified: 2024/10/21 11:29 by alexandru.gradinaru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0