Realizarea unui battle arena simplu, turn-based
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:
Cateva exemple, doar ca idee de inspiratie
Gasiti pe MS Teams inregistrat
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:
Pentru gestiunea personajelor sunt importante 3 elemente:
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:
Pentru a aplica comenzile la terminarea turului, acestea trebuie memorate si gestionate intr-o lista.
Command design pattern este unul dintre Gang of Four inițial și este util oricând doriți să urmăriți o serie specifică de acțiuni. Probabil că ați văzut command design pattern deja dacă ați jucat un joc care folosește funcționalitatea de undo/redo sau păstrează istoricul comenzilor într-o listă. Imaginați-vă un joc de strategie în care utilizatorul poate planifica mai multe ture înainte de a le executa efectiv. Acesta este command design pattern.
Command design pattern permite acțiunilor să fie reprezentate ca obiecte. Încapsularea acțiunilor ca obiecte vă permite să creați un sistem flexibil și extensibil pentru controlul comportamentului GameObjects ca răspuns la intrarea utilizatorului. Aceasta funcționează prin încapsularea unuia sau mai multor apeluri de metodă ca „obiect de comandă”, mai degrabă decât invocarea directă a unei metode. Apoi puteți stoca aceste obiecte de comandă într-o colecție, cum ar fi o coadă sau o stivă, care funcționează ca un mic buffer.
Stocarea obiectelor de comandă în acest fel vă permite să controlați timpul de execuție a acestora prin eventual întârzierea unei serii de acțiuni pentru redarea ulterioară. În mod similar, puteți să le refaceți sau să le anulați și să adăugați flexibilitate suplimentară pentru a controla execuția fiecărui obiect de comandă.
Iată câteva aplicații comune ale modelului în diferite genuri de joc:
Pentru a implementa un command pattern, veți avea nevoie de un obiect general care va conține acțiunea. Acest obiect de comandă va deține ce logică să efectueze și cum să o anuleze.
Există mai multe moduri de a implementa acest lucru, dar iată o versiune simplă folosind o interfață numită ICommand:
public interface ICommand { void Execute(); void Undo(); }
Î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.
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); } }
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
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.
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:
public class Player : MonoBehaviour { [SerializeField] private PlayerUIManager uiManager; [SerializeField] private PlayerSoundManager soundManager; private void UpdateSubsystems() { // Direct references - tight coupling. uiManager.UpdateUI(); soundManager.PlaySomeSound(); } }
Acestea reprezinta un mecanism care ne ajuta in a decupla aceste componente. Aderam astfel intr-un mod mai activ la 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:
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` ... } }
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.
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` ... } }
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:
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:
[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` ... } }
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:
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(); } }