← Volver al listado de tecnologías

Publicación y Monetización

Por: Artiko
defoldpublicacionmonetizacionmobileadsanalytics

Publicación y Monetización

En esta lección final aprenderás todo lo necesario para llevar tus juegos Defold al mercado. Cubriremos el proceso completo de publicación en diferentes plataformas, estrategias de monetización efectivas, y técnicas de optimización para maximizar el éxito de tus juegos.

Preparación para Publicación

Optimización Final del Juego

Antes de publicar, es crucial optimizar tu juego para garantizar la mejor experiencia posible:

-- game.project optimizado para release
[project]
title = Tu Juego Increíble
version = 1.0.0
publish_live_update_content = 0

[display]
width = 960
height = 640
high_dpi = 1
vsync = 1

[graphics]
max_debug_vertices = 0
texture_profiles = /assets/texture_profiles.json

[sound]
gain = 1.0
max_sound_buffers = 32
max_sound_instances = 256

[physics]
debug = 0
max_collisions = 64
max_contacts = 32

[resource]
max_resources = 1024

[android]
version_code = 1
package = com.tuestudio.tujuego
app_icon_36x36 = /assets/icons/icon_36.png
app_icon_48x48 = /assets/icons/icon_48.png
app_icon_72x72 = /assets/icons/icon_72.png
app_icon_96x96 = /assets/icons/icon_96.png
app_icon_144x144 = /assets/icons/icon_144.png
app_icon_192x192 = /assets/icons/icon_192.png

[ios]
app_icon_57x57 = /assets/icons/icon_57.png
app_icon_114x114 = /assets/icons/icon_114.png
app_icon_72x72 = /assets/icons/icon_72.png
app_icon_144x144 = /assets/icons/icon_144.png
bundle_identifier = com.tuestudio.tujuego

Texture Profiles para Optimización

// assets/texture_profiles.json
{
  "path_settings": [
    {
      "path": "**",
      "profile": "default"
    },
    {
      "path": "**.atlas",
      "profile": "atlas"
    },
    {
      "path": "**/ui/**",
      "profile": "ui"
    }
  ],
  "profiles": [
    {
      "name": "default",
      "platforms": ["ios", "android", "web"],
      "formats": [
        {
          "format": "TEXTURE_FORMAT_RGBA",
          "compression_level": "NORMAL",
          "compression_type": "COMPRESSION_TYPE_DEFAULT"
        }
      ]
    },
    {
      "name": "atlas",
      "platforms": ["ios", "android"],
      "formats": [
        {
          "format": "TEXTURE_FORMAT_ETC1",
          "compression_level": "HIGH"
        }
      ]
    },
    {
      "name": "ui",
      "platforms": ["ios", "android"],
      "formats": [
        {
          "format": "TEXTURE_FORMAT_RGBA",
          "compression_level": "NORMAL"
        }
      ]
    }
  ]
}

Parte 1: Publicación en Android

Configuración de Android Studio

  1. Instalar Android Studio y SDK
  2. Configurar variables de entorno:
export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools

Generar APK de Release

# En Defold Editor
# Project → Bundle → Android Application

# O desde línea de comandos:
java -jar $DEFOLD_HOME/bob.jar --platform android \
    --architectures armv7-android,arm64-android \
    --variant release \
    resolve build bundle

Configuración de Manifest Android

<!-- android_manifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="{{android.package}}"
    android:versionCode="{{android.version_code}}"
    android:versionName="{{project.version}}"
    android:installLocation="auto">

    <!-- Permisos necesarios -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <!-- Para ads y analytics -->
    <uses-permission android:name="com.google.android.gms.permission.AD_ID" />

    <application
        android:label="{{project.title}}"
        android:icon="@drawable/icon"
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
        android:hardwareAccelerated="true">

        <activity
            android:name="com.dynamo.android.DefoldActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:screenOrientation="landscape"
            android:configChanges="orientation|keyboardHidden|screenSize">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Configuración de AdMob -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713"/>

    </application>
