1. Home
  2. iThink
  3. Programmazione per Ragazzi
  4. Pong in Python con Pygame: il gioco completo
Programmazione per Ragazzi Divento Programmatore · Capitolo 8

Pong in Python con Pygame: il gioco completo

Codice Python sullo schermo di un computer
Foto: JohnsonMartin · Pixabay · Licenza dei contenuti Pixabay

Divento Programmatore — Capitolo 8: Pong: Due giocatori, un solo vincitore

Obiettivi di Apprendimento

Al termine di questo capitolo avrai costruito un gioco completo di Arcade Pong e imparerai a:

  • Comprendere il paradigma della Programmazione Orientata agli Oggetti (OOP)
  • Creare Classi in Python per definire modelli di oggetti di gioco
  • Istanziare Oggetti (creare copie reali partendo da una classe)
  • Utilizzare il costruttore __init__ per inizializzare lo stato interno degli oggetti
  • Comprendere l'uso del parametro self per fare riferimento all'oggetto corrente
  • Applicare metodi all'interno delle classi per gestirne il movimento e il disegno

Termini Fondamentali

| Termine | Simbolo | Significato | | --------- | --------- | ------------- | | Programmazione Orientata agli Oggetti (OOP) | 📦 | Stile di programmazione in cui il codice è organizzato attorno a "oggetti" reali che contengono sia i dati (attributi) che le azioni (metodi) | | Classe (class) | 📐 | Lo stampo o il progetto che descrive le caratteristiche e i comportamenti di un tipo di oggetto | | Oggetto / Istanza (object / instance) | 🍪 | Il singolo elemento creato fisicamente in memoria partendo da una classe | | Costruttore (__init__) | 🏗️ | Metodo speciale di Python eseguito in automatico non appena un nuovo oggetto viene creato, usato per impostarne le variabili iniziali | | Metodo (method) | ⚙️ | Una funzione definita all'interno di una classe che descrive un'azione compibile dall'oggetto (es. muovi() o disegna()) | | self | 👤 | Parola chiave speciale che rappresenta l'oggetto specifico su cui Python sta eseguendo il metodo in quel momento |


La Storia: La nascita dell'industria dei videogiochi

