Nighthawk.dk

Bevægelse med Collision Detection og Animationer

Skrevet d. 18. Sept. 2016 af Claus Nielsen.

Her ser vi på, hvordan man kan få entities til at bevæge sig, uden at de f.eks. kan gå igennem vægge (altså med collision detection). Desuden ser vi på, hvordan entities kan animeres, så de f.eks. ser ud som om de går eller løber, mens vi flytter dem.

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 eller player).
  • 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 og GLIDE).

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!
Relaterede ressourcer
Acknex Unlimited

Mange ressourcer til 3D Gamestudio.

Se mere her

3D Gamestudio

Game-engine til udvikling af 3D-spil.

Se mere her

×