diff --git a/GameCharacter.cs b/GameCharacter.cs index 282abd8..37f7a42 100644 --- a/GameCharacter.cs +++ b/GameCharacter.cs @@ -6,10 +6,11 @@ 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). + /// 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). /// public class GameCharacter { @@ -23,23 +24,19 @@ namespace MSCO public Transform Root { get; private set; } public string Description { get; private set; } - Animation anim; - string walkClip, idleClip; 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]; - /// - /// 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. - /// + /// Grundhaltung aller Knochen (Bind-Pose); jedes Frame wird von hier aus neu gebogen. class BoneState { public Transform Bone; - public Vector3 RestPos, OutPos; - public Quaternion RestRot, OutRot; - public bool HasOut; + public Vector3 RestPos; + public Quaternion RestRot; } readonly List posed = new List(); @@ -66,14 +63,16 @@ namespace MSCO 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.SetupAnimation(); c.RememberRestPose(); + c.bindInfo = bound > 0 ? "bind pose " + bound + " bones" : "no bind pose"; c.Description = template.name + " (" + candidates.Count + " templates) - " + c.BoneReport(); return c; } @@ -168,18 +167,62 @@ namespace MSCO return (walk ? 2 : 0) + (idle ? 1 : 0); } - /// Alles außer Aussehen und Animation entfernen (KI-FSMs, Physik, Sounds, Trigger ...). + /// + /// Alles außer dem Aussehen entfernen: KI-FSMs, Physik (Ragdoll-Gelenke), Sounds, Trigger und auch die + /// Spiel-Animationen - die Figur wird komplett selbst bewegt. + /// 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 (Behaviour b in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(b); + foreach (Animation a in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(a); foreach (AudioSource a in go.GetComponentsInChildren(true)) UnityEngine.Object.DestroyImmediate(a); + // Figur nicht ausblenden, nur weil ihr Ursprung gerade außerhalb des Bildes liegt + foreach (SkinnedMeshRenderer smr in go.GetComponentsInChildren(true)) + smr.updateWhenOffscreen = true; + } + + /// + /// 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. + /// + int ApplyBindPose() + { + SkinnedMeshRenderer body = null; + foreach (SkinnedMeshRenderer smr in Root.GetComponentsInChildren(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(); + 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() @@ -232,55 +275,22 @@ namespace MSCO 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); - } - + /// Grundhaltung aller Knochen merken (nicht Root selbst - den stellt Pose auf den Boden). 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) + foreach (Transform b in Root.GetComponentsInChildren(true)) + if (b != Root) posed.Add(new BoneState { Bone = b, RestPos = b.localPosition, RestRot = b.localRotation }); } - void ResetUnanimatedBones() + /// Jedes Frame zurück zur Grundhaltung, damit sich nichts aufsummiert. + void ResetToRest() { - foreach (BoneState s in posed) + foreach (BoneState st 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; + if (st.Bone == null) continue; + st.Bone.localPosition = st.RestPos; + st.Bone.localRotation = st.RestRot; } } @@ -289,26 +299,27 @@ namespace MSCO 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 ?? "-"); + sb.Append(", ").Append(bindInfo).Append(", procedural animation"); return sb.ToString(); } // ---------------------------------------------------------------- Pro Frame (LateUpdate, nach der Animation) + /// Eigener Laufzyklus: Tempo aus der Geschwindigkeit, weiches Ein-/Ausblenden beim Anhalten. 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 + 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; - ResetUnanimatedBones(); + ResetToRest(); frames++; // 1) Auf den Boden stellen: die (animierten) Füße stehen auf der Bodenhöhe des Spielers. @@ -329,13 +340,19 @@ namespace MSCO 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) + // 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) animatedFoot[i] = foot[i].position; + { + 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; @@ -361,16 +378,25 @@ namespace MSCO SolveTwoBone(thigh[i], knee[i], foot[i], target, forward); } - // Arme: rechts zum gehaltenen Item, im Auto beide nach vorne (Lenkrad) + // 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], upperArm[i].position + forward * 0.38f - up * 0.28f, -up); + 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); + } } - RememberOutput(); } /// Knochen so drehen, dass "child" in Richtung "target" zeigt - egal wie das Skelett ausgerichtet ist. diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 4122ece..7d8e0c5 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -1223,7 +1223,11 @@ namespace MSCO if (MSCO.PassengerKey != null && MSCO.PassengerKey.GetKeybindDown()) { string result = passenger.Toggle(localPlayer, playerCamera, objects, players); - if (result != null) SetStatus(result); + if (result != null) + { + SetStatus(result); + overlays.AddChat("", result, true); // im Spiel sichtbar, nicht nur in der Konsole + } } bool ready = IsPeerReady; diff --git a/Passenger.cs b/Passenger.cs index e82d232..0616242 100644 --- a/Passenger.cs +++ b/Passenger.cs @@ -11,7 +11,7 @@ namespace MSCO /// public class Passenger { - const float EnterRange = 4f; // so nah muss man am Fahrzeug stehen + const float EnterRange = 5f; // so nah muss man am Fahrzeug stehen (Abstand zur Fahrzeugmitte) const float CenterSeatThreshold = 0.2f; const float BehindCenterSeat = 0.7f; const float ExitSideOffset = 1.4f; // beim Aussteigen so weit neben den Sitz @@ -40,14 +40,22 @@ namespace MSCO SyncedBody best = null; RemotePlayer driver = null; float bestDist = EnterRange * EnterRange; - foreach (SyncedBody b in objects.RemoteDrivenVehicles()) + List driven = objects.RemoteDrivenVehicles(); + float nearest = float.MaxValue; + foreach (SyncedBody b in driven) { + float d = (b.Rb.worldCenterOfMass - player.position).sqrMagnitude; + nearest = Mathf.Min(nearest, d); PlayerInfo owner; if (!players.TryGetValue(b.OwnerId, out owner) || owner.Avatar == null) continue; - float d = (b.Rb.worldCenterOfMass - player.position).sqrMagnitude; if (d < bestDist) { bestDist = d; best = b; driver = owner.Avatar; } } - if (best == null) return "No vehicle driven by another player nearby"; + if (best == null) + { + // Genau sagen, woran es liegt + if (driven.Count == 0) return "Passenger: nobody is driving a vehicle right now (the driver must sit in the driver's seat)"; + return "Passenger: get closer to the car (" + Mathf.Sqrt(nearest).ToString("0") + " m away, max " + EnterRange.ToString("0") + " m)"; + } Transform vt = best.Rb.transform; Vector3 driverHead = vt.InverseTransformPoint(driver.HeadPosition);