⚔️ P21: Duelo Jedi (Multiplayer em Tempo Real com Firebase Realtime Database)
Bem-vindo ao projeto ápice da Fase 10: Robustez Técnica & Tempo Real! ⚔️
Nesta missão de engenharia avançada de redes e conexões móveis, você aprenderá a estabelecer comunicação bidirecional ultrarrápida de dados na nuvem para sincronizar o estado lógico de um jogo de combate entre dois smartphones físicos rodando o aplicativo simultaneamente. Utilizaremos o poder de sincronização reativa NoSQL do Firebase Realtime Database (ou canais abertos de Websockets) para disparar ataques e curas em um duelo de sabres de luz de Star Wars, alterando o HP do adversário no outro lado do mundo em frações de milissegundos.
✅ Pré-requisitos
Antes de começar, certifique-se de já ter estudado:
- 🎓 Trilha Essencial completa: Projetos P01-P10 (Cap 01-22)
- 🏗️ Projeto anterior: P20: Simulador de Batalha
🎯 Objetivo do Projeto
Estruturar a comunicação reativa bidirecional em tempo real contendo:
- Modelo de Estados em Rede: Mapear estruturas de dados mutáveis para sincronização de atributos lógicos da sala de batalha.
- Conexão Firebase Realtime DB: Injetar referências ativas de escuta e escrita na raiz de banco de dados NoSQL de chave-valor.
- Atualização em Background: Escutar modificações de rede de forma assíncrona com listeners de eventos (
ValueEventListener). - Feedback Visual Imediato: Exibir barras de HP dinâmicas e logs de ações na interface reativa do Compose.
graph TD
A[Jogador 1 clica em Atacar ⚔️] --> B[Dispara gravação local na raiz /duels/sala_1/opponentHp]
B --> C[Firebase Database recebe valor na Nuvem instantaneamente]
C -->|Notificação de Rede| D[Listener do Jogador 2 escuta alteração na raiz /duels/sala_1]
D --> E[Extrai dados da sala e atualiza DuelState do Compose local]
E --> F[Interface do Jogador 2 renderiza HP caindo em frações de milissegundos]
📖 Dicionário do Projeto
- Tempo Real (Realtime): Padrão de comunicação de rede caracterizado por latência extremamente baixa, no qual a entrega e processamento de informações ocorrem em frações de milissegundos.
- Firebase Realtime Database: O banco de dados NoSQL de chave-valor hospedado em nuvem desenvolvido pelo Google. Ele sincroniza dados automaticamente entre todos os clientes conectados em frações de segundo sem necessidade de APIs intermediárias.
- Websockets: O protocolo de rede de comunicação bidirecional completa (Full-Duplex) estabelecido em uma única conexão TCP de longa duração.
- ValueEventListener: Classe escutadora nativa do Firebase usada para capturar e ler qualquer alteração de dados ocorrida na referência do banco de dados na nuvem de forma invisível.
🛠️ Passo 1: Configurando o Firebase no Projeto (Gradle)
- Abra o arquivo
build.gradle (Module :app)e inclua a biblioteca oficial do Realtime Database:dependencies { // Firebase Realtime Database SDK implementation "com.google.firebase:firebase-database-ktx:20.3.0" ... }Clique em Sync Now.
🛠️ Passo 2: Codificando a Escuta e Escrita reativa na Nuvem
No seu arquivo MainActivity.kt, declare a referência da sala de batalha do Firebase e mapeie a gravação de ataques ao clicar no botão:
import com.google.firebase.database.FirebaseDatabase
// 1. Declarar a referência do banco
val databaseRef = FirebaseDatabase.getInstance().getReference("duels/sala_01")
// 2. Gravar alteração de HP na Nuvem ao atacar
databaseRef.child("opponentHp").setValue(newOpponentHp)
databaseRef.child("logMessage").setValue("Jogador 1 desferiu ataque de sabre de luz!")
// 3. Ouvir alterações em tempo real vinda da Nuvem
databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val duelState = snapshot.getValue(DuelState::class.java)
if (duelState != null) {
// Atualiza o estado Compose local da UI
state = duelState
}
}
override fun onCancelled(error: DatabaseError) {}
})
🧠 Passo 3: Garantindo a Experiência Reativa Multiplayer
Para fins de teste sem configurações de servidores complexas, nosso gabarito contém uma função inteligente simulateOpponentAction que atua como o motor de Inteligência Artificial Sith em threads paralelas, simulando a resposta ativa de rede e permitindo que o aluno consolide a lógica antes de implantar chaves de produção físicas.
🏆 Desafios para você (Upgrade!)
Se você terminou de programar a funcionalidade base e tudo está funcionando, experimente estes upgrades:
- Histórico de Golpes: adicione uma
LazyColumnexibindo o log de cada turno da batalha (quem atacou, dano causado). - Firebase Real: substitua a simulação
delay()por uma instância real doFirebaseDatabase, sincronizando o estado do duelo entre dois dispositivos.
📖 Gabarito Oficial de Código (Para Conferência)
Use os códigos Kotlin completos abaixo para validar sua implementação.
📄 Código de Lógica e Tela Completa (MainActivity.kt)
Disponível em: app/src/main/java/br/com/curso/duelojedi/MainActivity.kt
package br.com.curso.duelojedi
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JediTheme {
JediDuelScreen()
}
}
}
}
@Composable
fun JediTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFF00E676), // Sabre Verde
background = Color(0xFF06090C),
surface = Color(0xFF121820)
),
content = content
)
}
// 1. MODELO DE ESTADO DA ARENA MULTIPLAYER
data class DuelState(
val playerHp: Int = 100,
val opponentHp: Int = 100,
val isMyTurn: Boolean = true,
val logMessage: String = "Que a Força esteja com você. Duelo iniciado!"
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JediDuelScreen() {
var state by remember { mutableStateOf(DuelState()) }
val coroutineScope = rememberCoroutineScope()
// 2. SIMULAÇÃO DE ESCUTA DE REDE BIDIRECIONAL (WEBSOCKET / FIREBASE REALTIME DB)
fun simulateOpponentAction() {
coroutineScope.launch {
delay(2000) // Simula 2 segundos de raciocínio do oponente em outro celular
val damage = (10..25).random()
val newPlayerHp = if (state.playerHp - damage < 0) 0 else state.playerHp - damage
state = state.copy(
playerHp = newPlayerHp,
isMyTurn = true,
logMessage = "Oponente golpeou você! Dano de -$damage HP!"
)
}
}
Scaffold(
topBar = {
SmallTopAppBar(
title = { Text("JEDI REALTIME DUEL v1.0", fontWeight = FontWeight.Black, letterSpacing = 2.sp) },
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = Color(0xFF121820),
titleContentColor = Color(0xFF00E676)
)
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.background(Color(0xFF06090C))
.padding(24.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Status do HP do Oponente
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("OPONENTE (SITH)", color = Color.Red, fontWeight = FontWeight.Bold, fontSize = 12.sp)
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = state.opponentHp / 100f,
color = Color.Red,
trackColor = Color.Red.copy(alpha = 0.2f),
modifier = Modifier.fillMaxWidth().height(16.dp).background(Color.Black, shape = RoundedCornerShape(8.dp))
)
Text("${state.opponentHp} / 100 HP", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp)
}
// Log de Ações em tempo real
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF121820)),
shape = RoundedCornerShape(12.dp)
) {
Box(modifier = Modifier.padding(20.dp), contentAlignment = Alignment.Center) {
Text(
text = state.logMessage,
color = Color.White,
fontWeight = FontWeight.Medium,
fontSize = 16.sp
)
}
}
// Status do HP do Jogador
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("VOCÊ (JEDI)", color = Color(0xFF00E676), fontWeight = FontWeight.Bold, fontSize = 12.sp)
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = state.playerHp / 100f,
color = Color(0xFF00E676),
trackColor = Color(0xFF00E676).copy(alpha = 0.2f),
modifier = Modifier.fillMaxWidth().height(16.dp).background(Color.Black, shape = RoundedCornerShape(8.dp))
)
Text("${state.playerHp} / 100 HP", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp)
}
Spacer(modifier = Modifier.height(32.dp))
// Ações de Turno
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(
onClick = {
if (state.isMyTurn && state.playerHp > 0 && state.opponentHp > 0) {
val damage = (15..30).random()
val newOpponentHp = if (state.opponentHp - damage < 0) 0 else state.opponentHp - damage
state = state.copy(
opponentHp = newOpponentHp,
isMyTurn = false,
logMessage = "Você atacou com o Sabre de Luz! Dano de -$damage no Sith!"
)
simulateOpponentAction()
}
},
enabled = state.isMyTurn && state.playerHp > 0 && state.opponentHp > 0,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF00E676))
) {
Text("ATACAR ⚔️", color = Color.Black, fontWeight = FontWeight.Bold)
}
Button(
onClick = {
if (state.isMyTurn && state.playerHp > 0 && state.opponentHp > 0) {
val cure = (10..20).random()
val newPlayerHp = if (state.playerHp + cure > 100) 100 else state.playerHp + cure
state = state.copy(
playerHp = newPlayerHp,
isMyTurn = false,
logMessage = "Você usou a Força para se curar! Recobrou +$cure HP!"
)
simulateOpponentAction()
}
},
enabled = state.isMyTurn && state.playerHp > 0 && state.opponentHp > 0,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF121820))
) {
Text("CURAR-SE ✨", color = Color.White)
}
}
}
}
}