From d9624a31246edbca2a63c4f4ea5cbfb66887e70e Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 21 Jul 2026 12:09:08 +0900 Subject: [PATCH] Report the resolved camera in raycast responses Camera.main can resolve to a different GameObject than the one an agent expects when a scene has multiple MainCamera-tagged cameras. A round5 dogfooding session hit exactly this: raycast kept reporting "No physics hit" and there was no way to tell which camera the ray had actually been cast from. - Add CameraName/CameraPath to RaycastResponse, populated from the same Camera.main GameViewRaycastUtility already resolves - Thread the resolved Camera through GameViewRaycastResult so RaycastTool can report it on both the hit and no-hit paths (a no-hit response needs the camera identity just as much as a hit does, since that's the case round5 actually got stuck on) - Extend RaycastToolTests to cover both paths --- Assets/Tests/Editor/RaycastToolTests.cs | 17 +++++++++++++++++ .../Common/GameView/GameViewRaycastUtility.cs | 9 ++++++--- .../FirstPartyTools/Raycast/RaycastResponse.cs | 2 ++ .../FirstPartyTools/Raycast/RaycastTool.cs | 4 ++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Assets/Tests/Editor/RaycastToolTests.cs b/Assets/Tests/Editor/RaycastToolTests.cs index ccc229076..abca1f7eb 100644 --- a/Assets/Tests/Editor/RaycastToolTests.cs +++ b/Assets/Tests/Editor/RaycastToolTests.cs @@ -78,6 +78,8 @@ public async Task ExecuteAsync_WhenCoordinateIntersectsCollider_ShouldReturnHitA Assert.That(response.Success, Is.True); Assert.That(response.Hit, Is.True); Assert.That(response.HitGameObjectName, Is.EqualTo("RaycastToolTestsCube")); + Assert.That(response.CameraName, Is.EqualTo("RaycastToolTestsCamera")); + Assert.That(response.CameraPath, Does.Contain("RaycastToolTestsCamera")); Assert.That(response.InputCoordinateSystem, Is.EqualTo(UnityCliLoopConstants.COORDINATE_SYSTEM_TOP_LEFT_GAME_VIEW)); Assert.That(response.UnityCoordinateSystem, Is.EqualTo(UnityCliLoopConstants.COORDINATE_SYSTEM_BOTTOM_LEFT_GAME_VIEW)); Assert.That(response.CoordinateConversionFormula, Is.EqualTo(UnityCliLoopConstants.COORDINATE_CONVERSION_FORMULA_GAME_VIEW_INPUT_TO_UNITY)); @@ -101,6 +103,21 @@ public async Task ExecuteAsync_WhenCoordinateMissesCollider_ShouldReturnNoHit() Assert.That(response.HitGameObjectName, Is.Null); } + [Test] + public async Task ExecuteAsync_WhenCoordinateMissesCollider_ShouldStillReportResolvedCamera() + { + // Tests that the resolved Camera.main is reported even on a "No physics hit" response, so an + // agent can tell which camera the ray actually came from instead of assuming Camera.main. + CreateRaycastScene(); + Vector2 inputPosition = new Vector2(0f, 0f); + + RaycastResponse response = await ExecuteRaycast(inputPosition); + + Assert.That(response.Hit, Is.False); + Assert.That(response.CameraName, Is.EqualTo("RaycastToolTestsCamera")); + Assert.That(response.CameraPath, Does.Contain("RaycastToolTestsCamera")); + } + [Test] public async Task ExecuteAsync_WhenCameraIsMissing_ShouldReturnConversionMetadata() { diff --git a/Packages/src/Editor/FirstPartyTools/Common/GameView/GameViewRaycastUtility.cs b/Packages/src/Editor/FirstPartyTools/Common/GameView/GameViewRaycastUtility.cs index cec5d7a3b..d95a26997 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/GameView/GameViewRaycastUtility.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/GameView/GameViewRaycastUtility.cs @@ -20,7 +20,7 @@ internal static GameViewRaycastResult RaycastFromInputPosition( Camera mainCamera = Camera.main; if (mainCamera == null) { - return new GameViewRaycastResult(false, conversion, new RaycastHit[0]); + return new GameViewRaycastResult(false, conversion, new RaycastHit[0], null); } Ray ray = mainCamera.ScreenPointToRay(conversion.InjectedUnityPosition); @@ -34,7 +34,7 @@ internal static GameViewRaycastResult RaycastFromInputPosition( RaycastHit[] hits = Physics.RaycastAll(ray, maxDistance, visibleLayerMask); System.Array.Sort(hits, CompareHitsByDistance); - return new GameViewRaycastResult(true, conversion, hits); + return new GameViewRaycastResult(true, conversion, hits, mainCamera); } private static int CompareHitsByDistance(RaycastHit left, RaycastHit right) @@ -51,15 +51,18 @@ internal readonly struct GameViewRaycastResult public readonly bool CameraFound; public readonly GameViewCoordinateConversion Conversion; public readonly RaycastHit[] Hits; + public readonly Camera Camera; public GameViewRaycastResult( bool cameraFound, GameViewCoordinateConversion conversion, - RaycastHit[] hits) + RaycastHit[] hits, + Camera camera) { CameraFound = cameraFound; Conversion = conversion; Hits = hits; + Camera = camera; } } } diff --git a/Packages/src/Editor/FirstPartyTools/Raycast/RaycastResponse.cs b/Packages/src/Editor/FirstPartyTools/Raycast/RaycastResponse.cs index 38fac4519..3ed7581ed 100644 --- a/Packages/src/Editor/FirstPartyTools/Raycast/RaycastResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/Raycast/RaycastResponse.cs @@ -10,6 +10,8 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools public class RaycastResponse : UnityCliLoopToolResponse { public string Message { get; set; } = ""; + public string? CameraName { get; set; } + public string? CameraPath { get; set; } public bool Hit { get; set; } public string? HitGameObjectName { get; set; } public string? HitGameObjectPath { get; set; } diff --git a/Packages/src/Editor/FirstPartyTools/Raycast/RaycastTool.cs b/Packages/src/Editor/FirstPartyTools/Raycast/RaycastTool.cs index 0228ef291..5d6a45cde 100644 --- a/Packages/src/Editor/FirstPartyTools/Raycast/RaycastTool.cs +++ b/Packages/src/Editor/FirstPartyTools/Raycast/RaycastTool.cs @@ -49,6 +49,8 @@ protected override Task ExecuteAsync(RaycastSchema parameters, noHitResponse.Success = true; noHitResponse.Hit = false; noHitResponse.Message = $"No physics hit at ({inputPosition.x:F1}, {inputPosition.y:F1})."; + noHitResponse.CameraName = raycastResult.Camera.name; + noHitResponse.CameraPath = GameObjectPathUtility.GetFullPath(raycastResult.Camera.gameObject); return Task.FromResult(noHitResponse); } @@ -57,6 +59,8 @@ protected override Task ExecuteAsync(RaycastSchema parameters, response.Success = true; response.Hit = true; response.Message = $"Hit {nearestHit.collider.gameObject.name} at ({inputPosition.x:F1}, {inputPosition.y:F1})."; + response.CameraName = raycastResult.Camera.name; + response.CameraPath = GameObjectPathUtility.GetFullPath(raycastResult.Camera.gameObject); response.HitGameObjectName = nearestHit.collider.gameObject.name; response.HitGameObjectPath = GameObjectPathUtility.GetFullPath(nearestHit.collider.gameObject); response.HitLayer = nearestHit.collider.gameObject.layer;