This is an old revision of the document!


1. Recapitulare: Sisteme Split-Screen și Arhitecturi de Input Extensibile

🎮 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

  1. Două camere active simultan (split-screen orizontal sau vertical).
  2. Două entități controlabile independent (ex: mașini, personaje).
  3. Sistem de input separat pentru fiecare jucător (keyboard + controller sau două controllere).
  4. Interfață UI distinctă per jucător (timer, vitezometru, viață etc).
  5. Sistem de Replay / Ghost Car folosind patternul `Command`.
  6. 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.

Astfel, pentru un split-screen vertical se pot folosi doua camere cu valorile urmatoare pentru ViewPort Rect (x, y, width, height)

Camera 1 : (0, 0, 0.5, 1) ► left
Camera 2 : (0.5, 0, 0.5, 1) ► right

iar pentru pentru un split-screen orizontal

Camera 1 : (0, 0.5, 1, 0.5) ► top
Camera 2: (0, 0, 1, 0.5) ► bottom

  • 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 (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).

  • 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

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.

🧩 Exemple de design pattern-uri

🔸 Command Pattern (pentru input)

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);
    }
}

🔸 Observer Pattern (pentru UI)

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";
}

🎬 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.
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);
}
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;
}
public class GhostCarPlayer : MonoBehaviour
{
    public CarController ghostCar;
    public List<ICommand> replayCommands;
    private int currentIndex = 0;
    private float startTime;
 
    void Start() => startTime = Time.time;
 
    void Update()
    {
        if (currentIndex >= replayCommands.Count) return;
 
        float elapsed = Time.time - startTime;
        ICommand cmd = replayCommands[currentIndex];
 
        if (elapsed >= cmd.Timestamp)
        {
            cmd.Execute(ghostCar);
            currentIndex++;
        }
    }
}
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);
}

🧩 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.

📚 Resurse recomandate


pjv/laboratoare/2025/a01.1759648571.txt.gz · Last modified: 2025/10/05 10:16 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