Getting in on the driver's side moves the head past the passenger seat while the game puts the player into the driver's seat. Auto-boarding now only triggers from the passenger side (head on the seat's side, not further inside than the seat), not while the game controls the player (CharacterController off or parented to a vehicle) and not within 4 s after driving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
291 lines
14 KiB
C#
291 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<Behaviour> disabled = new List<Behaviour>();
|
|
CharacterController controller;
|
|
// Feste Collider des Spielers, die während der Fahrt aus sind (sonst stecken sie in Sitzen und Teilen des Autos)
|
|
readonly List<Collider> disabledColliders = new List<Collider>();
|
|
readonly List<Rigidbody> frozenBodies = new List<Rigidbody>();
|
|
// Fahrerkopf je Fahrzeug (lokal), aus DriverHeadPivot - null = nicht gefunden
|
|
readonly Dictionary<Rigidbody, Vector3?> driverHeads = new Dictionary<Rigidbody, Vector3?>();
|
|
|
|
float nextAutoCheck, dwellStart = -1f, lastDriving = -100f;
|
|
const float AfterDrivingPause = 4f; // nach dem Fahren (Ein-/Aussteigen am Fahrersitz) nicht automatisch
|
|
SyncedBody dwellVehicle;
|
|
bool autoArmed = true;
|
|
Vector3 lastExitSeat;
|
|
|
|
public bool IsRiding { get { return vehicle != null; } }
|
|
/// <summary>Fahrzeug, in dem wir mitfahren (sonst null).</summary>
|
|
public SyncedBody Vehicle { get { return vehicle; } }
|
|
/// <summary>Kopf auf dem Sitz, relativ zum Fahrzeug (genau, nicht aus der Weltposition des letzten Frames).</summary>
|
|
public Vector3 SeatHeadLocal { get { return seatHeadLocal; } }
|
|
|
|
/// <summary>Taste gedrückt: einsteigen oder aussteigen. Gibt einen Statustext zurück.</summary>
|
|
public string Toggle(Transform player, Camera cam, ObjectSync objects, IDictionary<int, PlayerInfo> 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)";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public string AutoUpdate(Transform player, Camera cam, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
|
{
|
|
if (objects.IsDriving) lastDriving = Time.time;
|
|
if (IsRiding || cam == null || player == null || objects.IsDriving) { dwellStart = -1f; return null; }
|
|
// Steigt man auf der Fahrerseite ein, setzt das Spiel einen über die Sitze auf den Fahrersitz - dabei kommt der
|
|
// Kopf kurz am Beifahrersitz vorbei. Solange das Spiel die Steuerung hat (CharacterController aus, Spieler an
|
|
// ein Fahrzeug gehängt) oder kurz nach dem Fahren: nicht automatisch einsteigen.
|
|
if (Time.time - lastDriving < AfterDrivingPause) { dwellStart = -1f; return null; }
|
|
CharacterController cc = player.GetComponent<CharacterController>();
|
|
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;
|
|
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;
|
|
// Nur von der Beifahrerseite her: Kopf auf der Seite des Beifahrersitzes und nicht weiter innen als der Sitz
|
|
// (wer auf der Fahrerseite einsteigt, ist auf der anderen Seite bzw. in der Mitte)
|
|
if (Mathf.Abs(seat.Value.x) >= CenterSeatThreshold
|
|
&& (Mathf.Sign(local.x) != Mathf.Sign(seat.Value.x) || Mathf.Abs(local.x) < Mathf.Abs(seat.Value.x) - 0.1f)) 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);
|
|
}
|
|
|
|
/// <summary>Beifahrersitz (Kopfposition, lokal im Fahrzeug) - aus dem Fahrersitz des Spiels oder dem Fahrer.</summary>
|
|
Vector3? SeatFor(SyncedBody b, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
|
{
|
|
Vector3? driverHead;
|
|
if (!driverHeads.TryGetValue(b.Rb, out driverHead))
|
|
{
|
|
driverHead = null;
|
|
foreach (Transform t in b.Rb.GetComponentsInChildren<Transform>(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
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
static float DistanceToVehicleSq(SyncedBody b, Vector3 point)
|
|
{
|
|
float best = (b.Rb.position - point).sqrMagnitude;
|
|
foreach (Collider c in b.Rb.GetComponentsInChildren<Collider>())
|
|
{
|
|
if (c == null || !c.enabled || c.isTrigger) continue;
|
|
float d = (c.bounds.ClosestPoint(point) - point).sqrMagnitude;
|
|
if (d < best) best = d;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/// <summary>Nach der Physik (LateUpdate): Spieler auf den Sitz setzen und mitdrehen.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Szenenwechsel/Verbindungsende: ohne Teleport alles zurücksetzen.</summary>
|
|
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<MonoBehaviour>())
|
|
{
|
|
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<CharacterController>();
|
|
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<Collider>())
|
|
{
|
|
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<Rigidbody>())
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|