Física i interacció
En aquest post comparteixo els reptes i aprenentatges que he tingut a l’hora d’implementar la física i les interaccions en el meu primer videojoc utilitzant Unreal Engine 5. A través de diverses proves, errors i ajustos, explico com vaig solucionar els problemes de col·lisions i com vaig arribar a una solució provisional fent servir només blueprints i evitant, per ara, utilitzar C++ per aconseguir la jugabilitat arcade que busco.
Marc Camps
10/11/20249 min read
Com ja sabeu, aquest és el meu primer videojoc i, a més, sóc un novell en Unreal Engine. A aquestes alçades he après força i penso que és la millor opció per al meu joc, però no totes les solucions són tan evidents com m’agradaria.
Abans de plantejar-me si tots els objectes interactuaven correctament a nivell físic, vaig crear un “character” i vaig començar a programar la jugabilitat de la recollida d’escombraries. Ho vaig fer primer perquè em semblava una tasca complexa de desenvolupar, ja que implica l’ús de “inverse kinematics”. Tot i això, no va ser així.
Ara, vist en perspectiva, crec que hauria d’haver començat per analitzar com interactuen els diferents elements del joc pel que fa a física. Quan vaig crear el primer NPC, el vaig basar en un “Pawn” i va ser llavors quan em vaig adonar que no podia interactuar correctament amb el jugador (character) quan col·lidien o amb altres elements flotants. Aquí és on va començar la meva confusió, ja que no havia après certs conceptes bàsics sobre “characters”, “pawns” i les seves interaccions físiques. A més, vaig començar a crear el jugador amb un “character” de manera una mica arbitrària, cosa que va augmentar la meva desconcert.
Esperava que les físiques reaccionessin com s’espera simplement “perquè els elements estan en un món físic”.
He passat força temps encallat amb aquest tema. He recorregut a fòrums, webs, vídeos de YouTube, cursos, experiments i he experimentat pèrdues severes de la meva sanitat fins que finalment ho vaig entendre. Ara, he trobat una solució que, per ara, em serveix per al meu prototip.
Abans d’entrar en detalls, posem-nos en context per entendre el problema i, posteriorment, la solució que he aplicat.
Jugabilitat desitjada
Aquest és un joc de naus amb vista isomètrica en el qual l’objectiu és netejar l’òrbita terrestre de deixalles espacials mentre esquives i et defenses d’atacs enemics, meteorits i altres esdeveniments.
Per tant, és necessari que el jugador i els NPC puguin col·lidir entre ells o contra qualsevol objecte inert que estigui flotant a l’espai. El resultat d’aquestes col·lisions ha de fer que ambdós objectes reaccionin empenyent-se mútuament, complint amb el principi d’"acció-reacció".
No ha de ser una simulació perfecta, ja que seria un joc massa difícil, així que he simplificat algunes coses. Per exemple, els moviments estan limitats al pla XY i la física no actua en tres dimensions. A més, busco un control senzill que s’allunyi de la simulació realista.
Tot i això, hi haurà moltes col·lisions, per tant, la física és clau i ha de funcionar bé.
Problemes trobats amb la física
Primer, vegem els diferents elements que ofereix Unreal per crear el joc.
Blueprints basats en “Character”
Aquests blueprints ofereixen moltes funcionalitats i aparentment podrien ser una bona opció per dos motius:
Permeten empènyer objectes físics de l’escenari.
Pots configurar fàcilment el moviment del jugador mitjançant el “Character Movement Component”.
Tot i això, vaig acabar descartant aquesta opció perquè, encara que el “Character” té moltes funcionalitats, la majoria no em són útils per al meu joc:
Requereix una “Skeletal mesh” i no tots els NPC la necessiten. Podria amagar-la, però per què tenir un objecte que no s’usa?
Obliga a tenir una col·lisió en forma de càpsula vertical, mentre que els meus objectes solen ser naus allargades horitzontalment. Activar la col·lisió per polígons no és una opció òptima a causa del cost computacional elevat.
Vaig intentar fer servir subobjectes per aconseguir una col·lisió més semblant a la forma de la nau, però no va funcionar bé, ja que es requereix que l’objecte de col·lisió sigui el “root” del blueprint. A més, el “Character Movement Component” atura la nau segons la col·lisió principal.
El “Character Movement Component” té massa opcions i complexitat per al que necessito.
He intentat fer coses molt estranyes amb subobjectes tractant d’aconseguir una col·lisió personalitzada, però només he aconseguit resultats força imprevisibles… és millor no forçar el “character” i fer-lo servir per al que serveix: per exemple, crear un personatge humanoide vertical en un joc d’acció. Definitivament no em serveix.
Blueprints basats en “Pawn”
Els blueprints “Pawn” són molt més simples que els “Character” i, per tant, molt més personalitzables. Com que estan pràcticament buits en crear-los, ofereixen molt de marge per configurar el necessari. A més, igual que amb el “Character”, es pot configurar el controlador que pren el control del “Pawn”, ja sigui una IA o un jugador.
Vaig crear un “pawn”, li vaig assignar una “skeletal mesh”, una col·lisió adequada, el component FloatingPawnMovement i la càmera.
Després, vaig dibuixar algunes línies en l’Event Graph perquè es mogués, però quan el “pawn” col·lisiona amb un objecte flotant…
Boom, és com si xoqués contra una paret de formigó armat.
El moviment del “Pawn” funciona de manera similar al del “Character”, basant-se en les col·lisions amb l’objecte “root” del blueprint. Tot i això, no sembla tenir la capacitat d’empènyer mitjançant la física altres objectes, ja que el component de moviment és més simple.
Vaig intentar programar manualment l’empenta en cada col·lisió aplicant “AddImpulse” a l’objecte flotant, però em portava molt de temps, així que vaig començar a buscar una opció intermèdia entre fer servir un “Pawn” gairebé buit i un “Character” massa complex.
Moviment del “Pawn” basat en física
Si activo l’opció “Simulate Physics” en el “root” del blueprint, es pot programar un sistema basat en físiques per moure tant el jugador com els NPC. En aquest cas, quan col·lisionen contra un objecte físic, l’empenyen de manera normal.
El problema amb aquest sistema és que genera una jugabilitat massa complexa, allunyant-se de l’estil arcade que vull per al joc. A més, el temps que estalviaria programant com s’empenyen els objectes amb cada col·lisió l’hauria d’invertir desenvolupant el moviment del “Pawn”.
La solució correcta: controlador de moviment propi per al “Pawn”
Segons el que he vist en fòrums i documentació, la millor solució per al meu cas sembla ser modificar algun component de moviment existent per als “Pawn” i afegir-hi la funcionalitat física que té el “Character”. Això implicaria utilitzar C++ i aprofundir en el codi d’Unreal Engine.
Crec que acabaré fent això, però abans vull assegurar-me que l’esforç valdrà la pena. La meva prioritat ara és comprovar si el projecte és viable amb un prototip de la forma més senzilla possible, així que, de moment, he descartat aquesta opció.
També cal destacar que és possible crear un component de moviment des de zero, cosa que ofereix infinites possibilitats, encara que això també implicaria un esforç més gran.
La meva solució estranya per al prototip
La idea general per al meu prototip consisteix a crear blueprints de tipus “Actor”, que anomenarem “physics colliders”, que embolcallaran els blueprints de tipus “Pawn”, tant per als NPC com per al jugador. L’“Actor” s’encarregarà de les col·lisions amb objectes físics, mentre que el “Pawn” tindrà la seva pròpia col·lisió per evitar travessar murs o altres “Pawns”.
Els “physics colliders”
Com que sempre intento aprofitar al màxim el codi que desenvolupo, faig servir herència en tot el que faig, i aquesta no és una excepció.
Primer, creo un blueprint de tipus “Actor” que anomeno “BP_Base_PhysicsCollision”. Aquest és el que embolcallarà el “Pawn” i s’encarregarà de les col·lisions que impliquin empènyer altres objectes físics, com les escombraries espacials.
És important recordar que les escombraries espacials seran actors força simples, amb l’opció “Simulate Physics” activada en el seu objecte “root”.
Perquè això funcioni, tant el “pawn” del jugador com els NPC han de ser capaços de generar i moure el seu “physics collider”. Per fer això, he implementat en “BP_Base_PhysicsCollision” un esdeveniment públic que assigna la transformació del “Pawn” (posició, rotació i escala), i com que aquest blueprint és la base, estarà present en tots els blueprints que en derivin.
El “Pawn” generarà aquest actor a la seva mateixa posició quan comenci el joc (esdeveniment “Begin Play”) i després, en cada “Tick”, actualitzarà la seva posició i rotació mitjançant “SetTransform”.
“BP_Base_PhysicsCollision” serà el blueprint base per als diferents tipus de col·lisió que tindrà cada pawn. Per exemple, per al jugador, tindré “BP_Player_PhysicsCollision”, que heretarà de la base “BP_Base_PhysicsCollision” i contindrà els objectes de col·lisió necessaris.
De la mateixa manera, cada NPC tindrà el seu propi “physics collision” basat en la mateixa estructura:
Jugador => BP_Player => BP_Player_PhysicsCollider
NPC => BP_NPC_Satellite => BP_NPC_Satellite_PhysicsCollider
NPC => BP_NPC_Enemy01 => BP_NPC_Enemy01_PhysicsCollider
Dins de cada “physics collision”, col·locarem els objectes de col·lisió que més s'adaptin, ja sigui una “static mesh” o una o més formes de col·lisió. Sempre cal tenir en compte que a més complexitat de col·lisió, més potència de càlcul serà necessària, així que no convé excedir-se amb col·lisions excessivament complexes.
Els “pawns”
Tots els “Pawns” han de gestionar el seu “physics collision”, que constitueix un comportament comú entre ells, independentment de si es tracta del jugador o d’un NPC. Per aquest motiu, he creat el blueprint base “BP_Base_PhysicsPawn”, del qual deriven tant el pawn del jugador com els dels NPC.
Aquest blueprint genera el seu “physics collision” en la mateixa posició i rotació que el pawn quan comença el joc (esdeveniment “Begin Play”). Després, en cada esdeveniment “Tick”, actualitza la posició i la rotació del “physics collision” gràcies a la lògica comuna implementada al blueprint base.
A més, conté el “Floating Pawn Movement Component” i una “static mesh” anomenada “Pawn Collision”, que són comuns per a tots els pawns.
Aquí, “PawnCollision” serà l’encarregada d’evitar que el pawn travessi murs o altres pawns, però no serà l’encarregada de les col·lisions amb objectes flotants, que són responsabilitat del “PhysicsCollision”.
S’estableixen unes variables públiques de configuració, com la classe de “PhysicsCollision” que es farà servir per detectar col·lisions amb objectes físics, i quina “static mesh” volem utilitzar per detectar col·lisions amb altres “pawns” i murs. La “static mesh” ha de ser una versió molt simplificada de la mesh utilitzada per determinar l’aspecte del pawn.
A partir d’aquí, tinc el “BP_Player”, que és el jugador, on s’implementa la lògica de moviment a través dels controls. Aquest blueprint hereta de “BP_Base_PhysicsPawn” i és important que es cridin els esdeveniments “Begin Play” i “Tick” del blueprint base per tal d'executar la lògica comuna.
A l'apartat de “Class Defaults” es pot seleccionar la mesh de col·lisió del pawn i la classe que s'utilitzarà per al “Physics Collision”.
Els canals de col·lisió
Arribats a aquest punt, poso tot en marxa i... no funciona. El “BP_Player” hauria de moure’s, però no ho fa. Això passa perquè, en tenir un objecte dins d’un altre, hi ha un conflicte de col·lisions.
Per solucionar-ho, vaig afegir a la configuració del projecte un nou preset de col·lisions anomenat “ExceptPhysicsCollision”. Aquest preset bloqueja totes les col·lisions excepte les del tipus “Physic Body” i assigna el tipus “Pawn” a l’objecte on s’aplica. D’altra banda, a totes les col·lisions dels actors “Physics Collision” els he aplicat el tipus “Physics Body” mitjançant el preset “Physics Actor”.
D’aquesta manera, els pawns ignoren els “physics body” que formen el blueprint que els envolta, permetent el seu moviment.
Conclusió
Després de molt experimentar, aquesta és la solució que he implementat per al meu prototip, que m’ha permès avançar en el desenvolupament sense complicar-me massa. La idea és, més endavant, aprofundir en la creació d’un component de moviment propi en C++ per al joc definitiu. De moment, aquesta solució basada en blueprints és més que suficient per validar la jugabilitat i les físiques que busco.