← Volver al listado de tecnologías

Integración de Anuncios (AdMob, Unity Ads, etc)

Por: Artiko
defoldadsadmobunity-adsmonetizaciónmobile

Integración de Anuncios (AdMob, Unity Ads, etc)

La monetización a través de publicidad es fundamental para el éxito de los juegos móviles gratuitos. Esta guía te enseñará a integrar diferentes redes publicitarias en tu juego Defold.

Configuración Base para Publicidad

1. Extension Setup

Instalación de Extensiones

# Añadir extensiones al game.project
[dependencies]
https://github.com/defold/extension-admob/archive/main.zip
https://github.com/defold/extension-unityads/archive/main.zip
https://github.com/defold/extension-ironsource/archive/main.zip

Configuración game.project

[android]
# AdMob App ID
manifest = /bundles/android/AndroidManifest.xml

[ios]
# AdMob App ID
infoplist = /bundles/ios/Info.plist

[project]
dependencies = https://github.com/defold/extension-admob/archive/main.zip,https://github.com/defold/extension-unityads/archive/main.zip

2. Ads Manager Base

Sistema de Gestión de Anuncios

-- ads_manager.script
local M = {}

function init(self)
    self.initialized = false
    self.current_provider = nil
    self.providers = {}
    self.ad_queue = {}

    -- Configuración de anuncios
    self.config = {
        enabled = true,
        test_mode = sys.get_config("project.debug", "0") == "1",
        show_delay = 2.0,  -- Delay mínimo entre anuncios
        last_ad_time = 0,
        retry_delay = 30.0,  -- Retry fallido después de 30s
        max_retries = 3
    }

    -- Estadísticas
    self.stats = {
        ads_requested = 0,
        ads_shown = 0,
        ads_clicked = 0,
        ads_failed = 0,
        revenue = 0
    }

    -- Estados de anuncios
    self.ad_states = {
        banner = {loaded = false, showing = false},
        interstitial = {loaded = false, showing = false},
        rewarded = {loaded = false, showing = false}
    }

    print("Ads Manager initialized")
end

