diff --git a/CHANGELOG.md b/CHANGELOG.md index c568dfe..9ff163e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ Web-Panel (Einstellungen), **OFFICIAL** vergeben nur die MSCO-Admins und wird hervorgehoben - Braucht keinen neuen Server: die Infos kommen vom Master (msco.mbm.wtf) und funktionieren mit 2.8-Servern +**Mitspieler als echte MSC-Menschen** +- Andere Spieler sehen aus wie die Leute im Spiel: MSCO nimmt eine Kopie eines MSC-Menschen (Körper, Gesicht, + Kleidung) und bewegt dessen Skelett mit der bisherigen Animation - laufen, springen, ducken, sitzen, Items halten +- Jeder Spieler bekommt (nach Spieler-Nummer) einen anderen Menschen; bei allen Mitspielern derselbe +- Konsole (F7): `model classic` zeigt wieder die MSCO-Figur, `model game` die MSC-Menschen; `rig` zeigt, wer welches + Modell hat. Findet MSCO keinen passenden Menschen, bleibt automatisch die MSCO-Figur + **Beifahrer** - **Automatisch einsteigen**: in die offene Beifahrertür an den Sitz treten - man setzt sich von selbst hin, keine Taste nötig. Aussteigen mit Leertaste oder der Beifahrer-Taste (P) diff --git a/Commands.cs b/Commands.cs index f7146ca..06fd638 100644 --- a/Commands.cs +++ b/Commands.cs @@ -21,6 +21,7 @@ namespace MSCO "fps [on|off] - show the frames per second (toggle)\n" + "/admin - admin menu (kick / ban / teleport / cheats)\n" + "vehicles | rig - debug info about vehicles / player models\n" + + "model [game|classic] - other players as MSC characters or the MSCO figure\n" + "vars - synced vehicle values, e.g. vars oil (debug)\n" + "debug - toggle sync debug output\n" + "clear - clear this console"; @@ -127,6 +128,13 @@ namespace MSCO case "rig": foreach (string line in mp.DescribeRigs()) print(line); break; + case "model": + // Mitspieler als echte MSC-Menschen ("game") oder als MSCO-Figur ("classic") + if (args.Length > 1) RemotePlayer.UseGameModels = args[1].ToLower() != "classic"; + else RemotePlayer.UseGameModels = !RemotePlayer.UseGameModels; + mp.RebuildAvatars(); + print("Player models: " + (RemotePlayer.UseGameModels ? "MSC characters (model classic = MSCO figure)" : "MSCO figure (model game = MSC characters)")); + break; case "vehicles": foreach (string line in mp.DescribeVehicles()) print(line); break; diff --git a/GameCharacter.cs b/GameCharacter.cs new file mode 100644 index 0000000..d1e74ad --- /dev/null +++ b/GameCharacter.cs @@ -0,0 +1,311 @@ +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; + } + } +} diff --git a/MSCO.csproj b/MSCO.csproj index 9d4481d..5ae1c37 100644 --- a/MSCO.csproj +++ b/MSCO.csproj @@ -106,6 +106,7 @@ + diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index a3b8c1c..40a0a2a 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -211,6 +211,12 @@ namespace MSCO return lines; } + /// Konsole "model": Figuren neu bauen (kommen mit dem nächsten Spielerzustand wieder). + public void RebuildAvatars() + { + foreach (PlayerInfo p in players.Values) DestroyAvatar(p); + } + /// Konsole "vars ": synchronisierte Fahrzeugwerte (Diagnose). public List DescribeVehicleValues(string filter) { return vehicleState.Describe(filter, 60); } @@ -2319,9 +2325,9 @@ namespace MSCO if (!players.TryGetValue(from, out p)) return; if (p.Avatar == null) { - // Platzhalter-Figur: Kapsel mit Kopf und Hand + // Echter MSC-Mensch (oder die eigene MSCO-Figur, wenn keiner passt / "model classic") p.Avatar = RemotePlayer.Create("MSCO_Player_" + from, PlayerColor(from), state, from); - MscoLog.Info("[MSCO] " + p.Name + " is in the game"); + MscoLog.Info("[MSCO] " + p.Name + " is in the game (" + p.Avatar.ModelDescription + ")"); } p.Avatar.SetTarget(state); } diff --git a/RemotePlayer.cs b/RemotePlayer.cs index 88f638a..59f14c2 100644 --- a/RemotePlayer.cs +++ b/RemotePlayer.cs @@ -62,7 +62,18 @@ namespace MSCO public int HeldId { get { return last.HeldId; } } public PlayerState LastState { get { return last; } } public Vector3 HeadPosition { get { return headPivot.position; } } - public string ModelDescription { get { return "MSCO figure, variant " + variant; } } + public string ModelDescription { get { return model != null ? model.Description : "MSCO figure, variant " + variant; } } + + /// Mitspieler als echte MSC-Menschen zeigen (Konsole "model game|classic", gespeichert). + public static bool UseGameModels + { + get { return PlayerPrefs.GetInt("MSCO_GameModels", 1) == 1; } + set { PlayerPrefs.SetInt("MSCO_GameModels", value ? 1 : 0); PlayerPrefs.Save(); } + } + + // Echte Spielfigur (null = eigene MSCO-Figur sichtbar); die MSCO-Figur liefert dann nur noch die Gelenkpunkte + GameCharacter model; + readonly GameCharacter.Rig rig = new GameCharacter.Rig(); // ---------------------------------------------------------------- Aufbau @@ -73,6 +84,7 @@ namespace MSCO rp.jacket = jacketColor; rp.variant = Mathf.Abs(variant); rp.Build(); + if (UseGameModels) rp.UseGameModel(); rp.SetTarget(state); root.transform.position = rp.targetFeet; root.transform.rotation = Quaternion.Euler(0f, rp.targetYaw, 0f); @@ -180,6 +192,16 @@ namespace MSCO view.SetParent(r, false); } + void UseGameModel() + { + model = GameCharacter.Create(transform, variant); + if (model == null) return; + // Eigene Figur unsichtbar (sie bewegt sich weiter und gibt die Gelenke vor) + Transform modelRoot = transform.Find("MSCO_Model"); + foreach (Renderer r in GetComponentsInChildren(true)) + if (modelRoot == null || !r.transform.IsChildOf(modelRoot)) r.enabled = false; + } + /// Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt. static Texture2D Plaid(Color baseColor) { @@ -379,6 +401,11 @@ namespace MSCO // Kopf headPivot.localPosition = head; headPivot.localRotation = Quaternion.Euler(pitch, 0f, 0f); + rig.Hip = hip; + rig.NeckBase = neckBase; + rig.Head = head; + rig.Up = up; + rig.Pitch = pitch; float swing = Mathf.Sin(walkPhase) * walkAmount * (1f - airAmount); for (int i = 0; i < 2; i++) @@ -400,6 +427,9 @@ namespace MSCO } Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward); Segment(thigh[i], hipJoint, knee, 0.15f, 0.16f); + rig.HipJoint[i] = hipJoint; + rig.Knee[i] = knee; + rig.Ankle[i] = ankle; Segment(shin[i], knee, ankle, 0.12f, 0.13f); // Stiefel: Kapsel von der Ferse zur Spitze, darunter eine flache Sohle Segment(boot[i], ankle + new Vector3(0f, -0.025f, -0.06f), ankle + new Vector3(0f, -0.03f, 0.14f), 0.12f, 0.1f); @@ -427,12 +457,16 @@ namespace MSCO Vector3 elbow = Joint(shoulder, handTarget, UpperArmLength, ForearmLength, new Vector3(side * 0.4f, -0.2f, -1f)); Segment(upperArm[i], shoulder, elbow, 0.11f, 0.11f); + rig.Shoulder[i] = shoulder; + rig.Elbow[i] = elbow; + rig.Hand[i] = handTarget; Segment(forearm[i], elbow, handTarget, 0.095f, 0.095f); Segment(cuff[i], Vector3.Lerp(elbow, handTarget, 0.82f), Vector3.Lerp(elbow, handTarget, 0.9f), 0.105f, 0.105f); hand[i].localPosition = handTarget; hand[i].localRotation = Quaternion.LookRotation(Vector3.forward, (handTarget - elbow).sqrMagnitude > 0f ? elbow - handTarget : Vector3.up); hand[i].localScale = new Vector3(0.085f, 0.125f, 0.11f); } + if (model != null) model.Apply(rig); } /// Zwei-Knochen-Gelenk (Knie/Ellbogen): Punkt zwischen a und b, zur Seite "bend" geknickt.