Capítulo 3: Tu Primer Juego Completo - Pong
Capítulo 3: Tu Primer Juego Completo - Pong
¡Es hora de crear tu primer juego completo! Pong es el juego perfecto para empezar: simple en concepto pero cubre todos los aspectos fundamentales del desarrollo de videojuegos. Al terminar este capítulo, tendrás un juego completamente funcional con menú, gameplay, puntuación, sonidos y efectos visuales.
¿Qué Vamos a Construir?
Nuestro Pong tendrá:
- 🎮 Dos paletas controlables (1 jugador vs CPU o 2 jugadores)
- ⚽ Pelota con física realista
- 🏆 Sistema de puntuación
- 🎵 Efectos de sonido y música
- 📱 Menú principal con opciones
- ✨ Efectos visuales y partículas
- 🎯 Dificultad ajustable
- 💾 Guardado de récords
Configuración del Proyecto
Crear el Proyecto
-
Abre Godot y crea un nuevo proyecto
- Nombre: “Pong Classic”
- Renderer: Compatibility (para máxima compatibilidad)
- Versión Control: Git (si usas control de versiones)
-
Configura la ventana del juego
- Project → Project Settings → Display → Window
- Width: 1280
- Height: 720
- Stretch Mode: canvas_items
- Aspect: keep
-
Configura la física 2D
- Project → Project Settings → Physics → 2D
- Default Gravity: 0 (sin gravedad en Pong)
Estructura de Carpetas
Crea esta estructura en el FileSystem:
res://
├── scenes/
│ ├── game/
│ ├── ui/
│ └── components/
├── scripts/
├── assets/
│ ├── sprites/
│ ├── sounds/
│ └── fonts/
└── autoload/
Escena Principal del Juego
Crear la Escena Base
- Crea una nueva escena 2D
- Renombra el nodo raíz a “Game”
- Guárdala como
res://scenes/game/game.tscn
Estructura de la Escena
Game (Node2D)
├── Background (ColorRect)
├── Court (Node2D)
│ ├── TopWall (StaticBody2D)
│ │ └── CollisionShape2D
│ ├── BottomWall (StaticBody2D)
│ │ └── CollisionShape2D
│ └── CenterLine (Line2D)
├── PlayerPaddle (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── CPUPaddle (CharacterBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── Ball (RigidBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
├── UI (CanvasLayer)
│ ├── PlayerScore (Label)
│ └── CPUScore (Label)
└── Sounds (Node)
├── HitSound (AudioStreamPlayer)
├── ScoreSound (AudioStreamPlayer)
└── WallSound (AudioStreamPlayer)
Configurar el Background
- Selecciona Background (ColorRect)
- En el Inspector:
- Anchor: Full Rect
- Color: #1a1a2e (azul oscuro)
Crear las Paredes
Pared Superior
- Selecciona TopWall (StaticBody2D)
- Position: (640, -10)
- Añade CollisionShape2D como hijo
- Shape: New RectangleShape2D
- Size: (1280, 20)
Pared Inferior
- Selecciona BottomWall
- Position: (640, 730)
- CollisionShape2D con RectangleShape2D
- Size: (1280, 20)
Línea Central
- Selecciona CenterLine (Line2D)
- Points: Añade dos puntos: (640, 0) y (640, 720)
- Width: 4
- Default Color: #ffffff50 (blanco semi-transparente)
Creando las Paletas
Paleta del Jugador
- Selecciona PlayerPaddle
- Position: (50, 360)
- Añade el script:
# player_paddle.gd
extends CharacterBody2D
const SPEED = 400.0
const PADDLE_HEIGHT = 100
@export var player_id: int = 1 # 1 para jugador 1, 2 para jugador 2
func _ready():
# Crear el sprite de la paleta
var sprite = $Sprite2D
if sprite.texture == null:
# Crear textura blanca simple
var image = Image.create(20, PADDLE_HEIGHT, false, Image.FORMAT_RGB8)
image.fill(Color.WHITE)
sprite.texture = ImageTexture.create_from_image(image)
# Configurar colisión
var collision = $CollisionShape2D
var shape = RectangleShape2D.new()
shape.size = Vector2(20, PADDLE_HEIGHT)
collision.shape = shape
func _physics_process(delta):
var direction = 0.0
# Controles para jugador 1
if player_id == 1:
if Input.is_action_pressed("player1_up"):
direction = -1.0
elif Input.is_action_pressed("player1_down"):
direction = 1.0
# Controles para jugador 2 (modo 2 jugadores)
elif player_id == 2:
if Input.is_action_pressed("player2_up"):
direction = -1.0
elif Input.is_action_pressed("player2_down"):
direction = 1.0
velocity.y = direction * SPEED
move_and_slide()
# Limitar movimiento dentro de la pantalla
global_position.y = clamp(global_position.y, 50, 670)
func get_paddle_center() -> Vector2:
return global_position
func get_paddle_height() -> float:
return PADDLE_HEIGHT
Paleta de la CPU
# cpu_paddle.gd
extends CharacterBody2D
const SPEED = 350.0
const PADDLE_HEIGHT = 100
@export var difficulty: float = 0.8 # 0.0 = fácil, 1.0 = imposible
@export var reaction_distance: float = 400.0
var ball: RigidBody2D
var target_y: float
func _ready():
# Crear sprite
var sprite = $Sprite2D
if sprite.texture == null:
var image = Image.create(20, PADDLE_HEIGHT, false, Image.FORMAT_RGB8)
image.fill(Color.WHITE)
sprite.texture = ImageTexture.create_from_image(image)
# Configurar colisión
var collision = $CollisionShape2D
var shape = RectangleShape2D.new()
shape.size = Vector2(20, PADDLE_HEIGHT)
collision.shape = shape
# Encontrar la pelota
await get_tree().process_frame
ball = get_node_or_null("../Ball")
func _physics_process(delta):
if not ball:
return
# IA simple: seguir la pelota cuando está cerca
var ball_pos = ball.global_position
var distance_to_ball = abs(ball_pos.x - global_position.x)
# Solo reaccionar cuando la pelota está cerca y viniendo hacia nosotros
if distance_to_ball < reaction_distance and ball.linear_velocity.x > 0:
# Predecir donde estará la pelota
var time_to_reach = distance_to_ball / abs(ball.linear_velocity.x)
var predicted_y = ball_pos.y + (ball.linear_velocity.y * time_to_reach * 0.5)
# Añadir algo de imperfección basada en dificultad
var error = randf_range(-50, 50) * (1.0 - difficulty)
target_y = predicted_y + error
else:
# Volver al centro cuando la pelota está lejos
target_y = 360
# Mover hacia el objetivo
var direction = sign(target_y - global_position.y)
velocity.y = direction * SPEED * difficulty
move_and_slide()
# Limitar movimiento
global_position.y = clamp(global_position.y, 50, 670)
Creando la Pelota
La pelota es el corazón del juego. Necesita física realista y comportamiento predecible.
# ball.gd
extends RigidBody2D
@export var initial_speed: float = 400.0
@export var max_speed: float = 800.0
@export var speed_increment: float = 20.0
signal goal_scored(player_scored: bool)
var current_speed: float
var hits: int = 0
func _ready():
# Configurar física
gravity_scale = 0
linear_damp = 0
angular_damp = 0
# Configurar bounce
var physics_material = PhysicsMaterial.new()
physics_material.bounce = 1.0
physics_material.friction = 0.0
physics_material_override = physics_material
# Crear sprite
var sprite = $Sprite2D
if sprite.texture == null:
var image = Image.create(20, 20, false, Image.FORMAT_RGB8)
image.fill(Color.WHITE)
# Hacer circular
for x in range(20):
for y in range(20):
var distance = Vector2(x - 10, y - 10).length()
if distance > 10:
image.set_pixel(x, y, Color.TRANSPARENT)
sprite.texture = ImageTexture.create_from_image(image)
# Configurar colisión
var collision = $CollisionShape2D
var shape = CircleShape2D.new()
shape.radius = 10
collision.shape = shape
# Conectar señales
body_entered.connect(_on_body_entered)
# Iniciar juego
reset_ball()
func reset_ball():
position = Vector2(640, 360)
hits = 0
current_speed = initial_speed
# Esperar un momento antes de lanzar
linear_velocity = Vector2.ZERO
await get_tree().create_timer(1.0).timeout
serve_ball()
func serve_ball():
# Dirección aleatoria
var direction = Vector2()
direction.x = 1.0 if randf() > 0.5 else -1.0
direction.y = randf_range(-0.5, 0.5)
direction = direction.normalized()
linear_velocity = direction * current_speed
func _on_body_entered(body):
if body.name == "PlayerPaddle" or body.name == "CPUPaddle":
# Sonido de golpe
get_node("../Sounds/HitSound").play()
# Incrementar velocidad
hits += 1
current_speed = min(current_speed + speed_increment, max_speed)
# Calcular nuevo ángulo basado en donde golpea la paleta
var paddle_height = body.get_paddle_height() if body.has_method("get_paddle_height") else 100
var paddle_center = body.get_paddle_center() if body.has_method("get_paddle_center") else body.global_position
var hit_position = global_position.y - paddle_center.y
var normalized_position = hit_position / (paddle_height / 2.0)
normalized_position = clamp(normalized_position, -1.0, 1.0)
# Nuevo vector de velocidad
var new_direction = Vector2()
new_direction.x = -sign(linear_velocity.x)
new_direction.y = normalized_position * 0.75
new_direction = new_direction.normalized()
linear_velocity = new_direction * current_speed
# Efecto visual
create_hit_effect()
elif body.name == "TopWall" or body.name == "BottomWall":
# Sonido de pared
get_node("../Sounds/WallSound").play()
linear_velocity.y = -linear_velocity.y
func create_hit_effect():
# Crear partículas o efecto visual
var particles = CPUParticles2D.new()
particles.emitting = true
particles.amount = 10
particles.lifetime = 0.3
particles.one_shot = true
particles.speed_scale = 2
particles.direction = Vector2(-sign(linear_velocity.x), 0)
particles.initial_velocity_min = 50
particles.initial_velocity_max = 150
particles.scale_amount_min = 0.5
particles.scale_amount_max = 1.0
particles.color = Color.YELLOW
get_parent().add_child(particles)
particles.global_position = global_position
# Auto-destruir después de emitir
await particles.finished
particles.queue_free()
func _integrate_forces(state):
# Detectar goles
if global_position.x < 0:
goal_scored.emit(false) # CPU anotó
reset_ball()
elif global_position.x > 1280:
goal_scored.emit(true) # Jugador anotó
reset_ball()
Sistema de Puntuación
# game_manager.gd (adjuntar al nodo Game)
extends Node2D
@export var max_score: int = 5
@export var game_mode: String = "1_player" # "1_player" o "2_players"
var player_score: int = 0
var cpu_score: int = 0
@onready var player_score_label = $UI/PlayerScore
@onready var cpu_score_label = $UI/CPUScore
@onready var ball = $Ball
@onready var player_paddle = $PlayerPaddle
@onready var cpu_paddle = $CPUPaddle
signal game_over(winner: String)
func _ready():
# Configurar Input Map
setup_input_actions()
# Conectar señales
ball.goal_scored.connect(_on_goal_scored)
# Inicializar UI
update_score_display()
# Configurar modo de juego
if game_mode == "2_players":
cpu_paddle.player_id = 2 # Convertir CPU en jugador 2
func setup_input_actions():
# Jugador 1
if not InputMap.has_action("player1_up"):
InputMap.add_action("player1_up")
var event = InputEventKey.new()
event.keycode = KEY_W
InputMap.action_add_event("player1_up", event)
if not InputMap.has_action("player1_down"):
InputMap.add_action("player1_down")
var event = InputEventKey.new()
event.keycode = KEY_S
InputMap.action_add_event("player1_down", event)
# Jugador 2
if not InputMap.has_action("player2_up"):
InputMap.add_action("player2_up")
var event = InputEventKey.new()
event.keycode = KEY_UP
InputMap.action_add_event("player2_up", event)
if not InputMap.has_action("player2_down"):
InputMap.add_action("player2_down")
var event = InputEventKey.new()
event.keycode = KEY_DOWN
InputMap.action_add_event("player2_down", event)
func _on_goal_scored(player_scored: bool):
if player_scored:
player_score += 1
$Sounds/ScoreSound.play()
create_goal_text("¡GOL!", Vector2(640, 200), Color.GREEN)
else:
cpu_score += 1
$Sounds/ScoreSound.play()
create_goal_text("¡GOL!", Vector2(640, 200), Color.RED)
update_score_display()
# Verificar victoria
if player_score >= max_score:
game_over.emit("Player")
show_game_over("¡GANASTE!")
elif cpu_score >= max_score:
game_over.emit("CPU")
show_game_over("GAME OVER")
func update_score_display():
player_score_label.text = str(player_score)
cpu_score_label.text = str(cpu_score)
func create_goal_text(text: String, pos: Vector2, color: Color):
var label = Label.new()
label.text = text
label.add_theme_font_size_override("font_size", 72)
label.modulate = color
label.position = pos
$UI.add_child(label)
# Animación
var tween = create_tween()
tween.set_trans(Tween.TRANS_ELASTIC)
tween.set_ease(Tween.EASE_OUT)
tween.tween_property(label, "scale", Vector2(1.5, 1.5), 0.5)
tween.parallel().tween_property(label, "modulate:a", 0.0, 1.0)
tween.tween_callback(label.queue_free)
func show_game_over(message: String):
# Pausar juego
get_tree().paused = true
# Crear overlay
var overlay = ColorRect.new()
overlay.color = Color(0, 0, 0, 0.7)
overlay.anchor_right = 1.0
overlay.anchor_bottom = 1.0
var label = Label.new()
label.text = message
label.add_theme_font_size_override("font_size", 96)
label.anchor_left = 0.5
label.anchor_top = 0.5
label.anchor_right = 0.5
label.anchor_bottom = 0.5
$UI.add_child(overlay)
overlay.add_child(label)
# Botones
var retry_button = Button.new()
retry_button.text = "Jugar de Nuevo"
retry_button.position = Vector2(540, 400)
retry_button.pressed.connect(_on_retry_pressed)
var menu_button = Button.new()
menu_button.text = "Menú Principal"
menu_button.position = Vector2(540, 450)
menu_button.pressed.connect(_on_menu_pressed)
overlay.add_child(retry_button)
overlay.add_child(menu_button)
func _on_retry_pressed():
get_tree().paused = false
get_tree().reload_current_scene()
func _on_menu_pressed():
get_tree().paused = false
get_tree().change_scene_to_file("res://scenes/ui/main_menu.tscn")
Configurando la UI
Labels de Puntuación
-
PlayerScore (Label)
- Position: (320, 50)
- Font Size: 72
- Text: “0”
- Align: Center
-
CPUScore (Label)
- Position: (960, 50)
- Font Size: 72
- Text: “0”
- Align: Center
Menú Principal
Crea una nueva escena para el menú:
# main_menu.gd
extends Control
@onready var animation_player = $AnimationPlayer
func _ready():
# Animar entrada
if animation_player:
animation_player.play("fade_in")
# Configurar botones
$VBoxContainer/OnePlayerButton.pressed.connect(_on_one_player_pressed)
$VBoxContainer/TwoPlayersButton.pressed.connect(_on_two_players_pressed)
$VBoxContainer/OptionsButton.pressed.connect(_on_options_pressed)
$VBoxContainer/QuitButton.pressed.connect(_on_quit_pressed)
# Música de menú
if not $MenuMusic.playing:
$MenuMusic.play()
func _on_one_player_pressed():
Global.game_mode = "1_player"
transition_to_game()
func _on_two_players_pressed():
Global.game_mode = "2_players"
transition_to_game()
func _on_options_pressed():
# Mostrar menú de opciones
$OptionsMenu.visible = true
func _on_quit_pressed():
get_tree().quit()
func transition_to_game():
if animation_player:
animation_player.play("fade_out")
await animation_player.animation_finished
get_tree().change_scene_to_file("res://scenes/game/game.tscn")
Estructura del Menú
MainMenu (Control)
├── Background (TextureRect)
├── Title (Label) - "PONG"
├── VBoxContainer
│ ├── OnePlayerButton (Button)
│ ├── TwoPlayersButton (Button)
│ ├── OptionsButton (Button)
│ └── QuitButton (Button)
├── OptionsMenu (Panel) [Oculto por defecto]
│ ├── DifficultySlider (HSlider)
│ ├── SoundSlider (HSlider)
│ ├── MusicSlider (HSlider)
│ └── BackButton (Button)
├── MenuMusic (AudioStreamPlayer)
└── AnimationPlayer
Sistema Global (Autoload)
Crea un script global para mantener configuraciones entre escenas:
# global.gd (Autoload)
extends Node
var game_mode: String = "1_player"
var difficulty: float = 0.5
var sound_volume: float = 1.0
var music_volume: float = 0.7
var high_score: int = 0
const SAVE_PATH = "user://pong_save.dat"
func _ready():
load_game()
# Configurar volúmenes
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("Master"),
linear_to_db(sound_volume)
)
func save_game():
var save_file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if save_file:
var save_data = {
"high_score": high_score,
"difficulty": difficulty,
"sound_volume": sound_volume,
"music_volume": music_volume
}
save_file.store_var(save_data)
save_file.close()
func load_game():
if FileAccess.file_exists(SAVE_PATH):
var save_file = FileAccess.open(SAVE_PATH, FileAccess.READ)
if save_file:
var save_data = save_file.get_var()
high_score = save_data.get("high_score", 0)
difficulty = save_data.get("difficulty", 0.5)
sound_volume = save_data.get("sound_volume", 1.0)
music_volume = save_data.get("music_volume", 0.7)
save_file.close()
func update_high_score(score: int):
if score > high_score:
high_score = score
save_game()
return true
return false
Para registrar el autoload:
- Project → Project Settings → Autoload
- Path:
res://scripts/global.gd - Node Name: “Global”
- Click “Add”
Efectos de Sonido y Música
Configurar Audio Buses
- Audio → Audio Bus Layout
- Crea estos buses:
- Master (default)
- Music
- SFX
Añadir Sonidos
Puedes crear sonidos simples programáticamente:
# sound_generator.gd
extends Node
func create_hit_sound() -> AudioStream:
var sample_rate = 44100
var duration = 0.1
var frequency = 440.0
var audio = AudioStreamWAV.new()
audio.format = AudioStreamWAV.FORMAT_16_BITS
audio.mix_rate = sample_rate
audio.stereo = false
var samples = PackedByteArray()
for i in range(int(sample_rate * duration)):
var sample = sin(2.0 * PI * frequency * i / sample_rate)
# Aplicar envelope
var envelope = 1.0 - (i / float(sample_rate * duration))
sample *= envelope
# Convertir a 16-bit
var value = int(sample * 32767.0)
samples.append(value & 0xFF)
samples.append((value >> 8) & 0xFF)
audio.data = samples
return audio
func create_score_sound() -> AudioStream:
# Sonido ascendente
var sample_rate = 44100
var duration = 0.5
var audio = AudioStreamWAV.new()
audio.format = AudioStreamWAV.FORMAT_16_BITS
audio.mix_rate = sample_rate
audio.stereo = false
var samples = PackedByteArray()
for i in range(int(sample_rate * duration)):
var t = i / float(sample_rate)
var frequency = 220.0 * (1.0 + t * 2.0) # Frecuencia ascendente
var sample = sin(2.0 * PI * frequency * t)
var envelope = 1.0 - (t / duration)
sample *= envelope
var value = int(sample * 32767.0)
samples.append(value & 0xFF)
samples.append((value >> 8) & 0xFF)
audio.data = samples
return audio
Mejoras Visuales
Estela de la Pelota
# ball_trail.gd
extends Line2D
@export var trail_length: int = 20
var trail_points: Array = []
func _ready():
width = 10
default_color = Color(1, 1, 1, 0.5)
gradient = Gradient.new()
gradient.add_point(0.0, Color(1, 1, 1, 0))
gradient.add_point(1.0, Color(1, 1, 1, 0.5))
func _process(delta):
var parent = get_parent()
if parent:
var current_pos = parent.global_position
trail_points.append(current_pos)
if trail_points.size() > trail_length:
trail_points.pop_front()
clear_points()
for point in trail_points:
add_point(to_local(point))
Efectos de Partículas
# particle_manager.gd
extends Node
func create_goal_particles(position: Vector2, color: Color):
var particles = CPUParticles2D.new()
particles.emitting = true
particles.amount = 50
particles.lifetime = 1.0
particles.one_shot = true
particles.emission_shape = CPUParticles2D.EMISSION_SHAPE_SPHERE
particles.spread = 45.0
particles.initial_velocity_min = 100
particles.initial_velocity_max = 300
particles.angular_velocity_min = -180
particles.angular_velocity_max = 180
particles.scale_amount_min = 0.5
particles.scale_amount_max = 1.5
particles.color = color
get_tree().current_scene.add_child(particles)
particles.global_position = position
await particles.finished
particles.queue_free()
Power-Ups (Característica Extra)
# power_up.gd
extends Area2D
enum PowerUpType {
SPEED_BOOST,
SLOW_MOTION,
MULTI_BALL,
GIANT_PADDLE,
TINY_PADDLE
}
@export var type: PowerUpType = PowerUpType.SPEED_BOOST
@export var duration: float = 5.0
signal collected(type: PowerUpType)
func _ready():
# Configurar visual según tipo
var sprite = $Sprite2D
match type:
PowerUpType.SPEED_BOOST:
modulate = Color.YELLOW
PowerUpType.SLOW_MOTION:
modulate = Color.BLUE
PowerUpType.MULTI_BALL:
modulate = Color.GREEN
PowerUpType.GIANT_PADDLE:
modulate = Color.ORANGE
PowerUpType.TINY_PADDLE:
modulate = Color.PURPLE
# Conectar señales
body_entered.connect(_on_body_entered)
# Auto-destruir después de un tiempo
await get_tree().create_timer(10.0).timeout
queue_free()
func _on_body_entered(body):
if body.name == "Ball":
collected.emit(type)
# Efecto visual
var tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.5, 1.5), 0.2)
tween.parallel().tween_property(self, "modulate:a", 0.0, 0.2)
tween.tween_callback(queue_free)
# Aplicar efecto
apply_power_up(body)
func apply_power_up(ball):
match type:
PowerUpType.SPEED_BOOST:
ball.current_speed *= 1.5
PowerUpType.SLOW_MOTION:
Engine.time_scale = 0.5
await get_tree().create_timer(duration * Engine.time_scale).timeout
Engine.time_scale = 1.0
# ... más efectos
Puliendo el Juego
Menú de Pausa
# pause_menu.gd
extends Control
var paused: bool = false
func _ready():
visible = false
process_mode = Node.PROCESS_MODE_WHEN_PAUSED
func _input(event):
if event.is_action_pressed("ui_cancel"):
toggle_pause()
func toggle_pause():
paused = !paused
get_tree().paused = paused
visible = paused
if paused:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
func _on_resume_pressed():
toggle_pause()
func _on_restart_pressed():
toggle_pause()
get_tree().reload_current_scene()
func _on_quit_pressed():
toggle_pause()
get_tree().change_scene_to_file("res://scenes/ui/main_menu.tscn")
Testing y Debugging
Sistema de Debug
# debug_overlay.gd
extends Control
@export var show_debug: bool = false
@onready var fps_label = $FPSLabel
@onready var ball_speed_label = $BallSpeedLabel
@onready var ball_position_label = $BallPositionLabel
func _ready():
visible = OS.is_debug_build()
func _process(delta):
if Input.is_action_just_pressed("toggle_debug"):
show_debug = !show_debug
visible = show_debug
if show_debug:
fps_label.text = "FPS: " + str(Engine.get_frames_per_second())
var ball = get_node_or_null("/root/Game/Ball")
if ball:
ball_speed_label.text = "Speed: " + str(int(ball.linear_velocity.length()))
ball_position_label.text = "Pos: " + str(ball.global_position)
Exportando el Juego
Preparar para Exportación
-
Optimizar assets
- Comprimir imágenes
- Reducir calidad de audio si es necesario
-
Configurar iconos
- Project Settings → Application → Config → Icon
- Usa un PNG de 256x256
-
Configurar pantalla de inicio
- Project Settings → Application → Boot Splash
Exportar a Diferentes Plataformas
Windows
- Project → Export → Add → Windows Desktop
- Export Path:
builds/windows/Pong.exe - Export Project
HTML5
- Project → Export → Add → HTML5
- Export Path:
builds/html5/index.html - Head Include:
<style>
body { margin: 0; padding: 0; background: #000; }
#canvas { display: block; margin: 0 auto; }
</style>
Android
- Instalar Android SDK
- Configurar en Editor Settings
- Project → Export → Add → Android
- Configurar package name:
com.tuempresa.pong - Export APK
Resumen del Capítulo
¡Felicidades! Has creado tu primer juego completo con:
- ✅ Gameplay funcional de Pong
- ✅ Física realista de la pelota
- ✅ IA para el oponente CPU
- ✅ Sistema de puntuación
- ✅ Menú principal y pausa
- ✅ Efectos de sonido y visuales
- ✅ Guardado de configuraciones
- ✅ Exportación a múltiples plataformas
Ejercicios y Mejoras
Fácil
- Añade diferentes niveles de dificultad
- Implementa un contador de rally (golpes consecutivos)
- Añade más efectos de sonido
Medio
- Crea diferentes modos de juego (primer a 11, tiempo límite)
- Implementa power-ups aleatorios
- Añade un sistema de logros
Difícil
- Modo torneo con brackets
- Multijugador online básico
- Tabla de puntuaciones online
Próximo Capítulo
En el siguiente capítulo profundizaremos en el sistema de nodos y escenas de Godot:
- Arquitectura de escenas complejas
- Instanciación dinámica
- Comunicación entre escenas
- Patrones de diseño en Godot
→ Capítulo 4: Sistema de Nodos y Escenas
Proyecto Final: Añade tu toque personal al Pong. ¿Tema retro? ¿Estilo neón? ¿Mecánicas únicas? ¡Comparte tu versión en itch.io!