function M.initialize_ads(self, providers_config)
    if self.initialized then return end

    self.providers_config = providers_config

    -- Inicializar proveedores en orden de prioridad
    for _, provider_config in ipairs(providers_config) do
        self:initialize_provider(provider_config)
    end

    self.initialized = true
    print("Ads system initialized with " .. #providers_config .. " providers")
end

function M.initialize_provider(self, config)
    local provider = {
        name = config.name,
        priority = config.priority,
        enabled = true,
        initialized = false,
        error_count = 0
    }

    if config.name == "admob" then
        self:initialize_admob(config, provider)
    elseif config.name == "unity" then
        self:initialize_unity_ads(config, provider)
    elseif config.name == "ironsource" then
        self:initialize_ironsource(config, provider)
    end

    table.insert(self.providers, provider)

    -- Ordenar por prioridad
    table.sort(self.providers, function(a, b)
        return a.priority < b.priority
    end)
end

function M.get_active_provider(self)
    for _, provider in ipairs(self.providers) do
        if provider.enabled and provider.initialized then
            return provider
        end
    end
    return nil
end

function M.show_ad(self, ad_type, placement, callback)
    if not self:can_show_ad() then
        if callback then callback(false, "Ad not ready or too soon") end
        return false
    end

    local provider = self:get_active_provider()
    if not provider then
        if callback then callback(false, "No active provider") end
        return false
    end

    self.stats.ads_requested = self.stats.ads_requested + 1

    local ad_data = {
        type = ad_type,
        placement = placement,
        callback = callback,
        provider = provider.name,
        timestamp = socket.gettime()
    }

    self:show_ad_with_provider(provider, ad_data)
    return true
end

function M.can_show_ad(self)
    if not self.config.enabled then return false end

    local current_time = socket.gettime()
    return current_time - self.config.last_ad_time >= self.config.show_delay
end

return M

Integración AdMob

1. Configuración AdMob

AndroidManifest.xml

<!-- AdMob configuration -->
<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>

<!-- Optional: AdMob optimization -->
<meta-data
    android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT"
    android:value="true"/>

<!-- Network security config -->
<application
    android:networkSecurityConfig="@xml/network_security_config">
</application>

Info.plist (iOS)

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>

<key>SKAdNetworkItems</key>
<array>
    <dict>
        <key>SKAdNetworkIdentifier</key>
        <string>cstr6suwn9.skadnetwork</string>
    </dict>
    <!-- Más IDs de SKAdNetwork según necesidades -->
</array>

2. AdMob Implementation

AdMob Manager

-- admob_manager.lua
local M = {}

function M.initialize(config, provider)
    if admob then
        -- Configurar AdMob
        local admob_config = {
            app_id = config.app_id,
            test_mode = config.test_mode or false
        }

        admob.set_callback(function(self, message_id, message)
            M.handle_admob_callback(self, message_id, message, provider)
        end)

        admob.initialize(admob_config, function(self, result)
            if result.success then
                provider.initialized = true
                print("AdMob initialized successfully")

                -- Precargar anuncios
                M.preload_ads(config)
            else
                provider.enabled = false
                print("AdMob initialization failed: " .. (result.error or "Unknown error"))
            end
        end)
    else
        print("AdMob extension not available")
        provider.enabled = false
    end
end

function M.preload_ads(config)
    -- Precargar banner
    if config.banner_id then
        admob.load_banner(config.banner_id, admob.SIZE_BANNER)
    end

    -- Precargar interstitial
    if config.interstitial_id then
        admob.load_interstitial(config.interstitial_id)
    end

    -- Precargar rewarded
    if config.rewarded_id then
        admob.load_rewarded_video(config.rewarded_id)
    end
end

function M.handle_admob_callback(self, message_id, message, provider)
    local ads_manager = require "main.ads_manager"

    if message_id == admob.MSG_BANNER then
        ads_manager:handle_banner_event(message, "admob")

    elseif message_id == admob.MSG_INTERSTITIAL then
        ads_manager:handle_interstitial_event(message, "admob")

    elseif message_id == admob.MSG_REWARDED then
        ads_manager:handle_rewarded_event(message, "admob")

    elseif message_id == admob.MSG_INITIALIZATION then
        if message.event == admob.EVENT_COMPLETED then
            print("AdMob initialization completed")
        elseif message.event == admob.EVENT_JSON_ERROR then
            print("AdMob JSON error: " .. (message.error or "Unknown"))
            provider.error_count = provider.error_count + 1
        end
    end
end

function M.show_banner(config, placement)
    if not admob or not config.banner_id then return false end

    local position = placement == "top" and admob.POS_TOP_CENTER or admob.POS_BOTTOM_CENTER

    admob.show_banner(config.banner_id, position)
    return true
end

function M.hide_banner()
    if admob then
        admob.hide_banner()
    end
end

function M.show_interstitial(config)
    if not admob or not config.interstitial_id then return false end

    if admob.is_interstitial_loaded(config.interstitial_id) then
        admob.show_interstitial(config.interstitial_id)
        return true
    else
        -- Recargar si no está disponible
        admob.load_interstitial(config.interstitial_id)
        return false
    end
end

function M.show_rewarded(config)
    if not admob or not config.rewarded_id then return false end

    if admob.is_rewarded_video_loaded(config.rewarded_id) then
        admob.show_rewarded_video(config.rewarded_id)
        return true
    else
        -- Recargar si no está disponible
        admob.load_rewarded_video(config.rewarded_id)
        return false
    end
end

return M

Integración Unity Ads

1. Unity Ads Setup

Unity Ads Manager

-- unity_ads_manager.lua
local M = {}

function M.initialize(config, provider)
    if unityads then
        local unity_config = {
            game_id = config.game_id,
            test_mode = config.test_mode or false
        }

        unityads.set_callback(function(self, message_id, message)
            M.handle_unity_callback(self, message_id, message, provider)
        end)

        unityads.initialize(unity_config, function(self, result)
            if result.success then
                provider.initialized = true
                print("Unity Ads initialized successfully")

                -- Configurar placements
                M.setup_placements(config)
            else
                provider.enabled = false
                print("Unity Ads initialization failed: " .. (result.error or "Unknown error"))
            end
        end)
    else
        print("Unity Ads extension not available")
        provider.enabled = false
    end
end

function M.setup_placements(config)
    -- Unity Ads usa placements predefinidos
    for placement_name, placement_config in pairs(config.placements or {}) do
        print("Unity Ads placement configured: " .. placement_name)
    end
end

function M.handle_unity_callback(self, message_id, message, provider)
    local ads_manager = require "main.ads_manager"

    if message_id == unityads.MSG_INIT then
        if message.event == unityads.EVENT_COMPLETED then
            print("Unity Ads initialization completed")
        elseif message.event == unityads.EVENT_FAILED then
            print("Unity Ads initialization failed: " .. (message.error or "Unknown"))
            provider.error_count = provider.error_count + 1
        end

    elseif message_id == unityads.MSG_LOAD then
        if message.event == unityads.EVENT_LOADED then
            ads_manager:handle_ad_loaded(message.placement_id, "unity")
        elseif message.event == unityads.EVENT_FAILED_TO_LOAD then
            ads_manager:handle_ad_load_failed(message.placement_id, "unity", message.error)
        end

    elseif message_id == unityads.MSG_SHOW then
        if message.event == unityads.EVENT_START then
            ads_manager:handle_ad_started(message.placement_id, "unity")
        elseif message.event == unityads.EVENT_COMPLETED then
            ads_manager:handle_ad_completed(message.placement_id, "unity", message.rewarded)
        elseif message.event == unityads.EVENT_SKIPPED then
            ads_manager:handle_ad_skipped(message.placement_id, "unity")
        elseif message.event == unityads.EVENT_FAILED_TO_SHOW then
            ads_manager:handle_ad_show_failed(message.placement_id, "unity", message.error)
        end
    end
end

function M.show_ad(config, placement_id, ad_type)
    if not unityads then return false end

    if unityads.is_ready(placement_id) then
        unityads.show(placement_id)
        return true
    else
        -- Cargar anuncio
        unityads.load(placement_id)
        return false
    end
end

function M.is_ready(placement_id)
    return unityads and unityads.is_ready(placement_id)
end

return M

Sistema de Waterfall

1. Ad Mediation

Waterfall Manager

-- waterfall_manager.lua
local M = {}

function init(self)
    self.waterfall_config = {}
    self.current_attempt = 1
    self.max_attempts = 3
    self.fallback_providers = {}
end

function M.setup_waterfall(ad_type, providers)
    self.waterfall_config[ad_type] = {
        providers = providers,
        current_index = 1,
        attempts = 0
    }
end

function M.show_ad_with_waterfall(self, ad_type, placement, callback)
    local config = self.waterfall_config[ad_type]
    if not config or #config.providers == 0 then
        if callback then callback(false, "No providers configured") end
        return false
    end

    self:attempt_next_provider(ad_type, placement, callback)
    return true
end

function attempt_next_provider(self, ad_type, placement, callback)
    local config = self.waterfall_config[ad_type]

    if config.current_index > #config.providers then
        -- Todos los proveedores fallaron
        if callback then callback(false, "All providers failed") end
        self:reset_waterfall(ad_type)
        return
    end

    local provider = config.providers[config.current_index]
    config.attempts = config.attempts + 1

    print(string.format("Attempting ad with provider %s (attempt %d)",
          provider.name, config.attempts))

    -- Intentar mostrar anuncio con el proveedor actual
    local success = self:try_provider(provider, ad_type, placement, function(success, error)
        if success then
            -- Éxito - resetear waterfall para próxima vez
            self:reset_waterfall(ad_type)
            if callback then callback(true) end
        else
            -- Falló - intentar siguiente proveedor
            print(string.format("Provider %s failed: %s", provider.name, error or "Unknown"))
            config.current_index = config.current_index + 1

            -- Delay antes del siguiente intento
            timer.delay(1.0, false, function()
                self:attempt_next_provider(ad_type, placement, callback)
            end)
        end
    end)

    if not success then
        -- Provider no disponible inmediatamente
        config.current_index = config.current_index + 1
        self:attempt_next_provider(ad_type, placement, callback)
    end
end

function try_provider(self, provider, ad_type, placement, callback)
    if provider.name == "admob" then
        return self:try_admob(provider, ad_type, placement, callback)
    elseif provider.name == "unity" then
        return self:try_unity_ads(provider, ad_type, placement, callback)
    elseif provider.name == "ironsource" then
        return self:try_ironsource(provider, ad_type, placement, callback)
    end

    return false
end

function reset_waterfall(self, ad_type)
    local config = self.waterfall_config[ad_type]
    if config then
        config.current_index = 1
        config.attempts = 0
    end
end

return M

Ad Placement Strategy

1. Intelligent Ad Timing

Ad Timing Manager

-- ad_timing_manager.lua
local M = {}

function init(self)
    self.placement_rules = {}
    self.user_behavior = {
        session_start = socket.gettime(),
        games_played = 0,
        total_playtime = 0,
        last_ad_shown = 0,
        ad_fatigue_level = 0,
        engagement_score = 1.0
    }

    self.timing_config = {
        min_session_time = 30,      -- 30s mínimo antes del primer ad
        min_between_ads = 120,      -- 2 minutos entre ads
        max_ads_per_session = 5,    -- Máximo 5 ads por sesión
        fatigue_threshold = 0.7,    -- Threshold de fatiga
        engagement_bonus = 1.2      -- Bonus por alto engagement
    }
end

function M.can_show_ad(self, placement)
    local current_time = socket.gettime()
    local session_time = current_time - self.user_behavior.session_start

    -- Verificar tiempo mínimo de sesión
    if session_time < self.timing_config.min_session_time then
        return false, "Session too short"
    end

    -- Verificar tiempo entre anuncios
    local time_since_last_ad = current_time - self.user_behavior.last_ad_shown
    if time_since_last_ad < self.timing_config.min_between_ads then
        return false, "Too soon since last ad"
    end

    -- Verificar límite de anuncios por sesión
    if self.user_behavior.ads_shown_this_session >= self.timing_config.max_ads_per_session then
        return false, "Session ad limit reached"
    end

    -- Verificar fatiga del usuario
    if self.user_behavior.ad_fatigue_level > self.timing_config.fatigue_threshold then
        return false, "User ad fatigue too high"
    end

    -- Verificar reglas específicas del placement
    local rule = self.placement_rules[placement]
    if rule and not self:check_placement_rule(rule) then
        return false, "Placement rule not met"
    end

    return true
end

function M.add_placement_rule(self, placement, rule)
    self.placement_rules[placement] = rule
end

function check_placement_rule(self, rule)
    if rule.min_level and self.user_behavior.current_level < rule.min_level then
        return false
    end

    if rule.min_games and self.user_behavior.games_played < rule.min_games then
        return false
    end

    if rule.min_playtime and self.user_behavior.total_playtime < rule.min_playtime then
        return false
    end

    if rule.only_after_death and not self.user_behavior.just_died then
        return false
    end

    return true
end

function M.record_ad_shown(self, placement, success)
    local current_time = socket.gettime()

    if success then
        self.user_behavior.last_ad_shown = current_time
        self.user_behavior.ads_shown_this_session =
            (self.user_behavior.ads_shown_this_session or 0) + 1

        -- Incrementar fatiga
        self.user_behavior.ad_fatigue_level =
            math.min(1.0, self.user_behavior.ad_fatigue_level + 0.1)

        print(string.format("Ad shown successfully. Fatigue level: %.2f",
              self.user_behavior.ad_fatigue_level))
    else
        -- Ad fallido - reducir ligeramente la fatiga
        self.user_behavior.ad_fatigue_level =
            math.max(0.0, self.user_behavior.ad_fatigue_level - 0.05)
    end
end

function M.update_engagement(self, engagement_delta)
    self.user_behavior.engagement_score =
        math.max(0.1, math.min(2.0, self.user_behavior.engagement_score + engagement_delta))

    -- Reducir fatiga con alto engagement
    if self.user_behavior.engagement_score > 1.0 then
        self.user_behavior.ad_fatigue_level =
            math.max(0.0, self.user_behavior.ad_fatigue_level - 0.02)
    end
end

return M

2. Rewarded Video Strategy

Rewarded Ads Manager

-- rewarded_ads_manager.lua
local M = {}

function init(self)
    self.rewards = {}
    self.watch_tracking = {}
    self.incentive_config = {
        coins_multiplier = 2,
        extra_lives = 1,
        power_ups = true,
        bonus_xp = 50
    }
end

function M.offer_rewarded_ad(self, reward_type, base_reward, context)
    local incentive = self:calculate_incentive(reward_type, base_reward, context)

    -- Mostrar oferta al usuario
    msg.post("main:/ui", "show_reward_offer", {
        reward_type = reward_type,
        base_reward = base_reward,
        bonus_reward = incentive.bonus,
        total_reward = incentive.total,
        context = context
    })

    return incentive
end

function calculate_incentive(self, reward_type, base_reward, context)
    local multiplier = 1.0
    local bonus = 0

    if reward_type == "coins" then
        multiplier = self.incentive_config.coins_multiplier
        bonus = base_reward * (multiplier - 1)

    elseif reward_type == "lives" then
        bonus = self.incentive_config.extra_lives

    elseif reward_type == "xp" then
        bonus = self.incentive_config.bonus_xp

    elseif reward_type == "power_up" then
        bonus = 1  -- Un power-up adicional
    end

    -- Ajustar según contexto
    if context == "game_over" then
        multiplier = multiplier * 1.5  -- Bonus extra en game over
    elseif context == "level_complete" then
        multiplier = multiplier * 1.2  -- Bonus menor en level complete
    end

    return {
        base = base_reward,
        bonus = math.floor(bonus * multiplier),
        total = base_reward + math.floor(bonus * multiplier),
        multiplier = multiplier
    }
end

function M.show_rewarded_ad(self, reward_data, callback)
    local ads_manager = require "main.ads_manager"

    -- Trackear el intento
    self.watch_tracking[reward_data.id] = {
        start_time = socket.gettime(),
        reward_data = reward_data,
        callback = callback
    }

    ads_manager:show_ad("rewarded", "game_reward", function(success, error)
        self:handle_rewarded_result(reward_data.id, success, error)
    end)
end

function handle_rewarded_result(self, reward_id, success, error)
    local tracking = self.watch_tracking[reward_id]
    if not tracking then return end

    local watch_time = socket.gettime() - tracking.start_time

    if success then
        -- Otorgar recompensa
        self:grant_reward(tracking.reward_data)

        -- Registrar analytics
        self:track_rewarded_view(tracking.reward_data, watch_time, true)

        if tracking.callback then
            tracking.callback(true, tracking.reward_data)
        end

        print("Rewarded ad completed - reward granted")
    else
        print("Rewarded ad failed: " .. (error or "Unknown error"))

        if tracking.callback then
            tracking.callback(false, error)
        end
    end

    self.watch_tracking[reward_id] = nil
end

function grant_reward(self, reward_data)
    if reward_data.type == "coins" then
        msg.post("main:/game_state", "add_coins", {amount = reward_data.total})

    elseif reward_data.type == "lives" then
        msg.post("main:/game_state", "add_lives", {amount = reward_data.total})

    elseif reward_data.type == "power_up" then
        msg.post("main:/game_state", "add_power_up", {
            type = reward_data.power_up_type,
            quantity = reward_data.total
        })

    elseif reward_data.type == "continue" then
        msg.post("main:/game_state", "continue_game")

    elseif reward_data.type == "xp" then
        msg.post("main:/game_state", "add_xp", {amount = reward_data.total})
    end

    -- Mostrar feedback visual
    msg.post("main:/ui", "show_reward_feedback", reward_data)
end

function track_rewarded_view(self, reward_data, watch_time, completed)
    -- Enviar a analytics
    msg.post("main:/analytics", "track_event", {
        event = "rewarded_ad_viewed",
        properties = {
            reward_type = reward_data.type,
            reward_amount = reward_data.total,
            watch_time = watch_time,
            completed = completed,
            context = reward_data.context
        }
    })
end

return M

Ad Revenue Optimization

1. A/B Testing Framework

Ad Testing Manager

-- ad_testing_manager.lua
local M = {}

function init(self)
    self.active_tests = {}
    self.user_group = self:determine_user_group()
    self.test_results = {}
end

function M.determine_user_group(self)
    -- Usar ID del dispositivo para asignar grupo consistente
    local device_id = sys.get_sys_info().device_ident or "unknown"
    local hash = 0

    for i = 1, #device_id do
        hash = hash + string.byte(device_id, i)
    end

    -- Distribuir en grupos A/B/C
    local group_number = hash % 3
    local groups = {"A", "B", "C"}

    return groups[group_number + 1]
end

function M.create_test(self, test_name, variants)
    self.active_tests[test_name] = {
        variants = variants,
        user_variant = variants[self.user_group] or variants["A"],
        start_time = socket.gettime(),
        metrics = {
            impressions = 0,
            clicks = 0,
            revenue = 0,
            completion_rate = 0
        }
    }

    print(string.format("A/B Test '%s' started. User in group %s",
          test_name, self.user_group))
end

function M.get_variant(self, test_name)
    local test = self.active_tests[test_name]
    return test and test.user_variant or nil
end

function M.track_test_metric(self, test_name, metric, value)
    local test = self.active_tests[test_name]
    if test then
        test.metrics[metric] = test.metrics[metric] + value

        -- Enviar a analytics
        msg.post("main:/analytics", "track_ab_test", {
            test = test_name,
            variant = test.user_variant.name,
            metric = metric,
            value = value
        })
    end
end

-- Ejemplo de test de posición de banner
function M.setup_banner_position_test(self)
    self:create_test("banner_position", {
        A = {name = "top", position = "top", multiplier = 1.0},
        B = {name = "bottom", position = "bottom", multiplier = 1.1},
        C = {name = "smart", position = "smart", multiplier = 0.9}
    })
end

-- Ejemplo de test de frecuencia de interstitials
function M.setup_interstitial_frequency_test(self)
    self:create_test("interstitial_frequency", {
        A = {name = "low", interval = 180, multiplier = 1.0},      -- 3 minutos
        B = {name = "medium", interval = 120, multiplier = 1.5},   -- 2 minutos
        C = {name = "high", interval = 90, multiplier = 2.0}       -- 1.5 minutos
    })
end

return M

2. Revenue Tracking

Revenue Analytics

-- revenue_analytics.lua
local M = {}

function init(self)
    self.revenue_data = {
        daily_revenue = 0,
        session_revenue = 0,
        lifetime_revenue = 0,
        ad_sources = {},
        ecpm_tracking = {}
    }

    self.tracking_config = {
        currency = "USD",
        precision = 0.001,  -- Track down to $0.001
        session_id = self:generate_session_id()
    }
end

function M.track_ad_revenue(self, provider, ad_type, revenue, currency)
    currency = currency or self.tracking_config.currency

    -- Convertir a USD si es necesario
    local usd_revenue = self:convert_to_usd(revenue, currency)

    -- Actualizar totales
    self.revenue_data.session_revenue = self.revenue_data.session_revenue + usd_revenue
    self.revenue_data.daily_revenue = self.revenue_data.daily_revenue + usd_revenue
    self.revenue_data.lifetime_revenue = self.revenue_data.lifetime_revenue + usd_revenue

    -- Trackear por fuente
    if not self.revenue_data.ad_sources[provider] then
        self.revenue_data.ad_sources[provider] = {
            revenue = 0,
            impressions = 0,
            ecpm = 0
        }
    end

    local source = self.revenue_data.ad_sources[provider]
    source.revenue = source.revenue + usd_revenue
    source.impressions = source.impressions + 1
    source.ecpm = (source.revenue / source.impressions) * 1000

    -- Enviar a analytics externos
    self:send_revenue_event(provider, ad_type, usd_revenue)

    print(string.format("Revenue tracked: $%.3f from %s %s (eCPM: $%.2f)",
          usd_revenue, provider, ad_type, source.ecpm))
end

function send_revenue_event(self, provider, ad_type, revenue)
    -- Enviar a múltiples plataformas de analytics
    local event_data = {
        event = "ad_revenue",
        properties = {
            provider = provider,
            ad_type = ad_type,
            revenue = revenue,
            currency = "USD",
            session_id = self.tracking_config.session_id,
            timestamp = socket.gettime()
        }
    }

    -- Enviar a Firebase Analytics
    msg.post("main:/firebase", "track_revenue", event_data)

    -- Enviar a Facebook Analytics
    msg.post("main:/facebook", "track_purchase", {
        amount = revenue,
        currency = "USD",
        content_type = "ad_revenue",
        content_id = provider .. "_" .. ad_type
    })

    -- Enviar a AppsFlyer
    msg.post("main:/appsflyer", "track_revenue", {
        revenue = revenue,
        currency = "USD",
        receipt_id = self:generate_receipt_id(),
        product_id = provider .. "_ad"
    })
end

function generate_session_id(self)
    return "session_" .. os.time() .. "_" .. math.random(1000, 9999)
end

function generate_receipt_id(self)
    return "ad_" .. os.time() .. "_" .. math.random(100000, 999999)
end

function convert_to_usd(self, amount, currency)
    -- Tasas de cambio simplificadas (en producción usar API real)
    local exchange_rates = {
        USD = 1.0,
        EUR = 1.18,
        GBP = 1.37,
        JPY = 0.0091,
        CAD = 0.79
    }

    return amount * (exchange_rates[currency] or 1.0)
end

return M

Debugging y Testing

1. Ad Debug Console

Debug Ad System

-- ad_debug.gui_script
function init(self)
    self.debug_enabled = sys.get_config("project.debug", "0") == "1"
    self.ad_log = {}
    self.max_log_entries = 50

    if self.debug_enabled then
        self:create_debug_ui()
    end
end

function create_debug_ui(self)
    -- Crear panel de debug
    self.debug_panel = gui.get_node("debug_panel")
    self.log_text = gui.get_node("log_text")
    self.test_buttons = {
        banner = gui.get_node("test_banner"),
        interstitial = gui.get_node("test_interstitial"),
        rewarded = gui.get_node("test_rewarded")
    }

    -- Mostrar estadísticas iniciales
    self:update_debug_display()
end

function on_message(self, message_id, message, sender)
    if not self.debug_enabled then return end

    if message_id == hash("ad_event") then
        self:log_ad_event(message)
        self:update_debug_display()

    elseif message_id == hash("update_stats") then
        self:update_debug_display()
    end
end

function log_ad_event(self, event)
    local log_entry = string.format("[%s] %s - %s: %s",
        os.date("%H:%M:%S"),
        event.provider or "unknown",
        event.ad_type or "unknown",
        event.event_type or "unknown"
    )

    table.insert(self.ad_log, log_entry)

    -- Mantener solo las últimas entradas
    if #self.ad_log > self.max_log_entries then
        table.remove(self.ad_log, 1)
    end
end

function update_debug_display(self)
    local ads_manager = require "main.ads_manager"
    local stats = ads_manager:get_stats()

    local debug_text = string.format(
        "=== AD DEBUG INFO ===\n" ..
        "Requested: %d\n" ..
        "Shown: %d\n" ..
        "Failed: %d\n" ..
        "Success Rate: %.1f%%\n" ..
        "Revenue: $%.3f\n\n" ..
        "=== RECENT EVENTS ===\n%s",
        stats.ads_requested,
        stats.ads_shown,
        stats.ads_failed,
        stats.ads_shown > 0 and (stats.ads_shown / stats.ads_requested * 100) or 0,
        stats.revenue,
        table.concat(self.ad_log, "\n")
    )

    gui.set_text(self.log_text, debug_text)
end

function on_input(self, action_id, action)
    if not self.debug_enabled or not action.pressed then
        return false
    end

    if action_id == hash("touch") then
        -- Test banner
        if gui.pick_node(self.test_buttons.banner, action.x, action.y) then
            msg.post("main:/ads_manager", "test_ad", {type = "banner"})
            return true

        -- Test interstitial
        elseif gui.pick_node(self.test_buttons.interstitial, action.x, action.y) then
            msg.post("main:/ads_manager", "test_ad", {type = "interstitial"})
            return true

        -- Test rewarded
        elseif gui.pick_node(self.test_buttons.rewarded, action.x, action.y) then
            msg.post("main:/ads_manager", "test_ad", {type = "rewarded"})
            return true
        end
    end

    return false
end

Configuración de Producción

1. Ad Configuration File

ads_config.json

{
    "providers": [
        {
            "name": "admob",
            "priority": 1,
            "enabled": true,
            "config": {
                "app_id": "ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy",
                "banner_id": "ca-app-pub-xxxxxxxxxxxxxxxx/zzzzzzzzzz",
                "interstitial_id": "ca-app-pub-xxxxxxxxxxxxxxxx/aaaaaaaaaa",
                "rewarded_id": "ca-app-pub-xxxxxxxxxxxxxxxx/bbbbbbbbbb",
                "test_mode": false
            }
        },
        {
            "name": "unity",
            "priority": 2,
            "enabled": true,
            "config": {
                "game_id": "1234567",
                "placements": {
                    "banner": "banner_placement",
                    "interstitial": "video_placement",
                    "rewarded": "rewarded_placement"
                },
                "test_mode": false
            }
        }
    ],
    "timing": {
        "min_session_time": 30,
        "min_between_ads": 120,
        "max_ads_per_session": 5
    },
    "ab_tests": {
        "banner_position": {
            "enabled": true,
            "variants": ["top", "bottom", "smart"]
        }
    }
}

Mejores Prácticas

1. User Experience

2. Performance

3. Revenue Optimization

4. Compliance

Esta implementación completa te permite monetizar tu juego efectivamente mientras mantienes una excelente experiencia de usuario.