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);
}
}
}