Nighthawk.dk

Funktioner, Variabler, m.m.

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

Nu hvor du er introduceret til SED og programmering, skal du lære om variabler og funktioner, der er grundlæggende elementer i alle programmeringssprog. Til sidst vises nogle ekstra ting, som kan være nyttige.

Funktioner

Du har allerede brugt funktioner - actions, som du lavede i sidste afsnit, er en form for funktion. Den primære forskel på actions og andre funktioner, er, at du bruger ordet action når du definerer dem. Du kan lave en "almindelig" funktion ved at bruge ordet function i stedet:

function min_funktion()
{
    // Gør noget her...
}

Når du har defineret en funktion, kan du bagefter kalde den, dvs. køre koden i funktionen, ved at skrive funktionens navn (inkl. paranteser) efterfulgt af semikolon: min_funktion();.

Det smarte ved at have funktioner, er, at det så bliver nemmere at genbruge kode - hvis du har to actions, der begge skal bruge nogle af de samme kommandoer, kan du putte dem over i en funktion, de begge kan kalde:

// Funktion der ligger start-positionen over i skill21, skill22 og skill23
function gem_startposition()
{
	my.skill21 = my.x;
	my.skill22 = my.y;
	my.skill23 = my.z;
}

// Dræb player hvis han er kommet tæt på
function draeb_player()
{
	// Tjek om "player"-pointeren er sat, dvs. om "player" er forskellig fra "NULL"
	if(player != NULL)
	{
		// Tjek om player er indenfor afstanden 100
		if(vec_dist(my.x,player.x) < 100)
		{
			// Player er for tæt på, så dræb ham!
			t_players_health = 0;
		}
	}
}

// Objekt der bevæger sig i en cirkel omkring start-positionen
action farlig_ting1()
{
	// Kald en funktion, som gemmer start-positionen i skill21-23
	gem_startposition();
	
	// Definer to variabler
	var radius = 100;
	var vinkel = 0;
	
	while(1)
	{
		// Sæt positionen til et punkt på en cirkel med centrum i start-positionen
		my.x = my.skill21 + radius * cos(vinkel);
		my.y = my.skill22 + radius * sin(vinkel);
		
		// Forøg vinklen (positionen i cirkel-bevægelsen)
		vinkel += 10 * time_step;
		
		// Kald funktionen der dræber player, hvis han er for tæt på
		draeb_player();
		
		wait(1);	// HUSK "wait" i uendelige loops
	}
}

// Objekt der bevæger sig op og ned
action farlig_ting2()
{
	// Kald en funktion, som gemmer start-positionen i skill21-23
	gem_startposition();
	
	// Definer to variabler
	var hojde = 80;
	var vinkel = 0;
	
	while(1)
	{
		// Sving op og ned (husk at z-aksen er op/ned i 3DGS)
		my.z = my.skill23 + hojde * (1 + cos(vinkel)) / 2;
		
		// Forøg vinklen
		vinkel += 10 * time_step;
		
		// Kald funktionen der dræber player, hvis han er for tæt på
		draeb_player();
		
		wait(1);	// HUSK "wait" i uendelige loops
	}
}

Læs koden grundigt og prøv begge actions af inden du læser videre, så du ved hvordan de virker inde i spillet!

De markerede dele af koden viser hvordan man definerer og kalder funktioner. Bemærk at vi ikke behøver et loop i funktion draeb_player, idet funktionen bliver kaldt fra et loop - koden i funktionen bliver altså alligevel kørt hver frame (hver gang funktionen kaldes).

Det meste af koden burde kunne forstås ud fra kommentarerne i koden, men her er igen lidt nyt: i begge actions definerer vi nogle variabler - dette kommer vi til lidt senere. Noget andet er brugen af funktionerne sin og cos, der altså bare er sinus- og cosinus-funktionerne kendt fra matematik; hvis du ikke kender dem, kan du evt. læse om trigonometri her, men i princippet giver cosinus og sinus bare henholdsvis x- og y-koordinaten til et punkt på en cirkel med radius 1, ud fra en vinkel. Indenfor spilprogrammering er trigonometri en af de nyttigste grene af matematikken.