</manifest>

Firmar APK para Google Play

# Crear keystore
keytool -genkey -v -keystore my-release-key.keystore \
    -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

# Firmar APK
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
    -keystore my-release-key.keystore my_application.apk alias_name

# Alinear APK
zipalign -v 4 my_application.apk my_application_aligned.apk

Parte 2: Publicación en iOS

Configuración de Xcode

-- game.project para iOS
[ios]
app_icon_57x57 = /assets/icons/icon_57.png
app_icon_114x114 = /assets/icons/icon_114.png
app_icon_120x120 = /assets/icons/icon_120.png
app_icon_180x180 = /assets/icons/icon_180.png
launch_image_320x480 = /assets/launch/launch_320x480.png
launch_image_640x960 = /assets/launch/launch_640x960.png
launch_image_640x1136 = /assets/launch/launch_640x1136.png

bundle_identifier = com.tuestudio.tujuego
bundle_name = Tu Juego
bundle_version = 1.0.0

Info.plist Personalizado

<!-- ios_info.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDisplayName</key>
    <string>{{ios.bundle_name}}</string>

    <key>CFBundleIdentifier</key>
    <string>{{ios.bundle_identifier}}</string>

    <key>CFBundleVersion</key>
    <string>{{ios.bundle_version}}</string>

    <key>UIRequiredDeviceCapabilities</key>
    <array>
        <string>armv7</string>
        <string>opengles-2</string>
    </array>

    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>

    <!-- Para ads -->
    <key>GADApplicationIdentifier</key>
    <string>ca-app-pub-3940256099942544~1458002511</string>
</dict>
</plist>

Parte 3: Sistema de Monetización

Integración de AdMob

-- monetization/ads_manager.script
local M = {}

function M.init()
    if admob then
        -- Configurar AdMob
        local admob_app_id = "ca-app-pub-3940256099942544~3347511713"  -- Test ID
        admob.set_callback(M.admob_callback)
        admob.init(admob_app_id)

        -- IDs de anuncios (usar test IDs durante desarrollo)
        M.banner_id = "ca-app-pub-3940256099942544/6300978111"
        M.interstitial_id = "ca-app-pub-3940256099942544/1033173712"
        M.rewarded_id = "ca-app-pub-3940256099942544/5224354917"

        M.ads_loaded = {
            banner = false,
            interstitial = false,
            rewarded = false
        }

        -- Cargar anuncios iniciales
        M.load_banner()
        M.load_interstitial()
        M.load_rewarded()

        print("AdMob inicializado")
    else
        print("AdMob no disponible en esta plataforma")
    end
end

function M.admob_callback(self, message_id, message)
    if message_id == admob.MSG_BANNER then
        if message.event == admob.EVENT_LOADED then
            M.ads_loaded.banner = true
            print("Banner cargado")
        elseif message.event == admob.EVENT_NOT_LOADED then
            print("Error cargando banner:", message.error)
        end

    elseif message_id == admob.MSG_INTERSTITIAL then
        if message.event == admob.EVENT_LOADED then
            M.ads_loaded.interstitial = true
            print("Interstitial cargado")
        elseif message.event == admob.EVENT_CLOSED then
            -- Recargar interstitial para próximo uso
            M.load_interstitial()
        end

    elseif message_id == admob.MSG_REWARDED then
        if message.event == admob.EVENT_LOADED then
            M.ads_loaded.rewarded = true
            print("Rewarded ad cargado")
        elseif message.event == admob.EVENT_REWARDED then
            -- Usuario completó el anuncio, dar recompensa
            M.give_reward(message.amount, message.type)
        elseif message.event == admob.EVENT_CLOSED then
            M.load_rewarded()
        end
    end
end

function M.load_banner()
    if admob then
        admob.load_banner(M.banner_id)
    end