Nel 1972, una piccolissima e sconosciuta azienda californiana chiamata Atari, fondata da Nolan Bushnell e Ted Dabney, decise di creare un gioco elettronico da installare nei locali pubblici (i bar e le sale giochi dell'epoca). Bushnell assegnò l'incarico a un giovane ingegnere elettronico appena assunto, Allan Alcorn, come esercizio di prova per fargli fare pratica.

Alcorn progettò un semplicissimo simulatore di ping-pong in cui due barrette bianche potevano muoversi in verticale per intercettare un quadratino bianco che rimbalzava sullo schermo, emettendo un caratteristico suono elettronico "bip" a ogni tocco. Installarono il prototipo del cabinato (chiamato semplicemente Pong) in una taverna di Sunnyvale, in California, inserendovi una scatola di latte vuota per raccogliere le monete.

Dopo poche ore, il gestore del locale chiamò Atari infuriato dicendo che la macchina si era rotta. Quando Alcorn andò a controllare, scoprì che il gioco non era rotto, ma semplicemente intasato di monete da 25 centesimi che impedivano il funzionamento del meccanismo! Pong fu il primo videogioco a riscuotere un successo commerciale travolgente, segnando la nascita ufficiale dell'industria mondiale dell'intrattenimento interattivo. Oggi, userai la programmazione a oggetti per ricreare questa storica pietra miliare!


Prima di tutto: cosa sono le Classi?

Prima di immergerti nel codice di Pong, un momento di chiarezza. Fino a ora hai usato funzioni per organizzare il tuo codice. Le classi sono il passo successivo: una classe è come uno stampino per biscotti — definisce la forma una volta sola, e puoi usarlo quante volte vuoi per creare oggetti tutti uguali ma indipendenti.

# Esempio semplicissimo: una Classe base
class Persona:
    def __init__(self, nome, eta):
        self.nome = nome   # ogni Persona ha il suo nome
        self.eta = eta     # e la sua età

# Creare due oggetti dalla stessa Classe
mario = Persona("Mario", 12)
luigi = Persona("Luigi", 14)
print(mario.nome)  # Output: Mario
print(luigi.eta)   # Output: 14

self è il modo in cui la classe si riferisce a se stessa: self.nome significa "il nome di questo oggetto specifico". Ora sei pronto per le classi Racchetta e Pallina di Pong!


Concept 1: Lo stampino per biscotti (Classi e Oggetti)

La Programmazione Orientata agli Oggetti può sembrare difficile all'inizio, ma si basa su un'analogia molto semplice del mondo reale: lo stampino per biscotti.

  ┌───────────────────────┐
  │   CLASSE (Stampino)   │ --> Definisce solo la forma geometrica del biscotto.
  └───────────┬───────────┘
              │
              ▼
  ┌───────────────────────┐
  │   OGGETTI (Biscotti)  │ --> Sono i biscotti reali fatti di pasta sfoglia
  └───────────────────────┘     (ognuno posizionato in un punto diverso).
  • La Classe è lo stampino. Non è un biscotto vero: definisce solo la forma, l'altezza, la larghezza e le regole del biscotto. Nel codice, scriverai class Racchetta: per spiegare a Python com'è fatta una racchetta.
  • L'Oggetto (o istanza) è il biscotto reale sfornato dallo stampino. Scrivendo racchetta_sinistra = Racchetta(...) crei un oggetto reale posizionato a sinistra dello schermo, e scrivendo racchetta_destra = Racchetta(...) ne crei un secondo a destra.

Entrambe le racchette usano lo stesso identico codice (la stessa classe), ma sono due oggetti distinti con posizioni e tasti di controllo diversi!


Concept 2: Cos'è quel self nel codice?

All'interno delle definizioni delle classi Python, noterai che il primo argomento di ogni funzione è sempre la parolina magica self (es: def muovi(self):).

Quando scrivi una classe, non sai ancora quale oggetto reale userà quel codice. self serve a dire a Python: "esegui questa azione sull'oggetto specifico che ti ha chiamato in questo momento".

Ad esempio, se scriviamo:

class Racchetta:
    def __init__(self, y):
        self.y = y # Imposta la coordinata Y di QUESTO specifico oggetto

Quando Python esegue racchetta_sinistra = Racchetta(250), la variabile self punterà a racchetta_sinistra, modificando la sua Y. Quando esegue racchetta_destra = Racchetta(100), self punterà a racchetta_destra, lasciando inalterata la racchetta sinistra!


Costruiamo il Gioco: Versione Base

Ecco la versione base strutturata in modo procedurale, con la pallina che rimbalza e le due racchette che possono essere spostate in verticale:

import pygame
import sys

pygame.init()
schermo = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Pong Base")
orologio = pygame.time.Clock()

# Coordinate e velocità degli elementi
palla_x, palla_y = 400, 300
palla_vel_x, palla_vel_y = 5, 5

racc_sx_y = 250
racc_dx_y = 250

while True:
    tasti = pygame.key.get_pressed()
    for evento in pygame.event.get():
        if evento.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    # Spostamento racchette
    if tasti[pygame.K_w] and racc_sx_y > 0:
        racc_sx_y -= 5
    if tasti[pygame.K_s] and racc_sx_y < 500:
        racc_sx_y += 5
    if tasti[pygame.K_UP] and racc_dx_y > 0:
        racc_dx_y -= 5
    if tasti[pygame.K_DOWN] and racc_dx_y < 500:
        racc_dx_y += 5

    # Spostamento pallina
    palla_x += palla_vel_x
    palla_y += palla_vel_y

    # Rimbalzo alto e basso
    if palla_y <= 0 or palla_y >= 600:
        palla_vel_y = -palla_vel_y

    # Reset se va fuori
    if palla_x < 0 or palla_x > 800:
        palla_x, palla_y = 400, 300

    # Rendering
    schermo.fill((0, 0, 0))
    pygame.draw.rect(schermo, (255, 255, 255), pygame.Rect(30, racc_sx_y, 20, 100))
    pygame.draw.rect(schermo, (255, 255, 255), pygame.Rect(750, racc_dx_y, 20, 100))
    pygame.draw.circle(schermo, (255, 255, 255), (palla_x, palla_y), 10)
    
    pygame.display.flip()
    orologio.tick(60)

Sfide: Rendi il tuo Pong un vero capolavoro OOP

Sfida 1 — Spostare le Racchette in Classe

Riscrivi il codice delle racchette incapsulandolo all'interno di una classe Racchetta, definendo un costruttore __init__, un metodo muovi(self, tasti) e un metodo disegna(self, schermo).

Sfida 2 — Velocità Pallina

Fai in modo che a ogni collisione riuscita con una racchetta, la pallina incrementi la propria velocità del 10%. In questo modo, lo scambio diventerà sempre più veloce e stimolante!

Sfida 3 — Punteggio Grafico

Utilizza il modulo pygame.font per caricare un font di sistema (es: Consolas o Arial) e stampare a schermo il punteggio dei due giocatori al centro della metà campo.

Sfida 4 — L'avversario AI

Aggiungi un parametro booleano is_ai alla classe Racchetta. Se impostato su True, la racchetta non leggerà l'input della tastiera ma seguirà in automatico la coordinata Y della pallina, permettendo al giocatore di sfidare il computer!


Il Codice Completo con Tutte le Sfide

Salva questo codice in VS Code come pong_oop.py e avvialo per sfidare l'intelligenza artificiale!

import pygame
import sys
import random

# Dimensioni finestra
LARGHEZZA = 800
ALTEZZA = 600

# Colori
NERO = (0, 0, 0)
BIANCO = (255, 255, 255)

class Racchetta:
    """Classe che rappresenta la racchetta di gioco."""
    def __init__(self, x, y, larghezza, altezza, velocita, tasto_su, tasto_giu, is_ai=False):
        self.x = x
        self.y = y
        self.larghezza = larghezza
        self.altezza = altezza
        self.velocita = velocita
        self.tasto_su = tasto_su
        self.tasto_giu = tasto_giu
        self.is_ai = is_ai

    def muovi(self, tasti, palla_y):
        """Sposta la racchetta in base ai tasti premuti o all'algoritmo AI."""
        if self.is_ai:
            # Algoritmo AI molto semplice: segue la pallina con un leggero ritardo
            if self.y + self.altezza/2 < palla_y:
                self.y += self.velocita
            elif self.y + self.altezza/2 > palla_y:
                self.y -= self.velocita
        else:
            # Movimento umano
            if tasti[self.tasto_su]:
                self.y -= self.velocita
            if tasti[self.tasto_giu]:
                self.y += self.velocita

        # Limiti dello schermo per evitare di sparire
        if self.y < 0:
            self.y = 0
        elif self.y > ALTEZZA - self.altezza:
            self.y = ALTEZZA - self.altezza

    def disegna(self, schermo):
        """Disegna la racchetta a schermo."""
        pygame.draw.rect(schermo, BIANCO, pygame.Rect(self.x, self.y, self.larghezza, self.altezza))


class Pallina:
    """Classe che rappresenta la pallina di gioco."""
    def __init__(self, x, y, raggio, vel_x, vel_y):
        self.x = x
        self.y = y
        self.raggio = raggio
        self.vel_x = vel_x
        self.vel_y = vel_y
        self.vel_iniziale_x = vel_x
        self.vel_iniziale_y = vel_y

    def reset(self):
        """Riposiziona la pallina al centro e inverte la direzione di lancio."""
        self.x = LARGHEZZA // 2
        self.y = ALTEZZA // 2
        self.vel_x = self.vel_iniziale_x * random.choice([1, -1])
        self.vel_y = self.vel_iniziale_y * random.choice([1, -1])

    def muovi(self):
        """Aggiorna la posizione e gestisce i rimbalzi sui bordi orizzontali."""
        self.x += self.vel_x
        self.y += self.vel_y

        # Rimbalzo su soffitto e pavimento
        if self.y - self.raggio <= 0 or self.y + self.raggio >= ALTEZZA:
            self.vel_y = -self.vel_y

    def disegna(self, schermo):
        """Disegna la pallina a schermo."""
        pygame.draw.circle(schermo, BIANCO, (int(self.x), int(self.y)), self.raggio)

    def controlla_collisione(self, racchetta):
        """Gestisce le collisioni elastiche con le racchette."""
        rettangolo_racchetta = pygame.Rect(racchetta.x, racchetta.y, racchetta.larghezza, racchetta.altezza)
        # Troviamo il punto più vicino sul rettangolo alla circonferenza
        punto_vicino_x = max(racchetta.x, min(self.x, racchetta.x + racchetta.larghezza))
        punto_vicino_y = max(racchetta.y, min(self.y, racchetta.y + racchetta.altezza))
        
        # Calcoliamo la distanza
        dist_x = self.x - punto_vicino_x
        dist_y = self.y - punto_vicino_y
        distanza_quadrata = dist_x**2 + dist_y**2
        
        if distanza_quadrata < self.raggio**2:
            # Collisione rilevata! Invertiamo la direzione orizzontale
            self.vel_x = -self.vel_x
            # Aumentiamo la velocità del 10% per rendere il gioco dinamico (Sfida 2)
            self.vel_x *= 1.1
            self.vel_y *= 1.1
            
            # Allontaniamo leggermente la pallina per evitare collisioni multiple nello stesso frame
            if self.x < LARGHEZZA / 2:
                self.x = racchetta.x + racchetta.larghezza + self.raggio
            else:
                self.x = racchetta.x - self.raggio


def main():
    pygame.init()
    schermo = pygame.display.set_mode((LARGHEZZA, ALTEZZA))
    pygame.display.set_caption("🏓 Arcade Pong OOP 🎮")
    orologio = pygame.time.Clock()
    
    # Creiamo gli oggetti di gioco (Istanze delle Classi)
    # Sfida 4: la racchetta di destra viene controllata dall'AI
    racchetta_sinistra = Racchetta(30, 250, 20, 100, 7, pygame.K_w, pygame.K_s)
    racchetta_destra = Racchetta(750, 250, 20, 100, 7, pygame.K_UP, pygame.K_DOWN, is_ai=True)
    pallina = Pallina(400, 300, 12, 5, 5)
    
    punteggio_sinistro = 0
    punteggio_destro = 0
    
    try:
        font = pygame.font.SysFont("Consolas", 60)
    except Exception:
        font = None
        
    while True:
        # 1. Rilevamento Input
        tasti = pygame.key.get_pressed()
        for evento in pygame.event.get():
            if evento.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
                
        # 2. Aggiornamento dello Stato
        racchetta_sinistra.muovi(tasti, pallina.y)
        racchetta_destra.muovi(tasti, pallina.y)
        pallina.muovi()
        
        # Gestiamo le collisioni con le racchette
        pallina.controlla_collisione(racchetta_sinistra)
        pallina.controlla_collisione(racchetta_destra)
        
        # Punti segnati
        if pallina.x < 0:
            punteggio_destro += 1
            pallina.reset()
        elif pallina.x > LARGHEZZA:
            punteggio_sinistro += 1
            pallina.reset()
            
        # 3. Disegniamo lo schermo (Rendering)
        schermo.fill(NERO)
        
        # Disegniamo la linea di mezzeria tratteggiata
        for y in range(0, ALTEZZA, 30):
            pygame.draw.rect(schermo, BIANCO, pygame.Rect(LARGHEZZA // 2 - 2, y, 4, 15))
            
        # Disegniamo gli oggetti
        racchetta_sinistra.disegna(schermo)
        racchetta_destra.disegna(schermo)
        pallina.disegna(schermo)
        
        # Disegniamo i punteggi (Sfida 3)
        if font:
            testo_sx = font.render(str(punteggio_sinistro), True, BIANCO)
            testo_dx = font.render(str(punteggio_destro), True, BIANCO)
            schermo.blit(testo_sx, (LARGHEZZA // 4 - testo_sx.get_width() // 2, 20))
            schermo.blit(testo_dx, (3 * LARGHEZZA // 4 - testo_dx.get_width() // 2, 20))
            
        pygame.display.flip()
        orologio.tick(60)

if __name__ == "__main__":
    main()

Analisi del Codice Completo

Analizziamo come la programmazione orientata agli oggetti ha strutturato il nostro codice:

1. Metodi specifici per comportamento

Le azioni come muovi() e disegna() non sono più sparpagliate all'interno del ciclo di gioco principale. Esse appartengono direttamente a Racchetta e Pallina. Il ciclo di gioco principale si limita a chiamare pallina.muovi() e racchetta_sinistra.muovi(tasti), rendendo il codice leggibile ed estensibile.

2. Calcolo della collisione circolare-rettangolare

Nel metodo controlla_collisione della classe Pallina, usiamo la matematica geometrica: troviamo il punto del rettangolo della racchetta più vicino alla sfera, ne calcoliamo la distanza e, se inferiore al raggio della sfera, applichiamo un rimbalzo invertendo vel_x.

3. Il costruttore __init__ per la flessibilità

Grazie ai parametri del costruttore, possiamo istanziare due racchette identiche nel comportamento ma posizionate in lati opposti e comandate da tasti diversi semplicemente passando argomenti differenti alla chiamata (es: pygame.K_w vs pygame.K_UP).


Curiosità: Home Pong

Lo sapevi che Pong ha ispirato la nascita della prima console per videogiochi domestica di successo? Si chiamava Home Pong (1975), si collegava direttamente alla televisione e aveva due manopole rotanti integrate nella scocca di plastica per controllare le racchette!


Provaci tu! — Pallina impazzita

Modifica il metodo reset() della classe Pallina in modo che a ogni punto segnato la pallina venga sparata a caso con una inclinazione e velocità verticale leggermente diversa, rendendo ogni inizio di partita imprevedibile!


Il Piccolo Glossario

| Termine | Definizione | | --------- | ------------- | | OOP (Object-Oriented Programming) | Paradigma di programmazione basato sull'uso di classi e oggetti per strutturare il codice. | | Class / Classe | Il modello teorico di base (lo stampo) che definisce come sarà strutturato un oggetto. | | Instance / Istanza | Sinonimo di oggetto reale, creato partendo da una classe. | | __init__ | Il costruttore standard di Python, usato per definire le proprietà iniziali degli oggetti. | | Method / Metodo | Una funzione dichiarata dentro una classe che opera direttamente sulle proprietà dell'oggetto. | | self | La parola chiave che Python usa per fare riferimento a se stesso e all'oggetto corrente. |


Prossimo capitolo

Appendice A: Setup Thonny e VS Code
Installazione passo-passo Windows/Mac/Linux, Thonny per principianti (Cap 1-6), VS Code per pygame (Cap 7+), estensioni consigliate, troubleshooting comune (PATH, permissions, antivirus).


Questo post è un estratto dal libro "Divento Programmatore: Python e i Giochi" (Volume 2 della collana "Cervello Elettronico") di Rocco Balzamà. Target: 10-14 anni · ~180 pagine · Thonny + VS Code.

Per approfondire

Fonti e riferimenti

Condividi LinkedIn Facebook X WhatsApp
Continua a leggere

Tutti gli articoli del blog

Strumenti gratuiti