diff --git a/Assets/Scripts/FrameRateLimiter.cs b/Assets/Scripts/FrameRateLimiter.cs
index 638a203..fbfc23f 100644
--- a/Assets/Scripts/FrameRateLimiter.cs
+++ b/Assets/Scripts/FrameRateLimiter.cs
@@ -1,58 +1,38 @@
using UnityEngine;
///
-/// Limits the game's frame rate.
-///
-/// Sets and adjusts VSync
-/// () accordingly. IMPORTANT: as long as
-/// VSync is active, Unity ignores targetFrameRate - the limit then only takes
-/// effect when VSync is off. This script therefore disables VSync automatically
-/// as soon as an FPS limit is set (see ).
-///
-/// Changes apply live: in the editor via OnValidate (also in Play mode while
-/// tweaking values in the Inspector).
-///
-/// Simply attach to any (ideally persistent) GameObject in the scene.
+/// Caps the frame rate via . VSync is
+/// disabled automatically while limiting, because it would otherwise override
+/// targetFrameRate. Changes apply live (also in Play mode) via OnValidate.
///
[DisallowMultipleComponent]
public class FrameRateLimiter : MonoBehaviour
{
[Header("Limit")]
- [Tooltip("ON = the frame rate is capped to 'Target FPS'. OFF = unlimited " +
- "(targetFrameRate = -1, platform default).")]
+ [Tooltip("Cap the frame rate to 'Target FPS' (off = unlimited).")]
public bool limitFrameRate = true;
- [Tooltip("Target frame rate in frames per second when the limit is active.")]
+ [Tooltip("Target frame rate when the limit is active.")]
[Range(1, 360)]
public int targetFps = 60;
[Header("VSync")]
- [Tooltip("When an FPS limit is set, disable VSync automatically. " +
- "Otherwise VSync would override targetFrameRate and the limit " +
- "would have no effect. Normally leave this ON.")]
+ [Tooltip("Disable VSync while limiting (VSync would otherwise override the FPS limit). Normally ON.")]
public bool disableVSyncWhenLimiting = true;
- [Tooltip("VSync count to use when NO limit is active " +
- "(0 = VSync off, 1 = every screen refresh, 2 = every second refresh).")]
+ [Tooltip("VSync count when no limit is active (0 = off, 1 = every refresh, 2 = every second refresh).")]
[Range(0, 2)]
public int vSyncCountWhenUnlimited = 0;
- void OnEnable()
- {
- Apply();
- }
+ void OnEnable() => Apply();
void OnValidate()
{
- // Live adjustment when changing values in the Inspector (also in Play mode).
if (Application.isPlaying)
Apply();
}
- ///
- /// Applies the current limit (FPS + VSync) to the Unity settings.
- /// Can also be called externally.
- ///
+ /// Applies the current FPS + VSync settings.
public void Apply()
{
if (limitFrameRate)
@@ -65,7 +45,7 @@ public class FrameRateLimiter : MonoBehaviour
else
{
QualitySettings.vSyncCount = vSyncCountWhenUnlimited;
- Application.targetFrameRate = -1; // platform default / unlimited
+ Application.targetFrameRate = -1;
}
}
diff --git a/Assets/Scripts/GameMode.cs b/Assets/Scripts/GameMode.cs
index ae8bf04..62f8d19 100644
--- a/Assets/Scripts/GameMode.cs
+++ b/Assets/Scripts/GameMode.cs
@@ -1,29 +1,24 @@
///
-/// Base class for a high-level game mode (state) handled by
-/// . A mode configures the relevant scene systems
-/// when it becomes active (), can clean up when it is left
-/// () and may run per-frame logic in .
-///
-/// Modes are plain C# classes (not MonoBehaviours). To add a new mode, derive
-/// from this class and register an instance in GameModeManager.BuildModes().
+/// Base class for a high-level game mode handled by .
+/// Modes are plain C# classes (not MonoBehaviours); Enter/Exit/Tick are driven
+/// manually by the manager. Derive from this and register an instance in
+/// GameModeManager.BuildModes().
///
public abstract class GameMode
{
- /// The owning manager, providing access to the controlled systems.
protected GameModeManager Manager { get; private set; }
- /// Human-readable name (shown in logs / debug UI).
+ /// Human-readable name (shown in logs).
public abstract string DisplayName { get; }
- /// Wires the mode to its manager. Called once during registration.
internal void Bind(GameModeManager manager) => Manager = manager;
- /// Called once when this mode becomes the active mode.
+ /// Called once when this mode becomes active.
public virtual void Enter() { }
- /// Called once when this mode stops being the active mode.
+ /// Called once when this mode stops being active.
public virtual void Exit() { }
- /// Called every frame while this mode is the active mode.
+ /// Called every frame while this mode is active.
public virtual void Tick() { }
}
diff --git a/Assets/Scripts/GameModeManager.cs b/Assets/Scripts/GameModeManager.cs
index 35cb003..d9c90b7 100644
--- a/Assets/Scripts/GameModeManager.cs
+++ b/Assets/Scripts/GameModeManager.cs
@@ -5,33 +5,21 @@ using UnityEngine.InputSystem;
#endif
///
-/// Central state manager that switches the game between high-level modes.
-/// Currently two modes exist:
-/// 0. Player Control - WASD movement, camera orbits freely with the mouse.
-/// 1. Paint - player stands still, left mouse paints the mesh, the
-/// camera orbits only while the right mouse button is held.
-///
-/// Pressing the switch key (Tab) cycles to the next registered mode, so adding
-/// more modes later automatically extends the cycle.
-///
-/// The manager owns references to the systems a mode may toggle (player
-/// controller, orbit camera input, mesh painter) and exposes them to the modes.
-/// Modes are plain C# classes derived from ; register
-/// additional ones in .
-///
-/// Attach this to a single, persistent GameObject in the scene.
+/// Switches the game between high-level modes (Player Control, Paint). The switch
+/// key (Tab) cycles to the next registered mode. Register additional modes in
+/// . Attach to a single, persistent scene GameObject.
///
[DisallowMultipleComponent]
public class GameModeManager : MonoBehaviour
{
[Header("Controlled Systems")]
- [Tooltip("Player movement controller. Empty = found automatically in the scene.")]
+ [Tooltip("Player movement controller. Empty = found automatically.")]
public PlayerCharacterController player;
- [Tooltip("Third-person orbit camera input. Empty = found automatically in the scene.")]
+ [Tooltip("Third-person orbit camera input. Empty = found automatically.")]
public ThirdPersonOrbitInput cameraInput;
- [Tooltip("Mesh painter. Empty = found automatically in the scene.")]
+ [Tooltip("Mesh painter. Empty = found automatically.")]
public MeshPainter painter;
[Header("Switching")]
@@ -42,10 +30,9 @@ public class GameModeManager : MonoBehaviour
public KeyCode switchKey = KeyCode.Tab;
#endif
- [Tooltip("Index of the mode that is active at startup (0 = Player Control).")]
+ [Tooltip("Mode active at startup (0 = Player Control).")]
public int startModeIndex = 0;
- // --- accessors used by the modes ---
public PlayerCharacterController Player => player;
public ThirdPersonOrbitInput CameraInput => cameraInput;
public MeshPainter Painter => painter;
@@ -59,10 +46,9 @@ public class GameModeManager : MonoBehaviour
private void Awake()
{
- // Auto-resolve references so the manager works with minimal wiring.
- if (player == null) player = FindFirstObjectByType();
- if (cameraInput == null) cameraInput = FindFirstObjectByType();
- if (painter == null) painter = FindFirstObjectByType();
+ if (!player) player = FindFirstObjectByType();
+ if (!cameraInput) cameraInput = FindFirstObjectByType();
+ if (!painter) painter = FindFirstObjectByType();
BuildModes();
}
diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs
index 71eb500..3146785 100644
--- a/Assets/Scripts/MeshPainter.cs
+++ b/Assets/Scripts/MeshPainter.cs
@@ -5,46 +5,28 @@ using UnityEngine.InputSystem;
#endif
///
-/// Live mesh painting in Play mode.
-/// Paints a fixed color (brush) onto the mesh's texture with the mouse.
-///
-/// TWO BACKENDS (switchable via , also live in Play mode):
-///
-/// * GPU (default): True shader painting into a RenderTexture. The brush is
-/// drawn via a vertex/fragment shader - the mesh is unwrapped into the texture
-/// layout using its UVs, and per texel the actual 3D position is tested against
-/// the brush sphere. Edge padding against seams runs as a GPU blit.
-/// Fast, because the GPU rasterizes all texels in parallel.
-///
-/// * CPU: Rasterization of the hit triangles on the CPU; per texel the 3D
-/// position is computed barycentrically and tested against the brush sphere.
-/// Edge padding via CPU dilation. Identical visual result, but more expensive.
-///
-/// BOTH backends paint seamlessly: a 3D sphere around the hit point captures
-/// triangles on both sides of a UV seam at once (front/back share the same 3D
-/// edge), no matter where they lie in the UV layout.
-///
-/// Simply attach to the GameObject you want to paint (e.g. paintman_UV/paintman_UV).
+/// Live mesh painting in Play mode: paints the brush color onto the mesh texture
+/// with the mouse. Two backends (switchable live via ): GPU
+/// shader painting into a RenderTexture (fast, default) and CPU rasterization
+/// (slower, identical result). Both paint seamlessly across UV seams because a 3D
+/// sphere around the hit point captures triangles on both sides of a seam at once.
+/// Attach to the GameObject you want to paint.
///
[DisallowMultipleComponent]
public class MeshPainter : MonoBehaviour
{
- ///
- /// When false, mouse painting is ignored (the canvas/material stays as-is).
- /// Toggled by the GameModeManager (painting is only enabled in paint mode).
- ///
+ /// When false, mouse painting is ignored. Toggled by the GameModeManager.
public bool PaintingEnabled { get; set; } = true;
[Header("Backend")]
- [Tooltip("ON = GPU painting (shader/RenderTexture, fast). OFF = CPU painting. " +
- "Can be toggled live in Play mode - the painted content is carried over.")]
+ [Tooltip("ON = GPU painting (fast), OFF = CPU painting. Can be toggled live in Play mode.")]
public bool useGpu = true;
[Header("Brush")]
[Tooltip("Color used for painting.")]
public Color brushColor = Color.red;
- [Tooltip("Brush radius in texels (visual size). Converted into a 3D radius per hit.")]
+ [Tooltip("Brush radius in texels. Converted into a 3D radius per hit.")]
[Range(1, 256)]
public int brushSize = 16;
@@ -55,9 +37,7 @@ public class MeshPainter : MonoBehaviour
[Tooltip("Base color of the texture at startup (if none exists).")]
public Color baseColor = Color.white;
- [Tooltip("Edge padding (texels) against visible UV seams. The color is 'bled' beyond the " +
- "UV island edges so that bilinear sampling there does not pull in the unpainted " +
- "base color. 0 = off.")]
+ [Tooltip("Edge padding (texels) bled beyond UV island edges so bilinear sampling does not pull in the base color. 0 = off.")]
[Range(0, 16)]
public int paddingTexels = 4;
@@ -75,43 +55,41 @@ public class MeshPainter : MonoBehaviour
private Mesh _bakedMesh;
private Material _materialInstance;
- // Mesh caches (in the transform's local space = collider space).
+ // Mesh caches (in local/collider space).
private Vector3[] _verts;
private int[] _tris;
private Vector2[] _uvs;
- private Vector3[] _triCenter; // centroid per triangle (for fast CPU culling)
- private float[] _triRadius; // circumscribed radius per triangle (for fast CPU culling)
- private float _avgUvToLocal = 1f; // average conversion: UV unit -> local unit
+ private Vector3[] _triCenter; // centroid per triangle (CPU culling)
+ private float[] _triRadius; // circumscribed radius per triangle (CPU culling)
+ private float _avgUvToLocal = 1f; // average UV unit -> local unit
private bool _hasLastScreen;
private Vector2 _lastScreen;
- private bool _gpuActive; // currently configured backend
+ private bool _gpuActive;
private bool _setupValid;
// --- CPU backend ---
private Texture2D _paintTexture;
private Color32[] _pixels;
- private bool[] _painted; // true = painted by the brush (persistent dilation source)
- private int[] _fillStamp; // transient marker for padding texels per dilation call
- private int _dilateStamp; // incremented per call -> padding from previous frames does not count as a source
+ private bool[] _painted; // true = painted by the brush (dilation source)
+ private int[] _fillStamp; // per-call marker for padding texels
+ private int _dilateStamp; // bumped per call so old padding is not a source
private readonly List _dilateScratch = new List(512);
- private readonly RaycastHit[] _rayHits = new RaycastHit[32]; // buffer so other colliders (player capsule, etc.) cannot block painting
+ private readonly RaycastHit[] _rayHits = new RaycastHit[32];
private int _dx0, _dy0, _dx1, _dy1; // dirty rectangle (CPU upload)
// --- GPU backend ---
- public Shader brushShader; // optional; otherwise Shader.Find("Hidden/MeshPaintBrush")
- public Shader dilateShader; // optional; otherwise Shader.Find("Hidden/MeshPaintDilate")
+ public Shader brushShader; // optional; else Shader.Find("Hidden/MeshPaintBrush")
+ public Shader dilateShader; // optional; else Shader.Find("Hidden/MeshPaintDilate")
private Material _brushMat;
private Material _dilateMat;
- private RenderTexture _displayRT; // assigned to the material as _BaseMap
+ private RenderTexture _displayRT;
private RenderTexture _tempRT; // scratch for the dilation blit (ping-pong)
private bool _drewThisStroke;
private bool _prevSRGB;
- // Step size (in screen pixels) for interpolation along the mouse movement.
- private const float ScreenStepPixels = 5f;
- // Upper bound of raycasts per frame (safeguard for very fast movements).
- private const int MaxStepsPerFrame = 256;
+ private const float ScreenStepPixels = 5f; // interpolation step along mouse movement
+ private const int MaxStepsPerFrame = 256; // raycast safeguard for fast movement
private static readonly int BaseMapId = Shader.PropertyToID("_BaseMap");
private static readonly int MainTexId = Shader.PropertyToID("_MainTex");
@@ -124,11 +102,11 @@ public class MeshPainter : MonoBehaviour
private void Start()
{
- if (paintCamera == null) paintCamera = Camera.main;
+ if (!paintCamera) paintCamera = Camera.main;
_renderer = GetComponent();
_skinned = GetComponent();
- if (_renderer == null)
+ if (!_renderer)
{
Debug.LogError("[MeshPainter] No Renderer found on this GameObject.", this);
enabled = false;
@@ -147,9 +125,9 @@ public class MeshPainter : MonoBehaviour
return;
}
- // Own material instance so the shared asset is not modified.
- _materialInstance = _renderer.material; // automatically creates an instance
- // Tint to white so the painted color appears unaltered.
+ // Own material instance so the shared asset is not modified; tint to white
+ // so the painted color appears unaltered.
+ _materialInstance = _renderer.material;
if (_materialInstance.HasProperty(BaseColorId)) _materialInstance.SetColor(BaseColorId, Color.white);
else if (_materialInstance.HasProperty(ColorId)) _materialInstance.SetColor(ColorId, Color.white);
@@ -160,14 +138,12 @@ public class MeshPainter : MonoBehaviour
"Hold the left mouse button and drag over the mesh.", this);
}
- // ------------------------------------------------------------------
- // Setup (shared)
- // ------------------------------------------------------------------
+ // --- Setup (shared) ---
private void SetupCollider()
{
_collider = GetComponent();
- if (_collider == null) _collider = gameObject.AddComponent();
+ if (!_collider) _collider = gameObject.AddComponent();
_collider.convex = false; // non-convex provides triangleIndex / textureCoord
_bakedMesh = new Mesh { name = "MeshPainter_Baked" };
@@ -176,7 +152,7 @@ public class MeshPainter : MonoBehaviour
private void BakeColliderMesh()
{
- if (_skinned != null)
+ if (_skinned)
{
// Static skinned mesh: bake into local coordinates (scale = 1).
_skinned.BakeMesh(_bakedMesh);
@@ -186,7 +162,7 @@ public class MeshPainter : MonoBehaviour
else
{
var mf = GetComponent();
- if (mf != null) _collider.sharedMesh = mf.sharedMesh;
+ if (mf) _collider.sharedMesh = mf.sharedMesh;
}
}
@@ -194,14 +170,14 @@ public class MeshPainter : MonoBehaviour
private void BuildMeshCaches()
{
Mesh m = _collider.sharedMesh;
- if (m == null) return;
+ if (!m) return;
_verts = m.vertices;
_tris = m.triangles;
_uvs = m.uv;
- // Fallback: UVs from the original mesh (BakeMesh usually keeps them, but better safe than sorry).
- if ((_uvs == null || _uvs.Length != _verts.Length) && _skinned != null && _skinned.sharedMesh != null)
+ // Fallback to the original mesh UVs if the baked mesh dropped them.
+ if ((_uvs == null || _uvs.Length != _verts.Length) && _skinned && _skinned.sharedMesh)
_uvs = _skinned.sharedMesh.uv;
if (_uvs == null || _uvs.Length != _verts.Length) return;
@@ -232,21 +208,21 @@ public class MeshPainter : MonoBehaviour
if (sumAreaUv > 1e-9) _avgUvToLocal = (float)System.Math.Sqrt(sumArea3D / sumAreaUv);
}
- /// Builds the base-color texels (copy of the existing material texture or baseColor).
+ /// Builds the base-color texels from the existing material texture or baseColor.
private Color32[] BuildBaseColors()
{
var px = new Color32[textureSize * textureSize];
Texture sourceTex = null;
var shared = _renderer.sharedMaterial;
- if (shared != null)
+ if (shared)
{
if (shared.HasProperty(BaseMapId)) sourceTex = shared.GetTexture(BaseMapId);
- if (sourceTex == null && shared.HasProperty(MainTexId)) sourceTex = shared.GetTexture(MainTexId);
+ if (!sourceTex && shared.HasProperty(MainTexId)) sourceTex = shared.GetTexture(MainTexId);
}
var src = sourceTex as Texture2D;
- if (src != null)
+ if (src)
{
try
{
@@ -261,11 +237,8 @@ public class MeshPainter : MonoBehaviour
return px;
}
- // ------------------------------------------------------------------
- // Backend selection / switching
- // ------------------------------------------------------------------
+ // --- Backend selection / switching ---
- /// Sets up the desired backend and carries over the initial image content.
private void SetupBackend(bool gpu, Color32[] fromColor32)
{
if (gpu) SetupGpu(fromColor32);
@@ -273,10 +246,9 @@ public class MeshPainter : MonoBehaviour
_gpuActive = gpu;
}
- /// Switches the backend at runtime and carries over the currently painted content.
+ /// Switches the backend at runtime, carrying over the painted content.
private void SwitchBackend(bool toGpu)
{
- // save the current content as Color32
Color32[] current = (_gpuActive ? ReadGpuPixels() : (Color32[])_pixels.Clone());
if (_gpuActive) TeardownGpu();
@@ -288,9 +260,7 @@ public class MeshPainter : MonoBehaviour
Debug.Log($"[MeshPainter] Backend switched -> {(toGpu ? "GPU" : "CPU")} painting.", this);
}
- // ------------------------------------------------------------------
- // CPU backend
- // ------------------------------------------------------------------
+ // --- CPU backend ---
private void SetupCpu(Color32[] initial)
{
@@ -317,22 +287,20 @@ public class MeshPainter : MonoBehaviour
private void TeardownCpu()
{
- if (_paintTexture != null) { Destroy(_paintTexture); _paintTexture = null; }
+ if (_paintTexture) { Destroy(_paintTexture); _paintTexture = null; }
_pixels = null;
_painted = null;
_fillStamp = null;
}
- // ------------------------------------------------------------------
- // GPU backend
- // ------------------------------------------------------------------
+ // --- GPU backend ---
private void SetupGpu(Color32[] initial)
{
- if (brushShader == null) brushShader = Shader.Find("Hidden/MeshPaintBrush");
- if (dilateShader == null) dilateShader = Shader.Find("Hidden/MeshPaintDilate");
+ if (!brushShader) brushShader = Shader.Find("Hidden/MeshPaintBrush");
+ if (!dilateShader) dilateShader = Shader.Find("Hidden/MeshPaintDilate");
- if (brushShader == null || dilateShader == null)
+ if (!brushShader || !dilateShader)
{
Debug.LogError("[MeshPainter] GPU shaders not found (Hidden/MeshPaintBrush or " +
"Hidden/MeshPaintDilate). Falling back to CPU painting.", this);
@@ -348,8 +316,8 @@ public class MeshPainter : MonoBehaviour
_displayRT = CreateRT("MeshPainter_DisplayRT");
_tempRT = CreateRT("MeshPainter_TempRT");
- // Bring the initial image content into the display RT. Alpha = 0 marks "unpainted"
- // (so the base color is not mistaken for a dilation source).
+ // Seed the display RT. Alpha = 0 marks "unpainted" so the base color is not
+ // mistaken for a dilation source.
var seed = initial ?? BuildBaseColors();
UploadColorsToRT(seed, _displayRT, paintedAlpha: false);
@@ -358,10 +326,10 @@ public class MeshPainter : MonoBehaviour
private void TeardownGpu()
{
- if (_displayRT != null) { _displayRT.Release(); Destroy(_displayRT); _displayRT = null; }
- if (_tempRT != null) { _tempRT.Release(); Destroy(_tempRT); _tempRT = null; }
- if (_brushMat != null) { Destroy(_brushMat); _brushMat = null; }
- if (_dilateMat != null) { Destroy(_dilateMat); _dilateMat = null; }
+ if (_displayRT) { _displayRT.Release(); Destroy(_displayRT); _displayRT = null; }
+ if (_tempRT) { _tempRT.Release(); Destroy(_tempRT); _tempRT = null; }
+ if (_brushMat) { Destroy(_brushMat); _brushMat = null; }
+ if (_dilateMat) { Destroy(_dilateMat); _dilateMat = null; }
}
private RenderTexture CreateRT(string rtName)
@@ -383,14 +351,13 @@ public class MeshPainter : MonoBehaviour
private void UploadColorsToRT(Color32[] colors, RenderTexture rt, bool paintedAlpha)
{
var tmp = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false, false);
+ // Alpha marks painted (255) vs unpainted (0) for the dilation pass.
if (paintedAlpha)
{
- // mark the entire content as "painted" (alpha = 255) -> no dilation needed.
for (int i = 0; i < colors.Length; i++) colors[i].a = 255;
}
else
{
- // mark the base color as "unpainted" (alpha = 0).
for (int i = 0; i < colors.Length; i++) colors[i].a = 0;
}
tmp.SetPixels32(colors);
@@ -413,26 +380,21 @@ public class MeshPainter : MonoBehaviour
return px;
}
- // ------------------------------------------------------------------
- // Material
- // ------------------------------------------------------------------
+ // --- Material ---
private void AssignTextureToMaterial(Texture tex)
{
- if (_materialInstance == null) return;
+ if (!_materialInstance) return;
if (_materialInstance.HasProperty(BaseMapId)) _materialInstance.SetTexture(BaseMapId, tex);
if (_materialInstance.HasProperty(MainTexId)) _materialInstance.SetTexture(MainTexId, tex);
}
- // ------------------------------------------------------------------
- // Update / input
- // ------------------------------------------------------------------
+ // --- Update / input ---
private void Update()
{
if (!_setupValid) return;
- // Live backend switching.
if (useGpu != _gpuActive) SwitchBackend(useGpu);
// Painting disabled by the game mode (e.g. player-control mode): do nothing.
@@ -442,7 +404,7 @@ public class MeshPainter : MonoBehaviour
return;
}
- if (updateColliderEveryFrame && _skinned != null)
+ if (updateColliderEveryFrame && _skinned)
{
BakeColliderMesh();
BuildMeshCaches();
@@ -457,15 +419,15 @@ public class MeshPainter : MonoBehaviour
return;
}
- if (paintCamera == null) return;
+ if (!paintCamera) return;
if (_gpuActive) BeginGpuStroke();
else { _dx0 = textureSize; _dy0 = textureSize; _dx1 = -1; _dy1 = -1; }
if (_hasLastScreen)
{
- // Interpolate along the mouse movement in screen space and re-raycast at
- // each step -> the stroke follows the mesh surface.
+ // Interpolate along the mouse movement and re-raycast at each step so the
+ // stroke follows the mesh surface.
float dist = Vector2.Distance(_lastScreen, screenPos);
int steps = Mathf.Clamp(Mathf.CeilToInt(dist / ScreenStepPixels), 1, MaxStepsPerFrame);
for (int s = 1; s <= steps; s++)
@@ -487,19 +449,14 @@ public class MeshPainter : MonoBehaviour
_hasLastScreen = true;
}
- ///
- /// Raycast at a screen position. If the ray hits the mesh, a 3D sphere is painted
- /// around the hit point (seamless) - via GPU or CPU depending on the backend.
- ///
+ /// Raycasts at a screen position and paints a 3D sphere around the mesh hit point.
private void PaintAtScreen(Vector2 screenPos)
{
Ray ray = paintCamera.ScreenPointToRay(screenPos);
- // Collect all hits and pick the nearest one that belongs to the paint mesh.
- // A plain Physics.Raycast returns only the closest collider, so anything in
- // front of the mesh (the player's CharacterController capsule, future props,
- // the camera-collision proxy) would block painting. Filtering by collider
- // keeps painting working from every camera angle.
+ // Pick the nearest hit that belongs to the paint mesh. A plain raycast would
+ // return only the closest collider, so anything in front of the mesh (the
+ // player capsule, props, the camera-collision proxy) could block painting.
int hitCount = Physics.RaycastNonAlloc(ray, _rayHits, 1000f, ~0, QueryTriggerInteraction.Ignore);
RaycastHit hit = default;
float nearest = float.MaxValue;
@@ -517,11 +474,10 @@ public class MeshPainter : MonoBehaviour
if (!found) return;
if (_verts == null || hit.triangleIndex < 0) return;
- // Bring the hit point into local mesh space (= collider/vertex space).
Vector3 pLocal = transform.InverseTransformPoint(hit.point);
- // Convert the brush radius (texels) into a local 3D radius using the UV
- // density of the hit triangle -> the brush stays visually the same size.
+ // Convert the brush radius (texels) into a local 3D radius using the hit
+ // triangle's UV density so the brush keeps a constant visual size.
float scale = _avgUvToLocal;
int t = hit.triangleIndex;
if (t * 3 + 2 < _tris.Length)
@@ -532,8 +488,7 @@ public class MeshPainter : MonoBehaviour
if (areaUv > 1e-9f)
{
float s = Mathf.Sqrt(area3D / areaUv);
- // guard against degenerate UVs: keep close to the average.
- scale = Mathf.Clamp(s, _avgUvToLocal * 0.25f, _avgUvToLocal * 4f);
+ scale = Mathf.Clamp(s, _avgUvToLocal * 0.25f, _avgUvToLocal * 4f); // guard degenerate UVs
}
}
@@ -544,15 +499,13 @@ public class MeshPainter : MonoBehaviour
else PaintSphere3D(pLocal, radiusLocal);
}
- // ------------------------------------------------------------------
- // GPU painting
- // ------------------------------------------------------------------
+ // --- GPU painting ---
private void BeginGpuStroke()
{
_drewThisStroke = false;
_prevSRGB = GL.sRGBWrite;
- GL.sRGBWrite = true; // correct sRGB encoding when writing into the sRGB RT (linear project)
+ GL.sRGBWrite = true; // correct sRGB encoding when writing into the sRGB RT
Graphics.SetRenderTarget(_displayRT);
_brushMat.SetColor(BrushColorId, brushColor);
}
@@ -574,8 +527,7 @@ public class MeshPainter : MonoBehaviour
{
Graphics.SetRenderTarget(null);
- // Edge padding against UV seams: blit the display RT through the dilation
- // shader into _tempRT and then swap (the result ends up in _displayRT again).
+ // Edge padding against UV seams: dilate the display RT into _tempRT, then swap.
if (_drewThisStroke && paddingTexels > 0)
{
_dilateMat.SetInt(PadId, paddingTexels);
@@ -588,13 +540,11 @@ public class MeshPainter : MonoBehaviour
GL.sRGBWrite = _prevSRGB;
}
- // ------------------------------------------------------------------
- // CPU painting (rasterization + dilation)
- // ------------------------------------------------------------------
+ // --- CPU painting (rasterization + dilation) ---
///
- /// Paints a sphere in local 3D space: rasterizes all triangles in range into
- /// the texture and colors every texel whose 3D position lies inside the sphere.
+ /// Paints a sphere in local 3D space: rasterizes triangles in range and colors
+ /// every texel whose 3D position lies inside the sphere.
///
private void PaintSphere3D(Vector3 pLocal, float radiusLocal)
{
@@ -605,7 +555,7 @@ public class MeshPainter : MonoBehaviour
for (int t = 0; t < triCount; t++)
{
- // Fast culling: sphere against the triangle's circumscribed circle.
+ // Fast culling: sphere vs. the triangle's circumscribed circle.
float reach = radiusLocal + _triRadius[t];
if ((_triCenter[t] - pLocal).sqrMagnitude > reach * reach) continue;
@@ -618,7 +568,6 @@ public class MeshPainter : MonoBehaviour
float p1x = uv1.x * ts, p1y = uv1.y * ts;
float p2x = uv2.x * ts, p2y = uv2.y * ts;
- // Bounding box in pixel space.
int minX = Mathf.Clamp(Mathf.FloorToInt(Mathf.Min(p0x, Mathf.Min(p1x, p2x))), 0, textureSize - 1);
int maxX = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(p0x, Mathf.Max(p1x, p2x))), 0, textureSize - 1);
int minY = Mathf.Clamp(Mathf.FloorToInt(Mathf.Min(p0y, Mathf.Min(p1y, p2y))), 0, textureSize - 1);
@@ -630,7 +579,7 @@ public class MeshPainter : MonoBehaviour
if (Mathf.Abs(d) < 1e-7f) continue;
float invD = 1f / d;
- const float eps = -1e-4f; // small tolerance against 1px cracks between triangles
+ const float eps = -1e-4f; // tolerance against 1px cracks between triangles
for (int y = minY; y <= maxY; y++)
{
@@ -668,9 +617,8 @@ public class MeshPainter : MonoBehaviour
}
///
- /// Edge padding / texture dilation against visible UV seams (CPU).
- /// Colors a thin border (pad texels) around the freshly painted areas with their
- /// color. The dilation source is exclusively brush-painted texels (_painted).
+ /// CPU edge padding against UV seams: bleeds the freshly painted colors outward
+ /// by 'pad' texels. The dilation source is only brush-painted texels (_painted).
///
private void DilatePadding(int pad)
{
@@ -681,7 +629,7 @@ public class MeshPainter : MonoBehaviour
int x1 = Mathf.Min(textureSize - 1, _dx1 + pad);
int y1 = Mathf.Min(textureSize - 1, _dy1 + pad);
- _dilateStamp++; // unique per call -> padding from previous frames does not count as a source
+ _dilateStamp++;
for (int iter = 0; iter < pad; iter++)
{
@@ -755,9 +703,7 @@ public class MeshPainter : MonoBehaviour
_paintTexture.Apply(false);
}
- // ------------------------------------------------------------------
- // Helpers
- // ------------------------------------------------------------------
+ // --- Helpers ---
private static float Cross2D(Vector2 a, Vector2 b) => a.x * b.y - a.y * b.x;
@@ -785,8 +731,8 @@ public class MeshPainter : MonoBehaviour
private void OnDestroy()
{
- if (_bakedMesh != null) Destroy(_bakedMesh);
- if (_materialInstance != null) Destroy(_materialInstance);
+ if (_bakedMesh) Destroy(_bakedMesh);
+ if (_materialInstance) Destroy(_materialInstance);
TeardownCpu();
TeardownGpu();
}
diff --git a/Assets/Scripts/PaintMode.cs b/Assets/Scripts/PaintMode.cs
index 2b1dfdf..77b82c7 100644
--- a/Assets/Scripts/PaintMode.cs
+++ b/Assets/Scripts/PaintMode.cs
@@ -1,7 +1,6 @@
///
-/// Mode 2: the player stands still and the mesh is painted with the left mouse
-/// button. The camera orbits only while the right mouse button is held, so
-/// dragging with the left button paints instead of rotating the view.
+/// Player stands still, the mesh is painted with the left mouse button. The
+/// camera orbits only while the right mouse button is held.
///
public class PaintMode : GameMode
{
@@ -9,13 +8,13 @@ public class PaintMode : GameMode
public override void Enter()
{
- if (Manager.Player != null)
+ if (Manager.Player)
Manager.Player.ControlEnabled = false;
- if (Manager.CameraInput != null)
+ if (Manager.CameraInput)
Manager.CameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.HoldRightMouseButton;
- if (Manager.Painter != null)
+ if (Manager.Painter)
Manager.Painter.PaintingEnabled = true;
}
}
diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs
index 677b829..e0ae282 100644
--- a/Assets/Scripts/PlayerCharacterController.cs
+++ b/Assets/Scripts/PlayerCharacterController.cs
@@ -4,49 +4,36 @@ using UnityEngine.InputSystem;
#endif
///
-/// Simple first/third-person style character controller driven by a
-/// : WASD for horizontal movement and Space to jump.
+/// CharacterController-based player: camera-relative WASD movement, manual gravity
+/// and jumping, sliding off too-steep slopes/edges, and a wall-climb mode.
///
-/// Movement can be camera-relative (W moves away from the camera) so it feels
-/// natural with the fixed scene camera. Gravity and jumping are integrated
-/// manually because CharacterController does not apply physics on its own.
+/// Wall-climb (entered on wall/ceiling contact) uses wall-relative controls: W
+/// moves toward the wall and climbs up once against it, S backs off and detaches,
+/// A/D strafe along the wall. Gravity and jumping are disabled while climbing;
+/// reaching the top edge mantles the character over onto the surface.
///
-/// Touching a wall or ceiling enters a wall-climb mode with
-/// wall-relative controls: W moves slowly toward the wall and climbs up once
-/// fully against it, S backs slowly away and detaches once far enough, and A/D
-/// strafe left/right along the wall. Gravity and jumping are ignored while
-/// climbing. Climbing up past the top edge mantles the character over onto the
-/// surface instead of dropping back down.
-///
-/// The script reads input through the new Input System (Keyboard.current) when it
-/// is enabled, and falls back to the legacy Input Manager otherwise. The project
-/// currently runs the new Input System only.
-///
-/// Attach this to the Player GameObject (the one that owns the CharacterController).
-///
-/// Note: the class is intentionally named PlayerCharacterController to avoid a
-/// name clash with Unity.VisualScripting's example "PlayerController" type.
+/// Named PlayerCharacterController to avoid a clash with Unity.VisualScripting's
+/// example "PlayerController".
///
[RequireComponent(typeof(CharacterController))]
[DisallowMultipleComponent]
public class PlayerCharacterController : MonoBehaviour
{
[Header("Movement")]
- [Tooltip("Horizontal movement speed in units per second.")]
+ [Tooltip("Horizontal movement speed (units/second).")]
public float moveSpeed = 4f;
- [Tooltip("If ON, WASD is interpreted relative to the camera (W = away from " +
- "the camera). If OFF, WASD moves along the world X/Z axes.")]
+ [Tooltip("Interpret WASD relative to the camera (off = world X/Z axes).")]
public bool cameraRelative = true;
[Tooltip("Camera used for camera-relative movement. Empty = Camera.main.")]
public Transform cameraTransform;
[Header("Rotation")]
- [Tooltip("If ON, the character smoothly turns to face its movement direction.")]
+ [Tooltip("Smoothly turn the character to face its movement direction.")]
public bool rotateTowardMovement = true;
- [Tooltip("Turn speed in degrees per second (used when 'Rotate Toward Movement' is on).")]
+ [Tooltip("Turn speed in degrees per second.")]
public float rotationSpeed = 720f;
[Header("Jump & Gravity")]
@@ -56,111 +43,93 @@ public class PlayerCharacterController : MonoBehaviour
[Tooltip("Gravity in units per second squared (negative = downward).")]
public float gravity = -19.62f;
- [Tooltip("Small downward velocity kept while grounded so the controller " +
- "stays firmly on the ground (and isGrounded stays reliable).")]
+ [Tooltip("Small downward velocity kept while grounded for reliable ground contact.")]
public float groundedStickVelocity = -2f;
[Header("Slope Sliding")]
- [Tooltip("If ON, the character slides down surfaces steeper than the " +
- "CharacterController's Slope Limit (and off sharp edges/corners) " +
- "instead of balancing on them.")]
+ [Tooltip("Slide down surfaces steeper than the CharacterController's Slope Limit instead of balancing on them.")]
public bool slideDownSteepSlopes = true;
- [Tooltip("How quickly the slide accelerates while on a too-steep surface " +
- "(units per second squared).")]
+ [Tooltip("How quickly the slide accelerates (units/second squared).")]
public float slideAcceleration = 20f;
- [Tooltip("Maximum downhill slide speed (units per second).")]
+ [Tooltip("Maximum downhill slide speed (units/second).")]
public float maxSlideSpeed = 8f;
- [Tooltip("How much steering control the player keeps while sliding " +
- "(0 = none, 1 = full normal control).")]
+ [Tooltip("Steering control kept while sliding (0 = none, 1 = full).")]
[Range(0f, 1f)]
public float slideControl = 0.3f;
[Header("Wall Climb")]
- [Tooltip("If ON, the character enters a wall-climb mode after touching a " +
- "wall or ceiling for a short time: it sticks to the surface, moves " +
- "at reduced speed and ignores gravity and jumping until contact is lost.")]
+ [Tooltip("Enter wall-climb mode on wall/ceiling contact: stick to the surface, move at reduced speed, ignore gravity and jumping.")]
public bool enableWallClimb = true;
- [Tooltip("Minimum surface angle from 'up' (in degrees) for a contact to " +
- "count as a climbable wall or ceiling. Surfaces between the slope " +
- "limit and this angle stay (sliding) slopes; at or above this they " +
- "are treated as walls/ceilings.")]
+ [Tooltip("Minimum surface angle from 'up' (degrees) that counts as a climbable wall/ceiling rather than a slope.")]
[Range(50f, 180f)]
public float wallMinAngle = 80f;
- [Tooltip("Speed multiplier for climbing up (W when against the wall) and " +
- "strafing left/right (A/D) while wall-climbing (0.5 = half speed).")]
+ [Tooltip("Speed multiplier for climbing up and strafing while wall-climbing (0.5 = half speed).")]
[Range(0f, 1f)]
public float wallClimbSpeedMultiplier = 0.5f;
- [Tooltip("Speed (units per second) for moving toward (W) or away from (S) the " +
- "wall while climbing. Deliberately slow so approaching/detaching is gentle.")]
+ [Tooltip("Slow speed for moving toward (W) or away from (S) the wall while climbing.")]
public float wallApproachSpeed = 0.5f;
- [Tooltip("Gap (in units) below which the character counts as fully against " +
- "the wall, so pressing forward (W) climbs up instead of moving closer.")]
+ [Tooltip("Gap below which the character counts as fully against the wall (W then climbs up).")]
public float wallContactDistance = 0.05f;
- [Tooltip("Gap (in units) from the wall at which holding back (S) leaves " +
- "wall-climb mode.")]
+ [Tooltip("Gap from the wall at which holding back (S) leaves wall-climb mode.")]
public float wallDetachDistance = 0.25f;
- [Tooltip("Grace period (seconds) of lost contact tolerated before leaving " +
- "wall-climb mode (prevents flicker on bumpy geometry/edges).")]
+ [Tooltip("Grace period (seconds) of lost contact tolerated before leaving wall-climb mode.")]
public float wallExitGrace = 0.15f;
///
- /// When false, movement/jump input is ignored (the player stands still), but
- /// gravity and ground contact keep running so the character stays grounded.
- /// Toggled by the GameModeManager (e.g. disabled in paint mode).
+ /// When false, movement/jump input is ignored but gravity and ground contact
+ /// keep running. Toggled by the GameModeManager.
///
public bool ControlEnabled { get; set; } = true;
- /// True while the character is in wall-climb mode (stuck to a wall/ceiling).
+ /// True while in wall-climb mode.
public bool IsWallClimbing => _isWallClimbing;
private CharacterController _controller;
private float _verticalVelocity;
- // Ground contact normal collected from the latest Move (used for slope sliding).
+ // Ground contact normal from the latest Move (for slope sliding).
private Vector3 _groundNormal = Vector3.up;
private bool _groundNormalUpdated;
private float _slideSpeed;
// Wall-climb state.
private bool _isWallClimbing;
- private Vector3 _wallNormal; // surface normal of the climbed wall/ceiling
+ private Vector3 _wallNormal;
private Vector3 _wallNormalAccum; // summed wall normals during the latest Move
- private bool _wallContactThisFrame; // any wall/ceiling touched during the latest Move
- private float _lostContactTimer; // continuous lost-contact time while climbing
- private readonly RaycastHit[] _wallProbe = new RaycastHit[8]; // buffer for the wall-distance probe
+ private bool _wallContactThisFrame;
+ private float _lostContactTimer;
+ private readonly RaycastHit[] _wallProbe = new RaycastHit[8];
- // Mantle (top-out) state: climbing up past the wall's top edge pulls the
- // character up and over onto the surface instead of dropping back down.
- private bool _mantling; // a mantle is currently in progress
- private bool _mantleClearedFeet; // phase 1 done: whole capsule is above the edge
- private float _mantleOverDist; // phase 2 forward distance moved over the top
- private float _mantleTimer; // safety cap so a mantle can never run forever
- private float _reentryCooldown; // blocks re-grabbing the wall right after topping out
+ // Mantle (top-out) state.
+ private bool _mantling;
+ private bool _mantleClearedFeet; // phase 1 done: capsule fully above the edge
+ private float _mantleOverDist; // phase 2 forward distance over the top
+ private float _mantleTimer; // safety cap
+ private float _reentryCooldown; // blocks re-grabbing right after topping out
void Awake()
{
_controller = GetComponent();
- if (cameraTransform == null && Camera.main != null)
+ if (!cameraTransform && Camera.main)
cameraTransform = Camera.main.transform;
}
void Update()
{
- // --- Read input -----------------------------------------------------
float h = 0f, v = 0f;
bool jumpPressed = false;
- // Skip input entirely when control is disabled (e.g. paint mode); gravity
- // and grounding below still run so the player stays planted on the ground.
+ // Skip input when control is disabled (e.g. paint mode); gravity and
+ // grounding below still run so the player stays planted.
if (ControlEnabled)
{
#if ENABLE_INPUT_SYSTEM
@@ -180,13 +149,12 @@ public class PlayerCharacterController : MonoBehaviour
#endif
}
- // --- Build the horizontal move direction ----------------------------
Vector3 inputDir = new Vector3(h, 0f, v);
if (inputDir.sqrMagnitude > 1f)
inputDir.Normalize();
Vector3 moveDir;
- if (cameraRelative && cameraTransform != null)
+ if (cameraRelative && cameraTransform)
{
// Project the camera axes onto the ground plane so movement stays horizontal.
Vector3 forward = cameraTransform.forward;
@@ -202,14 +170,10 @@ public class PlayerCharacterController : MonoBehaviour
moveDir = inputDir;
}
- // --- Wall-climb state machine ---------------------------------------
- // Evaluates the wall/ceiling contact gathered during the previous Move,
- // then enters/leaves wall-climb mode accordingly.
UpdateWallClimbState();
- // Latch the wall normal for this frame's movement, then reset the
- // per-frame contact accumulators (OnControllerColliderHit refills them
- // during the Move calls below / above).
+ // Latch the wall normal for this frame, then reset the per-frame contact
+ // accumulators (OnControllerColliderHit refills them during Move).
if (_wallContactThisFrame && _wallNormalAccum.sqrMagnitude > 1e-6f)
_wallNormal = _wallNormalAccum.normalized;
_wallContactThisFrame = false;
@@ -217,64 +181,51 @@ public class PlayerCharacterController : MonoBehaviour
if (_isWallClimbing)
{
- // While climbing: wall-relative WASD, no gravity, no jump, no slope
- // sliding, no forced alignment. Done for this frame.
ApplyWallClimbMovement(h, v, Time.deltaTime);
return;
}
- // --- Evaluate the ground slope (for sliding) ------------------------
- // The ground normal is gathered in OnControllerColliderHit during the
- // previous Move. If it is steeper than the controller's Slope Limit, the
- // surface is too steep to stand on (this also covers balancing on a sharp
- // box edge/corner, whose contact normal is steep).
+ // A ground normal steeper than the slope limit is too steep to stand on
+ // (also covers balancing on a sharp box edge).
float slopeAngle = Vector3.Angle(_groundNormal, Vector3.up);
bool onSteepSlope = slideDownSteepSlopes
&& _controller.isGrounded
&& slopeAngle > _controller.slopeLimit + 0.01f;
- // --- Gravity & jumping ---------------------------------------------
if (_controller.isGrounded)
{
- // Reset accumulated fall speed and keep a small downward bias on the ground.
if (_verticalVelocity < 0f)
_verticalVelocity = groundedStickVelocity;
- // No jumping off a slope that is too steep to stand on.
+ // No jumping off a slope too steep to stand on.
if (jumpPressed && !onSteepSlope)
_verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
_verticalVelocity += gravity * Time.deltaTime;
- // --- Slope sliding --------------------------------------------------
Vector3 slideVelocity = Vector3.zero;
if (onSteepSlope)
{
- // Steepest-descent direction along the slope surface.
Vector3 slideDir = Vector3.ProjectOnPlane(Vector3.down, _groundNormal).normalized;
- // Accelerate the slide up to the maximum slide speed.
_slideSpeed = Mathf.Min(_slideSpeed + slideAcceleration * Time.deltaTime, maxSlideSpeed);
slideVelocity = slideDir * _slideSpeed;
- // While sliding the player keeps only reduced steering control.
- moveDir *= slideControl;
+ moveDir *= slideControl; // reduced steering while sliding
}
else
{
_slideSpeed = 0f;
}
- // --- Apply movement -------------------------------------------------
Vector3 velocity = moveDir * moveSpeed + slideVelocity + Vector3.up * _verticalVelocity;
- // Reset the per-frame ground-normal collection, then Move (which fires
- // OnControllerColliderHit), then fall back to "up" if nothing was hit.
+ // Move fires OnControllerColliderHit, which refills the ground normal;
+ // fall back to "up" if nothing was hit.
_groundNormalUpdated = false;
_controller.Move(velocity * Time.deltaTime);
if (!_groundNormalUpdated)
_groundNormal = Vector3.up;
- // --- Rotate toward the movement direction ---------------------------
if (rotateTowardMovement && moveDir.sqrMagnitude > 0.0001f)
{
Quaternion target = Quaternion.LookRotation(moveDir, Vector3.up);
@@ -284,14 +235,11 @@ public class PlayerCharacterController : MonoBehaviour
}
///
- /// Drives entry into wall-climb mode: any wall/ceiling contact enters the mode
- /// immediately (or bails out when the feature/control is disabled). Leaving the
- /// mode while climbing is handled in
- /// (raycast-based).
+ /// Enters wall-climb mode on any wall/ceiling contact. Leaving while climbing
+ /// is handled in .
///
private void UpdateWallClimbState()
{
- // Cool down the post-mantle re-grab lock (runs whether climbing or not).
if (_reentryCooldown > 0f)
_reentryCooldown -= Time.deltaTime;
@@ -302,37 +250,29 @@ public class PlayerCharacterController : MonoBehaviour
return;
}
+ // The cooldown prevents instantly re-grabbing the edge we just mantled over.
if (!_isWallClimbing && _wallContactThisFrame && _reentryCooldown <= 0f)
{
- // Any wall/ceiling contact enters wall-climb mode immediately. The
- // cooldown above prevents instantly re-grabbing the edge we just
- // mantled over (which would otherwise cause a climb/drop flicker).
_isWallClimbing = true;
_lostContactTimer = 0f;
_verticalVelocity = 0f;
_slideSpeed = 0f;
}
- // While climbing, exit is decided in ApplyWallClimbMovement (intentional
- // back-off with S, or losing the wall after the grace period).
}
///
- /// Moves the character along the climbed surface using wall-relative WASD:
- /// W moves slowly toward the wall and, once fully against it, climbs up;
- /// S moves slowly away and detaches once far enough; A/D strafe left/right
- /// along the wall. The current wall normal and gap are measured each frame by
- /// a raycast toward the surface. No gravity, no jump, no forced alignment.
+ /// Wall-relative movement while climbing: W approaches the wall then climbs up,
+ /// S backs off and detaches, A/D strafe along the wall. The wall normal and gap
+ /// are measured each frame by a raycast. No gravity, jump or forced alignment.
///
private void ApplyWallClimbMovement(float h, float v, float dt)
{
- // A mantle (top-out) overrides normal climbing until it finishes.
if (_mantling)
{
MantleOverEdge(dt);
return;
}
- // Probe the wall ahead to refresh its normal and our distance to it.
bool found = ProbeWall(out Vector3 probedNormal, out float gap);
if (found)
{
@@ -341,9 +281,8 @@ public class PlayerCharacterController : MonoBehaviour
}
else
{
- // The body-center ray lost the wall. If we were climbing up (W) and the
- // wall still exists down at the feet, we have reached the top edge:
- // mantle up and over onto the surface instead of dropping.
+ // The center ray lost the wall. If we are climbing up (W) and the wall
+ // still exists at foot level, we reached the top edge -> mantle over.
if (v > 0f && WallInFront(_controller.height * 0.5f))
{
_mantling = true;
@@ -354,7 +293,7 @@ public class PlayerCharacterController : MonoBehaviour
return;
}
- // Otherwise the wall is genuinely gone: leave after the grace period.
+ // Wall genuinely gone: leave after the grace period.
_lostContactTimer += dt;
gap = wallDetachDistance + 1f; // treat as "far" so it cannot block exit
if (_lostContactTimer >= wallExitGrace)
@@ -364,31 +303,30 @@ public class PlayerCharacterController : MonoBehaviour
}
}
- // Build a basis on the wall plane: "up along the wall" and "right".
+ // Basis on the wall plane: "up along the wall" and "right".
Vector3 climbUp = Vector3.ProjectOnPlane(Vector3.up, _wallNormal);
- if (climbUp.sqrMagnitude < 1e-6f) // degenerate on a flat ceiling: fall back to facing
+ if (climbUp.sqrMagnitude < 1e-6f) // degenerate on a flat ceiling
climbUp = Vector3.ProjectOnPlane(transform.forward, _wallNormal);
climbUp.Normalize();
Vector3 right = Vector3.Cross(_wallNormal, climbUp).normalized;
float climbSpeed = moveSpeed * wallClimbSpeedMultiplier;
- // A/D always strafe left/right along the wall.
- Vector3 velocity = right * (h * climbSpeed);
+ Vector3 velocity = right * (h * climbSpeed); // A/D strafe
bool detach = false;
if (v > 0f)
{
- // W: move closer to the wall while there is a gap, otherwise climb up.
+ // W: approach the wall while there is a gap, otherwise climb up.
if (gap > wallContactDistance)
- velocity += -_wallNormal * (v * wallApproachSpeed); // slow approach
+ velocity += -_wallNormal * (v * wallApproachSpeed);
else
- velocity += climbUp * (v * climbSpeed); // climb up
+ velocity += climbUp * (v * climbSpeed);
}
else if (v < 0f)
{
- // S: slowly back away from the wall; detach once far enough.
- velocity += -_wallNormal * (v * wallApproachSpeed); // v < 0 => away from wall
+ // S: back away (v < 0 => away from wall); detach once far enough.
+ velocity += -_wallNormal * (v * wallApproachSpeed);
if (gap >= wallDetachDistance)
detach = true;
}
@@ -400,11 +338,8 @@ public class PlayerCharacterController : MonoBehaviour
}
///
- /// Casts a ray from the character center toward the wall (opposite the wall
- /// normal) to find the current surface normal and the gap between the
- /// controller's side and the wall. The player's own colliders (controller and
- /// the paintable mesh collider) are skipped. Returns false if no wall is found
- /// within reach.
+ /// Raycasts from the character center toward the wall to find the current
+ /// normal and the gap to it. Our own colliders are skipped. False = no wall.
///
private bool ProbeWall(out Vector3 normal, out float gap)
{
@@ -422,8 +357,7 @@ public class PlayerCharacterController : MonoBehaviour
bool found = false;
for (int i = 0; i < count; i++)
{
- // Skip our own hierarchy (the CharacterController and the child mesh collider).
- if (_wallProbe[i].collider.transform.IsChildOf(transform))
+ if (_wallProbe[i].collider.transform.IsChildOf(transform)) // skip our own colliders
continue;
if (_wallProbe[i].distance < best)
{
@@ -439,18 +373,15 @@ public class PlayerCharacterController : MonoBehaviour
}
///
- /// Pulls the character up and over the wall's top edge onto the surface.
- /// Phase 1 rises along the wall face until the whole capsule is above the
- /// edge; phase 2 moves forward over the top, after which wall-climb is left
- /// and gravity settles the character onto the surface. A short re-grab
- /// cooldown is armed so the freshly passed edge does not re-trigger a climb.
+ /// Pulls the character up and over the wall's top edge: phase 1 rises until the
+ /// capsule clears the edge, phase 2 moves forward onto the surface, then exits.
+ /// A short re-grab cooldown stops the passed edge from re-triggering a climb.
///
private void MantleOverEdge(float dt)
{
_mantleTimer += dt;
float speed = moveSpeed * wallClimbSpeedMultiplier;
- // Forward direction over the edge: horizontal, opposite the wall normal.
Vector3 forward = -_wallNormal;
forward.y = 0f;
forward = forward.sqrMagnitude > 1e-6f ? forward.normalized : transform.forward;
@@ -475,7 +406,7 @@ public class PlayerCharacterController : MonoBehaviour
}
}
- // Safety: never mantle indefinitely (e.g. if blocked by other geometry).
+ // Safety: never mantle indefinitely (e.g. blocked by other geometry).
if (_mantleTimer >= 2f)
{
_reentryCooldown = 0.3f;
@@ -484,9 +415,8 @@ public class PlayerCharacterController : MonoBehaviour
}
///
- /// Casts a horizontal ray toward the wall from a point
- /// below the controller center, ignoring our own colliders. Used to tell a top
- /// edge (wall still present lower down) from the wall being truly gone.
+ /// Horizontal ray toward the wall from a point below the center. Distinguishes a
+ /// top edge (wall still present lower down) from the wall being truly gone.
///
private bool WallInFront(float downOffset)
{
@@ -517,14 +447,11 @@ public class PlayerCharacterController : MonoBehaviour
}
///
- /// Collects contact normals during .
- /// Wall/ceiling contacts (angle at or above ) feed
- /// the wall-climb detector; among the upward-facing contacts the flattest one
- /// is kept as the ground normal the character is actually standing on.
+ /// Collects contact normals during Move: wall/ceiling contacts feed the
+ /// wall-climb detector, the flattest upward contact becomes the ground normal.
///
private void OnControllerColliderHit(ControllerColliderHit hit)
{
- // Detect walls/ceilings for wall-climbing (steep enough to not be a slope).
if (enableWallClimb)
{
float angle = Vector3.Angle(hit.normal, Vector3.up);
diff --git a/Assets/Scripts/PlayerControlMode.cs b/Assets/Scripts/PlayerControlMode.cs
index 2998da4..35587a6 100644
--- a/Assets/Scripts/PlayerControlMode.cs
+++ b/Assets/Scripts/PlayerControlMode.cs
@@ -1,6 +1,6 @@
///
-/// Mode 1: the player walks with WASD/Space and the camera orbits freely with
-/// the mouse (cursor locked/hidden). Mesh painting is disabled.
+/// Player walks with WASD/Space, the camera orbits freely with the mouse.
+/// Painting is disabled.
///
public class PlayerControlMode : GameMode
{
@@ -8,13 +8,13 @@ public class PlayerControlMode : GameMode
public override void Enter()
{
- if (Manager.Player != null)
+ if (Manager.Player)
Manager.Player.ControlEnabled = true;
- if (Manager.CameraInput != null)
+ if (Manager.CameraInput)
Manager.CameraInput.LookMode = ThirdPersonOrbitInput.OrbitLookMode.AlwaysActive;
- if (Manager.Painter != null)
+ if (Manager.Painter)
Manager.Painter.PaintingEnabled = false;
}
}
diff --git a/Assets/Scripts/ThirdPersonOrbitInput.cs b/Assets/Scripts/ThirdPersonOrbitInput.cs
index 188773f..e19d22f 100644
--- a/Assets/Scripts/ThirdPersonOrbitInput.cs
+++ b/Assets/Scripts/ThirdPersonOrbitInput.cs
@@ -6,30 +6,14 @@ using UnityEngine.InputSystem;
///
/// Feeds mouse input into a so the camera
-/// orbits around the follow target like a classic third-person camera.
-///
-/// How the orbit is driven is controlled by :
-/// * AlwaysActive - rotates with every mouse move, cursor locked/hidden.
-/// * HoldRightMouseButton - rotates only while the right mouse button is held;
-/// the cursor is free otherwise (so the left button can paint).
-/// * Disabled - no rotation, cursor free.
-///
-/// The look mode is normally set by the GameModeManager when switching modes,
-/// but can also be configured directly in the Inspector for standalone use.
-///
-/// Horizontal/vertical mouse movement drives the orbital axes (yaw/pitch); the
-/// mouse wheel zooms by changing the orbit radius. The script reads the new Input
-/// System (Mouse.current / Keyboard.current) when enabled and falls back to the
-/// legacy Input Manager otherwise. The project currently runs the new Input System only.
-///
-/// Attach this to the same GameObject as the CinemachineCamera /
-/// CinemachineOrbitalFollow (the camera rig, not the player).
+/// orbits the follow target. Mouse movement drives yaw/pitch, the wheel zooms.
+/// The orbit behaviour depends on (usually set by the
+/// GameModeManager). Attach to the camera rig (the CinemachineOrbitalFollow).
///
[RequireComponent(typeof(CinemachineOrbitalFollow))]
[DisallowMultipleComponent]
public class ThirdPersonOrbitInput : MonoBehaviour
{
- /// How the camera orbit is driven.
public enum OrbitLookMode
{
/// Rotate with every mouse move; cursor locked/hidden.
@@ -41,46 +25,39 @@ public class ThirdPersonOrbitInput : MonoBehaviour
}
[Header("Look Mode")]
- [Tooltip("How the camera orbit is driven. Usually set by GameModeManager:\n" +
- "AlwaysActive = rotates with every mouse move (cursor locked).\n" +
- "HoldRightMouseButton = rotates only while RMB is held (cursor free otherwise).\n" +
- "Disabled = no rotation.")]
+ [Tooltip("How the camera orbit is driven (usually set by GameModeManager).")]
[SerializeField] private OrbitLookMode lookMode = OrbitLookMode.AlwaysActive;
[Header("Rotation")]
- [Tooltip("Horizontal look sensitivity in degrees of yaw per pixel of mouse movement.")]
+ [Tooltip("Horizontal look sensitivity (degrees of yaw per pixel).")]
public float horizontalSensitivity = 0.2f;
- [Tooltip("Vertical look sensitivity in degrees of pitch per pixel of mouse movement.")]
+ [Tooltip("Vertical look sensitivity (degrees of pitch per pixel).")]
public float verticalSensitivity = 0.15f;
- [Tooltip("If ON, moving the mouse up tilts the camera down (inverted pitch).")]
+ [Tooltip("Invert vertical look (mouse up tilts the camera down).")]
public bool invertY = false;
[Header("Zoom")]
- [Tooltip("If ON, the mouse wheel zooms the camera in and out (changes orbit radius).")]
+ [Tooltip("Mouse wheel zooms the camera in and out (changes orbit radius).")]
public bool enableZoom = true;
- [Tooltip("How many world units the orbit radius changes per mouse wheel notch.")]
+ [Tooltip("World units the orbit radius changes per mouse wheel notch.")]
public float zoomSpeed = 0.5f;
- [Tooltip("Closest the camera can zoom in (orbit radius in units).")]
+ [Tooltip("Closest orbit radius (zoom in).")]
public float minRadius = 1.5f;
- [Tooltip("Farthest the camera can zoom out (orbit radius in units).")]
+ [Tooltip("Farthest orbit radius (zoom out).")]
public float maxRadius = 8f;
[Header("Cursor")]
- [Tooltip("If ON, the cursor is locked and hidden while the camera is actively " +
- "rotating, and freed otherwise so you can use the mouse normally.")]
+ [Tooltip("Lock and hide the cursor while the camera is actively rotating.")]
public bool manageCursor = true;
CinemachineOrbitalFollow _orbit;
- ///
- /// How the camera orbit is currently driven. Setting this also updates the
- /// cursor state immediately (e.g. when GameModeManager switches modes).
- ///
+ /// Current orbit mode. Setting it also refreshes the cursor state.
public OrbitLookMode LookMode
{
get => lookMode;
@@ -90,30 +67,21 @@ public class ThirdPersonOrbitInput : MonoBehaviour
void Awake()
{
_orbit = GetComponent();
- if (_orbit == null)
+ if (!_orbit)
Debug.LogError("[ThirdPersonOrbitInput] No CinemachineOrbitalFollow found on this GameObject.", this);
}
- void OnEnable()
- {
- ApplyCursorState();
- }
+ void OnEnable() => ApplyCursorState();
- void OnDisable()
- {
- // Make sure we never leave the cursor locked/hidden when this is turned off.
- SetCursorLocked(false);
- }
+ void OnDisable() => SetCursorLocked(false);
void Update()
{
- if (_orbit == null)
+ if (!_orbit)
return;
bool rotating = ShouldRotate();
- // Keep the cursor in sync every frame: in HoldRightMouseButton mode the
- // lock state follows the right mouse button.
if (manageCursor)
SetCursorLocked(rotating);
@@ -133,16 +101,13 @@ public class ThirdPersonOrbitInput : MonoBehaviour
float scroll = ReadScroll();
if (!Mathf.Approximately(scroll, 0f))
{
- // One wheel notch = one zoom step. Use only the sign so the step
- // size is exactly zoomSpeed regardless of the platform's raw wheel
- // magnitude (the new Input System reports ~1 per notch, the legacy
- // Windows path used ~120). Scrolling up (positive) zooms in.
+ // One wheel notch = one zoom step (sign only), independent of the
+ // platform's raw wheel magnitude. Scrolling up zooms in.
_orbit.Radius = Mathf.Clamp(_orbit.Radius - Mathf.Sign(scroll) * zoomSpeed, minRadius, maxRadius);
}
}
}
- /// Whether the camera should orbit this frame, based on the look mode.
bool ShouldRotate()
{
switch (lookMode)
@@ -159,10 +124,7 @@ public class ThirdPersonOrbitInput : MonoBehaviour
SetCursorLocked(ShouldRotate());
}
- ///
- /// Adds to an input axis, respecting its range:
- /// wrapping axes (e.g. yaw) wrap around, non-wrapping axes (e.g. pitch) clamp.
- ///
+ /// Adds to an input axis; wrapping axes wrap, others clamp to their range.
static void ApplyAxisDelta(ref InputAxis axis, float amount)
{
float value = axis.Value + amount;
@@ -200,7 +162,6 @@ public class ThirdPersonOrbitInput : MonoBehaviour
Vector2 ReadMouseDelta()
{
#if ENABLE_INPUT_SYSTEM
- // Mouse.current.delta is already the per-frame pixel movement (no deltaTime).
return Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero;
#else
return new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")) * 10f;
@@ -210,8 +171,7 @@ public class ThirdPersonOrbitInput : MonoBehaviour
float ReadScroll()
{
#if ENABLE_INPUT_SYSTEM
- // Raw wheel delta. Only its sign is used in Update (one notch = one zoom
- // step), so the platform-dependent magnitude does not matter here.
+ // Raw wheel delta; only its sign is used (one notch = one zoom step).
return Mouse.current != null ? Mouse.current.scroll.ReadValue().y : 0f;
#else
return Input.GetAxis("Mouse ScrollWheel");