end

function M.show_banner(position)
    if admob and M.ads_loaded.banner then
        local pos = position or admob.POS_BOTTOM_CENTER
        admob.show_banner(pos)
    end
end

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

function M.load_interstitial()
    if admob then
        admob.load_interstitial(M.interstitial_id)
    end
end

function M.show_interstitial()
    if admob and M.ads_loaded.interstitial then
        admob.show_interstitial()
        return true
    end
    return false
end

function M.load_rewarded()
    if admob then
        admob.load_rewarded_video(M.rewarded_id)
    end
end

function M.show_rewarded()
    if admob and M.ads_loaded.rewarded then
        admob.show_rewarded_video()
        return true
    end
    return false
end

function M.give_reward(amount, reward_type)
    print("Recompensa otorgada:", amount, reward_type)

    -- Lógica de recompensa específica del juego
    if reward_type == "coins" then
        msg.post("/game_manager", "add_coins", {amount = amount})
    elseif reward_type == "lives" then
        msg.post("/game_manager", "add_lives", {amount = amount})
    elseif reward_type == "continue" then
        msg.post("/game_manager", "continue_game")
    end
end

return M

Sistema de Compras In-App

-- monetization/iap_manager.script
local M = {}

-- Configuración de productos
local IAP_PRODUCTS = {
    remove_ads = {
        id = "com.tuestudio.tujuego.remove_ads",
        price = "$2.99",
        type = "non_consumable"
    },
    coin_pack_small = {
        id = "com.tuestudio.tujuego.coins_100",
        price = "$0.99",
        type = "consumable"
    },
    coin_pack_medium = {
        id = "com.tuestudio.tujuego.coins_500",
        price = "$3.99",
        type = "consumable"
    },
    coin_pack_large = {
        id = "com.tuestudio.tujuego.coins_1000",
        price = "$6.99",
        type = "consumable"
    }
}

function M.init()
    if iap then
        iap.set_callback(M.iap_callback)

        -- Lista de product IDs para la tienda
        local product_ids = {}
        for _, product in pairs(IAP_PRODUCTS) do
            table.insert(product_ids, product.id)
        end

        iap.list_products(product_ids)
        print("IAP Manager inicializado")
    else
        print("IAP no disponible en esta plataforma")
    end
end

