This shows you the differences between two versions of the page.
|
pjv:laboratoare:2025:a01 [2025/09/29 08:23] alexandru.gradinaru created |
pjv:laboratoare:2025:a01 [2025/10/06 13:54] (current) alexandru.gradinaru [Concept general – „Replay / Ghost Car”] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ===== 1. Sisteme split-screen si input ===== | + | ===== 1. Recapitulare: Sisteme Split-Screen și Arhitecturi de Input Extensibile ===== |
| + | |||
| + | <hidden> | ||
| + | Alege una dintre variantele de joc de mai jos: | ||
| + | |||
| + | ==== 🔹 Variantă A – Split-Screen Time Trial ==== | ||
| + | * Fiecare jucător concurează pe o pistă proprie (split-screen orizontal/vertical). | ||
| + | * Scop: terminarea cursei în cel mai scurt timp. | ||
| + | * Se afișează în UI timpul curent și cel mai bun timp. | ||
| + | |||
| + | ==== 🔹 Variantă B – Co-op Puzzle Split-Screen ==== | ||
| + | * Doi jucători cooperează pentru a activa simultan diverse mecanisme (ex: apăsarea a două butoane, deplasarea unor obiecte). | ||
| + | * Fiecare jucător are propria cameră și sistem de control. | ||
| + | * Puzzle-urile pot fi rezolvate doar prin coordonare (ex: un jucător deschide o ușă pentru celălalt). | ||
| + | |||
| + | ---- | ||
| + | </hidden> | ||
| + | <hidden> | ||
| + | - Implementarea unui **design pattern** dintre: | ||
| + | * **Command** – pentru acțiunile de input (Accelerate, Jump, MoveLeft etc.); | ||
| + | * **Observer** – pentru actualizarea automată a UI-ului în funcție de starea personajului; | ||
| + | * **State** – pentru comportamentele jucătorului (Idle, Moving, Crashed); | ||
| + | * **Strategy** – pentru tipul de control (uman / AI). | ||
| + | </hidden> | ||
| + | |||
| + | === Cerință : Joc Split-Screen Time Trial === | ||
| + | |||
| + | * Fiecare jucător concurează pe o pistă proprie (split-screen orizontal/vertical). | ||
| + | * Scop: terminarea cursei în cel mai scurt timp. | ||
| + | * Se afișează în UI timpul curent și cel mai bun timp. | ||
| + | |||
| + | === Cerințe minime === | ||
| + | - Două camere active simultan (split-screen orizontal sau vertical). | ||
| + | - Două entități controlabile independent (ex: mașini, personaje). | ||
| + | - Sistem de input separat pentru fiecare jucător (keyboard + controller sau două controllere). | ||
| + | - Interfață UI distinctă per jucător (timer, vitezometru, viață etc). | ||
| + | - Sistem de **Replay / Ghost Car** folosind patternul `Command`. | ||
| + | - **Observer** – pentru actualizarea automată a UI-ului în funcție de starea personajului; | ||
| + | |||
| + | ---- | ||
| + | |||
| + | === Cerințe opționale (bonus) === | ||
| + | * Adăugarea unui **Player Manager** care detectează automat dispozitivele conectate și alocă controlul. | ||
| + | * Introducerea unui **AIPlayer** controlat de un `Strategy` diferit (folosind strategy pattern). | ||
| + | * **Ecran de rezultate** comun la finalul cursei. | ||
| + | * **Sunet 3D** individual pentru fiecare cameră. | ||
| + | * State – pentru comportamentele jucătorului (Idle, Moving, Crashed); | ||
| + | |||
| + | ---- | ||
| + | |||
| + | === Scopul laboratorului === | ||
| + | În acest laborator, studenții vor recapitula noțiuni esențiale de dezvoltare a jocurilor multiplayer locale (split-screen), cu accent pe gestionarea camerelor, viewport-urilor și sistemelor de input independente. | ||
| + | De asemenea, se va introduce conceptul de **arhitectură extensibilă** prin utilizarea unor **design patterns** fundamentale în dezvoltarea jocurilor video. | ||
| + | |||
| + | ---- | ||
| + | |||
| + | === Obiective === | ||
| + | * Înțelegerea conceptului de **split-screen** și a modului de configurare a camerelor multiple. | ||
| + | * Crearea unui **sistem de input modular** pentru controlul a doi jucători independenți. | ||
| + | * Implementarea unui **design pattern** (Command, Observer, Strategy sau State) într-un context de gameplay. | ||
| + | * Înțelegerea interacțiunii dintre camere, UI și gameplay logic. | ||
| + | * Familiarizarea cu **New Input System** din Unity. | ||
| + | |||
| + | ---- | ||
| + | |||
| + | === Noțiuni teoretice === | ||
| + | |||
| + | Exista mai multe metode de a face un joc controlabil de mai multi jucatori in acelasi timp: | ||
| + | * Non-network: jucatorii joaca pe acelasi dispozitiv prin ture, controland caractere diferite sau avand ecranul impartit | ||
| + | * Network: jucatorii joaca pe dispozitive diferite, existand un server sau unul din juctori preluand si rolul de server (host) iar restul fiind clienti | ||
| + | |||
| + | ==== 1. Camere și Viewport ==== | ||
| + | |||
| + | Pentru jocuri locale de multiplayer se foloseste in general o tehnica de split-screen: folosirea mai multor camere care se afiseaza impart viewport-ul intantei locale a jocului [[https://docs.unity3d.com/ScriptReference/Camera-rect.html]]. | ||
| + | |||
| + | {{ :pjv:laboratoare:inspectorcamera35.png?200 |}} | ||
| + | |||
| + | Astfel, pentru un split-screen vertical se pot folosi doua camere cu valorile urmatoare pentru ViewPort Rect (x, y, width, height) | ||
| + | |||
| + | <code> | ||
| + | Camera 1 : (0, 0, 0.5, 1) ► left | ||
| + | Camera 2 : (0.5, 0, 0.5, 1) ► right | ||
| + | </code> | ||
| + | {{ :pjv:laboratoare:camera_horizontal_splitscreen.png?600 |}} | ||
| + | iar pentru pentru un split-screen orizontal | ||
| + | <code> | ||
| + | Camera 1 : (0, 0.5, 1, 0.5) ► top | ||
| + | Camera 2: (0, 0, 1, 0.5) ► bottom | ||
| + | </code> | ||
| + | {{ :pjv:laboratoare:camera_vertical_splitscreen.png?600 |}} | ||
| + | |||
| + | |||
| + | * Fiecare jucător are propria **cameră** și propriul **viewport**. | ||
| + | Ex.: `Camera.rect = new Rect(0f, 0.5f, 1f, 0.5f)` pentru ecran divizat orizontal. | ||
| + | * Fiecare cameră urmărește o mașină sau un personaj distinct. | ||
| + | * Considerații de performanță: fiecare cameră are propriul render pass → cost suplimentar. | ||
| + | |||
| + | ==== 2. GUI adaptiv per jucător ==== | ||
| + | |||
| + | Daca se doreste implementarea unui GUI personalizat/diferit pentru fiecare jucator, se pot folosi masti de camera ([[https://docs.unity3d.com/ScriptReference/Camera-cullingMask.html|culling masks]]). | ||
| + | Se vor plasa toate obiecte Jucatorului 1 intr-un layer separat (de ex Player1Layer), si obiectele jucatorului 2 in alt layer (Player2Layer) si se va asigura ca pentru fiecare camera sunt selectate layer-urile corecte (se vor exclude pentru camera 1 layer-ul cu Player2, si pentru camera 2 layer-ul cu Player 1). | ||
| + | |||
| + | {{ :pjv:laboratoare:layer-cullingmask.png?200 |}} | ||
| + | |||
| + | |||
| + | * UI-ul fiecărui jucător trebuie izolat (`Canvas` separat, *World Space* sau *Screen Space – Camera*). | ||
| + | * Folosirea **prefab-urilor** pentru HUD per jucător (vitezometru, timer, scor). | ||
| + | |||
| + | ==== 3. Sisteme de input ==== | ||
| + | |||
| + | <hidden> | ||
| + | Controlul jucatorilor va trebui sa se codeze separat prin definirea de axe sau taste specifice fiecarui jucator. | ||
| + | |||
| + | * Diferențe între **Old Input Manager** și **New Input System**: | ||
| + | * `PlayerInput` și `InputAction` oferă mapare pe dispozitive (tastatură, controller). | ||
| + | * Se pot instanția automat controale multiple prin `PlayerInputManager`. | ||
| + | * Concept: fiecare jucător are un **InputHandler** propriu. | ||
| + | </hidden> | ||
| + | |||
| + | === Sistemul de input legacy (Unity) === | ||
| + | |||
| + | În sistemul legacy de input din Unity testarea acțiunilor este destul de simplă în cod, dar are câteva limitări. În cel mai simplu mod, input-ul se poate testa prin polling-ul unor butoane de input specifice. | ||
| + | |||
| + | <code c#> | ||
| + | public class LegacyInputExample : MonoBehaviour | ||
| + | { | ||
| + | // Update is needed to poll every frame. | ||
| + | private void Update() | ||
| + | { | ||
| + | // I want 3 buttons to call the same logic: LMB (mouse), Space (key), LCtrl (key). | ||
| + | // In the legacy system, I need to hardcode the keycodes. | ||
| + | if (Input.GetMouseButtonDown(0) || | ||
| + | Input.GetKeyDown(KeyCode.Space) || | ||
| + | Input.GetKeyDown(KeyCode.LeftControl)) | ||
| + | { | ||
| + | Debug.Log("[LegacyInputExample] Action performed!"); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Definirea unui control split-scren / local co-op se poate realiza și folosind sistemul legacy, dar este probabil să necesite unele hardcoding-uri, așadar varianta recomandată pentru tratarea input-ului (fie ca e single-player sau nu) este folosind noul sistem bazat pe acțiuni, prezentat în continuare. | ||
| + | |||
| + | === Sistemul de input baza pe acțiuni (Unity) === | ||
| + | |||
| + | Noul sistem de input abstractizează partea de testare manuală a acestor input-uri prin **mapping-uri** și **acțiuni**. | ||
| + | |||
| + | Un **mapping** definește un set de acțiuni legate de anumite input-uri **fizice** (cum ar fi tastatura, mouse-ul, controllerele, etc.). Practic, un mapping este o **schemă de control** care asociază diferite acțiuni cu diverse metode de input. Puteți să vă gândiți la un mapping ca la modul în care un player interacționează cu jocul în funcție de context. Câteva exemple de mapping-uri: | ||
| + | * **MovementOnFoot** pentru mișcare pedestră (WASD pe tastatură sau joystick pe un controller) | ||
| + | * **MovementInCar** pentru condusul unei mașini (**aceleași taste WASD** pe tastatură sau joystick pe un controller, dar într-un alt context!) | ||
| + | * **MovementInBoat** pentru navigarea cu o barcă | ||
| + | |||
| + | O **acțiune** reprezintă o interacțiune definită de utilizator care poate fi activată prin diverse tipuri de input-uri. Acțiunile se pot lega la una sau mai multe intrări fizice printr-un **binding**. | ||
| + | |||
| + | Pentru a folosi acest sistem de input: | ||
| + | * Trebuie să aveți pachetul **Input System** integrat în proiect | ||
| + | * La o cale dorită în **Project window** din Unity, **click-dreapta -> Create -> Input Actions** | ||
| + | * În acest asset puteți crea mapping-uri, acțiuni și binding-uri, un exemplu este prezentat în imaginea de mai jos. | ||
| + | * De asemenea, este util să bifați **Generate C# Class** din inspectorul acestui asset pentru a-l utiliza mai ușor în script-urile voastre | ||
| + | |||
| + | {{ :irva:laboratoarevr:irva_2024_vr_l3_newinputsystem.png?700 |}} | ||
| + | |||
| + | În final, următorul snippet prezintă modul de utilizare al acestui sistem. Comentariile din cod ar trebuie să fie explicative. | ||
| + | |||
| + | <code c#> | ||
| + | public class ActionsInputExample : MonoBehaviour | ||
| + | { | ||
| + | private InputActionAssetExample playerInputActions; | ||
| + | private InputAction fireAction; | ||
| + | |||
| + | // Initialize input asset. | ||
| + | private void Awake() => playerInputActions = new InputActionAssetExample(); | ||
| + | |||
| + | // Get the action `Fire` from the mapping `PlayerMapping`. Enable it & bind event. | ||
| + | private void OnEnable() | ||
| + | { | ||
| + | fireAction = playerInputActions.PlayerMapping.Fire; | ||
| + | fireAction.Enable(); | ||
| + | fireAction.performed += FirePerformed; | ||
| + | } | ||
| + | |||
| + | private void OnDisable() | ||
| + | { | ||
| + | fireAction.Disable(); | ||
| + | fireAction.performed -= FirePerformed; | ||
| + | } | ||
| + | |||
| + | // Called whenever any of the bindings defined for the `Fire` action are called. | ||
| + | // No need for explicit checks or the `Update` method! | ||
| + | private void FirePerformed(InputAction.CallbackContext context) => Debug.Log("Fired action!"); | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | În mod alternativ, aceeași funcționalitate se poate obține prin polling în ''Update'' folosind ''ReadValue'' astfel: | ||
| + | |||
| + | <code c#> | ||
| + | public class ActionsInputExample : MonoBehaviour | ||
| + | { | ||
| + | private InputActionAssetExample _playerInputActions; | ||
| + | |||
| + | private void Awake() => _playerInputActions = new InputActionAssetExample(); | ||
| + | |||
| + | private void OnEnable() => _playerInputActions.Enable(); | ||
| + | |||
| + | private void OnDisable() => _playerInputActions.Disable(); | ||
| + | |||
| + | private void Update() | ||
| + | { | ||
| + | var fireAction = _playerInputActions.PlayerMapping.Fire.ReadValue<float>(); | ||
| + | if (fireAction > 0.5f) | ||
| + | { | ||
| + | Debug.Log("Fire action is currently being pressed."); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | === Control schemes (grupuri de binding-uri) utile pentru split-screen === | ||
| + | |||
| + | Pentru a diferenția input-ul fiecărui jucător, o metodă facilă este reprezentată de **control schemes**. Din **Input Action Editor** puteți crea un nou **Control scheme** sau grup, căruia în alăturăm binding-uri, astfel diferențiind input-ul fiecărui jucător. De exemplu, se poate crea o schemă de control pentru tastatură și alta pentru gamepad -- apoi, fiecare binding este selectat, iar în panoul din dreapta **Action Properties** se poate atribui apoi acest binding unui scheme de control. | ||
| + | |||
| + | Mai jos se observă un binding în grupul **Keyboard**. | ||
| + | |||
| + | {{ :irva:laboratoarevr:pajv_2025_l1_schema_1.png?500 |}} | ||
| + | |||
| + | În următoarea imagine sunt afișate toate schemele de control. Observați numele grupului între paranteze pătrate. | ||
| + | |||
| + | {{ :irva:laboratoarevr:pajv_2025_l1_schema_2.png?500 |}} | ||
| + | |||
| + | ---- | ||
| + | |||
| + | |||
| + | |||
| + | === Exemple de design pattern-uri === | ||
| + | |||
| + | ==== Command Pattern (pentru input) ==== | ||
| + | <code csharp> | ||
| + | public interface ICommand { | ||
| + | void Execute(CarController car); | ||
| + | } | ||
| + | |||
| + | public class AccelerateCommand : ICommand { | ||
| + | public void Execute(CarController car) => car.Accelerate(); | ||
| + | } | ||
| + | |||
| + | public class PlayerInputHandler : MonoBehaviour { | ||
| + | public CarController car; | ||
| + | private ICommand accelerate = new AccelerateCommand(); | ||
| + | void Update() { | ||
| + | if (Input.GetKey(KeyCode.W)) accelerate.Execute(car); | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | ==== Observer Pattern (pentru UI) ==== | ||
| + | <code csharp> | ||
| + | public class Car : MonoBehaviour { | ||
| + | public event Action<float> OnSpeedChanged; | ||
| + | private float speed; | ||
| + | void Update() { | ||
| + | speed = Mathf.Abs(GetComponent<Rigidbody>().velocity.magnitude); | ||
| + | OnSpeedChanged?.Invoke(speed); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | public class Speedometer : MonoBehaviour { | ||
| + | [SerializeField] private Text text; | ||
| + | public void Subscribe(Car car) => car.OnSpeedChanged += UpdateUI; | ||
| + | private void UpdateUI(float s) => text.text = $"{s:0.0} km/h"; | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ==== Concept general – „Replay / Ghost Car” ==== | ||
| + | |||
| + | În timpul cursei, fiecare acțiune de control (accelerare, frânare, viraj) este înregistrată ca un Command cu un timestamp. | ||
| + | |||
| + | După terminarea cursei, aceste comenzi pot fi reluate pentru a reproduce mișcarea exactă a mașinii – rezultând un Ghost Car. | ||
| + | |||
| + | Clase principale: | ||
| + | |||
| + | * ICommand – interfață generală pentru toate comenzile. | ||
| + | * AccelerateCommand, BrakeCommand, TurnCommand – implementări concrete. | ||
| + | * CommandRecorder – înregistrează comenzile în timpul jocului. | ||
| + | * CommandPlayer – redă comenzile în ordinea și la timpul în care au fost executate. | ||
| + | * CarController – execută acțiunile asupra mașinii. | ||
| + | |||
| + | {{ :pjv:laboratoare:2025:chatgpt_image_oct_5_2025_10_18_30_am.png?800 |}} | ||
| + | |||
| + | <code csharp> | ||
| + | public interface ICommand | ||
| + | { | ||
| + | void Execute(CarController car); | ||
| + | float Timestamp { get; set; } | ||
| + | } | ||
| + | |||
| + | public class AccelerateCommand : ICommand | ||
| + | { | ||
| + | public float Timestamp { get; set; } | ||
| + | public void Execute(CarController car) => car.Accelerate(); | ||
| + | } | ||
| + | |||
| + | public class BrakeCommand : ICommand | ||
| + | { | ||
| + | public float Timestamp { get; set; } | ||
| + | public void Execute(CarController car) => car.Brake(); | ||
| + | } | ||
| + | |||
| + | public class TurnCommand : ICommand | ||
| + | { | ||
| + | public float Timestamp { get; set; } | ||
| + | private float direction; | ||
| + | public TurnCommand(float dir) { direction = dir; } | ||
| + | public void Execute(CarController car) => car.Turn(direction); | ||
| + | } | ||
| + | |||
| + | </code> | ||
| + | |||
| + | <code csharp> | ||
| + | public class PlayerInputRecorder : MonoBehaviour | ||
| + | { | ||
| + | public CarController car; | ||
| + | private List<ICommand> recorded = new List<ICommand>(); | ||
| + | private float startTime; | ||
| + | |||
| + | void Start() => startTime = Time.time; | ||
| + | |||
| + | void Update() | ||
| + | { | ||
| + | float current = Time.time - startTime; | ||
| + | |||
| + | if (Input.GetKey(KeyCode.W)) | ||
| + | Record(new AccelerateCommand { Timestamp = current }); | ||
| + | |||
| + | if (Input.GetKey(KeyCode.S)) | ||
| + | Record(new BrakeCommand { Timestamp = current }); | ||
| + | |||
| + | if (Input.GetKey(KeyCode.A)) | ||
| + | Record(new TurnCommand(-1f) { Timestamp = current }); | ||
| + | |||
| + | if (Input.GetKey(KeyCode.D)) | ||
| + | Record(new TurnCommand(1f) { Timestamp = current }); | ||
| + | } | ||
| + | |||
| + | void Record(ICommand cmd) | ||
| + | { | ||
| + | cmd.Execute(car); // aplicăm acțiunea în timp real | ||
| + | recorded.Add(cmd); // și o înregistrăm pentru replay | ||
| + | } | ||
| + | |||
| + | public List<ICommand> GetRecordedCommands() => recorded; | ||
| + | } | ||
| + | |||
| + | </code> | ||
| + | |||
| + | <code csharp> | ||
| + | public class GhostCarPlayer : MonoBehaviour | ||
| + | { | ||
| + | public CarController ghostCar; | ||
| + | public List<ICommand> replayCommands; | ||
| + | private int currentIndex = 0; | ||
| + | private float startTime; | ||
| + | |||
| + | void Start() => startTime = Time.time; | ||
| + | |||
| + | void FixedUpdate() | ||
| + | { | ||
| + | if (currentIndex >= replayCommands.Count) return; | ||
| + | |||
| + | float elapsed = Time.time - startTime; | ||
| + | ICommand cmd = replayCommands[currentIndex]; | ||
| + | |||
| + | if (elapsed >= cmd.Timestamp) | ||
| + | { | ||
| + | cmd.Execute(ghostCar); | ||
| + | currentIndex++; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | </code> | ||
| + | |||
| + | <code csharp> | ||
| + | public class CarController : MonoBehaviour | ||
| + | { | ||
| + | private Rigidbody rb; | ||
| + | |||
| + | void Awake() => rb = GetComponent<Rigidbody>(); | ||
| + | |||
| + | public void Accelerate() => rb.AddForce(transform.forward * 5f, ForceMode.Acceleration); | ||
| + | public void Brake() => rb.velocity *= 0.95f; | ||
| + | public void Turn(float direction) => transform.Rotate(0, direction * 100f * Time.deltaTime, 0); | ||
| + | } | ||
| + | |||
| + | </code> | ||
| + | |||
| + | |||
| + | ==== Integrare în scenă ==== | ||
| + | |||
| + | Creezi două mașini: | ||
| + | |||
| + | * PlayerCar → are CarController + PlayerInputRecorder | ||
| + | * GhostCar → are CarController + GhostCarPlayer | ||
| + | |||
| + | După terminarea cursei: | ||
| + | |||
| + | * salvezi lista de comenzi din PlayerInputRecorder | ||
| + | * o transmiți către GhostCarPlayer.replayCommands | ||
| + | |||
| + | Rulezi scena în mod replay → GhostCar se mișcă exact ca jucătorul. | ||
| + | |||
| + | |||
| + | Avantaje ale Command Pattern aici: | ||
| + | |||
| + | * Separă complet inputul de logică — același cod de mișcare poate fi refolosit pentru AI, replay sau multiplayer. | ||
| + | * Poți salva comenzile pe disc (JSON) și relua oricând. | ||
| + | * Permite debugging ușor: poți „derula” fiecare comandă înapoi sau înainte. | ||
| + | |||
| + | Extensii posibile: | ||
| + | |||
| + | * Adaugă un RecordToFile() care serializează comenzile în JSON. | ||
| + | * Adaugă un replay speed slider (1x, 2x, slow-motion). | ||
| + | * Adaugă vizualizare „fantomă” cu shader semitransparent pentru GhostCar. | ||
| + | |||
| + | |||
| + | |||
| + | === Resurse recomandate === | ||
| + | * Unity Manual – [[https://docs.unity3d.com/Manual/MultipleCameras.html|Multiple Cameras and Split Screen]] | ||
| + | * Unity Learn – [[https://learn.unity.com/project/input-system|New Input System Fundamentals]] | ||
| + | * Game Programming Patterns – [[https://gameprogrammingpatterns.com/command.html|Command Pattern]] | ||
| + | * Bob Nystrom – ''Game Programming Patterns'', 2014 | ||
| + | |||
| + | ---- | ||
| + | |||
| + | |||
| + | <hidden> | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | === 🧩 Discuții finale === | ||
| + | * Cum se compară un design procedural (fără pattern-uri) cu unul bazat pe pattern-uri? | ||
| + | * Cum putem extinde arhitectura actuală pentru multiplayer online? | ||
| + | * Ce alte pattern-uri ar fi utile în Unity (Singleton, Object Pool, MVC, Component-based)? | ||
| + | |||
| + | ---- | ||
| + | |||
| + | === 🧠 Întrebări de reflecție === | ||
| + | - Care este diferența între camere și viewport-uri? | ||
| + | - Cum putem face UI-ul unui jucător să nu se suprapună peste al celuilalt? | ||
| + | - Ce avantaje aduce **Command** pattern în tratarea inputului? | ||
| + | - Ce se întâmplă dacă vrem să adăugăm un al treilea jucător? Cum scalăm arhitectura? | ||
| + | |||
| + | ---- | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | </hidden> | ||