📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
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)!
| 1. Arena Pronta | 2. Duelo em Ação (Física 2D) | 3. Vitória da Força |
|---|---|---|
| Posicionamento inicial das plataformas e orbe. | Game loop a 60 FPS, rebotes angulares e rastro. | Finalização do duelo e contagem de defesas. |
Antes de iniciar este laboratório, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
LaunchedEffect e delay(16) para atualizar a física sem bloquear a Main Thread.pointerInput: Mapear as coordenadas táteis do dedo do jogador diretamente na posição do sabre de luz.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]
[!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:
- Cancelamento Atômico do Loop: O
LaunchedEffect(jogoAtivo)encerra a corrotina imediatamente quando a partida é pausada ou finalizada.- Zero Alocações no Laço Físico: O
PhysicsEngineutiliza tipos primitivos (Float) e matemática escalar pura, evitando a criação de objetos no Heap e pausas do Garbage Collector.- 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).
- Higiene de Versionamento: Arquivo
.gitignoreconfigurado para isolar caches locais.
| 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. |
File > New > New Project).Duelo Jedi (ou android_p21_duelo_physics)br.com.curso.duelojediKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)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'
}
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>
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)
}
}
@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
}
}
}
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)
)
}
}
| 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. |
0.08f para 0.15f.SoundPool) para tocar o som de colisão de sabres a cada rebote!Disponível em: app/src/main/java/br/com/curso/duelojedi/MainActivity.kt.
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
./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.