Player models: bind pose + procedural animation (no more ragdoll poses); passenger feedback in chat
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+102
-76
@@ -6,10 +6,11 @@ using UnityEngine;
|
||||
namespace MSCO
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <summary>Grundhaltung aller Knochen (Bind-Pose); jedes Frame wird von hier aus neu gebogen.</summary>
|
||||
class BoneState
|
||||
{
|
||||
public Transform Bone;
|
||||
public Vector3 RestPos, OutPos;
|
||||
public Quaternion RestRot, OutRot;
|
||||
public bool HasOut;
|
||||
public Vector3 RestPos;
|
||||
public Quaternion RestRot;
|
||||
}
|
||||
readonly List<BoneState> posed = new List<BoneState>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>Alles außer Aussehen und Animation entfernen (KI-FSMs, Physik, Sounds, Trigger ...).</summary>
|
||||
/// <summary>
|
||||
/// Alles außer dem Aussehen entfernen: KI-FSMs, Physik (Ragdoll-Gelenke), Sounds, Trigger und auch die
|
||||
/// Spiel-Animationen - die Figur wird komplett selbst bewegt.
|
||||
/// </summary>
|
||||
static void Strip(GameObject go)
|
||||
{
|
||||
foreach (Joint j in go.GetComponentsInChildren<Joint>(true)) UnityEngine.Object.DestroyImmediate(j);
|
||||
foreach (Rigidbody r in go.GetComponentsInChildren<Rigidbody>(true)) UnityEngine.Object.DestroyImmediate(r);
|
||||
foreach (Collider c in go.GetComponentsInChildren<Collider>(true)) UnityEngine.Object.DestroyImmediate(c);
|
||||
foreach (Behaviour b in go.GetComponentsInChildren<Behaviour>(true))
|
||||
{
|
||||
if (b is Animation || b is Animator) continue;
|
||||
UnityEngine.Object.DestroyImmediate(b);
|
||||
}
|
||||
foreach (Behaviour b in go.GetComponentsInChildren<Behaviour>(true)) UnityEngine.Object.DestroyImmediate(b);
|
||||
foreach (Animation a in go.GetComponentsInChildren<Animation>(true)) UnityEngine.Object.DestroyImmediate(a);
|
||||
foreach (AudioSource a in go.GetComponentsInChildren<AudioSource>(true)) UnityEngine.Object.DestroyImmediate(a);
|
||||
// Figur nicht ausblenden, nur weil ihr Ursprung gerade außerhalb des Bildes liegt
|
||||
foreach (SkinnedMeshRenderer smr in go.GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
smr.updateWhenOffscreen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
int ApplyBindPose()
|
||||
{
|
||||
SkinnedMeshRenderer body = null;
|
||||
foreach (SkinnedMeshRenderer smr in Root.GetComponentsInChildren<SkinnedMeshRenderer>(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<int>();
|
||||
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<Animation>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Grundhaltung aller Knochen merken (nicht Root selbst - den stellt Pose auf den Boden).</summary>
|
||||
void RememberRestPose()
|
||||
{
|
||||
var bones = new List<Transform> { 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<Transform>(true))
|
||||
if (b != Root)
|
||||
posed.Add(new BoneState { Bone = b, RestPos = b.localPosition, RestRot = b.localRotation });
|
||||
}
|
||||
|
||||
void ResetUnanimatedBones()
|
||||
/// <summary>Jedes Frame zurück zur Grundhaltung, damit sich nichts aufsummiert.</summary>
|
||||
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)
|
||||
|
||||
/// <summary>Eigener Laufzyklus: Tempo aus der Geschwindigkeit, weiches Ein-/Ausblenden beim Anhalten.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Knochen so drehen, dass "child" in Richtung "target" zeigt - egal wie das Skelett ausgerichtet ist.</summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+12
-4
@@ -11,7 +11,7 @@ namespace MSCO
|
||||
/// </summary>
|
||||
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<SyncedBody> 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);
|
||||
|
||||
Reference in New Issue
Block a user