You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

12 KiB

Capitolo 5: Architettura del Codice

Panoramica della Struttura

Il progetto è organizzato in modo modulare con separazione delle responsabilità:

shader/
├── config/
│   └── settings.py          # Configurazione centralizzata
├── src/
│   ├── app.py              # Applicazione principale
│   ├── camera/
│   │   └── camera.py       # Sistema camera
│   ├── terrain/
│   │   └── generator.py    # Generazione terreno
│   └── rendering/
│       └── terrain_renderer.py  # Rendering OpenGL
└── main.py                 # Entry point

Principi di Design

Separation of Concerns

Ogni modulo ha una responsabilità specifica:

  • Config: Impostazioni
  • Camera: Vista 3D
  • Generator: Creazione terreno
  • Renderer: Visualizzazione
  • App: Coordinamento

Single Responsibility

Ogni classe fa una cosa e la fa bene.

Dependency Injection

Le classi ricevono dipendenze tramite parametri del costruttore.

Modulo: config/settings.py

Contiene tutte le costanti configurabili:

# Finestra
WINDOW_WIDTH = 1200
WINDOW_HEIGHT = 800
FPS = 60

# Terreno
TERRAIN = {
    'grid_size': 20,
    'tile_width': 30.0,
    'noise_scale': 8.0,
    'octaves': 4,
    ...
}

# Camera
CAMERA = {
    'initial_distance': 800.0,
    'zoom_speed': 10.0,
    ...
}

# Rendering
RENDERING = {
    'grid_line_width': 5.0,
    'background_color': (0.53, 0.81, 0.92, 1.0),
    ...
}

# Biomi
BIOME_COLORS = {...}
BIOME_THRESHOLDS = {...}

Vantaggi:

  • Tutto in un posto
  • Facile da modificare
  • Nessun valore "magico" nel codice

Modulo: src/terrain/generator.py

Classe TerrainGenerator

Responsabilità: Generare heightmap usando Perlin Noise

class TerrainGenerator:
    def __init__(self, config):
        # Carica parametri da config
        self.grid_size = config['grid_size']
        self.scale = config['noise_scale']
        self.octaves = config['noise_octaves']
        ...
    
    def generate(self):
        """Genera e restituisce heightmap 2D"""
        heightmap = np.zeros((self.grid_size, self.grid_size))
        
        for i in range(self.grid_size):
            for j in range(self.grid_size):
                height = noise.pnoise2(...)
                heightmap[i][j] = (height + 0.5) * self.height_multiplier
        
        return heightmap
    
    def _smooth_terrain(self, heightmap):
        """Applica smoothing (opzionale)"""
        # Media dei vicini
        ...

Input: Configurazione Output: Array 2D con altezze Dipendenze: NumPy, noise library

Modulo: src/rendering/terrain_renderer.py

Classe TerrainRenderer

Responsabilità: Disegnare il terreno con OpenGL

class TerrainRenderer:
    def __init__(self, terrain_config, rendering_config, 
                 biome_colors, biome_thresholds):
        self.tile_width = terrain_config['tile_width']
        self.colors = biome_colors
        self.thresholds = biome_thresholds
        ...
        self.heightmap = None
    
    def set_heightmap(self, heightmap):
        """Imposta heightmap da renderizzare"""
        self.heightmap = heightmap
    
    def render(self):
        """Disegna tutto il terreno"""
        self._render_tiles()
        self._draw_grid()
    
    def draw_tile(self, x, z, h1, h2, h3, h4):
        """Disegna una singola tile"""
        # Faccia superiore
        glBegin(GL_QUADS)
        glColor3f(*color)
        glVertex3f(...)
        glEnd()
        
        # Facce laterali con shading
        ...
    
    def get_color_for_height(self, height):
        """Mappa altezza → bioma → colore"""
        if height < self.thresholds['water']:
            return self.colors['water']
        ...