Parametre og returværdi

I eksemplet ovenfor havde vi funktionen draeb_player, der bl.a. tjekker om player er tæt nok på... Men hvor tæt skal player egentlig være? I draeb_player har vi sat denne afstand til 100, men hvad nu, hvis vi hellere vil bruge afstanden 150 i action farlig_ting2? Det er der heldigvis en nem løsning på, for funktioner kan have parametre, dvs. når man kalder en funktion, kan man sende værdier med, som funktionen kan bruge. Her er en forbedret udgave af draeb_player:

// Dræb player hvis han er kommet tæt på
function draeb_player(var afstand)
{
	// Tjek om "player"-pointeren er sat, dvs. om "player" er forskellig fra "NULL"
	if(player != NULL)
	{
		// Tjek om player er indenfor den angivne afstand
		if(vec_dist(my.x,player.x) < afstand)
		{
			// Player er for tæt på, så dræb ham!
			t_players_health = 0;
		}
	}
}

Vi bruger altså parantesen til at definere en eller flere parametre (her kun en enkelt, kaldet afstand), som vi bagefter kan bruge i vores funktion. Når vi nu kalder funktionen i action farlig_ting1 eller farlig_ting2, skal vi sende en værdi med - dvs. du skal ændre funktionskaldet i disse actions til f.eks. draeb_player(80); i farlig_ting1 og draeb_player(150); i farlig_ting2.

Nu har vi så set, hvordan man kan sende en værdi til en funktion, men man kan også få en værdi tilbage igen. Her er et eksempel på en funktion der opløfter en værdi i 2. potens og sender resultatet tilbage:

// En funktion der returnerer kvadratet på et tal
function oploeft_i_anden(var tal)
{
	// Lav en variabel, og sæt den lig med kvadratet af det tal, der er sendt til funktionen
	var resultat = tal * tal;
	
	// Send resultatet tilbage igen
	return(resultat);
	
	// Kode, der står efter "return", bliver ikke kørt
}

Vi kan nu kalde funktionen med f.eks. my.skill1 = oploeft_i_anden(4); - dette vil sætte skill1 lig med 42 (altså 16). Her er et andet eksempel:

// Funktion der laver lineær interpolation mellem to tal
function interpolation(var tal1, var tal2, var fraktion)
{
	// Definer først en variabel til at holde resultatet
	var resultat;
	
	// Beregn resultatet
	resultat = tal1 * (1 - fraktion) + tal2 * fraktion;
	
	// Send reultatet tilbage
	return(resultat);
}

Det vigtige her er ikke, hvad lineær interpolation er, men at vi har flere parametre i funktionen - hele tre, kaldet tal1, tal2 og fraktion. Bemærk at man bare sætter et komma ind mellem parametrene i parantesen, for at have flere parametre. Omvendt kan man kun sende én enkelt værdi tilbage igen med return!

Desuden behøver man ikke at lave en variabel til at gemme resultatet i, før det sendes tilbage - det gør bare nogle gange koden lidt mere overskuelig. Her er en kortere udgave:

// Funktion der laver lineær interpolation mellem to tal
function interpolation(var tal1, var tal2, var fraktion)
{
	// Beregn resultatet og send det tilbage
	return (tal1 * (1 - fraktion) + tal2 * fraktion);
}

Funktionskald ved input fra tastaturet

Udover at kalde funktioner fra actions aller andre funktioner, kan man kalde dem ved tryk på en tast på tastaturet. Se dette eksempel:

