Sync car doors, hood and trunk angles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
- **Räder** drehen sich bei Autos, die ein anderer Spieler fährt
|
||||||
- **Unfallschäden**: verbeulte Karosserie wird an alle übertragen
|
- **Unfallschäden**: verbeulte Karosserie wird an alle übertragen
|
||||||
- **Jobs, Aufträge und Laden**: Fortschritt (Auftrag angenommen, erledigt, bestellt ...) wird synchronisiert
|
- **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)
|
- **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
|
- `vehicles` in der F7-Konsole zeigt, wie viele Karosserie-Meshes, Job-Werte und Verkehrsautos synchronisiert werden
|
||||||
|
|
||||||
|
|||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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<int, Hinge> hinges = new Dictionary<int, Hinge>();
|
||||||
|
readonly HashSet<HingeJoint> seen = new HashSet<HingeJoint>();
|
||||||
|
readonly List<Hinge> outgoing = new List<Hinge>();
|
||||||
|
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<MsgType, byte[]> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Empfangene Stellungen halten (die Physik würde die Tür sonst beim Fahren weiterschwingen lassen).</summary>
|
||||||
|
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<HingeJoint>())
|
||||||
|
{
|
||||||
|
if (j == null || !seen.Add(j)) continue;
|
||||||
|
Rigidbody rb = j.GetComponent<Rigidbody>();
|
||||||
|
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<int>();
|
||||||
|
foreach (KeyValuePair<int, Hinge> kv in hinges)
|
||||||
|
{
|
||||||
|
Hinge h = kv.Value;
|
||||||
|
if (h.Rb == null || h.Body == null || h.Rb.GetComponent<HingeJoint>() == 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@
|
|||||||
<Compile Include="DamageSync.cs" />
|
<Compile Include="DamageSync.cs" />
|
||||||
<Compile Include="WorldVarSync.cs" />
|
<Compile Include="WorldVarSync.cs" />
|
||||||
<Compile Include="TrafficSync.cs" />
|
<Compile Include="TrafficSync.cs" />
|
||||||
|
<Compile Include="HingeSync.cs" />
|
||||||
<Compile Include="MpCommand.cs" />
|
<Compile Include="MpCommand.cs" />
|
||||||
<Compile Include="Commands.cs" />
|
<Compile Include="Commands.cs" />
|
||||||
<Compile Include="MscoLog.cs" />
|
<Compile Include="MscoLog.cs" />
|
||||||
|
|||||||
+13
-3
@@ -63,6 +63,7 @@ namespace MSCO
|
|||||||
readonly DamageSync damageSync = new DamageSync();
|
readonly DamageSync damageSync = new DamageSync();
|
||||||
readonly WorldVarSync worldVars = new WorldVarSync();
|
readonly WorldVarSync worldVars = new WorldVarSync();
|
||||||
readonly TrafficSync traffic = new TrafficSync();
|
readonly TrafficSync traffic = new TrafficSync();
|
||||||
|
readonly HingeSync hinges = new HingeSync();
|
||||||
readonly Overlays overlays = new Overlays();
|
readonly Overlays overlays = new Overlays();
|
||||||
bool isAdmin; // Server hat uns als Admin gemeldet
|
bool isAdmin; // Server hat uns als Admin gemeldet
|
||||||
readonly HashSet<string> bannedNames = new HashSet<string>(); // P2P-Host: für diese Sitzung gebannt
|
readonly HashSet<string> bannedNames = new HashSet<string>(); // P2P-Host: für diese Sitzung gebannt
|
||||||
@@ -207,6 +208,7 @@ namespace MSCO
|
|||||||
lines.Add("tracked doors/gates/hoods: " + doorSync.Count);
|
lines.Add("tracked doors/gates/hoods: " + doorSync.Count);
|
||||||
lines.Add("synced vehicle values (fuel, oil, bolts, wear ...): " + vehicleState.Count);
|
lines.Add("synced vehicle values (fuel, oil, bolts, wear ...): " + vehicleState.Count);
|
||||||
lines.Add("synced body meshes (crash damage): " + damageSync.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 job/shop values: " + worldVars.Count);
|
||||||
lines.Add("synced traffic cars: " + traffic.Count + (IsWorldAuthority ? " (simulated here)" : " (following)"));
|
lines.Add("synced traffic cars: " + traffic.Count + (IsWorldAuthority ? " (simulated here)" : " (following)"));
|
||||||
return lines;
|
return lines;
|
||||||
@@ -973,7 +975,7 @@ namespace MSCO
|
|||||||
worldSync.Reset();
|
worldSync.Reset();
|
||||||
moneySync.Reset();
|
moneySync.Reset();
|
||||||
doorSync.Reset();
|
doorSync.Reset();
|
||||||
vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset();
|
vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset();
|
||||||
isAdmin = false;
|
isAdmin = false;
|
||||||
if (overlays.AdminOpen || overlays.ChatOpen) overlays.CloseAll();
|
if (overlays.AdminOpen || overlays.ChatOpen) overlays.CloseAll();
|
||||||
FsmSync.Enabled = false;
|
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.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.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem
|
||||||
|| t == MsgType.ItemAppeared || t == MsgType.ObjDestroy || t == MsgType.VehicleDamage
|
|| 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)
|
void OnMessage(int peerId, Message m)
|
||||||
@@ -1296,6 +1298,9 @@ namespace MSCO
|
|||||||
case MsgType.WorldVars:
|
case MsgType.WorldVars:
|
||||||
worldVars.Handle(r);
|
worldVars.Handle(r);
|
||||||
break;
|
break;
|
||||||
|
case MsgType.HingeStates:
|
||||||
|
hinges.Handle(r);
|
||||||
|
break;
|
||||||
case MsgType.TrafficStates:
|
case MsgType.TrafficStates:
|
||||||
traffic.Handle(r, IsWorldAuthority);
|
traffic.Handle(r, IsWorldAuthority);
|
||||||
break;
|
break;
|
||||||
@@ -1416,7 +1421,7 @@ namespace MSCO
|
|||||||
passenger.Reset();
|
passenger.Reset();
|
||||||
localPlayer = null;
|
localPlayer = null;
|
||||||
objects.OnGameLoaded();
|
objects.OnGameLoaded();
|
||||||
vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset();
|
vehicleState.Reset(); damageSync.Reset(); worldVars.Reset(); traffic.Reset(); hinges.Reset();
|
||||||
vehicleFx.Reset(); wheelSpin.Reset();
|
vehicleFx.Reset(); wheelSpin.Reset();
|
||||||
worldSync.Reset();
|
worldSync.Reset();
|
||||||
moneySync.Reset();
|
moneySync.Reset();
|
||||||
@@ -1540,6 +1545,9 @@ namespace MSCO
|
|||||||
// Unfallschäden (verbeulte Karosserie)
|
// Unfallschäden (verbeulte Karosserie)
|
||||||
try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); }
|
try { if (objects.ScanComplete) damageSync.Update(objects, localId, SendGame); }
|
||||||
catch (System.Exception e) { ReportError("crash damage sync", e); }
|
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
|
// Jobs, Aufträge, Laden
|
||||||
try { if (objects.ScanComplete) worldVars.Update(SendGame); }
|
try { if (objects.ScanComplete) worldVars.Update(SendGame); }
|
||||||
catch (System.Exception e) { ReportError("jobs/shop sync", e); }
|
catch (System.Exception e) { ReportError("jobs/shop sync", e); }
|
||||||
@@ -1594,6 +1602,8 @@ namespace MSCO
|
|||||||
catch (System.Exception e) { ReportError("object movement", e); }
|
catch (System.Exception e) { ReportError("object movement", e); }
|
||||||
try { traffic.FixedUpdate(); }
|
try { traffic.FixedUpdate(); }
|
||||||
catch (System.Exception e) { ReportError("traffic movement", e); }
|
catch (System.Exception e) { ReportError("traffic movement", e); }
|
||||||
|
try { hinges.FixedUpdate(); }
|
||||||
|
catch (System.Exception e) { ReportError("car door movement", e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void LateUpdate()
|
void LateUpdate()
|
||||||
|
|||||||
+2
-1
@@ -49,7 +49,8 @@ namespace MSCO
|
|||||||
ObjDestroy = 38, // Item wurde verbraucht/zerstört (gegessen, getrunken ...): int Objekt-ID
|
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]
|
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.]
|
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
|
public struct Message
|
||||||
|
|||||||
Reference in New Issue
Block a user