Files

351 lines
9.3 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#
# © 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()