Door/hood sync via hinge spring instead of teleport (fixes NaN crash and parts falling off); join/leave toasts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,9 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d
|
|||||||
- **Backups** mit Datum/Uhrzeit, **RESTORE** holt einen älteren Stand zurück (der aktuelle wird vorher gesichert)
|
- **Backups** mit Datum/Uhrzeit, **RESTORE** holt einen älteren Stand zurück (der aktuelle wird vorher gesichert)
|
||||||
- Jede Aktion mit Sicherheitsabfrage; nur für Admins (vom Server geprüft)
|
- Jede Aktion mit Sicherheitsabfrage; nur für Admins (vom Server geprüft)
|
||||||
|
|
||||||
|
**Hinweise**
|
||||||
|
- Kleine Benachrichtigung oben rechts, wenn jemand beitritt oder das Spiel verlässt
|
||||||
|
|
||||||
**Trennen & Neu-Verbinden**
|
**Trennen & Neu-Verbinden**
|
||||||
- Gekickt, gebannt, Server beendet, Verbindung verloren: eigenes Fenster in der Bildmitte mit **Grund** und OK
|
- Gekickt, gebannt, Server beendet, Verbindung verloren: eigenes Fenster in der Bildmitte mit **Grund** und OK
|
||||||
(im Spiel wird der Grund vorher 2,5 s groß angezeigt)
|
(im Spiel wird der Grund vorher 2,5 s groß angezeigt)
|
||||||
@@ -30,6 +33,8 @@ Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d
|
|||||||
Nach Kick, Ban oder eigenem Trennen wird nicht neu verbunden
|
Nach Kick, Ban oder eigenem Trennen wird nicht neu verbunden
|
||||||
|
|
||||||
**Behoben**
|
**Behoben**
|
||||||
|
- Absturz („NaN … collider“) und abfallende Teile am Satsuma: Türen/Haube/Kofferraum wurden gegen das Scharnier an
|
||||||
|
ihre Position teleportiert – das erzeugte riesige Kräfte. Jetzt zieht die Feder des Scharniers sie sanft auf den Winkel
|
||||||
- Beim anderen Spieler „orgelte“ der Anlasser endlos: erzwungene Geräusche/Lichter werden losgelassen, sobald keine
|
- Beim anderen Spieler „orgelte“ der Anlasser endlos: erzwungene Geräusche/Lichter werden losgelassen, sobald keine
|
||||||
Daten mehr vom Fahrer kommen
|
Daten mehr vom Fahrer kommen
|
||||||
- Beim Neuladen der Welt wurden herumliegende Objekte als „verbraucht“ gemeldet und bei allen gelöscht. Jetzt werden
|
- Beim Neuladen der Welt wurden herumliegende Objekte als „verbraucht“ gemeldet und bei allen gelöscht. Jetzt werden
|
||||||
|
|||||||
+60
-40
@@ -7,8 +7,10 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Türen, Motorhaube und Kofferraum an Fahrzeugen: hängen mit einem Scharnier (HingeJoint) an der Karosserie
|
/// 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
|
/// und werden deshalb nicht vom Objekt-Sync bewegt. Übertragen wird der Öffnungswinkel des Scharniers.
|
||||||
/// offene Tür beim anderen genauso weit offen steht.
|
/// 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).
|
/// Wer sendet: Fährt jemand das Auto, der Fahrer. Ist es frei, wer gerade daneben steht (Tür auf/zu gemacht).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HingeSync
|
public class HingeSync
|
||||||
@@ -17,19 +19,23 @@ namespace MSCO
|
|||||||
const float SendInterval = 0.1f;
|
const float SendInterval = 0.1f;
|
||||||
const float MinAngle = 2f; // Grad - kleiner ist Zittern
|
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 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
|
const float ApplyHold = 1.5f; // so lange nach einer Nachricht die Feder halten und selbst nicht senden
|
||||||
|
const float SpringForce = 120f;
|
||||||
|
const float SpringDamper = 12f;
|
||||||
|
|
||||||
class Hinge
|
class Hinge
|
||||||
{
|
{
|
||||||
public int Key;
|
public int Key;
|
||||||
public string Root;
|
public string Root;
|
||||||
|
public HingeJoint Joint;
|
||||||
public Rigidbody Rb;
|
public Rigidbody Rb;
|
||||||
public Rigidbody Body; // woran das Scharnier hängt
|
public float SentAngle;
|
||||||
public Quaternion SentRot;
|
|
||||||
public bool HasSent;
|
public bool HasSent;
|
||||||
public Quaternion TargetRot;
|
public float TargetAngle;
|
||||||
public Vector3 TargetPos;
|
|
||||||
public float TargetTime = -100f;
|
public float TargetTime = -100f;
|
||||||
|
public bool Driving; // Feder gerade von uns gesetzt
|
||||||
|
public bool OrigUseSpring;
|
||||||
|
public JointSpring OrigSpring;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Dictionary<int, Hinge> hinges = new Dictionary<int, Hinge>();
|
readonly Dictionary<int, Hinge> hinges = new Dictionary<int, Hinge>();
|
||||||
@@ -41,6 +47,7 @@ namespace MSCO
|
|||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
foreach (Hinge h in hinges.Values) StopDriving(h);
|
||||||
hinges.Clear();
|
hinges.Clear();
|
||||||
seen.Clear();
|
seen.Clear();
|
||||||
nextScan = 0f;
|
nextScan = 0f;
|
||||||
@@ -60,15 +67,16 @@ namespace MSCO
|
|||||||
outgoing.Clear();
|
outgoing.Clear();
|
||||||
foreach (Hinge h in hinges.Values)
|
foreach (Hinge h in hinges.Values)
|
||||||
{
|
{
|
||||||
if (h.Rb == null || h.Body == null || !h.Rb.gameObject.activeInHierarchy) continue;
|
if (h.Joint == null || h.Rb == null || !h.Rb.gameObject.activeInHierarchy) continue;
|
||||||
if (Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt
|
if (Time.time - h.TargetTime < ApplyHold) continue; // gerade von anderen gesetzt
|
||||||
int owner = objects.VehicleOwner(h.Root);
|
int owner = objects.VehicleOwner(h.Root);
|
||||||
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt
|
if (owner != SyncedBody.NoOwner && owner != localId) continue; // der Fahrer schickt
|
||||||
if (owner == SyncedBody.NoOwner && (h.Rb.position - playerPos).sqrMagnitude > NearRange * NearRange) continue;
|
if (owner == SyncedBody.NoOwner && (h.Rb.position - playerPos).sqrMagnitude > NearRange * NearRange) continue;
|
||||||
|
|
||||||
Quaternion rel = Quaternion.Inverse(h.Body.rotation) * h.Rb.rotation;
|
float angle = h.Joint.angle;
|
||||||
if (h.HasSent && Quaternion.Angle(rel, h.SentRot) < MinAngle) continue;
|
if (float.IsNaN(angle) || float.IsInfinity(angle)) continue;
|
||||||
h.SentRot = rel;
|
if (h.HasSent && Mathf.Abs(angle - h.SentAngle) < MinAngle) continue;
|
||||||
|
h.SentAngle = angle;
|
||||||
h.HasSent = true;
|
h.HasSent = true;
|
||||||
outgoing.Add(h);
|
outgoing.Add(h);
|
||||||
}
|
}
|
||||||
@@ -77,14 +85,7 @@ namespace MSCO
|
|||||||
send(MsgType.HingeStates, NetSession.Build(w =>
|
send(MsgType.HingeStates, NetSession.Build(w =>
|
||||||
{
|
{
|
||||||
w.Write(outgoing.Count);
|
w.Write(outgoing.Count);
|
||||||
foreach (Hinge h in outgoing)
|
foreach (Hinge h in outgoing) { w.Write(h.Key); w.Write(h.SentAngle); }
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,35 +95,57 @@ namespace MSCO
|
|||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
int key = r.ReadInt32();
|
int key = r.ReadInt32();
|
||||||
var p = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
|
float angle = r.ReadSingle();
|
||||||
var q = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
|
|
||||||
Hinge h;
|
Hinge h;
|
||||||
if (!hinges.TryGetValue(key, out h)) { nextScan = 0f; continue; }
|
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
|
if (float.IsNaN(angle) || float.IsInfinity(angle) || Mathf.Abs(angle) > 180f) continue;
|
||||||
h.TargetPos = p;
|
h.TargetAngle = angle;
|
||||||
h.TargetRot = q;
|
|
||||||
h.TargetTime = Time.time;
|
h.TargetTime = Time.time;
|
||||||
h.SentRot = q; // nicht als eigene Bewegung zurückschicken
|
h.SentAngle = angle; // nicht als eigene Bewegung zurückschicken
|
||||||
h.HasSent = true;
|
h.HasSent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Empfangene Stellungen halten (die Physik würde die Tür sonst beim Fahren weiterschwingen lassen).</summary>
|
/// <summary>Feder des Scharniers auf den empfangenen Winkel stellen bzw. wieder zurückgeben.</summary>
|
||||||
public void FixedUpdate()
|
public void FixedUpdate()
|
||||||
{
|
{
|
||||||
foreach (Hinge h in hinges.Values)
|
foreach (Hinge h in hinges.Values)
|
||||||
{
|
{
|
||||||
if (Time.time - h.TargetTime > ApplyHold || h.Rb == null || h.Body == null) continue;
|
if (h.Joint == null || h.Rb == null) continue;
|
||||||
Quaternion rot = h.Body.rotation * h.TargetRot;
|
bool fresh = Time.time - h.TargetTime < ApplyHold && !h.Rb.isKinematic;
|
||||||
Vector3 pos = h.Body.transform.TransformPoint(h.TargetPos);
|
if (!fresh) { StopDriving(h); continue; }
|
||||||
float t = Mathf.Clamp01(Time.fixedDeltaTime * 15f);
|
|
||||||
h.Rb.MoveRotation(Quaternion.Slerp(h.Rb.rotation, rot, t));
|
if (!h.Driving)
|
||||||
h.Rb.MovePosition(Vector3.Lerp(h.Rb.position, pos, t));
|
{
|
||||||
h.Rb.angularVelocity = Vector3.zero;
|
h.OrigUseSpring = h.Joint.useSpring;
|
||||||
h.Rb.velocity = h.Body.velocity;
|
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()
|
void Scan()
|
||||||
{
|
{
|
||||||
int before = hinges.Count;
|
int before = hinges.Count;
|
||||||
@@ -132,17 +155,14 @@ namespace MSCO
|
|||||||
Rigidbody rb = j.GetComponent<Rigidbody>();
|
Rigidbody rb = j.GetComponent<Rigidbody>();
|
||||||
Rigidbody body = j.connectedBody;
|
Rigidbody body = j.connectedBody;
|
||||||
if (rb == null || body == null || !IsVehicle(body.transform.root.name)) continue;
|
if (rb == null || body == null || !IsVehicle(body.transform.root.name)) continue;
|
||||||
int key = Hash(FsmSync.PathOf(j.transform));
|
int key = Hash(FsmSync.PathOf(j.transform) + "|" + FsmSync.PathOf(body.transform));
|
||||||
if (hinges.ContainsKey(key)) continue;
|
if (hinges.ContainsKey(key)) continue;
|
||||||
hinges[key] = new Hinge { Key = key, Root = body.transform.root.name, Rb = rb, Body = body };
|
hinges[key] = new Hinge { Key = key, Root = body.transform.root.name, Joint = j, Rb = rb };
|
||||||
}
|
}
|
||||||
// Abgebaute Teile (Scharnier weg) vergessen
|
// Abgebaute Teile (Scharnier weg) vergessen
|
||||||
var gone = new List<int>();
|
var gone = new List<int>();
|
||||||
foreach (KeyValuePair<int, Hinge> kv in hinges)
|
foreach (KeyValuePair<int, Hinge> kv in hinges)
|
||||||
{
|
if (kv.Value.Joint == null || kv.Value.Rb == null) gone.Add(kv.Key);
|
||||||
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);
|
foreach (int k in gone) hinges.Remove(k);
|
||||||
if (gone.Count > 0) seen.RemoveWhere(x => x == null);
|
if (gone.Count > 0) seen.RemoveWhere(x => x == null);
|
||||||
if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced");
|
if (hinges.Count != before) MscoLog.Info("[MSCO] Vehicle doors/hood/trunk: " + hinges.Count + " synced");
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
<Compile Include="TrafficSync.cs" />
|
<Compile Include="TrafficSync.cs" />
|
||||||
<Compile Include="HingeSync.cs" />
|
<Compile Include="HingeSync.cs" />
|
||||||
<Compile Include="ControlSync.cs" />
|
<Compile Include="ControlSync.cs" />
|
||||||
|
<Compile Include="Toasts.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" />
|
||||||
|
|||||||
+10
-3
@@ -65,6 +65,10 @@ namespace MSCO
|
|||||||
readonly TrafficSync traffic = new TrafficSync();
|
readonly TrafficSync traffic = new TrafficSync();
|
||||||
readonly HingeSync hinges = new HingeSync();
|
readonly HingeSync hinges = new HingeSync();
|
||||||
readonly ControlSync controls = new ControlSync();
|
readonly ControlSync controls = new ControlSync();
|
||||||
|
readonly Toasts toasts = new Toasts();
|
||||||
|
|
||||||
|
void ToastJoined(string name) { toasts.Add(name, "joined the game", UiTheme.Online); }
|
||||||
|
void ToastLeft(string name) { toasts.Add(name, "left the game", UiTheme.Muted); }
|
||||||
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
|
||||||
@@ -928,6 +932,7 @@ namespace MSCO
|
|||||||
RemovePlayer(peerId);
|
RemovePlayer(peerId);
|
||||||
session.SendToAll(MsgType.PlayerLeft, NetSession.Build(w => w.Write(peerId)));
|
session.SendToAll(MsgType.PlayerLeft, NetSession.Build(w => w.Write(peerId)));
|
||||||
SetStatus(p.Name + " left (" + reason + ")");
|
SetStatus(p.Name + " left (" + reason + ")");
|
||||||
|
ToastLeft(p.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
string serverReason; // Grund, den der Server/Host beim Trennen mitgeschickt hat
|
string serverReason; // Grund, den der Server/Host beim Trennen mitgeschickt hat
|
||||||
@@ -1144,6 +1149,7 @@ namespace MSCO
|
|||||||
foreach (PlayerInfo p in list) { w.Write(p.Id); w.Write(p.Name); }
|
foreach (PlayerInfo p in list) { w.Write(p.Id); w.Write(p.Name); }
|
||||||
}));
|
}));
|
||||||
session.SendToAll(MsgType.PlayerJoined, NetSession.Build(w => { w.Write(peerId); w.Write(name); }), peerId);
|
session.SendToAll(MsgType.PlayerJoined, NetSession.Build(w => { w.Write(peerId); w.Write(name); }), peerId);
|
||||||
|
ToastJoined(name);
|
||||||
|
|
||||||
if (MenuHelper.InGame)
|
if (MenuHelper.InGame)
|
||||||
{
|
{
|
||||||
@@ -1216,6 +1222,7 @@ namespace MSCO
|
|||||||
string name = r.ReadString();
|
string name = r.ReadString();
|
||||||
AddPlayer(id, name, false);
|
AddPlayer(id, name, false);
|
||||||
SetStatus(name + " joined");
|
SetStatus(name + " joined");
|
||||||
|
ToastJoined(name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,7 +1230,7 @@ namespace MSCO
|
|||||||
{
|
{
|
||||||
int id = r.ReadInt32();
|
int id = r.ReadInt32();
|
||||||
PlayerInfo p;
|
PlayerInfo p;
|
||||||
if (players.TryGetValue(id, out p)) SetStatus(p.Name + " left");
|
if (players.TryGetValue(id, out p)) { SetStatus(p.Name + " left"); ToastLeft(p.Name); }
|
||||||
RemovePlayer(id);
|
RemovePlayer(id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1960,7 +1967,7 @@ namespace MSCO
|
|||||||
if (MenuHelper.InMenu)
|
if (MenuHelper.InMenu)
|
||||||
{
|
{
|
||||||
lobby.Draw(this);
|
lobby.Draw(this);
|
||||||
DrawPing();
|
DrawPing(); toasts.Draw();
|
||||||
overlays.Draw(this, false);
|
overlays.Draw(this, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1977,7 +1984,7 @@ namespace MSCO
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (session != null) DrawHud();
|
if (session != null) DrawHud();
|
||||||
DrawPing();
|
DrawPing(); toasts.Draw();
|
||||||
overlays.Draw(this, session != null);
|
overlays.Draw(this, session != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ namespace MSCO
|
|||||||
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]
|
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
|
||||||
VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
VehicleControls = 43 // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MSCO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Kleine Hinweise oben rechts (unter der Ping-Anzeige), z.B. "Bob joined" / "Bob left".
|
||||||
|
/// Gleiten von rechts herein, bleiben ein paar Sekunden und blenden dann aus. Höchstens 4 gleichzeitig.
|
||||||
|
/// </summary>
|
||||||
|
public class Toasts
|
||||||
|
{
|
||||||
|
const float Lifetime = 4.5f;
|
||||||
|
const float SlideIn = 0.25f;
|
||||||
|
const float FadeOut = 0.6f;
|
||||||
|
const float Width = 250f;
|
||||||
|
const float Height = 36f;
|
||||||
|
const int MaxShown = 4;
|
||||||
|
|
||||||
|
class Toast
|
||||||
|
{
|
||||||
|
public string Text;
|
||||||
|
public string Detail;
|
||||||
|
public Color Dot;
|
||||||
|
public float Start;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly List<Toast> toasts = new List<Toast>();
|
||||||
|
GUIStyle box, title, detail;
|
||||||
|
Texture2D dotTex;
|
||||||
|
|
||||||
|
public void Add(string text, string detail, Color dot)
|
||||||
|
{
|
||||||
|
toasts.Add(new Toast { Text = text, Detail = detail, Dot = dot, Start = Time.realtimeSinceStartup });
|
||||||
|
if (toasts.Count > MaxShown) toasts.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
if (toasts.Count == 0) return;
|
||||||
|
EnsureStyles();
|
||||||
|
float now = Time.realtimeSinceStartup;
|
||||||
|
toasts.RemoveAll(t => now - t.Start > Lifetime);
|
||||||
|
|
||||||
|
Color before = GUI.color;
|
||||||
|
float y = 40f; // unter der Ping-Anzeige
|
||||||
|
for (int i = toasts.Count - 1; i >= 0; i--) // neueste oben
|
||||||
|
{
|
||||||
|
Toast t = toasts[i];
|
||||||
|
float age = now - t.Start;
|
||||||
|
float slide = Mathf.Clamp01(age / SlideIn);
|
||||||
|
slide = 1f - (1f - slide) * (1f - slide); // weich abbremsen
|
||||||
|
float alpha = Mathf.Clamp01((Lifetime - age) / FadeOut);
|
||||||
|
|
||||||
|
var r = new Rect(Screen.width - 12 - Width + (1f - slide) * (Width + 20f), y, Width, Height);
|
||||||
|
GUI.color = new Color(1f, 1f, 1f, alpha);
|
||||||
|
GUI.Box(r, GUIContent.none, box);
|
||||||
|
|
||||||
|
GUI.color = new Color(t.Dot.r, t.Dot.g, t.Dot.b, alpha);
|
||||||
|
GUI.DrawTexture(new Rect(r.x + 14, r.y + Height / 2f - 4, 8, 8), dotTex);
|
||||||
|
|
||||||
|
GUI.color = new Color(1f, 1f, 1f, alpha);
|
||||||
|
GUI.Label(new Rect(r.x + 32, r.y, r.width - 40, Height), t.Text, title);
|
||||||
|
if (!string.IsNullOrEmpty(t.Detail))
|
||||||
|
{
|
||||||
|
float w = title.CalcSize(new GUIContent(t.Text)).x;
|
||||||
|
GUI.Label(new Rect(r.x + 32 + w + 6, r.y, r.width - 46 - w, Height), t.Detail, detail);
|
||||||
|
}
|
||||||
|
y += Height + 6f;
|
||||||
|
}
|
||||||
|
GUI.color = before;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureStyles()
|
||||||
|
{
|
||||||
|
if (box != null && box.normal.background != null) return;
|
||||||
|
box = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.88f), 6);
|
||||||
|
title = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleLeft };
|
||||||
|
title.normal.textColor = Color.white;
|
||||||
|
title.wordWrap = false;
|
||||||
|
detail = new GUIStyle(title) { fontStyle = FontStyle.Normal, fontSize = 12 };
|
||||||
|
detail.normal.textColor = UiTheme.Muted;
|
||||||
|
dotTex = RoundDot(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Texture2D RoundDot(int size)
|
||||||
|
{
|
||||||
|
var t = new Texture2D(size, size, TextureFormat.ARGB32, false);
|
||||||
|
float c = (size - 1) / 2f;
|
||||||
|
for (int x = 0; x < size; x++)
|
||||||
|
for (int y = 0; y < size; y++)
|
||||||
|
{
|
||||||
|
float d = Mathf.Sqrt((x - c) * (x - c) + (y - c) * (y - c));
|
||||||
|
t.SetPixel(x, y, new Color(1f, 1f, 1f, Mathf.Clamp01(c + 0.5f - d)));
|
||||||
|
}
|
||||||
|
t.Apply();
|
||||||
|
t.hideFlags = HideFlags.HideAndDontSave;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ namespace MSCO
|
|||||||
if (w.Model == null || w.Hub == null) continue;
|
if (w.Model == null || w.Hub == null) continue;
|
||||||
float speed = Vector3.Dot(vel, w.Hub.forward);
|
float speed = Vector3.Dot(vel, w.Hub.forward);
|
||||||
w.Angle = Mathf.Repeat(w.Angle + speed / w.Radius * Mathf.Rad2Deg * dt, 360f);
|
w.Angle = Mathf.Repeat(w.Angle + speed / w.Radius * Mathf.Rad2Deg * dt, 360f);
|
||||||
|
if (float.IsNaN(w.Angle) || float.IsInfinity(w.Angle)) w.Angle = 0f;
|
||||||
|
|
||||||
// Hat das Spiel das Modell in diesem Frame selbst gedreht (z.B. Lenkung), ist das die neue Basis
|
// Hat das Spiel das Modell in diesem Frame selbst gedreht (z.B. Lenkung), ist das die neue Basis
|
||||||
Quaternion current = w.Model.localRotation;
|
Quaternion current = w.Model.localRotation;
|
||||||
|
|||||||
Reference in New Issue
Block a user