using Azerion.BlueStack.Common; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace Azerion.BlueStack.API { // Struct to store screen space bounds internal struct VisibilityInfo { public float PercentageOfVisibleArea; // Percentage of visible area public float PercentageOfScreenSpace; // Percentage of screen space } internal struct ClickInfo { public Vector3 ClickPosition; public float ClickTime; } internal class ClickEventArgs : EventArgs { public ClickInfo StartClick { get; set; } public ClickInfo EndClick { get; set; } } internal enum HitStatus { Miss, ObstructedHit, UnobstructedHit, } [AddComponentMenu(""), DisallowMultipleComponent, HideInInspector] internal sealed class NativeAdObject : MonoBehaviour { // private const float _timeInterval = 0.1f; // TODO // private const int _iterations = 5; // TODO private float _lastInteractionTime; private ClickInfo _clickInfo; private bool _is2DColliderPresent; private bool _isInteractionStarted; // private bool _isRectTransPresent; // TODO - check again if required internal event EventHandler OnAdDisplay; internal event EventHandler OnAdClick; internal bool IsImpressionChecked; internal bool HasMadeImpression; internal bool IsImpressive; internal bool IsClickable; internal bool IsUIObject; internal float InteractionTime { get; private set; } internal string ObjectTag { get; private set; } private Canvas _canvas; private Canvas CanvasComponent { get { if (_canvas == null) { _canvas = GetComponent(); if (_canvas == null) { _canvas = GetComponentInParent(); } } return _canvas; } } private CanvasRenderer _canvasRenderer; private CanvasRenderer CanvasRendererComponent => _canvasRenderer ??= GetComponent(); private RectTransform _rectTransform; private RectTransform RectTransformComponent => _rectTransform ??= GetComponent(); private Renderer _renderer; private Renderer RendererComponent => _renderer ??= GetComponent(); private Collider2D _collider2D; private Collider2D Collider2DComponent => _collider2D ??= GetComponent(); private Collider _collider; private Collider ColliderComponent => _collider ??= GetComponent(); public void Start() { _lastInteractionTime = 0f; IsImpressionChecked = false; } public void Initialize(string nativeAssetID, bool isImpressive = false, bool isClickable = false) { ObjectTag = nativeAssetID; IsImpressive = isImpressive; IsClickable = isClickable; // Check for Collider or Collider2D if (!HasCollider()) { throw new MissingComponentException("No Collider or Collider2D component found."); } // Check if the GameObject has a Renderer or is a valid UI object if (!ValidateRendererOrUIObject(nativeAssetID)) { throw new InvalidOperationException("Not a valid Ad Object."); } if (IsImpressive) InvokeRepeating("CheckForImpression", 0.0f, TimeInterval()); } ClickInfo _startClick; ClickInfo _endClick; public void Update() { if (!IsClickable) return; // track clickable elements _lastInteractionTime += (Time.deltaTime - _lastInteractionTime) * 0.1f; var down = CrossPlatformInput.GetPrimaryDown(); var up = CrossPlatformInput.GetPrimaryUp(); if (down.Source != InputSourceType.None && down.IsPressed) { _isInteractionStarted = true; _startClick.ClickPosition = down.Position; _startClick.ClickTime = Time.time * 1000f; // Debug.Log($"{ObjectTag} Ad click started via {down.Source} at {down.Position}"); } else if (_isInteractionStarted && up.Source != InputSourceType.None && !up.IsPressed) { _isInteractionStarted = false; _endClick.ClickPosition = up.Position; _endClick.ClickTime = Time.time * 1000f; // Debug.Log($"{ObjectTag} Ad click ended via {up.Source} at {up.Position}"); HitStatus hitStatus = !_is2DColliderPresent ? CheckHitStatus3D(up.Position) : CheckHitStatus2D(up.Position); // Debug.Log($"{ObjectTag} HitStatus: {hitStatus}"); if (hitStatus == HitStatus.UnobstructedHit) { OnAdClick?.Invoke(this, new ClickEventArgs { StartClick = _startClick, EndClick = _endClick }); } } } // Method to get screen space bounds internal VisibilityInfo GetVisibilityInfo() { // Get camera component Camera mainCamera = NativeUtils.GetCamera(); VisibilityInfo visibilityInfo = new VisibilityInfo(); // Create a new struct // Get world space bounds Bounds worldSpaceBounds; if (!_is2DColliderPresent) // Check if 3D object { worldSpaceBounds = ColliderComponent.bounds; } else { worldSpaceBounds = Collider2DComponent.bounds; } // Get max bounds Vector3 maxBounds = NativeUtils.GetBoundsMax(gameObject, mainCamera, worldSpaceBounds); // Get min bounds Vector3 minBounds = NativeUtils.GetBoundsMin(gameObject, mainCamera, worldSpaceBounds); // Calculate screen space bounds float minX = Math.Max(0.0f, minBounds.x); float minY = Math.Max(0.0f, minBounds.y); float maxX = Math.Min(Screen.width, maxBounds.x); float maxY = Math.Min(Screen.height, maxBounds.y); // Increment amounts int xDivisions = (int)((maxX - minX) / 5.0); int yDivisions = (int)((maxY - minY) / 5.0); // Check if center is visible if (xDivisions <= 0 || yDivisions <= 0) { return visibilityInfo; } // Calculate actual area float actualWidth = maxBounds.x - minBounds.x; float actualHeight = maxBounds.y - minBounds.y; // Calculate screen space area float width = maxX - minX; float height = maxY - minY; // Check if height, width are neg if (width <= 0.0f || height <= 0.0f) { return visibilityInfo; } float areaOnScreen = width * height; float percentageOfAreaOnScreen = areaOnScreen / (actualWidth * actualHeight); // visibilityInfo.PercentageOfVisibleArea = !_is2DColliderPresent ? // CalculatePercentageOfVisibleArea3D(minX, maxX, minY, maxY, xDivisions, yDivisions) : // CalculatePercentageOfVisibleArea2D(minX, maxX, minY, maxY, xDivisions, yDivisions); // Set values in Visibility Info visibilityInfo.PercentageOfVisibleArea = CalculatePercentageOfVisibleArea( minX, maxX, minY, maxY, xDivisions, yDivisions, percentageOfAreaOnScreen ); visibilityInfo.PercentageOfScreenSpace = areaOnScreen / (Screen.width * Screen.height); return visibilityInfo; } // Methods to check if the given input vector is obstructed or unobstructed private HitStatus CheckHitStatus2D(Vector3 position) { // Create a ray from the camera to the specified screen point Ray ray = NativeUtils.GetCamera().ScreenPointToRay(position); // Perform a 2D raycast and store the result var rayIntersection2D = Physics2D.GetRayIntersection(ray); // Check if the raycast hit is obstructed by a Collider component if (rayIntersection2D.collider != null) { if (rayIntersection2D.collider == Collider2DComponent) { // Perform a 3D raycast and store the result if (Physics.Raycast(ray, out var hitInfo3D, float.PositiveInfinity)) { // Check if the 3D raycast hit is closer than the 2D raycast hit if (rayIntersection2D.distance >= hitInfo3D.distance) { return HitStatus.ObstructedHit; } } if (IsUIObject) { return CheckHitStatusCanvas(position); } // Return unobstructed hit if no obstructions were found return HitStatus.UnobstructedHit; } // Check if the 2D raycast is hit but Obstructed if(NativeUtils.CheckRayHits2DCollider(ray, Collider2DComponent)) { return HitStatus.ObstructedHit; } } // Check if the gameobject is rendered in the screen space overlay canvas and valid UI Object // else if (NativeUtils.IsRenderedInScreenSpaceOverlayCanvas(gameObject) && IsUIObject) // { // return CheckHitStatusCanvas(position); // } return HitStatus.Miss; } // Reuse these lists to avoid allocations each frame private static readonly List CachedRaycasters = new(); private static readonly List TempResults = new(); private static readonly List AllResults = new(); private static int _frameLastUpdated = -1; /// /// Updates the raycaster cache once per frame /// private static void RefreshRaycasterCache() { int currentFrame = Time.frameCount; if (currentFrame == _frameLastUpdated) return; _frameLastUpdated = currentFrame; CachedRaycasters.Clear(); #if UNITY_6000_0_OR_NEWER GraphicRaycaster[] raycasters = FindObjectsByType(FindObjectsSortMode.None); #else GraphicRaycaster[] raycasters = FindObjectsOfType(); #endif foreach (GraphicRaycaster r in raycasters) { if (r != null && r.isActiveAndEnabled) CachedRaycasters.Add(r); } } /// /// Checks if the ad object is visible, on top, or obstructed; considering Screen Space and World Space canvases /// private HitStatus CheckHitStatusCanvas(Vector3 screenPosition) { EventSystem eventSystem = EventSystem.current; if (eventSystem == null) return HitStatus.Miss; RefreshRaycasterCache(); PointerEventData pointerEventData = new PointerEventData(eventSystem) { position = screenPosition }; AllResults.Clear(); foreach (var raycaster in CachedRaycasters) { if (!raycaster.isActiveAndEnabled) continue; TempResults.Clear(); raycaster.Raycast(pointerEventData, TempResults); // Filter visible and raycastable graphics foreach (var r in TempResults) { if (r.gameObject.TryGetComponent(out Graphic g)) { if (!g.enabled || !g.raycastTarget || g.color.a <= 0.01f) continue; } AllResults.Add(r); } } if (AllResults.Count == 0) return HitStatus.Miss; // Find topmost result RaycastResult? topHit = null; bool hitSelf = false; foreach (var r in AllResults) { if (r.gameObject == gameObject) hitSelf = true; if (topHit == null || r.sortingOrder > topHit.Value.sortingOrder || (r.sortingOrder == topHit.Value.sortingOrder && r.depth > topHit.Value.depth)) { topHit = r; } } if (!hitSelf) return HitStatus.Miss; if (topHit.HasValue && topHit.Value.gameObject == gameObject) return HitStatus.UnobstructedHit; return HitStatus.ObstructedHit; } private HitStatus CheckHitStatus3D(Vector3 position) { // Create a ray from the camera to the input vector Ray ray = NativeUtils.GetCamera().ScreenPointToRay(position); // Initialize variables for raycast hits RaycastHit hitInfo3D; RaycastHit2D rayIntersection2D; // bool isHit = AdObjCollider.Raycast(ray, out hitInfo3D, float.PositiveInfinity); // Check for 3D raycast hit if (Physics.Raycast(ray, out hitInfo3D, float.PositiveInfinity)) { // Check if the hit object is the current object if (hitInfo3D.collider == ColliderComponent) { rayIntersection2D = Physics2D.GetRayIntersection(ray); if (rayIntersection2D.collider != null) { // Check if the 2D raycast hit is closer than the 3D raycast hit if (rayIntersection2D.distance <= hitInfo3D.distance) { return HitStatus.ObstructedHit; } } if (IsUIObject) { return CheckHitStatusCanvas(position); } // Return unobstructed hit if no obstructions were found return HitStatus.UnobstructedHit; } // Check if the 3D raycast is hit but Obstructed if(NativeUtils.CheckRayHits3DCollider(ray, ColliderComponent)) { return HitStatus.ObstructedHit; } } // Check if the gameobject is rendered in the screen space overlay canvas and valid UI Object // else if (NativeUtils.IsRenderedInScreenSpaceOverlayCanvas(gameObject) && IsUIObject) // { // return CheckHitStatusCanvas(position); // } return HitStatus.Miss; } // Method to check for "Unobstructed" Visible Area in screen space private float CalculatePercentageOfVisibleArea( float minX, float maxX, float minY, float maxY, int xDivisions, int yDivisions, float percentageOfAreaOnScreen) { // Initialize variables float numUnobstructedPoints = 0.0f; float totalPoints = 0.0f; float currentX = minX; float currentY = minY; // Increment through screen space while (currentX <= maxX) { while (currentY <= maxY) { // Check if point is unobstructed HitStatus obstructionCheckResult = !_is2DColliderPresent ? CheckHitStatus3D(new Vector3(currentX, currentY, 0.0f)) : CheckHitStatus2D(new Vector3(currentX, currentY, 0.0f)); if (obstructionCheckResult == HitStatus.UnobstructedHit) { numUnobstructedPoints++; totalPoints++; } else if (obstructionCheckResult == HitStatus.ObstructedHit) { totalPoints++; } currentY += yDivisions; } currentX += xDivisions; currentY = minY; } return percentageOfAreaOnScreen * (numUnobstructedPoints / totalPoints); } public void OnDestroy() { base.CancelInvoke(); } public void OnBecameVisible() { if (!IsImpressionChecked && IsImpressive) { InvokeRepeating("CheckForImpression", 1.0f, TimeInterval()); } } public void OnBecameInvisible() => CancelInvoke(); internal void StopImpressionCheck() { base.CancelInvoke(); IsImpressionChecked = true; } // Method to check if a UI element overlaps with the screen private bool DoesUIElementOverlapScreen() { if (RectTransformComponent == null) { Debug.LogWarning(ObjectTag + ": RectTransform component is missing."); return false; } // Create a rect for the screen size Rect screenRect = new Rect(0.0f, 0.0f, Screen.width, Screen.height); // Get the anchored position of the UI element float x = RectTransformComponent.anchoredPosition.x + RectTransformComponent.position.x; float y = Screen.height - RectTransformComponent.position.y - RectTransformComponent.anchoredPosition.y; // Get the size of the UI element Vector2 elementSize = Vector2.Scale(RectTransformComponent.rect.size, RectTransformComponent.lossyScale); // Create a rect for the UI element Rect elementRect = new Rect(x, y, elementSize.x, elementSize.y); // Check if the UI element rect overlaps with the screen rect // Debug.LogWarning("Does screenRect.Overlaps: " + screenRect.Overlaps(elementRect, false)); if (screenRect.Overlaps(elementRect, false)) { return true; } return false; } // Method to check if a gameobject is in the camera's view frustum private bool IsInViewFrustum() { // Check if the gameobject is rendered in the screen space overlay canvas if (!NativeUtils.IsRenderedInScreenSpaceOverlayCanvas(gameObject)) { // Get the camera component Camera mainCamera = NativeUtils.GetCamera(); // Get the bounds of the gameobject Bounds bounds; if (_is2DColliderPresent) { bounds = Collider2DComponent.bounds; } else { bounds = ColliderComponent.bounds; } // Debug.LogWarning("bounds: " + bounds); // Get the view frustum planes Plane[] frustumPlanes = GeometryUtility.CalculateFrustumPlanes(mainCamera); // Check if the gameobject bounds intersect with the view frustum planes // Debug.LogWarning("TestPlanesAABB: " + GeometryUtility.TestPlanesAABB(frustumPlanes, bounds)); if (!GeometryUtility.TestPlanesAABB(frustumPlanes, bounds)) { return false; } // Get the max bounds of the gameobject Vector3 maxBounds = NativeUtils.GetBoundsMax(gameObject, mainCamera, bounds); // Get the min bounds of the gameobject Vector3 minBounds = NativeUtils.GetBoundsMin(gameObject, mainCamera, bounds); float height = Math.Min(Screen.height, maxBounds.y) - Math.Max(0.0f, minBounds.y); float width = Math.Min(Screen.width, maxBounds.x) - Math.Max(0.0f, minBounds.x); // Debug.LogWarning("height: " + height); // Debug.LogWarning("width: " + width); // Check if the gameobject bounds are too small to be visible if (height <= 0.01f * Screen.height || width <= 0.01f * Screen.width) { return false; } } else // When rendered in the screen space overlay canvas { // Check if the UI element overlaps with the screen if (!DoesUIElementOverlapScreen()) { return false; } } return true; } internal void CheckForImpression() { VisibilityInfo visibilityInfo = GetVisibilityInfo(); if (!IsImpressionChecked && IsInViewFrustum()) { if (visibilityInfo.PercentageOfVisibleArea >= 0.5f) // if Visible Area is >= 50% { HasMadeImpression = true; OnAdDisplay?.Invoke(this, EventArgs.Empty); } else { HasMadeImpression = false; } Debug.Log(ObjectTag + " HasMadeImpression: " + HasMadeImpression); } } private float TimeInterval() { return 1.0f; } private bool HasCollider() { // Check for the presence of a Collider or Collider2D component if (ColliderComponent != null) { // Regular 3D collider found, no issues return true; } if (Collider2DComponent != null) { // 2D collider found, set the flag for later use _is2DColliderPresent = true; return true; } // No Collider or Collider2D found return false; } private bool ValidateRendererOrUIObject(string nativeAssetID) { // Check for Renderer component on the GameObject if (RendererComponent != null) { // Debug.Log($"{ObjectTag} RendererComponent.isVisible: {nativeAssetID} : {RendererComponent.isVisible}"); // Throw an exception if the object is not visible if (!RendererComponent.isVisible) { Debug.Log(ObjectTag + " Renderer Component not visible!!"); throw new InvalidOperationException(ObjectTag + " Object is not visible."); } // Valid Renderer present return true; } // If no Renderer, check if the GameObject is a valid UI object IsUIObject = IsValidUIObject(); if (IsUIObject) { return true; } // Neither Renderer nor valid UI object found return false; } private bool IsValidUIObject() { if (CanvasComponent == null) { // throw new MissingComponentException("Unable to find Canvas component in parents."); Debug.LogWarning(ObjectTag + ": Unable to find Canvas component in parents."); return false; } if (RectTransformComponent == null) { // throw new MissingComponentException("RectTransform component is missing."); Debug.LogWarning(ObjectTag + ": RectTransform component is missing."); return false; } if (CanvasRendererComponent == null) { // throw new MissingComponentException("CanvasRenderer component is missing."); Debug.LogWarning(ObjectTag + ": CanvasRenderer component is missing."); return false; } return true; } // TODO: More testing and Improvement required // TODO: BSSDK-401 : Move viewability to container level - may not have a container // In future if want to check the center position or full object inside the viewport // Check if the center is inside the viewport private bool IsCenterInsideViewport() { Rect rect = RectTransformComponent.rect; // Get the center position of the RectTransform in its local coordinates Vector2 centerLocalPosition = new Vector2(rect.center.x, rect.center.y); // Convert the local center position to world position Vector3 centerWorldPosition = RectTransformComponent.TransformPoint(centerLocalPosition); // Debug.Log(ObjectTag + " centerWorldPosition: " + centerWorldPosition); Vector3 centerViewportPoint = NativeUtils.GetCamera().WorldToViewportPoint(centerWorldPosition); // Debug.Log(ObjectTag + " viewportPoints: " + centerViewportPoint); if (centerViewportPoint.x >= 0 && centerViewportPoint.x <= 1 && centerViewportPoint.y >= 0 && centerViewportPoint.y <= 1 && centerViewportPoint.z > 0) { return true; } return false; } // Check if the object is completely inside the viewport private bool IsCompletelyInsideViewport() { Vector3[] corners = new Vector3[4]; RectTransformComponent.GetWorldCorners(corners); foreach (Vector3 corner in corners) { Vector3 cornerViewportPoint = NativeUtils.GetCamera().WorldToViewportPoint(corner); if (cornerViewportPoint.x >= 0 && cornerViewportPoint.x <= 1 && cornerViewportPoint.y >= 0 && cornerViewportPoint.y <= 1 && cornerViewportPoint.z > 0) { return true; } } return false; } } }