- Home
- iThink
- Programmazione per Ragazzi
- Progetto finale in Python: dall'idea al programma
Progetto finale in Python: dall'idea al programma
Divento Programmatore — Capitolo 8: Progetto finale — Crea il tuo gioco completo
È il momento di mettere tutto insieme. Hai imparato: variabili, controllo flusso, funzioni, liste/dizionari, moduli, Pygame. Ora costruiamo un gioco vero, completo, condivisibile.
Cosa imparerai
| Fase | Contenuto | | ------ | ----------- | | 1. Game Design Document (GDD) | Pianifica prima di codificare | | 2. Architettura a classi | Entity, Player, Enemy, Projectile, Particle | | 3. GameManager & State Machine | MENU, PLAYING, PAUSED, GAME_OVER | | 4. Collision Detection | AABB (rect), circolare, ottimizzazioni | | 5. Sistemi gioco | Punteggio, vite, livelli, high-score (JSON) | | 6. UI/Menu | Navigazione tastiera, HUD, schermate | | 7. Polish | Particelle, screen shake, effetti sonori | | 8. Packaging | PyInstaller → .exe Windows, .app macOS |
1. Game Design Document (GDD) — Pianifica prima
Non scrivere codice subito. Rispondi a queste domande su carta:
┌─────────────────────────────────────────────────────────────┐
│ GDD: "STAR DEFENDER" │
├─────────────────────────────────────────────────────────────┤
│ CONCEPT: Sparatutto verticale, difendi la base da ondate │
│ TARGET: 10-14 anni, partite 3-5 min │
│ CONTROLLI: Frecce/WASD muovi, SPAZIO spara, ESC pausa │
│ MECCANICHE CORE: │
│ - Player spara proiettili verso l'alto │
│ - Nemici scendono, sparano a caso │
│ - Power-up: fuoco rapido, scudo, vita extra │
│ - Boss ogni 5 ondate │
│ PROGRESSIONE: Ondate crescenti, velocità +10% per ondata │
│ VITTORIA: Sopravvivi 15 ondate (o infinito con high-score) │
│ GAME OVER: Base distrutta (vite = 0) │
│ ASSET NEEDED: Player, 3 nemici, proiettili, power-up, SFX │
└─────────────────────────────────────────────────────────────┘
Regola: GDD = bussola. Se non sai cosa codificare, rileggi GDD.
2. Struttura progetto
star_defender/
├── main.py # Entry point
├── config.py # Costanti, settings
├── assets/ # Immagini, suoni, font
│ ├── images/
│ ├── sounds/
│ └── fonts/
├── core/
│ ├── __init__.py
│ ├── entity.py # Classe base Entity
│ ├── player.py
│ ├── enemy.py
│ ├── projectile.py
│ ├── powerup.py
│ ├── particle.py
│ └── effects.py # Screen shake, flash
├── systems/
│ ├── __init__.py
│ ├── collision.py # Sistema collisioni
│ ├── spawn.py # Wave manager
│ ├── score.py # Punteggio, high-score
│ └── audio.py # Gestione suoni
├── ui/
│ ├── __init__.py
│ ├── hud.py # HUD in gioco
│ ├── menu.py # Menu principale, pause, game over
│ └── button.py
└── utils/
├── __init__.py
├── vector.py # Vettore2D (Cap. 6)
└── helpers.py
3. Configurazione centrale: config.py
# config.py
"""Tutte le costanti di gioco in un posto solo."""
from pygame.math import Vector2 as Vec2
# Finestra
SCREEN_WIDTH = 480
SCREEN_HEIGHT = 720
FPS = 60
TITLE = "Star Defender"
ICON_PATH = "assets/images/icon.png"
# Colori (R, G, B)
BG_COLOR = (10, 10, 20)
WHITE = (255, 255, 255)
RED = (255, 60, 60)
GREEN = (60, 255, 100)
BLUE = (60, 180, 255)
YELLOW = (255, 255, 60)
CYAN = (60, 255, 255)
ORANGE = (255, 160, 60)
PURPLE = (180, 60, 255)
# Player
PLAYER_SPEED = 400
PLAYER_FIRE_RATE = 0.15 # secondi
PLAYER_LIVES = 3
PLAYER_INVULN_TIME = 1.5 # secondi dopo hit
PLAYER_SIZE = (40, 30)
# Nemici
ENEMY_BASE_SPEED = 80
ENEMY_SPEED_INCREASE = 0.1 # +10% per onda
ENEMY_FIRE_CHANCE = 0.002 # per frame
ENEMY_TYPES = {
"basic": {"hp": 1, "score": 10, "color": RED, "size": (30, 30)},
"fast": {"hp": 1, "score": 20, "color": ORANGE, "size": (25, 25)},
"tank": {"hp": 3, "score": 50, "color": PURPLE, "size": (45, 40)},
"shooter": {"hp": 2, "score": 30, "color": CYAN, "size": (35, 35)},
}
# Proiettili
PROJECTILE_SPEED = 600
PROJECTILE_PLAYER_COLOR = YELLOW
PROJECTILE_ENEMY_COLOR = RED
PROJECTILE_SIZE = (4, 12)
# Power-up
POWERUP_TYPES = {
"rapid_fire": {"color": GREEN, "duration": 5.0, "symbol": "⚡"},
"shield": {"color": BLUE, "duration": 8.0, "symbol": "🛡"},
"extra_life": {"color": ORANGE, "duration": 0, "symbol": "♥"},
"spread": {"color": PURPLE, "duration": 6.0, "symbol": "✦"},
}
POWERUP_SPAWN_CHANCE = 0.15
POWERUP_SPEED = 100
# Particelle
PARTICLE_LIFETIME = 0.5
PARTICLE_COUNT_EXPLOSION = 15
# Wave system
WAVE_ENEMY_COUNT_BASE = 6
WAVE_ENEMY_INCREASE = 2
BOSS_WAVE_INTERVAL = 5
# High-score
HIGHSCORE_FILE = "highscore.json"
MAX_HIGHSCORES = 10
# Stati gioco
class GameState:
MENU = "menu"
PLAYING = "playing"
PAUSED = "paused"
GAME_OVER = "game_over"
VICTORY = "victory"
4. Entità base: core/entity.py
# core/entity.py
"""Classe base per tutti gli oggetti di gioco."""
import pygame
from pygame.math import Vector2 as Vec2
from config import *
class Entity:
def __init__(self, x: float, y: float, width: int, height: int, color: tuple):
self.pos = Vec2(x, y)
self.vel = Vec2(0, 0)
self.size = Vec2(width, height)
self.color = color
self.active = True
self.rect = pygame.Rect(0, 0, width, height)
self._update_rect()
def _update_rect(self):
self.rect.center = (int(self.pos.x), int(self.pos.y))
def update(self, dt: float):
self.pos += self.vel * dt
self._update_rect()
def draw(self, surface: pygame.Surface):
pygame.draw.rect(surface, self.color, self.rect, border_radius=4)
def is_off_screen(self) -> bool:
return (self.pos.y < -self.size.y or
self.pos.y > SCREEN_HEIGHT + self.size.y or
self.pos.x < -self.size.x or
self.pos.x > SCREEN_WIDTH + self.size.x)
def collides_with(self, other: "Entity") -> bool:
return self.rect.colliderect(other.rect)
def distance_to(self, other: "Entity") -> float:
return self.pos.distance_to(other.pos)
def kill(self):
self.active = False
5. Player: core/player.py
# core/player.py
"""Giocatore: movimento, sparo, power-up, invulnerabilità."""
import pygame
import time
from core.entity import Entity
from core.projectile import Projectile
from core.particle import create_explosion
from config import *
class Player(Entity):
def __init__(self, x: float, y: float):
super().__init__(x, y, *PLAYER_SIZE, BLUE)
self.lives = PLAYER_LIVES
self.score = 0
self.fire_cooldown = 0.0
self.invuln_timer = 0.0
self.visible = True
self.blink_timer = 0.0
# Power-up state
self.rapid_fire = False
self.rapid_fire_timer = 0.0
self.shield = False
self.shield_timer = 0.0
self.spread_shot = False
self.spread_timer = 0.0
def update(self, dt: float, keys: pygame.key.ScancodeWrapper, projectiles: list):
# Movimento
dir_x = (keys[pygame.K_RIGHT] or keys[pygame.K_d]) - \
(keys[pygame.K_LEFT] or keys[pygame.K_a])
dir_y = (keys[pygame.K_DOWN] or keys[pygame.K_s]) - \
(keys[pygame.K_UP] or keys[pygame.K_w])
if dir_x != 0 or dir_y != 0:
self.vel = Vec2(dir_x, dir_y).normalize() * PLAYER_SPEED
else:
self.vel = Vec2(0, 0)
super().update(dt)
# Clamp a schermo
self.pos.x = max(self.size.x/2, min(SCREEN_WIDTH - self.size.x/2, self.pos.x))
self.pos.y = max(self.size.y/2, min(SCREEN_HEIGHT - self.size.y/2, self.pos.y))
self._update_rect()
# Cooldown sparo
self.fire_cooldown = max(0, self.fire_cooldown - dt)
# Sparo
if keys[pygame.K_SPACE] and self.fire_cooldown <= 0:
self._shoot(projectiles)
rate = PLAYER_FIRE_RATE / 3 if self.rapid_fire else PLAYER_FIRE_RATE
self.fire_cooldown = rate
# Power-up timers
self._update_powerups(dt)
# Invulnerabilità lampeggio
if self.invuln_timer > 0:
self.invuln_timer -= dt
self.blink_timer += dt
if self.blink_timer > 0.1:
self.visible = not self.visible
self.blink_timer = 0
else:
self.visible = True
def _shoot(self, projectiles: list):
# Proiettile centrale
projectiles.append(Projectile(
self.pos.x, self.rect.top,
0, -PROJECTILE_SPEED,
PROJECTILE_PLAYER_COLOR, True
))
# Spread shot
if self.spread_shot:
for angle in (-15, 15):
projectiles.append(Projectile(
self.pos.x, self.rect.top,
angle, -PROJECTILE_SPEED,
PROJECTILE_PLAYER_COLOR, True
))
def _update_powerups(self, dt):
if self.rapid_fire:
self.rapid_fire_timer -= dt
if self.rapid_fire_timer <= 0:
self.rapid_fire = False
if self.shield:
self.shield_timer -= dt
if self.shield_timer <= 0:
self.shield = False
if self.spread_shot:
self.spread_timer -= dt
if self.spread_timer <= 0:
self.spread_shot = False
def apply_powerup(self, ptype: str):
data = POWERUP_TYPES[ptype]
if ptype == "rapid_fire":
self.rapid_fire = True
self.rapid_fire_timer = data["duration"]
elif ptype == "shield":
self.shield = True
self.shield_timer = data["duration"]
elif ptype == "extra_life":
self.lives = min(5, self.lives + 1)
elif ptype == "spread":
self.spread_shot = True
self.spread_timer = data["duration"]
def take_damage(self) -> bool:
if self.invuln_timer > 0 or self.shield:
if self.shield:
self.shield = False # Scudo si consuma
return False
self.lives -= 1
self.invuln_timer = PLAYER_INVULN_TIME
return True
def draw(self, surface: pygame.Surface):
if not self.visible:
return
super().draw(surface)
# Scudo visuale
if self.shield:
pygame.draw.circle(surface, BLUE, self.rect.center,
max(self.size) // 2 + 5, 2)
6. Nemici: core/enemy.py
# core/enemy.py
"""Nemici con tipi diversi e comportamento base."""
import random
import pygame
from core.entity import Entity
from core.projectile import Projectile
from config import *
class Enemy(Entity):
def __init__(self, x: float, y: float, etype: str = "basic"):
data = ENEMY_TYPES[etype]
super().__init__(x, y, *data["size"], data["color"])
self.etype = etype
self.hp = data["hp"]
self.max_hp = data["hp"]
self.score_value = data["score"]
self.vel = Vec2(0, ENEMY_BASE_SPEED)
self.fire_timer = random.uniform(0.5, 2.0)
self.shoot_delay = random.uniform(1.5, 3.0)
def update(self, dt: float, projectiles: list, player_pos: Vec2):
super().update(dt)
self.fire_timer -= dt
# Sparo base (solo shooter e tank)
if self.etype in ("shooter", "tank") and self.fire_timer <= 0:
self._shoot(projectiles, player_pos)
self.fire_timer = self.shoot_delay
def _shoot(self, projectiles: list, target: Vec2):
direction = (target - self.pos).normalize()
projectiles.append(Projectile(
self.pos.x, self.rect.bottom,
direction.x, direction.y,
PROJECTILE_ENEMY_COLOR, False
))
def take_damage(self, amount: int = 1) -> bool:
self.hp -= amount
return self.hp <= 0
def draw(self, surface: pygame.Surface):
super().draw(surface)
# Barra vita per tank
if self.max_hp > 1:
bar_w = self.size.x
bar_h = 4
pct = self.hp / self.max_hp
pygame.draw.rect(surface, RED,
(self.rect.left, self.rect.top - 8, bar_w, bar_h))
pygame.draw.rect(surface, GREEN,
(self.rect.left, self.rect.top - 8, bar_w * pct, bar_h))
7. Proiettili e Power-up: core/projectile.py, core/powerup.py
# core/projectile.py
"""Proiettili giocatore e nemici."""
import pygame
from core.entity import Entity
from config import *
class Projectile(Entity):
def __init__(self, x: float, y: float, dir_x: float, dir_y: float,
color: tuple, from_player: bool):
super().__init__(x, y, *PROJECTILE_SIZE, color)
self.from_player = from_player
if dir_x != 0 or dir_y != 0:
self.vel = Vec2(dir_x, dir_y).normalize() * PROJECTILE_SPEED
def update(self, dt: float):
super().update(dt)
if self.is_off_screen():
self.kill()
def draw(self, surface: pygame.Surface):
pygame.draw.rect(surface, self.color, self.rect, border_radius=2)
# core/powerup.py
"""Power-up cadono dai nemici distrutti."""
import pygame
from core.entity import Entity
from config import *
class PowerUp(Entity):
def __init__(self, x: float, y: float, ptype: str):
data = POWERUP_TYPES[ptype]
super().__init__(x, y, 24, 24, data["color"])
self.ptype = ptype
self.symbol = data["symbol"]
self.duration = data["duration"]
self.vel = Vec2(0, POWERUP_SPEED)
self.bob_timer = 0
self.bob_amount = 3
def update(self, dt: float):
super().update(dt)
self.bob_timer += dt * 3
self.rect.y += int(self.bob_amount * pygame.math.sin(self.bob_timer))
if self.is_off_screen():
self.kill()
def draw(self, surface: pygame.Surface):
# Sfondo cerchio
pygame.draw.circle(surface, self.color, self.rect.center, 14)
pygame.draw.circle(surface, (20, 20, 30), self.rect.center, 12)
# Simbolo testo
font = pygame.font.Font(None, 28)
txt = font.render(self.symbol, True, WHITE)
surface.blit(txt, txt.get_rect(center=self.rect.center))
8. Particelle ed effetti: core/particle.py, core/effects.py
# core/particle.py
"""Sistema particelle semplice per esplosioni."""
import random
import pygame
from pygame.math import Vector2 as Vec2
from config import *
class Particle:
def __init__(self, x: float, y: float, color: tuple):
self.pos = Vec2(x, y)
angle = random.uniform(0, 6.28)
speed = random.uniform(50, 150)
self.vel = Vec2(pygame.math.cos(angle), pygame.math.sin(angle)) * speed
self.color = color
self.life = PARTICLE_LIFETIME
self.max_life = self.life
self.size = random.randint(2, 5)
def update(self, dt: float):
self.pos += self.vel * dt
self.vel *= 0.95 # Attrito
self.life -= dt
def draw(self, surface: pygame.Surface):
if self.life <= 0:
return
alpha = int(255 * (self.life / self.max_life))
color = (*self.color[:3], alpha)
# Disegna su surface con per-pixel alpha
temp = pygame.Surface((self.size*2, self.size*2), pygame.SRCALPHA)
pygame.draw.circle(temp, color, (self.size, self.size), self.size)
surface.blit(temp, (self.pos.x - self.size, self.pos.y - self.size))
def is_dead(self) -> bool:
return self.life <= 0
def create_explosion(x: float, y: float, color: tuple, count: int = PARTICLE_COUNT_EXPLOSION) -> list:
return [Particle(x, y, color) for _ in range(count)]
# core/effects.py
"""Screen shake, flash, freeze frame."""
import random
import pygame
class EffectsManager:
def __init__(self):
self.shake_intensity = 0.0
self.shake_duration = 0.0
self.flash_color = None
self.flash_duration = 0.0
self.freeze_timer = 0.0
def shake(self, intensity: float, duration: float):
self.shake_intensity = max(self.shake_intensity, intensity)
self.shake_duration = max(self.shake_duration, duration)
def flash(self, color: tuple, duration: float):
self.flash_color = color
self.flash_duration = duration
def freeze(self, duration: float):
self.freeze_timer = duration
def update(self, dt: float):
if self.shake_duration > 0:
self.shake_duration -= dt
if self.shake_duration <= 0:
self.shake_intensity = 0
if self.flash_duration > 0:
self.flash_duration -= dt
if self.flash_duration <= 0:
self.flash_color = None
if self.freeze_timer > 0:
self.freeze_timer -= dt
def get_shake_offset(self) -> tuple:
if self.shake_intensity > 0:
return (random.uniform(-self.shake_intensity, self.shake_intensity),
random.uniform(-self.shake_intensity, self.shake_intensity))
return (0, 0)
def apply_flash(self, surface: pygame.Surface):
if self.flash_color:
overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
alpha = int(100 * (self.flash_duration / 0.1))
overlay.fill((*self.flash_color[:3], alpha))
surface.blit(overlay, (0, 0), special_flags=pygame.BLEND_RGBA_ADD)
def is_frozen(self) -> bool:
return self.freeze_timer > 0
9. Sistema collisioni: systems/collision.py
# systems/collision.py
"""Sistema collisioni ottimizzato: broad phase + narrow phase."""
from core.entity import Entity
from core.projectile import Projectile
from core.enemy import Enemy
from core.powerup import PowerUp
from core.particle import create_explosion
from config import *
class CollisionSystem:
def __init__(self):
self.particles = []
def check_collisions(self, player, projectiles: list, enemies: list,
powerups: list) -> dict:
"""Restituisce eventi: {'player_hit': bool, 'enemies_killed': list, 'powerups_collected': list}"""
events = {
"player_hit": False,
"enemies_killed": [],
"powerups_collected": [],
"score_gained": 0
}
# 1. Proiettili player vs Nemici
for proj in projectiles[:]:
if not proj.from_player or not proj.active:
continue
for enemy in enemies[:]:
if not enemy.active:
continue
if proj.collides_with(enemy):
proj.kill()
if enemy.take_damage():
enemy.active = False
events["enemies_killed"].append(enemy)
events["score_gained"] += enemy.score_value
# Particelle esplosione
self.particles.extend(create_explosion(
enemy.pos.x, enemy.pos.y, enemy.color))
else:
# Hit ma non ucciso
self.particles.extend(create_explosion(
proj.pos.x, proj.pos.y, YELLOW, 5))
break
# 2. Proiettili nemici vs Player
for proj in projectiles[:]:
if proj.from_player or not proj.active:
continue
if player.active and proj.collides_with(player):
proj.kill()
if player.take_damage():
events["player_hit"] = True
self.particles.extend(create_explosion(
player.pos.x, player.pos.y, BLUE, 20))
break
# 3. Nemici vs Player (collisione corpo)
for enemy in enemies:
if enemy.active and player.active and enemy.collides_with(player):
if player.take_damage():
events["player_hit"] = True
self.particles.extend(create_explosion(
player.pos.x, player.pos.y, BLUE, 20))
enemy.kill()
self.particles.extend(create_explosion(
enemy.pos.x, enemy.pos.y, enemy.color))
# 4. Player vs Power-up
for pup in powerups[:]:
if pup.active and player.active and pup.collides_with(player):
pup.kill()
events["powerups_collected"].append(pup.ptype)
return events
def update_particles(self, dt: float):
for p in self.particles[:]:
p.update(dt)
if p.is_dead():
self.particles.remove(p)
def draw_particles(self, surface: pygame.Surface):
for p in self.particles:
p.draw(surface)
10. Wave Manager: systems/spawn.py
# systems/spawn.py
"""Gestione ondate nemici, boss, power-up drop."""
import random
import pygame
from core.enemy import Enemy
from core.powerup import PowerUp
from config import *
class WaveManager:
def __init__(self):
self.wave = 0
self.enemies_in_wave = 0
self.enemies_spawned = 0
self.spawn_timer = 0
self.spawn_interval = 1.0
self.wave_cleared = True
self.boss_active = False
def start_next_wave(self):
self.wave += 1
self.enemies_in_wave = WAVE_ENEMY_COUNT_BASE + (self.wave - 1) * WAVE_ENEMY_INCREASE
self.enemies_spawned = 0
self.wave_cleared = False
self.boss_active = (self.wave % BOSS_WAVE_INTERVAL == 0)
# Velocità aumenta per onda
speed_mult = 1 + (self.wave - 1) * ENEMY_SPEED_INCREASE
return speed_mult
def update(self, dt: float, enemies: list, speed_mult: float) -> list:
"""Restituisce lista nuovi power-up droppati."""
new_powerups = []
if self.wave_cleared:
return new_powerups
self.spawn_timer -= dt
if self.enemies_spawned < self.enemies_in_wave and self.spawn_timer <= 0:
self._spawn_enemy(enemies, speed_mult)
self.enemies_spawned += 1
self.spawn_timer = self.spawn_interval
# Controlla fine onda
active_enemies = [e for e in enemies if e.active]
if self.enemies_spawned >= self.enemies_in_wave and not active_enemies:
self.wave_cleared = True
return new_powerups
def _spawn_enemy(self, enemies: list, speed_mult: float):
# Scegli tipo basato su onda
if self.boss_active:
etype = "tank" # Boss = tank potenziato
elif self.wave >= 3:
etype = random.choices(
["basic", "fast", "tank", "shooter"],
weights=[40, 25, 20, 15]
)[0]
else:
etype = random.choices(["basic", "fast"], weights=[70, 30])[0]
x = random.randint(40, SCREEN_WIDTH - 40)
y = -50
enemy = Enemy(x, y, etype)
enemy.vel.y *= speed_mult
if self.boss_active:
enemy.hp = enemy.max_hp = 10
enemy.score_value = 200
enemy.size = Vec2(60, 55)
enemy.color = PURPLE
enemies.append(enemy)
def on_enemy_killed(self, enemy: Enemy) -> list:
"""Chiamato quando nemico muore, restituisce power-up droppati."""
drops = []
if random.random() < POWERUP_SPAWN_CHANCE:
ptype = random.choice(list(POWERUP_TYPES.keys()))
drops.append(PowerUp(enemy.pos.x, enemy.pos.y, ptype))
return drops
11. Score & High-score: systems/score.py
# systems/score.py
"""Punteggio, high-score persistente (JSON)."""
import json
import os
from config import *
class ScoreSystem:
def __init__(self):
self.score = 0
self.highscores = self._load()
def _load(self) -> list:
if os.path.exists(HIGHSCORE_FILE):
try:
with open(HIGHSCORE_FILE, "r") as f:
return json.load(f)
except Exception:
return []
return []
def save(self):
with open(HIGHSCORE_FILE, "w") as f:
json.dump(self.highscores, f)
def add_score(self, points: int):
self.score += points
def reset(self):
self.score = 0
def check_highscore(self) -> bool:
"""True se score entra in top 10."""
if len(self.highscores) < MAX_HIGHSCORES:
return True
return self.score > min(hs["score"] for hs in self.highscores)
def add_highscore(self, name: str):
entry = {"name": name[:12], "score": self.score, "wave": 0}
self.highscores.append(entry)
self.highscores.sort(key=lambda x: x["score"], reverse=True)
self.highscores = self.highscores[:MAX_HIGHSCORES]
self.save()
def get_highscores(self) -> list:
return self.highscores
12. UI: Menu, HUD, Game Over — ui/menu.py, ui/hud.py
# ui/button.py
"""Pulsante riutilizzabile per menu."""
import pygame
from config import *
class Button:
def __init__(self, x: int, y: int, w: int, h: int, text: str,
font: pygame.font.Font, action=None):
self.rect = pygame.Rect(x, y, w, h)
self.text = text
self.font = font
self.action = action
self.hovered = False
self.colors = {
"bg": (60, 60, 80),
"bg_hover": (100, 100, 140),
"border": (180, 180, 200),
"text": WHITE,
}
def update(self, mouse_pos: tuple):
self.hovered = self.rect.collidepoint(mouse_pos)
def handle_event(self, event: pygame.event.Event) -> bool:
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
if self.hovered and self.action:
self.action()
return True
return False
def draw(self, surface: pygame.Surface):
color = self.colors["bg_hover"] if self.hovered else self.colors["bg"]
pygame.draw.rect(surface, color, self.rect, border_radius=8)
pygame.draw.rect(surface, self.colors["border"], self.rect, 2, border_radius=8)
txt = self.font.render(self.text, True, self.colors["text"])
surface.blit(txt, txt.get_rect(center=self.rect.center))
# ui/menu.py
"""Menu principale, pausa, game over."""
import pygame
from ui.button import Button
from config import *
class Menu:
def __init__(self, game):
self.game = game
self.font_title = pygame.font.Font(None, 64)
self.font_btn = pygame.font.Font(None, 36)
self.font_small = pygame.font.Font(None, 24)
self.buttons = []
self._build_main()
def _build_main(self):
self.buttons.clear()
cx = SCREEN_WIDTH // 2
self.buttons.append(Button(cx-100, 280, 200, 50, "GIOCA", self.font_btn,
lambda: self.game.change_state(GameState.PLAYING)))
self.buttons.append(Button(cx-100, 350, 200, 50, "HIGH SCORE", self.font_btn,
lambda: self.game.show_highscores()))
self.buttons.append(Button(cx-100, 420, 200, 50, "ESCI", self.font_btn,
self.game.quit))
def _build_pause(self):
self.buttons.clear()
cx = SCREEN_WIDTH // 2
self.buttons.append(Button(cx-100, 280, 200, 50, "RIPRENDI", self.font_btn,
lambda: self.game.change_state(GameState.PLAYING)))
self.buttons.append(Button(cx-100, 350, 200, 50, "MENU", self.font_btn,
lambda: self.game.change_state(GameState.MENU)))
def _build_game_over(self):
self.buttons.clear()
cx = SCREEN_WIDTH // 2
self.buttons.append(Button(cx-100, 380, 200, 50, "RIGIOCA", self.font_btn,
lambda: self.game.restart()))
self.buttons.append(Button(cx-100, 450, 200, 50, "MENU", self.font_btn,
lambda: self.game.change_state(GameState.MENU)))
def set_state(self, state: str):
if state == GameState.MENU:
self._build_main()
elif state == GameState.PAUSED:
self._build_pause()
elif state in (GameState.GAME_OVER, GameState.VICTORY):
self._build_game_over()
def update(self, mouse_pos: tuple):
for btn in self.buttons:
btn.update(mouse_pos)
def handle_event(self, event: pygame.event.Event):
for btn in self.buttons:
btn.handle_event(event)
def draw(self, surface: pygame.Surface, state: str, score: int = 0, wave: int = 0):
# Overlay scuro
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 180))
surface.blit(overlay, (0, 0))
if state == GameState.MENU:
title = self.font_title.render("STAR DEFENDER", True, CYAN)
surface.blit(title, title.get_rect(center=(SCREEN_WIDTH//2, 160)))
subtitle = self.font_small.render("Freccie/WASD: muovi | SPAZIO: spara | ESC: pausa", True, (150,150,150))
surface.blit(subtitle, subtitle.get_rect(center=(SCREEN_WIDTH//2, 220)))
elif state == GameState.PAUSED:
title = self.font_title.render("PAUSA", True, YELLOW)
surface.blit(title, title.get_rect(center=(SCREEN_WIDTH//2, 180)))
elif state in (GameState.GAME_OVER, GameState.VICTORY):
if state == GameState.GAME_OVER:
title = self.font_title.render("GAME OVER", True, RED)
else:
title = self.font_title.render("VITTORIA!", True, GREEN)
surface.blit(title, title.get_rect(center=(SCREEN_WIDTH//2, 180)))
score_txt = self.font_btn.render(f"Punteggio: {score} | Onda: {wave}", True, WHITE)
surface.blit(score_txt, score_txt.get_rect(center=(SCREEN_WIDTH//2, 260)))
if self.game.score_system.check_highscore():
hs_txt = self.font_small.render("NUOVO HIGH SCORE! Inserisci nome alla fine.", True, YELLOW)
surface.blit(hs_txt, hs_txt.get_rect(center=(SCREEN_WIDTH//2, 310)))
for btn in self.buttons:
btn.draw(surface)
# ui/hud.py
"""HUD in gioco: vite, score, wave, power-up attivi."""
import pygame
from config import *
class HUD:
def __init__(self):
self.font = pygame.font.Font(None, 28)
self.font_small = pygame.font.Font(None, 20)
def draw(self, surface: pygame.Surface, player, wave: int, score: int):
# Vite (cuori)
for i in range(player.lives):
x = 15 + i * 30
pygame.draw.polygon(surface, RED, [
(x+12, y:=15), (x+24, y+10), (x+12, y+24), (x, y+10)
])
# Score
score_surf = self.font.render(f"SCORE: {score}", True, WHITE)
surface.blit(score_surf, (SCREEN_WIDTH - score_surf.get_width() - 15, 10))
# Wave
wave_surf = self.font.render(f"WAVE: {wave}", True, CYAN)
surface.blit(wave_surf, (SCREEN_WIDTH//2 - wave_surf.get_width()//2, 10))
# Power-up attivi
y_off = 50
if player.rapid_fire:
txt = self.font_small.render(f"⚡ RAPID FIRE: {player.rapid_fire_timer:.1f}s", True, GREEN)
surface.blit(txt, (15, y_off))
y_off += 22
if player.shield:
txt = self.font_small.render(f"🛡 SHIELD: {player.shield_timer:.1f}s", True, BLUE)
surface.blit(txt, (15, y_off))
y_off += 22
if player.spread_shot:
txt = self.font_small.render(f"✦ SPREAD: {player.spread_timer:.1f}s", True, PURPLE)
surface.blit(txt, (15, y_off))
13. Game Manager: main.py (il cuore)
# main.py
"""Star Defender - Entry point e Game Manager."""
import pygame
import sys
from config import *
from core.player import Player
from core.enemy import Enemy
from core.projectile import Projectile
from core.powerup import PowerUp
from core.particle import create_explosion
from core.effects import EffectsManager
from systems.collision import CollisionSystem
from systems.spawn import WaveManager
from systems.score import ScoreSystem
from ui.menu import Menu
from ui.hud import HUD
from GameState import GameState
class Game:
def __init__(self):
pygame.init()
pygame.mixer.init()
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption(TITLE)
self.clock = pygame.time.Clock()
self.running = True
self.dt = 0
self.state = GameState.MENU
self.score_system = ScoreSystem()
self.collision = CollisionSystem()
self.wave_mgr = WaveManager()
self.effects = EffectsManager()
self.menu = Menu(self)
self.hud = HUD()
self._reset_game()
def _reset_game(self):
self.player = Player(SCREEN_WIDTH//2, SCREEN_HEIGHT - 80)
self.enemies = []
self.projectiles = []
self.powerups = []
self.score_system.reset()
self.wave_mgr = WaveManager()
self.speed_mult = self.wave_mgr.start_next_wave()
def change_state(self, new_state: str):
self.state = new_state
self.menu.set_state(new_state)
if new_state == GameState.PLAYING:
pygame.mouse.set_visible(False)
else:
pygame.mouse.set_visible(True)
def show_highscores(self):
# TODO: implementa schermata high-score
pass
def restart(self):
self._reset_game()
self.change_state(GameState.PLAYING)
def quit(self):
self.running = False
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
if self.state == GameState.PLAYING:
self.change_state(GameState.PAUSED)
elif self.state == GameState.PAUSED:
self.change_state(GameState.PLAYING)
elif self.state in (GameState.GAME_OVER, GameState.VICTORY):
self.change_state(GameState.MENU)
elif event.key == pygame.K_F11:
pygame.display.toggle_fullscreen()
self.menu.handle_event(event)
def update(self):
if self.state != GameState.PLAYING:
self.menu.update(pygame.mouse.get_pos())
return
# Freeze frame
if self.effects.is_frozen():
return
keys = pygame.key.get_pressed()
# Update entities
self.player.update(self.dt, keys, self.projectiles)
for enemy in self.enemies:
enemy.update(self.dt, self.projectiles, self.player.pos)
for proj in self.projectiles[:]:
proj.update(self.dt)
if not proj.active:
self.projectiles.remove(proj)
for pup in self.powerups[:]:
pup.update(self.dt)
if not pup.active:
self.powerups.remove(pup)
# Wave spawn
new_powerups = self.wave_mgr.update(self.dt, self.enemies, self.speed_mult)
self.powerups.extend(new_powerups)
# Collisioni
events = self.collision.check_collisions(
self.player, self.projectiles, self.enemies, self.powerups)
if events["player_hit"]:
self.effects.shake(8, 0.3)
self.effects.flash(RED, 0.1)
self.effects.freeze(0.05)
if self.player.lives <= 0:
self._game_over()
for enemy in events["enemies_killed"]:
self.effects.shake(4, 0.15)
self.effects.flash(enemy.color, 0.05)
drops = self.wave_mgr.on_enemy_killed(enemy)
self.powerups.extend(drops)
for ptype in events["powerups_collected"]:
self.player.apply_powerup(ptype)
self.effects.flash(GREEN, 0.1)
self.score_system.add_score(events["score_gained"])
# Particelle
self.collision.update_particles(self.dt)
# Effetti
self.effects.update(self.dt)
# Prossima onda
if self.wave_mgr.wave_cleared and self.state == GameState.PLAYING:
if self.wave_mgr.wave >= 15:
self._victory()
else:
self.speed_mult = self.wave_mgr.start_next_wave()
def _game_over(self):
self.change_state(GameState.GAME_OVER)
def _victory(self):
self.change_state(GameState.VICTORY)
def draw(self):
self.screen.fill(BG_COLOR)
if self.state == GameState.PLAYING or self.state == GameState.PAUSED:
# Entità
for enemy in self.enemies:
if enemy.active:
enemy.draw(self.screen)
for proj in self.projectiles:
if proj.active:
proj.draw(self.screen)
for pup in self.powerups:
if pup.active:
pup.draw(self.screen)
self.player.draw(self.screen)
# Particelle
self.collision.draw_particles(self.screen)
# HUD
self.hud.draw(self.screen, self.player, self.wave_mgr.wave, self.score_system.score)
# Pause overlay
if self.state == GameState.PAUSED:
self.menu.draw(self.screen, self.state)
elif self.state in (GameState.MENU, GameState.GAME_OVER, GameState.VICTORY):
self.menu.draw(self.screen, self.state,
self.score_system.score, self.wave_mgr.wave)
# Effetti screen shake/flash
shake_x, shake_y = self.effects.get_shake_offset()
self.effects.apply_flash(self.screen)
# Applica shake blittando su surface offset
if shake_x != 0 or shake_y != 0:
temp = self.screen.copy()
self.screen.fill(BG_COLOR)
self.screen.blit(temp, (shake_x, shake_y))
pygame.display.flip()
def run(self):
while self.running:
self.dt = self.clock.tick(FPS) / 1000.0
self.handle_events()
self.update()
self.draw()
pygame.quit()
sys.exit()
if __name__ == "__main__":
game = Game()
game.run()
14. Packaging: crea eseguibile condivisibile
PyInstaller
pip install pyinstaller
# Windows: .exe singolo file
pyinstaller --onefile --windowed --icon=assets/images/icon.ico \
--add-data "assets;assets" \
--name "StarDefender" main.py
# macOS: .app bundle
pyinstaller --onefile --windowed --icon=assets/images/icon.icns \
--add-data "assets:assets" \
--name "StarDefender" main.py
# Linux: binario
pyinstaller --onefile --windowed --add-data "assets:assets" main.py
Struttura dist/
dist/
└── StarDefender.exe # Windows
o
dist/
└── StarDefender.app # macOS (cartella)
└── Contents/MacOS/StarDefender
Spec file per controllo avanzato (StarDefender.spec)
# PyInstaller spec file
block_cipher = None
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('assets', 'assets')],
hiddenimports=['pygame', 'pygame._view'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['tkinter', 'matplotlib', 'numpy', 'pandas'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz, a.scripts, a.binaries, a.zipfiles, a.datas,
[],
name='StarDefender',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='assets/images/icon.ico'
)
pyinstaller StarDefender.spec
15. Checklist finale progetto
| ✅ Completato | Componente | | --------------- | ------------ | | [ ] GDD scritto | Design document | | [ ] Config centralizzato | config.py | | [ ] Entity base | core/entity.py | | [ ] Player completo | Movimento, sparo, power-up, invuln | | [ ] Nemici vari | Basic, fast, tank, shooter, boss | | [ ] Proiettili | Player/enemy, direzioni | | [ ] Power-up | 4 tipi, durate, visual | | [ ] Particelle | Esplosioni, fade | | [ ] Effetti | Screen shake, flash, freeze | | [ ] Collisioni | Broad/narrow phase, eventi | | [ ] Wave Manager | Spawn, difficoltà, boss | | [ ] Score System | Punteggio, high-score JSON | | [ ] Menu/HUD | Navigazione, info in-game | | [ ] Game Manager | State machine, loop principale | | [ ] Build eseguibile | PyInstaller .exe/.app |
16. Prossimi passi (oltre il libro)
- Asset reali: Sostituisci forme con sprite PNG (Kenney.nl, OpenGameArt.org)
- Audio: Aggiungi
systems/audio.pycon musica dinamica, SFX - Salvataggio:
savegame.jsonper continuità - Controller: Supporto gamepad (
pygame.joystick) - Particelle GPU:
pygame.gpuo shader per migliaia particelle - Multiplayer locale: Due player, split screen
- Level editor: Tiled map editor → JSON → parser Python
- Pubblica: itch.io, GitHub Releases, Microsoft Store (MSIX)
Conclusione della serie
Hai completato "Divento Programmatore: Python e i Giochi" (Cervello Elettronico Vol.2)!
| Capitolo | Competenza chiave | | ---------- | ------------------- | | 1 | Installazione, REPL, Hello World, codice = istruzioni | | 2 | Variabili, tipi, input, f-string, naming | | 3 | Boolean logic, if/elif/else, while, for, range | | 4 | Funzioni, parametri, return, scope, docstring, DRY | | 5 | Liste, dizionari, nesting, loop, comprensioni | | 6 | Moduli, stdlib (random, time, math), pip, package | | 7 | Pygame: finestra, event loop, draw, input, clock | | 8 | Progetto completo: architettura, sistemi, packaging |
Hai ora gli strumenti per creare qualsiasi gioco 2D. La programmazione è pratica: scrivi codice, rompi cose, ripara, ripeti.
"Il miglior modo per imparare a programmare è programmare." — Qualcuno di saggio
Risorse utili
- Pygame docs: https://www.pygame.org/docs/
- Python docs: https://docs.python.org/3/
- Asset gratuiti: https://kenney.nl/assets, https://opengameart.org/
- Comunità: r/pygame, r/learnpython, Discord Python Italia
- Altri libri: "Automate the Boring Stuff", "Python Crash Course", "Game Programming Patterns"
Fine della serie "Divento Programmatore: Python e i Giochi" (Cervello Elettronico Vol.2). Grazie per aver seguito il percorso! Condividi i tuoi giochi, continua a imparare, divertiti a programmare. 🚀