This is an old revision of the document!
Î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.
Exista mai multe metode de a face un joc controlabil de mai multi jucatori in acelasi timp:
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
Ex.: `Camera.rect = new Rect(0f, 0.5f, 1f, 0.5f)` pentru ecran divizat orizontal.
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).
Controlul jucatorilor va trebui sa se codeze separat prin definirea de axe sau taste specifice fiecarui jucator.
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); } }
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"; }
Î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:
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); }
Creezi două mașini:
După terminarea cursei:
Rulezi scena în mod replay → GhostCar se mișcă exact ca jucătorul.
Avantaje ale Command Pattern aici:
Extensii posibile:
Game Programming Patterns, 2014