using UnityEngine;
namespace MSCO
{
///
/// 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.
///
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.45f, ShinLength = 0.45f;
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;
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];
Vector3 targetFeet;
Vector3 targetHead;
Vector3 headWorld;
float targetYaw, targetPitch, pitch;
Quaternion targetViewRot = Quaternion.identity;
PlayerState last;
Vector3 lastPosition;
float walkPhase, walkAmount, speed;
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; } }
// ---------------------------------------------------------------- Aufbau
public static RemotePlayer Create(string name, Color jacketColor, PlayerState state, int variant)
{
var root = new GameObject(name);
var rp = root.AddComponent();
rp.jacket = jacketColor;
rp.variant = Mathf.Abs(variant);
rp.Build();
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("Torso", r, Color.white), plaid, new Vector2(2f, 3f));
belly = Textured(Sphere("Belly", r, Color.white, 1f), plaid, new Vector2(2f, 2f));
belt = Capsule("Belt", r, new Color(0.2f, 0.13f, 0.08f));
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] = Box("Boot", r, Boots);
sole[i] = Box("Sole", r, Sole);
upperArm[i] = Textured(Capsule("UpperArm", r, Color.white), plaid, new Vector2(1f, 2f));
forearm[i] = Textured(Capsule("Forearm", r, Color.white), plaid, new Vector2(1f, 2f));
cuff[i] = Capsule("Cuff", r, jacketDark);
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.21f, 0.255f, 0.235f);
Transform jaw = Sphere("Jaw", headPivot, Skin, 0.2f);
jaw.localPosition = new Vector3(0f, -0.065f, 0.02f);
jaw.localScale = new Vector3(0.18f, 0.13f, 0.19f);
Transform hair = Sphere("Hair", headPivot, hairColor, 0.24f);
hair.localPosition = new Vector3(0f, 0.025f, -0.03f);
hair.localScale = new Vector3(0.22f, 0.24f, 0.22f);
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.105f, -0.005f, 0f);
ear.localScale = new Vector3(0.035f, 0.065f, 0.05f);
Transform eye = Sphere("Eye", headPivot, Dark, 0.028f);
eye.localPosition = new Vector3(side * 0.042f, 0.018f, 0.105f);
Transform brow = Box("Brow", headPivot, hairColor);
brow.localPosition = new Vector3(side * 0.043f, 0.045f, 0.108f);
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.12f);
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.052f, 0.113f);
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.078f, 0.108f);
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.113f);
glasses.localScale = new Vector3(0.14f, 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.075f, -0.005f);
cap.localScale = new Vector3(0.235f, 0.15f, 0.245f);
Transform brim = Box("CapBrim", headPivot, jacketDark);
brim.localPosition = new Vector3(0f, 0.078f, 0.13f);
brim.localScale = new Vector3(0.19f, 0.018f, 0.11f);
brim.localRotation = Quaternion.Euler(-10f, 0f, 0f);
Transform patch = Box("CapPatch", headPivot, new Color(0.9f, 0.88f, 0.8f));
patch.localPosition = new Vector3(0f, 0.105f, 0.112f);
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);
}
/// Kleines Karomuster (Flanellhemd) in der Grundfarbe; wird auf die Kapseln gekachelt.
static Texture2D Plaid(Color baseColor)
{
const int size = 16;
var tex = new Texture2D(size, size, TextureFormat.RGB24, false);
Color light = Color.Lerp(baseColor, Color.white, 0.18f);
Color dark = baseColor * 0.62f;
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.45f); // dünne dunkle Linien
c.a = 1f;
tex.SetPixel(x, y, c);
}
}
tex.filterMode = FilterMode.Point;
tex.wrapMode = TextureWrapMode.Repeat;
tex.Apply();
return tex;
}
static Transform Textured(Transform part, Texture2D tex, Vector2 tiling)
{
Material m = part.GetComponent().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()); // stört sonst Physik/Trigger
var renderer = go.GetComponent();
renderer.material.color = color;
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)
{
// 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);
}
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;
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));
hip.y = Mathf.Clamp(neckBase.y - vertical, 0.3f, ThighLength + ShinLength + AnkleHeight - 0.02f);
}
Vector3 up = (neckBase - hip).normalized;
Vector3 right = Vector3.right;
Vector3 forward = Vector3.Cross(right, up).normalized;
// Rumpf
Quaternion torsoRot = Quaternion.LookRotation(forward, up);
Segment(torso, hip + up * 0.08f, neckBase, 0.4f, 0.25f);
belly.localPosition = hip + up * 0.2f + forward * 0.06f;
belly.localRotation = torsoRot;
belly.localScale = new Vector3(0.34f, 0.28f, 0.27f);
Segment(belt, hip + up * 0.05f, hip + up * 0.1f, 0.38f, 0.26f);
buckle.localPosition = hip + up * 0.075f + forward * 0.13f;
buckle.localRotation = torsoRot;
buckle.localScale = new Vector3(0.06f, 0.045f, 0.015f);
placket.localPosition = Vector3.Lerp(hip + up * 0.1f, neckBase, 0.62f) + forward * 0.122f;
placket.localRotation = torsoRot;
placket.localScale = new Vector3(0.025f, 0.26f, 0.01f);
Segment(pelvis, hip - right * HipWidth, hip + right * HipWidth, 0.22f, 0.24f);
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);
float swing = Mathf.Sin(walkPhase) * walkAmount;
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);
Vector3 knee = Joint(hipJoint, ankle, ThighLength, ShinLength, Vector3.forward);
Segment(thigh[i], hipJoint, knee, 0.17f, 0.17f);
Segment(shin[i], knee, ankle, 0.14f, 0.14f);
boot[i].localPosition = ankle + new Vector3(0f, -0.015f, 0.05f);
boot[i].localRotation = Quaternion.identity;
boot[i].localScale = new Vector3(0.12f, 0.1f, 0.27f);
sole[i].localPosition = ankle + new Vector3(0f, -0.065f, 0.05f);
sole[i].localRotation = Quaternion.identity;
sole[i].localScale = new Vector3(0.13f, 0.025f, 0.29f);
// Arme
Vector3 shoulder = neckBase - up * 0.06f + right * (ShoulderWidth * side);
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.03f, -(UpperArmLength + ForearmLength) + 0.04f, -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.12f, 0.12f);
Segment(forearm[i], elbow, handTarget, 0.11f, 0.11f);
Vector3 wrist = Vector3.Lerp(elbow, handTarget, 0.86f);
Segment(cuff[i], wrist, Vector3.Lerp(elbow, handTarget, 0.94f), 0.12f, 0.12f);
hand[i].localPosition = handTarget;
hand[i].localScale = new Vector3(0.085f, 0.1f, 0.1f);
}
}
/// Zwei-Knochen-Gelenk (Knie/Ellbogen): Punkt zwischen a und b, zur Seite "bend" geknickt.
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;
}
/// Kapsel von a nach b spannen (lokaler Raum der Figur).
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);
}
/// Wo das gehaltene Item beim Spieler gerade ist (relativ zum Blick).
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;
}
}
}