351 lines
9.3 KiB
GDScript
351 lines
9.3 KiB
GDScript
#
|
||
# © 2024-present https://github.com/cengiz-pz
|
||
#
|
||
|
||
@icon("icon.png")
|
||
class_name InlineAdaptiveBanner
|
||
extends Control
|
||
|
||
const MINIMUM_VALID_WIDTH: float = 50.0
|
||
const MINIMUM_CLIP_THRESHOLD: float = 0.05
|
||
const MAXIMUM_CLIP_THRESHOLD: float = 1.0
|
||
const MINIMUM_RESIZE_THRESHOLD: float = 10.0
|
||
const MAXIMUM_RESIZE_THRESHOLD: float = 500.0
|
||
|
||
## Maximum pixel height of the requested ad. If set to -1, height will be automatically determined.
|
||
@export var max_ad_height: int = -1
|
||
|
||
## Minimum portion of the banner’s area (as a percentage) that must be clipped before the banner is considered hidden.
|
||
@export_range(MINIMUM_CLIP_THRESHOLD, MAXIMUM_CLIP_THRESHOLD, 0.05) var clip_threshold: float = 0.1:
|
||
set(a_value):
|
||
clip_threshold = clampf(a_value, MINIMUM_CLIP_THRESHOLD, MAXIMUM_CLIP_THRESHOLD)
|
||
|
||
## Width-change threshold (in pixels) that triggers a banner reload when the ad container is resized by this amount
|
||
## or more.
|
||
@export_range(MINIMUM_RESIZE_THRESHOLD, MAXIMUM_RESIZE_THRESHOLD, 5.0) var resize_threshold: float = 50.0:
|
||
set(a_value):
|
||
resize_threshold = clampf(a_value, MINIMUM_RESIZE_THRESHOLD, MAXIMUM_RESIZE_THRESHOLD)
|
||
|
||
## Path to Admob node.
|
||
@export_node_path("Admob") var admob_path: NodePath
|
||
|
||
## Whether the inline-adaptive banner ad should be automatically loaded.
|
||
@export var auto_load: bool = false
|
||
|
||
@export_category("Ad Unit Identifier")
|
||
## Use ad unit identifier value defined in the Admob node.
|
||
@export var inherit_from_admob_node: bool = false
|
||
|
||
@export_group("Android", "ad_unit_id_android")
|
||
## Ad unit ID of the inline adaptive banner ad to be loaded on the Android platform.
|
||
@export var ad_unit_id_android_debug: String = "ca-app-pub-3940256099942544/2014213617"
|
||
|
||
## Ad unit ID of the inline adaptive banner ad to be loaded on the Android platform.
|
||
@export var ad_unit_id_android_real: String = ""
|
||
|
||
@export_group("iOS", "ad_unit_id_ios")
|
||
## Ad unit ID of the inline adaptive banner ad to be loaded on the iOS platform.
|
||
@export var ad_unit_id_ios_debug: String = "ca-app-pub-3940256099942544/2934735716"
|
||
|
||
## Ad unit ID of the inline adaptive banner ad to be loaded on the iOS platform.
|
||
@export var ad_unit_id_ios_real: String = ""
|
||
|
||
var admob: Admob
|
||
var _ad_unit_id: String
|
||
var ad_id: String = ""
|
||
|
||
var last_width: float
|
||
|
||
var pending_load: bool = false
|
||
var is_position_dirty: bool = false
|
||
var is_banner_visible: bool = false
|
||
var do_ignore_resize: bool = false
|
||
|
||
|
||
func _enter_tree() -> void:
|
||
var __admob: Admob
|
||
if admob_path:
|
||
__admob = get_node(admob_path)
|
||
|
||
if __admob:
|
||
initialize(__admob)
|
||
else:
|
||
push_warning("Admob node not found")
|
||
|
||
|
||
func _exit_tree() -> void:
|
||
if ad_id != "":
|
||
remove_ad()
|
||
|
||
|
||
func _ready() -> void:
|
||
if Engine.is_editor_hint():
|
||
return
|
||
|
||
size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
|
||
last_width = size.x
|
||
resized.connect(_on_resized)
|
||
|
||
|
||
func initialize(a_admob_node: Admob) -> void:
|
||
admob = a_admob_node
|
||
|
||
visibility_changed.connect(_on_visibility_changed)
|
||
admob.banner_ad_loaded.connect(_on_banner_ad_loaded)
|
||
admob.banner_ad_failed_to_load.connect(_on_banner_ad_failed_to_load)
|
||
admob.banner_ad_refreshed.connect(_on_banner_ad_refreshed)
|
||
admob.banner_ad_size_measured.connect(_on_banner_ad_size_measured)
|
||
|
||
if admob.is_initialization_completed:
|
||
if auto_load:
|
||
call_deferred("_deferred_load_ad")
|
||
else:
|
||
admob.initialization_completed.connect(_on_Admob_initialization_completed)
|
||
|
||
if inherit_from_admob_node:
|
||
_ad_unit_id = admob._banner_id
|
||
else:
|
||
if OS.has_feature("ios"):
|
||
if admob.is_real:
|
||
_ad_unit_id = ad_unit_id_ios_real
|
||
else:
|
||
_ad_unit_id = ad_unit_id_ios_debug
|
||
else:
|
||
if admob.is_real:
|
||
_ad_unit_id = ad_unit_id_android_real
|
||
else:
|
||
_ad_unit_id = ad_unit_id_android_debug
|
||
|
||
|
||
func _deferred_load_ad() -> void:
|
||
load_ad()
|
||
_mark_position_dirty()
|
||
|
||
|
||
# --------------------------------------------------------------
|
||
# AD MANIPULATION
|
||
# --------------------------------------------------------------
|
||
|
||
|
||
func load_ad() -> void:
|
||
if _ad_unit_id == "":
|
||
return
|
||
|
||
var req := LoadAdRequest.new()
|
||
req.set_ad_unit_id(_ad_unit_id)
|
||
req.set_ad_size(LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE)
|
||
|
||
if custom_minimum_size.x < MINIMUM_VALID_WIDTH:
|
||
GmpLogger.log_warn("Can't load ad. Invalid inline adaptive banner width: %.1f" % custom_minimum_size.x)
|
||
return
|
||
|
||
req.set_adaptive_width(round(custom_minimum_size.x))
|
||
|
||
if max_ad_height != -1:
|
||
req.set_adaptive_max_height(max_ad_height)
|
||
|
||
req.set_ad_position(LoadAdRequest.AdPosition.CUSTOM)
|
||
|
||
pending_load = true
|
||
admob.load_banner_ad(req)
|
||
|
||
|
||
func show_ad() -> void:
|
||
if not is_banner_visible:
|
||
admob.show_banner_ad(ad_id)
|
||
is_banner_visible = true
|
||
|
||
|
||
func hide_ad() -> void:
|
||
if is_banner_visible:
|
||
admob.hide_banner_ad(ad_id)
|
||
is_banner_visible = false
|
||
|
||
|
||
func move_ad(a_physical_position: Vector2) -> void:
|
||
admob.move_banner_ad(ad_id, a_physical_position.x, a_physical_position.y)
|
||
|
||
|
||
func remove_ad() -> void:
|
||
admob.remove_banner_ad(ad_id)
|
||
is_banner_visible = false
|
||
ad_id = ""
|
||
|
||
|
||
# --------------------------------------------------------------
|
||
# POSITIONING
|
||
# --------------------------------------------------------------
|
||
|
||
|
||
func _process(_delta: float) -> void:
|
||
if is_position_dirty:
|
||
_deferred_update_position()
|
||
is_position_dirty = false
|
||
else:
|
||
update_banner_position()
|
||
|
||
|
||
func _mark_position_dirty() -> void:
|
||
is_position_dirty = true
|
||
call_deferred("_deferred_update_position")
|
||
|
||
|
||
func _deferred_update_position() -> void:
|
||
update_banner_position()
|
||
|
||
|
||
func update_banner_position() -> void:
|
||
if ad_id == "":
|
||
return
|
||
|
||
# Check basic tree visibility (Tabs, hidden parents)
|
||
if not is_visible_in_tree():
|
||
hide_ad()
|
||
return
|
||
|
||
# Smart clipping (visibility logic)
|
||
var is_clipped_out: bool = false
|
||
|
||
if size.x > 1.0 and size.y > 1.0:
|
||
var virtual_rect := get_global_rect()
|
||
var original_area := virtual_rect.get_area()
|
||
var visible_region := virtual_rect
|
||
var parent = get_parent()
|
||
|
||
# Walk up the tree to find ScrollContainers or other clippers
|
||
while parent is Control:
|
||
if parent.clip_contents:
|
||
var parent_rect: Rect2 = parent.get_global_rect()
|
||
var intersection := visible_region.intersection(parent_rect)
|
||
|
||
# No intersection -> fully clipped (100%)
|
||
if intersection.get_area() <= 0:
|
||
is_clipped_out = true
|
||
break
|
||
|
||
visible_region = intersection
|
||
parent = parent.get_parent()
|
||
|
||
# Final check
|
||
if not get_viewport_rect().intersects(virtual_rect):
|
||
is_clipped_out = true
|
||
else:
|
||
# Calculate the percentage clipped
|
||
var visible_area := visible_region.get_area()
|
||
var clipped_ratio := 1.0 - (visible_area / original_area)
|
||
|
||
if clipped_ratio >= clip_threshold:
|
||
is_clipped_out = true
|
||
|
||
if is_clipped_out:
|
||
if is_banner_visible:
|
||
hide_ad()
|
||
else:
|
||
if not is_banner_visible:
|
||
show_ad()
|
||
|
||
move_ad(_godot_to_physical(global_position))
|
||
|
||
|
||
# --------------------------------------------------------------
|
||
# SIGNAL HANDLERS
|
||
# --------------------------------------------------------------
|
||
|
||
|
||
func _on_Admob_initialization_completed(_status: InitializationStatus) -> void:
|
||
if auto_load:
|
||
call_deferred("_deferred_load_ad")
|
||
|
||
|
||
func _on_resized() -> void:
|
||
if ad_id == "":
|
||
return
|
||
|
||
if do_ignore_resize:
|
||
do_ignore_resize = false
|
||
return
|
||
|
||
if abs(size.x - last_width) < resize_threshold:
|
||
return
|
||
|
||
last_width = size.x
|
||
remove_ad()
|
||
call_deferred("_deferred_load_ad")
|
||
|
||
_mark_position_dirty()
|
||
|
||
|
||
func _on_visibility_changed() -> void:
|
||
if ad_id != "":
|
||
if is_visible_in_tree():
|
||
show_ad()
|
||
else:
|
||
hide_ad()
|
||
|
||
|
||
func _on_banner_ad_loaded(loaded_ad_info: AdInfo, _response: ResponseInfo) -> void:
|
||
if !pending_load:
|
||
return
|
||
|
||
if loaded_ad_info.get_load_ad_request().get_ad_size() != LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE:
|
||
return
|
||
|
||
if loaded_ad_info.get_load_ad_request().get_ad_unit_id().casecmp_to(_ad_unit_id) != 0:
|
||
return
|
||
|
||
pending_load = false
|
||
ad_id = loaded_ad_info.get_ad_id()
|
||
|
||
_apply_loaded_size(Vector2(loaded_ad_info.get_measured_width(), loaded_ad_info.get_measured_height()))
|
||
_mark_position_dirty()
|
||
|
||
|
||
func _on_banner_ad_failed_to_load(failed_ad_info: AdInfo, _err: LoadAdError) -> void:
|
||
if failed_ad_info.get_load_ad_request().get_ad_size() != LoadAdRequest.RequestedAdSize.INLINE_ADAPTIVE:
|
||
return
|
||
|
||
if failed_ad_info.get_load_ad_request().get_ad_unit_id().casecmp_to(_ad_unit_id) != 0:
|
||
return
|
||
|
||
if pending_load:
|
||
pending_load = false
|
||
|
||
|
||
func _on_banner_ad_size_measured(ad_info: AdInfo) -> void:
|
||
if ad_info.get_ad_id().casecmp_to(ad_id) == 0:
|
||
_apply_loaded_size(Vector2(ad_info.get_measured_width(), ad_info.get_measured_height()))
|
||
|
||
|
||
func _on_banner_ad_refreshed(refreshed_ad_info: AdInfo, _response: ResponseInfo) -> void:
|
||
if refreshed_ad_info.get_ad_id().casecmp_to(ad_id) == 0:
|
||
hide_ad() # Prevent ad from briefly displaying out of position
|
||
_apply_loaded_size(Vector2(refreshed_ad_info.get_measured_width(), refreshed_ad_info.get_measured_height()))
|
||
_mark_position_dirty()
|
||
|
||
|
||
# --------------------------------------------------------------
|
||
# UTILITY
|
||
# --------------------------------------------------------------
|
||
|
||
|
||
func _apply_loaded_size(a_size: Vector2) -> void:
|
||
do_ignore_resize = true
|
||
|
||
custom_minimum_size = a_size
|
||
size = a_size
|
||
|
||
|
||
func _calculate_scale_factor() -> Vector2:
|
||
var screen_size: Vector2 = DisplayServer.window_get_size() as Vector2
|
||
var virtual_size: Vector2 = get_viewport().get_visible_rect().size
|
||
return screen_size / virtual_size
|
||
|
||
|
||
func _physical_to_godot(a_physical_size: Vector2) -> Vector2:
|
||
return a_physical_size / _calculate_scale_factor()
|
||
|
||
|
||
func _godot_to_physical(a_godot_size: Vector2) -> Vector2:
|
||
return a_godot_size * _calculate_scale_factor()
|