Passenger/driver sit still in moving cars: seat pose sent relative to the vehicle (backward-compatible), no backward pull; remote cars/traffic extrapolate in physics time and interpolate; UI polish (shadows, borders, gradient buttons, server list hover, fill bars, colored ping)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
33a415139b
commit
ce2fbdfc10
@@ -8,6 +8,20 @@
|
||||
- **Tags** der Server (z.B. `OFFICIAL`) erscheinen im Server-Profil; die Suche findet auch Tags
|
||||
- Braucht keinen neuen Server: die Infos kommen vom Master (msco.mbm.wtf) und funktionieren mit 2.8-Servern
|
||||
|
||||
**Mitfahren ohne Ruckeln**
|
||||
- Fahrer und Beifahrer sitzen beim anderen fest auf dem Sitz: die Figur wird relativ zum Auto übertragen und geglättet,
|
||||
statt in Weltkoordinaten hinter dem fahrenden Auto herzuhinken (vorher wurde der Beifahrer bei Tempo nach hinten gezogen)
|
||||
- Der Beifahrersitz wird jetzt aus der richtigen Fahrerposition berechnet (vorher aus der hinterherhinkenden Figur)
|
||||
- Autos anderer Spieler ruckeln nicht mehr bei jedem Netzwerkpaket zurück (Vorausberechnung in Physik-Zeit) und werden
|
||||
zwischen den Physik-Schritten flüssig bewegt – auch der NPC-Verkehr
|
||||
- Kompatibel: ältere Mods und der Server ignorieren die neuen Sitz-Daten
|
||||
|
||||
**Schönere Oberfläche**
|
||||
- Fenster (Launcher, Konsole, Admin-Menü, Hinweise) mit weichem Schatten und feinem Rand, orange Knöpfe mit Verlauf
|
||||
- Server-Browser: mehr Platz für die Liste, Zeilen leuchten beim Überfahren auf, Füllbalken für die Spielerzahl,
|
||||
Ping farbig (grün/gelb/rot), goldener Streifen bei hervorgehobenen Servern
|
||||
- Seitenleiste mit Trennlinie, Menüpunkte werden beim Überfahren hell
|
||||
|
||||
## 2.8
|
||||
|
||||
Server und Mod müssen beide 2.8 sein (Server: `git pull && docker compose up -d --build`).
|
||||
|
||||
+46
-9
@@ -13,7 +13,7 @@ namespace MSCO
|
||||
enum Page { P2P, ServerBrowser, Favorites }
|
||||
|
||||
const float Width = 760f;
|
||||
const float Height = 460f;
|
||||
const float Height = 500f;
|
||||
const float SidebarWidth = 190f;
|
||||
const float StatusHeight = 36f;
|
||||
|
||||
@@ -51,6 +51,8 @@ namespace MSCO
|
||||
|
||||
bool stylesReady;
|
||||
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
|
||||
Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull;
|
||||
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
|
||||
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
|
||||
string layoutError;
|
||||
GUIStyle dangerStyle;
|
||||
@@ -87,6 +89,7 @@ namespace MSCO
|
||||
|
||||
var window = new Rect((Screen.width - Width) / 2f, (Screen.height - Height) / 2f, Width, Height);
|
||||
ScreenRect = window;
|
||||
UiTheme.DrawShadow(window);
|
||||
GUI.Box(window, GUIContent.none, windowBox);
|
||||
|
||||
DrawSidebar(new Rect(window.x, window.y, SidebarWidth, Height - StatusHeight));
|
||||
@@ -120,6 +123,7 @@ namespace MSCO
|
||||
}
|
||||
GUI.Label(new Rect(textX, r.y + 24, r.xMax - textX, 40), "MSCO", logoStyle);
|
||||
GUI.Label(new Rect(r.x + 22, r.y + 70, r.width - 30, 20), "MY SUMMER CAR ONLINE", taglineStyle);
|
||||
GUI.DrawTexture(new Rect(r.x + 20, r.y + 98, r.width - 40, 1), texDivider);
|
||||
|
||||
float y = r.y + 110;
|
||||
if (NavButton(new Rect(r.x, y, r.width, 42), "P2P", page == Page.P2P)) page = Page.P2P;
|
||||
@@ -234,7 +238,7 @@ namespace MSCO
|
||||
GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, 70, header.height), "TYPE", headerCellStyle);
|
||||
GUI.Label(new Rect(header.x + header.width * 0.76f, header.y, 50, header.height), "PING", headerCellStyle);
|
||||
|
||||
serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(140));
|
||||
serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(180));
|
||||
if (shownError != null) GUILayout.Label(shownError, textStyle);
|
||||
if (servers.Count == 0)
|
||||
GUILayout.Label(browser.Loading ? "Loading servers..." : all.Count > 0 ? "No servers match your filters." : "No servers online right now.", textStyle);
|
||||
@@ -242,7 +246,10 @@ namespace MSCO
|
||||
{
|
||||
Rect row = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
|
||||
// Hervorgehobene Server (Keymaster-Admin) stehen oben - der Master liefert sie zuerst
|
||||
GUI.Box(new Rect(row.x, row.y + 2, row.width, row.height - 4), GUIContent.none, s.Featured ? featuredRowBox : rowBox);
|
||||
var rowRect = new Rect(row.x, row.y + 2, row.width, row.height - 4);
|
||||
bool hover = rowRect.Contains(Event.current.mousePosition);
|
||||
GUI.Box(rowRect, GUIContent.none, s.Featured ? featuredRowBox : hover ? rowHoverBox : rowBox);
|
||||
if (s.Featured) GUI.DrawTexture(new Rect(row.x, row.y + 8, 3, row.height - 16), texGold);
|
||||
|
||||
// Herz: als Favorit merken / entfernen (vor dem Zeilen-Knopf, damit es den Klick bekommt)
|
||||
Heart(new Rect(row.xMax - 58, row.y + 7, 20, 20), s);
|
||||
@@ -266,9 +273,14 @@ namespace MSCO
|
||||
nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version
|
||||
}
|
||||
GUI.Label(new Rect(nameX, row.y, row.x + row.width * 0.49f - nameX, row.height), s.Name, rowNameStyle);
|
||||
GUI.Label(new Rect(row.x + row.width * 0.5f, row.y, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle);
|
||||
// Spielerzahl mit Füllbalken (wie auf der Website)
|
||||
float px = row.x + row.width * 0.5f;
|
||||
GUI.Label(new Rect(px, row.y - 3, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle);
|
||||
float fill = s.MaxPlayers > 0 ? Mathf.Clamp01((float)s.Players / s.MaxPlayers) : 0f;
|
||||
GUI.DrawTexture(new Rect(px, row.y + 24, 48, 3), texBarBack);
|
||||
GUI.DrawTexture(new Rect(px, row.y + 24, Mathf.Max(2f, 48 * fill), 3), fill >= 1f ? texBarFull : texBarFill);
|
||||
Pill(row.x + row.width * 0.62f, row, s.RequireSteam ? "STEAM" : "NO STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
|
||||
GUI.Label(new Rect(row.x + row.width * 0.76f, row.y, 50, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", rowNameStyle);
|
||||
GUI.Label(new Rect(row.x + row.width * 0.76f, row.y, 60, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", PingStyle(s.Ping));
|
||||
GUI.Label(new Rect(row.xMax - 30, row.y, 20, row.height), "›", headerCellStyle); // Hinweis: Profil öffnen
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
@@ -354,7 +366,7 @@ namespace MSCO
|
||||
GUILayout.Space(8);
|
||||
|
||||
List<Favorites.Favorite> favs = Favorites.All;
|
||||
favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(220));
|
||||
favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(250));
|
||||
if (favs.Count == 0)
|
||||
GUILayout.Label("No favorites yet. Click the heart next to a server in the Server Browser, or add one by address below.", textStyle);
|
||||
Favorites.Favorite remove = null;
|
||||
@@ -562,7 +574,7 @@ namespace MSCO
|
||||
InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
|
||||
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
|
||||
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
|
||||
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", rowNameStyle);
|
||||
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
|
||||
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.Space(16);
|
||||
@@ -617,6 +629,13 @@ namespace MSCO
|
||||
GUILayout.Space(2);
|
||||
}
|
||||
|
||||
/// <summary>Ping farbig: grün gut, gelb geht so, rot schlecht.</summary>
|
||||
GUIStyle PingStyle(int ping)
|
||||
{
|
||||
if (ping < 0) return rowNameStyle;
|
||||
return ping < 80 ? pingGoodStyle : ping < 160 ? pingMidStyle : pingBadStyle;
|
||||
}
|
||||
|
||||
static string FormatUptime(int seconds)
|
||||
{
|
||||
int days = seconds / 86400, hours = seconds / 3600 % 24, minutes = seconds / 60 % 60;
|
||||
@@ -698,6 +717,7 @@ namespace MSCO
|
||||
|
||||
var r = new Rect((Screen.width - PromptWidth) / 2f, (Screen.height - PromptHeight) / 2f, PromptWidth, PromptHeight);
|
||||
ScreenRect = new Rect(0, 0, Screen.width, Screen.height); // Spielmenü dahinter komplett sperren
|
||||
UiTheme.DrawShadow(r);
|
||||
GUI.Box(r, GUIContent.none, windowBox);
|
||||
string title = reconnecting
|
||||
? ((mp.ReconnectMessage ?? "").StartsWith("Switching") ? "SWITCHING SERVER" : "RECONNECTING")
|
||||
@@ -783,6 +803,7 @@ namespace MSCO
|
||||
{
|
||||
var r = new Rect((Screen.width - 220) / 2f, 0, 220, 30);
|
||||
ScreenRect = r;
|
||||
UiTheme.DrawShadow(r);
|
||||
GUI.Box(r, GUIContent.none, sidebarBox);
|
||||
GUI.Box(new Rect(r.x + 10, r.yMax - 3, r.width - 20, 3), GUIContent.none, accentBox);
|
||||
Texture2D dot = mp.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? texPending : texMuted;
|
||||
@@ -809,9 +830,16 @@ namespace MSCO
|
||||
texPending = UiTheme.Rounded(Pending, 4);
|
||||
texMuted = UiTheme.Rounded(Muted, 4);
|
||||
texRow = Solid(Field);
|
||||
windowBox = UiTheme.Panel9(Background, 10);
|
||||
sidebarBox = UiTheme.Panel9(Sidebar, 10);
|
||||
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
|
||||
windowBox = UiTheme.Card(Background, 10);
|
||||
sidebarBox = UiTheme.Card(Sidebar, 10);
|
||||
rowBox = UiTheme.Panel9(Field, UiTheme.Radius);
|
||||
rowHoverBox = UiTheme.Card(FieldFocus, UiTheme.Radius);
|
||||
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
|
||||
texGold = Solid(Gold);
|
||||
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
|
||||
texBarFill = Solid(Online);
|
||||
texBarFull = Solid(UiTheme.Danger);
|
||||
accentBox = UiTheme.Panel9(Accent, 1);
|
||||
|
||||
pillStyle = UiTheme.Panel9(new Color(Accent.r, Accent.g, Accent.b, 0.18f), 4);
|
||||
@@ -846,6 +874,12 @@ namespace MSCO
|
||||
headerCellStyle.alignment = TextAnchor.MiddleLeft;
|
||||
rowNameStyle = Label(14, Color.white, FontStyle.Normal);
|
||||
rowNameStyle.alignment = TextAnchor.MiddleLeft;
|
||||
pingGoodStyle = new GUIStyle(rowNameStyle);
|
||||
pingGoodStyle.normal.textColor = Online;
|
||||
pingMidStyle = new GUIStyle(rowNameStyle);
|
||||
pingMidStyle.normal.textColor = Pending;
|
||||
pingBadStyle = new GUIStyle(rowNameStyle);
|
||||
pingBadStyle.normal.textColor = UiTheme.DangerHover;
|
||||
rowTagStyle = Label(11, Accent, FontStyle.Bold);
|
||||
rowTagStyle.alignment = TextAnchor.MiddleRight;
|
||||
badgeStyle = new GUIStyle(rowTagStyle);
|
||||
@@ -859,6 +893,7 @@ namespace MSCO
|
||||
toggleStyle.fontStyle = FontStyle.Bold;
|
||||
|
||||
navStyle = Button(Sidebar, Secondary, Muted, 14, FontStyle.Normal);
|
||||
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
|
||||
navStyle.alignment = TextAnchor.MiddleLeft;
|
||||
navStyle.padding = new RectOffset(26, 10, 0, 0);
|
||||
navActiveStyle = Button(Secondary, Secondary, Color.white, 14, FontStyle.Bold);
|
||||
@@ -866,9 +901,11 @@ namespace MSCO
|
||||
navActiveStyle.padding = new RectOffset(26, 10, 0, 0);
|
||||
|
||||
primaryStyle = Button(Accent, AccentHover, Color.white, 14, FontStyle.Bold);
|
||||
UiTheme.ApplyPrimaryGradient(primaryStyle);
|
||||
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 13, FontStyle.Bold);
|
||||
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, 14, FontStyle.Bold);
|
||||
tabStyle = Button(Sidebar, Secondary, Muted, 11, FontStyle.Bold);
|
||||
tabStyle.hover.textColor = Color.white;
|
||||
|
||||
fieldStyle = UiTheme.Field(14);
|
||||
|
||||
|
||||
+18
-1
@@ -1789,6 +1789,10 @@ namespace MSCO
|
||||
if (p.Avatar == null) continue;
|
||||
try
|
||||
{
|
||||
// Sitzt der Mitspieler in einem Fahrzeug, die Figur an unsere Kopie dieses Fahrzeugs hängen
|
||||
int seatId = p.Avatar.LastState.SeatId;
|
||||
SyncedBody seat = seatId != 0 ? objects.Get(seatId) : null;
|
||||
p.Avatar.Seat = seat != null && seat.Rb != null && seat.IsVehicle ? seat.Rb.transform : null;
|
||||
p.Avatar.Tick(Time.unscaledDeltaTime);
|
||||
|
||||
// Gehaltenes Item an die Hand der Figur setzen
|
||||
@@ -2057,9 +2061,22 @@ namespace MSCO
|
||||
CamRot = head.rotation,
|
||||
Seated = passenger.IsRiding || objects.IsDriving,
|
||||
FeetY = GroundBelowPlayer(head.position),
|
||||
HeldLocalRot = Quaternion.identity
|
||||
HeldLocalRot = Quaternion.identity,
|
||||
SeatView = Quaternion.identity
|
||||
};
|
||||
|
||||
// Im Fahrzeug: Kopf und Blick relativ zum Fahrzeug mitschicken
|
||||
SyncedBody seat = passenger.IsRiding ? passenger.Vehicle : objects.Driven;
|
||||
if (seat != null && seat.Rb != null)
|
||||
{
|
||||
Transform vt = seat.Rb.transform;
|
||||
state.SeatId = seat.Id;
|
||||
// Beifahrer: der Sitz ist bekannt - die Weltposition stammt noch vom letzten Frame, das Auto ist seitdem
|
||||
// weitergefahren (bei Tempo ein halber Meter)
|
||||
state.SeatHead = passenger.IsRiding ? passenger.SeatHeadLocal : vt.InverseTransformPoint(head.position);
|
||||
state.SeatView = Quaternion.Inverse(vt.rotation) * head.rotation;
|
||||
}
|
||||
|
||||
SyncedBody held = objects.Held;
|
||||
if (held != null)
|
||||
{
|
||||
|
||||
+27
-4
@@ -77,6 +77,12 @@ namespace MSCO
|
||||
public Quaternion HeldLocalRot; // relativ zur Kamera
|
||||
public bool Seated; // fährt oder sitzt als Beifahrer
|
||||
public float FeetY; // Bodenhöhe unter dem Spieler (für Ducken/Lehnen der Figur)
|
||||
// Im Fahrzeug: Kopf und Blick relativ zum Fahrzeug (ID des SyncedBody, 0 = nicht im Fahrzeug). Damit sitzt die
|
||||
// Figur beim anderen fest auf dem Sitz, statt in Weltkoordinaten hinter dem fahrenden Auto herzuhinken.
|
||||
// Steht am Ende der Nachricht: ältere Versionen und der Server lesen es einfach nicht mit.
|
||||
public int SeatId;
|
||||
public Vector3 SeatHead;
|
||||
public Quaternion SeatView;
|
||||
|
||||
public byte[] Serialize()
|
||||
{
|
||||
@@ -90,9 +96,15 @@ namespace MSCO
|
||||
w.Write(s.Seated);
|
||||
w.Write(s.FeetY);
|
||||
w.Write(s.HeldId);
|
||||
if (s.HeldId == 0) return;
|
||||
Write(w, s.HeldLocalPos);
|
||||
Write(w, s.HeldLocalRot);
|
||||
if (s.HeldId != 0)
|
||||
{
|
||||
Write(w, s.HeldLocalPos);
|
||||
Write(w, s.HeldLocalRot);
|
||||
}
|
||||
w.Write(s.SeatId);
|
||||
if (s.SeatId == 0) return;
|
||||
Write(w, s.SeatHead);
|
||||
Write(w, s.SeatView);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,13 +119,24 @@ namespace MSCO
|
||||
Seated = r.ReadBoolean(),
|
||||
FeetY = r.ReadSingle(),
|
||||
HeldId = r.ReadInt32(),
|
||||
HeldLocalRot = Quaternion.identity
|
||||
HeldLocalRot = Quaternion.identity,
|
||||
SeatView = Quaternion.identity
|
||||
};
|
||||
if (s.HeldId != 0)
|
||||
{
|
||||
s.HeldLocalPos = ReadVector(r);
|
||||
s.HeldLocalRot = ReadQuaternion(r);
|
||||
}
|
||||
// Ältere Versionen senden keinen Sitz
|
||||
if (r.BaseStream.Length - r.BaseStream.Position >= 4)
|
||||
{
|
||||
s.SeatId = r.ReadInt32();
|
||||
if (s.SeatId != 0)
|
||||
{
|
||||
s.SeatHead = ReadVector(r);
|
||||
s.SeatView = ReadQuaternion(r);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
+25
-5
@@ -25,6 +25,8 @@ namespace MSCO
|
||||
public int OwnerId = NoOwner;
|
||||
public bool OwnerActive; // Besitzer hält/fährt es gerade aktiv
|
||||
public bool WasKinematic; // Zustand bevor ein anderer Spieler es übernommen hat
|
||||
public RigidbodyInterpolation WasInterpolation;
|
||||
public bool InterpolationSet; // Fahrzeug eines anderen: bei uns interpoliert (siehe FreezeForRemote)
|
||||
// Gelenke der angebauten Teile, solange ein anderer das Fahrzeug fährt: unzerbrechlich (vorherige Bruchkraft/-moment)
|
||||
public Dictionary<Joint, Vector2> HeldJoints;
|
||||
public float NextPartScan;
|
||||
@@ -42,7 +44,7 @@ namespace MSCO
|
||||
public Vector3 TargetPos;
|
||||
public Quaternion TargetRot;
|
||||
public Vector3 TargetVel; // Geschwindigkeit beim Besitzer: damit wird die Position vorausberechnet
|
||||
public float TargetTime; // wann TargetPos ankam
|
||||
public float TargetTime; // wann TargetPos ankam (Physik-Zeit, Time.fixedTime)
|
||||
public float LastRecvTime;
|
||||
public float AvatarHoldTime = -100f; // zuletzt von der Hand eines Mitspielers platziert
|
||||
|
||||
@@ -209,6 +211,8 @@ namespace MSCO
|
||||
|
||||
/// <summary>Fährt der lokale Spieler gerade selbst?</summary>
|
||||
public bool IsDriving { get; private set; }
|
||||
/// <summary>Das Fahrzeug, das der lokale Spieler gerade fährt (sonst null).</summary>
|
||||
public SyncedBody Driven { get; private set; }
|
||||
|
||||
/// <summary>Fahrzeuge, die gerade ein anderer Spieler aktiv fährt.</summary>
|
||||
public List<SyncedBody> RemoteDrivenVehicles()
|
||||
@@ -315,6 +319,7 @@ namespace MSCO
|
||||
|
||||
SyncedBody held = HeldBody();
|
||||
SyncedBody driven = DrivenBody();
|
||||
Driven = driven;
|
||||
IsDriving = driven != null;
|
||||
bool motionClaimAllowed = Time.time - loadTime > MotionClaimGrace;
|
||||
|
||||
@@ -387,7 +392,9 @@ namespace MSCO
|
||||
if (Time.time - b.AvatarHoldTime < 0.2f) continue; // sitzt gerade in der Hand eines Mitspielers
|
||||
// Dort, wo das Objekt beim Besitzer JETZT ungefähr ist: letzte Position + Geschwindigkeit * Alter.
|
||||
// Ohne das hinken schnelle Objekte (fahrende Autos) immer einen Paketabstand hinterher und ruckeln.
|
||||
float age = Mathf.Min(Time.time - b.TargetTime, MaxExtrapolation);
|
||||
// Beides in Physik-Zeit: mit der Bildzeit (Time.time beim Empfang) wurde das Alter in den ersten
|
||||
// Physik-Schritten danach negativ - das Auto sprang bei jedem Paket ein Stück zurück (Ruckeln)
|
||||
float age = Mathf.Clamp(Time.fixedTime - b.TargetTime, 0f, MaxExtrapolation);
|
||||
Vector3 predicted = b.TargetPos + b.TargetVel * age;
|
||||
if ((b.Rb.position - predicted).sqrMagnitude > SnapDistance * SnapDistance)
|
||||
{
|
||||
@@ -489,7 +496,15 @@ namespace MSCO
|
||||
{
|
||||
b.WasKinematic = b.Rb.isKinematic;
|
||||
b.Rb.isKinematic = true;
|
||||
if (b.IsVehicle) HoldParts(b);
|
||||
if (b.IsVehicle)
|
||||
{
|
||||
HoldParts(b);
|
||||
// Physik läuft mit 50 Hz, das Bild öfter: ohne Interpolation springt das Auto nur in Physik-Schritten
|
||||
// weiter - wer darin mitfährt, sieht die Welt ruckeln. Nur bei Fahrzeugen (Items setzt die Hand direkt).
|
||||
b.WasInterpolation = b.Rb.interpolation;
|
||||
b.InterpolationSet = true;
|
||||
b.Rb.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
}
|
||||
}
|
||||
|
||||
void HoldParts(SyncedBody b)
|
||||
@@ -508,7 +523,12 @@ namespace MSCO
|
||||
/// <summary>Wieder selbst simulieren: Auto zurück auf seinen Zustand, Gelenke wieder mit ihrer Bruchkraft.</summary>
|
||||
void RestoreKinematic(SyncedBody b)
|
||||
{
|
||||
if (b.Rb != null) b.Rb.isKinematic = b.WasKinematic;
|
||||
if (b.Rb != null)
|
||||
{
|
||||
b.Rb.isKinematic = b.WasKinematic;
|
||||
if (b.InterpolationSet) b.Rb.interpolation = b.WasInterpolation;
|
||||
}
|
||||
b.InterpolationSet = false;
|
||||
if (b.HeldJoints == null) return;
|
||||
foreach (KeyValuePair<Joint, Vector2> kv in b.HeldJoints)
|
||||
{
|
||||
@@ -580,7 +600,7 @@ namespace MSCO
|
||||
b.TargetPos = pos;
|
||||
b.TargetRot = rot;
|
||||
b.TargetVel = vel.sqrMagnitude < 90f * 90f ? vel : Vector3.zero;
|
||||
b.TargetTime = Time.time;
|
||||
b.TargetTime = Time.fixedTime;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+4
-2
@@ -136,6 +136,7 @@ namespace MSCO
|
||||
{
|
||||
float width = Mathf.Min(Screen.width - 40, 960);
|
||||
var rect = new Rect((Screen.width - width) / 2f, 20, width, Mathf.Min(340, Screen.height * 0.5f));
|
||||
UiTheme.DrawShadow(rect);
|
||||
GUI.Box(rect, GUIContent.none, panel);
|
||||
|
||||
GUI.Label(new Rect(rect.x + 16, rect.y + 10, 300, 20), "MSCO CONSOLE", caption);
|
||||
@@ -271,6 +272,7 @@ namespace MSCO
|
||||
{
|
||||
float width = Mathf.Min(720, Screen.width - 40), height = Mathf.Min(580, Screen.height - 40);
|
||||
var rect = new Rect((Screen.width - width) / 2f, (Screen.height - height) / 2f, width, height);
|
||||
UiTheme.DrawShadow(rect);
|
||||
GUI.Box(rect, GUIContent.none, panel);
|
||||
|
||||
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == KeyCode.Escape)
|
||||
@@ -493,7 +495,7 @@ namespace MSCO
|
||||
void EnsureStyles()
|
||||
{
|
||||
if (stylesReady && panel.normal.background != null) return;
|
||||
panel = UiTheme.Panel9(UiTheme.Background);
|
||||
panel = UiTheme.Card(UiTheme.Background, 10);
|
||||
rowStyle = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.72f));
|
||||
rowStyle.padding = new RectOffset(12, 12, 6, 6);
|
||||
field = UiTheme.Field(14);
|
||||
@@ -505,7 +507,7 @@ namespace MSCO
|
||||
title = UiTheme.Label(22, UiTheme.Text, FontStyle.Bold);
|
||||
title.wordWrap = false;
|
||||
small = UiTheme.Label(11, UiTheme.Muted);
|
||||
primary = UiTheme.Button(UiTheme.Accent, UiTheme.AccentHover, Color.white);
|
||||
primary = UiTheme.PrimaryButton();
|
||||
secondary = UiTheme.Button(UiTheme.Secondary, UiTheme.SecondaryHover, UiTheme.Text, 12);
|
||||
danger = UiTheme.Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 12);
|
||||
chatName = UiTheme.Label(13, UiTheme.AccentHover, FontStyle.Bold, TextAnchor.MiddleLeft);
|
||||
|
||||
@@ -29,6 +29,10 @@ namespace MSCO
|
||||
readonly List<Rigidbody> frozenBodies = new List<Rigidbody>();
|
||||
|
||||
public bool IsRiding { get { return vehicle != null; } }
|
||||
/// <summary>Fahrzeug, in dem wir mitfahren (sonst null).</summary>
|
||||
public SyncedBody Vehicle { get { return vehicle; } }
|
||||
/// <summary>Kopf auf dem Sitz, relativ zum Fahrzeug (genau, nicht aus der Weltposition des letzten Frames).</summary>
|
||||
public Vector3 SeatHeadLocal { get { return seatHeadLocal; } }
|
||||
|
||||
/// <summary>Taste gedrückt: einsteigen oder aussteigen. Gibt einen Statustext zurück.</summary>
|
||||
public string Toggle(Transform player, Camera cam, ObjectSync objects, IDictionary<int, PlayerInfo> players)
|
||||
|
||||
@@ -49,6 +49,12 @@ namespace MSCO
|
||||
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)
|
||||
@@ -258,6 +264,15 @@ namespace MSCO
|
||||
|
||||
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);
|
||||
@@ -273,6 +288,32 @@ namespace MSCO
|
||||
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;
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace MSCO
|
||||
|
||||
var r = new Rect(Screen.width - 12 - Width + (1f - slide) * (Width + 20f), y, Width, Height);
|
||||
GUI.color = new Color(1f, 1f, 1f, alpha);
|
||||
UiTheme.DrawShadow(r);
|
||||
GUI.Box(r, GUIContent.none, box);
|
||||
|
||||
GUI.color = new Color(t.Dot.r, t.Dot.g, t.Dot.b, alpha);
|
||||
@@ -73,7 +74,7 @@ namespace MSCO
|
||||
void EnsureStyles()
|
||||
{
|
||||
if (box != null && box.normal.background != null) return;
|
||||
box = UiTheme.Panel9(new Color(0.06f, 0.06f, 0.07f, 0.88f), 6);
|
||||
box = UiTheme.Card(new Color(0.06f, 0.06f, 0.07f, 0.92f), 6);
|
||||
title = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleLeft };
|
||||
title.normal.textColor = Color.white;
|
||||
title.wordWrap = false;
|
||||
|
||||
+2
-2
@@ -100,7 +100,7 @@ namespace MSCO
|
||||
c.TargetPos = p;
|
||||
c.TargetRot = q;
|
||||
c.TargetVel = v;
|
||||
c.TargetTime = Time.time;
|
||||
c.TargetTime = Time.fixedTime; // Physik-Zeit wie in FixedUpdate (sonst negatives Alter = Rucken)
|
||||
if (!c.Following)
|
||||
{
|
||||
c.WasKinematic = c.Rb.isKinematic;
|
||||
@@ -117,7 +117,7 @@ namespace MSCO
|
||||
foreach (Car c in cars.Values)
|
||||
{
|
||||
if (!c.Following || c.Rb == null) continue;
|
||||
float ahead = Mathf.Min(Time.time - c.TargetTime, MaxExtrapolation);
|
||||
float ahead = Mathf.Clamp(Time.fixedTime - c.TargetTime, 0f, MaxExtrapolation);
|
||||
Vector3 goal = c.TargetPos + c.TargetVel * ahead;
|
||||
float t = Mathf.Clamp01(Time.fixedDeltaTime * Smoothing);
|
||||
if ((c.Rb.position - goal).sqrMagnitude > 25f) { c.Rb.position = goal; c.Rb.rotation = c.TargetRot; continue; }
|
||||
|
||||
+91
@@ -26,8 +26,13 @@ namespace MSCO
|
||||
public static readonly Color Muted = Hex(0x8e, 0x8e, 0x99);
|
||||
public static readonly Color Online = Hex(0x3d, 0xd6, 0x8c);
|
||||
public static readonly Color Pending = Hex(0xf2, 0xc2, 0x1d);
|
||||
public static readonly Color Gold = Hex(0xff, 0xb4, 0x5a);
|
||||
/// <summary>Feiner heller Rand um Fenster und Karten (wie auf der Website).</summary>
|
||||
public static readonly Color Border = new Color(1f, 1f, 1f, 0.08f);
|
||||
|
||||
public const int Radius = 6;
|
||||
const int ShadowBlur = 16;
|
||||
static GUIStyle shadowStyle;
|
||||
|
||||
public static Color Hex(int r, int g, int b, float a = 1f)
|
||||
{
|
||||
@@ -58,6 +63,92 @@ namespace MSCO
|
||||
return tex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wie Rounded, aber mit senkrechtem Verlauf (oben -> unten) und optional 1 px Rand. Die Textur ist höher als
|
||||
/// breit, damit der Verlauf beim 9-Slice-Strecken im Mittelteil erhalten bleibt.
|
||||
/// </summary>
|
||||
public static Texture2D RoundedEx(Color top, Color bottom, Color border, bool withBorder, int radius = Radius, int height = 48)
|
||||
{
|
||||
int width = radius * 2 + 4;
|
||||
height = Mathf.Max(height, width);
|
||||
var tex = new Texture2D(width, height, TextureFormat.ARGB32, false);
|
||||
tex.wrapMode = TextureWrapMode.Clamp;
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
// Texturzeile 0 ist unten
|
||||
Color fill = Color.Lerp(bottom, top, (y + 0.5f) / height);
|
||||
// Rand deckend auf die Fläche mischen (ein durchsichtiger Rand ließe das Spiel durchscheinen)
|
||||
Color edge = Color.Lerp(fill, new Color(border.r, border.g, border.b, fill.a), border.a);
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
float cx = Mathf.Clamp(x + 0.5f, radius, width - radius);
|
||||
float cy = Mathf.Clamp(y + 0.5f, radius, height - radius);
|
||||
float d = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy));
|
||||
Color c = withBorder ? Color.Lerp(edge, fill, Mathf.Clamp01(radius - 1f - d + 0.5f)) : fill;
|
||||
c.a *= Mathf.Clamp01(radius - d + 0.5f);
|
||||
tex.SetPixel(x, y, c);
|
||||
}
|
||||
}
|
||||
tex.Apply();
|
||||
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||
return tex;
|
||||
}
|
||||
|
||||
/// <summary>Fenster/Karte mit feinem Rand; mit GUI.Box zeichnen (vorher DrawShadow für Tiefe).</summary>
|
||||
public static GUIStyle Card(Color color, int radius = Radius)
|
||||
{
|
||||
var s = new GUIStyle();
|
||||
s.normal.background = RoundedEx(color, color, Border, true, radius, radius * 2 + 4);
|
||||
s.border = new RectOffset(radius + 2, radius + 2, radius + 2, radius + 2);
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>Orange Hauptknopf mit leichtem Verlauf (wie .btn auf der Website).</summary>
|
||||
public static GUIStyle PrimaryButton(int fontSize = 13)
|
||||
{
|
||||
GUIStyle s = Button(Accent, AccentHover, Color.white, fontSize);
|
||||
ApplyPrimaryGradient(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>Verlauf auf einen vorhandenen Knopf-Stil legen (Rundung und Rand bleiben).</summary>
|
||||
public static void ApplyPrimaryGradient(GUIStyle s)
|
||||
{
|
||||
s.normal.background = s.focused.background = RoundedEx(AccentHover, Accent, Color.clear, false);
|
||||
s.hover.background = s.active.background = s.onNormal.background =
|
||||
RoundedEx(Color.Lerp(AccentHover, Color.white, 0.12f), AccentHover, Color.clear, false);
|
||||
}
|
||||
|
||||
/// <summary>Weicher Schatten unter einem Fenster (etwas nach unten versetzt). Vor dem Fenster zeichnen.</summary>
|
||||
public static void DrawShadow(Rect r)
|
||||
{
|
||||
if (shadowStyle == null || shadowStyle.normal.background == null)
|
||||
{
|
||||
const int radius = 10;
|
||||
int reach = radius + ShadowBlur;
|
||||
int size = reach * 2 + 4;
|
||||
var tex = new Texture2D(size, size, TextureFormat.ARGB32, false);
|
||||
tex.wrapMode = TextureWrapMode.Clamp;
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
for (int y = 0; y < size; y++)
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float cx = Mathf.Clamp(x + 0.5f, reach, size - reach);
|
||||
float cy = Mathf.Clamp(y + 0.5f, reach, size - reach);
|
||||
float outside = Mathf.Max(0f, Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(cx, cy)) - radius);
|
||||
float fade = 1f - Mathf.Clamp01(outside / ShadowBlur);
|
||||
tex.SetPixel(x, y, new Color(0f, 0f, 0f, 0.45f * fade * fade));
|
||||
}
|
||||
tex.Apply();
|
||||
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||
shadowStyle = new GUIStyle();
|
||||
shadowStyle.normal.background = tex;
|
||||
shadowStyle.border = new RectOffset(reach + 2, reach + 2, reach + 2, reach + 2);
|
||||
}
|
||||
GUI.Box(new Rect(r.x - ShadowBlur, r.y - ShadowBlur + 6, r.width + ShadowBlur * 2, r.height + ShadowBlur * 2), GUIContent.none, shadowStyle);
|
||||
}
|
||||
|
||||
public static Texture2D Solid(Color color)
|
||||
{
|
||||
var tex = new Texture2D(1, 1);
|
||||
|
||||
Reference in New Issue
Block a user