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");