using System; using System.Collections.Generic; using System.Text; using UnityEngine; namespace MSCO { /// /// Mitspieler-Figur aus einem echten My-Summer-Car-NPC (Körper-Mesh "bodymesh" + Skelett mit "pelvis"). /// Der NPC wird kopiert und von seiner Logik befreit; es bleiben Aussehen und die Spiel-Animationen /// (Idle/Walk). Nach der Animation werden die Knochen für Ducken, Lehnen, Kopf-Nicken, Sitzen und /// das Greifen zum Item verbogen (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; } Animation anim; string walkClip, idleClip; Transform pelvis, spine, head; 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]; /// /// Knochen, die wir verbiegen. Bewegt die Animation einen davon nicht selbst, würden sich unsere /// Änderungen jedes Frame aufsummieren - dann wird er vorher auf die Grundhaltung zurückgesetzt. /// class BoneState { public Transform Bone; public Vector3 RestPos, OutPos; public Quaternion RestRot, OutRot; public bool HasOut; } 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 }; if (!c.FindBones()) { UnityEngine.Object.Destroy(clone); return null; } c.Normalize(); c.SetupAnimation(); c.RememberRestPose(); 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 Aussehen und Animation entfernen (KI-FSMs, Physik, Sounds, Trigger ...). 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)) { if (b is Animation || b is Animator) continue; UnityEngine.Object.DestroyImmediate(b); } foreach (AudioSource a in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(a); } 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); } void SetupAnimation() { anim = Root.GetComponentInChildren(); if (anim == null) return; foreach (AnimationState s in anim) { if (IsGesture(s.name)) continue; string n = s.name.ToLower(); if (walkClip == null && n.Contains("walk")) walkClip = s.name; if (idleClip == null && n.Contains("idle")) idleClip = s.name; } // Keine Steh-Animation: im Stand die Laufanimation angehalten zeigen (statt einer Geste) if (idleClip == null && walkClip == null && anim.clip != null && !IsGesture(anim.clip.name)) idleClip = anim.clip.name; if (walkClip != null) anim[walkClip].wrapMode = WrapMode.Loop; if (idleClip != null) anim[idleClip].wrapMode = WrapMode.Loop; if (idleClip != null) anim.Play(idleClip); } void RememberRestPose() { var bones = new List { pelvis, spine, head }; bones.AddRange(thigh); bones.AddRange(knee); bones.AddRange(upperArm); bones.AddRange(forearm); foreach (Transform b in bones) if (b != null) posed.Add(new BoneState { Bone = b, RestPos = b.localPosition, RestRot = b.localRotation }); } void ResetUnanimatedBones() { foreach (BoneState s in posed) { if (s.HasOut && s.Bone.localPosition == s.OutPos && s.Bone.localRotation == s.OutRot) { s.Bone.localPosition = s.RestPos; s.Bone.localRotation = s.RestRot; } } } void RememberOutput() { foreach (BoneState s in posed) { s.OutPos = s.Bone.localPosition; s.OutRot = s.Bone.localRotation; s.HasOut = true; } } 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(", anim idle=").Append(idleClip ?? "-").Append(" walk=").Append(walkClip ?? "-"); return sb.ToString(); } // ---------------------------------------------------------------- Pro Frame (LateUpdate, nach der Animation) public void Animate(float speed) { if (anim == null) return; bool walking = speed > 0.4f && walkClip != null; string clip = walking ? walkClip : (idleClip ?? walkClip); if (clip == null) return; if (!anim.IsPlaying(clip)) anim.CrossFade(clip, 0.25f); if (clip == walkClip) anim[walkClip].speed = walking ? Mathf.Clamp(speed / 1.4f, 0.6f, 2.2f) : 0f; // angehalten = stehen } 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; ResetUnanimatedBones(); 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); RememberOutput(); return; // nächstes Frame mit neuer Größe neu auf den Boden stellen } // Fußpositionen der Animation merken (die sollen stehen bleiben) for (int i = 0; i < 2; i++) if (foot[i] != null) animatedFoot[i] = foot[i].position; // 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) for (int i = 0; i < 2; i++) { if (upperArm[i] == null || forearm[i] == null || hand[i] == null) continue; 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], upperArm[i].position + forward * 0.38f - up * 0.28f, -up); } RememberOutput(); } /// 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); } } }