Refactor: extract shared helpers across scripts, trim remaining doc redundancy
Deduplication pass (44 extractions): - New SmoothedLook: shared smoothed yaw/pitch look tick + view seeding (FirstPersonController, FreeCamController) - New CharacterTeleport: disable-CC-around-teleport idiom (rescue, owner position apply, free-cam sync) - New GeometryUtil: RescaleToUnitScale + TransformBounds (concealment meter/volume, skinned collider baker) - Paint family: canonical map read/write shared between Net and Persistence, snapshot decode, backend stroke dispatch, RebuildBackend, ResolveSeed, chunked RPC send/gate/copy, CamoKnobs/HeightKnobs aggregates, SanitizeFileKey, NewMaskTexture, AdoptAlbedo, FindSlider - Player/modes: GameMode SetPlayerControl/SetPainting/SetOrbitInputEnabled, InputUtil.GameplayKeyPressed, NearestClimbHit, ComputeXRay delegates to ComputeResolutionFreeze, FingerIK TryInPlaneAngle - Match/misc: UnhookSessionCallbacks, SendEmptyNamedMessage, SwapScenes, RunSessionOpAsync, CovarianceEigen, CollectClusterPoints, MatchHud GUIStyle cache, HsvColorPicker rehook, VoxelCenter Documentation pass: remove 6 triplicated doc blocks and 2 restating inline comments left after the previous reduction. Behavior-neutral; hot-path rasterizer loops intentionally untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
066e308cde
commit
2243cc823e
40 files changed
+681
-724
No files matched your search
@@ -0,0 +1,21 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>Disables a CharacterController around a transform write so it cannot snap the transform back.</summary>
|
||||
public static class CharacterTeleport
|
||||
{
|
||||
public static void Move(CharacterController cc, Transform t, Vector3 position)
|
||||
{
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
t.position = position;
|
||||
if (cc) cc.enabled = reenable;
|
||||
}
|
||||
|
||||
public static void Move(CharacterController cc, Transform t, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
t.SetPositionAndRotation(position, rotation);
|
||||
if (cc) cc.enabled = reenable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52a1fdeaf96581840bc09373ff2c7ad0
|
||||
@@ -198,16 +198,7 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
int made = 0;
|
||||
for (int ci = 0; ci < clusters.Count; ci++)
|
||||
{
|
||||
var pset = new HashSet<int>();
|
||||
var pts = new List<Vector3>();
|
||||
List<int> cl = clusters[ci];
|
||||
for (int j = 0; j < cl.Count; j++)
|
||||
{
|
||||
int t = cl[j];
|
||||
if (pset.Add(triA[t])) pts.Add(wv[triA[t]]);
|
||||
if (pset.Add(triB[t])) pts.Add(wv[triB[t]]);
|
||||
if (pset.Add(triC[t])) pts.Add(wv[triC[t]]);
|
||||
}
|
||||
List<Vector3> pts = CollectClusterPoints(clusters[ci], wv, triA, triB, triC);
|
||||
|
||||
float thick = SlabThickness(pts, out Vector3 normal);
|
||||
List<Vector3> hullPts;
|
||||
@@ -389,13 +380,25 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
if (set.Add(c)) { pos.Add(wv[c]); addedOut?.Add(c); }
|
||||
}
|
||||
|
||||
/// <summary>Extent of the point cloud along its PCA minor axis (= slab thickness), plus that axis.</summary>
|
||||
private static float SlabThickness(List<Vector3> pts, out Vector3 normal)
|
||||
/// <summary>Unique welded vertex positions of a triangle cluster.</summary>
|
||||
private static List<Vector3> CollectClusterPoints(List<int> cluster, List<Vector3> wv, List<int> triA, List<int> triB, List<int> triC)
|
||||
{
|
||||
normal = Vector3.up;
|
||||
int n = pts.Count;
|
||||
if (n < 2) return 0f;
|
||||
var pts = new List<Vector3>(cluster.Count * 3);
|
||||
var seen = new HashSet<int>();
|
||||
for (int j = 0; j < cluster.Count; j++)
|
||||
{
|
||||
int t = cluster[j];
|
||||
if (seen.Add(triA[t])) pts.Add(wv[triA[t]]);
|
||||
if (seen.Add(triB[t])) pts.Add(wv[triB[t]]);
|
||||
if (seen.Add(triC[t])) pts.Add(wv[triC[t]]);
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
/// <summary>Mean-centered 3x3 covariance of the point cloud, eigendecomposed via <see cref="Jacobi3"/>.</summary>
|
||||
private static void CovarianceEigen(List<Vector3> pts, out double[,] vec, out double[] d)
|
||||
{
|
||||
int n = pts.Count;
|
||||
Vector3 mean = Vector3.zero;
|
||||
for (int i = 0; i < n; i++) mean += pts[i];
|
||||
mean /= n;
|
||||
@@ -409,7 +412,17 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
}
|
||||
|
||||
var a = new double[3, 3] { { c00, c01, c02 }, { c01, c11, c12 }, { c02, c12, c22 } };
|
||||
Jacobi3(a, out double[,] v, out double[] d);
|
||||
Jacobi3(a, out vec, out d);
|
||||
}
|
||||
|
||||
/// <summary>Extent of the point cloud along its PCA minor axis (= slab thickness), plus that axis.</summary>
|
||||
private static float SlabThickness(List<Vector3> pts, out Vector3 normal)
|
||||
{
|
||||
normal = Vector3.up;
|
||||
int n = pts.Count;
|
||||
if (n < 2) return 0f;
|
||||
|
||||
CovarianceEigen(pts, out double[,] v, out double[] d);
|
||||
|
||||
int mi = 0;
|
||||
if (d[1] < d[mi]) mi = 1;
|
||||
@@ -483,15 +496,7 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
/// </summary>
|
||||
private static float ClusterFill(List<int> cluster, List<Vector3> wv, List<int> triA, List<int> triB, List<int> triC, out Vector3 splitAxis)
|
||||
{
|
||||
var pts = new List<Vector3>(cluster.Count * 3);
|
||||
var seen = new HashSet<int>();
|
||||
for (int j = 0; j < cluster.Count; j++)
|
||||
{
|
||||
int t = cluster[j];
|
||||
if (seen.Add(triA[t])) pts.Add(wv[triA[t]]);
|
||||
if (seen.Add(triB[t])) pts.Add(wv[triB[t]]);
|
||||
if (seen.Add(triC[t])) pts.Add(wv[triC[t]]);
|
||||
}
|
||||
List<Vector3> pts = CollectClusterPoints(cluster, wv, triA, triB, triC);
|
||||
|
||||
PcaFrame(pts, out Vector3 u, out Vector3 v, out _);
|
||||
|
||||
@@ -555,23 +560,9 @@ public class ConcealmentColliderBaker : MonoBehaviour
|
||||
private static void PcaFrame(List<Vector3> pts, out Vector3 u, out Vector3 v, out Vector3 n)
|
||||
{
|
||||
u = Vector3.right; v = Vector3.up; n = Vector3.forward;
|
||||
int cnt = pts.Count;
|
||||
if (cnt < 2) return;
|
||||
if (pts.Count < 2) return;
|
||||
|
||||
Vector3 mean = Vector3.zero;
|
||||
for (int i = 0; i < cnt; i++) mean += pts[i];
|
||||
mean /= cnt;
|
||||
|
||||
double c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0;
|
||||
for (int i = 0; i < cnt; i++)
|
||||
{
|
||||
double x = pts[i].x - mean.x, y = pts[i].y - mean.y, z = pts[i].z - mean.z;
|
||||
c00 += x * x; c01 += x * y; c02 += x * z;
|
||||
c11 += y * y; c12 += y * z; c22 += z * z;
|
||||
}
|
||||
|
||||
var mat = new double[3, 3] { { c00, c01, c02 }, { c01, c11, c12 }, { c02, c12, c22 } };
|
||||
Jacobi3(mat, out double[,] vec, out double[] d);
|
||||
CovarianceEigen(pts, out double[,] vec, out double[] d);
|
||||
|
||||
int hi = 0, mid = 1, lo = 2;
|
||||
if (d[hi] < d[mid]) { int t = hi; hi = mid; mid = t; }
|
||||
|
||||
@@ -88,7 +88,6 @@ public class ConcealmentMeter : MonoBehaviour
|
||||
private readonly List<MeshCollider> _concave = new List<MeshCollider>();
|
||||
private readonly List<ConcealmentVolume> _volumes = new List<ConcealmentVolume>();
|
||||
|
||||
// Gizmo cache.
|
||||
private readonly List<Vector3> _gizPos = new List<Vector3>();
|
||||
private readonly List<bool> _gizEmbedded = new List<bool>();
|
||||
private float _gizVoxelWorld;
|
||||
@@ -132,9 +131,7 @@ public class ConcealmentMeter : MonoBehaviour
|
||||
// else a scaled player is measured at scale² (voxel world positions, broadphase box AND gizmos), which
|
||||
// also corrupts EmbeddedPercent. No-op at scale 1; the uniform divide is rotation-independent, which the
|
||||
// player's uniform scale guarantees. Mirrors SkinnedColliderBaker's identical fixup.
|
||||
Vector3 ls = targetRenderer.transform.lossyScale;
|
||||
if (ls.x != 1f || ls.y != 1f || ls.z != 1f)
|
||||
RescaleBakedToUnitScale(ls);
|
||||
GeometryUtil.RescaleToUnitScale(_baked, targetRenderer.transform.lossyScale, _vbuf);
|
||||
|
||||
_verts = _baked.vertices;
|
||||
_tris = _baked.triangles;
|
||||
@@ -156,7 +153,7 @@ public class ConcealmentMeter : MonoBehaviour
|
||||
Matrix4x4 l2w = targetRenderer.transform.localToWorldMatrix;
|
||||
// Derive the broadphase box from the ACTUAL transformed geometry, not SkinnedMeshRenderer.bounds
|
||||
// (the latter is a root-bone approximation that can be badly offset for a deformed/posed rig).
|
||||
Bounds worldBounds = TransformBounds(l2w, lb);
|
||||
Bounds worldBounds = GeometryUtil.TransformBounds(l2w, lb);
|
||||
BuildEnvironmentCandidates(worldBounds);
|
||||
|
||||
bool anyEnv = _convex.Count > 0 ||
|
||||
@@ -213,24 +210,6 @@ public class ConcealmentMeter : MonoBehaviour
|
||||
HasResult = true;
|
||||
}
|
||||
|
||||
/// <summary>Divides the player's world scale back out of the freshly baked mesh so it is true unit-scale
|
||||
/// local geometry (the space this class raises to world with). Reuses <see cref="_vbuf"/> — no per-eval alloc.</summary>
|
||||
private void RescaleBakedToUnitScale(Vector3 lossyScale)
|
||||
{
|
||||
float ix = lossyScale.x != 0f ? 1f / lossyScale.x : 0f;
|
||||
float iy = lossyScale.y != 0f ? 1f / lossyScale.y : 0f;
|
||||
float iz = lossyScale.z != 0f ? 1f / lossyScale.z : 0f;
|
||||
_baked.GetVertices(_vbuf);
|
||||
for (int i = 0; i < _vbuf.Count; i++)
|
||||
{
|
||||
Vector3 v = _vbuf[i];
|
||||
v.x *= ix; v.y *= iy; v.z *= iz;
|
||||
_vbuf[i] = v;
|
||||
}
|
||||
_baked.SetVertices(_vbuf);
|
||||
_baked.RecalculateBounds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rasterizes every triangle into the XZ column grid; each column accumulates the Y values
|
||||
/// where the surface crosses it (one per triangle whose XZ projection covers the column center).
|
||||
@@ -389,22 +368,6 @@ public class ConcealmentMeter : MonoBehaviour
|
||||
return (hits & 1) == 1;
|
||||
}
|
||||
|
||||
/// <summary>World AABB enclosing a local AABB after an affine transform (encapsulates the 8 transformed corners).</summary>
|
||||
private static Bounds TransformBounds(Matrix4x4 m, Bounds local)
|
||||
{
|
||||
Vector3 mn = local.min, mx = local.max;
|
||||
Vector3 c0 = m.MultiplyPoint3x4(mn);
|
||||
Bounds b = new Bounds(c0, Vector3.zero);
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mn.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mx.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(mx));
|
||||
return b;
|
||||
}
|
||||
|
||||
private static float AverageScale(Transform t)
|
||||
{
|
||||
Vector3 s = t.lossyScale;
|
||||
|
||||
@@ -60,16 +60,9 @@ public class ConcealmentVolume : MonoBehaviour
|
||||
{
|
||||
Vector3 mn = origin;
|
||||
Vector3 mx = origin + new Vector3(nx * voxelSize, ny * voxelSize, nz * voxelSize);
|
||||
Matrix4x4 m = transform.localToWorldMatrix;
|
||||
Bounds b = new Bounds(m.MultiplyPoint3x4(mn), Vector3.zero);
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mn.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mx.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(mx));
|
||||
return b;
|
||||
var local = new Bounds();
|
||||
local.SetMinMax(mn, mx);
|
||||
return GeometryUtil.TransformBounds(transform.localToWorldMatrix, local);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,13 +177,7 @@ public class ConcealmentVolume : MonoBehaviour
|
||||
bool enclosed = useFlood && !outside[idx];
|
||||
bool solid = false;
|
||||
if (useWinding)
|
||||
{
|
||||
Vector3 cWorldLocal = new Vector3(
|
||||
origin.x + (ix + 0.5f) * vs,
|
||||
origin.y + (iy + 0.5f) * vs,
|
||||
origin.z + (iz + 0.5f) * vs);
|
||||
solid = WindingNumber(cWorldLocal, verts, tris) > 0.5;
|
||||
}
|
||||
solid = WindingNumber(VoxelCenter(origin, ix, iy, iz, vs), verts, tris) > 0.5;
|
||||
bool inside = enclosed || solid;
|
||||
if (!inside) continue;
|
||||
bits[idx >> 5] |= 1u << (idx & 31);
|
||||
@@ -253,6 +240,10 @@ public class ConcealmentVolume : MonoBehaviour
|
||||
return sum / (4.0 * System.Math.PI);
|
||||
}
|
||||
|
||||
/// <summary>Local-space center of grid voxel (ix, iy, iz).</summary>
|
||||
private static Vector3 VoxelCenter(Vector3 origin, int ix, int iy, int iz, float size)
|
||||
=> new Vector3(origin.x + (ix + 0.5f) * size, origin.y + (iy + 0.5f) * size, origin.z + (iz + 0.5f) * size);
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!drawBakedGizmos || !HasBake) return;
|
||||
@@ -265,11 +256,7 @@ public class ConcealmentVolume : MonoBehaviour
|
||||
{
|
||||
int idx = ix + nx * (iy + ny * iz);
|
||||
if ((bits[idx >> 5] & (1u << (idx & 31))) == 0) continue;
|
||||
Vector3 c = new Vector3(
|
||||
origin.x + (ix + 0.5f) * voxelSize,
|
||||
origin.y + (iy + 0.5f) * voxelSize,
|
||||
origin.z + (iz + 0.5f) * voxelSize);
|
||||
Gizmos.DrawCube(c, cube);
|
||||
Gizmos.DrawCube(VoxelCenter(origin, ix, iy, iz, voxelSize), cube);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -306,11 +306,8 @@ public class FingerIK : MonoBehaviour
|
||||
|
||||
// World hinge axis (invariant under the joint's own twist about local X).
|
||||
Vector3 axisW = (j.rotation * HingeAxis(i)).normalized;
|
||||
Vector3 tipPlane = Vector3.ProjectOnPlane(toTip, axisW);
|
||||
Vector3 tgtPlane = Vector3.ProjectOnPlane(toTarget, axisW);
|
||||
if (tipPlane.sqrMagnitude < 1e-10f || tgtPlane.sqrMagnitude < 1e-10f) continue;
|
||||
if (!TryInPlaneAngle(toTip, toTarget, axisW, out float hingeAng)) continue;
|
||||
|
||||
float hingeAng = Vector3.SignedAngle(tipPlane, tgtPlane, axisW);
|
||||
j.rotation = Quaternion.AngleAxis(hingeAng, axisW) * j.rotation;
|
||||
|
||||
ConstrainToHinge(i);
|
||||
@@ -367,16 +364,25 @@ public class FingerIK : MonoBehaviour
|
||||
Vector3 toTip = tip.position - b.position;
|
||||
Vector3 toTarget = target.position - b.position;
|
||||
|
||||
float latAng = 0f;
|
||||
Vector3 tipPlane = Vector3.ProjectOnPlane(toTip, axisW);
|
||||
Vector3 tgtPlane = Vector3.ProjectOnPlane(toTarget, axisW);
|
||||
if (tipPlane.sqrMagnitude > 1e-10f && tgtPlane.sqrMagnitude > 1e-10f)
|
||||
latAng = Vector3.SignedAngle(tipPlane, tgtPlane, axisW);
|
||||
TryInPlaneAngle(toTip, toTarget, axisW, out float latAng); // degenerate -> 0 (spread bias still applies)
|
||||
|
||||
latAng = Mathf.Clamp(latAng + spread, spreadLimits.x, spreadLimits.y);
|
||||
_solvedLocal[0] = _solvedLocal[0] * Quaternion.AngleAxis(latAng, SpreadAxis);
|
||||
}
|
||||
|
||||
// Signed angle from toTip to toTarget projected onto the plane orthogonal to axis; false when
|
||||
// either projection is degenerate (angle is then 0 and must not be applied as a solve step).
|
||||
private static bool TryInPlaneAngle(Vector3 toTip, Vector3 toTarget, Vector3 axis, out float angle)
|
||||
{
|
||||
angle = 0f;
|
||||
Vector3 tipPlane = Vector3.ProjectOnPlane(toTip, axis);
|
||||
Vector3 tgtPlane = Vector3.ProjectOnPlane(toTarget, axis);
|
||||
if (tipPlane.sqrMagnitude < 1e-10f || tgtPlane.sqrMagnitude < 1e-10f) return false;
|
||||
|
||||
angle = Vector3.SignedAngle(tipPlane, tgtPlane, axis);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Base-joint spread/opposition about local Z, composed AFTER curl so the two DOFs don't
|
||||
// fight. Applied on the final base rotation when IK isn't driving the base laterally.
|
||||
private void ApplySpread()
|
||||
|
||||
@@ -53,12 +53,7 @@ public class FirstPersonController : MonoBehaviour
|
||||
CinemachineCamera _vcam;
|
||||
PlayerCharacterController _player; // body whose yaw we pin to the camera while active
|
||||
PlayerPoseController _poseController; // supplies the active pose's per-pose first-person eye offset
|
||||
float _yaw; // look target (raw input accumulates here)
|
||||
float _pitch;
|
||||
float _displayYaw; // smoothed value actually applied to the transform
|
||||
float _displayPitch;
|
||||
float _yawSmoothRef;
|
||||
float _pitchSmoothRef;
|
||||
SmoothedLook _look; // yaw/pitch accumulator + smoothing shared with the freecam
|
||||
bool _resolved;
|
||||
|
||||
void Awake()
|
||||
@@ -89,7 +84,7 @@ public class FirstPersonController : MonoBehaviour
|
||||
SyncToCurrentView();
|
||||
InputUtil.SetCursorLocked(true);
|
||||
ApplyTransform(); // place at the eye on the same frame so the blend starts from there
|
||||
if (_player) _player.FaceYaw = _displayYaw; // body faces the camera from frame one
|
||||
if (_player) _player.FaceYaw = _look.DisplayYaw; // body faces the camera from frame one
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -105,32 +100,18 @@ public class FirstPersonController : MonoBehaviour
|
||||
// LateUpdate so the player has already moved the CameraTarget this frame.
|
||||
LookTick();
|
||||
ApplyTransform();
|
||||
if (_player) _player.FaceYaw = _displayYaw; // keep the body's yaw locked to the camera
|
||||
if (_player) _player.FaceYaw = _look.DisplayYaw; // keep the body's yaw locked to the camera
|
||||
}
|
||||
|
||||
void LookTick()
|
||||
{
|
||||
Vector2 delta = InputUtil.ReadMouseDelta() * MouseSensitivity.Multiplier;
|
||||
_yaw += delta.x * horizontalSensitivity;
|
||||
float vs = verticalSensitivity * (invertY ? 1f : -1f);
|
||||
_pitch = Mathf.Clamp(_pitch + delta.y * vs, -maxPitch, maxPitch);
|
||||
|
||||
if (lookSmoothTime > 0f)
|
||||
{
|
||||
// SmoothDampAngle is frame-rate-independent and wraps yaw correctly.
|
||||
_displayYaw = Mathf.SmoothDampAngle(_displayYaw, _yaw, ref _yawSmoothRef, lookSmoothTime);
|
||||
_displayPitch = Mathf.SmoothDampAngle(_displayPitch, _pitch, ref _pitchSmoothRef, lookSmoothTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayYaw = _yaw;
|
||||
_displayPitch = _pitch;
|
||||
}
|
||||
_look.Tick(delta, horizontalSensitivity, verticalSensitivity, invertY, maxPitch, lookSmoothTime);
|
||||
}
|
||||
|
||||
void ApplyTransform()
|
||||
{
|
||||
Quaternion rot = Quaternion.Euler(_displayPitch, _displayYaw, 0f);
|
||||
Quaternion rot = _look.Rotation;
|
||||
if (cameraTarget)
|
||||
{
|
||||
// Sit at the first-person eye anchor, then raise/lower the eye by the active pose's first-person
|
||||
@@ -151,18 +132,9 @@ public class FirstPersonController : MonoBehaviour
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam)
|
||||
{
|
||||
Vector3 e = cam.transform.rotation.eulerAngles;
|
||||
_yaw = e.y;
|
||||
float p = e.x > 180f ? e.x - 360f : e.x;
|
||||
_pitch = Mathf.Clamp(p, -maxPitch, maxPitch);
|
||||
}
|
||||
|
||||
// Snap the smoothed state to the fresh pose so entering never lags/jumps to catch up.
|
||||
_displayYaw = _yaw;
|
||||
_displayPitch = _pitch;
|
||||
_yawSmoothRef = 0f;
|
||||
_pitchSmoothRef = 0f;
|
||||
_look.SyncTo(cam.transform.rotation, maxPitch);
|
||||
else
|
||||
_look.Snap(); // no camera: keep the current angles but still snap the smoothed state
|
||||
}
|
||||
|
||||
void SetMeshHidden(bool hidden)
|
||||
|
||||
@@ -14,18 +14,9 @@ public class FirstPersonMode : GameMode
|
||||
public override void Enter()
|
||||
{
|
||||
// Player stays fully controllable; only the view changes.
|
||||
if (Manager.Player)
|
||||
{
|
||||
Manager.Player.enabled = true;
|
||||
Manager.Player.ControlEnabled = true;
|
||||
}
|
||||
|
||||
if (Manager.Painter)
|
||||
Manager.Painter.PaintingEnabled = false;
|
||||
|
||||
// The orbit rig would otherwise keep grabbing the cursor and reading the mouse every frame.
|
||||
if (Manager.CameraInput)
|
||||
Manager.CameraInput.enabled = false;
|
||||
SetPlayerControl(true);
|
||||
SetPainting(false);
|
||||
SetOrbitInputEnabled(false);
|
||||
|
||||
if (Manager.FirstPerson)
|
||||
Manager.FirstPerson.SetActive(true);
|
||||
@@ -38,9 +29,9 @@ public class FirstPersonMode : GameMode
|
||||
if (Manager.FirstPerson)
|
||||
Manager.FirstPerson.SetActive(false);
|
||||
|
||||
SetOrbitInputEnabled(true);
|
||||
if (Manager.CameraInput)
|
||||
{
|
||||
Manager.CameraInput.enabled = true;
|
||||
// Carry the first-person facing into the orbit camera so the next orbit mode (Paint / Player
|
||||
// Control) keeps the current view angle instead of snapping back to the orbit's last-used angle.
|
||||
// Camera.main still shows the first-person view this frame (the brain hasn't blended yet).
|
||||
@@ -94,7 +85,7 @@ public class FirstPersonMode : GameMode
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var netObj = _hits[i].collider.GetComponentInParent<NetworkObject>();
|
||||
if (netObj && self && netObj == self) continue; // ignore our own collider
|
||||
if (netObj && self && netObj == self) continue;
|
||||
if (!netObj || !netObj.IsPlayerObject) return; // first solid hit is world geometry -> blocked
|
||||
|
||||
var setup = netObj.GetComponent<NetworkPlayerSetup>();
|
||||
|
||||
@@ -57,12 +57,7 @@ public class FreeCamController : MonoBehaviour
|
||||
|
||||
CharacterController _controller;
|
||||
CinemachineCamera _vcam;
|
||||
float _yaw; // look target (raw input accumulates here)
|
||||
float _pitch;
|
||||
float _displayYaw; // smoothed value actually applied to the transform
|
||||
float _displayPitch;
|
||||
float _yawSmoothRef;
|
||||
float _pitchSmoothRef;
|
||||
SmoothedLook _look; // yaw/pitch accumulator + smoothing shared with first person
|
||||
Vector3 _moveVelocity; // smoothed world-space flight velocity
|
||||
Vector3 _moveVelocityRef;
|
||||
bool _ignoredPlayer;
|
||||
@@ -110,24 +105,10 @@ public class FreeCamController : MonoBehaviour
|
||||
void LookTick()
|
||||
{
|
||||
Vector2 delta = InputUtil.ReadMouseDelta() * MouseSensitivity.Multiplier;
|
||||
_yaw += delta.x * horizontalSensitivity;
|
||||
float vs = verticalSensitivity * (invertY ? 1f : -1f);
|
||||
_pitch = Mathf.Clamp(_pitch + delta.y * vs, -maxPitch, maxPitch);
|
||||
|
||||
if (lookSmoothTime > 0f)
|
||||
{
|
||||
// SmoothDampAngle is frame-rate-independent and wraps yaw correctly.
|
||||
_displayYaw = Mathf.SmoothDampAngle(_displayYaw, _yaw, ref _yawSmoothRef, lookSmoothTime);
|
||||
_displayPitch = Mathf.SmoothDampAngle(_displayPitch, _pitch, ref _pitchSmoothRef, lookSmoothTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayYaw = _yaw;
|
||||
_displayPitch = _pitch;
|
||||
}
|
||||
_look.Tick(delta, horizontalSensitivity, verticalSensitivity, invertY, maxPitch, lookSmoothTime);
|
||||
|
||||
// Rotation is unconstrained by the CharacterController; only position is.
|
||||
transform.rotation = Quaternion.Euler(_displayPitch, _displayYaw, 0f);
|
||||
transform.rotation = _look.Rotation;
|
||||
}
|
||||
|
||||
void MoveTick()
|
||||
@@ -153,22 +134,9 @@ public class FreeCamController : MonoBehaviour
|
||||
var cam = Camera.main;
|
||||
if (!cam) return;
|
||||
|
||||
// CharacterController forbids direct transform writes while enabled, so toggle it for the teleport.
|
||||
bool wasEnabled = _controller.enabled;
|
||||
_controller.enabled = false;
|
||||
transform.SetPositionAndRotation(cam.transform.position, cam.transform.rotation);
|
||||
_controller.enabled = wasEnabled;
|
||||
CharacterTeleport.Move(_controller, transform, cam.transform.position, cam.transform.rotation);
|
||||
|
||||
Vector3 e = transform.rotation.eulerAngles;
|
||||
_yaw = e.y;
|
||||
float p = e.x > 180f ? e.x - 360f : e.x;
|
||||
_pitch = Mathf.Clamp(p, -maxPitch, maxPitch);
|
||||
|
||||
// Snap the smoothed state to the fresh pose so entering freecam never lags/jumps to catch up.
|
||||
_displayYaw = _yaw;
|
||||
_displayPitch = _pitch;
|
||||
_yawSmoothRef = 0f;
|
||||
_pitchSmoothRef = 0f;
|
||||
_look.SyncTo(transform.rotation, maxPitch);
|
||||
_moveVelocity = Vector3.zero;
|
||||
_moveVelocityRef = Vector3.zero;
|
||||
}
|
||||
|
||||
@@ -11,18 +11,9 @@ public class FreeCamMode : GameMode
|
||||
public override void Enter()
|
||||
{
|
||||
// Keep the player component alive (gravity/grounding) but reject input, like Paint mode.
|
||||
if (Manager.Player)
|
||||
{
|
||||
Manager.Player.enabled = true;
|
||||
Manager.Player.ControlEnabled = false;
|
||||
}
|
||||
|
||||
if (Manager.Painter)
|
||||
Manager.Painter.PaintingEnabled = false;
|
||||
|
||||
// The orbit rig would otherwise keep grabbing the cursor and reading the mouse every frame.
|
||||
if (Manager.CameraInput)
|
||||
Manager.CameraInput.enabled = false;
|
||||
SetPlayerControl(false);
|
||||
SetPainting(false);
|
||||
SetOrbitInputEnabled(false);
|
||||
|
||||
if (Manager.FreeCam)
|
||||
Manager.FreeCam.SetActive(true);
|
||||
@@ -35,7 +26,6 @@ public class FreeCamMode : GameMode
|
||||
if (Manager.FreeCam)
|
||||
Manager.FreeCam.SetActive(false);
|
||||
|
||||
if (Manager.CameraInput)
|
||||
Manager.CameraInput.enabled = true;
|
||||
SetOrbitInputEnabled(true);
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,29 @@ public abstract class GameMode
|
||||
|
||||
/// <summary>Called every frame while this mode is active.</summary>
|
||||
public virtual void Tick() { }
|
||||
|
||||
/// <summary>Enables the player component and sets whether it accepts movement input.</summary>
|
||||
protected void SetPlayerControl(bool controlEnabled)
|
||||
{
|
||||
if (Manager.Player)
|
||||
{
|
||||
Manager.Player.enabled = true;
|
||||
Manager.Player.ControlEnabled = controlEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Toggles painting on the mesh painter.</summary>
|
||||
protected void SetPainting(bool on)
|
||||
{
|
||||
if (Manager.Painter)
|
||||
Manager.Painter.PaintingEnabled = on;
|
||||
}
|
||||
|
||||
/// <summary>Enables/disables the orbit camera input component. Off while a dedicated camera rig is
|
||||
/// live: the orbit rig would otherwise keep grabbing the cursor and reading the mouse every frame.</summary>
|
||||
protected void SetOrbitInputEnabled(bool on)
|
||||
{
|
||||
if (Manager.CameraInput)
|
||||
Manager.CameraInput.enabled = on;
|
||||
}
|
||||
}
|
||||
@@ -145,14 +145,25 @@ public class GameModeManager : MonoBehaviour
|
||||
_paused = false;
|
||||
_resumeIndex = -1;
|
||||
|
||||
ClearPlayerBindings();
|
||||
TearDownActiveMode();
|
||||
BuildSpectatorModes();
|
||||
SwitchTo(0);
|
||||
}
|
||||
|
||||
/// <summary>Drops the player/painter bindings and notifies painter-following UI.</summary>
|
||||
private void ClearPlayerBindings()
|
||||
{
|
||||
player = null;
|
||||
painter = null;
|
||||
PainterChanged?.Invoke(null);
|
||||
}
|
||||
|
||||
/// <summary>Exits the active mode and marks no mode active.</summary>
|
||||
private void TearDownActiveMode()
|
||||
{
|
||||
ActiveMode?.Exit();
|
||||
_activeIndex = -1;
|
||||
BuildSpectatorModes();
|
||||
SwitchTo(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,8 +208,7 @@ public class GameModeManager : MonoBehaviour
|
||||
firstPerson.SetPlayer(localPlayer);
|
||||
|
||||
// Tear down the pre-spawn (player-less) mode and rebuild scoped to this avatar's role.
|
||||
ActiveMode?.Exit();
|
||||
_activeIndex = -1;
|
||||
TearDownActiveMode();
|
||||
|
||||
// Lobby avatar -> full set; match variant -> its role's modes. Scope is baked per prefab
|
||||
// variant, so it follows the spawned avatar with no replicated-phase timing race.
|
||||
@@ -224,12 +234,9 @@ public class GameModeManager : MonoBehaviour
|
||||
_paused = false;
|
||||
_resumeIndex = -1;
|
||||
|
||||
ActiveMode?.Exit();
|
||||
_activeIndex = -1;
|
||||
TearDownActiveMode();
|
||||
_modes.Clear();
|
||||
player = null;
|
||||
painter = null;
|
||||
PainterChanged?.Invoke(null);
|
||||
ClearPlayerBindings();
|
||||
|
||||
// The cycle is empty (no SwitchTo runs), so notify mode-following UI explicitly - otherwise the
|
||||
// paint HUD (hides only via ModeChanged) would stay open after a shutdown during PaintMode.
|
||||
@@ -308,7 +315,7 @@ public class GameModeManager : MonoBehaviour
|
||||
else if (painter) painter.PaintingEnabled = false; // degenerate paint-only set: at least stop painting
|
||||
}
|
||||
|
||||
if (WasSwitchPressed())
|
||||
if (InputUtil.GameplayKeyPressed(switchKey))
|
||||
{
|
||||
int next = (_activeIndex + 1) % _modes.Count;
|
||||
if (suppressPaint && _modes[next] is PaintMode)
|
||||
@@ -355,14 +362,4 @@ public class GameModeManager : MonoBehaviour
|
||||
Debug.Log($"[GameModeManager] Mode -> {ActiveMode.DisplayName}", this);
|
||||
ModeChanged?.Invoke(ActiveMode);
|
||||
}
|
||||
|
||||
private bool WasSwitchPressed()
|
||||
{
|
||||
if (TypingGuard.IsTyping) return false;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Keyboard.current != null && Keyboard.current[switchKey].wasPressedThisFrame;
|
||||
#else
|
||||
return Input.GetKeyDown(switchKey);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>Shared mesh/bounds geometry helpers.</summary>
|
||||
public static class GeometryUtil
|
||||
{
|
||||
/// <summary>Divides a world scale back out of a freshly baked mesh so it is true unit-scale local
|
||||
/// geometry (BakeMesh freezes the transform's lossyScale into the vertices). No-op at scale 1;
|
||||
/// <paramref name="scratch"/> is reused so nothing is allocated per bake.</summary>
|
||||
public static void RescaleToUnitScale(Mesh baked, Vector3 lossyScale, List<Vector3> scratch)
|
||||
{
|
||||
if (lossyScale.x == 1f && lossyScale.y == 1f && lossyScale.z == 1f) return;
|
||||
|
||||
float ix = lossyScale.x != 0f ? 1f / lossyScale.x : 0f;
|
||||
float iy = lossyScale.y != 0f ? 1f / lossyScale.y : 0f;
|
||||
float iz = lossyScale.z != 0f ? 1f / lossyScale.z : 0f;
|
||||
baked.GetVertices(scratch);
|
||||
for (int i = 0; i < scratch.Count; i++)
|
||||
{
|
||||
Vector3 v = scratch[i];
|
||||
v.x *= ix; v.y *= iy; v.z *= iz;
|
||||
scratch[i] = v;
|
||||
}
|
||||
baked.SetVertices(scratch);
|
||||
baked.RecalculateBounds();
|
||||
}
|
||||
|
||||
/// <summary>World AABB enclosing a local AABB after an affine transform (encapsulates the 8 transformed corners).</summary>
|
||||
public static Bounds TransformBounds(Matrix4x4 m, Bounds local)
|
||||
{
|
||||
Vector3 mn = local.min, mx = local.max;
|
||||
Vector3 c0 = m.MultiplyPoint3x4(mn);
|
||||
Bounds b = new Bounds(c0, Vector3.zero);
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mn.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mx.y, mn.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mx.x, mn.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(new Vector3(mn.x, mx.y, mx.z)));
|
||||
b.Encapsulate(m.MultiplyPoint3x4(mx));
|
||||
return b;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: deab7e342bb598049b9972454a8399e5
|
||||
@@ -110,11 +110,7 @@ public class HsvColorPicker : MonoBehaviour
|
||||
|
||||
_svHandle = sv.Find("Handle") as RectTransform;
|
||||
|
||||
if (sv.TryGetComponent(out UiPointerSurface svPointer))
|
||||
{
|
||||
svPointer.Picked -= OnSvPicked;
|
||||
svPointer.Picked += OnSvPicked;
|
||||
}
|
||||
Rehook(sv, OnSvPicked);
|
||||
}
|
||||
|
||||
var hue = transform.Find("Hue") as RectTransform;
|
||||
@@ -123,11 +119,7 @@ public class HsvColorPicker : MonoBehaviour
|
||||
if (hue.TryGetComponent(out Image hueImg)) hueImg.sprite = PaintHud.HueSprite();
|
||||
_hueHandle = hue.Find("Handle") as RectTransform;
|
||||
|
||||
if (hue.TryGetComponent(out UiPointerSurface huePointer))
|
||||
{
|
||||
huePointer.Picked -= OnHuePicked;
|
||||
huePointer.Picked += OnHuePicked;
|
||||
}
|
||||
Rehook(hue, OnHuePicked);
|
||||
}
|
||||
|
||||
// Alpha now lives on a standalone slider (PaintHud), not in the picker. Hide any alpha bar a
|
||||
@@ -141,6 +133,16 @@ public class HsvColorPicker : MonoBehaviour
|
||||
Refresh();
|
||||
}
|
||||
|
||||
// Idempotently hooks the handler to the surface's drag relay (unsubscribe-then-subscribe).
|
||||
private static void Rehook(Component surface, Action<Vector2> handler)
|
||||
{
|
||||
if (surface.TryGetComponent(out UiPointerSurface pointer))
|
||||
{
|
||||
pointer.Picked -= handler;
|
||||
pointer.Picked += handler;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSvPicked(Vector2 p)
|
||||
{
|
||||
_s = p.x;
|
||||
|
||||
@@ -104,4 +104,21 @@ public static class InputUtil
|
||||
return Input.GetMouseButtonDown(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Gameplay key pressed on this exact frame (edge), suppressed while a UI text field is
|
||||
/// focused so typing never triggers gameplay bindings. The parameter type follows the active input
|
||||
/// backend, matching how the callers' serialized key fields are declared.</summary>
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
public static bool GameplayKeyPressed(Key key)
|
||||
{
|
||||
if (TypingGuard.IsTyping) return false;
|
||||
return Keyboard.current != null && Keyboard.current[key].wasPressedThisFrame;
|
||||
}
|
||||
#else
|
||||
public static bool GameplayKeyPressed(KeyCode key)
|
||||
{
|
||||
if (TypingGuard.IsTyping) return false;
|
||||
return Input.GetKeyDown(key);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+13
-19
@@ -82,25 +82,19 @@ public class MatchHud : MonoBehaviour
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_phaseStyle == null)
|
||||
if (_phaseStyle == null) _phaseStyle = MakeCenteredBoldLabel(22);
|
||||
if (_timeStyle == null) _timeStyle = MakeCenteredBoldLabel(28);
|
||||
}
|
||||
|
||||
private static GUIStyle MakeCenteredBoldLabel(int fontSize)
|
||||
{
|
||||
var style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
_phaseStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 22,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
_phaseStyle.normal.textColor = Color.white;
|
||||
}
|
||||
if (_timeStyle == null)
|
||||
{
|
||||
_timeStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 28,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
_timeStyle.normal.textColor = Color.white;
|
||||
}
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = fontSize,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
style.normal.textColor = Color.white;
|
||||
return style;
|
||||
}
|
||||
}
|
||||
@@ -233,20 +233,26 @@ public class MatchManager : MonoBehaviour
|
||||
_net.OnClientStarted -= OnClientStarted;
|
||||
_net.OnServerStopped -= OnNetStopped;
|
||||
_net.OnClientStopped -= OnNetStopped;
|
||||
_net.OnClientConnectedCallback -= OnClientConnected;
|
||||
_net.OnClientDisconnectCallback -= OnClientDisconnected;
|
||||
if (_net.SceneManager != null) _net.SceneManager.OnSceneEvent -= OnSceneEvent;
|
||||
if (_net.CustomMessagingManager != null)
|
||||
{
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgAuthorityGranted);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRequestEndMatch);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
|
||||
}
|
||||
UnhookSessionCallbacks();
|
||||
}
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
// Unhooks the per-session callbacks and named-message handlers (shared by OnDisable and OnNetStopped).
|
||||
private void UnhookSessionCallbacks()
|
||||
{
|
||||
_net.OnClientConnectedCallback -= OnClientConnected;
|
||||
_net.OnClientDisconnectCallback -= OnClientDisconnected;
|
||||
if (_net.SceneManager != null) _net.SceneManager.OnSceneEvent -= OnSceneEvent;
|
||||
if (_net.CustomMessagingManager != null)
|
||||
{
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgAuthorityGranted);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRequestEndMatch);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServerStarted()
|
||||
{
|
||||
_cleanedUp = false;
|
||||
@@ -313,19 +319,7 @@ public class MatchManager : MonoBehaviour
|
||||
Phase = MatchPhase.Lobby;
|
||||
ApplyPhaseLocal(MatchSubPhase.None, 0f, _localMode);
|
||||
_pendingHiders.Clear();
|
||||
if (_net)
|
||||
{
|
||||
_net.OnClientConnectedCallback -= OnClientConnected;
|
||||
_net.OnClientDisconnectCallback -= OnClientDisconnected;
|
||||
if (_net.SceneManager != null) _net.SceneManager.OnSceneEvent -= OnSceneEvent;
|
||||
if (_net.CustomMessagingManager != null)
|
||||
{
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgAuthorityGranted);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgRequestEndMatch);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgPhase);
|
||||
_net.CustomMessagingManager.UnregisterNamedMessageHandler(MsgReportHit);
|
||||
}
|
||||
}
|
||||
if (_net) UnhookSessionCallbacks();
|
||||
|
||||
ReturnToMainOnly();
|
||||
}
|
||||
@@ -463,15 +457,7 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
private IEnumerator MatchSequence(HashSet<ulong> hunterIds)
|
||||
{
|
||||
var sm = _net.SceneManager;
|
||||
|
||||
// Strictly serialized: NGO allows only one scene op in flight at a time.
|
||||
Scene lobby = SceneManager.GetSceneByName(lobbySceneName);
|
||||
if (lobby.IsValid() && lobby.isLoaded)
|
||||
yield return SceneOp(() => sm.UnloadScene(lobby), SceneEventType.UnloadEventCompleted, lobbySceneName);
|
||||
|
||||
yield return SceneOp(() => sm.LoadScene(matchSceneName, LoadSceneMode.Additive),
|
||||
SceneEventType.LoadEventCompleted, matchSceneName);
|
||||
yield return SwapScenes(lobbySceneName, matchSceneName);
|
||||
|
||||
RespawnAll(id => hunterIds.Contains(id) ? hunterPrefab : hiderPrefab, matchSpawnCenter);
|
||||
|
||||
@@ -500,7 +486,7 @@ public class MatchManager : MonoBehaviour
|
||||
yield return WaitPhase(resolveDuration, null);
|
||||
|
||||
_phaseRoutine = null;
|
||||
EndMatch(); // swaps the map back to the lobby and respawns everyone as the neutral Player
|
||||
EndMatch();
|
||||
}
|
||||
|
||||
// Waits up to 'seconds' of server real time, returning early the moment 'until' (if supplied) is true.
|
||||
@@ -691,15 +677,7 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
private IEnumerator EndMatchSequence()
|
||||
{
|
||||
var sm = _net.SceneManager;
|
||||
|
||||
// Strictly serialized: NGO allows only one scene op in flight at a time.
|
||||
Scene map = SceneManager.GetSceneByName(matchSceneName);
|
||||
if (map.IsValid() && map.isLoaded)
|
||||
yield return SceneOp(() => sm.UnloadScene(map), SceneEventType.UnloadEventCompleted, matchSceneName);
|
||||
|
||||
yield return SceneOp(() => sm.LoadScene(lobbySceneName, LoadSceneMode.Additive),
|
||||
SceneEventType.LoadEventCompleted, lobbySceneName);
|
||||
yield return SwapScenes(matchSceneName, lobbySceneName);
|
||||
|
||||
RespawnAll(_ => _playerPrefab, lobbySpawnCenter);
|
||||
|
||||
@@ -707,6 +685,20 @@ public class MatchManager : MonoBehaviour
|
||||
Debug.Log("[MatchManager] Back in lobby.", this);
|
||||
}
|
||||
|
||||
// Unloads one gameplay scene (if loaded), then additively loads the next (shared by the match
|
||||
// start/end sequences).
|
||||
private IEnumerator SwapScenes(string unloadSceneName, string loadSceneName)
|
||||
{
|
||||
var sm = _net.SceneManager;
|
||||
|
||||
Scene old = SceneManager.GetSceneByName(unloadSceneName);
|
||||
if (old.IsValid() && old.isLoaded)
|
||||
yield return SceneOp(() => sm.UnloadScene(old), SceneEventType.UnloadEventCompleted, unloadSceneName);
|
||||
|
||||
yield return SceneOp(() => sm.LoadScene(loadSceneName, LoadSceneMode.Additive),
|
||||
SceneEventType.LoadEventCompleted, loadSceneName);
|
||||
}
|
||||
|
||||
// Runs an NGO scene op and waits for its server-side completion event. NGO rejects a new op while one
|
||||
// is in flight, so callers must await this before issuing the next.
|
||||
private IEnumerator SceneOp(System.Func<SceneEventProgressStatus> op, SceneEventType completed, string sceneName)
|
||||
@@ -823,18 +815,22 @@ public class MatchManager : MonoBehaviour
|
||||
public bool IsLocalSceneLobby => SceneManager.GetActiveScene().name == lobbySceneName;
|
||||
public bool IsLocalSceneMap => SceneManager.GetActiveScene().name == matchSceneName;
|
||||
|
||||
// Server -> client: tell the designated client it is the match authority (so it reads Esc locally).
|
||||
private void SendAuthorityGrant(ulong clientId)
|
||||
// Sends a named message whose payload carries no information (a named message still needs a buffer,
|
||||
// so a single dummy byte is written).
|
||||
private void SendEmptyNamedMessage(string messageName, ulong targetClientId)
|
||||
{
|
||||
var cmm = _net != null ? _net.CustomMessagingManager : null;
|
||||
if (cmm == null) return;
|
||||
using (var writer = new FastBufferWriter(sizeof(byte), Allocator.Temp))
|
||||
{
|
||||
writer.WriteByteSafe(1); // payload unused; a named message still needs a buffer
|
||||
cmm.SendNamedMessage(MsgAuthorityGranted, clientId, writer);
|
||||
writer.WriteByteSafe(1);
|
||||
cmm.SendNamedMessage(messageName, targetClientId, writer);
|
||||
}
|
||||
}
|
||||
|
||||
// Server -> client: tell the designated client it is the match authority (so it reads Esc locally).
|
||||
private void SendAuthorityGrant(ulong clientId) => SendEmptyNamedMessage(MsgAuthorityGranted, clientId);
|
||||
|
||||
// Client handler: this machine has been granted match authority.
|
||||
private void OnAuthorityGrantedMessage(ulong senderClientId, FastBufferReader reader)
|
||||
{
|
||||
@@ -844,16 +840,7 @@ public class MatchManager : MonoBehaviour
|
||||
|
||||
// Client -> server: ask the server to end the match. Only honored from the authority client (and only
|
||||
// while a match is running - EndMatch guards the phase).
|
||||
private void RequestEndMatchFromServer()
|
||||
{
|
||||
var cmm = _net != null ? _net.CustomMessagingManager : null;
|
||||
if (cmm == null) return;
|
||||
using (var writer = new FastBufferWriter(sizeof(byte), Allocator.Temp))
|
||||
{
|
||||
writer.WriteByteSafe(1);
|
||||
cmm.SendNamedMessage(MsgRequestEndMatch, NetworkManager.ServerClientId, writer);
|
||||
}
|
||||
}
|
||||
private void RequestEndMatchFromServer() => SendEmptyNamedMessage(MsgRequestEndMatch, NetworkManager.ServerClientId);
|
||||
|
||||
// Server handler: an authority client asked to end the match.
|
||||
private void OnRequestEndMatchMessage(ulong senderClientId, FastBufferReader reader)
|
||||
|
||||
@@ -26,11 +26,7 @@ public partial class MeshPainter
|
||||
var canonical = new Color32[MAP_COUNT][];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++) canonical[mi] = ReadCanonical(mi);
|
||||
|
||||
if (_gpuActive) TeardownGpu();
|
||||
else TeardownCpu();
|
||||
|
||||
SetupBackend(toGpu, canonical);
|
||||
_hasLastScreen = false;
|
||||
RebuildBackend(toGpu, canonical);
|
||||
|
||||
Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this);
|
||||
}
|
||||
@@ -40,14 +36,44 @@ public partial class MeshPainter
|
||||
/// late-join snapshot / undo). Runs the island/dilation seeding path so edge padding is rebuilt for
|
||||
/// the new content. The input arrays are never mutated (SetupBackend clones its seed).
|
||||
/// </summary>
|
||||
private void ReseedCanonical(Color32[][] canonical)
|
||||
private void ReseedCanonical(Color32[][] canonical) => RebuildBackend(useGpu, canonical);
|
||||
|
||||
/// <summary>Tears down the active backend and sets up the requested one on the given maps.</summary>
|
||||
private void RebuildBackend(bool gpu, Color32[][] canonical)
|
||||
{
|
||||
if (_gpuActive) TeardownGpu();
|
||||
else TeardownCpu();
|
||||
SetupBackend(useGpu, canonical);
|
||||
SetupBackend(gpu, canonical);
|
||||
_hasLastScreen = false;
|
||||
}
|
||||
|
||||
/// <summary>Seed source for a map: the supplied canonical data when valid, else the material seed.</summary>
|
||||
private Color32[] ResolveSeed(int mi, Color32[][] canonical)
|
||||
{
|
||||
return (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize)
|
||||
? canonical[mi] : _seedCanonical[mi];
|
||||
}
|
||||
|
||||
// Backend-agnostic stroke dispatch (GPU vs CPU), shared by local painting and replayed net stamps.
|
||||
|
||||
private void BeginBackendStroke()
|
||||
{
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else ResetCpuDirty();
|
||||
}
|
||||
|
||||
private void DrawBackendStamp(Vector3 pLocal, float radiusLocal, Vector3 u, Vector3 v, Vector3 n)
|
||||
{
|
||||
if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, n);
|
||||
else PaintStamp3D(pLocal, radiusLocal, u, v, n);
|
||||
}
|
||||
|
||||
private void EndBackendStroke()
|
||||
{
|
||||
if (_gpuActive) EndGpuStroke();
|
||||
else EndCpuStroke();
|
||||
}
|
||||
|
||||
/// <summary>Reads a map from the active backend into canonical (material) layout.</summary>
|
||||
private Color32[] ReadCanonical(int mi)
|
||||
{
|
||||
|
||||
@@ -34,8 +34,7 @@ public partial class MeshPainter
|
||||
/// <summary>CPU pixels are stored directly in material layout; just guarantee a sane alpha for the color maps.</summary>
|
||||
private Color32[] SeedCpuPixels(int mi, Color32[][] canonical)
|
||||
{
|
||||
var src = (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize)
|
||||
? canonical[mi] : _seedCanonical[mi];
|
||||
var src = ResolveSeed(mi, canonical);
|
||||
var px = (Color32[])src.Clone();
|
||||
if (mi != MAP_MSO) // albedo / emission: opaque. The mask map keeps alpha = smoothness.
|
||||
for (int i = 0; i < px.Length; i++) px[i].a = 255;
|
||||
|
||||
@@ -71,8 +71,7 @@ public partial class MeshPainter
|
||||
/// <summary>Seeds a GPU map's display RT from canonical data (coverage alpha = UV islands), then packs MS to final.</summary>
|
||||
private void SeedGpuMap(int mi, Color32[][] canonical)
|
||||
{
|
||||
var src = (canonical != null && canonical[mi] != null && canonical[mi].Length == textureSize * textureSize)
|
||||
? canonical[mi] : _seedCanonical[mi];
|
||||
var src = ResolveSeed(mi, canonical);
|
||||
var gm = _gpu[mi];
|
||||
|
||||
if (mi == MAP_MSO)
|
||||
@@ -108,8 +107,7 @@ public partial class MeshPainter
|
||||
Graphics.SetRenderTarget(rt);
|
||||
if (_brushMat.SetPass(1))
|
||||
{
|
||||
int sub = _bakedMesh.subMeshCount;
|
||||
for (int i = 0; i < sub; i++) Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i);
|
||||
DrawBakedMeshNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -258,6 +256,12 @@ public partial class MeshPainter
|
||||
private void DrawStamp()
|
||||
{
|
||||
if (!_brushMat.SetPass(0)) return;
|
||||
DrawBakedMeshNow();
|
||||
}
|
||||
|
||||
/// <summary>Draws the baked mesh once per sub-mesh with the pass the caller just set.</summary>
|
||||
private void DrawBakedMeshNow()
|
||||
{
|
||||
int sub = _bakedMesh.subMeshCount;
|
||||
for (int i = 0; i < sub; i++)
|
||||
Graphics.DrawMeshNow(_bakedMesh, Matrix4x4.identity, i);
|
||||
|
||||
@@ -139,18 +139,9 @@ public partial class MeshPainter
|
||||
if (spray) BuildSprayStampMaskSeeded(s.SpraySeed);
|
||||
else { ResolveActiveMask(); UseResolvedStampMask(); }
|
||||
|
||||
if (_gpuActive)
|
||||
{
|
||||
BeginGpuStroke();
|
||||
DrawBrushGpu(s.P, s.R, s.U, s.V, s.N);
|
||||
EndGpuStroke();
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetCpuDirty();
|
||||
PaintStamp3D(s.P, s.R, s.U, s.V, s.N);
|
||||
EndCpuStroke();
|
||||
}
|
||||
BeginBackendStroke();
|
||||
DrawBackendStamp(s.P, s.R, s.U, s.V, s.N);
|
||||
EndBackendStroke();
|
||||
|
||||
// Accumulate this peer's stroke dirty mask so a network checkpoint marker can commit the same
|
||||
// history entry as the owner (same stamps -> same touched maps -> COW-aligned with the owner).
|
||||
@@ -189,21 +180,7 @@ public partial class MeshPainter
|
||||
w.Write(textureSize);
|
||||
w.Write(MAP_COUNT);
|
||||
|
||||
var bytes = new byte[n * 4];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
Color32[] px = ReadCanonical(mi);
|
||||
int count = Mathf.Min(n, px.Length);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int o = i * 4;
|
||||
bytes[o] = px[i].r;
|
||||
bytes[o + 1] = px[i].g;
|
||||
bytes[o + 2] = px[i].b;
|
||||
bytes[o + 3] = px[i].a;
|
||||
}
|
||||
w.Write(bytes, 0, count * 4);
|
||||
}
|
||||
WriteCanonicalMaps(w);
|
||||
}
|
||||
compressed = Deflate(raw.ToArray());
|
||||
}
|
||||
@@ -217,19 +194,8 @@ public partial class MeshPainter
|
||||
/// </summary>
|
||||
public void ApplyPaintSnapshot(byte[] compressed, int baselineId)
|
||||
{
|
||||
if (!_setupValid || compressed == null || compressed.Length == 0) return;
|
||||
if (!TryDecodeSnapshot(compressed, "Paint", out Color32[][] canonical)) return;
|
||||
|
||||
Color32[][] canonical;
|
||||
try { canonical = DecodeSnapshot(compressed); }
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[MeshPainter] Paint snapshot decode failed: {e.Message}", this);
|
||||
return;
|
||||
}
|
||||
if (canonical == null) return;
|
||||
|
||||
// Re-seed exactly like SwitchBackend, but with externally supplied maps, so the island/dilation
|
||||
// seeding path runs and the edge padding is rebuilt correctly for the loaded content.
|
||||
ReseedCanonical(canonical);
|
||||
SetHistoryBaseline(canonical, baselineId);
|
||||
}
|
||||
@@ -242,22 +208,28 @@ public partial class MeshPainter
|
||||
/// </summary>
|
||||
public bool LoadPaintFromSnapshot(byte[] compressed)
|
||||
{
|
||||
if (!_setupValid || compressed == null || compressed.Length == 0) return false;
|
||||
|
||||
Color32[][] canonical;
|
||||
try { canonical = DecodeSnapshot(compressed); }
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[MeshPainter] Carry snapshot decode failed: {e.Message}", this);
|
||||
return false;
|
||||
}
|
||||
if (canonical == null) return false;
|
||||
if (!TryDecodeSnapshot(compressed, "Carry", out Color32[][] canonical)) return false;
|
||||
|
||||
ReseedCanonical(canonical);
|
||||
ResetHistoryToCurrent(canonical);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Guard + decode for an incoming snapshot blob; false = not ready / empty / undecodable.</summary>
|
||||
private bool TryDecodeSnapshot(byte[] compressed, string context, out Color32[][] canonical)
|
||||
{
|
||||
canonical = null;
|
||||
if (!_setupValid || compressed == null || compressed.Length == 0) return false;
|
||||
|
||||
try { canonical = DecodeSnapshot(compressed); }
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[MeshPainter] {context} snapshot decode failed: {e.Message}", this);
|
||||
return false;
|
||||
}
|
||||
return canonical != null;
|
||||
}
|
||||
|
||||
private Color32[][] DecodeSnapshot(byte[] compressed)
|
||||
{
|
||||
byte[] raw = Inflate(compressed);
|
||||
@@ -272,22 +244,7 @@ public partial class MeshPainter
|
||||
// Resolution / version mismatch: ignore rather than smear; the painter keeps its state.
|
||||
if (ver != SnapshotVersion || size != textureSize) return null;
|
||||
|
||||
int n = textureSize * textureSize;
|
||||
var result = new Color32[MAP_COUNT][];
|
||||
int load = Mathf.Min(maps, MAP_COUNT);
|
||||
for (int mi = 0; mi < load; mi++)
|
||||
{
|
||||
byte[] b = r.ReadBytes(n * 4);
|
||||
if (b.Length != n * 4) break; // truncated -> remaining maps stay at the material seed
|
||||
var px = new Color32[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int o = i * 4;
|
||||
px[i] = new Color32(b[o], b[o + 1], b[o + 2], b[o + 3]);
|
||||
}
|
||||
result[mi] = px;
|
||||
}
|
||||
return result;
|
||||
return ReadCanonicalMaps(r, maps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,13 +36,16 @@ public partial class MeshPainter
|
||||
private static string PaintLibraryDir =>
|
||||
Path.Combine(Application.persistentDataPath, "MeshPaint", "Saved");
|
||||
|
||||
private static string SanitizeKey(string s)
|
||||
/// <summary>Filesystem-safe key: invalid filename characters and spaces become underscores.</summary>
|
||||
internal static string SanitizeFileKey(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return "unnamed";
|
||||
foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_');
|
||||
return s.Replace(' ', '_');
|
||||
}
|
||||
|
||||
private static string SanitizeKey(string s) =>
|
||||
string.IsNullOrEmpty(s) ? "unnamed" : SanitizeFileKey(s);
|
||||
|
||||
/// <summary>Disk path for a slot. The display name lives inside the file; the file name is a sanitized key.</summary>
|
||||
private static string PathForName(string name) =>
|
||||
Path.Combine(PaintLibraryDir, SanitizeKey(name) + PaintFileExt);
|
||||
@@ -86,7 +89,6 @@ public partial class MeshPainter
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(PaintLibraryDir);
|
||||
int n = textureSize * textureSize;
|
||||
|
||||
using (var fs = new FileStream(PathForName(name), FileMode.Create, FileAccess.Write))
|
||||
using (var w = new BinaryWriter(fs))
|
||||
@@ -111,21 +113,7 @@ public partial class MeshPainter
|
||||
w.Write(HeightNormalIntensity);
|
||||
w.Write(ParallaxIntensity);
|
||||
|
||||
var bytes = new byte[n * 4];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
Color32[] px = ReadCanonical(mi);
|
||||
int count = Mathf.Min(n, px.Length);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int o = i * 4;
|
||||
bytes[o] = px[i].r;
|
||||
bytes[o + 1] = px[i].g;
|
||||
bytes[o + 2] = px[i].b;
|
||||
bytes[o + 3] = px[i].a;
|
||||
}
|
||||
w.Write(bytes, 0, count * 4);
|
||||
}
|
||||
WriteCanonicalMaps(w);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -147,8 +135,6 @@ public partial class MeshPainter
|
||||
Color32[][] canonical = ReadMaps(PathForName(name), out PaintScalars scalars);
|
||||
if (canonical == null) return false;
|
||||
|
||||
// Re-seed exactly like SwitchBackend / ApplyPaintSnapshot, so the island/dilation seeding path
|
||||
// runs and the edge padding is rebuilt correctly for the loaded content.
|
||||
ReseedCanonical(canonical);
|
||||
|
||||
// A load replaces the whole painting and is NOT undoable: the loaded maps become the new history
|
||||
@@ -202,9 +188,7 @@ public partial class MeshPainter
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
|
||||
using (var r = new BinaryReader(fs, Encoding.UTF8))
|
||||
{
|
||||
if (r.ReadUInt32() != PaintFileMagic) return null;
|
||||
int ver = r.ReadInt32();
|
||||
if (ver < 2 || ver > PaintFileVersion) return null; // list legacy v2..v3 slots + current
|
||||
if (!TryReadPaintVersion(r, out _)) return null;
|
||||
return r.ReadString();
|
||||
}
|
||||
}
|
||||
@@ -228,9 +212,7 @@ public partial class MeshPainter
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
|
||||
using (var r = new BinaryReader(fs, Encoding.UTF8))
|
||||
{
|
||||
if (r.ReadUInt32() != PaintFileMagic) return null;
|
||||
int ver = r.ReadInt32();
|
||||
if (ver < 2 || ver > PaintFileVersion) return null; // read legacy v2..v3 + current
|
||||
if (!TryReadPaintVersion(r, out int ver)) return null;
|
||||
r.ReadString(); // display name (header only; not needed for the pixel load)
|
||||
int w = r.ReadInt32();
|
||||
int h = r.ReadInt32();
|
||||
@@ -264,23 +246,7 @@ public partial class MeshPainter
|
||||
return null;
|
||||
}
|
||||
|
||||
int n = textureSize * textureSize;
|
||||
var result = new Color32[MAP_COUNT][];
|
||||
int load = Mathf.Min(maps, MAP_COUNT);
|
||||
for (int mi = 0; mi < load; mi++)
|
||||
{
|
||||
byte[] raw = r.ReadBytes(n * 4);
|
||||
if (raw.Length != n * 4) break; // truncated file -> remaining maps stay seeded
|
||||
|
||||
var px = new Color32[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int o = i * 4;
|
||||
px[i] = new Color32(raw[o], raw[o + 1], raw[o + 2], raw[o + 3]);
|
||||
}
|
||||
result[mi] = px;
|
||||
}
|
||||
return result;
|
||||
return ReadCanonicalMaps(r, maps);
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
@@ -289,4 +255,55 @@ public partial class MeshPainter
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Validates the paint-file magic and version range (legacy v2 .. current).</summary>
|
||||
private static bool TryReadPaintVersion(BinaryReader r, out int ver)
|
||||
{
|
||||
if (r.ReadUInt32() != PaintFileMagic) { ver = 0; return false; }
|
||||
ver = r.ReadInt32();
|
||||
return ver >= 2 && ver <= PaintFileVersion;
|
||||
}
|
||||
|
||||
/// <summary>Writes every map's canonical Color32 bytes to the stream (shared by slot save + net snapshot).</summary>
|
||||
private void WriteCanonicalMaps(BinaryWriter w)
|
||||
{
|
||||
int n = textureSize * textureSize;
|
||||
var bytes = new byte[n * 4];
|
||||
for (int mi = 0; mi < MAP_COUNT; mi++)
|
||||
{
|
||||
Color32[] px = ReadCanonical(mi);
|
||||
int count = Mathf.Min(n, px.Length);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int o = i * 4;
|
||||
bytes[o] = px[i].r;
|
||||
bytes[o + 1] = px[i].g;
|
||||
bytes[o + 2] = px[i].b;
|
||||
bytes[o + 3] = px[i].a;
|
||||
}
|
||||
w.Write(bytes, 0, count * 4);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads up to MAP_COUNT canonical map blocks; a truncated stream leaves the remaining maps null (seeded).</summary>
|
||||
private Color32[][] ReadCanonicalMaps(BinaryReader r, int mapsInFile)
|
||||
{
|
||||
int n = textureSize * textureSize;
|
||||
var result = new Color32[MAP_COUNT][];
|
||||
int load = Mathf.Min(mapsInFile, MAP_COUNT);
|
||||
for (int mi = 0; mi < load; mi++)
|
||||
{
|
||||
byte[] raw = r.ReadBytes(n * 4);
|
||||
if (raw.Length != n * 4) break; // truncated -> remaining maps stay at the material seed
|
||||
|
||||
var px = new Color32[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int o = i * 4;
|
||||
px[i] = new Color32(raw[o], raw[o + 1], raw[o + 2], raw[o + 3]);
|
||||
}
|
||||
result[mi] = px;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,14 @@ public partial class MeshPainter
|
||||
Picked?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Adopts a sampled colour as the brush albedo, preserving the brush's paint opacity (alpha).</summary>
|
||||
private void AdoptAlbedo(Color c) =>
|
||||
brushColor = new Color(Mathf.Clamp01(c.r), Mathf.Clamp01(c.g), Mathf.Clamp01(c.b), brushColor.a);
|
||||
|
||||
/// <summary>Zeroes the emission colour, preserving its paint opacity (alpha).</summary>
|
||||
private void ClearEmissionRgb() =>
|
||||
emissionColor = new Color(0f, 0f, 0f, emissionColor.a);
|
||||
|
||||
/// <summary>
|
||||
/// Picks from our own painted maps at the hit texel (bit-exact, backend-agnostic).
|
||||
/// </summary>
|
||||
@@ -149,7 +157,7 @@ public partial class MeshPainter
|
||||
{
|
||||
// No camera (headless) -> keep the material's raw albedo rather than failing.
|
||||
if (pipetteAlbedoFromScreen && TrySampleScreenColor(screenPos, out Color lit))
|
||||
brushColor = new Color(Mathf.Clamp01(lit.r), Mathf.Clamp01(lit.g), Mathf.Clamp01(lit.b), brushColor.a);
|
||||
AdoptAlbedo(lit);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -204,7 +212,7 @@ public partial class MeshPainter
|
||||
Color albedo = baseCol;
|
||||
if (hasUv && baseTex)
|
||||
albedo = SampleTexel(baseTex, ApplyST(mat, "_BaseMap", "_MainTex", uv), true) * baseCol;
|
||||
brushColor = new Color(Mathf.Clamp01(albedo.r), Mathf.Clamp01(albedo.g), Mathf.Clamp01(albedo.b), brushColor.a);
|
||||
AdoptAlbedo(albedo);
|
||||
|
||||
// --- Metallic / Smoothness: MetallicGloss map (linear) ---
|
||||
// URP/Lit: metallic = map.r ; smoothness = map.a * _Smoothness (or _Metallic/_Glossiness scalars).
|
||||
@@ -307,10 +315,10 @@ public partial class MeshPainter
|
||||
if (weightSum <= 1e-4f) return false;
|
||||
float inv = 1f / weightSum; // alphamap weights normally sum to 1, but guard anyway
|
||||
|
||||
brushColor = new Color(Mathf.Clamp01(albedo.r * inv), Mathf.Clamp01(albedo.g * inv), Mathf.Clamp01(albedo.b * inv), brushColor.a);
|
||||
AdoptAlbedo(albedo * inv);
|
||||
metallic = Mathf.Clamp01(metallicVal * inv);
|
||||
smoothness = Mathf.Clamp01(smoothVal * inv);
|
||||
emissionColor = new Color(0f, 0f, 0f, emissionColor.a); // occlusion left untouched (now painted height)
|
||||
ClearEmissionRgb(); // occlusion left untouched (now painted height)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -344,10 +352,10 @@ public partial class MeshPainter
|
||||
{
|
||||
if (!TrySampleScreenColor(screenPos, out Color c)) return false;
|
||||
|
||||
brushColor = new Color(Mathf.Clamp01(c.r), Mathf.Clamp01(c.g), Mathf.Clamp01(c.b), brushColor.a);
|
||||
AdoptAlbedo(c);
|
||||
metallic = 0f;
|
||||
smoothness = 0f;
|
||||
emissionColor = new Color(0f, 0f, 0f, emissionColor.a); // occlusion left untouched (now painted height)
|
||||
ClearEmissionRgb(); // occlusion left untouched (now painted height)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -176,8 +176,6 @@ public partial class MeshPainter
|
||||
/// <summary>Builds the albedo base-color texels from the existing material texture or baseColor.</summary>
|
||||
private Color32[] BuildBaseColors()
|
||||
{
|
||||
var px = new Color32[textureSize * textureSize];
|
||||
|
||||
Texture sourceTex = null;
|
||||
var shared = _renderer.sharedMaterial;
|
||||
if (shared)
|
||||
@@ -197,9 +195,7 @@ public partial class MeshPainter
|
||||
catch { /* not readable -> fall back to baseColor */ }
|
||||
}
|
||||
|
||||
Color32 c = baseColor;
|
||||
for (int i = 0; i < px.Length; i++) px[i] = c;
|
||||
return px;
|
||||
return FillCanonical(baseColor);
|
||||
}
|
||||
|
||||
/// <summary>Enables the URP keywords for the painted PBR maps and seeds the material scalars.</summary>
|
||||
|
||||
@@ -143,13 +143,7 @@ public partial class MeshPainter
|
||||
{
|
||||
if (_sprayTex) return;
|
||||
|
||||
_sprayTex = new Texture2D(SprayRes, SprayRes, TextureFormat.RGBA32, false, true) // linear: raw mask values
|
||||
{
|
||||
name = "MeshPainter_SprayMask",
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
_sprayTex = NewMaskTexture(SprayRes, "MeshPainter_SprayMask");
|
||||
_sprayPixels = new Color32[SprayRes * SprayRes];
|
||||
_sprayOpacity = new float[SprayRes * SprayRes];
|
||||
}
|
||||
|
||||
@@ -209,10 +209,17 @@ public partial class MeshPainter
|
||||
|
||||
private void RestoreCurrentState()
|
||||
{
|
||||
ReseedCanonical(_undoStates[_undoIndex]);
|
||||
RestoreEntry(_undoIndex);
|
||||
OnHistoryRestore?.Invoke(_undoIds[_undoIndex]); // owner -> peers reproduce the move by id
|
||||
}
|
||||
|
||||
/// <summary>Shows history entry <paramref name="idx"/>: re-seeds the backend and clears any in-progress stroke.</summary>
|
||||
private void RestoreEntry(int idx)
|
||||
{
|
||||
_undoIndex = idx;
|
||||
ReseedCanonical(_undoStates[idx]);
|
||||
_strokeActive = false;
|
||||
_strokeDirtyMaps = 0;
|
||||
OnHistoryRestore?.Invoke(_undoIds[_undoIndex]); // owner -> peers reproduce the move by id
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -225,10 +232,7 @@ public partial class MeshPainter
|
||||
int idx = _undoIds.IndexOf(id);
|
||||
if (idx < 0) return false;
|
||||
|
||||
_undoIndex = idx;
|
||||
ReseedCanonical(_undoStates[idx]);
|
||||
_strokeActive = false;
|
||||
_strokeDirtyMaps = 0;
|
||||
RestoreEntry(idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,20 @@ public partial class MeshPainter : MonoBehaviour
|
||||
set => SetMatFloat(ParallaxId, Mathf.Max(0f, value) * ParallaxBase);
|
||||
}
|
||||
|
||||
/// <summary>The three camo knobs as one vector (x = half-Lambert, y = its brightness, z = specular dampen).</summary>
|
||||
public Vector3 CamoKnobs
|
||||
{
|
||||
get => new(CamoHalfLambert, CamoHalfLambertBrightness, CamoSpecularDampen);
|
||||
set { CamoHalfLambert = value.x; CamoHalfLambertBrightness = value.y; CamoSpecularDampen = value.z; }
|
||||
}
|
||||
|
||||
/// <summary>The three painted-height knobs as one vector (x = occlusion threshold, y = relief normal intensity, z = parallax intensity).</summary>
|
||||
public Vector3 HeightKnobs
|
||||
{
|
||||
get => new(OcclusionThreshold, HeightNormalIntensity, ParallaxIntensity);
|
||||
set { OcclusionThreshold = value.x; HeightNormalIntensity = value.y; ParallaxIntensity = value.z; }
|
||||
}
|
||||
|
||||
/// <summary>Drives the see-through-walls X-ray overlay: sets the TARGET for the alpha of the authored
|
||||
/// <c>_XRayColor</c> (the rgb glow colour is preserved); Update eases the material alpha toward it
|
||||
/// over <see cref="xrayFadeDuration"/> seconds. A per-avatar runtime reveal, NOT a painted channel
|
||||
@@ -438,13 +452,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
if (_proceduralMask) return;
|
||||
|
||||
const int s = 256;
|
||||
_proceduralMask = new Texture2D(s, s, TextureFormat.RGBA32, false, true) // linear: raw mask values
|
||||
{
|
||||
name = "MeshPainter_CircleMask",
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
_proceduralMask = NewMaskTexture(s, "MeshPainter_CircleMask");
|
||||
|
||||
var px = new Color32[s * s];
|
||||
_proceduralOpacity = new float[s * s];
|
||||
@@ -468,6 +476,18 @@ public partial class MeshPainter : MonoBehaviour
|
||||
_procH = s;
|
||||
}
|
||||
|
||||
/// <summary>Creates a square RGBA32 brush-mask texture (linear: raw mask values, not colors).</summary>
|
||||
private static Texture2D NewMaskTexture(int size, string texName)
|
||||
{
|
||||
return new Texture2D(size, size, TextureFormat.RGBA32, false, true)
|
||||
{
|
||||
name = texName,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
filterMode = FilterMode.Bilinear,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Picks the active mask (list entry or procedural circle) and refreshes the CPU opacity cache on change.</summary>
|
||||
private void ResolveActiveMask()
|
||||
{
|
||||
@@ -802,8 +822,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
|
||||
if (stamp)
|
||||
{
|
||||
if (_gpuActive) BeginGpuStroke();
|
||||
else ResetCpuDirty();
|
||||
BeginBackendStroke();
|
||||
|
||||
if (!firstPress && paintInterval <= 0f)
|
||||
{
|
||||
@@ -819,8 +838,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
PaintAtScreen(screenPos);
|
||||
}
|
||||
|
||||
if (_gpuActive) EndGpuStroke();
|
||||
else EndCpuStroke();
|
||||
EndBackendStroke();
|
||||
}
|
||||
|
||||
_lastScreen = screenPos;
|
||||
@@ -850,8 +868,7 @@ public partial class MeshPainter : MonoBehaviour
|
||||
}
|
||||
else UseResolvedStampMask();
|
||||
|
||||
if (_gpuActive) DrawBrushGpu(pLocal, radiusLocal, u, v, nLocal);
|
||||
else PaintStamp3D(pLocal, radiusLocal, u, v, nLocal);
|
||||
DrawBackendStamp(pLocal, radiusLocal, u, v, nLocal);
|
||||
|
||||
EmitStamp(pLocal, radiusLocal, u, v, nLocal);
|
||||
MarkStrokePainted(); // a stamp landed -> this stroke is undoable
|
||||
|
||||
@@ -145,9 +145,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
|
||||
// Remote avatar: NetworkTransform owns the transform here, so disable the local
|
||||
// simulation (input/movement) and the CharacterController's own depenetration.
|
||||
if (controller) controller.enabled = false;
|
||||
var cc = GetComponent<CharacterController>();
|
||||
if (cc) cc.enabled = false;
|
||||
DisableLocalSimulation();
|
||||
|
||||
// The concealment meter (per-frame bake + voxelization + overlap) only feeds the LOCAL player;
|
||||
// a remote's concealment is computed on its own owner's machine, so here it is wasted CPU.
|
||||
@@ -206,16 +204,22 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
|
||||
if (IsOwner)
|
||||
{
|
||||
var controller = GetComponent<PlayerCharacterController>();
|
||||
if (controller) controller.enabled = false;
|
||||
var cc = GetComponent<CharacterController>();
|
||||
if (cc) cc.enabled = false;
|
||||
DisableLocalSimulation();
|
||||
|
||||
var manager = FindFirstObjectByType<GameModeManager>(FindObjectsInactive.Include);
|
||||
if (manager) manager.BindSpectator();
|
||||
}
|
||||
}
|
||||
|
||||
// Stops this avatar simulating locally: movement input and the CharacterController's own depenetration.
|
||||
private void DisableLocalSimulation()
|
||||
{
|
||||
var controller = GetComponent<PlayerCharacterController>();
|
||||
if (controller) controller.enabled = false;
|
||||
var cc = GetComponent<CharacterController>();
|
||||
if (cc) cc.enabled = false;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateVisibility();
|
||||
@@ -352,11 +356,9 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// Buried-hider reveal: already gated to hider + running match on the owner before it is published.
|
||||
if (_embeddedReveal.Value) return true;
|
||||
|
||||
// Resolution reveals every remaining (un-found) hider through walls, on every client.
|
||||
var match = MatchManager.Instance;
|
||||
return match && match.MatchActive &&
|
||||
match.LocalSubPhase == MatchManager.MatchSubPhase.Resolution &&
|
||||
role == PlayerRole.Hider && !_isFound.Value;
|
||||
// Resolution reveals every remaining (un-found) hider through walls, on every client -
|
||||
// exactly the still-in-play-hider-during-Resolution predicate the freeze uses.
|
||||
return ComputeResolutionFreeze();
|
||||
}
|
||||
|
||||
// True if THIS machine's local player is a Hunter role variant (so it must not see hiders during hide).
|
||||
@@ -392,10 +394,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
private void ApplyOwnerPosition(Vector3 worldPos)
|
||||
{
|
||||
var cc = GetComponent<CharacterController>();
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
transform.position = worldPos;
|
||||
if (cc) cc.enabled = reenable;
|
||||
CharacterTeleport.Move(cc, transform, worldPos);
|
||||
}
|
||||
|
||||
// Resolves the spawn-area collider for this avatar: the Role-specific area (Hunter/Hider) takes priority,
|
||||
@@ -446,12 +445,7 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
}
|
||||
|
||||
Vector3 target = new Vector3(pick.x, b.min.y + spawnAreaGroundLift, pick.z);
|
||||
|
||||
var cc = GetComponent<CharacterController>();
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
transform.position = target;
|
||||
if (cc) cc.enabled = reenable;
|
||||
ApplyOwnerPosition(target);
|
||||
}
|
||||
|
||||
// Collider.ClosestPoint only constrains to the real shape for primitives and CONVEX mesh colliders;
|
||||
@@ -481,11 +475,6 @@ public class NetworkPlayerSetup : NetworkBehaviour
|
||||
// Half-slot stagger per ring so outer slots don't line up radially with inner ones.
|
||||
float angle = (slot / (float)spawnRingSlots + ring * 0.5f / spawnRingSlots) * Mathf.PI * 2f;
|
||||
Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radius;
|
||||
|
||||
var cc = GetComponent<CharacterController>();
|
||||
bool reenable = cc && cc.enabled;
|
||||
if (cc) cc.enabled = false;
|
||||
transform.position += offset;
|
||||
if (cc) cc.enabled = reenable;
|
||||
ApplyOwnerPosition(transform.position + offset);
|
||||
}
|
||||
}
|
||||
@@ -149,14 +149,30 @@ public class OnlineSessionManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private async void HostAsync()
|
||||
// Shared busy/sign-in/error skeleton around one session operation; the body carries the
|
||||
// op-specific statuses and state.
|
||||
private async System.Threading.Tasks.Task RunSessionOpAsync(string failLabel, Func<System.Threading.Tasks.Task> body)
|
||||
{
|
||||
if (_busy || IsConnected) return;
|
||||
SetBusy(true);
|
||||
try
|
||||
{
|
||||
SetStatus("Signing in…");
|
||||
await EnsureSignedInAsync();
|
||||
await body();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SetStatus($"{failLabel} failed: {e.Message}");
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
|
||||
private async void HostAsync()
|
||||
{
|
||||
if (_busy || IsConnected) return;
|
||||
await RunSessionOpAsync("Host", async () =>
|
||||
{
|
||||
SetStatus("Creating session…");
|
||||
var options = new SessionOptions
|
||||
{
|
||||
@@ -165,55 +181,31 @@ public class OnlineSessionManager : MonoBehaviour
|
||||
}.WithRelayNetwork();
|
||||
_session = await MultiplayerService.Instance.CreateSessionAsync(options);
|
||||
SetStatus($"Hosting \"{_session.Name}\" (code {_session.Code})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SetStatus($"Host failed: {e.Message}");
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
});
|
||||
}
|
||||
|
||||
private async void RefreshAsync()
|
||||
{
|
||||
if (_busy) return;
|
||||
SetBusy(true);
|
||||
try
|
||||
await RunSessionOpAsync("Refresh", async () =>
|
||||
{
|
||||
SetStatus("Signing in…");
|
||||
await EnsureSignedInAsync();
|
||||
SetStatus("Querying sessions…");
|
||||
var results = await MultiplayerService.Instance.QuerySessionsAsync(new QuerySessionsOptions());
|
||||
_sessions = new List<ISessionInfo>(results.Sessions);
|
||||
SessionsChanged?.Invoke();
|
||||
SetStatus($"{_sessions.Count} session(s) found");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SetStatus($"Refresh failed: {e.Message}");
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
});
|
||||
}
|
||||
|
||||
private async void JoinAsync(string sessionId)
|
||||
{
|
||||
if (_busy || IsConnected || string.IsNullOrEmpty(sessionId)) return;
|
||||
SetBusy(true);
|
||||
try
|
||||
await RunSessionOpAsync("Join", async () =>
|
||||
{
|
||||
SetStatus("Signing in…");
|
||||
await EnsureSignedInAsync();
|
||||
SetStatus("Joining session…");
|
||||
_session = await MultiplayerService.Instance.JoinSessionByIdAsync(sessionId);
|
||||
SetStatus($"Joined \"{_session.Name}\"");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SetStatus($"Join failed: {e.Message}");
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
});
|
||||
}
|
||||
|
||||
private async void DisconnectAsync()
|
||||
|
||||
@@ -73,11 +73,7 @@ public partial class PaintHud
|
||||
return Path.Combine(HudSaveDir, key + ".hudlayout");
|
||||
}
|
||||
|
||||
private static string SanitizeHudKey(string s)
|
||||
{
|
||||
foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_');
|
||||
return s.Replace(' ', '_');
|
||||
}
|
||||
private static string SanitizeHudKey(string s) => MeshPainter.SanitizeFileKey(s);
|
||||
|
||||
[Serializable]
|
||||
private class HudState
|
||||
@@ -224,27 +220,18 @@ public partial class PaintHud
|
||||
// it - once per HUD, so a reset always returns to the genuine material default.
|
||||
if (!_hasDefaultCamo)
|
||||
{
|
||||
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
||||
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
||||
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
||||
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
||||
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
||||
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
||||
CaptureCamoDefaults();
|
||||
_hasDefaultCamo = true;
|
||||
}
|
||||
|
||||
if (_hasPendingCamo)
|
||||
{
|
||||
painter.CamoHalfLambert = _pendingCamo.x;
|
||||
painter.CamoHalfLambertBrightness = _pendingCamo.y;
|
||||
painter.CamoSpecularDampen = _pendingCamo.z;
|
||||
painter.CamoKnobs = _pendingCamo;
|
||||
_hasPendingCamo = false;
|
||||
}
|
||||
if (_hasPendingHeight)
|
||||
{
|
||||
painter.OcclusionThreshold = _pendingHeight.x;
|
||||
painter.HeightNormalIntensity = _pendingHeight.y;
|
||||
painter.ParallaxIntensity = _pendingHeight.z;
|
||||
painter.HeightKnobs = _pendingHeight;
|
||||
_hasPendingHeight = false;
|
||||
}
|
||||
SyncCamoSliders();
|
||||
@@ -471,6 +458,17 @@ public partial class PaintHud
|
||||
_hasDefaults = true;
|
||||
}
|
||||
|
||||
/// <summary>Snapshots the live camo + height knobs into the reset defaults (caller decides _hasDefaultCamo).</summary>
|
||||
private void CaptureCamoDefaults()
|
||||
{
|
||||
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
||||
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
||||
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
||||
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
||||
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
||||
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the HUD to its authored defaults, exactly as if the saved layout file had been deleted: the
|
||||
/// panels jump back to their prefab positions and every brush value returns to the painter's prefab /
|
||||
@@ -494,14 +492,7 @@ public partial class PaintHud
|
||||
// Reset clicked before the ready-gate captured the camo defaults? keep the live camo rather than
|
||||
// slamming it to the not-yet-filled 0 (which would flatten the material's lighting).
|
||||
if (!_hasDefaultCamo && painter.IsReady)
|
||||
{
|
||||
_defaults.camoHalfLambert = painter.CamoHalfLambert;
|
||||
_defaults.camoHalfLambertBrightness = painter.CamoHalfLambertBrightness;
|
||||
_defaults.camoSpecularDampen = painter.CamoSpecularDampen;
|
||||
_defaults.occlusionThreshold = painter.OcclusionThreshold;
|
||||
_defaults.heightNormalIntensity = painter.HeightNormalIntensity;
|
||||
_defaults.parallaxIntensity = painter.ParallaxIntensity;
|
||||
}
|
||||
CaptureCamoDefaults();
|
||||
|
||||
ApplyValues(painter, _defaults);
|
||||
SyncFromPainter();
|
||||
|
||||
+17
-18
@@ -155,11 +155,7 @@ public partial class PaintHud : MonoBehaviour
|
||||
{
|
||||
if (!newPainter || newPainter == painter || !_panel) return;
|
||||
|
||||
if (painter)
|
||||
{
|
||||
painter.Picked -= SyncFromPainter;
|
||||
painter.BrushSizeChanged -= SyncBrushSize;
|
||||
}
|
||||
UnhookPainterEvents();
|
||||
painter = newPainter;
|
||||
_camoSynced = false; // re-seed the camo sliders once this painter's material instance is ready
|
||||
|
||||
@@ -184,13 +180,13 @@ public partial class PaintHud : MonoBehaviour
|
||||
UpdateButtonStates(); // enable Save now that a painter is bound
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
private void OnDestroy() => UnhookPainterEvents();
|
||||
|
||||
private void UnhookPainterEvents()
|
||||
{
|
||||
if (painter)
|
||||
{
|
||||
painter.Picked -= SyncFromPainter;
|
||||
painter.BrushSizeChanged -= SyncBrushSize;
|
||||
}
|
||||
if (!painter) return;
|
||||
painter.Picked -= SyncFromPainter;
|
||||
painter.BrushSizeChanged -= SyncBrushSize;
|
||||
}
|
||||
|
||||
private void OnModeChanged(GameMode mode)
|
||||
@@ -383,8 +379,7 @@ public partial class PaintHud : MonoBehaviour
|
||||
|
||||
private static void SetSlider(Transform panel, string name, float value)
|
||||
{
|
||||
var cell = FindDeep(panel, name);
|
||||
var slider = cell ? cell.GetComponentInChildren<Slider>() : null;
|
||||
var slider = FindSlider(panel, name, out _);
|
||||
if (!slider) return;
|
||||
var curve = slider.GetComponent<SliderCurve>();
|
||||
if (curve) curve.SetOutput(value); // inverts the value onto the handle, then fires the slider listener
|
||||
@@ -470,8 +465,7 @@ public partial class PaintHud : MonoBehaviour
|
||||
{
|
||||
if (!tex) return null;
|
||||
if (_maskSprites.TryGetValue(tex, out var cached) && cached) return cached;
|
||||
var s = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
|
||||
s.hideFlags = HideFlags.HideAndDontSave;
|
||||
var s = ToSprite(tex);
|
||||
_maskSprites[tex] = s;
|
||||
return s;
|
||||
}
|
||||
@@ -525,11 +519,16 @@ public partial class PaintHud : MonoBehaviour
|
||||
toggle.onValueChanged.AddListener(v => group.alpha = v ? 1f : 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>Finds a slider cell by name and the Slider inside it (null when either is missing).</summary>
|
||||
private static Slider FindSlider(Transform panel, string name, out Transform cell)
|
||||
{
|
||||
cell = FindDeep(panel, name);
|
||||
return cell ? cell.GetComponentInChildren<Slider>() : null;
|
||||
}
|
||||
|
||||
private static void WireSlider(Transform panel, string name, float value, Action<float> onChanged)
|
||||
{
|
||||
var cell = FindDeep(panel, name);
|
||||
if (!cell) return;
|
||||
var slider = cell.GetComponentInChildren<Slider>();
|
||||
var slider = FindSlider(panel, name, out Transform cell);
|
||||
if (!slider) return;
|
||||
var curve = slider.GetComponent<SliderCurve>(); // optional non-linear response on the same object
|
||||
var valueRt = cell.Find("Value");
|
||||
|
||||
@@ -9,11 +9,7 @@ public class PaintMode : GameMode
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
if (Manager.Player)
|
||||
{
|
||||
Manager.Player.enabled = true;
|
||||
Manager.Player.ControlEnabled = false;
|
||||
}
|
||||
SetPlayerControl(false);
|
||||
|
||||
if (Manager.CameraInput)
|
||||
{
|
||||
@@ -21,8 +17,7 @@ public class PaintMode : GameMode
|
||||
Manager.CameraInput.PanEnabled = true;
|
||||
}
|
||||
|
||||
if (Manager.Painter)
|
||||
Manager.Painter.PaintingEnabled = true;
|
||||
SetPainting(true);
|
||||
}
|
||||
|
||||
public override void Exit()
|
||||
|
||||
@@ -486,17 +486,12 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill-plane rescue: snap the avatar back to its captured spawn point and clear all momentum. The
|
||||
/// CharacterController overrides direct transform writes, so it is toggled around the move (same
|
||||
/// pattern as <see cref="NetworkPlayerSetup.PlaceOnSpawnRing"/>). Owner-only; the authoritative
|
||||
/// NetworkTransform replicates the new position to remotes.
|
||||
/// Kill-plane rescue: snap the avatar back to its captured spawn point and clear all momentum.
|
||||
/// Owner-only; the authoritative NetworkTransform replicates the new position to remotes.
|
||||
/// </summary>
|
||||
private void RescueToSpawn()
|
||||
{
|
||||
bool reenable = _controller && _controller.enabled;
|
||||
if (_controller) _controller.enabled = false;
|
||||
transform.position = _spawnPosition;
|
||||
if (_controller) _controller.enabled = reenable;
|
||||
CharacterTeleport.Move(_controller, transform, _spawnPosition);
|
||||
|
||||
_verticalVelocity = 0f;
|
||||
_horizontalVelocity = Vector3.zero;
|
||||
@@ -800,12 +795,12 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
}
|
||||
else if (_climbLeftGround && vEff > 0f)
|
||||
{
|
||||
TopOutDetach(); // topped out upward onto walkable ground: hop over the lip (holding the up key carries on)
|
||||
TopOutDetach();
|
||||
return;
|
||||
}
|
||||
else if (_climbLeftGround || vEff < 0f)
|
||||
{
|
||||
ExitWallClimb(); // descended to the floor, or pulling down/away at the base: let go, no hop
|
||||
ExitWallClimb();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -989,34 +984,16 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
/// </summary>
|
||||
private bool ProbeWall(out Vector3 normal, out float gap)
|
||||
{
|
||||
normal = _wallNormal;
|
||||
gap = 0f;
|
||||
|
||||
Vector3 origin = WorldCapsuleCenter;
|
||||
Vector3 dir = -_wallNormal;
|
||||
float extent = CapsuleExtent(dir);
|
||||
float maxDist = extent + wallMaxOffset + ProbeReachSlack * CapsuleScale;
|
||||
|
||||
int count = Physics.RaycastNonAlloc(
|
||||
origin, dir, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
|
||||
if (NearestClimbHit(origin, dir, maxDist, extent, ceilingOnly: false, out normal, out gap))
|
||||
return true;
|
||||
|
||||
float best = float.MaxValue;
|
||||
bool found = false;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider)) // skip self, other player avatars, and no-climb surfaces
|
||||
continue;
|
||||
if (_wallProbe[i].distance < best)
|
||||
{
|
||||
best = _wallProbe[i].distance;
|
||||
normal = _wallProbe[i].normal;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
gap = Mathf.Max(0f, best - extent);
|
||||
return found;
|
||||
normal = _wallNormal;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1035,15 +1012,32 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
/// </summary>
|
||||
private bool ProbeCeiling(out Vector3 normal, out float gap)
|
||||
{
|
||||
normal = Vector3.down;
|
||||
gap = 0f;
|
||||
|
||||
Vector3 origin = WorldCapsuleCenter;
|
||||
float half = WorldHalfHeight;
|
||||
float maxDist = half + wallMaxOffset + ProbeReachSlack * CapsuleScale;
|
||||
|
||||
if (NearestClimbHit(origin, Vector3.up, maxDist, half, ceilingOnly: true, out normal, out gap))
|
||||
return true;
|
||||
|
||||
normal = Vector3.down;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raycasts along <paramref name="dir"/> and returns the nearest hit that a climb probe accepts
|
||||
/// (player avatars and no-climb surfaces never count; <paramref name="ceilingOnly"/> additionally
|
||||
/// rejects non-ceiling normals). <paramref name="gap"/> is the clearance beyond the capsule's
|
||||
/// support <paramref name="extent"/> along the probe direction. False = no valid hit
|
||||
/// (<paramref name="normal"/>/<paramref name="gap"/> are then zero; callers supply their fallback).
|
||||
/// </summary>
|
||||
private bool NearestClimbHit(Vector3 origin, Vector3 dir, float maxDist, float extent,
|
||||
bool ceilingOnly, out Vector3 normal, out float gap)
|
||||
{
|
||||
normal = Vector3.zero;
|
||||
gap = 0f;
|
||||
|
||||
int count = Physics.RaycastNonAlloc(
|
||||
origin, Vector3.up, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
|
||||
origin, dir, _wallProbe, maxDist, ~0, QueryTriggerInteraction.Ignore);
|
||||
|
||||
float best = float.MaxValue;
|
||||
bool found = false;
|
||||
@@ -1051,7 +1045,7 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
{
|
||||
if (IsPlayerCollider(_wallProbe[i].collider) || IsNoClimb(_wallProbe[i].collider))
|
||||
continue;
|
||||
if (_wallProbe[i].normal.y >= CeilingNormalThreshold) // not a ceiling
|
||||
if (ceilingOnly && _wallProbe[i].normal.y >= CeilingNormalThreshold) // not a ceiling
|
||||
continue;
|
||||
if (_wallProbe[i].distance < best)
|
||||
{
|
||||
@@ -1062,7 +1056,7 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
}
|
||||
|
||||
if (found)
|
||||
gap = Mathf.Max(0f, best - half);
|
||||
gap = Mathf.Max(0f, best - extent);
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,11 @@ public class PlayerControlMode : GameMode
|
||||
|
||||
public override void Enter()
|
||||
{
|
||||
if (Manager.Player)
|
||||
{
|
||||
Manager.Player.enabled = true;
|
||||
Manager.Player.ControlEnabled = true;
|
||||
}
|
||||
SetPlayerControl(true);
|
||||
|
||||
if (Manager.CameraInput)
|
||||
Manager.CameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.AlwaysActive;
|
||||
|
||||
if (Manager.Painter)
|
||||
Manager.Painter.PaintingEnabled = false;
|
||||
SetPainting(false);
|
||||
}
|
||||
}
|
||||
@@ -233,20 +233,14 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
// its own material via HUD + persistence).
|
||||
if (!IsOwner && _camoApplyPending && _painter && _painter.IsReady)
|
||||
{
|
||||
Vector3 c = _camo.Value;
|
||||
_painter.CamoHalfLambert = c.x;
|
||||
_painter.CamoHalfLambertBrightness = c.y;
|
||||
_painter.CamoSpecularDampen = c.z;
|
||||
_painter.CamoKnobs = _camo.Value;
|
||||
_camoApplyPending = false;
|
||||
}
|
||||
|
||||
// Remote replica: apply the owner's painted-height knobs once the material exists / on change.
|
||||
if (!IsOwner && _heightApplyPending && _painter && _painter.IsReady)
|
||||
{
|
||||
Vector3 h = _height.Value;
|
||||
_painter.OcclusionThreshold = h.x;
|
||||
_painter.HeightNormalIntensity = h.y;
|
||||
_painter.ParallaxIntensity = h.z;
|
||||
_painter.HeightKnobs = _height.Value;
|
||||
_heightApplyPending = false;
|
||||
}
|
||||
|
||||
@@ -274,11 +268,11 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
// published (it would briefly flatten remotes' lighting until the real value lands).
|
||||
if (_painter && _painter.IsReady)
|
||||
{
|
||||
var camo = new Vector3(_painter.CamoHalfLambert, _painter.CamoHalfLambertBrightness, _painter.CamoSpecularDampen);
|
||||
Vector3 camo = _painter.CamoKnobs;
|
||||
if (_camo.Value != camo)
|
||||
_camo.Value = camo;
|
||||
|
||||
var height = new Vector3(_painter.OcclusionThreshold, _painter.HeightNormalIntensity, _painter.ParallaxIntensity);
|
||||
Vector3 height = _painter.HeightKnobs;
|
||||
if (_height.Value != height)
|
||||
_height.Value = height;
|
||||
}
|
||||
@@ -365,20 +359,31 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
[ServerRpc]
|
||||
private void SubmitRestoreServerRpc(int seq, int id) => ApplyRestoreClientRpc(seq, id);
|
||||
|
||||
[ClientRpc]
|
||||
private void ApplyStampsClientRpc(int seq, PaintStampNet[] stamps)
|
||||
/// <summary>
|
||||
/// Common gate for the replicated commands: the owner already applied them locally, so only remotes
|
||||
/// (incl. the host's replica) proceed; during a snapshot transfer the event is buffered instead.
|
||||
/// Returns true when the caller should apply the event live.
|
||||
/// </summary>
|
||||
private bool GateRemoteEvent(NetEvent ev)
|
||||
{
|
||||
// The owner already painted these locally; only remotes (incl. the host's replica) replay them.
|
||||
if (IsOwner || !_painter)
|
||||
return;
|
||||
return false;
|
||||
|
||||
_seqHorizon = seq; // server replica tracks this as the snapshot cutoff source
|
||||
_seqHorizon = ev.seq; // server replica tracks this as the snapshot cutoff source
|
||||
|
||||
if (_awaitingSnapshot)
|
||||
{
|
||||
_buffered.Add(new NetEvent { seq = seq, kind = NetKind.Stamps, stamps = stamps });
|
||||
return;
|
||||
_buffered.Add(ev);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
private void ApplyStampsClientRpc(int seq, PaintStampNet[] stamps)
|
||||
{
|
||||
if (!GateRemoteEvent(new NetEvent { seq = seq, kind = NetKind.Stamps, stamps = stamps }))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < stamps.Length; i++)
|
||||
_painter.ApplyNetworkStamp(stamps[i]);
|
||||
@@ -387,17 +392,9 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
[ClientRpc]
|
||||
private void ApplyCheckpointClientRpc(int seq, int id)
|
||||
{
|
||||
if (IsOwner || !_painter)
|
||||
if (!GateRemoteEvent(new NetEvent { seq = seq, kind = NetKind.Checkpoint, id = id }))
|
||||
return;
|
||||
|
||||
_seqHorizon = seq;
|
||||
|
||||
if (_awaitingSnapshot)
|
||||
{
|
||||
_buffered.Add(new NetEvent { seq = seq, kind = NetKind.Checkpoint, id = id });
|
||||
return;
|
||||
}
|
||||
|
||||
_painter.RemoteCommitCheckpoint(id);
|
||||
LogSync($"committed checkpoint {id} (seq {seq})");
|
||||
}
|
||||
@@ -405,17 +402,9 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
[ClientRpc]
|
||||
private void ApplyRestoreClientRpc(int seq, int id)
|
||||
{
|
||||
if (IsOwner || !_painter)
|
||||
if (!GateRemoteEvent(new NetEvent { seq = seq, kind = NetKind.Restore, id = id }))
|
||||
return;
|
||||
|
||||
_seqHorizon = seq;
|
||||
|
||||
if (_awaitingSnapshot)
|
||||
{
|
||||
_buffered.Add(new NetEvent { seq = seq, kind = NetKind.Restore, id = id });
|
||||
return;
|
||||
}
|
||||
|
||||
// Reproduce the undo/redo from this peer's own history; if it doesn't hold that checkpoint (an
|
||||
// undo past this peer's join point), fetch a fresh snapshot of the resulting state instead.
|
||||
if (_painter.RemoteRestoreTo(id))
|
||||
@@ -520,14 +509,20 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
int xferId = ++_xferOut;
|
||||
|
||||
BeginSnapshotClientRpc(xferId, data.Length, capturedSeq, baselineId, send);
|
||||
yield return SendChunked(data, (off, chunk) => SnapshotChunkClientRpc(xferId, off, chunk, send));
|
||||
EndSnapshotClientRpc(xferId, send);
|
||||
}
|
||||
|
||||
/// <summary>Feeds a blob chunk-wise into sendChunk(offset, chunk), throttled to snapshotChunksPerFrame.</summary>
|
||||
private IEnumerator SendChunked(byte[] data, System.Action<int, byte[]> sendChunk)
|
||||
{
|
||||
int sentThisFrame = 0;
|
||||
for (int off = 0; off < data.Length; off += snapshotChunkBytes)
|
||||
{
|
||||
int len = Mathf.Min(snapshotChunkBytes, data.Length - off);
|
||||
var chunk = new byte[len];
|
||||
System.Array.Copy(data, off, chunk, 0, len);
|
||||
SnapshotChunkClientRpc(xferId, off, chunk, send);
|
||||
sendChunk(off, chunk);
|
||||
|
||||
if (++sentThisFrame >= snapshotChunksPerFrame)
|
||||
{
|
||||
@@ -535,8 +530,6 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
yield return null; // spread the burst so the reliable window does not overflow
|
||||
}
|
||||
}
|
||||
|
||||
EndSnapshotClientRpc(xferId, send);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
@@ -564,8 +557,14 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
private void SnapshotChunkClientRpc(int xferId, int offset, byte[] chunk, ClientRpcParams p = default)
|
||||
{
|
||||
if (xferId != _activeXferId || _snapBuffer == null) return; // stale chunk from a superseded transfer
|
||||
if (offset >= 0 && offset + chunk.Length <= _snapBuffer.Length)
|
||||
System.Array.Copy(chunk, 0, _snapBuffer, offset, chunk.Length);
|
||||
CopyChunkInto(_snapBuffer, offset, chunk);
|
||||
}
|
||||
|
||||
/// <summary>Copies a received chunk into the reassembly buffer, ignoring out-of-bounds offsets.</summary>
|
||||
private static void CopyChunkInto(byte[] buffer, int offset, byte[] chunk)
|
||||
{
|
||||
if (offset >= 0 && offset + chunk.Length <= buffer.Length)
|
||||
System.Array.Copy(chunk, 0, buffer, offset, chunk.Length);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
@@ -611,22 +610,7 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
private IEnumerator UploadSnapshot(byte[] data, int cutoff, int baselineId, bool broadcast, ulong target)
|
||||
{
|
||||
BeginUploadServerRpc(data.Length, cutoff, baselineId, broadcast, target);
|
||||
|
||||
int sentThisFrame = 0;
|
||||
for (int off = 0; off < data.Length; off += snapshotChunkBytes)
|
||||
{
|
||||
int len = Mathf.Min(snapshotChunkBytes, data.Length - off);
|
||||
var chunk = new byte[len];
|
||||
System.Array.Copy(data, off, chunk, 0, len);
|
||||
UploadChunkServerRpc(off, chunk);
|
||||
|
||||
if (++sentThisFrame >= snapshotChunksPerFrame)
|
||||
{
|
||||
sentThisFrame = 0;
|
||||
yield return null; // spread the burst so the reliable window does not overflow
|
||||
}
|
||||
}
|
||||
|
||||
yield return SendChunked(data, (off, chunk) => UploadChunkServerRpc(off, chunk));
|
||||
EndUploadServerRpc();
|
||||
}
|
||||
|
||||
@@ -644,8 +628,7 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
private void UploadChunkServerRpc(int offset, byte[] chunk)
|
||||
{
|
||||
if (_uploadBuffer == null) return;
|
||||
if (offset >= 0 && offset + chunk.Length <= _uploadBuffer.Length)
|
||||
System.Array.Copy(chunk, 0, _uploadBuffer, offset, chunk.Length);
|
||||
CopyChunkInto(_uploadBuffer, offset, chunk);
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
|
||||
@@ -179,7 +179,7 @@ public class PlayerPoseController : MonoBehaviour
|
||||
// Local control only - a remote's pose index is pushed in via SetNetworkState.
|
||||
if (!_networkControlled)
|
||||
{
|
||||
if (CycleKeyPressed())
|
||||
if (InputUtil.GameplayKeyPressed(cycleKey))
|
||||
NextPose();
|
||||
|
||||
// Sprinting in any state (ground, air, wall, ceiling) snaps the selection back to the default
|
||||
@@ -213,17 +213,21 @@ public class PlayerPoseController : MonoBehaviour
|
||||
// Camera target: a per-pose framing offset (applied in any state), eased so cycling poses glides
|
||||
// the camera, routed to the ThirdPersonCamera's follow target only (never the body).
|
||||
float cameraPoseTarget = pose ? pose.CameraTargetYOffset : 0f;
|
||||
_cameraPoseOffset = cameraOffsetSpeed > 0f
|
||||
? Mathf.MoveTowards(_cameraPoseOffset, cameraPoseTarget, cameraOffsetSpeed * Time.deltaTime)
|
||||
: cameraPoseTarget;
|
||||
_cameraPoseOffset = EaseCameraOffset(_cameraPoseOffset, cameraPoseTarget);
|
||||
_player.SetCameraTargetLocalYOffset(_cameraPoseOffset);
|
||||
|
||||
// Separate per-pose first-person eye offset - NOT routed to the shared camera target (that would
|
||||
// also move the ThirdPersonCamera); the FirstPersonController reads it directly.
|
||||
float firstPersonTarget = pose ? pose.FirstPersonCameraTargetYOffset : 0f;
|
||||
_firstPersonCameraOffset = cameraOffsetSpeed > 0f
|
||||
? Mathf.MoveTowards(_firstPersonCameraOffset, firstPersonTarget, cameraOffsetSpeed * Time.deltaTime)
|
||||
: firstPersonTarget;
|
||||
_firstPersonCameraOffset = EaseCameraOffset(_firstPersonCameraOffset, firstPersonTarget);
|
||||
}
|
||||
|
||||
/// <summary>Eases a camera y-offset toward its target at <see cref="cameraOffsetSpeed"/>; 0 snaps.</summary>
|
||||
private float EaseCameraOffset(float current, float target)
|
||||
{
|
||||
return cameraOffsetSpeed > 0f
|
||||
? Mathf.MoveTowards(current, target, cameraOffsetSpeed * Time.deltaTime)
|
||||
: target;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
@@ -339,17 +343,6 @@ public class PlayerPoseController : MonoBehaviour
|
||||
_player.SetCapsule(_capCenterY, _capRadius, _capHeight);
|
||||
}
|
||||
|
||||
private bool CycleKeyPressed()
|
||||
{
|
||||
if (TypingGuard.IsTyping) return false;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
var keyboard = Keyboard.current;
|
||||
return keyboard != null && keyboard[cycleKey].wasPressedThisFrame;
|
||||
#else
|
||||
return Input.GetKeyDown(cycleKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays and holds the given clip, overriding the Animator Controller. Switching to a different
|
||||
/// clip crossfades from the current pose over <see cref="poseBlendDuration"/> (the first pose, or a
|
||||
|
||||
@@ -73,30 +73,12 @@ public class SkinnedColliderBaker : MonoBehaviour
|
||||
// renderer), and MeshPainter / ConcealmentMeter — which raise the baked mesh to world via
|
||||
// localToWorldMatrix — stay correct at any player scale. No-op (and untouched) at scale 1; the
|
||||
// uniform divide is rotation-independent, which the player's uniform scale guarantees.
|
||||
Vector3 ls = transform.lossyScale;
|
||||
if (ls.x != 1f || ls.y != 1f || ls.z != 1f)
|
||||
RescaleToUnitScale(ls);
|
||||
GeometryUtil.RescaleToUnitScale(_baked, transform.lossyScale, _vbuf);
|
||||
|
||||
_collider.sharedMesh = null; // force a physics re-cook of the new geometry
|
||||
_collider.sharedMesh = _baked;
|
||||
}
|
||||
|
||||
private void RescaleToUnitScale(Vector3 lossyScale)
|
||||
{
|
||||
float ix = lossyScale.x != 0f ? 1f / lossyScale.x : 0f;
|
||||
float iy = lossyScale.y != 0f ? 1f / lossyScale.y : 0f;
|
||||
float iz = lossyScale.z != 0f ? 1f / lossyScale.z : 0f;
|
||||
_baked.GetVertices(_vbuf);
|
||||
for (int i = 0; i < _vbuf.Count; i++)
|
||||
{
|
||||
Vector3 v = _vbuf[i];
|
||||
v.x *= ix; v.y *= iy; v.z *= iz;
|
||||
_vbuf[i] = v;
|
||||
}
|
||||
_baked.SetVertices(_vbuf);
|
||||
_baked.RecalculateBounds();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_baked) Destroy(_baked);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Mouse-look yaw/pitch accumulator with optional SmoothDampAngle smoothing, shared by the
|
||||
/// first-person and freecam rigs. <see cref="Tick"/> accumulates a scaled mouse delta into the
|
||||
/// target angles and advances the smoothed display angles; <see cref="SyncTo"/> seeds everything
|
||||
/// from an existing rotation so going live produces no visible jump.
|
||||
/// </summary>
|
||||
public struct SmoothedLook
|
||||
{
|
||||
float _yaw; // look target (raw input accumulates here)
|
||||
float _pitch;
|
||||
float _displayYaw; // smoothed value actually applied to the transform
|
||||
float _displayPitch;
|
||||
float _yawSmoothRef;
|
||||
float _pitchSmoothRef;
|
||||
|
||||
/// <summary>Smoothed yaw shown this frame (drives the body's facing in first person).</summary>
|
||||
public float DisplayYaw => _displayYaw;
|
||||
|
||||
/// <summary>Rotation composed from the smoothed display angles.</summary>
|
||||
public Quaternion Rotation => Quaternion.Euler(_displayPitch, _displayYaw, 0f);
|
||||
|
||||
/// <summary>Accumulates one frame of look input and advances the smoothed display angles.</summary>
|
||||
public void Tick(Vector2 scaledDelta, float hSens, float vSens, bool invertY, float maxPitch, float smoothTime)
|
||||
{
|
||||
_yaw += scaledDelta.x * hSens;
|
||||
float vs = vSens * (invertY ? 1f : -1f);
|
||||
_pitch = Mathf.Clamp(_pitch + scaledDelta.y * vs, -maxPitch, maxPitch);
|
||||
|
||||
if (smoothTime > 0f)
|
||||
{
|
||||
// SmoothDampAngle is frame-rate-independent and wraps yaw correctly.
|
||||
_displayYaw = Mathf.SmoothDampAngle(_displayYaw, _yaw, ref _yawSmoothRef, smoothTime);
|
||||
_displayPitch = Mathf.SmoothDampAngle(_displayPitch, _pitch, ref _pitchSmoothRef, smoothTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayYaw = _yaw;
|
||||
_displayPitch = _pitch;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Seeds the look angles from an existing rotation, then snaps the smoothed state to them.</summary>
|
||||
public void SyncTo(Quaternion rotation, float maxPitch)
|
||||
{
|
||||
Vector3 e = rotation.eulerAngles;
|
||||
_yaw = e.y;
|
||||
float p = e.x > 180f ? e.x - 360f : e.x;
|
||||
_pitch = Mathf.Clamp(p, -maxPitch, maxPitch);
|
||||
Snap();
|
||||
}
|
||||
|
||||
/// <summary>Snaps the smoothed state to the current targets so entering never lags/jumps to catch up.</summary>
|
||||
public void Snap()
|
||||
{
|
||||
_displayYaw = _yaw;
|
||||
_displayPitch = _pitch;
|
||||
_yawSmoothRef = 0f;
|
||||
_pitchSmoothRef = 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b943136bd10858b42b2790061dc1213e
|
||||
Reference in new issue
Block a user