Input: Heightmap, configurazione Output: Draw calls OpenGL Dipendenze: OpenGL

Modulo: src/camera/camera.py

Classe Camera

Responsabilità: Gestire vista 3D e input utente

class Camera:
    def __init__(self, config):
        self.distance = config['initial_distance']
        self.height = config['initial_height']
        self.zoom_speed = config['zoom_speed']
        ...
    
    def handle_input(self, keys):
        """Processa input tastiera"""
        if keys[pygame.K_UP]:
            self.distance -= self.zoom_speed
        ...
    
    def apply(self, aspect_ratio):
        """Applica trasformazioni camera"""
        self.setup_projection(aspect_ratio)
        self.setup_modelview()
    
    def setup_projection(self, aspect_ratio):
        """Imposta matrice proiezione"""
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(self.fov, aspect_ratio, ...)
    
    def setup_modelview(self):
        """Imposta matrice vista"""
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        gluLookAt(...)

Input: Tasti premuti Output: Matrici OpenGL Dipendenze: OpenGL, Pygame

Modulo: src/app.py

Classe IsometricTerrainApp

Responsabilità: Coordinare tutti i componenti

class IsometricTerrainApp:
    def __init__(self, config):
        # Inizializza Pygame e OpenGL
        pygame.init()
        self.display = (config.WINDOW_WIDTH, config.WINDOW_HEIGHT)
        pygame.display.set_mode(self.display, DOUBLEBUF | OPENGL)
        
        # Crea componenti
        self.camera = Camera(config.CAMERA)
        self.terrain_generator = TerrainGenerator(config.TERRAIN)
        self.renderer = TerrainRenderer(...)
        
        # Setup OpenGL
        self._setup_opengl()
        
        # Genera terreno
        heightmap = self.terrain_generator.generate()
        self.renderer.set_heightmap(heightmap)
        
        self.clock = pygame.time.Clock()
    
    def _setup_opengl(self):
        """Configura stati OpenGL"""
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_LIGHTING)
        ...
    
    def run(self):
        """Loop principale applicazione"""
        self.running = True
        while self.running:
            self.handle_events()
            self.update()
            self.render()
            self.clock.tick(self.config.FPS)
    
    def handle_events(self):
        """Gestisce eventi Pygame"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    self._regenerate_terrain()
    
    def update(self):
        """Aggiorna stato"""
        keys = pygame.key.get_pressed()
        self.camera.handle_input(keys)
    
    def render(self):
        """Renderizza scena"""
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(*self.config.RENDERING['background_color'])
        
        aspect_ratio = self.display[0] / self.display[1]
        self.camera.apply(aspect_ratio)
        
        self.renderer.render()
        
        pygame.display.flip()

Modulo: main.py

Entry point dell'applicazione:

#!/usr/bin/env python3
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from config import settings
from src.app import IsometricTerrainApp

def main():
    try:
        app = IsometricTerrainApp(settings)
        app.run()
    except KeyboardInterrupt:
        print("\nExiting...")
        sys.exit(0)

if __name__ == "__main__":
    main()

Flusso dell'Applicazione

1. AVVIO
   main.py
   └─→ Importa settings
   └─→ Crea IsometricTerrainApp

2. INIZIALIZZAZIONE
   IsometricTerrainApp.__init__()
   ├─→ Crea finestra Pygame/OpenGL
   ├─→ Crea Camera
   ├─→ Crea TerrainGenerator
   │   └─→ Genera heightmap
   ├─→ Crea TerrainRenderer
   │   └─→ Riceve heightmap
   └─→ Setup stati OpenGL

3. LOOP PRINCIPALE (60 FPS)
   app.run()
   │
   ├─→ handle_events()
   │   └─→ ESC/R/Close window
   │
   ├─→ update()
   │   └─→ camera.handle_input(keys)
   │       └─→ Aggiorna distance/height
   │
   ├─→ render()
   │   ├─→ Pulisci buffer
   │   ├─→ camera.apply()
   │   │   ├─→ setup_projection()
   │   │   └─→ setup_modelview()
   │   ├─→ renderer.render()
   │   │   ├─→ Disegna tile
   │   │   └─→ Disegna grid
   │   └─→ pygame.display.flip()
   │
   └─→ clock.tick(60)  # Limita a 60 FPS

4. CHIUSURA
   pygame.quit()

Pattern di Design Utilizzati

1. Configuration Object

Tutte le impostazioni in un oggetto centralizzato.

2. Dependency Injection

# Le dipendenze sono passate esplicitamente
camera = Camera(config)
renderer = TerrainRenderer(config, colors, thresholds)

3. Single Responsibility Principle

Ogni classe ha un solo motivo per cambiare.

4. Composition Over Inheritance

App compone Camera, Generator, Renderer invece di ereditare.

5. Encapsulation

Dettagli implementativi nascosti (metodi _private).

Interazioni tra Componenti

┌─────────────────┐
│   main.py       │
└────────┬────────┘
         │ crea
         ↓
┌─────────────────────────┐
│  IsometricTerrainApp    │
├─────────────────────────┤
│ + camera                │←─────┐
│ + terrain_generator     │←─────┼─── Composizione
│ + renderer              │←─────┘
└───────┬─────────────────┘
        │
        ├──→ camera.handle_input(keys)
        ├──→ camera.apply(aspect)
        ├──→ terrain_generator.generate()
        └──→ renderer.render()

┌────────────┐   heightmap   ┌─────────────────┐
│ Generator  │──────────────→│    Renderer     │
└────────────┘                └─────────────────┘

Vantaggi dell'Architettura

Modularità

Ogni componente è indipendente e sostituibile.

Testabilità

Puoi testare Generator senza Renderer o Camera.

Manutenibilità

Modifiche localizzate, nessun effetto a cascata.

Riutilizzabilità

TerrainGenerator può essere usato in altri progetti.

Leggibilità

Codice organizzato logicamente, facile da navigare.

Estensione del Progetto

Aggiungere un Nuovo Bioma

1. Config:

# config/settings.py
BIOME_COLORS['lava'] = (1.0, 0.3, 0.0)
BIOME_THRESHOLDS['lava'] = 85.0

2. Renderer:

# src/rendering/terrain_renderer.py
def get_color_for_height(self, height):
    ...
    elif height < self.thresholds['lava']:
        return self.colors['lava']

Aggiungere Rotazione Camera

1. Config:

# config/settings.py
CAMERA = {
    ...
    'rotation_angle': 0.0,
    'rotation_speed': 2.0,
}

2. Camera:

# src/camera/camera.py
def handle_input(self, keys):
    ...
    if keys[pygame.K_q]:
        self.rotation_angle += self.rotation_speed
    if keys[pygame.K_e]:
        self.rotation_angle -= self.rotation_speed

Salvare/Caricare Terreni

1. Nuovo modulo:

# src/terrain/serializer.py
class TerrainSerializer:
    @staticmethod
    def save(heightmap, filename):
        np.save(filename, heightmap)
    
    @staticmethod
    def load(filename):
        return np.load(filename)

2. App:

# src/app.py
def handle_events(self):
    ...
    elif event.key == pygame.K_s:
        TerrainSerializer.save(self.renderer.heightmap, 'terrain.npy')

Struttura

  • config: Configurazione
  • src/camera: Sistema vista
  • src/terrain: Generazione
  • src/rendering: Visualizzazione
  • src/app: Coordinamento

Componenti Principali

  • TerrainGenerator: Crea heightmap
  • TerrainRenderer: Disegna mesh
  • Camera: Gestisce vista
  • IsometricTerrainApp: Orchestra tutto

Flusso

Init → Loop (Events → Update → Render) → Quit


Prossimo Capitolo: Il Sistema di Rendering →

← Capitolo Precedente | Torna all'indice