using System.Collections.Generic; using UnityEngine; namespace MSCO { /// /// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten, /// sobald das Fahrzeug FÄHRT (egal wer fährt). Am stehenden Auto wird niemand automatisch festgehalten - sonst hing /// man fest, sobald man sich zum Schrauben in den Motorraum beugte. Aussteigen mit Leertaste oder der Beifahrer-Taste. /// Steht man per Taste neben dem Auto, kommt man auf den Beifahrersitz (Fahrerposition des Spiels, gespiegelt). /// Während der Fahrt wird der Spieler jedes Frame an seine Stelle im Fahrzeug gesetzt und dreht mit; umschauen geht. /// 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.15f; // ... so lange const float MovingSpeed = 1.2f; // m/s: ab hier "fährt" das Fahrzeug (dann wird man festgehalten) 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, lastDriving = -100f; const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch SyncedBody dwellVehicle; bool autoArmed = true; // Geschwindigkeit je Fahrzeug aus der Bewegung (Fahrzeuge anderer sind kinematisch - deren velocity ist 0) readonly Dictionary lastVehiclePos = new Dictionary(); readonly Dictionary vehicleSpeed = new Dictionary(); float lastSpeedSample = -1f; 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 inside = VehicleAround(objects, cam.transform.position); if (inside != null) { Enter(inside, inside.Rb.transform.InverseTransformPoint(cam.transform.position), player); return "Riding along (press again or Space to get out)"; } 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ß): ist man mit dem Kopf IM Fahrzeug (Kabine, Ladefläche ...), wird die aktuelle Position im /// Fahrzeug festgehalten - man fährt genau dort mit, wo man ist (kein Umsetzen auf einen berechneten Sitz). /// Gibt einen Statustext zurück, wenn man "eingestiegen" ist. /// public string AutoUpdate(Transform player, Camera cam, ObjectSync objects, IDictionary players) { if (objects.IsDriving) lastDriving = Time.time; if (IsRiding || cam == null || player == null || objects.IsDriving) { dwellStart = -1f; return null; } // Beim Ein-/Aussteigen am Fahrersitz übernimmt das Spiel die Steuerung (CharacterController aus, Spieler an ein // Fahrzeug gehängt) - dann und kurz nach dem Fahren nicht festhalten if (Time.time - lastDriving < AfterDrivingPause) { dwellStart = -1f; return null; } CharacterController cc = player.GetComponent(); if ((cc != null && !cc.enabled) || player.parent != null) { lastDriving = Time.time; dwellStart = -1f; return null; } if (Time.time < nextAutoCheck) return null; nextAutoCheck = Time.time + AutoCheckInterval; SampleSpeeds(objects); Vector3 head = cam.transform.position; // Nach dem Aussteigen erst einmal aus dem Fahrzeug heraus, sonst säße man sofort wieder drin if (!autoArmed) { if (VehicleAround(objects, head) == null) autoArmed = true; else return null; } SyncedBody found = VehicleAround(objects, head); if (found != null && SpeedOf(found) < MovingSpeed) found = null; // steht: frei bewegen (z.B. schrauben) 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, found.Rb.transform.InverseTransformPoint(head), player); // genau hier festhalten return "Riding along (Space or the passenger key to get out)"; } /// Geschwindigkeit aller Fahrzeuge in der Nähe aus ihrer Bewegung seit der letzten Messung. void SampleSpeeds(ObjectSync objects) { float now = Time.time, dt = lastSpeedSample < 0f ? 0f : now - lastSpeedSample; lastSpeedSample = now; foreach (SyncedBody b in objects.AllVehicles()) { if (b.Rb == null) continue; Vector3 pos = b.Rb.position, last; if (dt > 0.01f && lastVehiclePos.TryGetValue(b.Rb, out last)) vehicleSpeed[b.Rb] = (pos - last).magnitude / dt; lastVehiclePos[b.Rb] = pos; } } float SpeedOf(SyncedBody b) { float v; return b.Rb != null && vehicleSpeed.TryGetValue(b.Rb, out v) ? v : 0f; } /// Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem). SyncedBody VehicleAround(ObjectSync objects, Vector3 head) { foreach (SyncedBody b in objects.AllVehicles()) { if (b.Rb == null || (b.Rb.position - head).sqrMagnitude > 10f * 10f) continue; Bounds inner = InnerBounds(b); if (inner.Contains(b.Rb.transform.InverseTransformPoint(head))) return b; } return null; } readonly Dictionary innerBounds = new Dictionary(); /// /// Innenraum des Fahrzeugs (lokal): Umriss aller festen Collider, seitlich und vorne/hinten etwas kleiner - neben /// der geschlossenen Tür steht man noch draußen, erst in der offenen Tür bzw. drin ist man "im Auto". /// Bounds InnerBounds(SyncedBody b) { Bounds result; if (innerBounds.TryGetValue(b.Rb, out result)) return result; Transform t = b.Rb.transform; bool any = false; foreach (Collider c in b.Rb.GetComponentsInChildren()) { if (c == null || c.isTrigger || !c.enabled) continue; Bounds wb = c.bounds; for (int i = 0; i < 8; i++) { Vector3 corner = wb.center + Vector3.Scale(wb.extents, new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1)); Vector3 lp = t.InverseTransformPoint(corner); if (!any) { result = new Bounds(lp, Vector3.zero); any = true; } else result.Encapsulate(lp); } } if (!any) result = new Bounds(Vector3.zero, Vector3.zero); else { Vector3 size = result.size; // Weltachsen-Umrisse schräger Teile sind zu groß: seitlich 0.25 m, vorne/hinten 0.3 m kleiner, // oben 0.6 m Luft (Kopf über dem Dach der Ladefläche / im offenen Fahrzeug) result.size = new Vector3(Mathf.Max(0.1f, size.x - 0.5f), size.y + 0.6f, Mathf.Max(0.1f, size.z - 0.6f)); result.center += new Vector3(0f, 0.3f, 0f); } innerBounds[b.Rb] = result; return result; } 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 // Zur näheren Seite neben das Fahrzeug (außerhalb seines Umrisses), auf Kopfhöhe von vorher float side = seatHeadLocal.x >= 0f ? 1f : -1f; Bounds inner = InnerBounds(vehicle); float x = inner.size.sqrMagnitude > 0f ? inner.center.x + side * (inner.extents.x + 0.85f) : seatHeadLocal.x + side * ExitSideOffset; var local = new Vector3(x, seatHeadLocal.y, seatHeadLocal.z); Vector3 outside = vehicle.Rb.transform.TransformPoint(local); 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(); innerBounds.Clear(); lastVehiclePos.Clear(); vehicleSpeed.Clear(); lastSpeedSample = -1f; 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(); } } } }