← Volver al listado de tecnologías
Proceso Completo de Publicación en App Store y Google Play
Proceso Completo de Publicación en App Store y Google Play
Publicar tu juego en las tiendas móviles es un proceso complejo que requiere preparación cuidadosa. Esta guía te llevará paso a paso por todo el proceso.
Preparación Pre-Lanzamiento
1. Checklist de Preparación
Lista de Verificación Técnica
-- pre_launch_checklist.lua
local M = {}
M.TECHNICAL_CHECKLIST = {
builds = {
{item = "Build de release firmado correctamente", completed = false},
{item = "Todas las extensiones nativas funcionando", completed = false},
{item = "Sin referencias a desarrollo/debug", completed = false},
{item = "Iconos en todas las resoluciones requeridas", completed = false},
{item = "Screenshots de todas las pantallas", completed = false},
{item = "Configuración de permisos correcta", completed = false}
},
compliance = {
{item = "Política de privacidad implementada", completed = false},
{item = "Términos de servicio creados", completed = false},
{item = "Cumplimiento COPPA (menores)", completed = false},
{item = "Cumplimiento GDPR (Europa)", completed = false},
{item = "Certificados de edad/rating", completed = false}
},
monetization = {
{item = "IAP configurados y testeados", completed = false},
{item = "Ads integrados y funcionando", completed = false},
{item = "Analytics implementado", completed = false},
{item = "Crash reporting activo", completed = false},
{item = "Remote config configurado", completed = false}
},
testing = {
{item = "Testing en múltiples dispositivos", completed = false},
{item = "Testing de performance", completed = false},
{item = "Testing de memoria", completed = false},
{item = "Testing de batería", completed = false},
{item = "Testing de conectividad", completed = false}
}
}
function M.check_build_requirements(self)
local issues = {}
-- Verificar configuración de build
if not self:has_release_keystore() then
table.insert(issues, "Falta keystore para release")
end
if not self:has_all_required_icons() then
table.insert(issues, "Faltan iconos en algunas resoluciones")
end
if self:has_debug_code() then
table.insert(issues, "Código de debug encontrado en release")
end
if not self:has_privacy_policy() then
table.insert(issues, "Falta política de privacidad")
end
return issues
end
function M.generate_build_report(self)
local report = {
timestamp = os.time(),
version = sys.get_config("project.version"),
platform = sys.get_sys_info().system_name,
issues = self:check_build_requirements(),
file_sizes = self:get_build_sizes(),
permissions = self:get_required_permissions()
}
return report
end
return M
2. Asset Preparation
Generador de Assets para Tiendas
-- store_assets_generator.lua
local M = {}
M.REQUIRED_ASSETS = {
ios = {
icons = {
{size = "20x20", scale = "2x", filename = "Icon-40.png"},
{size = "20x20", scale = "3x", filename = "Icon-60.png"},
{size = "29x29", scale = "2x", filename = "Icon-58.png"},
{size = "29x29", scale = "3x", filename = "Icon-87.png"},
{size = "40x40", scale = "2x", filename = "Icon-80.png"},
{size = "40x40", scale = "3x", filename = "Icon-120.png"},
{size = "60x60", scale = "2x", filename = "Icon-120.png"},
{size = "60x60", scale = "3x", filename = "Icon-180.png"},
{size = "1024x1024", scale = "1x", filename = "Icon-1024.png"}
},
screenshots = {
{device = "iPhone 6.5", size = "1242x2688", count = 3},
{device = "iPhone 6.1", size = "828x1792", count = 3},
{device = "iPhone 5.5", size = "1242x2208", count = 3},
{device = "iPad Pro 12.9", size = "2048x2732", count = 3}
}
},
android = {
icons = {
{density = "mdpi", size = "48x48", folder = "drawable-mdpi"},
{density = "hdpi", size = "72x72", folder = "drawable-hdpi"},
{density = "xhdpi", size = "96x96", folder = "drawable-xhdpi"},
{density = "xxhdpi", size = "144x144", folder = "drawable-xxhdpi"},
{density = "xxxhdpi", size = "192x192", folder = "drawable-xxxhdpi"}
},
screenshots = {
{type = "phone", size = "1080x1920", count = 8},
{type = "tablet_7", size = "1200x1920", count = 8},
{type = "tablet_10", size = "1600x2560", count = 8}
},
feature_graphic = {
size = "1024x500",
format = "PNG or JPG"
}
}
}
function M.validate_assets(platform)
local required = M.REQUIRED_ASSETS[platform]
local missing = {}
-- Verificar iconos
for _, icon in ipairs(required.icons) do
local path = self:get_icon_path(platform, icon)
if not self:file_exists(path) then
table.insert(missing, "Icon: " .. icon.filename or icon.size)
end
end
-- Verificar screenshots
for _, screenshot_type in ipairs(required.screenshots) do
local screenshot_count = self:count_screenshots(platform, screenshot_type)
if screenshot_count < screenshot_type.count then
table.insert(missing, string.format("Screenshots %s: %d/%d",
screenshot_type.device or screenshot_type.type,
screenshot_count, screenshot_type.count))
end
end
return missing
end
function M.generate_store_listing_template(platform, app_info)
if platform == "ios" then
return M.generate_ios_listing(app_info)
elseif platform == "android" then
return M.generate_android_listing(app_info)
end
end
function M.generate_ios_listing(app_info)
return {
name = app_info.name,
subtitle = app_info.subtitle,
description = M.format_ios_description(app_info.description),
keywords = table.concat(app_info.keywords, ","),
support_url = app_info.support_url,
marketing_url = app_info.marketing_url,
privacy_policy_url = app_info.privacy_policy_url,
category = app_info.category,
age_rating = app_info.age_rating
}
end
function M.generate_android_listing(app_info)
return {
title = app_info.name,
short_description = app_info.short_description,
full_description = M.format_android_description(app_info.description),
category = app_info.category,
content_rating = app_info.content_rating,
website = app_info.website,
email = app_info.contact_email,
privacy_policy = app_info.privacy_policy_url
}
end
return M
Configuración de App Store (iOS)
1. App Store Connect Setup
Configuración de Aplicación iOS
-- ios_store_config.lua
local M = {}
M.APP_STORE_CONFIG = {
app_information = {
name = "Mi Juego Increíble",
bundle_id = "com.miestudio.mijuego",
sku = "MIJUEGO2024",
primary_language = "Spanish (Spain)",
category = "Games",
secondary_category = "Action"
},
pricing_and_availability = {
price_tier = "Free",
availability = {
worldwide = true,
specific_countries = {}
},
app_store_visibility = true,
pre_order = false
},
version_information = {
version = "1.0",
copyright = "© 2024 Mi Estudio",
age_rating = {
violence = "None",
profanity = "None",
sexual_content = "None",
horror = "None",
gambling = "None"
}
},
review_information = {
contact_email = "[email protected]",
contact_phone = "+1234567890",
demo_account = {
username = "[email protected]",
password = "DemoPassword123"
},
notes = "Este juego incluye compras in-app opcionales."
}
}
M.LOCALIZATIONS = {
["es-ES"] = {
name = "Mi Juego Increíble",
subtitle = "Aventura épica móvil",
description = [[
¡Embárcate en una aventura épica en este emocionante juego de acción!
CARACTERÍSTICAS PRINCIPALES:
• Más de 100 niveles desafiantes
• Gráficos impresionantes y efectos visuales
• Sistema de progresión profundo
• Multijugador en línea
• Eventos especiales semanales
GAMEPLAY ADICTIVO:
Domina controles intuitivos mientras navegas por mundos peligrosos llenos de enemigos y tesoros. Mejora tu personaje, desbloquea nuevas habilidades y compite con jugadores de todo el mundo.
CARACTERÍSTICAS SOCIALES:
• Conecta con Game Center
• Compite en tablas de clasificación
• Desbloquea logros únicos
• Comparte tus mejores momentos
ACTUALIZACIONES REGULARES:
Agregamos constantemente nuevo contenido, niveles y características basadas en los comentarios de los jugadores.
¡Descarga ahora y únete a millones de jugadores en esta aventura épica!
]],
keywords = "juego,acción,aventura,multijugador,móvil,gratis",
release_notes = "¡Primera versión! Incluye 100 niveles, modo multijugador y características sociales."
},
["en-US"] = {
name = "My Amazing Game",
subtitle = "Epic mobile adventure",
description = "English description here...",
keywords = "game,action,adventure,multiplayer,mobile,free",
release_notes = "Initial release! Features 100 levels, multiplayer mode, and social features."
}
}
function M.generate_app_store_metadata()
local metadata = {
config = M.APP_STORE_CONFIG,
localizations = M.LOCALIZATIONS,
build_info = {
version = sys.get_config("project.version"),
build_number = os.time(),
min_ios_version = "12.0"
}
}
return metadata
end
function M.validate_app_store_submission()
local issues = {}
-- Verificar información requerida
if not M.APP_STORE_CONFIG.app_information.name then
table.insert(issues, "App name is required")
end
if string.len(M.APP_STORE_CONFIG.app_information.name) > 50 then
table.insert(issues, "App name exceeds 50 characters")
end
-- Verificar descripción
local desc = M.LOCALIZATIONS["es-ES"].description
if string.len(desc) > 4000 then
table.insert(issues, "Description exceeds 4000 characters")
end
-- Verificar keywords
local keywords = M.LOCALIZATIONS["es-ES"].keywords
if string.len(keywords) > 100 then
table.insert(issues, "Keywords exceed 100 characters")
end
return issues
end
return M
2. Build Signing y Provisioning
Configuración de Certificados
#!/bin/bash
# ios_signing_setup.sh
echo "Setting up iOS code signing..."
# Verificar herramientas
if ! command -v xcodebuild &> /dev/null; then
echo "Error: Xcode command line tools not installed"
exit 1
fi
# Variables
TEAM_ID="ABCD123456"
BUNDLE_ID="com.miestudio.mijuego"
APP_NAME="Mi Juego Increíble"
# Crear certificado de desarrollo
echo "Creating development certificate..."
security create-keypair -a 2048 -t RSA -f PKCS12 -k login.keychain
# Configurar provisioning profile
echo "Setting up provisioning profile..."
cat > ExportOptions.plist << EOF
<?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>teamID</key>
<string>$TEAM_ID</string>
<key>method</key>
<string>app-store</string>
<key>destination</key>
<string>export</string>
<key>uploadBitcode</key>
<true/>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<true/>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
EOF
# Build para App Store
echo "Building for App Store..."
xcodebuild -workspace "YourGame.xcworkspace" \
-scheme "YourGame" \
-configuration Release \
-destination generic/platform=iOS \
-archivePath "build/YourGame.xcarchive" \
archive
# Export para App Store
echo "Exporting for App Store..."
xcodebuild -exportArchive \
-archivePath "build/YourGame.xcarchive" \
-exportPath "build/AppStore" \
-exportOptionsPlist ExportOptions.plist
echo "iOS build complete!"
Configuración de Google Play Store
1. Google Play Console Setup
Configuración de Aplicación Android
-- android_store_config.lua
local M = {}
M.PLAY_STORE_CONFIG = {
store_listing = {
title = "Mi Juego Increíble",
short_description = "Aventura épica móvil con más de 100 niveles desafiantes",
full_description = [[
¡Embárcate en una aventura épica en este emocionante juego de acción!
🎮 CARACTERÍSTICAS PRINCIPALES
• Más de 100 niveles únicos y desafiantes
• Gráficos 3D impresionantes optimizados para móviles
• Sistema de progresión profundo con mejoras de personaje
• Modo multijugador en tiempo real
• Eventos especiales y desafíos semanales
• Completamente GRATIS para jugar
⚡ GAMEPLAY ADICTIVO
Domina controles táctiles intuitivos mientras exploras mundos peligrosos llenos de enemigos, tesoros ocultos y secretos por descubrir. Mejora tu personaje, desbloquea habilidades poderosas y equipo épico.
🏆 CARACTERÍSTICAS SOCIALES
• Integración completa con Google Play Games
• Compite en tablas de clasificación globales
• Desbloquea más de 50 logros únicos
• Comparte capturas y momentos épicos
• Invita y desafía a tus amigos
🔄 ACTUALIZACIONES CONSTANTES
Agregamos regularmente:
• Nuevos niveles y mundos
• Personajes y habilidades
• Eventos temáticos
• Mejoras basadas en feedback de jugadores
📱 OPTIMIZADO PARA MÓVILES
• Funciona sin conexión a internet
• Batería optimizada para sesiones largas
• Compatible con dispositivos de gama baja
• Interfaz adaptable a cualquier pantalla
¡Únete a millones de jugadores y descarga GRATIS ahora!
]],
category = "GAME_ACTION",
tags = ["acción", "aventura", "multijugador", "gratis"],
website = "https://miestudio.com/mijuego",
email = "[email protected]",
privacy_policy = "https://miestudio.com/privacy"
},
content_rating = {
questionnaire = {
violence = {
cartoon_violence = false,
realistic_violence = false,
blood = false,
sexual_violence = false
},
interactive_elements = {
users_interact = true,
shares_info = false,
shares_location = false
},
substance_use = {
alcohol = false,
drugs = false,
tobacco = false
}
}
},
pricing_distribution = {
pricing = "free",
countries = "all",
device_categories = ["phone", "tablet"],
exclude_countries = []
},
app_content = {
target_audience = {
age_group = "13_17",
appeals_to_children = false
},
ads = {
contains_ads = true,
ad_networks = ["AdMob", "Unity Ads"]
},
data_safety = {
collects_data = true,
shares_data = false,
data_types = [
"device_identifiers",
"app_activity",
"app_info_performance"
]
}
}
}
M.RELEASE_NOTES = {
["es-ES"] = "¡Primera versión disponible! Incluye 100 niveles emocionantes, modo multijugador y características sociales completas.",
["en-US"] = "Initial release! Features 100 exciting levels, multiplayer mode, and full social features.",
["pt-BR"] = "Lançamento inicial! Apresenta 100 níveis emocionantes, modo multijogador e recursos sociais completos.",
["fr-FR"] = "Version initiale ! Comprend 100 niveaux passionnants, un mode multijoueur et des fonctionnalités sociales complètes."
}
function M.generate_play_store_metadata()
local metadata = {
config = M.PLAY_STORE_CONFIG,
release_notes = M.RELEASE_NOTES,
build_info = {
version_name = sys.get_config("project.version"),
version_code = tonumber(sys.get_config("android.version_code", "1")),
min_sdk_version = tonumber(sys.get_config("android.minimum_sdk_version", "21")),
target_sdk_version = tonumber(sys.get_config("android.target_sdk_version", "34"))
}
}
return metadata
end
function M.validate_play_store_submission()
local issues = {}
-- Verificar título
local title = M.PLAY_STORE_CONFIG.store_listing.title
if string.len(title) > 50 then
table.insert(issues, "Title exceeds 50 characters")
end
-- Verificar descripción corta
local short_desc = M.PLAY_STORE_CONFIG.store_listing.short_description
if string.len(short_desc) > 80 then
table.insert(issues, "Short description exceeds 80 characters")
end
-- Verificar descripción completa
local full_desc = M.PLAY_STORE_CONFIG.store_listing.full_description
if string.len(full_desc) > 4000 then
table.insert(issues, "Full description exceeds 4000 characters")
end
return issues
end
return M
2. Android App Bundle Generation
Generación de AAB
#!/bin/bash
# android_bundle_build.sh
echo "Building Android App Bundle..."
# Variables
APP_NAME="MiJuego"
PACKAGE_NAME="com.miestudio.mijuego"
VERSION_NAME="1.0"
VERSION_CODE="1"
KEYSTORE_PATH="release-key.keystore"
KEYSTORE_ALIAS="mijuego-key"
# Verificar herramientas
if ! command -v bundletool &> /dev/null; then
echo "Error: bundletool not found. Download from: https://github.com/google/bundletool"
exit 1
fi
# Limpiar builds anteriores
echo "Cleaning previous builds..."
rm -rf build/android/*
# Configurar variables de entorno
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export ANDROID_HOME=~/Android/Sdk
# Build con Defold
echo "Building with Defold..."
# Aquí usarías el build server de Defold o bob.jar
java -jar bob.jar --platform armv7-android,arm64-android \
--bundle-output build/android/${APP_NAME}.aab \
--variant release \
resolve build bundle
# Verificar el bundle
echo "Verifying bundle..."
bundletool validate --bundle=build/android/${APP_NAME}.aab
# Generar APKs universales para testing
echo "Generating universal APK for testing..."
bundletool build-apks \
--bundle=build/android/${APP_NAME}.aab \
--output=build/android/${APP_NAME}.apks \
--mode=universal
# Extract APK universal
bundletool extract-apks \
--apks=build/android/${APP_NAME}.apks \
--output-dir=build/android/extracted \
--device-spec=universal.json
echo "Android App Bundle build complete!"
echo "AAB file: build/android/${APP_NAME}.aab"
echo "Test APK: build/android/extracted/universal.apk"
# Mostrar información del bundle
echo "Bundle information:"
bundletool dump manifest --bundle=build/android/${APP_NAME}.aab
ASO (App Store Optimization)
1. Keyword Research y Optimization
Sistema de ASO
-- aso_optimizer.lua
local M = {}
M.KEYWORD_RESEARCH = {
primary_keywords = {
{keyword = "juego acción", volume = 10000, difficulty = 8, relevance = 10},
{keyword = "aventura móvil", volume = 5000, difficulty = 6, relevance = 9},
{keyword = "multijugador", volume = 8000, difficulty = 7, relevance = 8},
{keyword = "juego gratis", volume = 50000, difficulty = 9, relevance = 7}
},
secondary_keywords = {
{keyword = "rpg móvil", volume = 3000, difficulty = 5, relevance = 6},
{keyword = "juego offline", volume = 2000, difficulty = 4, relevance = 5},
{keyword = "battle royale", volume = 15000, difficulty = 9, relevance = 4}
},
long_tail_keywords = {
"mejor juego acción 2024",
"juego aventura sin internet",
"multijugador tiempo real móvil",
"juego gratis sin anuncios"
}
}
M.ASO_TEMPLATES = {
ios_title = "Mi Juego - Aventura Épica",
ios_subtitle = "Acción Multijugador Gratis",
android_title = "Mi Juego: Aventura Acción",
android_short_desc = "Épica aventura multijugador con 100+ niveles. ¡Gratis!",
description_structure = {
hook = "¡La aventura más épica te espera!",
features = {
"100+ niveles únicos",
"Multijugador en tiempo real",
"Gráficos impresionantes",
"Completamente GRATIS"
},
social_proof = "Únete a millones de jugadores",
call_to_action = "¡Descarga GRATIS ahora!"
}
}
function M.optimize_title(platform, base_title, keywords)
local optimized = base_title
local char_limit = platform == "ios" and 50 or 50
-- Añadir keywords más importantes que quepan
for _, kw in ipairs(keywords) do
local test_title = optimized .. " " .. kw.keyword
if string.len(test_title) <= char_limit then
if not string.find(string.lower(optimized), string.lower(kw.keyword)) then
optimized = test_title
end
end
end
return optimized
end
function M.generate_description(template, keywords)
local description = template.hook .. "\n\n"
-- Sección de características
description = description .. "🎮 CARACTERÍSTICAS:\n"
for _, feature in ipairs(template.features) do
description = description .. "• " .. feature .. "\n"
end
-- Insertar keywords naturalmente
description = description .. "\n" .. self:insert_keywords_naturally(template, keywords)
-- Call to action
description = description .. "\n\n" .. template.call_to_action
return description
end
function insert_keywords_naturally(self, template, keywords)
local sections = {
"Experimenta la mejor " .. keywords[1].keyword .. " en tu dispositivo móvil.",
"Perfecto para fans de " .. keywords[2].keyword .. " y " .. keywords[3].keyword .. ".",
"Este " .. keywords[4].keyword .. " redefine lo que esperas de los juegos móviles."
}
return table.concat(sections, " ")
end
function M.track_aso_performance()
return {
keyword_rankings = {
{keyword = "juego acción", position = 15, change = "+3"},
{keyword = "aventura móvil", position = 8, change = "+5"},
{keyword = "multijugador", position = 25, change = "-2"}
},
conversion_metrics = {
impressions = 10000,
page_views = 1200,
installs = 350,
conversion_rate = 29.2
}
}
end
return M
2. Screenshot y Visual Optimization
Generador de Screenshots para Tiendas
-- screenshot_generator.lua
local M = {}
M.SCREENSHOT_TEMPLATES = {
ios = {
{
title = "¡Aventura Épica!",
subtitle = "100+ Niveles Únicos",
gameplay_focus = "level_selection",
device = "iPhone 12 Pro",
size = "1170x2532"
},
{
title = "Multijugador Épico",
subtitle = "Compite en Tiempo Real",
gameplay_focus = "multiplayer_battle",
device = "iPhone 12 Pro",
size = "1170x2532"
},
{
title = "Gráficos Increíbles",
subtitle = "Optimizado para Móviles",
gameplay_focus = "gameplay_action",
device = "iPhone 12 Pro",
size = "1170x2532"
}
},
android = {
{
title = "¡AVENTURA ÉPICA!",
subtitle = "Más de 100 niveles únicos",
features = ["Multijugador", "Gratis", "Sin Internet"],
gameplay_focus = "level_overview"
},
{
title = "MULTIJUGADOR",
subtitle = "Batalla en tiempo real",
features = ["PvP", "Rankings", "Torneos"],
gameplay_focus = "multiplayer_action"
}
}
}
function M.generate_screenshot_config(platform, template)
local config = {
platform = platform,
template = template,
overlay_elements = {
title = {
text = template.title,
font_size = platform == "ios" and 48 or 52,
color = "#FFFFFF",
stroke_color = "#000000",
position = "top"
},
subtitle = {
text = template.subtitle,
font_size = platform == "ios" and 32 or 36,
color = "#FFD700",
position = "below_title"
}
},
effects = {
glow = true,
gradient_overlay = true,
device_frame = platform == "ios"
}
}
if platform == "android" and template.features then
config.overlay_elements.features = {
items = template.features,
style = "badge",
position = "bottom"
}
end
return config
end
function M.capture_gameplay_screenshot(scene_name, config)
-- Configurar cámara para screenshot
msg.post("main:/camera", "set_screenshot_mode", {
resolution = config.size,
scene = scene_name,
ui_overlay = true
})
-- Capturar screenshot
msg.post("@render:", "capture_screen", {
path = "/screenshots/" .. scene_name .. "_raw.png"
}, function(self, message_id, message)
if message_id == hash("screenshot_captured") then
-- Procesar screenshot con overlays
M.process_screenshot(message.path, config)
end
end)
end
function M.process_screenshot(image_path, config)
-- Esta función procesaría la imagen añadiendo overlays
-- En una implementación real, usarías herramientas externas
-- o una extensión nativa para edición de imágenes
local processed_path = string.gsub(image_path, "_raw", "_store")
print("Processing screenshot: " .. image_path)
print("Output: " .. processed_path)
print("Config: " .. json.encode(config))
-- Simular procesamiento
timer.delay(2.0, false, function()
print("Screenshot processed successfully!")
msg.post("main:/ui", "screenshot_ready", {
path = processed_path,
config = config
})
end)
end
return M
Release Management
1. Version Control y Release Pipeline
Sistema de Release Automation
#!/bin/bash
# release_pipeline.sh
set -e # Exit on any error
# Configuration
VERSION=$1
BUILD_NUMBER=$2
RELEASE_TYPE=${3:-"patch"} # patch, minor, major
if [ -z "$VERSION" ] || [ -z "$BUILD_NUMBER" ]; then
echo "Usage: $0 <version> <build_number> [release_type]"
echo "Example: $0 1.2.3 42 minor"
exit 1
fi
echo "🚀 Starting release pipeline for version $VERSION build $BUILD_NUMBER"
# Step 1: Pre-flight checks
echo "📋 Running pre-flight checks..."
./scripts/pre_flight_check.sh
# Step 2: Update version numbers
echo "📝 Updating version numbers..."
sed -i.bak "s/version = .*/version = $VERSION/" game.project
sed -i.bak "s/version_code = .*/version_code = $BUILD_NUMBER/" game.project
# Step 3: Build for all platforms
echo "🔨 Building for all platforms..."
# iOS Build
echo "Building iOS..."
java -jar bob.jar \
--platform arm64-ios \
--bundle-output builds/ios/game_${VERSION}.ipa \
--variant release \
resolve build bundle
# Android Build
echo "Building Android..."
java -jar bob.jar \
--platform armv7-android,arm64-android \
--bundle-output builds/android/game_${VERSION}.aab \
--variant release \
resolve build bundle
# Step 4: Run automated tests
echo "🧪 Running automated tests..."
./scripts/run_tests.sh
# Step 5: Generate release notes
echo "📄 Generating release notes..."
./scripts/generate_release_notes.sh $VERSION > releases/release_notes_$VERSION.md
# Step 6: Upload to stores (if not dry-run)
if [ "$DRY_RUN" != "true" ]; then
echo "📤 Uploading to stores..."
# Upload to TestFlight
xcrun altool --upload-app \
-f builds/ios/game_${VERSION}.ipa \
-t ios \
-u "$APPLE_ID" \
-p "$APPLE_PASSWORD"
# Upload to Play Console (Internal Track)
bundletool upload-bundle \
--bundle builds/android/game_${VERSION}.aab \
--track internal
fi
# Step 7: Git tagging
echo "🏷️ Creating git tag..."
git add .
git commit -m "Release version $VERSION build $BUILD_NUMBER"
git tag -a "v$VERSION" -m "Release version $VERSION"
git push origin main --tags
# Step 8: Notification
echo "📢 Sending notifications..."
./scripts/send_release_notification.sh $VERSION $BUILD_NUMBER
echo "✅ Release pipeline completed successfully!"
echo "📊 Build artifacts:"
echo " iOS: builds/ios/game_${VERSION}.ipa"
echo " Android: builds/android/game_${VERSION}.aab"
echo " Release notes: releases/release_notes_$VERSION.md"
2. Phased Rollout Strategy
Sistema de Rollout Progresivo
-- rollout_manager.lua
local M = {}
M.ROLLOUT_PHASES = {
{
name = "Internal Testing",
percentage = 0,
duration_days = 3,
criteria = {
crash_rate_threshold = 0.1,
anr_rate_threshold = 0.05,
min_rating = 4.0
},
users = "internal_testers"
},
{
name = "Closed Beta",
percentage = 1,
duration_days = 7,
criteria = {
crash_rate_threshold = 0.5,
anr_rate_threshold = 0.2,
min_rating = 3.8
},
users = "beta_testers"
},
{
name = "Open Beta",
percentage = 5,
duration_days = 5,
criteria = {
crash_rate_threshold = 1.0,
anr_rate_threshold = 0.5,
min_rating = 3.5
},
users = "open_beta"
},
{
name = "Gradual Rollout 20%",
percentage = 20,
duration_days = 3,
criteria = {
crash_rate_threshold = 2.0,
anr_rate_threshold = 1.0,
min_rating = 3.0
},
users = "production"
},
{
name = "Full Rollout",
percentage = 100,
duration_days = 0,
criteria = {},
users = "production"
}
}
function M.get_current_phase(version)
-- En una implementación real, esto vendría de tu sistema de tracking
return {
phase_index = 3,
start_date = os.time() - (2 * 24 * 60 * 60), -- Hace 2 días
metrics = {
crash_rate = 0.8,
anr_rate = 0.3,
rating = 3.7,
downloads = 1250,
reviews = 45
}
}
end
function M.should_proceed_to_next_phase(current_phase_info)
local phase = M.ROLLOUT_PHASES[current_phase_info.phase_index]
local metrics = current_phase_info.metrics
-- Verificar criterios de calidad
if metrics.crash_rate > phase.criteria.crash_rate_threshold then
return false, "Crash rate too high"
end
if metrics.anr_rate > phase.criteria.anr_rate_threshold then
return false, "ANR rate too high"
end
if metrics.rating < phase.criteria.min_rating then
return false, "Rating too low"
end
-- Verificar duración mínima
local days_in_phase = (os.time() - current_phase_info.start_date) / (24 * 60 * 60)
if days_in_phase < phase.duration_days then
return false, "Minimum duration not met"
end
return true, "All criteria met"
end
function M.generate_rollout_report(version)
local current_phase = M.get_current_phase(version)
local can_proceed, reason = M.should_proceed_to_next_phase(current_phase)
return {
version = version,
current_phase = M.ROLLOUT_PHASES[current_phase.phase_index],
metrics = current_phase.metrics,
can_proceed = can_proceed,
proceed_reason = reason,
recommendations = M.get_recommendations(current_phase)
}
end
function M.get_recommendations(current_phase_info)
local recommendations = {}
local metrics = current_phase_info.metrics
if metrics.crash_rate > 1.0 then
table.insert(recommendations, "Consider hotfix for crash issues")
end
if metrics.rating < 3.5 then
table.insert(recommendations, "Monitor user feedback closely")
end
if metrics.downloads < 1000 then
table.insert(recommendations, "Consider increasing marketing efforts")
end
return recommendations
end
return M
Post-Launch Optimization
1. Performance Monitoring
Sistema de Monitoring Post-Launch
-- post_launch_monitor.lua
local M = {}
function init(self)
self.metrics = {
downloads = 0,
active_users = 0,
retention_day_1 = 0,
retention_day_7 = 0,
crash_rate = 0,
rating = 0,
revenue = 0
}
self.alerts = {
crash_rate_threshold = 2.0,
rating_threshold = 3.0,
retention_threshold = 20.0
}
end
function M.collect_store_metrics(self)
-- En una implementación real, esto se conectaría a APIs de las tiendas
return {
app_store = {
downloads_today = 150,
rating = 4.2,
reviews_count = 89,
impressions = 5420,
conversion_rate = 2.8
},
google_play = {
downloads_today = 320,
rating = 3.9,
reviews_count = 156,
impressions = 8950,
conversion_rate = 3.6
}
}
end
function M.analyze_user_feedback(self, reviews)
local sentiment_analysis = {
positive = 0,
neutral = 0,
negative = 0,
common_issues = {},
feature_requests = {}
}
for _, review in ipairs(reviews) do
-- Análisis básico de sentimiento
local sentiment = self:analyze_sentiment(review.text)
sentiment_analysis[sentiment] = sentiment_analysis[sentiment] + 1
-- Extraer problemas comunes
local issues = self:extract_issues(review.text)
for _, issue in ipairs(issues) do
sentiment_analysis.common_issues[issue] = (sentiment_analysis.common_issues[issue] or 0) + 1
end
end
return sentiment_analysis
end
function analyze_sentiment(self, text)
-- Análisis básico de sentimiento
local positive_words = {"great", "amazing", "love", "excellent", "fantastic"}
local negative_words = {"bad", "terrible", "hate", "awful", "worst"}
local positive_count = 0
local negative_count = 0
local lower_text = string.lower(text)
for _, word in ipairs(positive_words) do
if string.find(lower_text, word) then
positive_count = positive_count + 1
end
end
for _, word in ipairs(negative_words) do
if string.find(lower_text, word) then
negative_count = negative_count + 1
end
end
if positive_count > negative_count then
return "positive"
elseif negative_count > positive_count then
return "negative"
else
return "neutral"
end
end
function M.generate_health_report(self)
local store_metrics = self:collect_store_metrics()
local combined_metrics = {
total_downloads = store_metrics.app_store.downloads_today + store_metrics.google_play.downloads_today,
avg_rating = (store_metrics.app_store.rating + store_metrics.google_play.rating) / 2,
total_reviews = store_metrics.app_store.reviews_count + store_metrics.google_play.reviews_count
}
local health_score = self:calculate_health_score(combined_metrics)
return {
timestamp = os.time(),
metrics = combined_metrics,
health_score = health_score,
alerts = self:check_alerts(combined_metrics),
recommendations = self:get_health_recommendations(combined_metrics)
}
end
function calculate_health_score(self, metrics)
local score = 0
-- Rating score (0-40 points)
score = score + (metrics.avg_rating / 5.0) * 40
-- Download score (0-30 points)
local download_score = math.min(metrics.total_downloads / 1000, 1.0) * 30
score = score + download_score
-- Review engagement score (0-30 points)
local review_score = math.min(metrics.total_reviews / 100, 1.0) * 30
score = score + review_score
return math.floor(score)
end
function check_alerts(self, metrics)
local alerts = {}
if metrics.avg_rating < self.alerts.rating_threshold then
table.insert(alerts, {
level = "critical",
message = "Average rating below threshold",
value = metrics.avg_rating,
threshold = self.alerts.rating_threshold
})
end
return alerts
end
return M
2. Update Strategy
Sistema de Actualizaciones
-- update_strategy.lua
local M = {}
M.UPDATE_TYPES = {
HOTFIX = {
timeline = "1-3 days",
review_time = "24 hours",
scope = "Critical bugs only"
},
PATCH = {
timeline = "1-2 weeks",
review_time = "24-48 hours",
scope = "Bug fixes and minor features"
},
MINOR = {
timeline = "4-6 weeks",
review_time = "2-7 days",
scope = "New features and improvements"
},
MAJOR = {
timeline = "3-6 months",
review_time = "2-7 days",
scope = "Major features and overhauls"
}
}
function M.plan_update(issue_severity, user_impact, development_effort)
local update_type = "PATCH" -- Default
if issue_severity == "critical" and user_impact == "high" then
update_type = "HOTFIX"
elseif development_effort == "high" or user_impact == "major" then
update_type = "MINOR"
end
return {
type = update_type,
timeline = M.UPDATE_TYPES[update_type].timeline,
expected_review_time = M.UPDATE_TYPES[update_type].review_time,
scope = M.UPDATE_TYPES[update_type].scope
}
end
function M.generate_update_changelog(version, changes)
local changelog = "Version " .. version .. "\n\n"
-- Agrupar cambios por tipo
local grouped_changes = {
new_features = {},
improvements = {},
bug_fixes = {},
known_issues = {}
}
for _, change in ipairs(changes) do
table.insert(grouped_changes[change.type], change.description)
end
-- Generar changelog formateado
if #grouped_changes.new_features > 0 then
changelog = changelog .. "🆕 NEW FEATURES:\n"
for _, feature in ipairs(grouped_changes.new_features) do
changelog = changelog .. "• " .. feature .. "\n"
end
changelog = changelog .. "\n"
end
if #grouped_changes.improvements > 0 then
changelog = changelog .. "⚡ IMPROVEMENTS:\n"
for _, improvement in ipairs(grouped_changes.improvements) do
changelog = changelog .. "• " .. improvement .. "\n"
end
changelog = changelog .. "\n"
end
if #grouped_changes.bug_fixes > 0 then
changelog = changelog .. "🐛 BUG FIXES:\n"
for _, fix in ipairs(grouped_changes.bug_fixes) do
changelog = changelog .. "• " .. fix .. "\n"
end
changelog = changelog .. "\n"
end
changelog = changelog .. "\n¡Gracias por jugar! Seguimos trabajando para mejorar tu experiencia."
return changelog
end
return M
Compliance y Políticas
1. Privacy Policy Generator
Generador de Política de Privacidad
-- privacy_policy_generator.lua
local M = {}
M.PRIVACY_TEMPLATE = {
app_name = "Mi Juego Increíble",
company_name = "Mi Estudio",
contact_email = "[email protected]",
effective_date = "1 de enero de 2024",
data_collected = {
"Identificadores del dispositivo",
"Información de rendimiento de la aplicación",
"Datos de uso y actividad del juego",
"Información de compras in-app"
},
data_usage = {
"Mejorar la experiencia de juego",
"Proporcionar soporte al cliente",
"Analizar el rendimiento de la aplicación",
"Personalizar contenido y ofertas"
},
third_party_services = {
"Google Analytics",
"Firebase Crashlytics",
"AdMob",
"Unity Ads"
}
}
function M.generate_privacy_policy()
local policy = string.format([[
POLÍTICA DE PRIVACIDAD
Última actualización: %s
Esta política de privacidad describe cómo %s ("nosotros", "nuestro" o "nos") recopila, usa y comparte información cuando utilizas nuestra aplicación móvil %s.
1. INFORMACIÓN QUE RECOPILAMOS
Recopilamos los siguientes tipos de información:
%s
2. CÓMO UTILIZAMOS LA INFORMACIÓN
Utilizamos la información recopilada para:
%s
3. SERVICIOS DE TERCEROS
Nuestra aplicación utiliza los siguientes servicios de terceros:
%s
4. SEGURIDAD DE LOS DATOS
Implementamos medidas de seguridad apropiadas para proteger tu información contra acceso no autorizado, alteración, divulgación o destrucción.
5. CONTACTO
Si tienes preguntas sobre esta política de privacidad, puedes contactarnos en:
Email: %s
Esta política puede actualizarse ocasionalmente. Te notificaremos sobre cambios significativos.
]],
M.PRIVACY_TEMPLATE.effective_date,
M.PRIVACY_TEMPLATE.company_name,
M.PRIVACY_TEMPLATE.app_name,
M.format_list(M.PRIVACY_TEMPLATE.data_collected),
M.format_list(M.PRIVACY_TEMPLATE.data_usage),
M.format_list(M.PRIVACY_TEMPLATE.third_party_services),
M.PRIVACY_TEMPLATE.contact_email)
return policy
end
function M.format_list(items)
local formatted = ""
for _, item in ipairs(items) do
formatted = formatted .. "• " .. item .. "\n"
end
return formatted
end
return M
Mejores Prácticas
1. Launch Checklist
- ✅ Testear en múltiples dispositivos y OS versions
- ✅ Verificar todas las integraciones (ads, analytics, IAP)
- ✅ Optimizar assets y performance
- ✅ Preparar materiales de marketing
- ✅ Configurar monitoring post-launch
- ✅ Tener plan de rollback preparado
2. Store Optimization
- Usar keywords relevantes en título y descripción
- Crear screenshots atractivos con texto overlay
- Optimizar icono para destacar en tienda
- Localizar contenido para mercados principales
- Monitorear rankings y ajustar ASO
3. Launch Strategy
- Soft launch en mercados pequeños primero
- Rollout gradual para detectar problemas
- Monitoring activo los primeros días
- Responder rápidamente a reviews
- Tener comunicación clara con usuarios
4. Post-Launch
- Monitorear métricas clave diariamente
- Planificar updates regulares
- Mantener engagement con comunidad
- Analizar feedback de usuarios
- Iterar basado en datos
Esta guía completa te proporciona todo lo necesario para publicar exitosamente tu juego en las principales tiendas móviles y mantenerlo optimizado post-lanzamiento.