📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.

⚔️ P21: Duelo Jedi (Física 2D, Detecção de Colisão & Game Loop)

Bem-vindo à décima etapa da Fase 7: Arquitetura Avançada & Computação Gráfica em Tempo Real! ⚔️

Neste projeto prático, você aprenderá como implementar uma simulação física bidimensional (2D) completa com detecção geométrica de colisão (Círculo vs Retângulo), controle de vetores de velocidade e reflexão angular, alimentados por um Game Loop a 60 FPS no Jetpack Compose com gestos de toque em tempo real!

Com a temática épica do universo Star Wars (Jedi vs Sith), você controlará um sabre de luz verde neon deslizando o dedo pela tela para rebater orbes de plasma quântico contra a IA do Lorde Sith, aplicando as melhores práticas de otimização gráfica para evitar quedas de quadros (Frame Drops) e consumo excessivo de bateria (Battery Drain)!

Fluxo Completo de Telas do Duelo Jedi


📱 Galeria de Telas da Aplicação

1. Arena Pronta 2. Duelo em Ação (Física 2D) 3. Vitória da Força
Tela 1 - Arena Pronta Tela 2 - Combate em Ação Tela 3 - Vitória
Posicionamento inicial das plataformas e orbe. Game loop a 60 FPS, rebotes angulares e rastro. Finalização do duelo e contagem de defesas.

✅ Pré-requisitos e Continuidade

Antes de iniciar este laboratório, certifique-se de ter compreendido:


🎯 Objetivos de Aprendizagem

Ao concluir este projeto autoguiado, você será capaz de:

  1. Implementar Detecção de Colisão em Tempo Real: Criar algoritmos geométricos $O(1)$ de proximidade entre círculos e caixas delimitadoras (Bounding Boxes).
  2. Construir um Game Loop a 60 FPS no Compose: Utilizar corrotinas com LaunchedEffect e delay(16) para atualizar a física sem bloquear a Main Thread.
  3. Controlar Gestos de Arraste com pointerInput: Mapear as coordenadas táteis do dedo do jogador diretamente na posição do sabre de luz.
  4. Calcular Ângulos de Reflexão Dinâmicos: Alterar a trajetória horizontal do projétil de acordo com o ponto de impacto na plataforma.
  5. Otimizar Renderização no Canvas: Desenhar sabres de luz com efeitos de glow e halo de plasma com aceleração de hardware nativa.

🏗️ Ciclo do Game Loop e Física 2D

graph TD
    A[LaunchedEffect jogoAtivo executa a cada 16ms ~60 FPS] --> B[nextX = ballX + ballVx / nextY = ballY + ballVy]
    B --> C[IA do Sith interpola posição sithPaddleX em direção à orbe]
    C --> D{Colisão com as paredes laterais?}
    D -- Sim --> E[Inverte velocidade horizontal: ballVx = -ballVx]
    D -- Não --> F{Colisão com o Sabre Jedi ou Sith?}
    F -- Sim --> G[Inverte velocidade vertical ballVy e aplica aceleração angular]
    F -- Não --> H{Ultrapassou o limite superior ou inferior?}
    H -- Sim --> I[Deduz 25 HP do combatente e reposiciona orbe no centro]
    H -- Não --> J[Canvas atualiza coordenadas e renderiza novo frame]

🛡️ Boas Práticas de Otimização e Eficiência Energética

[!IMPORTANT] Prevenção de Esgotamento de Bateria e Gargalos de Memória: Um Game Loop sem controle de ciclo de vida pode continuar rodando indefinidamente em segundo plano quando o usuário bloqueia o celular, consumindo CPU desnecessariamente.

Nossa Estratégia de Engenharia:

  1. Cancelamento Atômico do Loop: O LaunchedEffect(jogoAtivo) encerra a corrotina imediatamente quando a partida é pausada ou finalizada.
  2. Zero Alocações no Laço Físico: O PhysicsEngine utiliza tipos primitivos (Float) e matemática escalar pura, evitando a criação de objetos no Heap e pausas do Garbage Collector.
  3. Normalização de Coordenadas (0.0 a 1.0): A física é calculada em escala percentual independente da resolução do dispositivo, adaptando-se perfeitamente a qualquer densidade de tela (DPI).
  4. Higiene de Versionamento: Arquivo .gitignore configurado para isolar caches locais.

📖 Dicionário Técnico do Projeto

Termo / Componente O que é e para que serve?
Game Loop Laço contínuo de repetição que processa entradas, atualiza a física e renderiza o próximo quadro visual.
PhysicsEngine Módulo de lógica desacoplada responsável por verificar colisões e limites da arena.
Circle-to-Rect Collision Algoritmo que encontra o ponto mais próximo do retângulo em relação ao centro do círculo e valida a distância pelo raio.
pointerInput Modificador do Jetpack Compose que escuta eventos de toque e arraste de ponteiro na tela.
coerceIn Função do Kotlin que restringe um valor numérico entre limites mínimos e máximos definidos.

🛠️ Passo a Passo de Implementação

🚀 Passo 1: Criando o Novo Projeto no Android Studio

  1. Abra o Android Studio e clique em New Project (ou File > New > New Project).
  2. Selecione o template Empty Activity (com o ícone do Jetpack Compose 🌌).
  3. Preencha as configurações do projeto:
    • Name: Duelo Jedi (ou android_p21_duelo_physics)
    • Package name: br.com.curso.duelojedi
    • Save location: Pasta do seu projeto no repositório
    • Language: Kotlin
    • Minimum SDK: API 24 ("Nougat"; Android 7.0) ou superior
    • Build configuration language: Groovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)
  4. Clique em Finish e aguarde o Gradle sincronizar.

