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')
Riepilogo
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 →