[#_lesson_24_physics] == Lezione 24: Fisica image::learn/24-physics.png[La lezione finita, 480] Fino a ora ogni cosa che si muoveva nei tuoi giochi si muoveva perché i conti li facevi tu. Un numero saliva di due a ogni fotogramma; se superava un bordo lo facevi tornare indietro. È il modo onesto di far muovere un gioco, e la maggior parte dei giochi è fatta così. Questa lezione passa i conti a qualcun altro. Un *motore fisico* simula il mondo -- la gravità, le cadute, le scivolate, le pile, i rimbalzi, i rotolamenti e le cose che si urtano -- e muove i tuoi nodi al posto tuo. Alla fine avrai un vassoio di casse e un cannone che le butta giù. L'ultima sezione parla di quando non fare niente di tutto questo, ed è la parte da leggere due volte. [#_what_it_does_and_what_it_does_not] === Che cosa fa, e che cosa non fa Dai a un nodo un *corpo* e la simulazione se ne prende carico. Da quel momento cade per gravità, atterra su qualunque cosa ci sia sotto, si inclina quando viene colpito fuori centro, resta fermo in una pila, scivola fino a fermarsi, e il nodo segue ovunque la simulazione lo metta. Smetti di impostarne la posizione. È questo il patto: cedi la posizione e la rotazione di quel nodo, e in cambio ottieni un comportamento che non hai dovuto scrivere. Quello che non fa è qualsiasi cosa che riguardi il tuo gioco. Non sa che cos'è un punteggio, né che la cassa rossa è quella che vale punti, né che cadere dal bordo costa una vita. Non anima un personaggio che cammina, non sa che sensazione dovrebbe dare un salto, e non farà muovere un oggetto in un modo che puoi prevedere esattamente. La fisica è una macchina che produce un caos plausibile. Le unità sono metri, chilogrammi e secondi. La gravità è `0, -9.81, 0` a meno che tu non dica altrimenti, cioè quella della Terra, verso il basso, in metri al secondo quadrato. Una cassa larga un'unità è larga un metro, e cade come tale. Se la tua scena è costruita a un'altra scala, tutto sembrerà cadere nello sciroppo o in un cartone animato, e la soluzione è costruire in metri. [#_a_floor_and_one_crate] === Un pavimento e una cassa Crea una cartella, crea `crates.singe` e scrivi questo. È una scena, una lastra, una cassa sopra la lastra e tre righe di fisica. [source,lua] ---- sceneEnable(true) sceneSetBackground(16, 18, 26) sceneSetAmbient(40, 42, 50) local stone = materialNew() materialSetColor(stone, 110, 110, 120) materialSetRoughness(stone, 0.9) local wood = materialNew() materialSetColor(wood, 190, 140, 80) materialSetRoughness(wood, 0.8) local floor = nodeNew() nodeSetMesh(floor, meshBox(16, 0.4, 16), stone) nodeSetPosition(floor, 0, -0.2, 0) bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16) local crate = nodeNew() nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), wood) nodeSetPosition(crate, 0.3, 4, 0) nodeSetRotation(crate, 20, 0, 15) bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8) bodySetMass(crate, 4) local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, -5, 9, 6) nodeLookAt(sun, 0, 0, 0) lightSetIntensity(sun, 1.7) lightSetShadow(sun, true) local camera = nodeNew() nodeSetPosition(camera, 0, 4, 11) nodeLookAt(camera, 0, 1, 0) cameraSet(camera) function onOverlayUpdate() return OVERLAY_UPDATED end ---- Eseguilo con `Singe -R crates`. La cassa cade da quattro metri d'altezza, atterra su uno spigolo perché l'hai inclinata, si rovescia su una faccia e si ferma. Premi *F5* per eseguirlo di nuovo e guardala un'altra volta. Niente in `onOverlayUpdate` muove qualcosa. Hanno fatto tutto le due chiamate a `bodyNew`. [#_what_just_happened_20] === Cos'è appena successo [source,lua] ---- bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16) ---- `bodyNew` dà un corpo a un nodo. Prende il nodo, che tipo di corpo è, che forma deve usare la simulazione per lui, e le dimensioni di quella forma. Ci sono tre tipi, e scegliere quello giusto è gran parte dell'abilità. * `BODY_STATIC` non si muove mai. Viene messo dove si trova il nodo quando crei il corpo, e lì resta per sempre. Pavimenti, muri, il terreno, il cabinato, il livello. Spostare il suo nodo dopo non sposta il corpo, ed è una trappola che vale la pena ricordare: statico vuol dire statico. * `BODY_DYNAMIC` viene mosso dalla simulazione. Cade, rimbalza, scivola, viene spinto, e si trascina dietro il suo nodo. Questo è una cassa, un barile, una palla, un pezzo di detrito. * `BODY_KINEMATIC` va dove metti *tu* il suo nodo, e spinge via i corpi dinamici senza mai essere spinto a sua volta. Una piattaforma mobile, una racchetta, una porta su un binario, il pugno che un personaggio fa partire. È il ponte tra i tuoi conti e la simulazione: tu guidi lui, lui guida loro. La forma non è la mesh. È un sostituto approssimativo che la simulazione usa perché sa rispondere a domande su una scatola molto più in fretta che su novecento triangoli. `SHAPE_BOX` prende una larghezza, un'altezza e una profondità; `SHAPE_SPHERE` prende un raggio; `SHAPE_CAPSULE` e `SHAPE_CYLINDER` prendono un raggio e un'altezza. C'è `SHAPE_HULL`, che avvolge come una pellicola qualunque mesh ci sia sotto il nodo -- la risposta solita per un modello -- e `SHAPE_MESH`, i triangoli esatti, che lo scenario statico può usare e un oggetto che cade no. Le dimensioni tocca a te azzeccarle. Niente controlla che il corpo che hai descritto corrisponda alla mesh che hai disegnato, e una cassa disegnata a `0.8` con un corpo fatto a `2` fluttuerà sopra il pavimento e farà cadere le cose a distanza. Quando qualcosa si comporta come se avesse un'altra dimensione, ce l'ha. [source,lua] ---- bodySetMass(crate, 4) ---- La massa di un corpo dinamico viene dalla sua forma alla densità dell'acqua, il che rende le cose molto più pesanti di quanto ti aspetti: una scatola grande quanto una porta pesa circa centoquaranta chilogrammi, e niente di quello che puoi lanciarle contro la sposterà. Imposta la massa di qualsiasi cosa debba sembrare leggera. Quattro chilogrammi sono una cassa che potresti sollevare. La massa conta perché le forze vengono ripartite in base a essa. La stessa spinta sposta più lontano una cosa leggera, e un impulso pari a massa per velocità è quello che serve per mettere in moto qualcosa a quella velocità. Regola prima la massa e poi le forze, o finirai per inseguire la tua coda. [#_making_things_move] === Far muovere le cose Tre chiamate spingono in giro un corpo dinamico, e non sono intercambiabili. `bodySetVelocity(node, x, y, z)` dice «adesso stai viaggiando a questa velocità», sostituendo qualunque cosa stesse facendo. È un servizio, un lancio, una comparsa con già un po' di velocità addosso. `bodyApplyImpulse(node, ix, iy, iz)` è un colpo: un calcio, un'esplosione, una mazza che incontra una palla. Si aggiunge a quello che il corpo stava già facendo. Dagli un punto del mondo come altri tre numeri e colpisce *lì* invece che al centro, quindi una cassa colpita vicino alla cima si rovescia invece di scivolare. `bodyApplyForce(node, fx, fy, fz)` è una spinta che dura: un razzo, un ventilatore, un nastro trasportatore. Una forza applicata per un solo fotogramma non fa quasi niente, il che sorprende chi si aspettava un impulso. La gravità stessa è `physicsSetGravity(x, y, z)` e vale per tutto il mondo. La luna è `physicsSetGravity(0, -1.62, 0)`, lo spazio è tutto zeri, e un gioco con la gravità di lato è a una riga di distanza. Altre due impostazioni completano la sensazione di un oggetto. `bodySetBounce(node, amount)` va da `0`, che si ferma di colpo, a `1`, che torna indietro con tutto quello con cui è arrivato; il valore predefinito è un `0.1` appena percettibile. `bodySetFriction(node, amount)` è `0` per il ghiaccio e `0.5` per impostazione predefinita. Entrambe si combinano tra le due cose che si toccano, quindi un pavimento ghiacciato vuole il numero basso sul pavimento, non su tutto quello che ci cammina sopra. [#_knowing_when_things_touch] === Sapere quando le cose si toccano [source,lua] ---- function onCollision(nodeA, nodeB, x, y, z, speed) if speed > 4 then knocks = knocks + 1 end end ---- Lì `knocks` è una variabile tua, che aspetta in cima al file con `local knocks = 0`, e il giocattolo qui sotto la stampa. `onCollision` è una callback, come `onOverlayUpdate`: la scrivi tu, il motore la chiama. Arriva una volta per ogni nuovo contatto -- due corpi che restano a contatto non lo segnalano di nuovo -- con entrambi i nodi, il punto del mondo in cui si sono incontrati, e la velocità con cui si sono incontrati. La velocità è quello che permette a una botta e a uno schianto di suonare diversi, ed è il motivo per cui il controllo qui sopra ignora il lento assestarsi di una pila. L'altra metà della coppia è `onTrigger`. Rendi un corpo un trigger con `bodySetTrigger(node, true)` e smette di spingere le cose: diventa una zona che segnala che cosa ci entra e che cosa ne esce tramite `onTrigger(trigger, other, entered)`. Un traguardo, una porta, una fossa, un oggetto da raccogliere. È il discendente 3D dei controlli di collisione dell'undicesima lezione, e per qualsiasi cosa che abbia la forma di «il giocatore è arrivato qui?» è la chiamata che vuoi, invece di un corpo solido. [#_the_toy] === Il giocattolo Ora tutto quanto. Parti dallo script qui sopra e trasformalo in questo: un vassoio con quattro pareti, sei casse in piramide, e una palla d'acciaio che puoi puntare e sparare. I pezzi sono tutti cose che hai già incontrato. Tutto quello che il nuovo codice ricorda va in cima al file, sopra la scena: [source,lua] ---- local crates = {} local balls = {} local aimX = 0 local aimLeft = false local aimRight = false local knocks = 0 local showShapes = false ---- Altri due handle condivisi vanno insieme ai materiali: un metallo lucido per la palla e per il segnalino di mira, e una sola mesh di cassa per tutte e sei le casse. [source,lua] ---- local steel = materialNew() materialSetColor(steel, 200, 205, 215) materialSetMetallic(steel, 1) materialSetRoughness(steel, 0.25) local crateMesh = meshBox(0.8, 0.8, 0.8) ---- Le pareti del vassoio sono quattro scatole statiche, fatte in un ciclo a partire da una tabella di posizioni, ed è la sesta lezione che fa un lavoro onesto: [source,lua] ---- local wallMesh = meshBox(16, 1, 0.4) for _, side in ipairs({ { 0, -8, 0 }, { 0, 8, 0 }, { -8, 0, 90 }, { 8, 0, 90 } }) do local wall = nodeNew() nodeSetMesh(wall, wallMesh, stone) nodeSetPosition(wall, side[1], 0.5, side[2]) nodeSetRotation(wall, 0, side[3], 0) bodyNew(wall, BODY_STATIC, SHAPE_BOX, 16, 1, 0.4) end ---- Ogni voce è una X, una Z e una rotazione attorno a Y. Due pareti stanno di traverso dietro e davanti; due sono girate di novanta gradi per fare i lati. Una sola mesh basta per tutte e quattro, e il corpo viene creato dopo che il nodo è stato posizionato e girato, perché un corpo statico prende la posizione e la rotazione del nodo nel momento in cui viene creato. Le casse vengono da una funzione, così sei casse sono sei righe: [source,lua] ---- local function crateAt(x, y) local crate = nodeNew() nodeSetMesh(crate, crateMesh, wood) nodeSetPosition(crate, x, y, -2) bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8) bodySetMass(crate, 4) crates[#crates + 1] = crate end ---- Tiene ogni cassa che crea nella tabella `crates`, ed è così che più tardi la callback di disegno può fare una domanda a tutte. Sei chiamate costruiscono la piramide, tre sul pavimento, due sopra quelle e una in cima: [source,lua] ---- crateAt(-0.9, 0.4) crateAt(0, 0.4) crateAt(0.9, 0.4) crateAt(-0.45, 1.25) crateAt(0.45, 1.25) crateAt(0, 2.1) ---- Le file superiori stanno un dito più in alto di quelle sotto e cadono al loro posto appena il gioco parte. Lasciare un piccolo spazio è più facile che impilare con precisione, ed è più gentile verso la simulazione che consegnarle una pila già schiacciata. Sparare crea ogni volta una palla nuova: [source,lua] ---- local function fire() local ball = nodeNew() nodeSetMesh(ball, meshSphere(0.35, 24), steel) nodeSetPosition(ball, aimX, 1.2, 6) bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35) bodySetMass(ball, 3) bodySetBounce(ball, 0.3) bodySetVelocity(ball, 0, 2, -14) balls[#balls + 1] = ball if #balls > 5 then nodeDelete(balls[1]) table.remove(balls, 1) end end ---- Guarda l'ordine. Il nodo viene creato, riceve una forma da disegnare, viene *posizionato*, e solo allora riceve un corpo. Un corpo dinamico parte da dove si trova il suo nodo, e dopo il traffico va nell'altro senso: è il corpo che muove il nodo. Impostare da te, dopo, la posizione di un nodo dinamico significa combattere la simulazione, e vince la simulazione. Quando qualcosa va spostato, dagli una velocità, colpiscilo con un impulso, o fai quello che fa questa funzione -- creane uno nuovo e lascia andare quello vecchio. Lasciar andare quello vecchio è `nodeDelete`, che libera il nodo, tutto quello che c'è sotto e il corpo che ci viaggiava sopra. Senza quelle tre righe ogni colpo resterebbe nel mondo per sempre, e qualche centinaio di palle dopo la simulazione farebbe un sacco di lavoro su cose che nessuno vede. Fare pulizia fa parte dell'usare la fisica. I comandi e la visualizzazione sono il resto: [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then fire() elseif what == SWITCH_BUTTON3 then showShapes = not showShapes if showShapes then physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS) else physicsSetDebug(DEBUG_NONE) end elseif what == SWITCH_LEFT then aimLeft = true elseif what == SWITCH_RIGHT then aimRight = true end end ---- La barra spaziatrice spara, Maiusc accende e spegne la vista della fisica, e le frecce, tenute premute, fanno scorrere il punto di lancio. Una piastra di metallo piatta senza corpo, spostata a ogni fotogramma in `aimX`, mostra da dove partirà la prossima palla. Lo script completo, con la callback del rilascio e il resto della preparazione, è `24-physics.singe` nella cartella `learn`. `physicsSetDebug` è la chiamata più utile di questa lezione. Disegna quello che la simulazione contiene davvero -- le forme vere in wireframe, e una croce rossa dovunque due corpi si sono toccati in questo passo -- sopra la tua scena. La prima volta che una cassa fluttua un palmo sopra il pavimento, o ci cade attraverso, o fa cadere qualcosa da due metri di distanza, accendila e la risposta sarà sullo schermo. Aggiungi `DEBUG_STATIC` per vedere anche il pavimento e le pareti. Finché è spenta non costa niente. Infine, il conteggio: [source,lua] ---- function onOverlayUpdate() local over = 0 local settled = true if aimLeft then aimX = math.max(aimX - 0.08, -5) end if aimRight then aimX = math.min(aimX + 0.08, 5) end nodeSetPosition(marker, aimX, 0.05, 6) for _, crate in ipairs(crates) do local pitch, _, roll = nodeGetRotation(crate) if math.abs(pitch) > 30 or math.abs(roll) > 30 then over = over + 1 end if not bodyIsResting(crate) then settled = false end end overlayClear() overlayPrint(2, 2, "Arrows aim. Space fires. Shift shows the shapes.") overlayPrint(2, 4, "knocked over " .. over .. " of " .. #crates .. " hard hits " .. knocks) if settled then overlayPrint(2, 6, "everything has settled") end return OVERLAY_UPDATED end ---- Una cassa conta come rovesciata quando è inclinata di più di trenta gradi, che è una regola del gioco scritta in una riga di aritmetica sopra qualunque cosa abbia fatto la simulazione. `nodeGetRotation` dà tre angoli e qui servono il primo e il terzo, quindi quello in mezzo viene raccolto in una variabile chiamata `_`. Quel trattino basso è un nome normale, senza niente di speciale, usato per convenzione per dire «questo lo ignoro». `bodyIsResting` dice se la simulazione ha addormentato un corpo perché ha smesso di muoversi. È così che sai che un tiro è finito: non un timer, non una stima, ma ogni cassa addormentata. Un gioco di bowling conta i punti di ogni turno proprio lì. [#_when_not_to_use_it] === Quando non usarla La fisica è lo strumento più allettante di qualsiasi motore e quello che fa sprecare più fine settimana. Ci sono tre cose vere su di lei, e nessuna è colpa del motore. *È costosa.* Ogni corpo dinamico è lavoro a ogni passo, sessanta volte al secondo, che qualcuno lo guardi o no. Quaranta casse vanno bene. Quattromila no, e su una macchina piccola nemmeno quattrocento. *È imprevedibile.* Esegui il tuo giocattolo due volte e le casse atterrano in modo diverso. È ciò che la fa sembrare viva, ed è anche il motivo per cui un gioco non può contarci per niente che debba venire uguale ogni volta. Una porta che deve aprirsi, una piattaforma che deve arrivare, un salto che il giocatore deve poter fare: quelli sono aritmetica, o un tween, o un corpo cinematico su un percorso che hai scritto tu. Non lasciare mai una cosa che il giocatore deve fare a una simulazione che può farla finire dietro una cassa. *È difficile da regolare.* La massa, l'attrito, il rimbalzo, la forma e la dimensione si tirano l'una con l'altra, e «non sembra giusto» raramente ha una causa sola. Ogni ora che passi a rendere convincente una cassa fisica è un'ora che non hai passato sul gioco. Quindi usala quando il caos *è* il punto: cose che crollano, detriti che volano, una pila che si assesta, una palla che sbatacchia per una stanza, un veicolo che corre su un terreno accidentato, un ragdoll che ruzzola giù per una scala. Nessuno sa scriverlo a mano e nessuno sa dirti esattamente che aspetto dovrebbe avere. Non usarla quando sai che cosa dovrebbe succedere. Un proiettile che vola dritto sono due righe della seconda lezione. Una piattaforma che va su e giù è `math.sin`. Un giocatore che cammina e salta come i giocatori si aspettano vuole un controller del personaggio (`playerNew`, nel riferimento), perché una persona non è una scatola e non si comporta mai come tale. Un nemico che pattuglia vuole un percorso. Un menu che entra scorrendo vuole un tween. La regola che funziona meglio: usa la fisica per le cose che il giocatore guarda, e l'aritmetica per le cose che il giocatore fa. [#_try_it_24] === Prova tu . *Cambia il mondo.* Aggiungi `physicsSetGravity(0, -1.62, 0)` vicino all'inizio e spara di nuovo. Poi prova `0, -30, 0`. Le stesse casse, un altro pianeta, una riga. . *Cambia la palla.* Rendi la palla dieci volte più pesante con `bodySetMass(ball, 30)`, poi dieci volte più leggera con `0.3`, mantenendo la stessa velocità. Poi rimetti la massa com'era e cambia invece il `-14`. Due modi di colpire più forte, e non danno la stessa sensazione. . *Fai le casse di ghiaccio.* Aggiungi `bodySetFriction(crate, 0.02)` in `crateAt`, e aggiungi una riga che dia un attrito basso anche al pavimento. Guarda la pila che si rifiuta di stare in piedi fin dall'inizio. . *Sbaglia apposta la forma.* In `crateAt`, cambia le dimensioni del corpo in `2, 2, 2` lasciando la mesh a `0.8`. Eseguilo, guarda le casse fluttuare e spingersi a vicenda dal nulla, poi premi Maiusc e guarda la vista di debug spiegarlo in una sola immagine. . *Conta i punti come si deve.* Usa `bodyIsResting` su ogni cassa per accorgerti di quando il tiro è finito, e solo allora conta quante sono rovesciate e aggiungilo a un punteggio. Questa è la parte di un gioco di bowling che tiene il punteggio. [#_break_it_on_purpose_23] === Rompilo apposta Aggiungi una riga sotto il `bodyNew` del pavimento per renderlo pesante, che è una cosa ragionevole da pensare di volere: [source,lua] ---- bodySetMass(floor, 200) ---- Singe ferma il gioco con qualcosa del genere: ---- 32:bodySetMass: Node 1 is not a dynamic body, or the mass is not positive. ---- Il primo numero è la riga nel tuo file, quindi il tuo sarà diverso. Il numero dopo `Node` è l'handle del nodo, il numero che ti ha restituito `nodeNew`. La lamentela è esatta: la massa appartiene alle cose che la simulazione muove, e un corpo statico non viene mosso da niente, quindi non ha una massa da impostare. Chiederlo è un segno che il corpo è del tipo sbagliato per quello che avevi in mente -- e se vuoi davvero un pavimento pesante che si possa spostare, quello che vuoi è un corpo dinamico con una massa grande, o uno cinematico che sposti tu. Lo stesso messaggio compare per una massa di `0`, che la gente scrive intendendo «senza peso». Senza peso non è una massa. È un mondo senza gravità, oppure `playerSetGravityScale` su un controller del personaggio. [#_what_you_learned_24] === Cosa hai imparato * Un corpo si attacca a un nodo, e da quel momento il nodo e il corpo sono una cosa sola. * Statico non si muove mai, dinamico viene mosso dalla simulazione, e cinematico viene mosso da te e spinge via tutto il resto. * La forma di collisione è un sostituto della mesh, e tocca a te fare in modo che le sue dimensioni corrispondano a quello che hai disegnato. * La massa predefinita è il volume della forma alla densità dell'acqua, quindi impostala su qualsiasi cosa debba sembrare leggera. * `bodySetVelocity` sostituisce il moto di un corpo, `bodyApplyImpulse` è un colpo, e `bodyApplyForce` è una spinta che dura. * Non impostare la posizione di un nodo dinamico; eliminalo e creane uno nuovo quando devi farlo ripartire da capo. * `onCollision` segnala ogni nuovo contatto con il suo punto e la sua velocità, e un corpo trigger segnala arrivi e partenze tramite `onTrigger`. * `bodyIsResting` ti dice quando tutto si è fermato, che di solito è quando un turno è finito. * `physicsSetDebug` disegna la vista della simulazione stessa, e risponde alla maggior parte delle domande sulla fisica più in fretta che leggere il codice. * La fisica è costosa, irripetibile e pignola. Usala per il caos, non per le cose da cui dipende il gioco. [#_next_time_24] === La prossima volta Le casse che cadono sono un tipo di spettacolo. L'altro tipo è fatto di mille cosine che vivono mezzo secondo: scintille, fumo, fuoco, pioggia e polvere. La prossima lezione parla di emettitori, e finisce con un'esplosione. È la venticinquesima lezione.