📦 Passo 2: Configurando Dependências no Gradle (build.gradle)

Abra app > build.gradle:

dependencies {
    implementation platform('androidx.compose:compose-bom:2024.02.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.activity:activity-compose:1.8.2'
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}

📄 Passo 3: Manifesto do Aplicativo (AndroidManifest.xml)

Abra app > src > main > AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:label="Duelo Jedi"
        android:supportsRtl="true"
        android:theme="@style/Theme.CartaoTreinador">

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

</manifest>

📐 Passo 4: Implementando a Engine de Colisão Geométrica (PhysicsEngine)

Abra app > src > main > java > br > com > curso > duelojedi > MainActivity.kt:

object PhysicsEngine {
    fun verificarColisaoRetangulo(
        circleX: Float,
        circleY: Float,
        radius: Float,
        rectX: Float,
        rectY: Float,
        rectWidth: Float,
        rectHeight: Float
    ): Boolean {
        val closestX = circleX.coerceIn(rectX, rectX + rectWidth)
        val closestY = circleY.coerceIn(rectY, rectY + rectHeight)
        val distanceX = circleX - closestX
        val distanceY = circleY - closestY
        val distanceSquared = (distanceX * distanceX) + (distanceY * distanceY)
        return distanceSquared <= (radius * radius)
    }
}

⏱️ Passo 5: O Game Loop a 60 FPS com Gestos Táteis

@Composable
fun DueloJediScreen() {
    var jogoAtivo by remember { mutableStateOf(false) }
    var jediPaddleX by remember { mutableFloatStateOf(0.5f) }
    var sithPaddleX by remember { mutableFloatStateOf(0.5f) }
    var ballX by remember { mutableFloatStateOf(0.5f) }
    var ballY by remember { mutableFloatStateOf(0.5f) }
    var ballVx by remember { mutableFloatStateOf(0.012f) }
    var ballVy by remember { mutableFloatStateOf(0.016f) }

    LaunchedEffect(jogoAtivo) {
        while (jogoAtivo) {
            delay(16) // ~60 FPS

            var nextX = ballX + ballVx
            var nextY = ballY + ballVy

            // IA do Sith rastreando a orbe suavemente
            sithPaddleX += (ballX - 0.14f - sithPaddleX) * 0.08f

            // Rebote lateral
            if (nextX <= 0.035f || nextX >= 0.965f) ballVx = -ballVx

            // Detecção com o Sabre do Jedi
            if (PhysicsEngine.verificarColisaoRetangulo(nextX, nextY, 0.035f, jediPaddleX, 0.88f, 0.28f, 0.025f)) {
                ballVy = -kotlin.math.abs(ballVy) * 1.04f
            }

            ballX = nextX
            ballY = nextY
        }
    }
}

🌌 Passo 6: Renderização dos Sabres de Luz no Canvas

Box(
    modifier = Modifier
        .fillMaxWidth()
        .weight(1f)
        .pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                change.consume()
                jediPaddleX = (jediPaddleX + (dragAmount.x / size.width)).coerceIn(0f, 0.72f)
            }
        }
) {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val w = size.width
        val h = size.height

        // 1. Sabre Sith (Topo)
        drawRoundRect(
            color = Color(0xFFFF1744),
            topLeft = Offset(sithPaddleX * w, 0.08f * h),
            size = Size(0.28f * w, 0.025f * h),
            cornerRadius = CornerRadius(8f, 8f)
        )

        // 2. Sabre Jedi (Base)
        drawRoundRect(
            color = Color(0xFF00E676),
            topLeft = Offset(jediPaddleX * w, 0.88f * h),
            size = Size(0.28f * w, 0.025f * h),
            cornerRadius = CornerRadius(8f, 8f)
        )

        // 3. Orbe de Plasma
        drawCircle(
            color = Color(0xFF00E5FF),
            radius = 0.035f * w,
            center = Offset(ballX * w, ballY * h)
        )
    }
}

🔍 Guia de Diagnóstico & Resolução de Problemas (Troubleshooting)

Sintoma Observado Causa Provável Como Resolver
A orbe atravessa o sabre em alta velocidade (Tunneling) O incremento de ballVy é maior que a espessura do sabre entre frames. Aumente a taxa de amostragem ou aplique verificação por segmento contínuo no PhysicsEngine.
O arraste do sabre trava a rolagem da tela O evento de gesto não consumiu o evento de toque. Chame change.consume() dentro do lambda detectDragGestures.
O sabre ultrapassa a borda direita da tela O limite superior do arraste permitiu ir até 1.0f em vez de 1.0f - paddleWidthPercent. Use .coerceIn(0f, 1f - paddleWidthPercent).
O jogo continua rodando mesmo após sair da tela O Game Loop não foi cancelado quando o Composable saiu da composição. O LaunchedEffect(jogoAtivo) cancela o loop automaticamente ao mudar de estado.

🏆 Desafios e Upgrades (Mão na Massa!)

  1. Múltiplas Orbes de Força: Modifique o estado para suportar um array com 2 ou 3 orbes simultâneas na arena.
  2. 🎚️ Modo Difícil com IA Rápida: Aumente a velocidade de rastreamento da IA do Sith de 0.08f para 0.15f.
  3. 🔊 Efeitos Sonoros de Sabre: Combine com os ensinamentos do projeto P17 (SoundPool) para tocar o som de colisão de sabres a cada rebote!

📖 Gabarito Oficial de Código (Para Conferência)

Disponível em: app/src/main/java/br/com/curso/duelojedi/MainActivity.kt.


🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd proj_aplicacoes_full_stack/projetos/android_p21_duelo_physics

2. Execute a aplicação e os testes

./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta proj_aplicacoes_full_stack/projetos/android_p21_duelo_physics.