Files
MSCO/RemotePlayer.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

508 lines
26 KiB
C#

using UnityEngine;
namespace MSCO
{
/// <summary>
/// Figur eines Mitspielers im Stil der My-Summer-Car-Leute (selbst gebaut, keine NPC-Kopie):
/// Karohemd/Arbeitsjacke, Jeans, Arbeitsschuhe, Schnurrbart, Kappe oder Haare, leichter Bauch.
/// Aus Kopfposition (Kamera) und Bodenhöhe wird jedes Frame die Pose berechnet: Ducken beugt die Knie,
/// nach vorne Lehnen neigt den Oberkörper, Laufen bewegt Beine und Arme, beim Halten greift der rechte Arm
/// zum Item, im Auto sitzt die Figur.
/// </summary>
public class RemotePlayer : MonoBehaviour
{
const float Smoothing = 15f;
const float StandingEyeHeight = 1.65f;
const float MaxHeadPitch = 55f;
// Körpermaße (Meter)
const float ThighLength = 0.41f, ShinLength = 0.41f; // im Stehen fast gestreckt
const float UpperArmLength = 0.3f, ForearmLength = 0.29f;
const float TorsoLength = 0.55f; // Hüfte bis Hals
const float HipWidth = 0.1f, ShoulderWidth = 0.21f;
const float AnkleHeight = 0.07f;
const float LegReach = ThighLength + ShinLength + AnkleHeight - 0.02f; // Hüfthöhe bei gestrecktem Bein
static readonly Color Skin = new Color(0.86f, 0.66f, 0.52f);
static readonly Color Jeans = new Color(0.2f, 0.26f, 0.38f);
static readonly Color Boots = new Color(0.22f, 0.15f, 0.09f);
static readonly Color Sole = new Color(0.07f, 0.06f, 0.05f);
static readonly Color Dark = new Color(0.08f, 0.08f, 0.08f);
static readonly Color Metal = new Color(0.62f, 0.62f, 0.6f);
static readonly Color[] HairColors =
{
new Color(0.23f, 0.15f, 0.09f), new Color(0.45f, 0.33f, 0.2f), new Color(0.12f, 0.1f, 0.08f), new Color(0.55f, 0.5f, 0.45f)
};
Color jacket;
int variant;
Transform headPivot, view;
Transform pelvis, torso, belt, neck, belly, collar, placket, buckle;
readonly Transform[] thigh = new Transform[2], shin = new Transform[2], boot = new Transform[2];
readonly Transform[] upperArm = new Transform[2], forearm = new Transform[2], hand = new Transform[2];
readonly Transform[] sole = new Transform[2], cuff = new Transform[2], shoulderBall = new Transform[2];
Vector3 targetFeet;
Vector3 targetHead;
Vector3 headWorld;
float targetYaw, targetPitch, pitch;
Quaternion targetViewRot = Quaternion.identity;
PlayerState last;
// Fahrzeug, in dem die Figur sitzt (setzt der Manager jedes Frame; null = läuft frei)
public Transform Seat;
Transform lastSeat;
Vector3 seatHead;
Quaternion seatView = Quaternion.identity;
Vector3 lastPosition;
float walkPhase, walkAmount, speed;
float airAmount; // 0 = steht am Boden, 1 = in der Luft (Sprung, Fallen)
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 model != null ? model.Description : "MSCO figure, variant " + variant; } }
/// <summary>Mitspieler als echte MSC-Menschen zeigen (Konsole "model game|classic", gespeichert).</summary>
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
public static RemotePlayer Create(string name, Color jacketColor, PlayerState state, int variant)
{
var root = new GameObject(name);
var rp = root.AddComponent<RemotePlayer>();
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);
rp.headWorld = rp.targetHead;
rp.pitch = rp.targetPitch;
rp.view.rotation = rp.targetViewRot;
rp.lastPosition = root.transform.position;
rp.Pose(0f);
return rp;
}
void Build()
{
Transform r = transform;
Color jacketDark = jacket * 0.7f;
jacketDark.a = 1f;
Color hairColor = HairColors[variant % HairColors.Length];
bool wearsCap = variant % 3 != 2;
bool sunglasses = variant % 4 == 1;
Texture2D plaid = Plaid(jacket);
// Körper: Karohemd/Jacke, Jeans, Gürtel, leichter Bauch (wie die Leute in MSC)
pelvis = Capsule("Pelvis", r, Jeans);
torso = Textured(Capsule("Chest", r, Color.white), plaid, new Vector2(4f, 4f));
belly = Textured(Capsule("Belly", r, Color.white), plaid, new Vector2(4f, 2f));
belt = Primitive(PrimitiveType.Cylinder, "Belt", r, new Color(0.2f, 0.13f, 0.08f)); // rund um den Bauch
buckle = Box("Buckle", r, Metal);
placket = Box("Buttons", r, jacketDark);
collar = Capsule("Collar", r, jacketDark);
neck = Capsule("Neck", r, Skin);
for (int i = 0; i < 2; i++)
{
thigh[i] = Capsule("Thigh", r, Jeans);
shin[i] = Capsule("Shin", r, Jeans * 0.92f);
boot[i] = Capsule("Boot", r, Boots); // abgerundete Arbeitsstiefel statt Klötze
sole[i] = Capsule("Sole", r, Sole);
upperArm[i] = Textured(Capsule("UpperArm", r, Color.white), plaid, new Vector2(2f, 3f));
forearm[i] = Textured(Capsule("Forearm", r, Color.white), plaid, new Vector2(2f, 3f));
cuff[i] = Capsule("Cuff", r, jacketDark);
shoulderBall[i] = Textured(Sphere("Shoulder", r, Color.white, 0.13f), plaid, new Vector2(2f, 2f)); // runde Schulter
hand[i] = Sphere("Hand", r, Skin, 0.1f);
}
// Kopf: dreht mit Blick (hoch/runter), Gesicht nach +Z
headPivot = new GameObject("Head").transform;
headPivot.SetParent(r, false);
Transform skull = Sphere("Skull", headPivot, Skin, 0.24f);
skull.localScale = new Vector3(0.225f, 0.27f, 0.25f);
Transform jaw = Sphere("Jaw", headPivot, Skin, 0.2f);
jaw.localPosition = new Vector3(0f, -0.07f, 0.022f);
jaw.localScale = new Vector3(0.19f, 0.14f, 0.2f);
Transform hair = Sphere("Hair", headPivot, hairColor, 0.24f);
hair.localPosition = new Vector3(0f, 0.028f, -0.032f);
hair.localScale = new Vector3(0.235f, 0.255f, 0.235f);
for (int i = 0; i < 2; i++)
{
float side = i == 0 ? -1f : 1f;
Transform ear = Sphere("Ear", headPivot, Skin * 0.96f, 0.05f);
ear.localPosition = new Vector3(side * 0.112f, -0.005f, 0f);
ear.localScale = new Vector3(0.035f, 0.065f, 0.05f);
Transform eye = Sphere("Eye", headPivot, Dark, 0.026f);
eye.localPosition = new Vector3(side * 0.045f, 0.018f, 0.112f);
Transform brow = Box("Brow", headPivot, hairColor);
brow.localPosition = new Vector3(side * 0.046f, 0.046f, 0.117f);
brow.localScale = new Vector3(0.045f, 0.012f, 0.012f);
brow.localRotation = Quaternion.Euler(0f, 0f, side * -8f);
}
Transform nose = Box("Nose", headPivot, Skin * 0.93f);
nose.localPosition = new Vector3(0f, -0.012f, 0.128f);
nose.localScale = new Vector3(0.035f, 0.055f, 0.045f);
// Der typische Schnurrbart
Transform mustache = Box("Mustache", headPivot, hairColor);
mustache.localPosition = new Vector3(0f, -0.054f, 0.12f);
mustache.localScale = new Vector3(0.085f, 0.024f, 0.03f);
Transform mouth = Box("Mouth", headPivot, new Color(0.45f, 0.25f, 0.22f));
mouth.localPosition = new Vector3(0f, -0.082f, 0.114f);
mouth.localScale = new Vector3(0.05f, 0.01f, 0.01f);
if (sunglasses)
{
Transform glasses = Box("Sunglasses", headPivot, Dark);
glasses.localPosition = new Vector3(0f, 0.018f, 0.121f);
glasses.localScale = new Vector3(0.15f, 0.035f, 0.012f);
}
if (wearsCap)
{
// Kappe ("lippis") in der Jackenfarbe
Transform cap = Sphere("Cap", headPivot, jacketDark, 0.25f);
cap.localPosition = new Vector3(0f, 0.08f, -0.005f);
cap.localScale = new Vector3(0.25f, 0.16f, 0.26f);
Transform brim = Box("CapBrim", headPivot, jacketDark);
brim.localPosition = new Vector3(0f, 0.07f, 0.15f);
brim.localScale = new Vector3(0.2f, 0.014f, 0.1f);
brim.localRotation = Quaternion.Euler(8f, 0f, 0f); // Schirm leicht nach unten
Color patchColor = Color.Lerp(jacketDark, Color.white, 0.35f);
Transform patch = Box("CapPatch", headPivot, patchColor);
patch.localPosition = new Vector3(0f, 0.115f, 0.118f);
patch.localScale = new Vector3(0.06f, 0.035f, 0.01f);
patch.localRotation = Quaternion.Euler(-25f, 0f, 0f);
}
// Unsichtbarer "Blick" mit der exakten Kameradrehung - daran hängt das gehaltene Item
view = new GameObject("View").transform;
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<Renderer>(true))
if (modelRoot == null || !r.transform.IsChildOf(modelRoot)) r.enabled = false;
}
/// <summary>Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.</summary>
static Texture2D Plaid(Color baseColor)
{
const int size = 16;
var tex = new Texture2D(size, size, TextureFormat.RGB24, false);
// Weiches Flanell-Karo: wenig Kontrast, dünne Linien - wirkt aus der Entfernung wie Stoff statt Schachbrett
Color light = Color.Lerp(baseColor, Color.white, 0.1f);
Color dark = baseColor * 0.78f;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
bool bandX = (x / 4) % 2 == 0, bandY = (y / 4) % 2 == 0;
Color c = bandX && bandY ? dark : bandX || bandY ? baseColor : light;
if (x % 8 == 1 || y % 8 == 1) c = Color.Lerp(c, Color.black, 0.25f); // dünne dunkle Linien
c.a = 1f;
tex.SetPixel(x, y, c);
}
}
tex.filterMode = FilterMode.Bilinear;
tex.wrapMode = TextureWrapMode.Repeat;
tex.Apply();
return tex;
}
static Transform Textured(Transform part, Texture2D tex, Vector2 tiling)
{
Material m = part.GetComponent<Renderer>().material;
m.mainTexture = tex;
m.mainTextureScale = tiling;
return part;
}
static Transform Capsule(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Capsule, name, parent, color); }
static Transform Box(string name, Transform parent, Color color) { return Primitive(PrimitiveType.Cube, name, parent, color); }
static Transform Sphere(string name, Transform parent, Color color, float size)
{
Transform t = Primitive(PrimitiveType.Sphere, name, parent, color);
t.localScale = Vector3.one * size;
return t;
}
static Transform Primitive(PrimitiveType type, string name, Transform parent, Color color)
{
GameObject go = GameObject.CreatePrimitive(type);
go.name = name;
Destroy(go.GetComponent<Collider>()); // stört sonst Physik/Trigger
var renderer = go.GetComponent<Renderer>();
renderer.material.color = color;
// Matt wie Stoff/Haut statt glänzendem Plastik (Standard-Shader)
if (renderer.material.HasProperty("_Glossiness")) renderer.material.SetFloat("_Glossiness", 0.08f);
if (renderer.material.HasProperty("_Metallic")) renderer.material.SetFloat("_Metallic", 0f);
renderer.receiveShadows = true;
go.transform.SetParent(parent, false);
return go.transform;
}
// ---------------------------------------------------------------- Zustand
public void SetTarget(PlayerState state)
{
last = state;
targetHead = state.CamPos;
// Füße: Bodenhöhe vom Spieler, waagerecht an der Spielerposition (Lehnen = Kopf woanders als Füße).
// Liegt der Kopf unplausibel weit weg (andere Kamera), Füße unter den Kopf.
float feetY = state.FeetY != 0f ? state.FeetY : state.CamPos.y - StandingEyeHeight;
var feet = new Vector3(state.Position.x, feetY, state.Position.z);
var horizontal = new Vector3(state.CamPos.x - feet.x, 0f, state.CamPos.z - feet.z);
if (horizontal.sqrMagnitude > 0.8f * 0.8f || state.Seated) feet = new Vector3(state.CamPos.x, feetY, state.CamPos.z);
targetFeet = feet;
// Blickrichtung ohne seitliches Kippen
Vector3 forward = state.CamRot * Vector3.forward;
Vector3 flat = new Vector3(forward.x, 0f, forward.z);
targetYaw = flat.sqrMagnitude > 0.0001f ? Quaternion.LookRotation(flat).eulerAngles.y : state.RotationY;
targetPitch = Mathf.Clamp(-Mathf.Asin(Mathf.Clamp(forward.y, -1f, 1f)) * Mathf.Rad2Deg, -MaxHeadPitch, MaxHeadPitch);
targetViewRot = state.CamRot;
}
public void Tick(float dt)
{
if (Seat != null && last.SeatId != 0)
{
TickSeated(dt);
UpdateWalk(dt);
Pose(dt);
return;
}
lastSeat = null;
// Bei großen Sprüngen (Teleport, Laden) direkt setzen statt gleiten
bool snap = (transform.position - targetFeet).sqrMagnitude > 100f;
float t = snap ? 1f : Mathf.Clamp01(dt * Smoothing);
transform.position = Vector3.Lerp(transform.position, targetFeet, t);
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(0f, targetYaw, 0f), t);
headWorld = snap ? targetHead : Vector3.Lerp(headWorld, targetHead, t);
pitch = Mathf.Lerp(pitch, targetPitch, t);
view.position = headWorld;
view.rotation = Quaternion.Slerp(view.rotation, targetViewRot, t);
UpdateWalk(dt);
Pose(dt);
}
/// <summary>
/// Im Fahrzeug: relativ zum Fahrzeug glätten und jedes Frame an dessen aktuelle Lage setzen. In Weltkoordinaten
/// hinkt die Figur einem fahrenden Auto um Tempo / Glättung hinterher (bei 90 km/h fast 2 m) und ruckelt.
/// </summary>
void TickSeated(float dt)
{
bool fresh = lastSeat != Seat; // gerade eingestiegen: direkt auf den Sitz
lastSeat = Seat;
float t = fresh ? 1f : Mathf.Clamp01(dt * Smoothing);
seatHead = Vector3.Lerp(seatHead, last.SeatHead, t);
seatView = Quaternion.Slerp(seatView, last.SeatView, t);
Vector3 up = Seat.up;
Quaternion viewRot = Seat.rotation * seatView;
Vector3 forward = viewRot * Vector3.forward;
Vector3 flat = forward - up * Vector3.Dot(forward, up);
if (flat.sqrMagnitude < 0.0001f) flat = Seat.forward;
headWorld = Seat.TransformPoint(seatHead);
transform.rotation = Quaternion.LookRotation(flat, up); // neigt sich mit dem Auto (Berg, Kurve)
transform.position = headWorld - up * 1.2f;
pitch = Mathf.Clamp(-Mathf.Asin(Mathf.Clamp(Vector3.Dot(forward.normalized, up), -1f, 1f)) * Mathf.Rad2Deg, -MaxHeadPitch, MaxHeadPitch);
view.position = headWorld;
view.rotation = viewRot;
}
void UpdateWalk(float dt)
{
Vector3 moved = transform.position - lastPosition;
lastPosition = transform.position;
moved.y = 0f;
speed = dt > 0f ? moved.magnitude / dt : 0f;
if (speed > 12f || last.Seated) speed = 0f; // Teleport oder im Auto
walkAmount = Mathf.Lerp(walkAmount, Mathf.Clamp01(speed / 2.5f), Mathf.Clamp01(dt * 8f));
walkPhase += speed * dt * 3.4f;
}
// ---------------------------------------------------------------- Pose (alles im lokalen Raum der Figur)
void Pose(float dt)
{
Vector3 head = transform.InverseTransformPoint(headWorld) + new Vector3(0f, -0.02f, -0.03f);
Vector3 neckTop = head + new Vector3(0f, -0.12f, -0.01f);
Vector3 neckBase = head + new Vector3(0f, -0.2f, -0.02f);
// Hüfte: beim Lehnen nach hinten ausweichen, beim Ducken absinken (Füße bleiben am Boden)
Vector3 hip;
float air = 0f;
if (last.Seated)
{
hip = neckBase + new Vector3(0f, -TorsoLength, -0.08f);
}
else
{
var lean = new Vector3(neckBase.x, 0f, neckBase.z);
hip = -lean * 0.25f;
float horizontal = (new Vector3(neckBase.x - hip.x, 0f, neckBase.z - hip.z)).magnitude;
float vertical = Mathf.Sqrt(Mathf.Max(0.04f, TorsoLength * TorsoLength - horizontal * horizontal));
// Ist der Kopf höher, als die Beine reichen (Sprung, Fallen), hebt die ganze Figur ab - vorher blieben die
// Füße am Boden und nur der Oberkörper ging nach oben
hip.y = Mathf.Max(0.3f, neckBase.y - vertical);
air = Mathf.Max(0f, hip.y - LegReach);
}
airAmount = Mathf.Lerp(airAmount, last.Seated ? 0f : Mathf.Clamp01(air / 0.12f), dt > 0f ? Mathf.Clamp01(dt * 10f) : 1f);
Vector3 up = (neckBase - hip).normalized;
Vector3 right = Vector3.right;
Vector3 forward = Vector3.Cross(right, up).normalized;
// Rumpf
Quaternion torsoRot = Quaternion.LookRotation(forward, up);
// Brust (breit oben) und Bauch (etwas tiefer = leichter Bauch) - fließend statt Klotz mit Kugel
Segment(torso, hip + up * 0.26f, neckBase - up * 0.04f, 0.37f, 0.22f);
Segment(belly, hip + up * 0.07f + forward * 0.015f, hip + up * 0.3f + forward * 0.015f, 0.33f, 0.25f);
belt.localPosition = hip + up * 0.06f;
belt.localRotation = torsoRot;
belt.localScale = new Vector3(0.34f, 0.024f, 0.27f); // Zylinder: 0.048 m hoch, elliptisch
buckle.localPosition = hip + up * 0.06f + forward * 0.136f;
buckle.localRotation = torsoRot;
buckle.localScale = new Vector3(0.055f, 0.04f, 0.012f);
placket.localPosition = Vector3.Lerp(hip + up * 0.12f, neckBase, 0.6f) + forward * 0.124f;
placket.localRotation = torsoRot;
placket.localScale = new Vector3(0.022f, 0.3f, 0.01f);
Segment(pelvis, hip - right * (HipWidth * 0.8f) - up * 0.02f, hip + right * (HipWidth * 0.8f) - up * 0.02f, 0.2f, 0.22f);
Segment(collar, neckBase - up * 0.02f, neckBase + up * 0.035f, 0.17f, 0.15f);
Segment(neck, neckBase, neckTop, 0.1f, 0.1f);
// 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++)
{
float side = i == 0 ? -1f : 1f;
float phase = i == 0 ? swing : -swing;
// Beine
Vector3 hipJoint = hip + right * (HipWidth * side);
Vector3 ankle;
if (last.Seated)
ankle = hipJoint + new Vector3(0f, -0.4f, 0.48f);
else
{
ankle = new Vector3(HipWidth * side, AnkleHeight + Mathf.Max(0f, Mathf.Cos(walkPhase + i * Mathf.PI)) * 0.07f * walkAmount, phase * 0.3f);
// In der Luft: Füße unter der Hüfte mitnehmen und die Knie etwas anziehen
ankle.y += air + 0.1f * airAmount;
ankle.z -= 0.06f * airAmount;
}
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);
Segment(sole[i], ankle + new Vector3(0f, -0.07f, -0.075f), ankle + new Vector3(0f, -0.07f, 0.155f), 0.125f, 0.03f);
// Arme
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side);
shoulderBall[i].localPosition = shoulder + up * 0.005f;
shoulderBall[i].localRotation = torsoRot;
shoulderBall[i].localScale = new Vector3(0.125f, 0.12f, 0.13f);
Vector3 handTarget;
Vector3 heldPos;
Quaternion heldRot;
if (i == 1 && TryGetHeldPose(out heldPos, out heldRot))
handTarget = transform.InverseTransformPoint(heldPos);
else if (last.Seated)
handTarget = shoulder + new Vector3(-side * 0.05f, -0.3f, 0.38f); // Hände nach vorne (Lenkrad)
else
handTarget = shoulder + new Vector3(side * (0.06f + 0.12f * airAmount), -(UpperArmLength + ForearmLength) + 0.05f + 0.14f * airAmount, 0.02f - phase * 0.22f);
// Nicht weiter als die Armlänge
Vector3 reach = handTarget - shoulder;
float maxReach = UpperArmLength + ForearmLength - 0.01f;
if (reach.magnitude > maxReach) handTarget = shoulder + reach.normalized * maxReach;
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);
}
/// <summary>Zwei-Knochen-Gelenk (Knie/Ellbogen): Punkt zwischen a und b, zur Seite "bend" geknickt.</summary>
static Vector3 Joint(Vector3 a, Vector3 b, float lengthA, float lengthB, Vector3 bend)
{
Vector3 ab = b - a;
float d = Mathf.Clamp(ab.magnitude, 0.01f, lengthA + lengthB - 0.001f);
Vector3 dir = ab.normalized;
// Abstand vom Startpunkt entlang ab und seitlicher Abstand (Kosinussatz)
float along = (lengthA * lengthA - lengthB * lengthB + d * d) / (2f * d);
float sideways = Mathf.Sqrt(Mathf.Max(0f, lengthA * lengthA - along * along));
Vector3 bendDir = bend - dir * Vector3.Dot(bend, dir);
if (bendDir.sqrMagnitude < 0.0001f) bendDir = Vector3.forward - dir * Vector3.Dot(Vector3.forward, dir);
return a + dir * along + bendDir.normalized * sideways;
}
/// <summary>Kapsel von a nach b spannen (lokaler Raum der Figur).</summary>
static void Segment(Transform part, Vector3 a, Vector3 b, float width, float depth)
{
Vector3 dir = b - a;
float length = dir.magnitude;
part.localPosition = (a + b) * 0.5f;
part.localRotation = length > 0.0001f ? Quaternion.FromToRotation(Vector3.up, dir / length) : Quaternion.identity;
part.localScale = new Vector3(width, length * 0.5f + Mathf.Min(width, depth) * 0.5f, depth);
}
/// <summary>Wo das gehaltene Item beim Spieler gerade ist (relativ zum Blick).</summary>
public bool TryGetHeldPose(out Vector3 pos, out Quaternion rot)
{
pos = Vector3.zero;
rot = Quaternion.identity;
if (last.HeldId == 0) return false;
pos = view.TransformPoint(last.HeldLocalPos);
rot = view.rotation * last.HeldLocalRot;
return true;
}
}
}