From 90c1af4513c2eb26154e0813b2bf8082ce5c99e8 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sun, 27 Sep 2026 15:13:08 +0200 Subject: [PATCH] Sync car doors, hood and trunk angles Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + HingeSync.cs | 167 ++++++++++++++++++++++++++++++++++++++++++ MSCO.csproj | 1 + MultiplayerManager.cs | 16 +++- NetSession.cs | 3 +- 5 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 HingeSync.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b1862b7..fff0003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d - **Räder** drehen sich bei Autos, die ein anderer Spieler fährt - **Unfallschäden**: verbeulte Karosserie wird an alle übertragen - **Jobs, Aufträge und Laden**: Fortschritt (Auftrag angenommen, erledigt, bestellt ...) wird synchronisiert +- **Autotüren, Motorhaube, Kofferraum**: stehen bei allen im selben Winkel offen - **Verkehr**: NPC-Autos fahren bei allen an derselben Stelle (der erste Spieler simuliert, die anderen folgen) - `vehicles` in der F7-Konsole zeigt, wie viele Karosserie-Meshes, Job-Werte und Verkehrsautos synchronisiert werden diff --git a/HingeSync.cs b/HingeSync.cs new file mode 100644 index 0000000..404e791 --- /dev/null +++ b/HingeSync.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace MSCO +{ + /// + /// Türen, Motorhaube und Kofferraum an Fahrzeugen: hängen mit einem Scharnier (HingeJoint) an der Karosserie + /// und werden deshalb nicht vom Objekt-Sync bewegt. Übertragen wird die Stellung relativ zum Auto, damit eine + /// offene Tür beim anderen genauso weit offen steht. + /// Wer sendet: Fährt jemand das Auto, der Fahrer. Ist es frei, wer gerade daneben steht (Tür auf/zu gemacht). + /// + public class HingeSync + { + const float ScanInterval = 10f; + const float SendInterval = 0.1f; + const float MinAngle = 2f; // Grad - kleiner ist Zittern + const float NearRange = 4f; // so nah muss man an einer Tür stehen, um sie zu bewegen + const float ApplyHold = 1.5f; // so lange nach einer Nachricht die Stellung halten und selbst nicht senden + + class Hinge + { + public int Key; + public string Root; + public Rigidbody Rb; + public Rigidbody Body; // woran das Scharnier hängt + public Quaternion SentRot; + public bool HasSent; + public Quaternion TargetRot; + public Vector3 TargetPos; + public float TargetTime = -100f; + } + + readonly Dictionary hinges = new Dictionary(); + readonly HashSet seen = new HashSet(); + readonly List outgoing = new List(); + float nextScan, sendTimer; + + public int Count { get { return hinges.Count; } } + + public void Reset() + { + hinges.Clear(); + seen.Clear(); + nextScan = 0f; + } + + public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action send) + { + if (Time.time >= nextScan) + { + nextScan = Time.time + ScanInterval; + Scan(); + } + sendTimer += dt; + if (sendTimer < SendInterval) return; + sendTimer = 0f; + + outgoing.Clear(); + foreach (Hinge h in hinges.Values) + { + if (h.Rb == null || h.Body == null || !h.Rb.gameObject.activeInHierarchy) continue; + if (Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt + int owner = objects.VehicleOwner(h.Root); + if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt + if (owner == SyncedBody.NoOwner && (h.Rb.position - playerPos).sqrMagnitude > NearRange * NearRange) continue; + + Quaternion rel = Quaternion.Inverse(h.Body.rotation) * h.Rb.rotation; + if (h.HasSent && Quaternion.Angle(rel, h.SentRot) < MinAngle) continue; + h.SentRot = rel; + h.HasSent = true; + outgoing.Add(h); + } + if (outgoing.Count == 0) return; + + send(MsgType.HingeStates, NetSession.Build(w => + { + w.Write(outgoing.Count); + foreach (Hinge h in outgoing) + { + Vector3 p = h.Body.transform.InverseTransformPoint(h.Rb.position); + Quaternion q = h.SentRot; + w.Write(h.Key); + w.Write(p.x); w.Write(p.y); w.Write(p.z); + w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w); + } + })); + } + + public void Handle(BinaryReader r) + { + int count = r.ReadInt32(); + for (int i = 0; i < count; i++) + { + int key = r.ReadInt32(); + var p = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + var q = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + Hinge h; + if (!hinges.TryGetValue(key, out h)) { nextScan = 0f; continue; } + if (float.IsNaN(p.x) || float.IsNaN(q.w) || p.sqrMagnitude > 25f) continue; // ein Scharnier ist nie weit weg + h.TargetPos = p; + h.TargetRot = q; + h.TargetTime = Time.time; + h.SentRot = q; // nicht als eigene Bewegung zurückschicken + h.HasSent = true; + } + } + + /// Empfangene Stellungen halten (die Physik würde die Tür sonst beim Fahren weiterschwingen lassen). + public void FixedUpdate() + { + foreach (Hinge h in hinges.Values) + { + if (Time.time - h.TargetTime > ApplyHold || h.Rb == null || h.Body == null) continue; + Quaternion rot = h.Body.rotation * h.TargetRot; + Vector3 pos = h.Body.transform.TransformPoint(h.TargetPos); + float t = Mathf.Clamp01(Time.fixedDeltaTime * 15f); + h.Rb.MoveRotation(Quaternion.Slerp(h.Rb.rotation, rot, t)); + h.Rb.MovePosition(Vector3.Lerp(h.Rb.position, pos, t)); + h.Rb.angularVelocity = Vector3.zero; + h.Rb.velocity = h.Body.velocity; + } + } + + void Scan() + { + int before = hinges.Count; + foreach (HingeJoint j in UnityEngine.Object.FindObjectsOfType()) + { + if (j == null || !seen.Add(j)) continue; + Rigidbody rb = j.GetComponent(); + Rigidbody body = j.connectedBody; + if (rb == null || body == null || !IsVehicle(body.transform.root.name)) continue; + int key = Hash(FsmSync.PathOf(j.transform)); + if (hinges.ContainsKey(key)) continue; + hinges[key] = new Hinge { Key = key, Root = body.transform.root.name, Rb = rb, Body = body }; + } + // Abgebaute Teile (Scharnier weg) vergessen + var gone = new List(); + foreach (KeyValuePair kv in hinges) + { + Hinge h = kv.Value; + if (h.Rb == null || h.Body == null || h.Rb.GetComponent() == null) gone.Add(kv.Key); + } + foreach (int k in gone) hinges.Remove(k); + if (gone.Count > 0) seen.RemoveWhere(x => x == null); + if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced"); + } + + static bool IsVehicle(string root) + { + foreach (string v in ObjectSync.VehicleRoots) if (root.StartsWith(v)) return true; + return false; + } + + static int Hash(string s) + { + unchecked + { + uint h = 2166136261; + foreach (char c in s) { h ^= c; h *= 16777619; } + return (int)h; + } + } + } +} diff --git a/MSCO.csproj b/MSCO.csproj index ae071d0..30c8d7d 100644 --- a/MSCO.csproj +++ b/MSCO.csproj @@ -77,6 +77,7 @@ + diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index e9a350f..87029c0 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -63,6 +63,7 @@ namespace MSCO readonly DamageSync damageSync = new DamageSync(); readonly WorldVarSync worldVars = new WorldVarSync(); readonly TrafficSync traffic = new TrafficSync(); + readonly HingeSync hinges = new HingeSync(); readonly Overlays overlays = new Overlays(); bool isAdmin; // Server hat uns als Admin gemeldet readonly HashSet bannedNames = new HashSet(); // P2P-Host: für diese Sitzung gebannt @@ -207,6 +208,7 @@ namespace MSCO lines.Add("tracked doors/gates/hoods: " + doorSync.Count); lines.Add("synced vehicle values (fuel, oil, bolts, wear ...): " + vehicleState.Count); lines.Add("synced body meshes (crash damage): " + damageSync.Count); + lines.Add("synced car doors/hoods/trunks: " + hinges.Count); lines.Add("synced job/shop values: " + worldVars.Count); lines.Add("synced traffic cars: " + traffic.Count + (IsWorldAuthority ? " (simulated here)" : " (following)")); return lines; @@ -973,7 +975,7 @@ namespace MSCO worldSync.Reset(); moneySync.Reset(); doorSync.Reset(); - vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); + vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset(); isAdmin = false; if (overlays.AdminOpen || overlays.ChatOpen) overlays.CloseAll(); FsmSync.Enabled = false; @@ -1002,7 +1004,7 @@ namespace MSCO || t == MsgType.ObjRelease || t == MsgType.FsmEvent || t == MsgType.VehicleFx || t == MsgType.WorldTime || t == MsgType.MoneyDelta || t == MsgType.DoorState || t == MsgType.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem || t == MsgType.ItemAppeared || t == MsgType.ObjDestroy || t == MsgType.VehicleDamage - || t == MsgType.TrafficStates || t == MsgType.WorldVars; + || t == MsgType.TrafficStates || t == MsgType.WorldVars || t == MsgType.HingeStates; } void OnMessage(int peerId, Message m) @@ -1296,6 +1298,9 @@ namespace MSCO case MsgType.WorldVars: worldVars.Handle(r); break; + case MsgType.HingeStates: + hinges.Handle(r); + break; case MsgType.TrafficStates: traffic.Handle(r, IsWorldAuthority); break; @@ -1416,7 +1421,7 @@ namespace MSCO passenger.Reset(); localPlayer = null; objects.OnGameLoaded(); - vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); + vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset(); vehicleFx.Reset(); wheelSpin.Reset(); worldSync.Reset(); moneySync.Reset(); @@ -1540,6 +1545,9 @@ namespace MSCO // Unfallschäden (verbeulte Karosserie) try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); } catch (System.Exception e) { ReportError("crash damage sync", e); } + // Türen, Motorhaube, Kofferraum der Fahrzeuge + try { if (objects.ScanComplete && localPlayer != null) hinges.Update(objects, localId, localPlayer.position, Time.deltaTime, SendGame); } + catch (System.Exception e) { ReportError("car door sync", e); } // Jobs, Aufträge, Laden try { if (objects.ScanComplete) worldVars.Update(SendGame); } catch (System.Exception e) { ReportError("jobs/shop sync", e); } @@ -1594,6 +1602,8 @@ namespace MSCO catch (System.Exception e) { ReportError("object movement", e); } try { traffic.FixedUpdate(); } catch (System.Exception e) { ReportError("traffic movement", e); } + try { hinges.FixedUpdate(); } + catch (System.Exception e) { ReportError("car door movement", e); } } void LateUpdate() diff --git a/NetSession.cs b/NetSession.cs index 5ebb2bf..f6a1cec 100644 --- a/NetSession.cs +++ b/NetSession.cs @@ -49,7 +49,8 @@ namespace MSCO ObjDestroy = 38, // Item wurde verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID VehicleDamage = 39, // Unfallschaden: int Mesh-Schlüssel, int Eckpunkte gesamt, int n, je [int Index, 3 float] TrafficStates = 40, // Verkehr (nur Welt-Autorität): int n, je [int Schlüssel, 3 float Pos, 4 float Rot, 3 float Geschw.] - WorldVars = 41 // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert] + WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert] + HingeStates = 42 // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, 3 float Pos, 4 float Rot relativ zum Auto] } public struct Message