function lommelygte()
{
	// Hvis der ikke findes nogen player, stop funktionen her
	if(player == NULL) {return;}
	
	// Er "lommelygten" tændt?
	if(player.lightrange > 0)
	{
		// Hvis ja, sluk den
		player.lightrange = 0;
	}
	else
	{
		// Hvis nej, tænd den
		player.lightrange = 300;
	}
}
/////////////////////////////////////////////////////////////////
// The main() function is started at game start
function main()
{
// entry: Warning Verbosity (0,1,2,3,4)
// entry_help: Sets sensitivity of warnings (0 = none, 1 = some, 4 = all).
	warn_level = 2;	

#ifndef startup_h 
// load the level
	level_load(t_levelname);
#endif

	on_l = lommelygte;
}

Først definerer vi en funktion, lommelygte, som vi vil kalde hver gang, der trykkes på L. Dette kan gøres ved at køre koden on_l = lommelygte;, og for at sikre os, at denne kommando bliver kørt, placerer vi den i main-funktionen. main-funktionen er en speciel funktion, som automatisk bliver kørt, når dit spil starter - det er bl.a. denne funktion, der sørger for at køre det level du har lavet i WED vha. level_load-kommandoen. Du skulle gerne have en main-funktion i din kodefil, så du behøver kun tilføje on_l = lommelygte; til sidst i funktionen.

Bemærk at din funktion lommelygte skal være defineret før du bruger on_l = lommelygte;. Desuden skal din funktion hverken have parametre eller returværdi når du bruger den på denne måde.

Linien if(player == NULL) {return;} skal nok lige forklares: som du måske har gættet, er denne linie nødvendig, i tilfælde af at der ikke er nogen player. Hvis dette er tilfældet kaldes return; uden nogen værdi at sende tilbage. Dvs. selv når en funktion ikke skal have nogen returværdi, kan return være nyttig, fordi den så bare stopper funktionen, så koden i resten af funktionen ikke bliver kørt.

Ligesom med lommelygte kan du lave andre funktioner, som du knytter til forskellige taster på tastaturet (brug f.eks. on_f, on_g, on_space, on_ctrl, osv.).

Prototyper

Se følgende script (kode) - det vil resultere i en fejl, men hvad er der egentlig galt her?

action cirkelbevaegelse()
{
	vec_set(my.skill21,my.x); // Sæt skill21-23 til x-, y- og z-start-koordinaterne
	
	while(1)
	{
		saet_position(150,my.skill40); // Kald funktion
		
		my.skill40 += 10 * time_step;  // Forøg vinkel (position på cirkel)
		
		wait(1); // Vigtigt!
	}
}

function saet_position(var radius, var vinkel)
{
	my.x = my.skill21 + radius * cos(vinkel);
	my.y = my.skill22 + radius * sin(vinkel);
	my.z = my.skill23;
}

Problemet er, at funktionen saet_position bliver kaldt i action cirkelbevaegelse før funktionen er defineret - dvs. når koden i action cirkelbevaegelse forsøger at kalde saet_position, så har computeren/spillet ikke set den funktion før, og kan derfor ikke finde ud af det!

Der er to måder at løse dette problem på: enten kan du bare definere funktionen saet_position før du definerer action cirkelbevaegelse - det er den løsning vi har brugt tidligere. Den anden måde er vha. prototyper. En prototype er bare en definition af din funktion uden nogen kodeblok (husk derfor semikolon):

function saet_position(var radius, var vinkel);

action cirkelbevaegelse()
{
	vec_set(my.skill21,my.x); // Sæt skill21-23 til x-, y- og z-start-koordinaterne
	
	while(1)
	{
		saet_position(150,my.skill40); // Kald funktion
		
		my.skill40 += 10 * time_step;  // Forøg vinkel (position på cirkel)
		
		wait(1); // Vigtigt!
	}
}

function saet_position(var radius, var vinkel)
{
	my.x = my.skill21 + radius * cos(vinkel);
	my.y = my.skill22 + radius * sin(vinkel);
	my.z = my.skill23;
}

Prototypen fortæller altså, at du har en funktion, kaldet saet_position og med to parametre, som du definerer senere i koden. Nu hvor du har fortalt, at den findes, kan du sagtens kalde den, i alt det kode der kommer efter prototypen.