function M.iap_callback(self, message_id, message)
    if message_id == iap.MSG_PRODUCTS then
        -- Productos disponibles recibidos
        M.products = message.products
        print("Productos IAP disponibles:", #M.products)

    elseif message_id == iap.MSG_PURCHASE then
        if message.state == iap.TRANS_STATE_PURCHASED then
            M.process_purchase(message.ident)
        elseif message.state == iap.TRANS_STATE_FAILED then
            print("Compra fallida:", message.error)
            M.show_purchase_error()
        end

    elseif message_id == iap.MSG_RESTORE then
        -- Restaurar compras (para productos no consumibles)
        for _, transaction in ipairs(message.transactions) do
            if transaction.state == iap.TRANS_STATE_PURCHASED then
                M.process_purchase(transaction.ident)
            end
        end
    end
end

function M.purchase_product(product_name)
    local product = IAP_PRODUCTS[product_name]
    if product and iap then
        print("Iniciando compra:", product.id)
        iap.buy(product.id)
    end
end

function M.process_purchase(product_id)
    print("Procesando compra:", product_id)

    -- Procesar según tipo de producto
    if product_id == IAP_PRODUCTS.remove_ads.id then
        M.remove_ads()
    elseif product_id == IAP_PRODUCTS.coin_pack_small.id then
        M.add_coins(100)
    elseif product_id == IAP_PRODUCTS.coin_pack_medium.id then
        M.add_coins(500)
    elseif product_id == IAP_PRODUCTS.coin_pack_large.id then
        M.add_coins(1000)
    end

    -- Confirmar transacción
    if iap then
        iap.finish_transaction(product_id)
    end

    -- Mostrar confirmación al usuario
    M.show_purchase_success(product_id)
end

function M.remove_ads()
    -- Marcar ads como removidos permanentemente
    local save_data = {ads_removed = true}
    local save_string = json.encode(save_data)
    local file = io.open(sys.get_save_file("iap", "purchases.json"), "w")
    if file then
        file:write(save_string)
        file:close()
    end

    -- Notificar al ads manager
    msg.post("/ads_manager", "disable_ads")
    print("Anuncios removidos permanentemente")
end

function M.add_coins(amount)
    msg.post("/game_manager", "add_coins", {amount = amount})
    print("Agregadas", amount, "monedas")
end

function M.restore_purchases()
    if iap then
        iap.restore()
    end
end

function M.show_purchase_success(product_id)
    msg.post("/ui", "show_purchase_success", {product = product_id})
end

function M.show_purchase_error()
    msg.post("/ui", "show_purchase_error")
end

-- Verificar si los ads fueron removidos
function M.ads_removed()
    local file = io.open(sys.get_save_file("iap", "purchases.json"), "r")
    if file then
        local save_string = file:read("*a")
        file:close()
        local save_data = json.decode(save_string)
        return save_data and save_data.ads_removed or false
    end
    return false
end

return M

Parte 4: Analytics y Métricas

Integración de Google Analytics

-- analytics/analytics_manager.script
local M = {}

function M.init()
    -- Configurar Firebase Analytics (si está disponible)
    if firebase and firebase.analytics then
        firebase.analytics.set_analytics_collection_enabled(true)
        print("Firebase Analytics inicializado")
    end

    -- Configurar analytics custom
    M.session_start_time = os.time()
    M.events_queue = {}

    -- Eventos de sesión
    M.track_event("session_start", {
        device_model = sys.get_sys_info().device_model,
        platform = sys.get_sys_info().system_name
    })
end

function M.track_event(event_name, parameters)
    parameters = parameters or {}
    parameters.timestamp = os.time()

    print("Analytics:", event_name, json.encode(parameters))

    -- Enviar a Firebase si está disponible
    if firebase and firebase.analytics then
        firebase.analytics.log_event(event_name, parameters)
    end

    -- Agregar a cola local para backup
    table.insert(M.events_queue, {
        event = event_name,
        params = parameters
    })

    -- Enviar eventos acumulados si la cola es muy grande
    if #M.events_queue >= 10 then
        M.flush_events()
    end
end

function M.flush_events()
    if #M.events_queue > 0 then
        -- En un proyecto real, enviarías esto a tu servidor
        print("Enviando", #M.events_queue, "eventos al servidor")
        M.events_queue = {}
    end
end

-- Eventos específicos del juego
function M.track_level_start(level_number)
    M.track_event("level_start", {
        level_number = level_number,
        player_level = get_player_level(),
        coins = get_player_coins()
    })
end

function M.track_level_complete(level_number, completion_time, score)
    M.track_event("level_complete", {
        level_number = level_number,
        completion_time = completion_time,
        score = score,
        attempts = get_level_attempts(level_number)
    })
end

function M.track_level_fail(level_number, fail_reason, progress_percentage)
    M.track_event("level_fail", {
        level_number = level_number,
        fail_reason = fail_reason,
        progress_percentage = progress_percentage
    })
end

function M.track_purchase(product_id, currency, value)
    M.track_event("purchase", {
        item_id = product_id,
        currency = currency,
        value = value
    })
end

function M.track_ad_impression(ad_type, placement)
    M.track_event("ad_impression", {
        ad_type = ad_type,  -- banner, interstitial, rewarded
        placement = placement,  -- main_menu, level_complete, game_over
        session_time = os.time() - M.session_start_time
    })
end

function M.track_retention(day_number)
    M.track_event("retention", {
        day_number = day_number,
        total_sessions = get_total_sessions(),
        total_playtime = get_total_playtime()
    })
end

-- Funciones helper (implementar según tu sistema de datos)
function get_player_level()
    return 1  -- Implementar
end

function get_player_coins()
    return 0  -- Implementar
end

function get_level_attempts(level)
    return 1  -- Implementar
end

function get_total_sessions()
    return 1  -- Implementar
end

function get_total_playtime()
    return 0  -- Implementar
end

return M

Parte 5: Optimización para Tiendas

App Store Optimization (ASO)

Elementos Clave para ASO:

  1. Título y Subtítulo

    • Incluir palabras clave relevantes
    • Máximo 30 caracteres para el título
    • Descriptivo pero atractivo
  2. Descripción Optimizada

🎮 ¡El juego de plataformas más adictivo del año!

🏃‍♂️ CARACTERÍSTICAS:
• 50+ niveles desafiantes
• Gráficos retro encantadores
• Controles precisos y fluidos
• Sin pay-to-win, skill puro
• Funciona offline

🎯 PERFECTO PARA:
• Fans de juegos clásicos
• Jugadores casuales y hardcore
• Todas las edades

⭐ PREMIOS Y RECONOCIMIENTOS:
"Gameplay perfecto" - Gaming Review
"Imprescindible" - Mobile Games Weekly

📱 CARACTERÍSTICAS TÉCNICAS:
• Tamaño ultra ligero (< 20MB)
• Batería optimizada
• Sin anuncios intrusivos
• Guardado automático en la nube
  1. Screenshots Efectivos

    • Mostrar gameplay real
    • Destacar características únicas
    • Usar texto explicativo mínimo
    • Variedad de situaciones
  2. Keywords Strategy

-- Investigación de keywords
primary_keywords = {
    "platformer", "retro games", "pixel art",
    "offline games", "casual games"
}

long_tail_keywords = {
    "best offline platformer",
    "retro pixel art games",
    "challenging platform games"
}

Preparación de Assets

-- Script para generar todos los tamaños de iconos
-- icons/generate_icons.py
from PIL import Image
import os

def resize_icon(source_path, target_path, size):
    img = Image.open(source_path)
    img = img.resize((size, size), Image.LANCZOS)
    img.save(target_path)

# Tamaños para Android
android_sizes = [
    (36, "ldpi"), (48, "mdpi"), (72, "hdpi"),
    (96, "xhdpi"), (144, "xxhdpi"), (192, "xxxhdpi")
]

# Tamaños para iOS
ios_sizes = [
    57, 114, 120, 180, 76, 152, 40, 80, 29, 58, 87
]

source_icon = "icon_1024.png"

for size, density in android_sizes:
    resize_icon(source_icon, f"android/icon_{size}.png", size)

for size in ios_sizes:
    resize_icon(source_icon, f"ios/icon_{size}.png", size)

Parte 6: Estrategias de Marketing

Soft Launch Strategy

-- analytics/soft_launch_metrics.script
local M = {}

function M.init()
    M.metrics = {
        retention_day_1 = 0,
        retention_day_7 = 0,
        session_length_avg = 0,
        ltv_estimate = 0,
        churn_rate = 0
    }

    M.target_metrics = {
        retention_day_1 = 0.4,  -- 40%
        retention_day_7 = 0.15,  -- 15%
        session_length_avg = 300,  -- 5 minutos
        ltv_estimate = 1.50,  -- $1.50
        churn_rate = 0.8  -- 80% máximo
    }
end

function M.evaluate_soft_launch()
    local ready_for_global = true
    local issues = {}

    for metric, target in pairs(M.target_metrics) do
        local current = M.metrics[metric]

        if metric == "churn_rate" then
            if current > target then
                ready_for_global = false
                table.insert(issues, metric .. " too high: " .. current)
            end
        else
            if current < target then
                ready_for_global = false
                table.insert(issues, metric .. " too low: " .. current)
            end
        end
    end

    return ready_for_global, issues
end

Live Ops y Eventos

-- liveops/event_manager.script
local M = {}

local EVENTS = {
    daily_login = {
        type = "recurring",
        rewards = {
            {day = 1, coins = 100},
            {day = 2, coins = 150},
            {day = 3, coins = 200},
            {day = 7, coins = 500, special_item = "golden_key"}
        }
    },
    weekend_boost = {
        type = "scheduled",
        start_time = "2025-01-25 00:00",
        end_time = "2025-01-27 23:59",
        multiplier = 2.0,
        affects = "coin_rewards"
    },
    new_year_event = {
        type = "special",
        start_time = "2024-12-31 00:00",
        end_time = "2025-01-07 23:59",
        special_levels = {51, 52, 53},
        exclusive_rewards = {"new_year_skin", "fireworks_effect"}
    }
}

function M.init()
    M.active_events = {}
    check_active_events()
end

function M.update(dt)
    -- Verificar eventos activos cada minuto
    if os.time() % 60 == 0 then
        check_active_events()
    end
end

function check_active_events()
    local current_time = os.time()

    for event_name, event_data in pairs(EVENTS) do
        if event_data.type == "scheduled" or event_data.type == "special" then
            local start_time = parse_time(event_data.start_time)
            local end_time = parse_time(event_data.end_time)

            if current_time >= start_time and current_time <= end_time then
                if not M.active_events[event_name] then
                    start_event(event_name, event_data)
                end
            else
                if M.active_events[event_name] then
                    end_event(event_name)
                end
            end
        end
    end
end

function start_event(event_name, event_data)
    M.active_events[event_name] = event_data
    print("Evento iniciado:", event_name)

    -- Notificar al jugador
    msg.post("/ui", "show_event_notification", {
        event = event_name,
        data = event_data
    })
end

function end_event(event_name)
    M.active_events[event_name] = nil
    print("Evento terminado:", event_name)
end

function parse_time(time_string)
    -- Convertir string de tiempo a timestamp
    local pattern = "(%d+)-(%d+)-(%d+) (%d+):(%d+)"
    local year, month, day, hour, min = time_string:match(pattern)
    return os.time({
        year = tonumber(year),
        month = tonumber(month),
        day = tonumber(day),
        hour = tonumber(hour),
        min = tonumber(min)
    })
end

return M

Parte 7: Post-Launch y Mantenimiento

Sistema de Feedback

-- feedback/feedback_manager.script
local M = {}

function M.init()
    M.feedback_triggers = {
        level_complete_count = 10,  -- Después de 10 niveles
        session_count = 5,  -- Después de 5 sesiones
        playtime_minutes = 60  -- Después de 1 hora
    }

    M.feedback_shown = false
end

function M.check_feedback_trigger()
    if M.feedback_shown then
        return
    end

    local stats = get_player_stats()

    if stats.levels_completed >= M.feedback_triggers.level_complete_count and
       stats.sessions >= M.feedback_triggers.session_count and
       stats.playtime >= M.feedback_triggers.playtime_minutes then

        show_feedback_request()
    end
end

function show_feedback_request()
    M.feedback_shown = true

    msg.post("/ui", "show_rating_dialog", {
        title = "¿Te gusta nuestro juego?",
        message = "Tu opinión nos ayuda a mejorar. ¿Podrías calificarnos?",
        buttons = {"Calificar", "Más tarde", "No, gracias"}
    })
end

function handle_feedback_response(response)
    if response == "rate" then
        open_store_rating()
    elseif response == "later" then
        -- Preguntar de nuevo en 3 días
        schedule_feedback_retry(3 * 24 * 60 * 60)
    elseif response == "never" then
        M.feedback_shown = true  -- No preguntar más
    end
end

function open_store_rating()
    local store_url = ""

    if sys.get_sys_info().system_name == "Android" then
        store_url = "market://details?id=" .. sys.get_config("android.package")
    elseif sys.get_sys_info().system_name == "iPhone OS" then
        store_url = "itms-apps://itunes.apple.com/app/id" .. get_app_store_id()
    end

    if store_url ~= "" then
        sys.open_url(store_url)
    end
end

return M

Update System

-- updates/update_manager.script
local M = {}

function M.init()
    M.current_version = sys.get_config("project.version")
    M.update_check_interval = 24 * 60 * 60  -- 24 horas

    check_for_updates()
end

function check_for_updates()
    -- En un proyecto real, consultar servidor
    local server_url = "https://api.tujuego.com/version"

    http.request(server_url, "GET", function(self, id, response)
        if response.status == 200 then
            local server_data = json.decode(response.response)
            handle_version_response(server_data)
        end
    end)
end

function handle_version_response(data)
    local server_version = data.version
    local force_update = data.force_update or false
    local update_message = data.message or "Nueva versión disponible"

    if version_compare(server_version, M.current_version) > 0 then
        show_update_dialog(update_message, force_update)
    end
end

function version_compare(v1, v2)
    local v1_parts = string_split(v1, ".")
    local v2_parts = string_split(v2, ".")

    for i = 1, math.max(#v1_parts, #v2_parts) do
        local v1_part = tonumber(v1_parts[i]) or 0
        local v2_part = tonumber(v2_parts[i]) or 0

        if v1_part > v2_part then
            return 1
        elseif v1_part < v2_part then
            return -1
        end
    end

    return 0
end

function string_split(str, delimiter)
    local result = {}
    for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do
        table.insert(result, match)
    end
    return result
end

return M

Ejercicio Final: Lanzamiento Completo

Checklist de Lanzamiento

-- Checklist completo para lanzamiento
launch_checklist = {
    technical = {
        "✓ Game.project optimizado para release",
        "✓ Todos los assets comprimidos",
        "✓ Texture profiles configurados",
        "✓ APK/IPA firmado correctamente",
        "✓ Testing en dispositivos reales",
        "✓ Crash testing completado",
        "✓ Performance testing (60 FPS consistente)",
        "✓ Batería optimizada"
    },

    store_preparation = {
        "✓ Screenshots de alta calidad",
        "✓ Descripción optimizada ASO",
        "✓ Keywords research completo",
        "✓ Iconos en todos los tamaños",
        "✓ Video trailer (opcional)",
        "✓ Políticas de privacidad",
        "✓ Términos de servicio"
    },

    monetization = {
        "✓ AdMob integrado y testeado",
        "✓ IAP configurado",
        "✓ Analytics implementado",
        "✓ A/B testing setup",
        "✓ Precios optimizados por región"
    },

    marketing = {
        "✓ Soft launch completado",
        "✓ Métricas objetivo alcanzadas",
        "✓ Press kit preparado",
        "✓ Redes sociales configuradas",
        "✓ Influencer outreach plan",
        "✓ Launch day plan"
    }
}

Conclusión

¡Felicidades! Has completado el tutorial más completo de Defold Engine. Ahora tienes todas las herramientas necesarias para:

Próximos Pasos Recomendados

  1. Practica constantemente - Crea proyectos pequeños regularmente
  2. Únete a la comunidad - Forum.defold.com es increíblemente útil
  3. Estudia juegos exitosos - Analiza qué los hace especiales
  4. Experimenta con géneros - No te limites a un solo tipo de juego
  5. Itera basado en feedback - Los datos de usuarios son oro

Recursos Adicionales


⬅️ Anterior: Audio y Música | 🏠 Volver al Índice

¡Tu viaje como desarrollador de juegos con Defold apenas comienza! Con las bases sólidas que has construido, estás listo para crear experiencias increíbles que lleguen a millones de jugadores. ¡Éxito en tus proyectos!