using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
///
/// Beifahrer: Wer in die offene Beifahrertür an den Sitz tritt, setzt sich automatisch hin - egal ob schon jemand
/// fährt oder das Auto noch steht (die Taste geht weiterhin, auch zum Aussteigen; Leertaste steigt ebenfalls aus).
/// Der Sitz ist die Fahrerposition des Spiels (DriverHeadPivot), auf die andere Seite gespiegelt (bei Fahrzeugen mit
/// Sitz in der Mitte, z.B. Traktor oder Moped: dahinter). Während der Fahrt wird der Spieler jedes Frame auf den Sitz
/// gesetzt und dreht mit dem Fahrzeug mit; umschauen geht weiter.
///
public class Passenger
{
const float EnterRange = 3f; // per Taste: so nah muss man an der Karosserie stehen
const float AutoEnterRange = 1.0f; // automatisch: Kopf so nah am Beifahrersitz ...
const float AutoEnterSide = 0.6f; // ... und seitlich höchstens so weit außerhalb des Sitzes - an der
// geschlossenen Tür steht man weiter draußen, nur in der offenen Tür so nah
const float AutoEnterDwell = 0.35f; // ... so lange (nicht beim bloßen Vorbeilaufen)
const float AutoRearmRange = 2f; // nach dem Aussteigen erst wieder, wenn man so weit weg war
const float AutoCheckInterval = 0.15f;
const float CenterSeatThreshold = 0.2f;
const float BehindCenterSeat = 0.7f;
const float ExitSideOffset = 1.4f; // beim Aussteigen so weit neben den Sitz
// Bewegungs-Skripte des Spielers (Unity Standard Assets), während der Fahrt aus
static readonly string[] MovementScripts = { "CharacterMotor", "FPSInputController" };
SyncedBody vehicle;
Vector3 seatHeadLocal;
float lastYaw;
readonly List disabled = new List();
CharacterController controller;
// Feste Collider des Spielers, die während der Fahrt aus sind (sonst stecken sie in Sitzen und Teilen des Autos)
readonly List disabledColliders = new List();
readonly List frozenBodies = new List();
// Fahrerkopf je Fahrzeug (lokal), aus DriverHeadPivot - null = nicht gefunden
readonly Dictionary driverHeads = new Dictionary();
float nextAutoCheck, dwellStart = -1f;
SyncedBody dwellVehicle;
bool autoArmed = true;
Vector3 lastExitSeat;
public bool IsRiding { get { return vehicle != null; } }
/// Fahrzeug, in dem wir mitfahren (sonst null).
public SyncedBody Vehicle { get { return vehicle; } }
/// Kopf auf dem Sitz, relativ zum Fahrzeug (genau, nicht aus der Weltposition des letzten Frames).
public Vector3 SeatHeadLocal { get { return seatHeadLocal; } }
/// Taste gedrückt: einsteigen oder aussteigen. Gibt einen Statustext zurück.
public string Toggle(Transform player, Camera cam, ObjectSync objects, IDictionary players)
{
if (IsRiding)
{
Exit(player, cam);
return "Got out";
}
if (cam == null || objects.IsDriving) return null;
SyncedBody best = null;
Vector3 bestSeat = Vector3.zero;
float bestDist = EnterRange * EnterRange, nearest = float.MaxValue;
foreach (SyncedBody b in objects.AllVehicles())
{
if (b.Rb == null || (b.Rb.position - player.position).sqrMagnitude > 30f * 30f) continue;
float d = DistanceToVehicleSq(b, player.position);
Vector3? seat = SeatFor(b, objects, players);
if (seat == null) continue;
nearest = Mathf.Min(nearest, d);
if (d < bestDist) { bestDist = d; best = b; bestSeat = seat.Value; }
}
if (best == null)
{
if (nearest == float.MaxValue) return "Passenger: no vehicle nearby";
return "Passenger: get closer to the car (" + Mathf.Sqrt(nearest).ToString("0") + " m away, max " + EnterRange.ToString("0") + " m)";
}
Enter(best, bestSeat, player);
return "Riding as passenger (press again or Space to get out)";
}
///
/// Jedes Frame (zu Fuß): steht man mit dem Kopf am Beifahrersitz (in der offenen Tür), automatisch hinsetzen.
/// Gibt einen Statustext zurück, wenn man eingestiegen ist.
///
public string AutoUpdate(Transform player, Camera cam, ObjectSync objects, IDictionary players)
{
if (IsRiding || cam == null || player == null || objects.IsDriving) { dwellStart = -1f; return null; }
if (Time.time < nextAutoCheck) return null;
nextAutoCheck = Time.time + AutoCheckInterval;
Vector3 head = cam.transform.position;
// Nach dem Aussteigen erst einmal weggehen, sonst säße man sofort wieder drin
if (!autoArmed)
{
if ((head - lastExitSeat).sqrMagnitude > AutoRearmRange * AutoRearmRange) autoArmed = true;
else return null;
}
SyncedBody found = null;
Vector3 foundSeat = Vector3.zero;
foreach (SyncedBody b in objects.AllVehicles())
{
if (b.Rb == null || (b.Rb.position - head).sqrMagnitude > 8f * 8f) continue;
Vector3? seat = SeatFor(b, objects, players);
if (seat == null) continue;
if ((b.Rb.transform.TransformPoint(seat.Value) - head).sqrMagnitude > AutoEnterRange * AutoEnterRange) continue;
Vector3 local = b.Rb.transform.InverseTransformPoint(head);
if (Mathf.Abs(local.x) > Mathf.Abs(seat.Value.x) + AutoEnterSide) continue;
found = b;
foundSeat = seat.Value;
break;
}
if (found == null || found != dwellVehicle)
{
dwellVehicle = found;
dwellStart = found != null ? Time.time : -1f;
return null;
}
if (Time.time - dwellStart < AutoEnterDwell) return null;
dwellStart = -1f;
dwellVehicle = null;
Enter(found, foundSeat, player);
return "Riding as passenger (Space or the passenger key to get out)";
}
void Enter(SyncedBody b, Vector3 seat, Transform player)
{
seatHeadLocal = seat;
vehicle = b;
lastYaw = b.Rb.transform.eulerAngles.y;
SetMovementEnabled(player, false);
}
/// Beifahrersitz (Kopfposition, lokal im Fahrzeug) - aus dem Fahrersitz des Spiels oder dem Fahrer.
Vector3? SeatFor(SyncedBody b, ObjectSync objects, IDictionary players)
{
Vector3? driverHead;
if (!driverHeads.TryGetValue(b.Rb, out driverHead))
{
driverHead = null;
foreach (Transform t in b.Rb.GetComponentsInChildren(true))
{
if (t.name != "DriverHeadPivot") continue;
driverHead = b.Rb.transform.InverseTransformPoint(t.position);
break;
}
driverHeads[b.Rb] = driverHead;
}
// Ohne DriverHeadPivot: nur, wenn gerade jemand fährt (dessen Kopf ist die Fahrerposition)
if (driverHead == null && objects.IsRemoteOwnedBody(b) && b.OwnerActive)
{
PlayerInfo owner;
if (players.TryGetValue(b.OwnerId, out owner) && owner.Avatar != null)
driverHead = b.Rb.transform.InverseTransformPoint(owner.Avatar.HeadPosition);
}
if (driverHead == null) return null;
Vector3 d = driverHead.Value;
return Mathf.Abs(d.x) < CenterSeatThreshold
? d - new Vector3(0f, 0f, BehindCenterSeat) // Sitz in der Mitte: dahinter
: new Vector3(-d.x, d.y, d.z); // gegenüber vom Fahrer
}
///
/// Abstand zur nächsten Stelle der Karosserie (Collider), nicht zum Schwerpunkt - den legt MSC teils weit weg,
/// dann hieß es "26 m entfernt", obwohl man direkt an der Tür stand.
///
static float DistanceToVehicleSq(SyncedBody b, Vector3 point)
{
float best = (b.Rb.position - point).sqrMagnitude;
foreach (Collider c in b.Rb.GetComponentsInChildren())
{
if (c == null || !c.enabled || c.isTrigger) continue;
float d = (c.bounds.ClosestPoint(point) - point).sqrMagnitude;
if (d < best) best = d;
}
return best;
}
/// Nach der Physik (LateUpdate): Spieler auf den Sitz setzen und mitdrehen.
public void LateUpdate(Transform player, Camera cam, ObjectSync objects)
{
if (!IsRiding) return;
if (vehicle.Rb == null || cam == null)
{
Exit(player, cam);
return;
}
// Selbst ans Steuer gesetzt (Fahrersitz des Spiels): Beifahrer-Modus ohne Umsetzen beenden
if (objects.IsDriving)
{
Reset();
return;
}
Transform vt = vehicle.Rb.transform;
float yaw = vt.eulerAngles.y;
player.Rotate(0f, Mathf.DeltaAngle(lastYaw, yaw), 0f, Space.World);
lastYaw = yaw;
Vector3 cameraOffset = cam.transform.position - player.position;
player.position = vt.TransformPoint(seatHeadLocal) - cameraOffset;
}
public void Exit(Transform player, Camera cam)
{
if (!IsRiding) return;
if (player != null && vehicle.Rb != null)
{
lastExitSeat = vehicle.Rb.transform.TransformPoint(seatHeadLocal);
autoArmed = false;
// Auf der Seite des Sitzes neben das Fahrzeug stellen
float side = seatHeadLocal.x >= 0f ? 1f : -1f;
Vector3 outside = vehicle.Rb.transform.TransformPoint(seatHeadLocal + new Vector3(side * ExitSideOffset, 0f, 0f));
Vector3 cameraOffset = cam != null ? cam.transform.position - player.position : Vector3.up * 1.6f;
player.position = outside - cameraOffset + Vector3.up * 0.1f;
}
vehicle = null;
SetMovementEnabled(player, true);
}
/// Szenenwechsel/Verbindungsende: ohne Teleport alles zurücksetzen.
public void Reset()
{
vehicle = null;
dwellVehicle = null;
dwellStart = -1f;
driverHeads.Clear();
SetMovementEnabled(null, true);
}
void SetMovementEnabled(Transform player, bool enabled)
{
if (!enabled)
{
disabled.Clear();
foreach (MonoBehaviour mb in player.GetComponents())
{
if (mb == null || !mb.enabled) continue;
if (System.Array.IndexOf(MovementScripts, mb.GetType().Name) < 0) continue;
mb.enabled = false;
disabled.Add(mb);
}
controller = player.GetComponent();
if (controller != null && controller.enabled) controller.enabled = false;
else controller = null;
// Der Spieler sitzt mitten im Auto: seine festen Collider würden Sitze und Teile wegdrücken
// (Teile reißen ab, Sitze fliegen, bei Tempo stürzt die Physik ab). Wie beim echten Einsteigen: aus.
disabledColliders.Clear();
foreach (Collider c in player.GetComponentsInChildren())
{
if (c == null || !c.enabled || c.isTrigger || c is CharacterController) continue;
c.enabled = false;
disabledColliders.Add(c);
}
frozenBodies.Clear();
foreach (Rigidbody rb in player.GetComponentsInChildren())
{
if (rb == null || rb.isKinematic) continue;
rb.isKinematic = true;
frozenBodies.Add(rb);
}
}
else
{
foreach (Behaviour b in disabled) if (b != null) b.enabled = true;
disabled.Clear();
if (controller != null) controller.enabled = true;
controller = null;
foreach (Collider c in disabledColliders) if (c != null) c.enabled = true;
disabledColliders.Clear();
foreach (Rigidbody rb in frozenBodies) if (rb != null) rb.isKinematic = false;
frozenBodies.Clear();
}
}
}
}