Hvis man har et kæmpe stort spil, med rigtig mange kodefiler, og hundredevis af funktioner, som kalder hinanden på kryds og tværs, kan det være nødvendigt at bruge prototyper. Det er i sådanne tilfælde almindeligt at samle alle prototyper oppe i starten af sin kodefil, inden man begynder at definere nogen af kodeblokkene til funktionerne - dermed sikrer man, at de altid kan kalde hinanden, hvis det bliver nødvendigt.

Variabler

Vi har allerede introduceret variabler i eksemplerne ovenfor, men lad os først få defineret en variabel: det er en plads i computerens hukommelse, hvor du kan gemme en værdi. Du kan også forestille dig, at det er en skuffe: du kan putte ting i skuffen (= give en variabel en værdi), tage ting op fra skuffen og evt. lægge noget andet i, i stedet (= ændre værdien af variablen), eller du kan nøjes med at kigge i skuffen (= bruge værdien af din variabel, uden at ændre den).

For at bruge en variabel, skal den først defineres, og definitionen af en variabel består af to ting: variabel-typen (svarende til form og størrelse på skuffen) og et navn (så du senere kan finde skuffen igen). I forbindelse med 3DGS behøver du ikke bruge andet end typen var til at opbevare tal i, men i andre programmeringssprog finder du mange forskellige typer - og da det programmeringssprog 3DGS bruger, Lite-C, er baseret på sproget C, kan du også bruge variabel-typerne fra C: f.eks. int for heltal, float for decimaltal, og double for decimaltal med højere præcision. Jeg vil som udgangspunkt anbefale at holde sig til 3D Gamestudios var-type.

Du definerer en variabel ved først at skrive variabel-typen, og dernæst navnet:

var en_variabel;
var en_anden_variabel = 0;

Som det fremgår af eksemplet, kan man vælge at give variablen en værdi, samtidig med at den defineres - dette kaldes initialisering af variablen. Det er generelt en rigtig god idé at initialisere sine variabler, så man ikke kommer til at forsøge at bruge dem, inden de har fået nogen værdi.

Når først din variabel er defineret og initialiseret til en værdi, kan du bruge dem ligesom med skills - men du skal ikke have my. foran variabel-navnet:

function variabel_test(var c)
{
	var a = 5;
	var b = 10;
	
	a = b + 2;	// a er nu 12
	b += 6;		// b er nu 16
	
	return (a + b + c);	// Sender værdien 28 + c tilbage
}

Bemærk at kun a og b bliver initialiseret - c er en parameter for funktionen, så den bliver automatisk initialiseret til den værdi, som bliver sendt til funktionen, når den bliver kaldt. Du skal ikke forsøge selv at initialisere parametre!

Scope

Variabler er ikke bare variabler - de kan også inddeles i lokale og globale variabler, hvor de globale variabler altid er tilgængelige, mens de lokale variabler kun er tilgængelige i den kodeblok, hvor de er defineret. Dvs. udenfor den kodeblok, hvor en lokal variabel er defineret, eksisterer den simpelthen ikke! Det område i koden, hvor en variabel eksisterer (dvs. fra den er defineret, til den kodeblok, den er defineret i, stopper) kaldes en variabels scope.

Hvis du definerer en variabel i en kodeblok (f.eks. i en funktion, en action, eller en if-sætning) vil det være en lokal variabel. Hvis du i stedet definerer en variabel udenfor en kodeblok, vil det være en global variabel, som alle vil kunne se.

Tid til et eksempel:

var global_variabel = 0;

action brug_global_variabel()
{
	global_variabel += 1;
}

action brug_lokal_variabel()
{
	var lokal_variabel = 0;
	
	lokal_variabel += 1;
}

Hvis vi satte 10 entities ind i WED - 5 med action brug_global_variabel og 5 med action brug_lokal_variabel - så ville global_variabel ende på værdien 5, fordi alle 5 entities bruger den samme variabel. Samtidig vil lokal_variabel kun få værdien 1, og hver af de 5 entities vil have sin egen kopi af lokal_variabel.

