diff --git a/GameCharacter.cs b/GameCharacter.cs
deleted file mode 100644
index 37f7a42..0000000
--- a/GameCharacter.cs
+++ /dev/null
@@ -1,431 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using UnityEngine;
-
-namespace MSCO
-{
- ///
- /// Mitspieler-Figur mit dem Aussehen eines echten My-Summer-Car-NPCs (Körper-Mesh "bodymesh" + Skelett "pelvis").
- /// Vom NPC wird nur das Aussehen übernommen: Seine aktuelle Haltung (evtl. Ragdoll, liegend, betrunken) wird
- /// verworfen und das Skelett auf die Bind-Pose des Meshes gesetzt. Bewegt wird die Figur komplett selbst -
- /// wie die frühere Ersatzfigur: Laufzyklus der Beine, schwingende Arme, Ducken, Lehnen, Kopf-Nicken, Sitzen
- /// und Greifen zum Item (Zwei-Knochen-IK, unabhängig von den Achsen des Skeletts).
- ///
- public class GameCharacter
- {
- const float StandingEyeHeight = 1.65f;
- const float EyeAboveHeadBone = 0.1f; // Augen liegen etwas über dem Kopf-Knochen
- const float AnkleHeight = 0.07f; // Fuß-Knochen über dem Boden
-
- int frames;
- bool calibrated;
-
- public Transform Root { get; private set; }
- public string Description { get; private set; }
-
- Transform pelvis, spine, head;
- float walkPhase, walkAmount; // eigener Laufzyklus (0..2π) und wie stark gerade gelaufen wird
- string bindInfo = "";
- readonly Transform[] thigh = new Transform[2], knee = new Transform[2], foot = new Transform[2];
- readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
- readonly Vector3[] animatedFoot = new Vector3[2];
-
- /// Grundhaltung aller Knochen (Bind-Pose); jedes Frame wird von hier aus neu gebogen.
- class BoneState
- {
- public Transform Bone;
- public Vector3 RestPos;
- public Quaternion RestRot;
- }
- readonly List posed = new List();
-
- public Vector3 HeadPosition { get { return head.position + Root.up * EyeAboveHeadBone; } }
-
- // ---------------------------------------------------------------- Vorlage finden und kopieren
-
- static List templates;
-
- /// Kopiert einen NPC aus der Spielszene als Figur. null, wenn keine passende Vorlage da ist.
- public static GameCharacter TryCreate(Transform parent, int variant)
- {
- List candidates = FindTemplates();
- if (candidates.Count == 0) return null;
- GameObject template = candidates[Mathf.Abs(variant) % candidates.Count];
-
- GameObject clone = (GameObject)UnityEngine.Object.Instantiate(template);
- clone.name = "Model";
- Strip(clone);
- clone.SetActive(true);
- clone.transform.SetParent(parent, false);
- clone.transform.localPosition = Vector3.zero;
- clone.transform.localRotation = Quaternion.identity;
- clone.transform.localScale = Vector3.one;
-
- var c = new GameCharacter { Root = clone.transform };
- // Haltung des Vorbilds verwerfen (Ragdoll/liegend/sitzend) -> saubere Grundhaltung des Meshes
- int bound = c.ApplyBindPose();
- if (!c.FindBones())
- {
- UnityEngine.Object.Destroy(clone);
- return null;
- }
- c.Normalize();
- c.RememberRestPose();
- c.bindInfo = bound > 0 ? "bind pose " + bound + " bones" : "no bind pose";
- c.Description = template.name + " (" + candidates.Count + " templates) - " + c.BoneReport();
- return c;
- }
-
- /// NPC-Körper in der Szene (auch inaktive): SkinnedMeshRenderer "bodymesh" mit "pelvis"-Knochen.
- static List FindTemplates()
- {
- if (templates != null)
- {
- templates.RemoveAll(t => t == null);
- if (templates.Count > 0) return templates;
- }
- var scored = new List>();
- var seen = new HashSet();
- foreach (UnityEngine.Object o in Resources.FindObjectsOfTypeAll(typeof(SkinnedMeshRenderer)))
- {
- var smr = (SkinnedMeshRenderer)o;
- if (smr == null || !smr.name.ToLower().Contains("bodymesh") || smr.transform.parent == null) continue;
- bool hasPelvis = false;
- if (smr.bones != null)
- foreach (Transform b in smr.bones)
- if (b != null && b.name == "pelvis") { hasPelvis = true; break; }
- if (!hasPelvis) continue;
-
- GameObject root = smr.transform.parent.gameObject; // Körper + Skelett liegen gemeinsam darunter
- if (root.transform.root.name == "PLAYER" || root.name.StartsWith("MSCO")) continue;
- // Keine Sammelgruppen mit mehreren Personen kopieren (nur eine Figur, evtl. mit Varianten dünn/normal/dick)
- if (PelvisCount(root.transform) != 1 || root.GetComponentsInChildren(true).Length > 600) continue;
-
- int score = AnimationScore(ref root);
- if (!seen.Add(root.name + "/" + smr.sharedMesh)) continue; // gleiche Figur nicht mehrfach
- scored.Add(new KeyValuePair(root, score));
- }
-
- // Nur die besten: am liebsten NPCs mit Lauf- UND Steh-Animation (ohne Gesten wie Anhalter/Betrunken)
- int best = 0;
- foreach (var kv in scored) best = Mathf.Max(best, kv.Value);
- templates = new List();
- foreach (var kv in scored) if (kv.Value == best) templates.Add(kv.Key);
- templates.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
- return templates;
- }
-
- static int PelvisCount(Transform root)
- {
- int count = 0;
- foreach (Transform t in root.GetComponentsInChildren(true)) if (t.name == "pelvis") count++;
- return count;
- }
-
- // Animationen, die keine normale Steh-/Laufhaltung sind
- static readonly string[] GestureWords = { "hitch", "drunk", "fat", "lift", "sit", "wave", "point", "dance", "angry", "back", "theatre", "car" };
-
- static bool IsGesture(string clip)
- {
- string n = clip.ToLower();
- foreach (string g in GestureWords) if (n.Contains(g)) return true;
- return false;
- }
-
- ///
- /// Bewertet die Animationen eines NPCs (liegt die Animation eine Ebene höher, wird dieser Knoten
- /// die Vorlage). 3 = normales Laufen + Stehen, 2 = nur Laufen, 1 = nur Stehen, 0 = keine.
- ///
- static int AnimationScore(ref GameObject root)
- {
- Animation[] found = root.GetComponentsInChildren(true); // Unity 5.0: auch inaktive finden
- Animation anim = found.Length > 0 ? found[0] : null;
- if (anim == null)
- {
- Transform p = root.transform.parent;
- for (int i = 0; i < 2 && p != null && anim == null; i++, p = p.parent)
- {
- var candidate = p.GetComponent();
- if (candidate != null && PelvisCount(p) == 1)
- {
- anim = candidate;
- root = p.gameObject;
- }
- }
- }
- if (anim == null) return 0;
-
- bool walk = false, idle = false;
- foreach (AnimationState s in anim)
- {
- if (IsGesture(s.name)) continue;
- string n = s.name.ToLower();
- if (n.Contains("walk")) walk = true;
- if (n.Contains("idle")) idle = true;
- }
- return (walk ? 2 : 0) + (idle ? 1 : 0);
- }
-
- ///
- /// Alles außer dem Aussehen entfernen: KI-FSMs, Physik (Ragdoll-Gelenke), Sounds, Trigger und auch die
- /// Spiel-Animationen - die Figur wird komplett selbst bewegt.
- ///
- static void Strip(GameObject go)
- {
- foreach (Joint j in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(j);
- foreach (Rigidbody r in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(r);
- foreach (Collider c in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(c);
- foreach (Behaviour b in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(b);
- foreach (Animation a in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(a);
- foreach (AudioSource a in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(a);
- // Figur nicht ausblenden, nur weil ihr Ursprung gerade außerhalb des Bildes liegt
- foreach (SkinnedMeshRenderer smr in go.GetComponentsInChildren(true))
- smr.updateWhenOffscreen = true;
- }
-
- ///
- /// Setzt alle Knochen auf die Haltung, in der das Mesh gebaut wurde (Bind-Pose aus sharedMesh.bindposes).
- /// So ist die Figur aufrecht, egal wie das Vorbild gerade lag. Gibt die Zahl der gesetzten Knochen zurück.
- ///
- int ApplyBindPose()
- {
- SkinnedMeshRenderer body = null;
- foreach (SkinnedMeshRenderer smr in Root.GetComponentsInChildren(true))
- if (smr.name.ToLower().Contains("bodymesh") && smr.sharedMesh != null) { body = smr; break; }
- if (body == null || body.bones == null) return 0;
-
- Matrix4x4[] bindposes = body.sharedMesh.bindposes;
- Transform[] bones = body.bones;
- int count = Mathf.Min(bindposes.Length, bones.Length);
-
- // Eltern vor Kindern setzen (Weltwerte; Kinder überschreiben danach nur sich selbst)
- var order = new List();
- for (int i = 0; i < count; i++) if (bones[i] != null) order.Add(i);
- order.Sort((a, b) => Depth(bones[a]).CompareTo(Depth(bones[b])));
-
- Matrix4x4 meshToWorld = body.transform.localToWorldMatrix;
- int set = 0;
- foreach (int i in order)
- {
- Matrix4x4 world = meshToWorld * bindposes[i].inverse;
- Vector3 forward = world.GetColumn(2), up = world.GetColumn(1);
- if (forward.sqrMagnitude < 1e-8f || up.sqrMagnitude < 1e-8f) continue;
- bones[i].position = world.GetColumn(3);
- bones[i].rotation = Quaternion.LookRotation(forward, up);
- set++;
- }
- return set;
- }
-
- static int Depth(Transform t)
- {
- int d = 0;
- for (; t != null; t = t.parent) d++;
- return d;
- }
-
- bool FindBones()
- {
- pelvis = Find("pelvis");
- spine = Find("spine_upper") ?? Find("spine_middle") ?? Find("spine_mid");
- head = Find("head");
- if (pelvis == null || spine == null || head == null) return false;
-
- string[] sides = { "left", "right" };
- for (int i = 0; i < 2; i++)
- {
- knee[i] = Find("knee_" + sides[i]);
- if (knee[i] != null)
- {
- thigh[i] = knee[i].parent;
- foot[i] = knee[i].childCount > 0 ? knee[i].GetChild(0) : null;
- }
- hand[i] = Find("hand_" + sides[i]);
- if (hand[i] != null && hand[i].parent != null)
- {
- forearm[i] = hand[i].parent;
- upperArm[i] = forearm[i].parent;
- }
- }
- return true;
- }
-
- Transform Find(string name)
- {
- foreach (Transform t in Root.GetComponentsInChildren(true))
- if (string.Equals(t.name, name, StringComparison.OrdinalIgnoreCase)) return t;
- return null;
- }
-
- /// Größe auf Spielergröße bringen, Füße auf den Boden, Blickrichtung nach +Z.
- void Normalize()
- {
- // Blickrichtung: Zehen (Kind des Fußes) liegen vor dem Fuß
- Transform toe = foot[0] != null && foot[0].childCount > 0 ? foot[0].GetChild(0) : null;
- if (toe != null && Root.parent.InverseTransformPoint(toe.position).z < Root.parent.InverseTransformPoint(foot[0].position).z - 0.01f)
- Root.localRotation = Quaternion.Euler(0f, 180f, 0f);
-
- float eye = Root.parent.InverseTransformPoint(head.position).y + EyeAboveHeadBone;
- if (eye > 0.5f) Root.localScale = Vector3.one * Mathf.Clamp(StandingEyeHeight / eye, 0.6f, 1.6f);
-
- float lowest = float.MaxValue;
- for (int i = 0; i < 2; i++)
- if (foot[i] != null) lowest = Mathf.Min(lowest, Root.parent.InverseTransformPoint(foot[i].position).y);
- if (lowest < float.MaxValue) Root.localPosition = new Vector3(0f, -lowest + 0.06f, 0f);
- }
-
- /// Grundhaltung aller Knochen merken (nicht Root selbst - den stellt Pose auf den Boden).
- void RememberRestPose()
- {
- foreach (Transform b in Root.GetComponentsInChildren(true))
- if (b != Root)
- posed.Add(new BoneState { Bone = b, RestPos = b.localPosition, RestRot = b.localRotation });
- }
-
- /// Jedes Frame zurück zur Grundhaltung, damit sich nichts aufsummiert.
- void ResetToRest()
- {
- foreach (BoneState st in posed)
- {
- if (st.Bone == null) continue;
- st.Bone.localPosition = st.RestPos;
- st.Bone.localRotation = st.RestRot;
- }
- }
-
- string BoneReport()
- {
- var sb = new StringBuilder();
- sb.Append("legs ").Append(knee[0] != null && foot[0] != null ? "ok" : "missing");
- sb.Append(", arms ").Append(hand[1] != null && upperArm[1] != null ? "ok" : "missing");
- sb.Append(", ").Append(bindInfo).Append(", procedural animation");
- return sb.ToString();
- }
-
- // ---------------------------------------------------------------- Pro Frame (LateUpdate, nach der Animation)
-
- /// Eigener Laufzyklus: Tempo aus der Geschwindigkeit, weiches Ein-/Ausblenden beim Anhalten.
- public void Animate(float speed)
- {
- float dt = Time.deltaTime;
- float target = Mathf.Clamp01((speed - 0.2f) / 1.2f);
- walkAmount = Mathf.MoveTowards(walkAmount, target, dt * 4f);
- // ca. 1,8 Schritte pro Sekunde beim Gehen, schneller beim Rennen
- walkPhase += dt * Mathf.Lerp(0f, Mathf.Clamp(speed * 2.6f, 5f, 13f), walkAmount);
- if (walkPhase > Mathf.PI * 2f) walkPhase -= Mathf.PI * 2f;
- }
-
- public void Pose(Transform body, Vector3 headTarget, float pitch, bool seated, bool holding, Vector3 heldPos)
- {
- Vector3 up = body.up, forward = body.forward, right = body.right;
- ResetToRest();
- frames++;
-
- // 1) Auf den Boden stellen: die (animierten) Füße stehen auf der Bodenhöhe des Spielers.
- // Die Grundhaltung eines NPCs passt oft nicht zur Animation - deshalb jedes Frame nachmessen.
- float lowestFoot = float.MaxValue;
- for (int i = 0; i < 2; i++)
- if (foot[i] != null) lowestFoot = Mathf.Min(lowestFoot, Vector3.Dot(foot[i].position - body.position, up));
- if (lowestFoot < float.MaxValue)
- {
- Vector3 lp = Root.localPosition;
- lp.y -= lowestFoot - AnkleHeight;
- Root.localPosition = lp;
- }
-
- // 2) Größe einmal an die laufende Animation anpassen (Augenhöhe wie der Spieler)
- if (!calibrated && frames > 5 && !seated)
- {
- calibrated = true;
- float eye = Vector3.Dot(head.position + up * EyeAboveHeadBone - body.position, up);
- if (eye > 0.5f) Root.localScale *= Mathf.Clamp(StandingEyeHeight / eye, 0.7f, 1.4f);
- return; // nächstes Frame mit neuer Größe neu auf den Boden stellen
- }
-
- // Fußziele: Grundstellung unter der Hüfte plus Laufzyklus (Schritt vor/zurück, Fuß anheben)
- float stride = 0.28f * walkAmount, lift = 0.13f * walkAmount;
- for (int i = 0; i < 2; i++)
- {
- if (foot[i] == null) continue;
- float phase = walkPhase + (i == 0 ? 0f : Mathf.PI);
- animatedFoot[i] = foot[i].position + forward * (Mathf.Sin(phase) * stride) + up * (Mathf.Max(0f, Mathf.Cos(phase)) * lift);
- }
- // Leichtes Wippen beim Gehen
- pelvis.position += up * (Mathf.Abs(Mathf.Sin(walkPhase)) * 0.03f * walkAmount);
-
- // 3) Becken: beim Ducken/Sitzen absenken, beim Lehnen nach hinten ausweichen
- Vector3 eyeNow = head.position + up * EyeAboveHeadBone;
- Vector3 offset = headTarget - eyeNow;
- Vector3 horizontal = offset - up * Vector3.Dot(offset, up);
- if (horizontal.magnitude > 0.6f) horizontal = horizontal.normalized * 0.6f;
- float drop = Mathf.Clamp(Vector3.Dot(offset, up), -1.1f, 0f);
- pelvis.position += up * drop - horizontal * 0.25f;
-
- // Wirbelsäule so drehen, dass der Kopf an der Kamera sitzt (nur wenn nötig)
- if (horizontal.sqrMagnitude > 0.0025f || drop < -0.05f)
- Aim(spine, head.position + up * EyeAboveHeadBone, head.position + up * EyeAboveHeadBone + horizontal * 0.75f);
- // Kopf nicken
- head.rotation = Quaternion.AngleAxis(pitch, right) * head.rotation;
-
- // Beine
- for (int i = 0; i < 2; i++)
- {
- if (thigh[i] == null || knee[i] == null || foot[i] == null) continue;
- Vector3 target = seated
- ? thigh[i].position + forward * 0.45f - up * 0.4f
- : animatedFoot[i];
- SolveTwoBone(thigh[i], knee[i], foot[i], target, forward);
- }
-
- // Arme: rechts zum gehaltenen Item, im Auto beide nach vorne (Lenkrad), sonst locker hängend und beim
- // Gehen gegengleich zu den Beinen schwingend (die Grundhaltung ist oft ein T mit waagrechten Armen)
- for (int i = 0; i < 2; i++)
- {
- if (upperArm[i] == null || forearm[i] == null || hand[i] == null) continue;
- Vector3 shoulder = upperArm[i].position;
- float armLength = (forearm[i].position - shoulder).magnitude + (hand[i].position - forearm[i].position).magnitude;
- float side = Vector3.Dot(shoulder - body.position, right) >= 0f ? 1f : -1f;
- if (i == 1 && holding)
- SolveTwoBone(upperArm[i], forearm[i], hand[i], heldPos, -forward - up * 0.5f);
- else if (seated)
- SolveTwoBone(upperArm[i], forearm[i], hand[i], shoulder + forward * 0.38f - up * 0.28f, -up);
- else
- {
- float swing = Mathf.Sin(walkPhase + (i == 0 ? Mathf.PI : 0f)) * 0.22f * walkAmount;
- Vector3 handTarget = shoulder - up * (armLength * 0.92f) + right * (side * 0.07f) + forward * (0.05f + swing);
- SolveTwoBone(upperArm[i], forearm[i], hand[i], handTarget, -forward);
- }
- }
- }
-
- /// Knochen so drehen, dass "child" in Richtung "target" zeigt - egal wie das Skelett ausgerichtet ist.
- static void Aim(Transform bone, Vector3 childPos, Vector3 target)
- {
- Vector3 from = childPos - bone.position;
- Vector3 to = target - bone.position;
- if (from.sqrMagnitude < 1e-6f || to.sqrMagnitude < 1e-6f) return;
- bone.rotation = Quaternion.FromToRotation(from, to) * bone.rotation;
- }
-
- static void SolveTwoBone(Transform upper, Transform lower, Transform end, Vector3 target, Vector3 bendHint)
- {
- float a = (lower.position - upper.position).magnitude;
- float b = (end.position - lower.position).magnitude;
- if (a < 0.01f || b < 0.01f) return;
-
- Vector3 root = upper.position;
- Vector3 toTarget = target - root;
- float d = Mathf.Clamp(toTarget.magnitude, 0.01f, a + b - 0.001f);
- Vector3 dir = toTarget.normalized;
- float along = (a * a - b * b + d * d) / (2f * d);
- float sideways = Mathf.Sqrt(Mathf.Max(0f, a * a - along * along));
- Vector3 bend = bendHint - dir * Vector3.Dot(bendHint, dir);
- if (bend.sqrMagnitude < 1e-6f) bend = Vector3.up - dir * Vector3.Dot(Vector3.up, dir);
- Vector3 joint = root + dir * along + bend.normalized * sideways;
-
- Aim(upper, lower.position, joint);
- Aim(lower, end.position, root + dir * d);
- }
- }
-}
diff --git a/MSCO.csproj b/MSCO.csproj
index 6b90015..91eec66 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -86,7 +86,6 @@
-
diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs
index 2ceeb71..57a5506 100644
--- a/MultiplayerManager.cs
+++ b/MultiplayerManager.cs
@@ -1435,8 +1435,12 @@ namespace MSCO
static Color PlayerColor(int id)
{
- Color[] colors = { Color.cyan, new Color(1f, 0.55f, 0.1f), Color.green, Color.magenta,
- Color.yellow, new Color(0.4f, 0.6f, 1f), Color.red, Color.white };
+ // Gedeckte Farben wie Flanellhemden/Arbeitsjacken in MSC
+ Color[] colors =
+ {
+ new Color(0.6f, 0.13f, 0.12f), new Color(0.16f, 0.25f, 0.5f), new Color(0.2f, 0.38f, 0.2f), new Color(0.7f, 0.52f, 0.14f),
+ new Color(0.42f, 0.28f, 0.17f), new Color(0.33f, 0.36f, 0.4f), new Color(0.45f, 0.14f, 0.3f), new Color(0.38f, 0.4f, 0.16f)
+ };
return colors[id % colors.Length];
}
diff --git a/ObjectSync.cs b/ObjectSync.cs
index 4984abd..5f3c519 100644
--- a/ObjectSync.cs
+++ b/ObjectSync.cs
@@ -63,6 +63,7 @@ namespace MSCO
const float ReleaseAfterStill = 1f;
const float SnapDistance = 10f;
const float RemoteTimeout = 3f; // Besitzer meldet sich nicht mehr (z.B. Teil wurde dort montiert)
+ const float RunawaySpeed = 90f; // m/s - schneller fährt in MSC nichts; darüber hat die Physik es weggeschleudert
const float HandoverCooldown = 3f; // nach Freigabe/Übergabe keine Übernahme durch Bewegung
const float MaxHoldDistance = 1.5f; // gehaltene Items sind so nah an der Kamera
const float VehicleClaimSpeed = 0.7f; // Fahrzeuge erst ab echter Bewegung übernehmen (nicht Federn)
@@ -245,6 +246,7 @@ namespace MSCO
if (!b.Rb.gameObject.activeInHierarchy) continue;
bool active = b == held || b == driven;
+ if (!IsRemoteOwned(b)) StopRunaway(b);
if (b.OwnerId == SyncedBody.NoOwner)
{
if (active) Claim(b, true);
@@ -461,6 +463,25 @@ namespace MSCO
}
}
+ ///
+ /// Wird ein Objekt von der Physik weggeschleudert (z.B. nach dem Setzen einer Position, die im Boden lag),
+ /// sofort anhalten und auf die letzte gute Position zurücksetzen - statt es quer über die Karte fliegen zu lassen.
+ ///
+ void StopRunaway(SyncedBody b)
+ {
+ Rigidbody rb = b.Rb;
+ if (rb.isKinematic || rb.velocity.sqrMagnitude < RunawaySpeed * RunawaySpeed) return;
+ rb.velocity = Vector3.zero;
+ rb.angularVelocity = Vector3.zero;
+ if (IsLocal(b) && b.LastSentTime > 0f && IsValidPose(b.LastSentPos, b.LastSentRot))
+ {
+ rb.transform.position = AboveGround(b.LastSentPos, rb.transform);
+ rb.transform.rotation = b.LastSentRot;
+ }
+ else rb.transform.position = AboveGround(rb.position, rb.transform);
+ ModConsole.Warning("[MSCO] Stopped " + b.RootName + " - it was flung away by the physics");
+ }
+
/// Position innerhalb der Karte, Drehung gültig (gleiche Regel wie auf dem Server).
static bool IsValidPose(Vector3 p, Quaternion q)
{
diff --git a/README.md b/README.md
index 5665535..786014b 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,8 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
und nach dem Spielen wiederhergestellt
**Synchronisiert**
-- Spieler als Spielfigur (Modell aus den NPCs des Spiels) mit Blickrichtung, Ducken/Lehnen, Namensschild
+- Spieler als eigene Figur im MSC-Stil (Karohemd, Jeans, Schnurrbart, Kappe – je Spieler etwas anders) mit
+ Blickrichtung, Laufen, Ducken/Lehnen, Sitzen, gehaltenem Item und Namensschild
- Items, lose Teile, Schrauben und Fahrzeuge (auch mit G geschoben) mit Besitzer-System – inkl. was jemand in der Hand hält
- Fahrzeuge: Position, Lichter, Motorgeräusch; als **Beifahrer** mitfahren (Taste P)
- **Fahrzeugzustand:** Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schrauben-Festigkeit, Schäden, Verschleiß,
@@ -99,7 +100,7 @@ msbuild MSCO.csproj -p:Configuration=Release
| `ObjectSync.cs` | Items/Teile/Fahrzeuge mit Besitzer-System |
| `VehicleStateSync.cs`, `VehicleFx.cs`, `Passenger.cs` | Fahrzeugzustand, Lichter/Sound, Beifahrer |
| `FsmSync.cs`, `DoorSync.cs`, `WorldSync.cs`, `MoneySync.cs` | Montage-Events, Türen, Zeit/Wetter, Geld |
-| `RemotePlayer.cs`, `GameCharacter.cs` | Spielfiguren der Mitspieler |
+| `RemotePlayer.cs` | Spielfiguren der Mitspieler |
| `SaveSync.cs`, `MenuHelper.cs` | Spielstand übertragen/sichern, Laden aus dem Menü |
| `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Discord |
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
diff --git a/RemotePlayer.cs b/RemotePlayer.cs
index b54fa78..90cd1e1 100644
--- a/RemotePlayer.cs
+++ b/RemotePlayer.cs
@@ -3,10 +3,11 @@ using UnityEngine;
namespace MSCO
{
///
- /// Figur eines Mitspielers mit echter Körperhaltung. Aus Kopfposition (Kamera) und Bodenhöhe wird
- /// jedes Frame die Pose berechnet: Ducken beugt die Knie, nach vorne Lehnen neigt den Oberkörper,
- /// Laufen bewegt Beine und Arme, beim Halten greift der rechte Arm (mit Ellbogen) zum Item, im Auto
- /// sitzt die Figur. Die Körperteile sind abgerundete Kapseln zwischen den Gelenken.
+ /// Figur eines Mitspielers im Stil der My-Summer-Car-Leute (selbst gebaut, keine NPC-Kopie):
+ /// Karohemd/Arbeitsjacke, Jeans, Arbeitsschuhe, Schnurrbart, Kappe oder Haare, leichter Bauch.
+ /// Aus Kopfposition (Kamera) und Bodenhöhe wird jedes Frame die Pose berechnet: Ducken beugt die Knie,
+ /// nach vorne Lehnen neigt den Oberkörper, Laufen bewegt Beine und Arme, beim Halten greift der rechte Arm
+ /// zum Item, im Auto sitzt die Figur.
///
public class RemotePlayer : MonoBehaviour
{
@@ -21,16 +22,24 @@ namespace MSCO
const float HipWidth = 0.1f, ShoulderWidth = 0.21f;
const float AnkleHeight = 0.07f;
- static readonly Color Skin = new Color(0.93f, 0.76f, 0.62f);
- static readonly Color Jeans = new Color(0.18f, 0.25f, 0.42f);
- static readonly Color Boots = new Color(0.24f, 0.16f, 0.1f);
+ static readonly Color Skin = new Color(0.86f, 0.66f, 0.52f);
+ static readonly Color Jeans = new Color(0.2f, 0.26f, 0.38f);
+ static readonly Color Boots = new Color(0.22f, 0.15f, 0.09f);
+ static readonly Color Sole = new Color(0.07f, 0.06f, 0.05f);
static readonly Color Dark = new Color(0.08f, 0.08f, 0.08f);
+ static readonly Color Metal = new Color(0.62f, 0.62f, 0.6f);
+ static readonly Color[] HairColors =
+ {
+ new Color(0.23f, 0.15f, 0.09f), new Color(0.45f, 0.33f, 0.2f), new Color(0.12f, 0.1f, 0.08f), new Color(0.55f, 0.5f, 0.45f)
+ };
Color jacket;
+ int variant;
Transform headPivot, view;
- Transform pelvis, torso, belt, neck;
+ Transform pelvis, torso, belt, neck, belly, collar, placket, buckle;
readonly Transform[] thigh = new Transform[2], shin = new Transform[2], boot = new Transform[2];
readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
+ readonly Transform[] sole = new Transform[2], cuff = new Transform[2];
Vector3 targetFeet;
Vector3 targetHead;
@@ -42,13 +51,10 @@ namespace MSCO
Vector3 lastPosition;
float walkPhase, walkAmount, speed;
- // Echtes Spielmodell (NPC aus der Szene); null = eigene Figur als Rückfall
- GameCharacter model;
-
public int HeldId { get { return last.HeldId; } }
public PlayerState LastState { get { return last; } }
- public Vector3 HeadPosition { get { return model != null ? model.HeadPosition : headPivot.position; } }
- public string ModelDescription { get { return model != null ? "game model: " + model.Description : "fallback figure (no NPC template found)"; } }
+ public Vector3 HeadPosition { get { return headPivot.position; } }
+ public string ModelDescription { get { return "MSCO figure, variant " + variant; } }
// ---------------------------------------------------------------- Aufbau
@@ -57,43 +63,48 @@ namespace MSCO
var root = new GameObject(name);
var rp = root.AddComponent();
rp.jacket = jacketColor;
+ rp.variant = Mathf.Abs(variant);
rp.Build();
rp.SetTarget(state);
root.transform.position = rp.targetFeet;
root.transform.rotation = Quaternion.Euler(0f, rp.targetYaw, 0f);
-
- // Wenn möglich einen echten NPC aus dem Spiel als Figur nehmen, sonst die eigene Figur
- try { rp.model = GameCharacter.TryCreate(root.transform, variant); }
- catch (System.Exception e) { MSCLoader.ModConsole.Warning("[MSCO] Game model failed: " + e.Message); }
- if (rp.model != null)
- foreach (Renderer r in root.GetComponentsInChildren(true))
- if (!r.transform.IsChildOf(rp.model.Root)) r.enabled = false;
rp.headWorld = rp.targetHead;
rp.pitch = rp.targetPitch;
rp.view.rotation = rp.targetViewRot;
rp.lastPosition = root.transform.position;
- if (rp.model == null) rp.Pose(0f);
+ rp.Pose(0f);
return rp;
}
void Build()
{
Transform r = transform;
- Color jacketDark = jacket * 0.75f;
+ Color jacketDark = jacket * 0.7f;
jacketDark.a = 1f;
+ Color hairColor = HairColors[variant % HairColors.Length];
+ bool wearsCap = variant % 3 != 2;
+ bool sunglasses = variant % 4 == 1;
+ Texture2D plaid = Plaid(jacket);
+ // Körper: Karohemd/Jacke, Jeans, Gürtel, leichter Bauch (wie die Leute in MSC)
pelvis = Capsule("Pelvis", r, Jeans);
- torso = Capsule("Torso", r, jacket);
- belt = Capsule("Belt", r, Dark);
+ torso = Textured(Capsule("Torso", r, Color.white), plaid, new Vector2(2f, 3f));
+ belly = Textured(Sphere("Belly", r, Color.white, 1f), plaid, new Vector2(2f, 2f));
+ belt = Capsule("Belt", r, new Color(0.2f, 0.13f, 0.08f));
+ buckle = Box("Buckle", r, Metal);
+ placket = Box("Buttons", r, jacketDark);
+ collar = Capsule("Collar", r, jacketDark);
neck = Capsule("Neck", r, Skin);
for (int i = 0; i < 2; i++)
{
thigh[i] = Capsule("Thigh", r, Jeans);
- shin[i] = Capsule("Shin", r, Jeans);
+ shin[i] = Capsule("Shin", r, Jeans * 0.92f);
boot[i] = Box("Boot", r, Boots);
- upperArm[i] = Capsule("UpperArm", r, jacket);
- forearm[i] = Capsule("Forearm", r, jacketDark);
+ sole[i] = Box("Sole", r, Sole);
+ upperArm[i] = Textured(Capsule("UpperArm", r, Color.white), plaid, new Vector2(1f, 2f));
+ forearm[i] = Textured(Capsule("Forearm", r, Color.white), plaid, new Vector2(1f, 2f));
+ cuff[i] = Capsule("Cuff", r, jacketDark);
hand[i] = Sphere("Hand", r, Skin, 0.1f);
}
@@ -101,31 +112,96 @@ namespace MSCO
headPivot = new GameObject("Head").transform;
headPivot.SetParent(r, false);
Transform skull = Sphere("Skull", headPivot, Skin, 0.24f);
- skull.localScale = new Vector3(0.22f, 0.26f, 0.24f);
- Transform hair = Sphere("Hair", headPivot, new Color(0.25f, 0.17f, 0.1f), 0.24f);
- hair.localPosition = new Vector3(0f, 0.02f, -0.025f);
- hair.localScale = new Vector3(0.23f, 0.25f, 0.23f);
- Transform cap = Sphere("Cap", headPivot, jacketDark, 0.25f);
- cap.localPosition = new Vector3(0f, 0.07f, -0.005f);
- cap.localScale = new Vector3(0.245f, 0.16f, 0.255f);
- Transform brim = Box("CapBrim", headPivot, jacketDark);
- brim.localPosition = new Vector3(0f, 0.075f, 0.13f);
- brim.localScale = new Vector3(0.2f, 0.02f, 0.11f);
- brim.localRotation = Quaternion.Euler(-8f, 0f, 0f);
+ skull.localScale = new Vector3(0.21f, 0.255f, 0.235f);
+ Transform jaw = Sphere("Jaw", headPivot, Skin, 0.2f);
+ jaw.localPosition = new Vector3(0f, -0.065f, 0.02f);
+ jaw.localScale = new Vector3(0.18f, 0.13f, 0.19f);
+ Transform hair = Sphere("Hair", headPivot, hairColor, 0.24f);
+ hair.localPosition = new Vector3(0f, 0.025f, -0.03f);
+ hair.localScale = new Vector3(0.22f, 0.24f, 0.22f);
for (int i = 0; i < 2; i++)
{
- Transform eye = Sphere("Eye", headPivot, Dark, 0.035f);
- eye.localPosition = new Vector3(i == 0 ? -0.045f : 0.045f, 0.015f, 0.105f);
+ float side = i == 0 ? -1f : 1f;
+ Transform ear = Sphere("Ear", headPivot, Skin * 0.96f, 0.05f);
+ ear.localPosition = new Vector3(side * 0.105f, -0.005f, 0f);
+ ear.localScale = new Vector3(0.035f, 0.065f, 0.05f);
+ Transform eye = Sphere("Eye", headPivot, Dark, 0.028f);
+ eye.localPosition = new Vector3(side * 0.042f, 0.018f, 0.105f);
+ Transform brow = Box("Brow", headPivot, hairColor);
+ brow.localPosition = new Vector3(side * 0.043f, 0.045f, 0.108f);
+ brow.localScale = new Vector3(0.045f, 0.012f, 0.012f);
+ brow.localRotation = Quaternion.Euler(0f, 0f, side * -8f);
+ }
+ Transform nose = Box("Nose", headPivot, Skin * 0.93f);
+ nose.localPosition = new Vector3(0f, -0.012f, 0.12f);
+ nose.localScale = new Vector3(0.035f, 0.055f, 0.045f);
+ // Der typische Schnurrbart
+ Transform mustache = Box("Mustache", headPivot, hairColor);
+ mustache.localPosition = new Vector3(0f, -0.052f, 0.113f);
+ mustache.localScale = new Vector3(0.085f, 0.024f, 0.03f);
+ Transform mouth = Box("Mouth", headPivot, new Color(0.45f, 0.25f, 0.22f));
+ mouth.localPosition = new Vector3(0f, -0.078f, 0.108f);
+ mouth.localScale = new Vector3(0.05f, 0.01f, 0.01f);
+
+ if (sunglasses)
+ {
+ Transform glasses = Box("Sunglasses", headPivot, Dark);
+ glasses.localPosition = new Vector3(0f, 0.018f, 0.113f);
+ glasses.localScale = new Vector3(0.14f, 0.035f, 0.012f);
+ }
+ if (wearsCap)
+ {
+ // Kappe ("lippis") in der Jackenfarbe
+ Transform cap = Sphere("Cap", headPivot, jacketDark, 0.25f);
+ cap.localPosition = new Vector3(0f, 0.075f, -0.005f);
+ cap.localScale = new Vector3(0.235f, 0.15f, 0.245f);
+ Transform brim = Box("CapBrim", headPivot, jacketDark);
+ brim.localPosition = new Vector3(0f, 0.078f, 0.13f);
+ brim.localScale = new Vector3(0.19f, 0.018f, 0.11f);
+ brim.localRotation = Quaternion.Euler(-10f, 0f, 0f);
+ Transform patch = Box("CapPatch", headPivot, new Color(0.9f, 0.88f, 0.8f));
+ patch.localPosition = new Vector3(0f, 0.105f, 0.112f);
+ patch.localScale = new Vector3(0.06f, 0.035f, 0.01f);
+ patch.localRotation = Quaternion.Euler(-25f, 0f, 0f);
}
- Transform nose = Box("Nose", headPivot, Skin * 0.95f);
- nose.localPosition = new Vector3(0f, -0.02f, 0.12f);
- nose.localScale = new Vector3(0.035f, 0.05f, 0.04f);
// Unsichtbarer "Blick" mit der exakten Kameradrehung - daran hängt das gehaltene Item
view = new GameObject("View").transform;
view.SetParent(r, false);
}
+ /// Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.
+ static Texture2D Plaid(Color baseColor)
+ {
+ const int size = 16;
+ var tex = new Texture2D(size, size, TextureFormat.RGB24, false);
+ Color light = Color.Lerp(baseColor, Color.white, 0.18f);
+ Color dark = baseColor * 0.62f;
+ for (int y = 0; y < size; y++)
+ {
+ for (int x = 0; x < size; x++)
+ {
+ bool bandX = (x / 4) % 2 == 0, bandY = (y / 4) % 2 == 0;
+ Color c = bandX && bandY ? dark : bandX || bandY ? baseColor : light;
+ if (x % 8 == 1 || y % 8 == 1) c = Color.Lerp(c, Color.black, 0.45f); // dünne dunkle Linien
+ c.a = 1f;
+ tex.SetPixel(x, y, c);
+ }
+ }
+ tex.filterMode = FilterMode.Point;
+ tex.wrapMode = TextureWrapMode.Repeat;
+ tex.Apply();
+ return tex;
+ }
+
+ static Transform Textured(Transform part, Texture2D tex, Vector2 tiling)
+ {
+ Material m = part.GetComponent().material;
+ m.mainTexture = tex;
+ m.mainTextureScale = tiling;
+ return part;
+ }
+
static Transform Capsule(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Capsule, name, parent, color); }
static Transform Box(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Cube, name, parent, color); }
@@ -186,18 +262,7 @@ namespace MSCO
view.rotation = Quaternion.Slerp(view.rotation, targetViewRot, t);
UpdateWalk(dt);
- if (model != null) model.Animate(speed); // Spiel-Animation (Idle/Walk) läuft vor LateUpdate
- else Pose(dt);
- }
-
- /// Spielmodell: nach der Animation Ducken/Lehnen/Kopf/Arme anpassen.
- void LateUpdate()
- {
- if (model == null) return;
- Vector3 heldPos;
- Quaternion heldRot;
- bool holding = TryGetHeldPose(out heldPos, out heldRot);
- model.Pose(transform, headWorld, pitch, last.Seated, holding, heldPos);
+ Pose(dt);
}
void UpdateWalk(float dt)
@@ -240,10 +305,21 @@ namespace MSCO
Vector3 forward = Vector3.Cross(right, up).normalized;
// Rumpf
- Segment(torso, hip + up * 0.08f, neckBase, 0.38f, 0.24f);
- Segment(belt, hip + up * 0.06f, hip + up * 0.1f, 0.37f, 0.23f);
- Segment(pelvis, hip - right * HipWidth, hip + right * HipWidth, 0.2f, 0.22f);
- Segment(neck, neckBase, neckTop, 0.09f, 0.09f);
+ Quaternion torsoRot = Quaternion.LookRotation(forward, up);
+ Segment(torso, hip + up * 0.08f, neckBase, 0.4f, 0.25f);
+ belly.localPosition = hip + up * 0.2f + forward * 0.06f;
+ belly.localRotation = torsoRot;
+ belly.localScale = new Vector3(0.34f, 0.28f, 0.27f);
+ Segment(belt, hip + up * 0.05f, hip + up * 0.1f, 0.38f, 0.26f);
+ buckle.localPosition = hip + up * 0.075f + forward * 0.13f;
+ buckle.localRotation = torsoRot;
+ buckle.localScale = new Vector3(0.06f, 0.045f, 0.015f);
+ placket.localPosition = Vector3.Lerp(hip + up * 0.1f, neckBase, 0.62f) + forward * 0.122f;
+ placket.localRotation = torsoRot;
+ placket.localScale = new Vector3(0.025f, 0.26f, 0.01f);
+ Segment(pelvis, hip - right * HipWidth, hip + right * HipWidth, 0.22f, 0.24f);
+ Segment(collar, neckBase - up * 0.02f, neckBase + up * 0.035f, 0.17f, 0.15f);
+ Segment(neck, neckBase, neckTop, 0.1f, 0.1f);
// Kopf
headPivot.localPosition = head;
@@ -265,9 +341,12 @@ namespace MSCO
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
Segment(thigh[i], hipJoint, knee, 0.17f, 0.17f);
Segment(shin[i], knee, ankle, 0.14f, 0.14f);
- boot[i].localPosition = ankle + new Vector3(0f, -0.02f, 0.05f);
+ boot[i].localPosition = ankle + new Vector3(0f, -0.015f, 0.05f);
boot[i].localRotation = Quaternion.identity;
boot[i].localScale = new Vector3(0.12f, 0.1f, 0.27f);
+ sole[i].localPosition = ankle + new Vector3(0f, -0.065f, 0.05f);
+ sole[i].localRotation = Quaternion.identity;
+ sole[i].localScale = new Vector3(0.13f, 0.025f, 0.29f);
// Arme
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side);
@@ -289,7 +368,10 @@ namespace MSCO
Vector3 elbow = Joint(shoulder, handTarget, UpperArmLength, ForearmLength, new Vector3(side * 0.4f, -0.2f, -1f));
Segment(upperArm[i], shoulder, elbow, 0.12f, 0.12f);
Segment(forearm[i], elbow, handTarget, 0.11f, 0.11f);
+ Vector3 wrist = Vector3.Lerp(elbow, handTarget, 0.86f);
+ Segment(cuff[i], wrist, Vector3.Lerp(elbow, handTarget, 0.94f), 0.12f, 0.12f);
hand[i].localPosition = handTarget;
+ hand[i].localScale = new Vector3(0.085f, 0.1f, 0.1f);
}
}