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 der Öffnungswinkel des Scharniers. /// Beim Empfänger wird die Tür NICHT hingesetzt (Teleportieren gegen das Gelenk erzeugt riesige Kräfte - Teile /// reißen ab, im schlimmsten Fall stürzt die Physik ab), sondern die Feder des Scharniers zieht sie sanft auf /// den Winkel. Danach bekommt das Scharnier seine ursprüngliche Einstellung zurück. /// 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 = 3f; // so lange nach einer Nachricht die Feder halten und selbst nicht senden // Steht das Fahrzeug, schickt nur, wer die Tür gerade selbst bewegt hat (Klick in der Nähe). Vorher schickte // jeder in der Nähe: setzte sich die Tür beim anderen nach dem Übernehmen etwas anders, schickte er zurück - // die Türen schaukelten hin und her. const float InteractRange = 2.5f; const float InteractAuthority = 4f; // so lange nach dem Klick (Tür schwingt aus) const float SpringForce = 120f; const float SpringDamper = 12f; class Hinge { public int Key; public string Root; public HingeJoint Joint; public Rigidbody Rb; public float SentAngle; public bool HasSent; public float TargetAngle; public float TargetTime = -100f; public bool Driving; // Feder gerade von uns gesetzt public float LocalAuthorityUntil = -100f; public bool OrigUseSpring; public JointSpring OrigSpring; } 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() { foreach (Hinge h in hinges.Values) StopDriving(h); hinges.Clear(); seen.Clear(); nextScan = -1f; // erster Suchlauf verteilt, siehe Update } public void Update(ObjectSync objects, int localId, Vector3 playerPos, float dt, Action send) { if (nextScan < 0f) nextScan = Time.time + 1.7f; // nicht alle Sync-Teile im selben Frame suchen lassen 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.Joint == null || h.Rb == null || !h.Rb.gameObject.activeInHierarchy) continue; bool mine = Time.time < h.LocalAuthorityUntil; if (!mine && Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt int owner = objects.VehicleOwner(h.Root); // Fährt jemand: der Fahrer schickt (Türen schwingen während der Fahrt). Hat man selbst gerade an der Tür // geklickt, schickt man auch - sonst würde die Feder sie beim Fahrer gleich wieder zudrücken. if (!mine && owner != SyncedBody.NoOwner && owner != localId) continue; if (!mine && owner == SyncedBody.NoOwner) { h.SentAngle = SafeAngle(h, h.SentAngle); continue; } // stehendes Auto: nur wer klickt float angle = h.Joint.angle; if (float.IsNaN(angle) || float.IsInfinity(angle)) continue; if (h.HasSent && Mathf.Abs(angle - h.SentAngle) < MinAngle) continue; h.SentAngle = angle; 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) { w.Write(h.Key); w.Write(h.SentAngle); } })); } /// Jedes Frame: Klick (linke Maustaste / Benutzen) in der Nähe einer Tür = wir bewegen sie gerade. public void NoteInteraction(Vector3 playerPos) { if (!Input.GetMouseButtonDown(0) && !Input.GetKeyDown(KeyCode.F)) return; foreach (Hinge h in hinges.Values) { if (h.Rb == null || (h.Rb.worldCenterOfMass - playerPos).sqrMagnitude > InteractRange * InteractRange) continue; h.LocalAuthorityUntil = Time.time + InteractAuthority; h.TargetTime = -100f; // eigene Bewegung: keine Feder mehr auf den alten Winkel } } static float SafeAngle(Hinge h, float fallback) { float a = h.Joint.angle; return float.IsNaN(a) || float.IsInfinity(a) ? fallback : a; } public void Handle(BinaryReader r) { int count = r.ReadInt32(); for (int i = 0; i < count; i++) { int key = r.ReadInt32(); float angle = r.ReadSingle(); Hinge h; if (!hinges.TryGetValue(key, out h)) { nextScan = 0f; continue; } if (float.IsNaN(angle) || float.IsInfinity(angle) || Mathf.Abs(angle) > 180f) continue; if (Time.time < h.LocalAuthorityUntil) continue; // wir bewegen sie gerade selbst h.TargetAngle = angle; h.TargetTime = Time.time; h.SentAngle = angle; // nicht als eigene Bewegung zurückschicken h.HasSent = true; } } /// Feder des Scharniers auf den empfangenen Winkel stellen bzw. wieder zurückgeben. public void FixedUpdate() { foreach (Hinge h in hinges.Values) { if (h.Joint == null || h.Rb == null) continue; bool fresh = Time.time - h.TargetTime < ApplyHold && !h.Rb.isKinematic; if (!fresh) { StopDriving(h); continue; } if (!h.Driving) { h.OrigUseSpring = h.Joint.useSpring; h.OrigSpring = h.Joint.spring; h.Driving = true; } float target = h.TargetAngle; if (h.Joint.useLimits) { JointLimits l = h.Joint.limits; target = Mathf.Clamp(target, Mathf.Min(l.min, l.max), Mathf.Max(l.min, l.max)); } JointSpring s = h.Joint.spring; s.spring = SpringForce; s.damper = SpringDamper; s.targetPosition = target; h.Joint.spring = s; h.Joint.useSpring = true; h.Rb.WakeUp(); } } static void StopDriving(Hinge h) { if (!h.Driving) return; h.Driving = false; if (h.Joint == null) return; h.Joint.spring = h.OrigSpring; h.Joint.useSpring = h.OrigUseSpring; } 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) + "|" + FsmSync.PathOf(body.transform)); if (hinges.ContainsKey(key)) continue; hinges[key] = new Hinge { Key = key, Root = body.transform.root.name, Joint = j, Rb = rb }; } // Abgebaute Teile (Scharnier weg) vergessen var gone = new List(); foreach (KeyValuePair kv in hinges) if (kv.Value.Joint == null || kv.Value.Rb == 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; } } } }