diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59292a6..23ffb31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# Changelog
+## 2.10.8
+
+**Stabilität – weniger Hänger**
+- Senden läuft jetzt in einem eigenen Thread: vorher schrieb das Spiel selbst ins Netzwerk und stand still, sobald die
+ Leitung kurz voll war (Spielstand hochladen, schwaches WLAN). Kleine Pakete werden gebündelt
+- Speichern für den Server (friert das Spiel einige Sekunden ein): bei Routine-Speicherungen wartet das Spiel auf einen
+ ruhigen Moment (nicht in schneller Fahrt) und zeigt vorher „Saving the world for the server …“. Zusammen mit Server
+ 2.10.5 wird viel seltener gespeichert (beim Beitreten nur, wenn der letzte Stand älter als 15 Minuten ist, sonst
+ alle 30 statt 10 Minuten)
+- Eigene dynamische Schrift für alle MSCO-Anzeigen (Namensschilder, HUD, Menüs): vorher schrieb Unity hunderte
+ Warnungen ins Log – mit Rucklern
+- Admin-Menü: der Reiter VEHICLE durchsucht das Spiel nur noch alle 30 s bzw. nach einer Aktion (vorher alle 5 s –
+ spürbarer Ruckler bei offenem Menü); Uhrzeit/Wetter suchen ihre FSMs seltener
+- Weniger Speichermüll pro Nachricht (wiederverwendete Puffer) – der Garbage Collector läuft seltener
+- Hänger-Meldungen im Log sagen jetzt auch, wenn der Garbage Collector oder das Speichern die Ursache war, und wie
+ viele Hänger es seit der letzten Meldung gab
+
## 2.10.7
**Sicherheit**
diff --git a/LobbyUI.cs b/LobbyUI.cs
index d6e8366..3e342a2 100644
--- a/LobbyUI.cs
+++ b/LobbyUI.cs
@@ -1178,6 +1178,7 @@ namespace MSCO
static GUIStyle Label(int size, Color color, FontStyle font)
{
var s = new GUIStyle(GUI.skin.label);
+ if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
s.fontSize = size;
s.fontStyle = font;
s.normal.textColor = color;
@@ -1188,6 +1189,7 @@ namespace MSCO
static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font, int radius = UiTheme.Radius, Color border = default(Color))
{
var s = new GUIStyle(GUI.skin.button);
+ if (UiTheme.DynamicFont != null) s.font = UiTheme.DynamicFont;
bool withBorder = border.a > 0f;
int h = radius * 2 + 4;
Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h);
diff --git a/MSCO.cs b/MSCO.cs
index b8e805a..33c4a58 100644
--- a/MSCO.cs
+++ b/MSCO.cs
@@ -11,7 +11,7 @@ namespace MSCO
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
///
- public const string ModVersion = "2.10.7";
+ public const string ModVersion = "2.10.8";
///
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server.
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index ab5ef6d..6b825a8 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -1635,7 +1635,12 @@ namespace MSCO
case MsgType.SaveRequest:
// Server möchte den aktuellen Spielstand (für neue Spieler / zum Sichern)
- StartCoroutine(UploadSave(r.ReadInt32()));
+ {
+ int requestId = r.ReadInt32();
+ // Neuere Server sagen, ob es eilt (jemand tritt bei) - sonst darf auf einen ruhigen Moment gewartet werden
+ bool urgent = r.BaseStream.Position >= r.BaseStream.Length || r.ReadBoolean();
+ StartCoroutine(UploadSave(requestId, urgent));
+ }
break;
case MsgType.Reconnect:
@@ -2290,19 +2295,34 @@ namespace MSCO
}
/// Zu Beginn jedes Bilds: war das letzte Bild ein Hänger? Dann Ursache melden (höchstens alle 20 s).
+ int lastGcCount = -1;
+ int hitchesSinceReport;
+
void CheckHitch()
{
float dt = Time.unscaledDeltaTime;
- if (dt > HitchSeconds && MenuHelper.InGame && Time.realtimeSinceStartup >= nextHitchReport)
+ // Lief in diesem Frame der Garbage Collector? (hält bei großem Speicher das ganze Spiel an)
+ int gc = System.GC.CollectionCount(0);
+ bool gcRan = lastGcCount >= 0 && gc != lastGcCount;
+ lastGcCount = gc;
+ if (dt > HitchSeconds && MenuHelper.InGame)
{
- nextHitchReport = Time.realtimeSinceStartup + 20f;
- int total = Mathf.RoundToInt(dt * 1000f);
- if (frameMsco > dt * 1000f * 0.4f)
- MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") +
- " ms (MSCO total " + frameMsco.ToString("0") + " ms)");
- else
- MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" +
- (Time.realtimeSinceStartup - lastSaveTriggered < 5f ? " - the game was saving" : " - the game itself (loading, saving, physics)"));
+ hitchesSinceReport++;
+ if (Time.realtimeSinceStartup >= nextHitchReport)
+ {
+ nextHitchReport = Time.realtimeSinceStartup + 20f;
+ int total = Mathf.RoundToInt(dt * 1000f);
+ string more = hitchesSinceReport > 1 ? " [" + hitchesSinceReport + " hitches since the last report]" : "";
+ hitchesSinceReport = 0;
+ // Speichern friert genau den Frame ein, in dem es ausgelöst wurde - also auch so weit zurück schauen
+ bool saving = Time.realtimeSinceStartup - lastSaveTriggered < dt + 5f;
+ if (frameMsco > dt * 1000f * 0.4f)
+ MscoLog.Warning("[MSCO] Hitch " + total + " ms - caused by MSCO: " + slowestPart + " " + slowestMs.ToString("0") +
+ " ms (MSCO total " + frameMsco.ToString("0") + " ms)" + (gcRan ? ", garbage collection ran" : "") + more);
+ else
+ MscoLog.Info("[MSCO] Hitch " + total + " ms - not MSCO (MSCO used " + frameMsco.ToString("0") + " ms)" +
+ (saving ? " - the game was saving" : gcRan ? " - garbage collection (memory cleanup)" : " - the game itself (loading, physics)") + more);
+ }
}
frameMsco = 0;
slowestMs = 0;
@@ -2490,9 +2510,30 @@ namespace MSCO
/// genau die aktuelle Welt - Schrauben, Teile, alles - statt eines alten Stands mit nachgespielten Aktionen.
/// Der eigene Spielstand ist davon nicht betroffen: er liegt gesichert im Backup und kommt im Hauptmenü zurück.
///
- System.Collections.IEnumerator UploadSave(int requestId)
+ System.Collections.IEnumerator UploadSave(int requestId, bool urgent)
{
if (!MenuHelper.InGame || !dedicated || session == null) yield break;
+ // Speichern lässt das Spiel einige Sekunden hängen: bei einer Routine-Anfrage nicht mitten in schneller Fahrt,
+ // sondern einen ruhigen Moment abwarten (höchstens 20 s - der Server wartet länger)
+ if (!urgent)
+ {
+ float giveUp = Time.realtimeSinceStartup + 20f;
+ while (Time.realtimeSinceStartup < giveUp && localPlayer != null && session != null)
+ {
+ Vector3 from = localPlayer.position;
+ float t0 = Time.realtimeSinceStartup;
+ while (Time.realtimeSinceStartup < t0 + 0.5f) yield return null;
+ if (localPlayer == null) break;
+ float speed = (localPlayer.position - from).magnitude / Mathf.Max(0.05f, Time.realtimeSinceStartup - t0);
+ if (speed < 3f) break;
+ }
+ if (session == null || !MenuHelper.InGame) yield break;
+ }
+ // Erst den Hinweis zeigen, dann speichern (sonst friert das Bild ein, bevor man ihn sieht)
+ centerMessage = "Saving the world for the server ...";
+ centerMessageUntil = Time.realtimeSinceStartup + 4f;
+ yield return null;
+ yield return null;
lastSaveTriggered = Time.realtimeSinceStartup;
PlayMakerFSM.BroadcastEvent("SAVEGAME");
float until = Time.realtimeSinceStartup + 3f; // das Spiel schreibt die Dateien über mehrere Frames
@@ -2630,10 +2671,21 @@ namespace MSCO
void OnGUI()
{
PartStart();
+ // Eigene dynamische Schrift (siehe UiTheme.DynamicFont); danach wieder die bisherige
+ GUISkin skin = GUI.skin;
+ Font previousFont = skin.font;
+ Font font = UiTheme.DynamicFont;
+ if (font != null) skin.font = font;
try { DrawGui(); }
- finally { PartEnd("interface (OnGUI)"); }
+ finally
+ {
+ skin.font = previousFont;
+ PartEnd("interface (OnGUI)");
+ }
}
+ string centerMessageSource, centerMessageUpper; // Großschreibung nur einmal pro Meldung (nicht jedes Frame)
+
void DrawGui()
{
InputLock.KeepCursorFree();
@@ -2652,7 +2704,8 @@ namespace MSCO
{
int oldSize = savingStyle.fontSize;
savingStyle.fontSize = 30;
- GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), centerMessage.ToUpper(), savingStyle);
+ if (!ReferenceEquals(centerMessage, centerMessageSource)) { centerMessageSource = centerMessage; centerMessageUpper = centerMessage.ToUpper(); }
+ GUI.Label(new Rect(0, Screen.height / 2f - 40, Screen.width, 80), centerMessageUpper, savingStyle);
savingStyle.fontSize = oldSize;
}
diff --git a/NetSession.cs b/NetSession.cs
index b5aee3f..2bd018d 100644
--- a/NetSession.cs
+++ b/NetSession.cs
@@ -200,7 +200,8 @@ namespace MSCO
{
if (payload == null) payload = new byte[0];
var frame = new byte[5 + payload.Length];
- BitConverter.GetBytes(payload.Length + 1).CopyTo(frame, 0);
+ int length = payload.Length + 1; // wie BitConverter (Little Endian), ohne zusätzliches Array
+ frame[0] = (byte)length; frame[1] = (byte)(length >> 8); frame[2] = (byte)(length >> 16); frame[3] = (byte)(length >> 24);
frame[4] = (byte)type;
Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
return frame;
@@ -215,16 +216,30 @@ namespace MSCO
}
}
- /// TCP-Verbindung (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt.
+ ///
+ /// TCP-Verbindung (LAN / IP). Lesen ist nicht-blockierend und wird über Poll() abgefragt. Senden läuft über eine
+ /// Warteschlange und einen eigenen Thread: vorher schrieb das Spiel selbst (blockierend) - war die Leitung kurz voll
+ /// (Spielstand hochladen, schwaches WLAN), stand das ganze Spiel. Kleine Pakete werden dabei zusammengefasst.
+ ///
public class Connection : IConnection
{
- public bool IsClosed { get; private set; }
- public string CloseReason { get; private set; }
+ volatile bool closed;
+ volatile string closeReason;
+ public bool IsClosed { get { return closed; } }
+ public string CloseReason { get { return closeReason; } }
+
+ // Mehr als das unterwegs = die Gegenseite nimmt nichts mehr an
+ const long MaxQueuedBytes = 96L * 1024 * 1024;
+ const int MaxBatchBytes = 256 * 1024;
readonly TcpClient client;
readonly NetworkStream stream;
readonly FrameParser parser = new FrameParser();
readonly byte[] readBuffer = new byte[64 * 1024];
+ readonly Queue outgoing = new Queue();
+ readonly object gate = new object();
+ long queuedBytes;
+ readonly System.Threading.Thread writer;
public Connection(TcpClient client)
{
@@ -232,11 +247,46 @@ namespace MSCO
client.NoDelay = true;
client.SendBufferSize = 4 * 1024 * 1024;
stream = client.GetStream();
+ writer = new System.Threading.Thread(WriteLoop) { IsBackground = true, Name = "MSCO send" };
+ writer.Start();
}
+ /// Sende-Thread: wartet auf Pakete und schreibt sie (gebündelt) in die Verbindung.
+ void WriteLoop()
+ {
+ var batch = new MemoryStream(MaxBatchBytes);
+ try
+ {
+ while (true)
+ {
+ batch.SetLength(0);
+ lock (gate)
+ {
+ while (outgoing.Count == 0 && !closed) System.Threading.Monitor.Wait(gate);
+ if (outgoing.Count == 0) return; // geschlossen und nichts mehr zu senden
+ // Mehrere kleine Pakete in einem Rutsch (große einzeln)
+ do
+ {
+ byte[] frame = outgoing.Dequeue();
+ queuedBytes -= frame.Length;
+ batch.Write(frame, 0, frame.Length);
+ } while (outgoing.Count > 0 && batch.Length + outgoing.Peek().Length <= MaxBatchBytes);
+ System.Threading.Monitor.PulseAll(gate); // Close() wartet evtl. aufs Leerwerden
+ }
+ stream.Write(batch.GetBuffer(), 0, (int)batch.Length);
+ }
+ }
+ catch (Exception e)
+ {
+ if (!closed) Close("Network error: " + e.Message);
+ }
+ }
+
+ readonly List polled = new List(); // wiederverwendet: der Aufrufer geht sie sofort durch
public List Poll()
{
- var messages = new List();
+ var messages = polled;
+ messages.Clear();
if (IsClosed) return messages;
try
{
@@ -263,23 +313,37 @@ namespace MSCO
public void Send(MsgType type, byte[] payload)
{
- if (IsClosed) return;
- try
+ if (closed) return;
+ byte[] frame = FrameParser.Frame(type, payload);
+ bool tooMuch;
+ lock (gate)
{
- byte[] frame = FrameParser.Frame(type, payload);
- stream.Write(frame, 0, frame.Length); // blockierend, damit große Dateien komplett rausgehen
- }
- catch (Exception e)
- {
- Close("Network error: " + e.Message);
+ tooMuch = queuedBytes + frame.Length > MaxQueuedBytes;
+ if (!tooMuch)
+ {
+ outgoing.Enqueue(frame);
+ queuedBytes += frame.Length;
+ System.Threading.Monitor.Pulse(gate);
+ }
}
+ if (tooMuch) Close("The connection is too slow - too much data waiting to be sent");
}
public void Close(string reason)
{
- if (IsClosed) return;
- IsClosed = true;
- CloseReason = reason;
+ lock (gate)
+ {
+ if (closed) return;
+ // Was schon in der Warteschlange liegt (z.B. eine Abschiedsnachricht), kurz noch rausgehen lassen
+ var until = DateTime.UtcNow.AddMilliseconds(300);
+ while (outgoing.Count > 0 && DateTime.UtcNow < until && System.Threading.Thread.CurrentThread != writer)
+ System.Threading.Monitor.Wait(gate, 50);
+ closed = true;
+ closeReason = reason;
+ outgoing.Clear();
+ queuedBytes = 0;
+ System.Threading.Monitor.PulseAll(gate);
+ }
try { client.Close(); } catch { }
}
}
@@ -313,9 +377,11 @@ namespace MSCO
lastReceive = Time.realtimeSinceStartup;
}
+ readonly List polled = new List(); // wiederverwendet: der Aufrufer geht sie sofort durch
public List Poll()
{
- var messages = new List();
+ var messages = polled;
+ messages.Clear();
if (IsClosed) return messages;
try { parser.Parse(messages); }
catch (Exception e) { Close("Network error: " + e.Message); }
@@ -419,9 +485,11 @@ namespace MSCO
return new NetSession { IsSteam = true, server = new SteamConnection(host), pendingConnected = true };
}
+ readonly List polledEvents = new List(); // wiederverwendet (jedes Frame)
public List Poll()
{
- var events = new List();
+ var events = polledEvents;
+ events.Clear();
if (IsClosed) return events;
try
{
@@ -568,15 +636,41 @@ namespace MSCO
try { if (listener != null) listener.Stop(); } catch { }
}
+ // Puffer zum Bauen von Nachrichten pro Thread wiederverwenden (vorher pro Nachricht neuer Speicherstrom + Schreiber:
+ // bei vielen Nachrichten pro Sekunde viel Speichermüll -> häufiger Garbage Collector -> Ruckler)
+ [ThreadStatic] static MemoryStream buildStream;
+ [ThreadStatic] static BinaryWriter buildWriter;
+ [ThreadStatic] static bool building;
+
public static byte[] Build(Action write)
{
- using (var ms = new MemoryStream())
- using (var w = new BinaryWriter(ms))
+ if (building)
{
- write(w);
- w.Flush();
- return ms.ToArray();
+ // Build innerhalb von Build: eigener Puffer
+ using (var ms = new MemoryStream())
+ using (var w = new BinaryWriter(ms))
+ {
+ write(w);
+ w.Flush();
+ return ms.ToArray();
+ }
}
+ if (buildStream == null)
+ {
+ buildStream = new MemoryStream(4096);
+ buildWriter = new BinaryWriter(buildStream);
+ }
+ building = true;
+ try
+ {
+ buildStream.SetLength(0);
+ write(buildWriter);
+ buildWriter.Flush();
+ byte[] result = buildStream.ToArray();
+ if (buildStream.Capacity > 1024 * 1024) { buildStream = null; buildWriter = null; } // große Dateien nicht ewig im Speicher halten
+ return result;
+ }
+ finally { building = false; }
}
/// Stellt der Nachricht die Absender-ID voran (für weitergeleitete Nachrichten).
diff --git a/UiTheme.cs b/UiTheme.cs
index e2c58d9..5ad6c6c 100644
--- a/UiTheme.cs
+++ b/UiTheme.cs
@@ -10,6 +10,26 @@ namespace MSCO
///
public static class UiTheme
{
+ static Font dynamicFont;
+ static bool fontSearched;
+
+ ///
+ /// Dynamische Schrift (eingebautes Arial) für alle MSCO-Oberflächen: Schriftgröße/Fett gehen nur mit dynamischen
+ /// Schriften - sonst schreibt Unity jedes Mal eine Warnung ins Log (400-mal in einer Sitzung, mit Rucklern).
+ ///
+ public static Font DynamicFont
+ {
+ get
+ {
+ if (!fontSearched)
+ {
+ fontSearched = true;
+ try { dynamicFont = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font; } catch { }
+ if (dynamicFont != null && !dynamicFont.dynamic) dynamicFont = null;
+ }
+ return dynamicFont;
+ }
+ }
public static readonly Color Background = Hex(0x16, 0x16, 0x1a, 0.97f);
public static readonly Color Panel = Hex(0x0f, 0x0f, 0x12, 0.98f);
public static readonly Color Row = Hex(0x22, 0x22, 0x28);
@@ -170,6 +190,7 @@ namespace MSCO
public static GUIStyle Button(Color normal, Color hover, Color text, int fontSize = 13, bool bold = true)
{
var s = new GUIStyle(GUI.skin.button);
+ if (DynamicFont != null) s.font = DynamicFont;
s.normal.background = Rounded(normal);
s.hover.background = Rounded(hover);
s.active.background = Rounded(hover);
@@ -186,6 +207,7 @@ namespace MSCO
public static GUIStyle Field(int fontSize = 14)
{
var s = new GUIStyle(GUI.skin.textField);
+ if (DynamicFont != null) s.font = DynamicFont;
s.normal.background = Rounded(FieldColor);
s.hover.background = Rounded(FieldFocus);
s.focused.background = Rounded(FieldFocus);
@@ -200,6 +222,7 @@ namespace MSCO
public static GUIStyle Label(int fontSize, Color color, FontStyle font = FontStyle.Normal, TextAnchor align = TextAnchor.UpperLeft)
{
var s = new GUIStyle(GUI.skin.label);
+ if (DynamicFont != null) s.font = DynamicFont;
s.fontSize = fontSize;
s.fontStyle = font;
s.normal.textColor = color;
diff --git a/VehicleAdmin.cs b/VehicleAdmin.cs
index 206a404..6813fdc 100644
--- a/VehicleAdmin.cs
+++ b/VehicleAdmin.cs
@@ -61,10 +61,13 @@ namespace MSCO
new[] { "Radiator", "Water", "Coolant", "6" },
};
- /// Database neu einlesen (höchstens alle 5 s, außer erzwungen).
+ ///
+ /// Database neu einlesen (höchstens alle 30 s, außer erzwungen - nach jeder Aktion). Das Durchsuchen aller FSMs
+ /// dauert spürbar (Ruckler bei offenem Admin-Menü); die angezeigten Werte selbst sind ohnehin live.
+ ///
public static void Refresh(bool force)
{
- if (!force && Time.realtimeSinceStartup - scannedAt < 5f) return;
+ if (!force && Time.realtimeSinceStartup - scannedAt < 30f) return;
scannedAt = Time.realtimeSinceStartup;
fluids.Clear();
parts.Clear();
diff --git a/WorldAdmin.cs b/WorldAdmin.cs
index eb692c7..7645bee 100644
--- a/WorldAdmin.cs
+++ b/WorldAdmin.cs
@@ -28,9 +28,9 @@ namespace MSCO
static void Find()
{
if (sun != null && weather != null) return;
- if (Time.realtimeSinceStartup - searchedAt < 3f) return;
+ if (Time.realtimeSinceStartup - searchedAt < 15f) return; // nicht ständig suchen, falls es sie nicht gibt
searchedAt = Time.realtimeSinceStartup;
- foreach (PlayMakerFSM f in SceneCache.Fsms(5f))
+ foreach (PlayMakerFSM f in SceneCache.Fsms(60f))
{
if (f == null) continue;
if (sun == null && f.FsmVariables.FindFsmInt("Time") != null && f.Fsm.GetState("12-14") != null
diff --git a/examples/ShowBoltSize/ShowBoltSize.cs b/examples/ShowBoltSize/ShowBoltSize.cs
index 9504df0..31dc5e8 100644
--- a/examples/ShowBoltSize/ShowBoltSize.cs
+++ b/examples/ShowBoltSize/ShowBoltSize.cs
@@ -7,9 +7,9 @@ using UnityEngine;
[assembly: AssemblyTitle("Show Bolt Size")]
[assembly: AssemblyProduct("Show Bolt Size")]
[assembly: AssemblyDescription("Shows the size of the bolt you look at, so you pick the right wrench.")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
-[assembly: AssemblyInformationalVersion("1.0.0")]
+[assembly: AssemblyVersion("1.0.1.0")]
+[assembly: AssemblyFileVersion("1.0.1.0")]
+[assembly: AssemblyInformationalVersion("1.0.1")]
namespace ShowBoltSize
{
@@ -22,12 +22,12 @@ namespace ShowBoltSize
{
public override string ID { get { return "showboltsize"; } }
public override string Name { get { return "Show Bolt Size"; } }
- public override string Version { get { return "1.0.0"; } }
+ public override string Version { get { return "1.0.1"; } }
public override string Author { get { return "MBM"; } }
const float MaxDistance = 1.3f;
const float BoltRadius = 0.025f; // so groß wirkt eine Schraube beim Zielen
- const float ScanInterval = 2f;
+ const float ScanInterval = 5f;
sealed class Bolt
{
@@ -41,28 +41,47 @@ namespace ShowBoltSize
float nextScan;
string label;
GUIStyle style;
+ Camera camera;
+ bool reported;
- public override void OnMenuLoad() { inGame = false; bolts.Clear(); label = null; }
+ public override void OnMenuLoad() { inGame = false; bolts.Clear(); label = null; camera = null; reported = false; }
public override void OnGameLoad() { inGame = true; nextScan = 0f; }
+ ///
+ /// Spielerkamera: Camera.main gibt es in My Summer Car nicht immer (die Kamera des Spielers ist "FPSCamera").
+ ///
+ Camera FindCamera()
+ {
+ if (camera != null && camera.enabled && camera.gameObject.activeInHierarchy) return camera;
+ camera = null;
+ foreach (Camera c in Camera.allCameras)
+ if (c.name == "FPSCamera") { camera = c; break; }
+ if (camera == null) camera = Camera.main;
+ return camera;
+ }
+
public override void Update()
{
label = null;
if (!inGame) return;
- Camera cam = Camera.main;
+ Camera cam = FindCamera();
if (cam == null) return;
- if (Time.realtimeSinceStartup >= nextScan) Scan();
+ if (Time.realtimeSinceStartup >= nextScan) Scan(cam);
Transform eye = cam.transform;
+ Vector3 eyePos = eye.position, forward = eye.forward;
Bolt best = null;
float bestAngle = float.MaxValue;
- foreach (Bolt b in bolts)
+ for (int i = 0; i < bolts.Count; i++)
{
- if (b.Transform == null || !b.Transform.gameObject.activeInHierarchy) continue;
- Vector3 to = b.Transform.position - eye.position;
- float distance = to.magnitude;
- if (distance > MaxDistance || distance < 0.05f) continue;
- float angle = Vector3.Angle(eye.forward, to);
+ Bolt b = bolts[i];
+ if (b.Transform == null) continue;
+ Vector3 to = b.Transform.position - eyePos;
+ float sq = to.sqrMagnitude;
+ if (sq > MaxDistance * MaxDistance || sq < 0.0025f) continue;
+ if (!b.Transform.gameObject.activeInHierarchy) continue;
+ float distance = Mathf.Sqrt(sq);
+ float angle = Vector3.Angle(forward, to);
float limit = Mathf.Atan2(BoltRadius, distance) * Mathf.Rad2Deg + 0.5f;
if (angle < limit && angle < bestAngle)
{
@@ -73,11 +92,15 @@ namespace ShowBoltSize
if (best != null) label = Describe(best);
}
- void Scan()
+ ///
+ /// Schrauben aus MSCOs gemeinsamer Liste aller FSMs holen (SceneCache) - nicht selbst das ganze Spiel durchsuchen,
+ /// das ließ das Spiel alle paar Sekunden kurz hängen.
+ ///
+ void Scan(Camera cam)
{
nextScan = Time.realtimeSinceStartup + ScanInterval;
bolts.Clear();
- foreach (PlayMakerFSM fsm in Object.FindObjectsOfType())
+ foreach (PlayMakerFSM fsm in SceneCache.Fsms(30f))
{
if (fsm == null || fsm.FsmName != "Screw") continue;
FsmFloat f = fsm.FsmVariables.FindFsmFloat("BoltSize");
@@ -85,6 +108,11 @@ namespace ShowBoltSize
if (f == null && i == null) continue;
bolts.Add(new Bolt { Transform = fsm.transform, SizeF = f, SizeI = i });
}
+ if (!reported)
+ {
+ reported = true;
+ Log("camera " + cam.name + ", " + bolts.Count + " bolts with a size");
+ }
}
/// Größe in mm. Das Spiel speichert sie je nach Schraube als 10 oder als 1.0 (= 10 mm).