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>
508 lines
26 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|