Bevægelse med collision detection
Du har nu set eksempler på flere forskellige actions, og i nogle tilfælde har vi endda haft entities til f.eks. at bevæge sig rundt i en cirkelbevægelse. Dette blev gjort ved bare at ændre x-, y- og z-koordinaterne, og du har måske set, at entities med disse actions kan bevæge sig igennem f.eks. mure!
Hvis man vil flytte en entity, eller lade den bevæge sig, uden at den kan gå gennem mure, skal man bruge collision detection, dvs. en metode,
hvor man tjekker om man rammer (kolliderer med) f.eks. en mur. Dette er heldigvis nemt: du skal bare bruge kommandoen c_move
til at flytte din entity!
Vektorer
Inden vi går videre, skal vi lige have styr på vektorer - ikke selve matematikken bagved (du behøver i princippet ikke lære vektorregning først, men det er en fordel at kunne det), men du skal vide hvordan de fungerer i 3DGS.
En vektor er et matematisk objekt, der består af en retning og en længde. Da vi arbejder med 3D-grafik består en vektor af 3 tal, svarende til komponenter langs x-, y- og z-akserne i dit koordinatsystem,
og du kan lave en vektor i 3DGS ved at skrive vector(1,2,3)
- her er tallene 1, 2 og 3 så henholdsvis x-, y- og z-komponenterne.
3DGS indeholder en masse funktioner til at behandle vektorer: de har alle et navn der begynder med vec_
, f.eks. vec_set
, der sætter alle tre komponenter af én vektor,
til samme værdier som de tre komponenter af en anden vektor. vec_length
beregner længden af en vektor, vec_dist
beregner afstanden mellem to punkter, angivet med stedvektorer (dvs. den beregner længden af forskellen mellem to vektorer).
vec_add
lægger to vektorer sammen, vec_sub
trækker to vektorer fra hinanden, vec_scale
ganger alle komponenterne i en vektor med et tal,
og vec_normalize
ændrer længden på en vektor, uden at ændre retningen. Der findes mange flere vektor-funktioner, som du kan læse om i manualen (tryk F1 i SED, gå om under fanen Indeks, og søg på vec_
).
Her er nogle eksempler - bemærk at target
og normal
er to vektorer, der kan bruges til at holde midlertidige resultater (de bliver brugt af forskellige funktioner i 3DGS):
vec_set(my.x, vector(100,250,-50)); // Flytter min position (både x-, y- og z-koordinater) var afstand_til_player = vec_dist(my.x, player.x); // Beregner afstand til player vec_set(target, vector(3,0,0)); vec_add(target, vector(0,4,0)); var afstand_til_nulpunkt = vec_dist(target, nullvector); // Længden er 5 // Du kan også sætte komponenterne enkeltvis target.x = 10; target.z = vec_dist(target, nullvector); vec_set(target, nullvector); // Nulstil vektor
Bemærk at nullvector
er en vektor hvor alle komponenterne er 0 (og du kan ikke ændre nullvector
!).
En anden vigtig ting er, at target
og normal
er globale variabler, dvs. der findes kun én kopi af dem, og ændres de ét sted i koden, vil de også være ændret alle andre steder.
Dette er normalt ikke noget problem, men hvis du sætter dem til en værdi, og derefter bruger en wait
-kommando, kan du ikke regne med, at de bagefter har samme værdi.
c_move
Vi er nu klar til få en entity til at bevæge sig med collision detection: vi skal bruge c_move
-funktionen, som tager fire parametre:
- entity - Den entity der skal bevæges (f.eks.
my
/me
ellerplayer
). - reldist - En vektor der angiver den relative afstand, du vil flytte entity'en.
- absdist - En vektor der angiver den absolutte afstand, du vil flytte entity'en
- mode - Her kan du indstille hvilken form for collision detection du vil have (f.eks.
IGNORE_PASSABLE
ogGLIDE
).
Den første parameter giver nogenlunde sig selv: i en action kan vi bruge my
eller me
for at flytte den entity, som har fået denne action.
Vi springer i første omgang den relative afstand over (dette kan gøre ved at sætte den til nullvector
), og den absolutte afstand angiver afstanden langs x-, y- og z-koordinaterne,
som din entity skal flyttes. Den sidste parameter, mode, kan du læse mere om i manualen (tryk F1 i SED, gå til fanen Indeks, og søg efter c_move).
Her ses et eksempel, som bare flytter din entity afstanden 200 (eller mindre, hvis den f.eks. rammer ind i en mur!):
action flyt_mig() { // Flyt afstanden 100 langs x-aksen UDEN collision-detection vec_add(my.x,vector(100,0,0)); // Flyt afstanden 100 langs x-aksen MED collision-detection c_move(my, nullvector, vector(100,0,0), IGNORE_PASSABLE + GLIDE); }
Men hvad nu, hvis vi har en person, som står og kigger i en bestemt retning, og vil gå i netop denne retning? Vi kan bruge my.pan
og my.tilt
til at finde den retning,
vores entity kigger i, men at bevæge os afstanden 100 i denne retning (i stedet for bare langs x-aksen), er lidt mere kompliceret:
action flyt_mig() { // Flyt afstanden 100 langs den retning vi kigger i UDEN collision-detection vec_add(my.x, vector(100*cos(my.pan)*sin(my.tilt), 100*sin(my.pan)*sin(my.tilt), 100*cos(my.tilt))); // Flyt afstanden 100 langs den retning vi kigger i MED collision-detection c_move(my, nullvector, vector(100*cos(my.pan)*sin(my.tilt), 100*sin(my.pan)*sin(my.tilt), 100*cos(my.tilt)), IGNORE_PASSABLE + GLIDE); }
Det ser lidt mere kompliceret ud, end det burde være... Er der ikke en nemmere måde? Jo! Det er her vi kan bruge den relative afstand i c_move: her kan vi angive hvor meget en entity skal bevæge sig i forhold til den retning den kigger i. I vektoren med den relative afstand angiver x-koordinaten hvor meget der skal flyttes lige ud, y-koordinaten hvor meget der skal flyttes sidelæns, og z-koordinaten hvor meget der skal flyttes op/ned - men alt sammen i forhold til den retning, som din entity kigger i! Ovenstående eksempel kan derfor skrives som:
action flyt_mig() { // Flyt afstanden 100 langs den retning vi kigger i UDEN collision-detection vec_add(my.x, vector(100*cos(my.pan)*sin(my.tilt), 100*sin(my.pan)*sin(my.tilt), 100*cos(my.tilt))); // Flyt afstanden 100 langs den retning vi kigger i MED collision-detection c_move(my, vector(100,0,0), nullvector, IGNORE_PASSABLE + GLIDE); }
Når du bruger c_move
behøver du ikke vælge mellem at bruge relativ eller absolut afstand - du kan sagtens kombinere dem. Jeg bruge ofte x- og y-komponenterne i den relative afstand (med z-komponten sat til 0),
og så bruger jeg z-komponenten i den absolutte afstand til f.eks. at "lave tyngdekraft" (med x- og y-komponenter sat til 0). Bemærk dog at det ofte er bedst at bruge c_trace
til at se, hvornår man skal "lave tyngdekraft",
og til sidst i dette afsnit kommer vi ind på dette.
Vi kan nu prøve at lave en entity, der følger efter player:
action gidsel() { while(player == NULL) {wait(1);} while(1) { // Find en vektor, der peger fra mig mod player vec_set(target, player.x); vec_sub(target, my.x); // Drej mig, så jeg kigger i vektorens retning (mod player) vec_to_angle(my.pan, target); // Sørg for at jeg ikke kommer til at kigge opad eller nedad (det ser dumt ud) my.tilt = 0; // Hvis jeg er hverken for tæt på eller langt fra player if((vec_dist(my.x,player.x) > 120) && (vec_dist(my.x,player.x) < 400)) { // Bevæg mig ligeud (dvs. hen imod player, som jeg jo kigger på) // Husk også lidt tyngdekraft c_move(my, vector(5 * time_step,0,0), vector(0,0,-10 * time_step), IGNORE_PASSABLE + GLIDE); } else { // Vi skal huske tyngdekraften selvom vi ikke bevæger os mod player c_move(my, nullvector, vector(0,0,-10 * time_step), IGNORE_PASSABLE + GLIDE); } wait(1); } }
Som navnet angiver, kan denne action f.eks. bruges til et gidsel, der skal reddes.
Animationer
En entity med vores action gidsel ovenfor ser lidt forkert ud - han står helt stille, og "glider" henover gulvet. Det må vi gøre noget ved!
Første skridt er at finde ud af, hvilke animationer vores model har: start derfor med at åbne din model i MED (jeg har valgt player.mdl):
Tryk på knappen Animate, vist på billedet ovenfor, og du kan nu se de forskellige animationer vha. den slider der er ved siden af Animate-knappen. Ved siden af slideren står navnet på de enkelte frames i animationerne. player.mdl har en stand-animation på 7 frames, walk på 4 frames, run på 4 frames, osv.
Vi vil nu ændre action gidsel ovenfor, så vi afspiller walk-animationen når han går, og stand-animationen når han ikke gør.
Til at animere en entity bruges ent_animate
, som tager fire parametre: først en entity (f.eks. my
), derefter hvilken animation, der skal afspilles (f.eks. "walk"
eller "stand"
, husk anførselstegn!).
Tredje parameter angiver, hvor langt vi et nået gennem animationen, i procent - dvs. værdien 0 svarer til første frame i animationen (f.eks. stand1 eller walk1),
mens værdien 100 er sidste frame (f.eks. stand7 eller walk4).
Den sidste parameter angiver bl.a. om det skal være en "engangs-animation" (f.eks. et hop, eller når man dør) eller en cyklisk animation, dvs. en animation der kører i ring ligeså længe det skal være (f.eks. når man går eller løber).
Her ses en opdateret version af koden (med cykliske animationer):
action gidsel() { while(player == NULL) {wait(1);} while(1) { // Find en vektor, der peger fra mig mod player vec_set(target, player.x); vec_sub(target, my.x); // Drej mig, så jeg kigger i vektorens retning (mod player) vec_to_angle(my.pan, target); // Sørg for at jeg ikke kommer til at kigge opad eller nedad (det ser dumt ud) my.tilt = 0; // Hvis jeg er et hverken for tæt på eller langt fra player if((vec_dist(my.x,player.x) > 120) && (vec_dist(my.x,player.x) < 400)) { // Bevæg mig ligeud (dvs. hen imod player, som jeg jo kigger på) // Husk også lidt tyngdekraft c_move(my,vector(5 * time_step,0,0), vector(0,0,-10 * time_step), IGNORE_PASSABLE + GLIDE); // Afspil cyklisk animation ent_animate(my, "walk", my.skill48, ANM_CYCLE); my.skill48 += 5 * time_step; // Sæt hastighed på afspilning af animation her } else { // Vi skal huske tyngdekraften selvom vi ikke bevæger os mod player c_move(my, nullvector, vector(0,0,-10 * time_step), IGNORE_PASSABLE + GLIDE); // Afspil cyklisk animation ent_animate(my, "stand", my.skill48, ANM_CYCLE); my.skill48 += 2 * time_step; // Sæt hastighed på afspilning af animation her } my.skill48 %= 100; // Tag resten fra division med 100 ("modulus 100") wait(1); } }
Her bruges skill48 til at holde styr på, hvor langt vi er nået igennem en animation, og vi lægger hele tiden noget til skill48, så vi nærmer os de 100%. Læg mærke til, at der lægges mere til skill48 for walk-animationen end for stand - det betyder, at walk-animationen går hurtigere. Hvis du vælger at bruge en run-animation i stedet, så skal du nok lægge endnu mere til skill48 i loopet, så animationen bliver afspillet hurtigt nok, men det kommer også an på hvor hurtigt han bevæger sig.
Den sidste parameter i ent_animate
er sat til ANM_CYCLE
, der gør at vi får en cyklisk animation.
En vigtig "detalje" her er kommandoen my.skill48 %= 100;
, der sørger for at animationen kører i ring: modulus-operatoren, %
, giver resten ved division.
F.eks. vil 12 divideret med 8 give 1 og en rest på 4. Koden my.skill48 %= 100;
tager altså resten fra skill48 divideret med 100, så hvis skill48 er mindre end 100 har det ingen effekt,
og hvis skill48 er større end 100, ender skill48 under 100 (f.eks. vil 125 % 100
give 25). Samme kommando kunne skrives som while(my.skill48 > 100) {my.skill48 -= 100;}
,
men det er knap så elegant.
Hvis du i stedet skal afspille en engangs-animation, i stedet for en cyklisk, kan det f.eks. se således ud:
action engangsanimation() { while(1) { // Hvis vi trykker på "e" på tastaturet if(key_e) { my.skill48 = 0; // Vi starter med første frame while(my.skill48 < 100) // Loop gennem alle frames (indtil 100%) { ent_animate(my,"jump",my.skill48,0); // Afspil animation my.skill48 += 5 * time_step; // Gå mod de 100% wait(1); } } // Afspil cyklisk animation når vi ikke bruger engangs-animationen ent_animate(my,"stand",my.skill48,ANM_CYCLE); my.skill48 += 2 * time_step; my.skill48 %= 100; wait(1); } }Jeg er desværre ikke nået videre med denne artikelserie, men fortsætter den gerne hvis nogen ønsker det!