From ec5e58bf97347a656f34a3e17babcb7f94748362 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Thu, 23 Oct 2025 05:39:03 +0900 Subject: [PATCH 01/12] Add picts --- Resources.meta | 3 + Resources/Packages.meta | 3 + .../com.nowsprinting.test-helper.ui.meta | 8 ++ .../com.nowsprinting.test-helper.ui/eye.png | Bin 0 -> 250 bytes .../eye.png.meta | 130 ++++++++++++++++++ .../eye_slash.png | Bin 0 -> 358 bytes .../eye_slash.png.meta | 130 ++++++++++++++++++ .../hand_slash.png | Bin 0 -> 397 bytes .../hand_slash.png.meta | 130 ++++++++++++++++++ .../com.nowsprinting.test-helper.ui/lock.png | Bin 0 -> 255 bytes .../lock.png.meta | 130 ++++++++++++++++++ package.json | 2 + 12 files changed, 536 insertions(+) create mode 100644 Resources.meta create mode 100644 Resources/Packages.meta create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui.meta create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/eye.png create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/eye.png.meta create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/eye_slash.png create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/eye_slash.png.meta create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/hand_slash.png create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/hand_slash.png.meta create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/lock.png create mode 100644 Resources/Packages/com.nowsprinting.test-helper.ui/lock.png.meta diff --git a/Resources.meta b/Resources.meta new file mode 100644 index 0000000..cf6251b --- /dev/null +++ b/Resources.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 385985186548481c87fb5065d7d95096 +timeCreated: 1761165373 \ No newline at end of file diff --git a/Resources/Packages.meta b/Resources/Packages.meta new file mode 100644 index 0000000..dcec2f0 --- /dev/null +++ b/Resources/Packages.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d01dc5109c714179affe711a73be1953 +timeCreated: 1761172041 \ No newline at end of file diff --git a/Resources/Packages/com.nowsprinting.test-helper.ui.meta b/Resources/Packages/com.nowsprinting.test-helper.ui.meta new file mode 100644 index 0000000..faaa9a8 --- /dev/null +++ b/Resources/Packages/com.nowsprinting.test-helper.ui.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9e19182f2a4a4b4cb32b214baa912bf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/Packages/com.nowsprinting.test-helper.ui/eye.png b/Resources/Packages/com.nowsprinting.test-helper.ui/eye.png new file mode 100644 index 0000000000000000000000000000000000000000..c198760605ba8a20b1e93226c45a5d68e1635776 GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;&H|6fVg?3oArNM~bhqvgP*A$W zHKHUqKdq!Zu_%?nIW?!avREOwq%WEFi0Z XA#GL|V=3#QcOYR;S3j3^P6e)zjW zslj@IvO~IqXTv53Pln3@?hUUSx*SB5cpF!>a4|)8z2^K9Td>up+3Wp*ul}kH7Y~KI zxX5sKxJdPJD6_aN;J(;1;V$bYk@<(^mVdWNWE7}x;cbX-5;%~?AXlf!{p`6s|NkXc z2JTOq)fqTib_;SzPrl7F344$rjF6*2UngBTNg5dxF literal 0 HcmV?d00001 diff --git a/Resources/Packages/com.nowsprinting.test-helper.ui/eye_slash.png.meta b/Resources/Packages/com.nowsprinting.test-helper.ui/eye_slash.png.meta new file mode 100644 index 0000000..05d43e0 --- /dev/null +++ b/Resources/Packages/com.nowsprinting.test-helper.ui/eye_slash.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: ec87b0175f61d4413a622b131274168e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 2 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/Packages/com.nowsprinting.test-helper.ui/hand_slash.png b/Resources/Packages/com.nowsprinting.test-helper.ui/hand_slash.png new file mode 100644 index 0000000000000000000000000000000000000000..fcfc8b7a07ca15e2228c50049106da39974148a3 GIT binary patch literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;&H|6fVg?3oArNM~bhqvgP*A$W zHKHUqKdq!Zu_%?nIW?!avREOwq%&c*yQ;aI*_Ws1|(ZQGbYHiE#51~1p1NfPtfuK1g4S~&+W z>`$<)4Ue$ly&$~mL+58FhB*q%f69++;fgvW=HPAd;-b$c9*5?tIrwV8`%oS2sVJL6fQS91~ z{d4yOSxtr)u6u4MG}NE+;SXZi?XZWPfuV%?5r3LI^MQ8^KYOM!ZMiRSAmqR&Yla8x l4E5Oz&WxeVFoPKw8ic#o<}ilLZU%Xg!PC{xWt~$(69CCUQT6}; literal 0 HcmV?d00001 diff --git a/Resources/Packages/com.nowsprinting.test-helper.ui/lock.png.meta b/Resources/Packages/com.nowsprinting.test-helper.ui/lock.png.meta new file mode 100644 index 0000000..b2b5e0a --- /dev/null +++ b/Resources/Packages/com.nowsprinting.test-helper.ui/lock.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 322265e2cbc2d436bb4196c4ae66d9b5 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 2 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 1fd8f5a..4aec908 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "Documentation~", "Editor/", "Editor.meta", + "Resources/", + "Resources.meta", "Runtime/", "Runtime.meta", "Samples~", From 5bf840dc4c70d1fa78d3f0228c1c06a8ede1b013 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Thu, 23 Oct 2025 08:06:16 +0900 Subject: [PATCH 02/12] Add DebugVisualizer --- .../Strategies/DefaultReachableStrategy.cs | 12 +- Runtime/Strategies/IReachableStrategy.cs | 2 +- Runtime/Visualizers.meta | 3 + Runtime/Visualizers/DefaultDebugVisualizer.cs | 168 ++++++++++++++++++ .../DefaultDebugVisualizer.cs.meta | 3 + Runtime/Visualizers/FadeOutBehaviour.cs | 47 +++++ Runtime/Visualizers/FadeOutBehaviour.cs.meta | 3 + Runtime/Visualizers/IVisualizer.cs | 21 +++ Runtime/Visualizers/IVisualizer.cs.meta | 3 + Tests/Runtime/Visualizers.meta | 3 + .../Visualizers/DefaultDebugVisualizerTest.cs | 87 +++++++++ .../DefaultDebugVisualizerTest.cs.meta | 3 + Tests/Scenes/GameObjectFinderUI.unity | 19 +- 13 files changed, 355 insertions(+), 19 deletions(-) create mode 100644 Runtime/Visualizers.meta create mode 100644 Runtime/Visualizers/DefaultDebugVisualizer.cs create mode 100644 Runtime/Visualizers/DefaultDebugVisualizer.cs.meta create mode 100644 Runtime/Visualizers/FadeOutBehaviour.cs create mode 100644 Runtime/Visualizers/FadeOutBehaviour.cs.meta create mode 100644 Runtime/Visualizers/IVisualizer.cs create mode 100644 Runtime/Visualizers/IVisualizer.cs.meta create mode 100644 Tests/Runtime/Visualizers.meta create mode 100644 Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs create mode 100644 Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs.meta diff --git a/Runtime/Strategies/DefaultReachableStrategy.cs b/Runtime/Strategies/DefaultReachableStrategy.cs index d1b739f..4ecae87 100644 --- a/Runtime/Strategies/DefaultReachableStrategy.cs +++ b/Runtime/Strategies/DefaultReachableStrategy.cs @@ -25,7 +25,7 @@ public class DefaultReachableStrategy : IReachableStrategy /// /// Constructor. /// - /// Function returns the screen position of GameObject + /// Function returns the screen point of GameObject /// Logger set if you need verbose output public DefaultReachableStrategy(Func getScreenPoint = null, ILogger verboseLogger = null) { @@ -33,14 +33,10 @@ public DefaultReachableStrategy(Func getScreenPoint = null, _verboseLogger = verboseLogger; } - /// - /// Returns whether the GameObject is reachable from the user or not and screen position. + /// + /// /// Default implementation uses DefaultScreenPointStrategy, checks whether a raycast from Camera.main to the pivot position passes through. - /// - /// Target GameObject - /// Returns the front-most raycast hit result, even if it can not handle the press event - /// Logger set if you need verbose output - /// True if GameObject is reachable from user, Raycast screen position + /// public bool IsReachable(GameObject gameObject, out RaycastResult raycastResult, ILogger verboseLogger = null) { verboseLogger = verboseLogger ?? _verboseLogger; // If null, use the specified in the constructor. diff --git a/Runtime/Strategies/IReachableStrategy.cs b/Runtime/Strategies/IReachableStrategy.cs index 5e3c509..57eb5c7 100644 --- a/Runtime/Strategies/IReachableStrategy.cs +++ b/Runtime/Strategies/IReachableStrategy.cs @@ -13,7 +13,7 @@ namespace TestHelper.UI.Strategies public interface IReachableStrategy { /// - /// Returns whether the GameObject is reachable from the user or not and screen position. + /// Returns whether the GameObject is reachable from the user or not and screen point. /// /// Target GameObject /// Returns the front-most raycast hit result, even if it can not handle the press event diff --git a/Runtime/Visualizers.meta b/Runtime/Visualizers.meta new file mode 100644 index 0000000..5381609 --- /dev/null +++ b/Runtime/Visualizers.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 523346cba6b945f49dea28c9d4fa65f2 +timeCreated: 1761167766 \ No newline at end of file diff --git a/Runtime/Visualizers/DefaultDebugVisualizer.cs b/Runtime/Visualizers/DefaultDebugVisualizer.cs new file mode 100644 index 0000000..a839e17 --- /dev/null +++ b/Runtime/Visualizers/DefaultDebugVisualizer.cs @@ -0,0 +1,168 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using TestHelper.UI.Strategies; +using UnityEngine; +using UnityEngine.UI; +using Object = UnityEngine.Object; + +namespace TestHelper.UI.Visualizers +{ + /// + /// Implementation of visualizers for debugging that use default pictograms. + /// + [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] + public class DefaultDebugVisualizer : IVisualizer + { + private const string ResourcesBasePath = "Packages/com.nowsprinting.test-helper.ui"; + + /// + /// Image file path with the "not reachable" state in the Resources folder. + /// + public string NotReachablePictPath { private get; set; } = $"{ResourcesBasePath}/eye_slash"; + + /// + /// Image color for the "not reachable" state. + /// + public Color NotReachablePictColor { private get; set; } = new Color(1f, 0f, 0f); + + /// + /// Indicate color for the blocker. + /// + public Color NotReachableBlockerColor { private get; set; } = new Color(1f, 1f, 1f); + + /// + /// Screen resolution (short side) for which the pictograms size is intended. + /// + public int ReferenceScreenResolutionShortSide { private get; set; } = 480; + + /// + /// Overlay Canvas sorting order. + /// This can only be specified before the first pictogram is shown. + /// + public int CanvasSortingOrder { private get; set; } = 1000; + + /// + /// Indicator lifetime in seconds. + /// + public float IndicatorLifetime { private get; set; } = 1.0f; + + /// + /// Function of get screen point. + /// + public Func GetScreenPoint { private get; set; } = + DefaultScreenPointStrategy.GetScreenPoint; + + private readonly Dictionary _pics = new Dictionary(); + private Canvas _overlayCanvas; + + /// + public void ShowNotReachableIndicator(Vector2 screenPoint, GameObject blocker = null) + { + try + { + if (blocker && blocker.TryGetComponent(out var rectTransform)) + { + var blockerIndicator = CreateBlockerIndicator(rectTransform); + blockerIndicator.transform.position = GetScreenPoint(blocker); + } + // TODO: 3D objects + + var indicator = CreateIndicator(NotReachablePictPath, NotReachablePictColor); + indicator.transform.position = screenPoint; + } + catch (Exception e) + { + Debug.LogWarning($"Failed to show not reachable indicator: {e}"); + } + } + + private GameObject CreateBlockerIndicator(RectTransform blockerRectTransform) + { + var indicator = new GameObject($"Blocker Indicator", typeof(Image), + typeof(FadeOutBehaviour)); + indicator.transform.SetParent(GetOrCreateOverlayCanvas().transform); + + var rectTransform = indicator.GetComponent(); + rectTransform.sizeDelta = blockerRectTransform.rect.size * blockerRectTransform.lossyScale; + + var image = indicator.GetComponent(); + image.color = NotReachableBlockerColor; + image.raycastTarget = false; // Disable raycast target to avoid blocking UI interactions + + var fadeout = indicator.GetComponent(); + fadeout.Lifetime = IndicatorLifetime; + fadeout.Acceleration = 0.2f; // Decelerated fade-out + + return indicator; + } + + private GameObject CreateIndicator(string pictPath, Color pictColor) + { + var indicator = new GameObject($"Indicator", typeof(Image), typeof(ContentSizeFitter), + typeof(FadeOutBehaviour)); + indicator.transform.SetParent(GetOrCreateOverlayCanvas().transform); + indicator.transform.localScale = CalcScale(); + // Note: Why not use CanvasScaler? Screen points may move depending on the aspect ratio. + + var image = indicator.GetComponent(); + image.sprite = GetOrCreateSprite(pictPath); + image.color = pictColor; + image.raycastTarget = false; // Disable raycast target to avoid blocking UI interactions + + var contentSizeFitter = indicator.GetComponent(); + contentSizeFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; + contentSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + var fadeout = indicator.GetComponent(); + fadeout.Lifetime = IndicatorLifetime; + fadeout.Acceleration = 2.0f; // Accelerated fade-out + + return indicator; + } + + private Vector3 CalcScale() + { + var shortSide = Math.Min(Screen.width, Screen.height); + var scale = (float)shortSide / ReferenceScreenResolutionShortSide; + return new Vector3(scale, scale, 1f); + } + + private Sprite GetOrCreateSprite(string pictPath) + { + if (_pics.TryGetValue(pictPath, out var sprite) && sprite != null) + { + return sprite; + } + + sprite = Resources.Load(pictPath); + if (sprite == null) + { + throw new InvalidOperationException($"Sprite not found at path: {pictPath}"); + } + + _pics[pictPath] = sprite; + return sprite; + } + + private Canvas GetOrCreateOverlayCanvas() + { + if (_overlayCanvas != null) + { + return _overlayCanvas; + } + + var gameObject = new GameObject("DebugVisualizer Overlay Canvas", typeof(Canvas)); + Object.DontDestroyOnLoad(gameObject); + + _overlayCanvas = gameObject.GetComponent(); + _overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay; + _overlayCanvas.sortingOrder = CanvasSortingOrder; + + return _overlayCanvas; + } + } +} diff --git a/Runtime/Visualizers/DefaultDebugVisualizer.cs.meta b/Runtime/Visualizers/DefaultDebugVisualizer.cs.meta new file mode 100644 index 0000000..6ed3292 --- /dev/null +++ b/Runtime/Visualizers/DefaultDebugVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 000d1761eac54b54817d3982e83901f1 +timeCreated: 1761167840 \ No newline at end of file diff --git a/Runtime/Visualizers/FadeOutBehaviour.cs b/Runtime/Visualizers/FadeOutBehaviour.cs new file mode 100644 index 0000000..e7a736e --- /dev/null +++ b/Runtime/Visualizers/FadeOutBehaviour.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using UnityEngine; +using UnityEngine.UI; + +namespace TestHelper.UI.Visualizers +{ + /// + /// Fade-out behavior for indicators. + /// + [RequireComponent(typeof(Image))] + public class FadeOutBehaviour : MonoBehaviour + { + /// + /// Indicator lifetime in seconds. + /// + public float Lifetime { private get; set; } = 1.0f; + + /// + /// Exponent for acceleration. + /// 1 = linear, + /// >1 = accelerating (slow -> fast), + /// <1 = decelerating. + /// + public float Acceleration { private get; set; } = 1.0f; + + private Image _image; + private float _elapsed; + + private void Start() + { + _image = GetComponent(); + Destroy(gameObject, Lifetime); + } + + private void Update() + { + _elapsed += Time.deltaTime; + var t = Mathf.Clamp01(_elapsed / Lifetime); // 0..1 + var accelerated = Mathf.Pow(t, Acceleration); // 0..1 with acceleration + var color = _image.color; + color.a = 1f - accelerated; + _image.color = color; + } + } +} diff --git a/Runtime/Visualizers/FadeOutBehaviour.cs.meta b/Runtime/Visualizers/FadeOutBehaviour.cs.meta new file mode 100644 index 0000000..96f4be1 --- /dev/null +++ b/Runtime/Visualizers/FadeOutBehaviour.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 898ca7d7177b4ee295cbb5d739246046 +timeCreated: 1761196037 \ No newline at end of file diff --git a/Runtime/Visualizers/IVisualizer.cs b/Runtime/Visualizers/IVisualizer.cs new file mode 100644 index 0000000..2384e87 --- /dev/null +++ b/Runtime/Visualizers/IVisualizer.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using UnityEngine; + +namespace TestHelper.UI.Visualizers +{ + /// + /// Visualizer used for debugging UI tests. + /// For usage, see and . + /// + public interface IVisualizer + { + /// + /// Show the visual indication of the "not reachable" screen point. + /// + /// Screen point of "not reachable". + /// GameObject that blocked the raycaster. + void ShowNotReachableIndicator(Vector2 screenPoint, GameObject blocker = null); + } +} diff --git a/Runtime/Visualizers/IVisualizer.cs.meta b/Runtime/Visualizers/IVisualizer.cs.meta new file mode 100644 index 0000000..4766618 --- /dev/null +++ b/Runtime/Visualizers/IVisualizer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f7518bbbe6104fce9cf7dbab93e6385f +timeCreated: 1761167809 \ No newline at end of file diff --git a/Tests/Runtime/Visualizers.meta b/Tests/Runtime/Visualizers.meta new file mode 100644 index 0000000..7d428a0 --- /dev/null +++ b/Tests/Runtime/Visualizers.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e18f21f295664b12bcf4821fc1193ed4 +timeCreated: 1761247858 \ No newline at end of file diff --git a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs new file mode 100644 index 0000000..2cc320e --- /dev/null +++ b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using TestHelper.Attributes; +using TestHelper.RuntimeInternals; +using UnityEngine; +using UnityEngine.UI; + +namespace TestHelper.UI.Visualizers +{ + [TestFixture] + public class DefaultDebugVisualizerTest + { + private const string TestScenePath = "../../Scenes/Canvas.unity"; + private const float IndicatorLifetime = 0.2f; + private readonly GameObjectFinder _finder = new GameObjectFinder(0.1d); + private readonly IVisualizer _sut = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; + private readonly List _referenceObjects = new List(); + + [SetUp] + public async Task SetUp() + { + var canvas = await _finder.FindByNameAsync("Canvas", reachable: false); + // Note: CanvasScaler settings: Scale With Screen Size, Reference Resolution: 640x480, Match Width Or Height: 0 + + // Create reference images and screen points + _referenceObjects.Clear(); + var anchoredPositions = new Vector2[] + { + new Vector2(-200, -100), + new Vector2(-200, 100), + new Vector2(200, -100), + new Vector2(200, 100) + }; + foreach (var anchoredPosition in anchoredPositions) + { + var image = new GameObject(null, typeof(Image)); + image.transform.SetParent(canvas.GameObject.transform, false); + image.GetComponent().color = Color.gray; + image.GetComponent().anchoredPosition = anchoredPosition; + image.GetComponent().sizeDelta = new Vector2(160, 30); // same as a Button's default + _referenceObjects.Add(image); + } + } + + [Test] + [LoadScene(TestScenePath)] + public async Task ShowNotReachableIndicator_Horizontal([Values] GameViewResolution resolution) + { + var (width, height, name) = resolution.GetParameter(); + GameViewControlHelper.SetResolution(width, height, name); + await UniTask.NextFrame(); + + foreach (var reference in _referenceObjects) + { + var screenPoint = RectTransformUtility.WorldToScreenPoint(null, reference.transform.position); + _sut.ShowNotReachableIndicator(screenPoint, reference); + } + + await ScreenshotHelper.TakeScreenshotAsync(); + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + } + + [Test] + [LoadScene(TestScenePath)] + public async Task ShowNotReachableIndicator_Vertical([Values] GameViewResolution resolution) + { + var (width, height, name) = resolution.GetParameter(); + GameViewControlHelper.SetResolution(height, width, name); // flip + await UniTask.NextFrame(); + + foreach (var reference in _referenceObjects) + { + var screenPoint = RectTransformUtility.WorldToScreenPoint(null, reference.transform.position); + _sut.ShowNotReachableIndicator(screenPoint, reference); + } + + await ScreenshotHelper.TakeScreenshotAsync(); + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + } + } +} diff --git a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs.meta b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs.meta new file mode 100644 index 0000000..858a723 --- /dev/null +++ b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 673de2fba774446f9bbc5f601e80b625 +timeCreated: 1761247876 \ No newline at end of file diff --git a/Tests/Scenes/GameObjectFinderUI.unity b/Tests/Scenes/GameObjectFinderUI.unity index f3593ba..1827102 100644 --- a/Tests/Scenes/GameObjectFinderUI.unity +++ b/Tests/Scenes/GameObjectFinderUI.unity @@ -13,7 +13,7 @@ OcclusionCullingSettings: --- !u!104 &2 RenderSettings: m_ObjectHideFlags: 0 - serializedVersion: 9 + serializedVersion: 10 m_Fog: 0 m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_FogMode: 3 @@ -38,13 +38,12 @@ RenderSettings: m_ReflectionIntensity: 1 m_CustomReflection: {fileID: 0} m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.44657838, g: 0.49641228, b: 0.57481676, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: m_ObjectHideFlags: 0 - serializedVersion: 12 - m_GIWorkflowMode: 1 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 m_GISettings: serializedVersion: 2 m_BounceScale: 1 @@ -67,9 +66,6 @@ LightmapSettings: m_LightmapParameters: {fileID: 0} m_LightmapsBakeMode: 1 m_TextureCompression: 1 - m_FinalGather: 0 - m_FinalGatherFiltering: 1 - m_FinalGatherRayCount: 256 m_ReflectionCompression: 2 m_MixedBakeMode: 2 m_BakeBackend: 1 @@ -1100,7 +1096,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_RaycastTarget: 1 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 @@ -1150,9 +1146,8 @@ Light: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1671282987} m_Enabled: 1 - serializedVersion: 10 + serializedVersion: 11 m_Type: 1 - m_Shape: 0 m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} m_Intensity: 1 m_Range: 10 @@ -1202,8 +1197,12 @@ Light: m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} m_UseBoundingSphereOverride: 0 m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 m_ShadowRadius: 0 m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 --- !u!4 &1671282989 Transform: m_ObjectHideFlags: 0 From f56351b4cc23d6264156b66ff84d2feeac36db36 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 08:43:34 +0900 Subject: [PATCH 03/12] Add visualize "not reachable" to GameObjectFinder --- Runtime/GameObjectFinder.cs | 27 +++- .../Strategies/DefaultReachableStrategy.cs | 2 +- Tests/Runtime/GameObjectFinderTest.cs | 123 +++++++++++++++++- 3 files changed, 148 insertions(+), 4 deletions(-) diff --git a/Runtime/GameObjectFinder.cs b/Runtime/GameObjectFinder.cs index 57932de..e9942a2 100644 --- a/Runtime/GameObjectFinder.cs +++ b/Runtime/GameObjectFinder.cs @@ -10,6 +10,7 @@ using TestHelper.UI.GameObjectMatchers; using TestHelper.UI.Paginators; using TestHelper.UI.Strategies; +using TestHelper.UI.Visualizers; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.EventSystems; @@ -28,6 +29,7 @@ public class GameObjectFinder private readonly double _timeoutSeconds; private readonly IReachableStrategy _reachableStrategy; private readonly Func _isInteractable; + private readonly IVisualizer _visualizer; private const double MinTimeoutSeconds = 0.01d; private const double MaxPollingIntervalSeconds = 1.0d; @@ -38,9 +40,11 @@ public class GameObjectFinder /// Seconds to wait until GameObject appears. /// Strategy to examine whether GameObject is reachable from the user. Default is DefaultReachableStrategy. /// The function returns the Component is interactable or not. Default is DefaultComponentInteractableStrategy.IsInteractable. + /// Visualizer set if you need to show fault indicators. public GameObjectFinder(double timeoutSeconds = 1.0d, IReachableStrategy reachableStrategy = null, - Func isInteractable = null) + Func isInteractable = null, + IVisualizer visualizer = null) { Assert.IsTrue(timeoutSeconds > MinTimeoutSeconds, $"TimeoutSeconds must be greater than {MinTimeoutSeconds}."); @@ -48,6 +52,7 @@ public GameObjectFinder(double timeoutSeconds = 1.0d, _timeoutSeconds = timeoutSeconds; _reachableStrategy = reachableStrategy ?? new DefaultReachableStrategy(); _isInteractable = isInteractable ?? DefaultComponentInteractableStrategy.IsInteractable; + _visualizer = visualizer; } private static Scene GetDontDestroyOnLoadScene() @@ -109,6 +114,24 @@ private enum Reason None } + private List FilterToOnlyReachable(List gameObjects) + { + var reachable = new List(); + foreach (var gameObject in gameObjects) + { + if (_reachableStrategy.IsReachable(gameObject, out var raycastResult)) + { + reachable.Add(gameObject); + } + else + { + _visualizer?.ShowNotReachableIndicator(raycastResult.screenPosition, raycastResult.gameObject); + } + } + + return reachable; + } + private (GameObject, RaycastResult, Reason) FindByMatcher(IGameObjectMatcher matcher, bool reachable, bool interactable, Scene scene = default) { @@ -124,7 +147,7 @@ private enum Reason if (reachable) { - foundObjects = foundObjects.Where(obj => _reachableStrategy.IsReachable(obj, out _)).ToList(); + foundObjects = FilterToOnlyReachable(foundObjects); if (!foundObjects.Any()) { return (null, default, Reason.NotReachable); diff --git a/Runtime/Strategies/DefaultReachableStrategy.cs b/Runtime/Strategies/DefaultReachableStrategy.cs index 4ecae87..43fa14c 100644 --- a/Runtime/Strategies/DefaultReachableStrategy.cs +++ b/Runtime/Strategies/DefaultReachableStrategy.cs @@ -62,7 +62,7 @@ public bool IsReachable(GameObject gameObject, out RaycastResult raycastResult, verboseLogger.Log(message.ToString()); } - raycastResult = default; + raycastResult = new RaycastResult() { screenPosition = pointerEventData.position }; return false; } diff --git a/Tests/Runtime/GameObjectFinderTest.cs b/Tests/Runtime/GameObjectFinderTest.cs index 453b5e1..421d86f 100644 --- a/Tests/Runtime/GameObjectFinderTest.cs +++ b/Tests/Runtime/GameObjectFinderTest.cs @@ -6,16 +6,18 @@ using System.Threading.Tasks; using NUnit.Framework; using TestHelper.Attributes; +using TestHelper.RuntimeInternals; using TestHelper.UI.Exceptions; using TestHelper.UI.Extensions; using TestHelper.UI.GameObjectMatchers; using TestHelper.UI.Paginators; using TestHelper.UI.Strategies; using TestHelper.UI.TestDoubles; -using TestHelper.RuntimeInternals; +using TestHelper.UI.Visualizers; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; +using Is = TestHelper.Constraints.Is; #if !UNITY_2022_1_OR_NEWER using System.IO; #endif @@ -462,5 +464,124 @@ public async Task FindByMatcherAsync_SomePageReturnsNotReachable_SavesReasonAndR } } } + + [TestFixture] + public class Visualizer + { + private const string TestScenePath = "../Scenes/GameObjectFinderUI.unity"; + private const float IndicatorLifetime = 0.2f; + private readonly GameObjectFinder _sut; + + public Visualizer() + { + var visualizer = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; + _sut = new GameObjectFinder(0.1d, visualizer: visualizer); + } + + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_Hit_IndicatorIsNotShown() + { + await _sut.FindByNameAsync("Interactable"); + + try + { + var matcher = new ComponentMatcher(typeof(FadeOutBehaviour)); + await _sut.FindByMatcherAsync(matcher, reachable: false); + Assert.Fail("Indicator should not be shown."); + } + catch (TimeoutException) + { + // pass + } + } + + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_NotRequiredReachable_IndicatorIsNotShown() + { + await _sut.FindByNameAsync("BehindTheWall", reachable: false); + + try + { + var matcher = new ComponentMatcher(typeof(FadeOutBehaviour)); + await _sut.FindByMatcherAsync(matcher, reachable: false); + Assert.Fail("Indicator should not be shown."); + } + catch (TimeoutException) + { + // pass + } + } + + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_NotHit_NotReachableIndicatorIsShown() + { + var target = await _sut.FindByNameAsync("Interactable"); + target.GameObject.GetComponent().raycastTarget = false; + target.GameObject.GetComponentInChildren().raycastTarget = false; + + try + { + await _sut.FindByNameAsync("Interactable", reachable: true); + Assert.Fail("Expected TimeoutException but was not thrown"); + } + catch (TimeoutException) + { + } + + var indicator = GameObject.Find("Indicator"); // exist multiple, so only one + Assert.That(indicator, Is.Not.Null); + Assert.That(indicator.GetComponent().sprite.name, Is.EqualTo("eye_slash")); + Assert.That(indicator.GetComponent().raycastTarget, Is.False); + + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + Assert.That(indicator, Is.Destroyed); + } + + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_Blocked_NotReachableIndicatorIsShown() + { + try + { + await _sut.FindByNameAsync("BehindTheWall", reachable: true); + Assert.Fail("Expected TimeoutException but was not thrown"); + } + catch (TimeoutException) + { + } + + var indicator = GameObject.Find("Indicator"); // exist multiple, so only one + Assert.That(indicator, Is.Not.Null); + Assert.That(indicator.GetComponent().sprite.name, Is.EqualTo("eye_slash")); + Assert.That(indicator.GetComponent().raycastTarget, Is.False); + + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + Assert.That(indicator, Is.Destroyed); + } + + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_Blocked_NotReachableBlockerIndicatorIsShown() + { + try + { + await _sut.FindByNameAsync("BehindTheWall", reachable: true); + Assert.Fail("Expected TimeoutException but was not thrown"); + } + catch (TimeoutException) + { + } + + var indicator = GameObject.Find("Blocker Indicator"); // exist multiple, so only one + Assert.That(indicator, Is.Not.Null); + Assert.That(indicator.GetComponent().raycastTarget, Is.False); + + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + Assert.That(indicator, Is.Destroyed); + } + } } } From d5d1ae880309d05b84332a745a740c8b6d1e253e Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 11:04:10 +0900 Subject: [PATCH 04/12] Add visualize "not interactable" to GameObjectFinder --- Runtime/GameObjectFinder.cs | 20 +++++++++- Runtime/Visualizers/DefaultDebugVisualizer.cs | 24 +++++++++++ Runtime/Visualizers/IVisualizer.cs | 6 +++ Tests/Runtime/GameObjectFinderTest.cs | 40 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/Runtime/GameObjectFinder.cs b/Runtime/GameObjectFinder.cs index e9942a2..b2826e1 100644 --- a/Runtime/GameObjectFinder.cs +++ b/Runtime/GameObjectFinder.cs @@ -132,6 +132,24 @@ private List FilterToOnlyReachable(List gameObjects) return reachable; } + private List FilterToOnlyInteractable(List gameObjects) + { + var interactable = new List(); + foreach (var gameObject in gameObjects) + { + if (gameObject.GetComponents().Any(_isInteractable)) + { + interactable.Add(gameObject); + } + else + { + _visualizer?.ShowNotInteractableIndicator(gameObject); + } + } + + return interactable; + } + private (GameObject, RaycastResult, Reason) FindByMatcher(IGameObjectMatcher matcher, bool reachable, bool interactable, Scene scene = default) { @@ -156,7 +174,7 @@ private List FilterToOnlyReachable(List gameObjects) if (interactable) { - foundObjects = foundObjects.Where(obj => obj.GetComponents().Any(_isInteractable)).ToList(); + foundObjects = FilterToOnlyInteractable(foundObjects); if (!foundObjects.Any()) { return (null, default, Reason.NotInteractable); diff --git a/Runtime/Visualizers/DefaultDebugVisualizer.cs b/Runtime/Visualizers/DefaultDebugVisualizer.cs index a839e17..0a88ab5 100644 --- a/Runtime/Visualizers/DefaultDebugVisualizer.cs +++ b/Runtime/Visualizers/DefaultDebugVisualizer.cs @@ -34,6 +34,16 @@ public class DefaultDebugVisualizer : IVisualizer /// public Color NotReachableBlockerColor { private get; set; } = new Color(1f, 1f, 1f); + /// + /// Image file path with the "not interactable" state in the Resources folder. + /// + public string NotInteractablePictPath { private get; set; } = $"{ResourcesBasePath}/hand_slash"; + + /// + /// Image color for the "not interactable" state. + /// + public Color NotInteractablePictColor { private get; set; } = new Color(1f, 0f, 0f); + /// /// Screen resolution (short side) for which the pictograms size is intended. /// @@ -80,6 +90,20 @@ public void ShowNotReachableIndicator(Vector2 screenPoint, GameObject blocker = } } + /// + public void ShowNotInteractableIndicator(GameObject gameObject) + { + try + { + var indicator = CreateIndicator(NotInteractablePictPath, NotInteractablePictColor); + indicator.transform.position = GetScreenPoint(gameObject); + } + catch (Exception e) + { + Debug.LogWarning($"Failed to show not interactable indicator: {e}"); + } + } + private GameObject CreateBlockerIndicator(RectTransform blockerRectTransform) { var indicator = new GameObject($"Blocker Indicator", typeof(Image), diff --git a/Runtime/Visualizers/IVisualizer.cs b/Runtime/Visualizers/IVisualizer.cs index 2384e87..31bf751 100644 --- a/Runtime/Visualizers/IVisualizer.cs +++ b/Runtime/Visualizers/IVisualizer.cs @@ -17,5 +17,11 @@ public interface IVisualizer /// Screen point of "not reachable". /// GameObject that blocked the raycaster. void ShowNotReachableIndicator(Vector2 screenPoint, GameObject blocker = null); + + /// + /// Show the visual indication of the "not interactable" GameObject. + /// + /// + void ShowNotInteractableIndicator(GameObject gameObject); } } diff --git a/Tests/Runtime/GameObjectFinderTest.cs b/Tests/Runtime/GameObjectFinderTest.cs index 421d86f..b475c42 100644 --- a/Tests/Runtime/GameObjectFinderTest.cs +++ b/Tests/Runtime/GameObjectFinderTest.cs @@ -514,6 +514,24 @@ public async Task FindWithVisualizer_NotRequiredReachable_IndicatorIsNotShown() } } + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_NotRequiredInteractable_IndicatorIsNotShown() + { + await _sut.FindByNameAsync("NotInteractable", interactable: false); + + try + { + var matcher = new ComponentMatcher(typeof(FadeOutBehaviour)); + await _sut.FindByMatcherAsync(matcher, reachable: false); + Assert.Fail("Indicator should not be shown."); + } + catch (TimeoutException) + { + // pass + } + } + [Test] [LoadScene(TestScenePath)] public async Task FindWithVisualizer_NotHit_NotReachableIndicatorIsShown() @@ -582,6 +600,28 @@ public async Task FindWithVisualizer_Blocked_NotReachableBlockerIndicatorIsShown await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life Assert.That(indicator, Is.Destroyed); } + + [Test] + [LoadScene(TestScenePath)] + public async Task FindWithVisualizer_NotInteractable_NotInteractableIndicatorIsShown() + { + try + { + await _sut.FindByNameAsync("NotInteractable", interactable: true); + Assert.Fail("Expected TimeoutException but was not thrown"); + } + catch (TimeoutException) + { + } + + var indicator = GameObject.Find("Indicator"); // exist multiple, so only one + Assert.That(indicator, Is.Not.Null); + Assert.That(indicator.GetComponent().sprite.name, Is.EqualTo("hand_slash")); + Assert.That(indicator.GetComponent().raycastTarget, Is.False); + + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + Assert.That(indicator, Is.Destroyed); + } } } } From 0277528252307496d1b35130c07e1fbedc205a81 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 11:48:49 +0900 Subject: [PATCH 05/12] Make DefaultDebugVisualizer implement IDisposable --- Runtime/Visualizers/DefaultDebugVisualizer.cs | 12 ++++++++++-- Tests/Runtime/GameObjectFinderTest.cs | 16 ++++++++++++---- .../Visualizers/DefaultDebugVisualizerTest.cs | 18 +++++++++++++++--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Runtime/Visualizers/DefaultDebugVisualizer.cs b/Runtime/Visualizers/DefaultDebugVisualizer.cs index 0a88ab5..c5d1066 100644 --- a/Runtime/Visualizers/DefaultDebugVisualizer.cs +++ b/Runtime/Visualizers/DefaultDebugVisualizer.cs @@ -15,7 +15,7 @@ namespace TestHelper.UI.Visualizers /// Implementation of visualizers for debugging that use default pictograms. /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] - public class DefaultDebugVisualizer : IVisualizer + public sealed class DefaultDebugVisualizer : IVisualizer, IDisposable { private const string ResourcesBasePath = "Packages/com.nowsprinting.test-helper.ui"; @@ -69,6 +69,14 @@ public class DefaultDebugVisualizer : IVisualizer private readonly Dictionary _pics = new Dictionary(); private Canvas _overlayCanvas; + public void Dispose() + { + if (_overlayCanvas) + { + Object.Destroy(_overlayCanvas); + } + } + /// public void ShowNotReachableIndicator(Vector2 screenPoint, GameObject blocker = null) { @@ -174,7 +182,7 @@ private Sprite GetOrCreateSprite(string pictPath) private Canvas GetOrCreateOverlayCanvas() { - if (_overlayCanvas != null) + if (_overlayCanvas) { return _overlayCanvas; } diff --git a/Tests/Runtime/GameObjectFinderTest.cs b/Tests/Runtime/GameObjectFinderTest.cs index b475c42..e09d935 100644 --- a/Tests/Runtime/GameObjectFinderTest.cs +++ b/Tests/Runtime/GameObjectFinderTest.cs @@ -470,12 +470,20 @@ public class Visualizer { private const string TestScenePath = "../Scenes/GameObjectFinderUI.unity"; private const float IndicatorLifetime = 0.2f; - private readonly GameObjectFinder _sut; + private DefaultDebugVisualizer _visualizer; + private GameObjectFinder _sut; - public Visualizer() + [OneTimeSetUp] + public void OneTimeSetUp() { - var visualizer = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; - _sut = new GameObjectFinder(0.1d, visualizer: visualizer); + _visualizer = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; + _sut = new GameObjectFinder(0.1d, visualizer: _visualizer); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _visualizer.Dispose(); } [Test] diff --git a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs index 2cc320e..68eb424 100644 --- a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs +++ b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs @@ -18,14 +18,26 @@ public class DefaultDebugVisualizerTest { private const string TestScenePath = "../../Scenes/Canvas.unity"; private const float IndicatorLifetime = 0.2f; - private readonly GameObjectFinder _finder = new GameObjectFinder(0.1d); - private readonly IVisualizer _sut = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; private readonly List _referenceObjects = new List(); + private DefaultDebugVisualizer _sut; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _sut = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _sut.Dispose(); + } [SetUp] public async Task SetUp() { - var canvas = await _finder.FindByNameAsync("Canvas", reachable: false); + var finder = new GameObjectFinder(0.1d); + var canvas = await finder.FindByNameAsync("Canvas", reachable: false); // Note: CanvasScaler settings: Scale With Screen Size, Reference Resolution: 640x480, Match Width Or Height: 0 // Create reference images and screen points From 6862bc9a9b8678840a9c6827cd1c4fe7722faa76 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 14:02:54 +0900 Subject: [PATCH 06/12] Add visualize "not reachable" and "ignored" to Monkey --- Runtime/Monkey.cs | 25 ++++-- Runtime/MonkeyConfig.cs | 6 ++ Runtime/Visualizers/DefaultDebugVisualizer.cs | 24 ++++++ Runtime/Visualizers/IVisualizer.cs | 6 ++ Tests/Runtime/MonkeyTest.cs | 84 ++++++++++++++++++- 5 files changed, 138 insertions(+), 7 deletions(-) diff --git a/Runtime/Monkey.cs b/Runtime/Monkey.cs index 539ca06..6562d7d 100644 --- a/Runtime/Monkey.cs +++ b/Runtime/Monkey.cs @@ -14,6 +14,7 @@ using TestHelper.UI.Operators; using TestHelper.UI.Random; using TestHelper.UI.Strategies; +using TestHelper.UI.Visualizers; using UnityEngine; using UnityEngine.EventSystems; @@ -72,6 +73,7 @@ public static async UniTask Run(MonkeyConfig config, bool oneStepMode = false, config.IgnoreStrategy, config.ReachableStrategy, config.Verbose, + config.Visualizer, cancellationToken); if (didAction) { @@ -152,10 +154,11 @@ private static void SetupOperators(MonkeyConfig config) IIgnoreStrategy ignoreStrategy, IReachableStrategy reachableStrategy, bool verbose = false, + IVisualizer visualizer = null, CancellationToken cancellationToken = default) { - return RunStep(random, logger, interactableComponentsFinder, ignoreStrategy, reachableStrategy, verbose, - cancellationToken); + return RunStep(random, logger, interactableComponentsFinder, ignoreStrategy, reachableStrategy, + verbose, visualizer, cancellationToken); } internal static async UniTask<(bool, int)> RunStep( @@ -165,11 +168,13 @@ private static void SetupOperators(MonkeyConfig config) IIgnoreStrategy ignoreStrategy, IReachableStrategy reachableStrategy, bool verbose = false, + IVisualizer visualizer = null, CancellationToken cancellationToken = default) { var lotteryEntries = GetLotteryEntries(interactableComponentsFinder, verbose ? logger : null).Distinct(); var (selectedObject, selectedOperator, raycastResult) = LotteryOperator( - lotteryEntries, random, ignoreStrategy, reachableStrategy, verbose ? logger : null); + lotteryEntries, random, ignoreStrategy, reachableStrategy, + verbose ? logger : null, visualizer: visualizer); if (selectedObject == null || selectedOperator == null) { return (false, 0); @@ -220,15 +225,23 @@ internal static (GameObject, IOperator, RaycastResult) LotteryOperator( IRandom random, IIgnoreStrategy ignoreStrategy, IReachableStrategy reachableStrategy, - ILogger verboseLogger = null) + ILogger verboseLogger = null, + IVisualizer visualizer = null) { var operatorList = operators.ToList(); while (operatorList.Count > 0) { var (selectedObject, selectedOperator) = operatorList[random.Next(operatorList.Count)]; - if (!ignoreStrategy.IsIgnored(selectedObject, verboseLogger) && - reachableStrategy.IsReachable(selectedObject, out var raycastResult, verboseLogger)) + if (ignoreStrategy.IsIgnored(selectedObject, verboseLogger)) + { + visualizer?.ShowIgnoredIndicator(selectedObject); + } + else if (!reachableStrategy.IsReachable(selectedObject, out var raycastResult, verboseLogger)) + { + visualizer?.ShowNotReachableIndicator(raycastResult.screenPosition, raycastResult.gameObject); + } + else { return (selectedObject, selectedOperator, raycastResult); } diff --git a/Runtime/MonkeyConfig.cs b/Runtime/MonkeyConfig.cs index a78b111..111f029 100644 --- a/Runtime/MonkeyConfig.cs +++ b/Runtime/MonkeyConfig.cs @@ -7,6 +7,7 @@ using TestHelper.UI.Exceptions; using TestHelper.UI.Operators; using TestHelper.UI.Strategies; +using TestHelper.UI.Visualizers; using UnityEngine; namespace TestHelper.UI @@ -54,6 +55,11 @@ public class MonkeyConfig /// public bool Verbose { get; set; } + /// + /// Show the visual indication if set a instance. + /// + public IVisualizer Visualizer { get; set; } + /// /// Show Gizmos on GameView during running monkey test if true /// diff --git a/Runtime/Visualizers/DefaultDebugVisualizer.cs b/Runtime/Visualizers/DefaultDebugVisualizer.cs index c5d1066..0ebe468 100644 --- a/Runtime/Visualizers/DefaultDebugVisualizer.cs +++ b/Runtime/Visualizers/DefaultDebugVisualizer.cs @@ -44,6 +44,16 @@ public sealed class DefaultDebugVisualizer : IVisualizer, IDisposable /// public Color NotInteractablePictColor { private get; set; } = new Color(1f, 0f, 0f); + /// + /// Image file path with the "ignored" state in the Resources folder. + /// + public string IgnoredPictPath { private get; set; } = $"{ResourcesBasePath}/lock"; + + /// + /// Image color for the "ignored" state. + /// + public Color IgnoredPictColor { private get; set; } = new Color(1f, 0.68f, 0f); + /// /// Screen resolution (short side) for which the pictograms size is intended. /// @@ -112,6 +122,20 @@ public void ShowNotInteractableIndicator(GameObject gameObject) } } + /// + public void ShowIgnoredIndicator(GameObject gameObject) + { + try + { + var indicator = CreateIndicator(IgnoredPictPath, IgnoredPictColor); + indicator.transform.position = GetScreenPoint(gameObject); + } + catch (Exception e) + { + Debug.LogWarning($"Failed to show ignored indicator: {e}"); + } + } + private GameObject CreateBlockerIndicator(RectTransform blockerRectTransform) { var indicator = new GameObject($"Blocker Indicator", typeof(Image), diff --git a/Runtime/Visualizers/IVisualizer.cs b/Runtime/Visualizers/IVisualizer.cs index 31bf751..bf69ee2 100644 --- a/Runtime/Visualizers/IVisualizer.cs +++ b/Runtime/Visualizers/IVisualizer.cs @@ -23,5 +23,11 @@ public interface IVisualizer /// /// void ShowNotInteractableIndicator(GameObject gameObject); + + /// + /// Show the visual indication of the "ignored" GameObject. + /// + /// + void ShowIgnoredIndicator(GameObject gameObject); } } diff --git a/Tests/Runtime/MonkeyTest.cs b/Tests/Runtime/MonkeyTest.cs index 78ad00e..0a04ea9 100644 --- a/Tests/Runtime/MonkeyTest.cs +++ b/Tests/Runtime/MonkeyTest.cs @@ -18,8 +18,11 @@ using TestHelper.UI.Operators; using TestHelper.UI.Strategies; using TestHelper.UI.TestDoubles; +using TestHelper.UI.Visualizers; using UnityEngine; using UnityEngine.TestTools; +using UnityEngine.UI; +using Is = TestHelper.Constraints.Is; namespace TestHelper.UI { @@ -343,7 +346,7 @@ public void LotteryOperator_BingoReachableComponent_ReturnOperator() [UnityPlatform(RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor, RuntimePlatform.LinuxEditor)] public class Screenshots { - private const int FileSizeThreshold = 5441; // VGA size solid color file size + private const int FileSizeThreshold = 5441; // VGA size solid color file size private readonly string _defaultOutputDirectory = CommandLineArgs.GetScreenshotDirectory(); private string _filename; private string _path; @@ -610,6 +613,85 @@ public void LotteryOperator_NotReachableObjectOnly_LogNotLottery() } } + [TestFixture] + public class Visualizer + { + private const float IndicatorLifetime = 0.2f; + private DefaultDebugVisualizer _visualizer; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + _visualizer = new DefaultDebugVisualizer() { IndicatorLifetime = IndicatorLifetime }; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _visualizer.Dispose(); + } + + [Test] + [LoadScene("../Scenes/PhysicsRaycasterSandbox.unity")] + public async Task LotteryOperator_IgnoredObjectOnly_IgnoredIndicatorIsShown() + { + var cube = GameObject.Find("Cube"); + cube.AddComponent(); + cube.transform.position = new Vector3(0, 0, 0); + cube.AddComponent(); // ignored + + await UniTask.NextFrame(); + + var operators = new List<(GameObject, IOperator)> { (cube, new UguiClickOperator()), }; + var random = new RandomWrapper(); + var ignoreStrategy = new DefaultIgnoreStrategy(); + var reachableStrategy = new DefaultReachableStrategy(); + Monkey.LotteryOperator(operators, random, ignoreStrategy, reachableStrategy, visualizer: _visualizer); + + await UniTask.DelayFrame(5); + await ScreenshotHelper.TakeScreenshotAsync(); + + var indicator = GameObject.Find("Indicator"); // exist multiple, so only one + Assert.That(indicator, Is.Not.Null); + Assert.That(indicator.GetComponent().sprite.name, Is.EqualTo("lock")); + Assert.That(indicator.GetComponent().raycastTarget, Is.False); + + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + Assert.That(indicator, Is.Destroyed); + } + + [Test] + [LoadScene("../Scenes/PhysicsRaycasterSandbox.unity")] + public async Task LotteryOperator_NotReachableObjectOnly_NotReachableIndicatorIsShown() + { + var cube = GameObject.Find("Cube"); + cube.AddComponent(); + cube.transform.position = new Vector3(0, 0, 0); + + var blocker = GameObject.CreatePrimitive(PrimitiveType.Quad); + blocker.transform.position = new Vector3(0, 1, -9); + + await UniTask.NextFrame(); + + var operators = new List<(GameObject, IOperator)> { (cube, new UguiClickOperator()), }; + var random = new RandomWrapper(); + var ignoreStrategy = new DefaultIgnoreStrategy(); + var reachableStrategy = new DefaultReachableStrategy(); + Monkey.LotteryOperator(operators, random, ignoreStrategy, reachableStrategy, visualizer: _visualizer); + + await UniTask.DelayFrame(5); + await ScreenshotHelper.TakeScreenshotAsync(); + + var indicator = GameObject.Find("Indicator"); // exist multiple, so only one + Assert.That(indicator, Is.Not.Null); + Assert.That(indicator.GetComponent().sprite.name, Is.EqualTo("eye_slash")); + Assert.That(indicator.GetComponent().raycastTarget, Is.False); + + await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life + Assert.That(indicator, Is.Destroyed); + } + } + [TestFixture] [GameViewResolution(GameViewResolution.VGA)] public class DetectingInfiniteLoop From fa60da02b5067cff0fd939550558a3c9021adda0 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 16:51:08 +0900 Subject: [PATCH 07/12] Fix README.md --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index bf7d0b6..1b4aac2 100644 --- a/README.md +++ b/README.md @@ -587,6 +587,18 @@ If multiple `GameObjects` matching the condition are found, throw `MultipleGameO Multiple GameObjects matching the condition (name=Button) were found. ``` +#### Debug Visualizer + +Using the Debug Visualizer can help you investigate why a `GameObject` cannot be found. +`DefaultDebugVisualizer` shows visual indicators when "not reachable" or "not interactable" occurs. + +To use it, simply pass an instance to the `GameObjectFinder` constructor, like this: + +```csharp +var visualizer = new DefaultDebugVisualizer(); +var finder = new GameObjectFinder(visualizer: _visualizer); +``` + ### Monkey @@ -699,6 +711,21 @@ If this condition persists, a `TimeoutException` will be thrown. Lottery entries are empty or all of not reachable. ``` +#### Debug Visualizer + +Using the Debug Visualizer can help you investigate why a `GameObject` cannot be operation. +`DefaultDebugVisualizer` shows visual indicators when "not reachable" or "ignored" occurs. + +To use it, simply set an instance to the `ConkeyConfig.Visualizer`, like this: + +```csharp +var config = new MonkeyConfig() +{ + Visualizer = new DefaultDebugVisualizer(), +}; +await Monkey.Run(config); +``` + ## Installation From 229bca336b8492f4bee504bcb12cca8c29545f35 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 17:26:25 +0900 Subject: [PATCH 08/12] Fix wording --- README.md | 4 ++-- Runtime/MonkeyConfig.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1b4aac2..58d44f0 100644 --- a/README.md +++ b/README.md @@ -713,10 +713,10 @@ Lottery entries are empty or all of not reachable. #### Debug Visualizer -Using the Debug Visualizer can help you investigate why a `GameObject` cannot be operation. +Using the Debug Visualizer can help you investigate why a `GameObject` cannot be operated on. `DefaultDebugVisualizer` shows visual indicators when "not reachable" or "ignored" occurs. -To use it, simply set an instance to the `ConkeyConfig.Visualizer`, like this: +To use it, simply set an instance to the `MonkeyConfig.Visualizer`, like this: ```csharp var config = new MonkeyConfig() diff --git a/Runtime/MonkeyConfig.cs b/Runtime/MonkeyConfig.cs index 111f029..c63785d 100644 --- a/Runtime/MonkeyConfig.cs +++ b/Runtime/MonkeyConfig.cs @@ -56,7 +56,7 @@ public class MonkeyConfig public bool Verbose { get; set; } /// - /// Show the visual indication if set a instance. + /// Show the visual indication if set an instance. /// public IVisualizer Visualizer { get; set; } From 58c847aaea24402c0402adfb1d162a59fbb0d9cb Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 17:51:00 +0900 Subject: [PATCH 09/12] Fix tests --- Tests/Runtime/MonkeyTest.cs | 2 -- Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/Tests/Runtime/MonkeyTest.cs b/Tests/Runtime/MonkeyTest.cs index 0a04ea9..f70df3a 100644 --- a/Tests/Runtime/MonkeyTest.cs +++ b/Tests/Runtime/MonkeyTest.cs @@ -649,7 +649,6 @@ public async Task LotteryOperator_IgnoredObjectOnly_IgnoredIndicatorIsShown() Monkey.LotteryOperator(operators, random, ignoreStrategy, reachableStrategy, visualizer: _visualizer); await UniTask.DelayFrame(5); - await ScreenshotHelper.TakeScreenshotAsync(); var indicator = GameObject.Find("Indicator"); // exist multiple, so only one Assert.That(indicator, Is.Not.Null); @@ -680,7 +679,6 @@ public async Task LotteryOperator_NotReachableObjectOnly_NotReachableIndicatorIs Monkey.LotteryOperator(operators, random, ignoreStrategy, reachableStrategy, visualizer: _visualizer); await UniTask.DelayFrame(5); - await ScreenshotHelper.TakeScreenshotAsync(); var indicator = GameObject.Find("Indicator"); // exist multiple, so only one Assert.That(indicator, Is.Not.Null); diff --git a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs index 68eb424..b403aab 100644 --- a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs +++ b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs @@ -74,7 +74,6 @@ public async Task ShowNotReachableIndicator_Horizontal([Values] GameViewResoluti _sut.ShowNotReachableIndicator(screenPoint, reference); } - await ScreenshotHelper.TakeScreenshotAsync(); await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life } @@ -92,7 +91,6 @@ public async Task ShowNotReachableIndicator_Vertical([Values] GameViewResolution _sut.ShowNotReachableIndicator(screenPoint, reference); } - await ScreenshotHelper.TakeScreenshotAsync(); await Task.Delay(TimeSpan.FromSeconds(IndicatorLifetime)); // wait for end of life } } From 62bc812e83ff1aee460d7b3670dc863f5b0dcb35 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 20:01:00 +0900 Subject: [PATCH 10/12] Fix tests --- Tests/Runtime/MonkeyTest.cs | 15 +++++++-------- .../Visualizers/DefaultDebugVisualizerTest.cs | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Tests/Runtime/MonkeyTest.cs b/Tests/Runtime/MonkeyTest.cs index f70df3a..795df81 100644 --- a/Tests/Runtime/MonkeyTest.cs +++ b/Tests/Runtime/MonkeyTest.cs @@ -616,7 +616,7 @@ public void LotteryOperator_NotReachableObjectOnly_LogNotLottery() [TestFixture] public class Visualizer { - private const float IndicatorLifetime = 0.2f; + private const float IndicatorLifetime = 0.5f; private DefaultDebugVisualizer _visualizer; [OneTimeSetUp] @@ -636,11 +636,10 @@ public void OneTimeTearDown() public async Task LotteryOperator_IgnoredObjectOnly_IgnoredIndicatorIsShown() { var cube = GameObject.Find("Cube"); - cube.AddComponent(); cube.transform.position = new Vector3(0, 0, 0); cube.AddComponent(); // ignored - await UniTask.NextFrame(); + await UniTask.DelayFrame(5); // warm up for physics raycaster (maybe) var operators = new List<(GameObject, IOperator)> { (cube, new UguiClickOperator()), }; var random = new RandomWrapper(); @@ -648,7 +647,7 @@ public async Task LotteryOperator_IgnoredObjectOnly_IgnoredIndicatorIsShown() var reachableStrategy = new DefaultReachableStrategy(); Monkey.LotteryOperator(operators, random, ignoreStrategy, reachableStrategy, visualizer: _visualizer); - await UniTask.DelayFrame(5); + await UniTask.NextFrame(); var indicator = GameObject.Find("Indicator"); // exist multiple, so only one Assert.That(indicator, Is.Not.Null); @@ -664,13 +663,13 @@ public async Task LotteryOperator_IgnoredObjectOnly_IgnoredIndicatorIsShown() public async Task LotteryOperator_NotReachableObjectOnly_NotReachableIndicatorIsShown() { var cube = GameObject.Find("Cube"); - cube.AddComponent(); cube.transform.position = new Vector3(0, 0, 0); var blocker = GameObject.CreatePrimitive(PrimitiveType.Quad); - blocker.transform.position = new Vector3(0, 1, -9); + blocker.transform.position = new Vector3(0, 1, -7); + blocker.GetComponent().materials[0].color = Color.gray; - await UniTask.NextFrame(); + await UniTask.DelayFrame(5); // warm up for physics raycaster (maybe) var operators = new List<(GameObject, IOperator)> { (cube, new UguiClickOperator()), }; var random = new RandomWrapper(); @@ -678,7 +677,7 @@ public async Task LotteryOperator_NotReachableObjectOnly_NotReachableIndicatorIs var reachableStrategy = new DefaultReachableStrategy(); Monkey.LotteryOperator(operators, random, ignoreStrategy, reachableStrategy, visualizer: _visualizer); - await UniTask.DelayFrame(5); + await UniTask.NextFrame(); var indicator = GameObject.Find("Indicator"); // exist multiple, so only one Assert.That(indicator, Is.Not.Null); diff --git a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs index b403aab..1842942 100644 --- a/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs +++ b/Tests/Runtime/Visualizers/DefaultDebugVisualizerTest.cs @@ -82,7 +82,7 @@ public async Task ShowNotReachableIndicator_Horizontal([Values] GameViewResoluti public async Task ShowNotReachableIndicator_Vertical([Values] GameViewResolution resolution) { var (width, height, name) = resolution.GetParameter(); - GameViewControlHelper.SetResolution(height, width, name); // flip + GameViewControlHelper.SetResolution(height, width, $"{name} Portrait"); await UniTask.NextFrame(); foreach (var reference in _referenceObjects) From e6b6d325a823385c7117659282cad341d9fb8c79 Mon Sep 17 00:00:00 2001 From: Koji Hasegawa Date: Fri, 24 Oct 2025 21:00:23 +0900 Subject: [PATCH 11/12] Fix pict --- .../com.nowsprinting.test-helper.ui/lock.png | Bin 255 -> 255 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Resources/Packages/com.nowsprinting.test-helper.ui/lock.png b/Resources/Packages/com.nowsprinting.test-helper.ui/lock.png index 590aeaa33123562d5efc546e799f060d80af2347..cb4149a8aab7c4eaf9d66977d01ae2e2bb087d5a 100644 GIT binary patch delta 120 zcmey*_@8k?Um=4Tn~iX)o`e4>g&+oNRX-1gcPxd^yBMy2Dp#})Vz|& zOBl);EBF~0Ua%dJKV!$%aF_9uq&92De#M5B4NuD%4)8PlyT;JQw2F-Z1{#D7C36_# So=i&wiFmsDxvX)KwVDn|2htHe~ z7 Date: Fri, 24 Oct 2025 22:18:17 +0900 Subject: [PATCH 12/12] Refactor --- Runtime/GameObjectFinder.cs | 54 +++++++++++++++---------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/Runtime/GameObjectFinder.cs b/Runtime/GameObjectFinder.cs index b2826e1..52b614e 100644 --- a/Runtime/GameObjectFinder.cs +++ b/Runtime/GameObjectFinder.cs @@ -114,40 +114,38 @@ private enum Reason None } - private List FilterToOnlyReachable(List gameObjects) + private bool FilterToOnlyReachable(ref List objects) { - var reachable = new List(); - foreach (var gameObject in gameObjects) + for (var i = objects.Count - 1; i >= 0; i--) { - if (_reachableStrategy.IsReachable(gameObject, out var raycastResult)) + var current = objects[i]; + if (_reachableStrategy.IsReachable(current, out var raycastResult)) { - reachable.Add(gameObject); - } - else - { - _visualizer?.ShowNotReachableIndicator(raycastResult.screenPosition, raycastResult.gameObject); + continue; } + + _visualizer?.ShowNotReachableIndicator(raycastResult.screenPosition, raycastResult.gameObject); + objects.RemoveAt(i); } - return reachable; + return objects.Count > 0; } - private List FilterToOnlyInteractable(List gameObjects) + private bool FilterToOnlyInteractable(ref List objects) { - var interactable = new List(); - foreach (var gameObject in gameObjects) + for (var i = objects.Count - 1; i >= 0; i--) { - if (gameObject.GetComponents().Any(_isInteractable)) - { - interactable.Add(gameObject); - } - else + var current = objects[i]; + if (current.GetComponents().Any(c => _isInteractable(c))) { - _visualizer?.ShowNotInteractableIndicator(gameObject); + continue; } + + _visualizer?.ShowNotInteractableIndicator(current); + objects.RemoveAt(i); } - return interactable; + return objects.Count > 0; } private (GameObject, RaycastResult, Reason) FindByMatcher(IGameObjectMatcher matcher, @@ -163,22 +161,14 @@ private List FilterToOnlyInteractable(List gameObjects) return (null, default, Reason.NotFound); } - if (reachable) + if (reachable && !FilterToOnlyReachable(ref foundObjects)) { - foundObjects = FilterToOnlyReachable(foundObjects); - if (!foundObjects.Any()) - { - return (null, default, Reason.NotReachable); - } + return (null, default, Reason.NotReachable); } - if (interactable) + if (interactable && !FilterToOnlyInteractable(ref foundObjects)) { - foundObjects = FilterToOnlyInteractable(foundObjects); - if (!foundObjects.Any()) - { - return (null, default, Reason.NotInteractable); - } + return (null, default, Reason.NotInteractable); } if (foundObjects.Count > 1)