using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; namespace MSCO { /// /// Lichter, Dauergeräusche (Motor, Blinker, Hupe, Radio ...) und Anzeigen (Tacho, Drehzahlmesser, Tank-/Temperatur- /// zeiger, Lenkrad) von Fahrzeugen. Wer ein Fahrzeug besitzt (fährt/schiebt), schickt regelmäßig deren Zustand; /// bei allen anderen wird dieser Zustand jedes Frame übernommen. Die Zeiger bewegen sich beim Beifahrer sonst nicht, /// weil das Auto dort nicht selbst fährt (die Spiel-Logik sieht Geschwindigkeit/Drehzahl 0). /// public class VehicleFx { const float SendInterval = 0.1f; const float DialSmoothing = 14f; // Zeiger gleiten zwischen den Paketen weich zur neuen Stellung /// Objekte, deren Drehung eine Anzeige ist: Tacho-/Drehzahl-/Tank-/Temperaturzeiger, Lenkrad. static bool IsDial(string name) { string n = name.ToLower(); return n.Contains("needle") || n.Contains("steeringwheel"); } const float CacheRefresh = 5f; // Teile können an-/abgebaut werden const float ApplyTimeout = 2f; // ohne neue Daten nichts mehr erzwingen struct AudioState { public bool Playing; public float Pitch; public float Volume; } class Components { public float BuiltAt; public Transform Root; public readonly Dictionary Lights = new Dictionary(); public readonly Dictionary Audio = new Dictionary(); public readonly Dictionary Dials = new Dictionary(); public readonly Dictionary Particles = new Dictionary(); } class Snapshot { public float ReceivedAt; public readonly Dictionary Lights = new Dictionary(); public readonly Dictionary Audio = new Dictionary(); public readonly Dictionary Dials = new Dictionary(); public readonly Dictionary Particles = new Dictionary(); } readonly Dictionary components = new Dictionary(); readonly Dictionary incoming = new Dictionary(); float sendTimer; public void Reset() { components.Clear(); incoming.Clear(); } // ---------------------------------------------------------------- Senden public void Update(ObjectSync objects, Action send, float dt, bool worldAuthority) { sendTimer += dt; if (sendTimer < SendInterval) return; sendTimer = 0f; // Eigene Fahrzeuge (fahren/schieben) und - wenn wir die Welt vorgeben - auch freie, geparkte Fahrzeuge: // sonst würde z.B. ein brennendes, abgestelltes Auto nur bei einem Spieler brennen List vehicles = objects.LocalVehicles(); if (worldAuthority) vehicles.AddRange(objects.FreeVehicles()); foreach (SyncedBody b in vehicles) { Components c = GetComponents(b); if (c == null) continue; int id = b.Id; send(MsgType.VehicleFx, NetSession.Build(w => { w.Write(id); w.Write(c.Lights.Count); foreach (KeyValuePair l in c.Lights) { w.Write(l.Key); w.Write(l.Value != null && l.Value.enabled && l.Value.gameObject.activeInHierarchy); } int audioCount = 0; foreach (AudioSource a in c.Audio.Values) if (a != null) audioCount++; w.Write(audioCount); foreach (KeyValuePair a in c.Audio) { if (a.Value == null) continue; w.Write(a.Key); w.Write(a.Value.isPlaying); w.Write(a.Value.pitch); w.Write(a.Value.volume); } // Anzeigen (am Ende angehängt - ältere Versionen lesen hier nicht weiter) int dialCount = 0; foreach (Transform t in c.Dials.Values) if (t != null) dialCount++; w.Write(dialCount); foreach (KeyValuePair d in c.Dials) { if (d.Value == null) continue; Quaternion q = d.Value.localRotation; w.Write(d.Key); w.Write(q.x); w.Write(q.y); w.Write(q.z); w.Write(q.w); } // Partikel-Effekte (Feuer, Rauch, Dampf, Funken): läuft / läuft nicht int particleCount = 0; foreach (ParticleSystem ps in c.Particles.Values) if (ps != null) particleCount++; w.Write(particleCount); foreach (KeyValuePair ps in c.Particles) { if (ps.Value == null) continue; w.Write(ps.Key); w.Write(ps.Value.gameObject.activeInHierarchy && ps.Value.isPlaying); } })); } } // ---------------------------------------------------------------- Empfangen public void Handle(BinaryReader r) { int id = r.ReadInt32(); var s = new Snapshot { ReceivedAt = Time.time }; int lights = r.ReadInt32(); for (int i = 0; i < lights; i++) s.Lights[r.ReadInt32()] = r.ReadBoolean(); int audio = r.ReadInt32(); for (int i = 0; i < audio; i++) { int key = r.ReadInt32(); s.Audio[key] = new AudioState { Playing = r.ReadBoolean(), Pitch = r.ReadSingle(), Volume = r.ReadSingle() }; } if (r.BaseStream.Length - r.BaseStream.Position >= 4) { int dials = r.ReadInt32(); for (int i = 0; i < dials; i++) { int key = r.ReadInt32(); var q = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); if (!float.IsNaN(q.x) && !float.IsNaN(q.w)) s.Dials[key] = q; } } if (r.BaseStream.Length - r.BaseStream.Position >= 4) { int particles = r.ReadInt32(); for (int i = 0; i < particles; i++) s.Particles[r.ReadInt32()] = r.ReadBoolean(); } incoming[id] = s; } /// Jedes Frame nach den Spiel-FSMs (LateUpdate), damit unsere Werte gewinnen. public void Apply(ObjectSync objects) { foreach (KeyValuePair kv in incoming) { if (Time.time - kv.Value.ReceivedAt > ApplyTimeout) continue; SyncedBody b = objects.Get(kv.Key); if (b == null || objects.IsLocalBody(b)) continue; // eigene Fahrzeuge simulieren wir selbst Components c = GetComponents(b); if (c == null) continue; foreach (KeyValuePair l in kv.Value.Lights) { Light light; if (c.Lights.TryGetValue(l.Key, out light) && light != null) SetLight(light, l.Value, c.Root); } foreach (KeyValuePair a in kv.Value.Audio) { AudioSource src; if (c.Audio.TryGetValue(a.Key, out src) && src != null) SetAudio(src, a.Value); } foreach (KeyValuePair p in kv.Value.Particles) { ParticleSystem ps; if (c.Particles.TryGetValue(p.Key, out ps) && ps != null) SetParticles(ps, p.Value, c.Root); } float t = Mathf.Clamp01(Time.deltaTime * DialSmoothing); foreach (KeyValuePair d in kv.Value.Dials) { Transform dial; if (c.Dials.TryGetValue(d.Key, out dial) && dial != null) dial.localRotation = Quaternion.Slerp(dial.localRotation, d.Value, t); } } } static void SetLight(Light light, bool on, Transform root) { if (!on) { if (light.enabled) light.enabled = false; return; } // Nur die Lampe selbst einschalten - übergeordnete Objekte NICHT aktivieren: das weckt Spiel-Logik auf // (FSMs im OnEnable), die dort nicht laufen soll, und kann das Spiel abstürzen lassen if (!light.enabled) light.enabled = true; } static void SetParticles(ParticleSystem ps, bool playing, Transform root) { if (playing) { // Nur Effekte auf bereits aktiven Objekten starten (Objekte aktivieren weckt Spiel-Logik - Absturzgefahr) if (ps.gameObject.activeInHierarchy && !ps.isPlaying) ps.Play(); } else if (ps.isPlaying) ps.Stop(); } static void SetAudio(AudioSource src, AudioState state) { if (!src.gameObject.activeInHierarchy) return; if (state.Playing && !src.isPlaying) src.Play(); else if (!state.Playing && src.isPlaying) src.Stop(); src.pitch = state.Pitch; src.volume = state.Volume; } // ---------------------------------------------------------------- Komponenten Components GetComponents(SyncedBody b) { if (b.Rb == null) return null; Components c; if (components.TryGetValue(b.Id, out c) && Time.time - c.BuiltAt < CacheRefresh) return c; c = new Components { BuiltAt = Time.time, Root = b.Rb.transform }; var seen = new Dictionary(); foreach (Light l in b.Rb.GetComponentsInChildren(true)) c.Lights[Key(l.transform, c.Root, seen)] = l; seen.Clear(); foreach (AudioSource a in b.Rb.GetComponentsInChildren(true)) if (a.loop) c.Audio[Key(a.transform, c.Root, seen)] = a; // nur Dauergeräusche seen.Clear(); foreach (Transform t in b.Rb.GetComponentsInChildren(true)) if (IsDial(t.name)) c.Dials[Key(t, c.Root, seen)] = t; seen.Clear(); foreach (ParticleSystem ps in b.Rb.GetComponentsInChildren(true)) c.Particles[Key(ps.transform, c.Root, seen)] = ps; components[b.Id] = c; return c; } /// Schlüssel aus dem Pfad unterhalb des Fahrzeugs (+ Nummer bei gleichen Pfaden). static int Key(Transform t, Transform root, Dictionary seen) { var sb = new StringBuilder(); for (; t != null && t != root; t = t.parent) sb.Insert(0, "/" + t.name); string path = sb.ToString(); int n; seen.TryGetValue(path, out n); seen[path] = n + 1; path += "#" + n; unchecked { uint h = 2166136261; foreach (byte c in Encoding.UTF8.GetBytes(path)) { h ^= c; h *= 16777619; } return (int)h; } } } }