Instead of snapping to a computed passenger seat, a player whose head is inside a vehicle's (shrunk) outline is held exactly there and moves with it; works for cabins, open doors and beds, with or without a driver. Getting out puts the player beside the vehicle on the nearer side. P next to the car still uses the passenger seat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
333 lines
16 KiB
C#
333 lines
16 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// Mitfahren: Wer mit dem Kopf im Fahrzeug ist (Kabine, offene Tür, Ladefläche ...), wird genau dort festgehalten und
|
|
/// fährt mit - egal ob schon jemand fährt oder das Auto noch steht. 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.
|
|
/// </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 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)";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </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; }
|
|
// 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<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 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 || 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)";
|
|
}
|
|
|
|
/// <summary>Fahrzeug, in dessen Umriss der Kopf gerade ist (null = in keinem).</summary>
|
|
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<Rigidbody, Bounds> innerBounds = new Dictionary<Rigidbody, Bounds>();
|
|
|
|
/// <summary>
|
|
/// 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".
|
|
/// </summary>
|
|
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<Collider>())
|
|
{
|
|
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);
|
|
}
|
|
|
|
/// <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
|
|
// 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);
|
|
}
|
|
|
|
/// <summary>Szenenwechsel/Verbindungsende: ohne Teleport alles zurücksetzen.</summary>
|
|
public void Reset()
|
|
{
|
|
vehicle = null;
|
|
dwellVehicle = null;
|
|
dwellStart = -1f;
|
|
driverHeads.Clear();
|
|
innerBounds.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();
|
|
}
|
|
}
|
|
}
|
|
}
|