using System;
using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
///
/// Echte Spielfigur für Mitspieler: eine Kopie eines MSC-Menschen (SkinnedMeshRenderer "bodymesh" mit Skelett
/// pelvis/knee_left/ankle_left/shoulder_left/arm_left/hand_left/head ...). Die Knochen werden jedes Frame aus der
/// Pose der MSCO-Figur gesetzt (Hüfte, Knie, Knöchel, Schulter, Ellbogen, Hand, Kopf) - laufen, springen, ducken,
/// sitzen und Items halten funktionieren dadurch genau wie vorher. Die Bewegung wird über Richtungen übertragen
/// (Ruhelage aus der Bind-Pose des Modells), dadurch ist egal, wie die Knochen im Modell ausgerichtet sind.
///
public class GameCharacter
{
/// Gelenkpunkte der MSCO-Figur im lokalen Raum des Spielers (jedes Frame von RemotePlayer.Pose).
public class Rig
{
public Vector3 Hip, NeckBase, Head;
public Vector3 Up = Vector3.up;
public float Pitch;
public readonly Vector3[] HipJoint = new Vector3[2], Knee = new Vector3[2], Ankle = new Vector3[2];
public readonly Vector3[] Shoulder = new Vector3[2], Elbow = new Vector3[2], Hand = new Vector3[2];
}
// ---------------------------------------------------------------- Vorlagen im Spiel finden
static readonly string[] RequiredBones =
{ "pelvis", "head", "knee_left", "knee_right", "ankle_left", "ankle_right", "hand_left", "hand_right" };
static List templates;
static float nextTemplateSearch;
/// Passende Menschen im Spiel (sortiert nach Pfad: bei allen Spielern gleiche Reihenfolge).
static List Templates()
{
if (templates != null && templates.Count > 0 && templates.TrueForAll(t => t != null)) return templates;
if (Time.realtimeSinceStartup < nextTemplateSearch) return templates ?? new List();
nextTemplateSearch = Time.realtimeSinceStartup + 10f;
var found = new List>();
var seenMeshes = new HashSet();
foreach (SkinnedMeshRenderer smr in Resources.FindObjectsOfTypeAll())
{
if (smr == null || smr.sharedMesh == null || !smr.name.StartsWith("bodymesh") || smr.name.Contains("kid")) continue;
if (smr.hideFlags != HideFlags.None || smr.bones == null || smr.bones.Length < 10) continue;
if (smr.sharedMesh.bindposes == null || smr.sharedMesh.bindposes.Length != smr.bones.Length) continue;
if (!HasBones(smr)) continue;
Transform root = CloneRoot(smr);
if (root == null || !SafeToClone(root)) continue;
if (!seenMeshes.Add(smr.sharedMesh)) continue; // gleiches Aussehen nur einmal
found.Add(new KeyValuePair(PathOf(smr.transform), smr));
}
found.Sort((a, b) => string.CompareOrdinal(a.Key, b.Key));
templates = found.ConvertAll(kv => kv.Value);
if (templates.Count > 0) MscoLog.Info("[MSCO] Player models: " + templates.Count + " MSC characters found");
return templates;
}
static bool HasBones(SkinnedMeshRenderer smr)
{
var names = new HashSet();
foreach (Transform b in smr.bones) if (b != null) names.Add(b.name);
foreach (string r in RequiredBones) if (!names.Contains(r)) return false;
return true;
}
/// Kleinster gemeinsamer Vorfahre von Mesh und Skelett - das wird kopiert.
static Transform CloneRoot(SkinnedMeshRenderer smr)
{
Transform bone = smr.rootBone != null ? smr.rootBone : smr.bones[0];
if (bone == null) return null;
for (Transform a = smr.transform; a != null; a = a.parent)
if (bone == a || bone.IsChildOf(a)) return a;
return null;
}
///
/// Kopieren darf keine Spiel-Logik starten: entweder hängen in dem Teilbaum keine Skripte (PlayMaker ...),
/// oder er ist ohnehin gerade inaktiv (dann wird er für die Kopie kurz ausgeschaltet, ohne Folgen).
///
static bool SafeToClone(Transform root)
{
if (!root.gameObject.activeInHierarchy) return true;
return root.GetComponentsInChildren(true).Length == 0;
}
static string PathOf(Transform t)
{
string path = t.name;
for (Transform p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
return path;
}
// ---------------------------------------------------------------- Instanz
class Bone
{
public Transform T;
public Quaternion RestRot; // relativ zum Spieler
public Vector3 RestPos; // relativ zum Spieler
public Vector3 RestDir; // zum Kind-Gelenk (Arme/Beine)
}
GameObject clone;
Transform owner;
Bone pelvis, head;
readonly List spine = new List();
readonly Bone[] upperLeg = new Bone[2], lowerLeg = new Bone[2], foot = new Bone[2];
readonly Bone[] upperArm = new Bone[2], lowerArm = new Bone[2];
Vector3 pelvisOffset;
public string Description { get; private set; }
/// Figur aus einer Spielvorlage bauen (variant wählt den Menschen). null, wenn keine passt.
public static GameCharacter Create(Transform owner, int variant)
{
List list = Templates();
if (list.Count == 0) return null;
var c = new GameCharacter();
try
{
if (c.Build(owner, list[Mathf.Abs(variant) % list.Count])) return c;
}
catch (Exception e)
{
MscoLog.Warning("[MSCO] Player model could not be built: " + e.Message);
}
c.Destroy();
return null;
}
bool Build(Transform owner, SkinnedMeshRenderer template)
{
this.owner = owner;
Transform tplRoot = CloneRoot(template);
GameObject tpl = tplRoot.gameObject;
// Kopieren, ohne dass Skripte der Vorlage anlaufen (siehe SafeToClone)
bool wasActive = tpl.activeSelf;
bool toggle = wasActive && !tpl.activeInHierarchy;
if (toggle) tpl.SetActive(false);
try { clone = (GameObject)UnityEngine.Object.Instantiate(tpl); }
finally { if (toggle) tpl.SetActive(true); }
clone.name = "MSCO_Model";
Strip(clone);
SkinnedMeshRenderer smr = null;
foreach (SkinnedMeshRenderer s in clone.GetComponentsInChildren(true))
if (s.sharedMesh == template.sharedMesh) { smr = s; break; }
if (smr == null) return false;
Description = "MSC character " + PathOf(template.transform);
// Knochen nach Namen
var byName = new Dictionary();
foreach (Transform b in smr.bones) if (b != null && !byName.ContainsKey(b.name)) byName[b.name] = b;
// In die Bind-Pose (Ruhelage des Modells) setzen: Eltern vor Kindern
Matrix4x4[] bind = smr.sharedMesh.bindposes;
var order = new List();
for (int i = 0; i < smr.bones.Length; i++) if (smr.bones[i] != null) order.Add(i);
order.Sort((a, b) => Depth(smr.bones[a]).CompareTo(Depth(smr.bones[b])));
foreach (int i in order)
{
Matrix4x4 m = smr.transform.localToWorldMatrix * bind[i].inverse;
smr.bones[i].position = m.GetColumn(3);
smr.bones[i].rotation = Quaternion.LookRotation(m.GetColumn(2), m.GetColumn(1));
}
// Gliedmaßen geometrisch bestimmen: in jeder Kette (Becken -> ... -> Knöchel -> Zehen bzw. Rumpf -> ... -> Hand
// -> Finger) sind die zwei längsten aufeinanderfolgenden Stücke Ober-/Unterschenkel bzw. Ober-/Unterarm.
// So ist egal, ob ein Knochen nach seinem Anfang oder seinem Ende benannt ist.
clone.transform.SetParent(owner, true);
Transform headT = byName["head"], pelvisT = byName["pelvis"];
var trunk = new HashSet { pelvisT };
for (Transform t = headT; t != null && t != pelvisT; t = t.parent) trunk.Add(t);
Transform[][] legs = { Limb(byName["ankle_left"], trunk), Limb(byName["ankle_right"], trunk) };
Transform[][] arms = { Limb(byName["hand_left"], trunk), Limb(byName["hand_right"], trunk) };
foreach (Transform[] l in legs) if (l == null) return false;
foreach (Transform[] l in arms) if (l == null) return false;
Vector3 up = (headT.position - pelvisT.position).normalized;
Vector3 right = (arms[1][0].position - arms[0][0].position).normalized;
Vector3 forward = Vector3.Cross(right, up).normalized;
if (forward.sqrMagnitude < 0.5f) return false;
Quaternion facing = Quaternion.LookRotation(forward, up);
float legLength = (legs[0][1].position - legs[0][0].position).magnitude + (legs[0][2].position - legs[0][1].position).magnitude;
if (legLength < 0.01f) return false;
float scale = 0.84f / legLength; // Bein der MSCO-Figur (Oberschenkel + Schienbein + etwas Knöchel)
// Erst drehen und skalieren, dann Ruhelage relativ zum Spieler merken
clone.transform.localScale = clone.transform.localScale * scale;
clone.transform.rotation = owner.rotation * Quaternion.Inverse(facing) * clone.transform.rotation;
clone.transform.position += owner.position - pelvisT.position;
pelvis = MakeBone(pelvisT, null);
head = MakeBone(headT, null);
for (Transform s = headT.parent; s != null && s != pelvisT; s = s.parent) spine.Add(MakeBone(s, null));
for (int i = 0; i < 2; i++)
{
upperLeg[i] = MakeBone(legs[i][0], legs[i][1]);
lowerLeg[i] = MakeBone(legs[i][1], legs[i][2]);
foot[i] = MakeBone(legs[i][2], null);
upperArm[i] = MakeBone(arms[i][0], arms[i][1]);
lowerArm[i] = MakeBone(arms[i][1], arms[i][2]);
}
Vector3 hipCenter = (upperLeg[0].RestPos + upperLeg[1].RestPos) * 0.5f;
pelvisOffset = pelvis.RestPos - hipCenter;
smr.updateWhenOffscreen = true; // Knochen bewegen sich weit weg von der gespeicherten Hülle
foreach (Renderer r in clone.GetComponentsInChildren(true)) r.enabled = true;
clone.SetActive(true);
return true;
}
///
/// Gliedmaße als [Anfang oberes Stück, Gelenk (Knie/Ellbogen), Ende (Knöchel/Handgelenk)]: Kette vom Rumpf bis zum
/// Ende (über end hinaus entlang des längsten Kindes), dann die zwei längsten aufeinanderfolgenden Stücke.
///
static Transform[] Limb(Transform end, HashSet trunk)
{
var chain = new List();
for (Transform t = end; t != null && !trunk.Contains(t); t = t.parent) chain.Insert(0, t);
for (Transform t = end; t.childCount > 0; )
{
Transform next = null;
float best = -1f;
foreach (Transform c in t)
{
float d = (c.position - t.position).sqrMagnitude;
if (d > best) { best = d; next = c; }
}
chain.Add(next);
t = next;
}
if (chain.Count < 3) return null;
int bestK = -1;
float bestLen = 0f;
for (int k = 0; k + 2 < chain.Count; k++)
{
float len = Mathf.Min((chain[k + 1].position - chain[k].position).magnitude, (chain[k + 2].position - chain[k + 1].position).magnitude);
if (len > bestLen) { bestLen = len; bestK = k; }
}
return bestK < 0 ? null : new[] { chain[bestK], chain[bestK + 1], chain[bestK + 2] };
}
static int Depth(Transform t)
{
int d = 0;
for (; t != null; t = t.parent) d++;
return d;
}
Bone MakeBone(Transform t, Transform child)
{
var b = new Bone
{
T = t,
RestRot = Quaternion.Inverse(owner.rotation) * t.rotation,
RestPos = owner.InverseTransformPoint(t.position)
};
if (child != null) b.RestDir = owner.InverseTransformPoint(child.position) - b.RestPos;
return b;
}
/// Alles außer Aussehen entfernen (Skripte, Physik, Kollision, Ton, Animation).
static void Strip(GameObject go)
{
foreach (Joint j in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(j);
foreach (Component c in go.GetComponentsInChildren(true))
{
if (c == null || c is Transform || c is Renderer || c is MeshFilter) continue;
UnityEngine.Object.DestroyImmediate(c);
}
}
// ---------------------------------------------------------------- jedes Frame
public void Apply(Rig rig)
{
if (clone == null) return;
Quaternion root = owner.rotation;
Quaternion lean = Quaternion.FromToRotation(Vector3.up, rig.Up);
pelvis.T.position = owner.TransformPoint(rig.Hip + lean * pelvisOffset);
pelvis.T.rotation = root * lean * pelvis.RestRot;
for (int i = spine.Count - 1; i >= 0; i--) spine[i].T.rotation = root * lean * spine[i].RestRot; // von unten nach oben
head.T.rotation = root * Quaternion.Euler(rig.Pitch, 0f, 0f) * head.RestRot;
for (int i = 0; i < 2; i++)
{
Aim(upperLeg[i], rig.Knee[i] - rig.HipJoint[i], root);
Aim(lowerLeg[i], rig.Ankle[i] - rig.Knee[i], root);
foot[i].T.rotation = root * foot[i].RestRot; // Füße bleiben flach
Aim(upperArm[i], rig.Elbow[i] - rig.Shoulder[i], root);
Aim(lowerArm[i], rig.Hand[i] - rig.Elbow[i], root);
}
}
static void Aim(Bone b, Vector3 desired, Quaternion root)
{
if (desired.sqrMagnitude < 1e-6f || b.RestDir.sqrMagnitude < 1e-6f) return;
b.T.rotation = root * Quaternion.FromToRotation(b.RestDir, desired) * b.RestRot;
}
public void Destroy()
{
if (clone != null) UnityEngine.Object.Destroy(clone);
clone = null;
}
}
}