Own MSC-style player figure instead of cloned NPCs; stop objects flung away by physics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 23:28:43 +02:00
co-authored by Claude Opus 5.5
parent 24cd485bcc
commit e050712c0d
6 changed files with 172 additions and 496 deletions
-431
View File
@@ -1,431 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// 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).
/// </summary>
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];
/// <summary>Grundhaltung aller Knochen (Bind-Pose); jedes Frame wird von hier aus neu gebogen.</summary>
class BoneState
{
public Transform Bone;
public Vector3 RestPos;
public Quaternion RestRot;
}
readonly List<BoneState> posed = new List<BoneState>();
public Vector3 HeadPosition { get { return head.position + Root.up * EyeAboveHeadBone; } }
// ---------------------------------------------------------------- Vorlage finden und kopieren
static List<GameObject> templates;
/// <summary>Kopiert einen NPC aus der Spielszene als Figur. null, wenn keine passende Vorlage da ist.</summary>
public static GameCharacter TryCreate(Transform parent, int variant)
{
List<GameObject> 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;
}
/// <summary>NPC-Körper in der Szene (auch inaktive): SkinnedMeshRenderer "bodymesh" mit "pelvis"-Knochen.</summary>
static List<GameObject> FindTemplates()
{
if (templates != null)
{
templates.RemoveAll(t => t == null);
if (templates.Count > 0) return templates;
}
var scored = new List<KeyValuePair<GameObject, int>>();
var seen = new HashSet<string>();
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<Transform>(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<GameObject, int>(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<GameObject>();
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<Transform>(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;
}
/// <summary>
/// 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.
/// </summary>
static int AnimationScore(ref GameObject root)
{
Animation[] found = root.GetComponentsInChildren<Animation>(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<Animation>();
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);
}
/// <summary>
/// Alles außer dem Aussehen entfernen: KI-FSMs, Physik (Ragdoll-Gelenke), Sounds, Trigger und auch die
/// Spiel-Animationen - die Figur wird komplett selbst bewegt.
/// </summary>
static void Strip(GameObject go)
{
foreach (Joint j in go.GetComponentsInChildren<Joint>(true)) UnityEngine.Object.DestroyImmediate(j);
foreach (Rigidbody r in go.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(r);
foreach (Collider c in go.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
foreach (Behaviour b in go.GetComponentsInChildren<Behaviour>(true)) UnityEngine.Object.DestroyImmediate(b);
foreach (Animation a in go.GetComponentsInChildren<Animation>(true)) UnityEngine.Object.DestroyImmediate(a);
foreach (AudioSource a in go.GetComponentsInChildren<AudioSource>(true)) UnityEngine.Object.DestroyImmediate(a);
// Figur nicht ausblenden, nur weil ihr Ursprung gerade außerhalb des Bildes liegt
foreach (SkinnedMeshRenderer smr in go.GetComponentsInChildren<SkinnedMeshRenderer>(true))
smr.updateWhenOffscreen = true;
}
/// <summary>
/// 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.
/// </summary>
int ApplyBindPose()
{
SkinnedMeshRenderer body = null;
foreach (SkinnedMeshRenderer smr in Root.GetComponentsInChildren<SkinnedMeshRenderer>(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<int>();
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<Transform>(true))
if (string.Equals(t.name, name, StringComparison.OrdinalIgnoreCase)) return t;
return null;
}
/// <summary>Größe auf Spielergröße bringen, Füße auf den Boden, Blickrichtung nach +Z.</summary>
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);
}
/// <summary>Grundhaltung aller Knochen merken (nicht Root selbst - den stellt Pose auf den Boden).</summary>
void RememberRestPose()
{
foreach (Transform b in Root.GetComponentsInChildren<Transform>(true))
if (b != Root)
posed.Add(new BoneState { Bone = b, RestPos = b.localPosition, RestRot = b.localRotation });
}
/// <summary>Jedes Frame zurück zur Grundhaltung, damit sich nichts aufsummiert.</summary>
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)
/// <summary>Eigener Laufzyklus: Tempo aus der Geschwindigkeit, weiches Ein-/Ausblenden beim Anhalten.</summary>
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);
}
}
}
/// <summary>Knochen so drehen, dass "child" in Richtung "target" zeigt - egal wie das Skelett ausgerichtet ist.</summary>
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);
}
}
}
-1
View File
@@ -86,7 +86,6 @@
<Compile Include="DiscordPresence.cs" /> <Compile Include="DiscordPresence.cs" />
<Compile Include="DoorSync.cs" /> <Compile Include="DoorSync.cs" />
<Compile Include="FsmSync.cs" /> <Compile Include="FsmSync.cs" />
<Compile Include="GameCharacter.cs" />
<Compile Include="JoinLinks.cs" /> <Compile Include="JoinLinks.cs" />
<Compile Include="LobbyUI.cs" /> <Compile Include="LobbyUI.cs" />
<Compile Include="MenuHelper.cs" /> <Compile Include="MenuHelper.cs" />
+6 -2
View File
@@ -1435,8 +1435,12 @@ namespace MSCO
static Color PlayerColor(int id) static Color PlayerColor(int id)
{ {
Color[] colors = { Color.cyan, new Color(1f, 0.55f, 0.1f), Color.green, Color.magenta, // Gedeckte Farben wie Flanellhemden/Arbeitsjacken in MSC
Color.yellow, new Color(0.4f, 0.6f, 1f), Color.red, Color.white }; 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]; return colors[id % colors.Length];
} }
+21
View File
@@ -63,6 +63,7 @@ namespace MSCO
const float ReleaseAfterStill = 1f; const float ReleaseAfterStill = 1f;
const float SnapDistance = 10f; const float SnapDistance = 10f;
const float RemoteTimeout = 3f; // Besitzer meldet sich nicht mehr (z.B. Teil wurde dort montiert) 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 HandoverCooldown = 3f; // nach Freigabe/Übergabe keine Übernahme durch Bewegung
const float MaxHoldDistance = 1.5f; // gehaltene Items sind so nah an der Kamera 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) 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; if (!b.Rb.gameObject.activeInHierarchy) continue;
bool active = b == held || b == driven; bool active = b == held || b == driven;
if (!IsRemoteOwned(b)) StopRunaway(b);
if (b.OwnerId == SyncedBody.NoOwner) if (b.OwnerId == SyncedBody.NoOwner)
{ {
if (active) Claim(b, true); if (active) Claim(b, true);
@@ -461,6 +463,25 @@ namespace MSCO
} }
} }
/// <summary>
/// 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.
/// </summary>
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");
}
/// <summary>Position innerhalb der Karte, Drehung gültig (gleiche Regel wie auf dem Server).</summary> /// <summary>Position innerhalb der Karte, Drehung gültig (gleiche Regel wie auf dem Server).</summary>
static bool IsValidPose(Vector3 p, Quaternion q) static bool IsValidPose(Vector3 p, Quaternion q)
{ {
+3 -2
View File
@@ -20,7 +20,8 @@ Multiplayer-Mod für **My Summer Car** (MSCLoader). Spiel mit bis zu 3 Spielern
und nach dem Spielen wiederhergestellt und nach dem Spielen wiederhergestellt
**Synchronisiert** **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 - 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) - Fahrzeuge: Position, Lichter, Motorgeräusch; als **Beifahrer** mitfahren (Taste P)
- **Fahrzeugzustand:** Tank, Öl, Kühlwasser, Bremsflüssigkeit, Batterie, Schrauben-Festigkeit, Schäden, Verschleiß, - **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 | | `ObjectSync.cs` | Items/Teile/Fahrzeuge mit Besitzer-System |
| `VehicleStateSync.cs`, `VehicleFx.cs`, `Passenger.cs` | Fahrzeugzustand, Lichter/Sound, Beifahrer | | `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 | | `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ü | | `SaveSync.cs`, `MenuHelper.cs` | Spielstand übertragen/sichern, Laden aus dem Menü |
| `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Discord | | `SteamLobby.cs`, `JoinLinks.cs`, `DiscordPresence.cs` | Steam-Lobbys, `msco://`-Links, Discord |
| `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche | | `SafetyPatches.cs`, `InputLock.cs`, `SceneCache.cs` | Absturzschutz, Eingabesperre, geteilte FSM-Suche |
+142 -60
View File
@@ -3,10 +3,11 @@ using UnityEngine;
namespace MSCO namespace MSCO
{ {
/// <summary> /// <summary>
/// Figur eines Mitspielers mit echter Körperhaltung. Aus Kopfposition (Kamera) und Bodenhöhe wird /// Figur eines Mitspielers im Stil der My-Summer-Car-Leute (selbst gebaut, keine NPC-Kopie):
/// jedes Frame die Pose berechnet: Ducken beugt die Knie, nach vorne Lehnen neigt den Oberkörper, /// Karohemd/Arbeitsjacke, Jeans, Arbeitsschuhe, Schnurrbart, Kappe oder Haare, leichter Bauch.
/// Laufen bewegt Beine und Arme, beim Halten greift der rechte Arm (mit Ellbogen) zum Item, im Auto /// Aus Kopfposition (Kamera) und Bodenhöhe wird jedes Frame die Pose berechnet: Ducken beugt die Knie,
/// sitzt die Figur. Die Körperteile sind abgerundete Kapseln zwischen den Gelenken. /// 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.
/// </summary> /// </summary>
public class RemotePlayer : MonoBehaviour public class RemotePlayer : MonoBehaviour
{ {
@@ -21,16 +22,24 @@ namespace MSCO
const float HipWidth = 0.1f, ShoulderWidth = 0.21f; const float HipWidth = 0.1f, ShoulderWidth = 0.21f;
const float AnkleHeight = 0.07f; const float AnkleHeight = 0.07f;
static readonly Color Skin = new Color(0.93f, 0.76f, 0.62f); static readonly Color Skin = new Color(0.86f, 0.66f, 0.52f);
static readonly Color Jeans = new Color(0.18f, 0.25f, 0.42f); static readonly Color Jeans = new Color(0.2f, 0.26f, 0.38f);
static readonly Color Boots = new Color(0.24f, 0.16f, 0.1f); 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 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; Color jacket;
int variant;
Transform headPivot, view; 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[] 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[] 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 targetFeet;
Vector3 targetHead; Vector3 targetHead;
@@ -42,13 +51,10 @@ namespace MSCO
Vector3 lastPosition; Vector3 lastPosition;
float walkPhase, walkAmount, speed; 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 int HeldId { get { return last.HeldId; } }
public PlayerState LastState { get { return last; } } public PlayerState LastState { get { return last; } }
public Vector3 HeadPosition { get { return model != null ? model.HeadPosition : headPivot.position; } } public Vector3 HeadPosition { get { return headPivot.position; } }
public string ModelDescription { get { return model != null ? "game model: " + model.Description : "fallback figure (no NPC template found)"; } } public string ModelDescription { get { return "MSCO figure, variant " + variant; } }
// ---------------------------------------------------------------- Aufbau // ---------------------------------------------------------------- Aufbau
@@ -57,43 +63,48 @@ namespace MSCO
var root = new GameObject(name); var root = new GameObject(name);
var rp = root.AddComponent<RemotePlayer>(); var rp = root.AddComponent<RemotePlayer>();
rp.jacket = jacketColor; rp.jacket = jacketColor;
rp.variant = Mathf.Abs(variant);
rp.Build(); rp.Build();
rp.SetTarget(state); rp.SetTarget(state);
root.transform.position = rp.targetFeet; root.transform.position = rp.targetFeet;
root.transform.rotation = Quaternion.Euler(0f, rp.targetYaw, 0f); 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<Renderer>(true))
if (!r.transform.IsChildOf(rp.model.Root)) r.enabled = false;
rp.headWorld = rp.targetHead; rp.headWorld = rp.targetHead;
rp.pitch = rp.targetPitch; rp.pitch = rp.targetPitch;
rp.view.rotation = rp.targetViewRot; rp.view.rotation = rp.targetViewRot;
rp.lastPosition = root.transform.position; rp.lastPosition = root.transform.position;
if (rp.model == null) rp.Pose(0f); rp.Pose(0f);
return rp; return rp;
} }
void Build() void Build()
{ {
Transform r = transform; Transform r = transform;
Color jacketDark = jacket * 0.75f; Color jacketDark = jacket * 0.7f;
jacketDark.a = 1f; 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); pelvis = Capsule("Pelvis", r, Jeans);
torso = Capsule("Torso", r, jacket); torso = Textured(Capsule("Torso", r, Color.white), plaid, new Vector2(2f, 3f));
belt = Capsule("Belt", r, Dark); 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); neck = Capsule("Neck", r, Skin);
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
thigh[i] = Capsule("Thigh", r, Jeans); 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); boot[i] = Box("Boot", r, Boots);
upperArm[i] = Capsule("UpperArm", r, jacket); sole[i] = Box("Sole", r, Sole);
forearm[i] = Capsule("Forearm", r, jacketDark); 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); hand[i] = Sphere("Hand", r, Skin, 0.1f);
} }
@@ -101,31 +112,96 @@ namespace MSCO
headPivot = new GameObject("Head").transform; headPivot = new GameObject("Head").transform;
headPivot.SetParent(r, false); headPivot.SetParent(r, false);
Transform skull = Sphere("Skull", headPivot, Skin, 0.24f); Transform skull = Sphere("Skull", headPivot, Skin, 0.24f);
skull.localScale = new Vector3(0.22f, 0.26f, 0.24f); skull.localScale = new Vector3(0.21f, 0.255f, 0.235f);
Transform hair = Sphere("Hair", headPivot, new Color(0.25f, 0.17f, 0.1f), 0.24f); Transform jaw = Sphere("Jaw", headPivot, Skin, 0.2f);
hair.localPosition = new Vector3(0f, 0.02f, -0.025f); jaw.localPosition = new Vector3(0f, -0.065f, 0.02f);
hair.localScale = new Vector3(0.23f, 0.25f, 0.23f); jaw.localScale = new Vector3(0.18f, 0.13f, 0.19f);
Transform cap = Sphere("Cap", headPivot, jacketDark, 0.25f); Transform hair = Sphere("Hair", headPivot, hairColor, 0.24f);
cap.localPosition = new Vector3(0f, 0.07f, -0.005f); hair.localPosition = new Vector3(0f, 0.025f, -0.03f);
cap.localScale = new Vector3(0.245f, 0.16f, 0.255f); hair.localScale = new Vector3(0.22f, 0.24f, 0.22f);
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);
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
Transform eye = Sphere("Eye", headPivot, Dark, 0.035f); float side = i == 0 ? -1f : 1f;
eye.localPosition = new Vector3(i == 0 ? -0.045f : 0.045f, 0.015f, 0.105f); 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 // Unsichtbarer "Blick" mit der exakten Kameradrehung - daran hängt das gehaltene Item
view = new GameObject("View").transform; view = new GameObject("View").transform;
view.SetParent(r, false); view.SetParent(r, false);
} }
/// <summary>Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.</summary>
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<Renderer>().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 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); } 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); view.rotation = Quaternion.Slerp(view.rotation, targetViewRot, t);
UpdateWalk(dt); UpdateWalk(dt);
if (model != null) model.Animate(speed); // Spiel-Animation (Idle/Walk) läuft vor LateUpdate Pose(dt);
else Pose(dt);
}
/// <summary>Spielmodell: nach der Animation Ducken/Lehnen/Kopf/Arme anpassen.</summary>
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);
} }
void UpdateWalk(float dt) void UpdateWalk(float dt)
@@ -240,10 +305,21 @@ namespace MSCO
Vector3 forward = Vector3.Cross(right, up).normalized; Vector3 forward = Vector3.Cross(right, up).normalized;
// Rumpf // Rumpf
Segment(torso, hip + up * 0.08f, neckBase, 0.38f, 0.24f); Quaternion torsoRot = Quaternion.LookRotation(forward, up);
Segment(belt, hip + up * 0.06f, hip + up * 0.1f, 0.37f, 0.23f); Segment(torso, hip + up * 0.08f, neckBase, 0.4f, 0.25f);
Segment(pelvis, hip - right * HipWidth, hip + right * HipWidth, 0.2f, 0.22f); belly.localPosition = hip + up * 0.2f + forward * 0.06f;
Segment(neck, neckBase, neckTop, 0.09f, 0.09f); 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 // Kopf
headPivot.localPosition = head; headPivot.localPosition = head;
@@ -265,9 +341,12 @@ namespace MSCO
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward); Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
Segment(thigh[i], hipJoint, knee, 0.17f, 0.17f); Segment(thigh[i], hipJoint, knee, 0.17f, 0.17f);
Segment(shin[i], knee, ankle, 0.14f, 0.14f); 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].localRotation = Quaternion.identity;
boot[i].localScale = new Vector3(0.12f, 0.1f, 0.27f); 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 // Arme
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side); 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)); 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(upperArm[i], shoulder, elbow, 0.12f, 0.12f);
Segment(forearm[i], elbow, handTarget, 0.11f, 0.11f); 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].localPosition = handTarget;
hand[i].localScale = new Vector3(0.085f, 0.1f, 0.1f);
} }
} }