From 983291a4b360305592e704021cdcd2fb82d5e0e0 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Fri, 2 Oct 2026 10:23:25 +0200 Subject: [PATCH] Satsuma no longer falls apart when driving together; automatic passenger seat - A driven vehicle is no longer released after 3 s without data (a short hitch of the driver, e.g. saving, stopped the car instantly on the passenger's side and ripped the parts off, which then synced to everyone): 30 s timeout, on release it keeps the driver's velocity and part joints stay unbreakable for 3 more seconds - Cargo in a moving vehicle belongs to its driver (passengers no longer claim items in the car, which then followed late and kinematic at the driver's end and knocked seats and parts off) - Passenger: sit down automatically by stepping into the open passenger door, also without a driver (seat from the game's DriverHeadPivot); Space or P to get out Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 14 ++++ MultiplayerManager.cs | 18 ++++-- ObjectSync.cs | 117 ++++++++++++++++++++++++++++++++- Passenger.cs | 147 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 262 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3fd54..c568dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ Web-Panel (Einstellungen), **OFFICIAL** vergeben nur die MSCO-Admins und wird hervorgehoben - Braucht keinen neuen Server: die Infos kommen vom Master (msco.mbm.wtf) und funktionieren mit 2.8-Servern +**Beifahrer** +- **Automatisch einsteigen**: in die offene Beifahrertür an den Sitz treten - man setzt sich von selbst hin, keine Taste + nötig. Aussteigen mit Leertaste oder der Beifahrer-Taste (P) +- Geht jetzt auch, **wenn noch niemand fährt** (Sitzposition kommt aus dem Fahrersitz des Spiels); fährt danach jemand + los, fährt man mit. Setzt man sich selbst ans Steuer, endet der Beifahrer-Modus + +**Satsuma zerfällt nicht mehr beim gemeinsamen Fahren** +- Hing das Spiel des Fahrers kurz (z.B. beim Speichern für den Server), gab der Mitfahrer das Auto nach 3 s frei: es + blieb bei ihm schlagartig stehen, die Teile wollten mit Tempo weiter und rissen ab - und das „Teil abgefallen“ ging an + alle. Ein gefahrenes Auto wird jetzt erst nach 30 s ohne Daten freigegeben, rollt dann mit der Geschwindigkeit des + Fahrers weiter, und die Teile bleiben noch 3 s fest, bis alles eingeschwungen ist +- Ladung im Auto (Kisten, Werkzeug, Teile im Kofferraum oder auf dem Sitz) gehört jetzt immer dem Fahrer. Vorher + übernahm sie oft der Mitfahrer; beim Fahrer folgte sie dann verspätet und „unendlich schwer“ und schlug Sitze und Teile ab + **Mitfahren ohne Ruckeln** - Fahrer und Beifahrer sitzen beim anderen fest auf dem Sitz: die Figur wird relativ zum Auto übertragen und geglättet, statt in Weltkoordinaten hinter dem fahrenden Auto herzuhinken (vorher wurde der Beifahrer bei Tempo nach hinten gezogen) diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 3ab8cfc..a3b8c1c 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -1850,14 +1850,20 @@ namespace MSCO StartCoroutine(ApplyWorldSnapshot(snapshot)); } + string passengerResult = null; if (MSCO.PassengerKey != null && !overlays.AnyOpen && MSCO.PassengerKey.GetKeybindDown()) + passengerResult = passenger.Toggle(localPlayer, playerCamera, objects, players); + else if (passenger.IsRiding && !overlays.AnyOpen && Input.GetKeyDown(KeyCode.Space)) { - string result = passenger.Toggle(localPlayer, playerCamera, objects, players); - if (result != null) - { - SetStatus(result); - overlays.AddChat("", result, true); // im Spiel sichtbar, nicht nur in der Konsole - } + passenger.Exit(localPlayer, playerCamera); // aussteigen wie zu Fuß losspringen + passengerResult = "Got out"; + } + else if (IsPeerReady && MenuHelper.InGame && localPlayer != null && !overlays.AnyOpen) + passengerResult = passenger.AutoUpdate(localPlayer, playerCamera, objects, players); // in die Beifahrertür treten + if (passengerResult != null) + { + SetStatus(passengerResult); + overlays.AddChat("", passengerResult, true); // im Spiel sichtbar, nicht nur in der Konsole } bool ready = IsPeerReady; diff --git a/ObjectSync.cs b/ObjectSync.cs index 1908b7e..bdde037 100644 --- a/ObjectSync.cs +++ b/ObjectSync.cs @@ -30,6 +30,14 @@ namespace MSCO // Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment) public Dictionary HeldJoints; public float NextPartScan; + // Nach der Rückgabe erst so spät wieder zerbrechlich (bis dahin schwingen die Teile ein) + public float JointRestoreAt = float.MaxValue; + // Item liegt in diesem Fahrzeug (Kofferraum, Sitz ...): gehört dann dem, der das Fahrzeug fährt + public SyncedBody Carrier; + public float NextCarrierCheck; + // Fahrzeug: Umriss in lokalen Koordinaten (für "liegt im Auto") + public Bounds LocalBounds; + public bool HasLocalBounds; public float StillTime; public float NoMotionClaimUntil; // kurz nach einer Übergabe nicht wegen Nachfedern/Nachrollen übernehmen // Am Auto angebautes Teil (Gelenk zum Fahrzeug oder unter einem anderen Rigidbody): bewegt sich mit dem Auto, @@ -82,6 +90,12 @@ namespace MSCO const float ReleaseAfterStill = 1f; const float SnapDistance = 10f; const float RemoteTimeout = 3f; // Besitzer meldet sich nicht mehr (z.B. Teil wurde dort montiert) + // Ein gefahrenes Auto NICHT nach 3 s freigeben: hängt das Spiel des Fahrers kurz (z.B. beim Speichern), blieb es + // bei den anderen schlagartig stehen und die Teile rissen ab. Trennt der Fahrer, gibt ReleaseAllOf es frei. + const float DrivenVehicleTimeout = 30f; + const float JointSettleTime = 3f; // so lange nach der Rückgabe bleiben die Gelenke der Teile unzerbrechlich + const float CarrierCheckInterval = 0.4f; + const float CarrierMinSpeed = 1.5f; // ab dieser Geschwindigkeit nimmt ein Fahrzeug seine Ladung mit const float RunawaySpeed = 90f; // m/s - schneller fährt in MSC nichts; darüber hat die Physik es weggeschleudert const float HandoverCooldown = 3f; // nach Freigabe/Übergabe keine Übernahme durch Bewegung const float MaxHoldDistance = 1.5f; // gehaltene Items sind so nah an der Kamera @@ -191,6 +205,9 @@ namespace MSCO public bool IsRemoteOwnedBody(SyncedBody b) { return IsRemoteOwned(b); } + /// Alle erfassten Fahrzeuge (Liste wird alle 2 s erneuert, nicht verändern). + public List AllVehicles() { return Vehicles(); } + /// Für "mp vehicles": Zustand aller erfassten Fahrzeuge (zur Fehlersuche). public List DescribeVehicles() { @@ -258,7 +275,10 @@ namespace MSCO public void Reset() { foreach (SyncedBody b in byId.Values) + { if (IsRemoteOwned(b) && b.Rb != null) RestoreKinematic(b); + RestoreJoints(b); + } byId.Clear(); byRb.Clear(); rejected.Clear(); @@ -338,13 +358,25 @@ namespace MSCO b.LastPos = b.Rb.position; if (!b.Rb.gameObject.activeInHierarchy) continue; if (UpdateAttached(b)) continue; // angebaut: bewegt sich mit dem Auto + if (b.HeldJoints != null && !IsRemoteOwned(b) && Time.time >= b.JointRestoreAt) RestoreJoints(b); bool active = b == held || b == driven; if (!IsRemoteOwned(b)) StopRunaway(b); + // Ladung eines fahrenden Autos gehört dessen Fahrer: nur bei ihm wird sie mit dem Auto simuliert. + // Übernahm sie ein Mitfahrer, folgte sie beim Fahrer verspätet und kinematisch (unendlich schwer) und + // schlug Sitze und Teile ab. + SyncedBody carrier = active || b.IsVehicle ? null : CarrierOf(b); + bool carrierRemote = carrier != null && IsRemoteOwned(carrier); + bool carrierMine = carrier != null && IsLocal(carrier) && IsCarrying(carrier); if (b.OwnerId == SyncedBody.NoOwner) { if (active) Claim(b, true); - else if (motionClaimAllowed && Time.time >= b.NoMotionClaimUntil && IsMoving(b) && IsNear(b, player)) Claim(b, false); + else if (carrierMine && IsMoving(b)) Claim(b, false); + else if (motionClaimAllowed && Time.time >= b.NoMotionClaimUntil && IsMoving(b) && IsNear(b, player) && !carrierRemote) Claim(b, false); + } + else if (IsLocal(b) && !active && carrierRemote && carrier.OwnerActive) + { + Release(b); // fährt ein anderer: der übernimmt die Ladung } else if (IsLocal(b)) { @@ -367,7 +399,8 @@ namespace MSCO // zeigt oft noch auf ein altes Item; trägt es jemand an unserem Kopf vorbei, würden wir es ihm sonst entreißen bool grab = b == driven || !b.OwnerActive || JustPicked; if (active && grab && Wins(LocalId, true, b.OwnerId, b.OwnerActive)) Claim(b, true); // Spieler greift sich das Objekt - else if (Time.time - b.LastRecvTime > RemoteTimeout) SetFree(b); + else if (carrierMine && !b.OwnerActive) Claim(b, false); // liegt in unserem fahrenden Auto + else if (Time.time - b.LastRecvTime > (b.IsVehicle && b.OwnerActive ? DrivenVehicleTimeout : RemoteTimeout)) SetFree(b); } } ReportConsumed(player); @@ -498,6 +531,7 @@ namespace MSCO b.Rb.isKinematic = true; if (b.IsVehicle) { + b.JointRestoreAt = float.MaxValue; HoldParts(b); // Physik läuft mit 50 Hz, das Bild öfter: ohne Interpolation springt das Auto nur in Physik-Schritten // weiter - wer darin mitfährt, sieht die Welt ruckeln. Nur bei Fahrzeugen (Items setzt die Hand direkt). @@ -527,9 +561,24 @@ namespace MSCO { b.Rb.isKinematic = b.WasKinematic; if (b.InterpolationSet) b.Rb.interpolation = b.WasInterpolation; + // Mitten in der Fahrt zurückgegeben (Fahrer übergibt, steigt aus ...): mit seiner Geschwindigkeit + // weiterrollen statt schlagartig stehenzubleiben - die Teile wollen sonst mit Tempo weiter und reißen ab + if (b.IsVehicle && !b.Rb.isKinematic) + { + b.Rb.velocity = Time.fixedTime - b.TargetTime < 0.5f ? b.TargetVel : Vector3.zero; + b.Rb.angularVelocity = Vector3.zero; + } } b.InterpolationSet = false; if (b.HeldJoints == null) return; + if (b.IsVehicle) { b.JointRestoreAt = Time.time + JointSettleTime; return; } // erst nach dem Einschwingen + RestoreJoints(b); + } + + void RestoreJoints(SyncedBody b) + { + b.JointRestoreAt = float.MaxValue; + if (b.HeldJoints == null) return; foreach (KeyValuePair kv in b.HeldJoints) { if (kv.Key == null) continue; // Teil wurde inzwischen abgebaut @@ -539,6 +588,70 @@ namespace MSCO b.HeldJoints = null; } + // ---------------------------------------------------------------- Ladung in Fahrzeugen + + /// Fährt das Fahrzeug gerade (nimmt also seine Ladung mit)? + bool IsCarrying(SyncedBody v) + { + if (v.Rb == null) return false; + if (IsRemoteOwned(v)) return v.OwnerActive || v.TargetVel.sqrMagnitude > CarrierMinSpeed * CarrierMinSpeed; + return v == Driven || (!v.Rb.isKinematic && v.Rb.velocity.sqrMagnitude > CarrierMinSpeed * CarrierMinSpeed); + } + + /// In welchem Fahrzeug liegt das Item gerade (null = in keinem)? Zwischengespeichert. + SyncedBody CarrierOf(SyncedBody item) + { + if (Time.time < item.NextCarrierCheck) return item.Carrier != null && item.Carrier.Rb != null ? item.Carrier : null; + item.NextCarrierCheck = Time.time + CarrierCheckInterval; + item.Carrier = null; + Vector3 p = item.Rb.worldCenterOfMass; + foreach (SyncedBody v in Vehicles()) + { + if (v.Rb == null || (v.Rb.position - p).sqrMagnitude > 15f * 15f) continue; + if (!v.HasLocalBounds) ComputeLocalBounds(v); + if (v.LocalBounds.Contains(v.Rb.transform.InverseTransformPoint(p))) { item.Carrier = v; break; } + } + return item.Carrier; + } + + readonly List vehicleList = new List(); + float nextVehicleList; + + List Vehicles() + { + if (Time.time < nextVehicleList) return vehicleList; + nextVehicleList = Time.time + 2f; + vehicleList.Clear(); + foreach (SyncedBody b in byId.Values) if (b.IsVehicle && b.Rb != null) vehicleList.Add(b); + return vehicleList; + } + + /// Umriss des Fahrzeugs (alle festen Collider) in seinen lokalen Koordinaten, etwas nach oben erweitert. + static void ComputeLocalBounds(SyncedBody v) + { + Transform t = v.Rb.transform; + bool any = false; + var local = new Bounds(); + foreach (Collider c in v.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) { local = new Bounds(lp, Vector3.zero); any = true; } + else local.Encapsulate(lp); + } + } + if (!any) local = new Bounds(Vector3.zero, new Vector3(2f, 1.5f, 4f)); + // Weltachsen-Umrisse sind bei schrägem Auto zu groß - etwas verkleinern, oben Platz für Ladung lassen + local.Expand(new Vector3(-0.2f, 0f, -0.2f)); + local.max = new Vector3(local.max.x, local.max.y + 0.5f, local.max.z); + v.LocalBounds = local; + v.HasLocalBounds = true; + } + void SendOwnedStates() { sendList.Clear(); diff --git a/Passenger.cs b/Passenger.cs index af3d8ea..1539b18 100644 --- a/Passenger.cs +++ b/Passenger.cs @@ -4,14 +4,21 @@ using UnityEngine; namespace MSCO { /// - /// Beifahrer: Neben einem Fahrzeug, das ein anderer Spieler fährt, per Taste einsteigen. - /// Der Sitz ist die Fahrerposition, 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. + /// 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; // so nah muss man an der Karosserie stehen + 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 @@ -27,6 +34,13 @@ namespace MSCO // 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). @@ -42,38 +56,109 @@ namespace MSCO Exit(player, cam); return "Got out"; } - if (cam == null) return null; + if (cam == null || objects.IsDriving) return null; SyncedBody best = null; - RemotePlayer driver = null; - float bestDist = EnterRange * EnterRange; - List driven = objects.RemoteDrivenVehicles(); - float nearest = float.MaxValue; - foreach (SyncedBody b in driven) + 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); - PlayerInfo owner; - if (!players.TryGetValue(b.OwnerId, out owner) || owner.Avatar == null) continue; - if (d < bestDist) { bestDist = d; best = b; driver = owner.Avatar; } + if (d < bestDist) { bestDist = d; best = b; bestSeat = seat.Value; } } if (best == null) { - // Genau sagen, woran es liegt - if (driven.Count == 0) return "Passenger: nobody is driving a vehicle right now (the driver must sit in the driver's seat)"; + 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)"; + } - Transform vt = best.Rb.transform; - Vector3 driverHead = vt.InverseTransformPoint(driver.HeadPosition); - seatHeadLocal = Mathf.Abs(driverHead.x) < CenterSeatThreshold - ? driverHead - new Vector3(0f, 0f, BehindCenterSeat) // Sitz in der Mitte: dahinter - : new Vector3(-driverHead.x, driverHead.y, driverHead.z); // gegenüber vom Fahrer + /// + /// 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; - vehicle = best; - lastYaw = vt.eulerAngles.y; + // 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); - return "Riding as passenger (press again to get out)"; + } + + /// 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 } /// @@ -96,12 +181,17 @@ namespace MSCO public void LateUpdate(Transform player, Camera cam, ObjectSync objects) { if (!IsRiding) return; - // Fahrer ausgestiegen oder Fahrzeug weg: automatisch aussteigen - if (vehicle.Rb == null || cam == null || !objects.IsRemoteOwnedBody(vehicle)) + 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; @@ -117,6 +207,8 @@ namespace MSCO 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)); @@ -131,6 +223,9 @@ namespace MSCO public void Reset() { vehicle = null; + dwellVehicle = null; + dwellStart = -1f; + driverHeads.Clear(); SetMovementEnabled(null, true); }