Dvs. hvis lokal_variabel ændres for én entity, vil de andre 4 entities med action brug_lokal_variabel ikke få ændret værdien af deres version af lokal_variabel, fordi hver entity har sin egen "kopi" af variablen. Helt modsat forholder det sig med global_variabel, som der kun findes én version af - og alle entites med action brug_global_variabel bruger derfor den samme variabel; de har ikke hver sin kopi!

Precompiler-direktiver

Der findes en række såkaldte precompiler-direktiver, der er nogle specielle kommandoer, der udføres inden resten af den kode du har skrevet. De starter altid med #, så de er nemme at genkende. Her gennemgås de to vigtigste.

Flere filer - #include

Efterhånden som du får lavet flere actions, funktioner, m.m., vil det være en fordel at have forskellige filer, til de forskellige dele, så det hele bliver mere overskueligt. Dette gøres vha. #include-kommandoer i starten af din kodefil: lav en ny fil, minfil.c, i samme mappe som din anden kodefil og dit level, og tilføj så dette i starten af din kodefil (efter de andre #include-linier):

#include "minfil.c"

Bemærk: du skal ikke have de samme #include-linier eller en function main i den nye fil!

Rækkefølgen, som du inkluderer filer i, har også betydning, og jeg vil derfor anbefale, at du samler alle dine globale variabler, funktions-prototyper, og lignende definitioner i én fil, som er den første fil du inkluderer efter acknex.h og default.c (hvis du altså bruger disse).

Aliaser og konstanter - #define

Hvis du vil lave et alias (altså et ekstra navn) til noget, kan du bruge #define. Lad os som eksempel se på action farligt_omraade fra sidste afsnit, her i en udgave, der bruger skill1 og skill2:

action farligt_omraade()
{
	// Vent indtil "player"-pointeren er sat
	while(player == NULL) {wait(1);}
	
	while(1)
	{
		// Tjek om player er indenfor afstanden angivet i skill1
		if(vec_dist(my.x,player.x) < my.skill1)
		{
			// Player mister liv, når han er for tæt på
			t_players_health -= my.skill2 * time_step;
		}
		
		// Vent 1 frame. VIGTIGT når man har et uendeligt loop!
		wait(1);
	}
}

Her ville det være smart at bruge #define to gange:

#define farlig_afstand skill1
#define skade skill2

// use: farlig_afstand, skade
action farligt_omraade()
{
	// Vent indtil "player"-pointeren er sat
	while(player == NULL) {wait(1);}
	
	while(1)
	{
		// Tjek om player er indenfor afstanden angivet i skill1
		if(vec_dist(my.x,player.x) < my.farlig_afstand)
		{
			// Player mister liv, når han er for tæt på
			t_players_health -= my.skade * time_step;
		}
		
		// Vent 1 frame. VIGTIGT når man har et uendeligt loop!
		wait(1);
	}
}

Nu blev koden også lidt mere intuitiv, når man f.eks. kan bruge my.skade i stedet for my.skill2, og det kan nogle gange være en fordel! Dette er muligt pga. #define-kommandoerne.

En smart, ekstra gevinst ved at bruge #define her, kommer fra linien // use: farlig_afstand, skade som står på linien lige før vores action bliver defineret: da det er skrevet som en kommentar, har koden ikke nogen betydning for spillet i sig selv, men når vi arbejder på vores level i WED, så forstår WED sådanne kommentarer umiddelbart før en action:

Man kan altså gøre det nemmere at bruge en action i WED vha. #define og specielle kommentarer med // use: !

Udover at omdøbe skills, kan man f.eks. også definere konstanter:

#define lyshastighed 299792458
#define pi 3.14159265359

Så nu kan man altså skrive lyshastighed og pi i stedet for at skrive tallene, hvis man skal bruge dem i sin kode.

Relaterede ressourcer
Acknex Unlimited

Mange ressourcer til 3D Gamestudio.

Se mere her

3D Gamestudio

Game-engine til udvikling af 3D-spil.

Se mere her

×