Trim comments and use implicit null checks for Unity objects
Reduce comments to non-obvious "why" notes and convert UnityEngine.Object null checks to implicit bool (if (obj) / if (!obj)). Input System devices (Mouse.current, Keyboard.current) and plain arrays keep explicit != null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
3da613e1a1
commit
f758c96dc5
8 files changed
+220
-427
No files matched your search
@@ -1,58 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Limits the game's frame rate.
|
||||
///
|
||||
/// Sets <see cref="Application.targetFrameRate"/> and adjusts VSync
|
||||
/// (<see cref="QualitySettings.vSyncCount"/>) 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 <see cref="disableVSyncWhenLimiting"/>).
|
||||
///
|
||||
/// 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 <see cref="Application.targetFrameRate"/>. VSync is
|
||||
/// disabled automatically while limiting, because it would otherwise override
|
||||
/// targetFrameRate. Changes apply live (also in Play mode) via OnValidate.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the current limit (FPS + VSync) to the Unity settings.
|
||||
/// Can also be called externally.
|
||||
/// </summary>
|
||||
/// <summary>Applies the current FPS + VSync settings.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
/// <summary>
|
||||
/// Base class for a high-level game mode (state) handled by
|
||||
/// <see cref="GameModeManager"/>. A mode configures the relevant scene systems
|
||||
/// when it becomes active (<see cref="Enter"/>), can clean up when it is left
|
||||
/// (<see cref="Exit"/>) and may run per-frame logic in <see cref="Tick"/>.
|
||||
///
|
||||
/// 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 <see cref="GameModeManager"/>.
|
||||
/// 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().
|
||||
/// </summary>
|
||||
public abstract class GameMode
|
||||
{
|
||||
/// <summary>The owning manager, providing access to the controlled systems.</summary>
|
||||
protected GameModeManager Manager { get; private set; }
|
||||
|
||||
/// <summary>Human-readable name (shown in logs / debug UI).</summary>
|
||||
/// <summary>Human-readable name (shown in logs).</summary>
|
||||
public abstract string DisplayName { get; }
|
||||
|
||||
/// <summary>Wires the mode to its manager. Called once during registration.</summary>
|
||||
internal void Bind(GameModeManager manager) => Manager = manager;
|
||||
|
||||
/// <summary>Called once when this mode becomes the active mode.</summary>
|
||||
/// <summary>Called once when this mode becomes active.</summary>
|
||||
public virtual void Enter() { }
|
||||
|
||||
/// <summary>Called once when this mode stops being the active mode.</summary>
|
||||
/// <summary>Called once when this mode stops being active.</summary>
|
||||
public virtual void Exit() { }
|
||||
|
||||
/// <summary>Called every frame while this mode is the active mode.</summary>
|
||||
/// <summary>Called every frame while this mode is active.</summary>
|
||||
public virtual void Tick() { }
|
||||
}
|
||||
@@ -5,33 +5,21 @@ using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="GameMode"/>; register
|
||||
/// additional ones in <see cref="BuildModes"/>.
|
||||
///
|
||||
/// 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
|
||||
/// <see cref="BuildModes"/>. Attach to a single, persistent scene GameObject.
|
||||
/// </summary>
|
||||
[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<PlayerCharacterController>();
|
||||
if (cameraInput == null) cameraInput = FindFirstObjectByType<ThirdPersonOrbitInput>();
|
||||
if (painter == null) painter = FindFirstObjectByType<MeshPainter>();
|
||||
if (!player) player = FindFirstObjectByType<PlayerCharacterController>();
|
||||
if (!cameraInput) cameraInput = FindFirstObjectByType<ThirdPersonOrbitInput>();
|
||||
if (!painter) painter = FindFirstObjectByType<MeshPainter>();
|
||||
|
||||
BuildModes();
|
||||
}
|
||||
|
||||
+83
-137
@@ -5,46 +5,28 @@ using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Live mesh painting in Play mode.
|
||||
/// Paints a fixed color (brush) onto the mesh's texture with the mouse.
|
||||
///
|
||||
/// TWO BACKENDS (switchable via <see cref="useGpu"/>, 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 <see cref="useGpu"/>): 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.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MeshPainter : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// When false, mouse painting is ignored (the canvas/material stays as-is).
|
||||
/// Toggled by the GameModeManager (painting is only enabled in paint mode).
|
||||
/// </summary>
|
||||
/// <summary>When false, mouse painting is ignored. Toggled by the GameModeManager.</summary>
|
||||
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<int> _dilateScratch = new List<int>(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<Renderer>();
|
||||
_skinned = GetComponent<SkinnedMeshRenderer>();
|
||||
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<MeshCollider>();
|
||||
if (_collider == null) _collider = gameObject.AddComponent<MeshCollider>();
|
||||
if (!_collider) _collider = gameObject.AddComponent<MeshCollider>();
|
||||
_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<MeshFilter>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Builds the base-color texels (copy of the existing material texture or baseColor).</summary>
|
||||
/// <summary>Builds the base-color texels from the existing material texture or baseColor.</summary>
|
||||
private Color32[] BuildBaseColors()
|
||||
{
|
||||
var px = new Color32[textureSize * textureSize];
|
||||
|
||||
Texture sourceTex = null;
|
||||
var shared = _renderer.sharedMaterial;
|
||||
if (shared != 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 ---
|
||||
|
||||
/// <summary>Sets up the desired backend and carries over the initial image content.</summary>
|
||||
private void SetupBackend(bool gpu, Color32[] fromColor32)
|
||||
{
|
||||
if (gpu) SetupGpu(fromColor32);
|
||||
@@ -273,10 +246,9 @@ public class MeshPainter : MonoBehaviour
|
||||
_gpuActive = gpu;
|
||||
}
|
||||
|
||||
/// <summary>Switches the backend at runtime and carries over the currently painted content.</summary>
|
||||
/// <summary>Switches the backend at runtime, carrying over the painted content.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <summary>Raycasts at a screen position and paints a 3D sphere around the mesh hit point.</summary>
|
||||
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) ---
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -4,49 +4,36 @@ using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Simple first/third-person style character controller driven by a
|
||||
/// <see cref="CharacterController"/>: 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".
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool ControlEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>True while the character is in wall-climb mode (stuck to a wall/ceiling).</summary>
|
||||
/// <summary>True while in wall-climb mode.</summary>
|
||||
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<CharacterController>();
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="ApplyWallClimbMovement"/>
|
||||
/// (raycast-based).
|
||||
/// Enters wall-climb mode on any wall/ceiling contact. Leaving while climbing
|
||||
/// is handled in <see cref="ApplyWallClimbMovement"/>.
|
||||
/// </summary>
|
||||
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).
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Casts a horizontal ray toward the wall from a point <paramref name="downOffset"/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private bool WallInFront(float downOffset)
|
||||
{
|
||||
@@ -517,14 +447,11 @@ public class PlayerCharacterController : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects contact normals during <see cref="CharacterController.Move"/>.
|
||||
/// Wall/ceiling contacts (angle at or above <see cref="wallMinAngle"/>) 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.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -6,30 +6,14 @@ using UnityEngine.InputSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Feeds mouse input into a <see cref="CinemachineOrbitalFollow"/> so the camera
|
||||
/// orbits around the follow target like a classic third-person camera.
|
||||
///
|
||||
/// How the orbit is driven is controlled by <see cref="LookMode"/>:
|
||||
/// * 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 <see cref="LookMode"/> (usually set by the
|
||||
/// GameModeManager). Attach to the camera rig (the CinemachineOrbitalFollow).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CinemachineOrbitalFollow))]
|
||||
[DisallowMultipleComponent]
|
||||
public class ThirdPersonOrbitInput : MonoBehaviour
|
||||
{
|
||||
/// <summary>How the camera orbit is driven.</summary>
|
||||
public enum OrbitLookMode
|
||||
{
|
||||
/// <summary>Rotate with every mouse move; cursor locked/hidden.</summary>
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// How the camera orbit is currently driven. Setting this also updates the
|
||||
/// cursor state immediately (e.g. when GameModeManager switches modes).
|
||||
/// </summary>
|
||||
/// <summary>Current orbit mode. Setting it also refreshes the cursor state.</summary>
|
||||
public OrbitLookMode LookMode
|
||||
{
|
||||
get => lookMode;
|
||||
@@ -90,30 +67,21 @@ public class ThirdPersonOrbitInput : MonoBehaviour
|
||||
void Awake()
|
||||
{
|
||||
_orbit = GetComponent<CinemachineOrbitalFollow>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the camera should orbit this frame, based on the look mode.</summary>
|
||||
bool ShouldRotate()
|
||||
{
|
||||
switch (lookMode)
|
||||
@@ -159,10 +124,7 @@ public class ThirdPersonOrbitInput : MonoBehaviour
|
||||
SetCursorLocked(ShouldRotate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds <paramref name="amount"/> to an input axis, respecting its range:
|
||||
/// wrapping axes (e.g. yaw) wrap around, non-wrapping axes (e.g. pitch) clamp.
|
||||
/// </summary>
|
||||
/// <summary>Adds to an input axis; wrapping axes wrap, others clamp to their range.</summary>
|
||||
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");
|
||||
|
||||
Reference in new issue
Block a user