Files
MSCO/GameCharacter.cs
T
MrMahoandClaude Opus 5.5 cd8c1293e0 Other players as real MSC characters
GameCharacter clones a MSC human (bodymesh + skeleton, no scripts, physics or
animation), puts it into its bind pose, aligns and scales it, and drives the
bones every frame from the joint points of the existing MSCO figure (hip,
knee, ankle, shoulder, elbow, hand, head pitch, torso lean) by rotating each
limb from its rest direction. Limb bones are picked geometrically, so bone
naming conventions do not matter. Variant per player id; console 'model
classic|game' switches, the MSCO figure is the automatic fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:36:50 +02:00

312 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// 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.
/// </summary>
public class GameCharacter
{
/// <summary>Gelenkpunkte der MSCO-Figur im lokalen Raum des Spielers (jedes Frame von RemotePlayer.Pose).</summary>
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<SkinnedMeshRenderer> templates;
static float nextTemplateSearch;
/// <summary>Passende Menschen im Spiel (sortiert nach Pfad: bei allen Spielern gleiche Reihenfolge).</summary>
static List<SkinnedMeshRenderer> Templates()
{
if (templates != null && templates.Count > 0 && templates.TrueForAll(t => t != null)) return templates;
if (Time.realtimeSinceStartup < nextTemplateSearch) return templates ?? new List<SkinnedMeshRenderer>();
nextTemplateSearch = Time.realtimeSinceStartup + 10f;
var found = new List<KeyValuePair<string, SkinnedMeshRenderer>>();
var seenMeshes = new HashSet<Mesh>();
foreach (SkinnedMeshRenderer smr in Resources.FindObjectsOfTypeAll<SkinnedMeshRenderer>())
{
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<string, SkinnedMeshRenderer>(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<string>();
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;
}
/// <summary>Kleinster gemeinsamer Vorfahre von Mesh und Skelett - das wird kopiert.</summary>
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;
}
/// <summary>
/// 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).
/// </summary>
static bool SafeToClone(Transform root)
{
if (!root.gameObject.activeInHierarchy) return true;
return root.GetComponentsInChildren<MonoBehaviour>(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<Bone> spine = new List<Bone>();
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; }
/// <summary>Figur aus einer Spielvorlage bauen (variant wählt den Menschen). null, wenn keine passt.</summary>
public static GameCharacter Create(Transform owner, int variant)
{
List<SkinnedMeshRenderer> 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<SkinnedMeshRenderer>(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<string, Transform>();
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<int>();
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<Transform> { 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<Renderer>(true)) r.enabled = true;
clone.SetActive(true);
return true;
}
/// <summary>
/// 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.
/// </summary>
static Transform[] Limb(Transform end, HashSet<Transform> trunk)
{
var chain = new List<Transform>();
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;
}
/// <summary>Alles außer Aussehen entfernen (Skripte, Physik, Kollision, Ton, Animation).</summary>
static void Strip(GameObject go)
{
foreach (Joint j in go.GetComponentsInChildren<Joint>(true)) UnityEngine.Object.DestroyImmediate(j);
foreach (Component c in go.GetComponentsInChildren<Component>(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;
}
}
}