Nighthawk.dk

Programmering med Unity/C#

Skrevet d. 16. March 2022 af Claus Nielsen.

Her kommer du i gang med programmering i Unity. Du laver en ny komponent til GameObjects, som gør dig i stand til at flytte et GameObject inde i spillet.

Vi bruger komponenten til at lave kasser, som spilleren kan flytte rundt på.

For at komme i gang med lidt programmering, vil vi lave en kasse, som spilleren kan skubbe rundt i banen.

Vi holder det super simpelt, men det vil alligevel give nogle ekstra muligheder i forhold til gameplay i spillet.

Vi skal lave et script, dvs. et lille program, som vi skriver i programmeringssproget C# (udtales C-sharp), og som kan bruges som en komponent på et GameObject i Unity. Du kan lære mere om programmering i C# i denne artikel-serie.

Et nyt script

Først og fremmest skal der laves et nyt script. Gå ind i Assets/Scripts-mappen i Project-panelet, højreklik og vælg Create -> C# Script. Kald scriptet "Kasse".

Dobbelt-klik på scriptet i Project-panelet for at åbne scriptet i din kode-editor (f.eks. Visual Studio).

Opsætning af kode-editor

Hvis du ikke allerede har gjort det (eller det ikke er gjort automatisk), skal du først tilknytte en kode-editor til Unity.

Dette gør du i Unity under Edit -> Preferences, og vælg External Tools i menuen i venstre side. Vælg den kode-editor du vil bruge - f.eks. Visual Studio Community.

Vælg kode-editor, som skal forbindes til Unity.

Når du har startet din kode-editor op fra Unity, skulle du se følgende kode:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Kasse : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Den første markerede linie i koden fortæller, at vi vil lave en ny komponent (MonoBehaviour) med navnet Kasse. Vi vil her fokusere på alt hvad der står mellem de markerede { og }, som afgrænser den kodeblok, der udgør komponenten.

Bemærk at alt hvad der står efter to skråstreger, //, er kommentarer, dvs. resten af linien bliver ikke anset for kode. Kommentarer kan bruges til at forklare hvordan koden virker.

Vi har i koden to metoder (eller funktioner), kaldet void Start() og void Update(). De ligger mellem de markerede { og }, dvs. de er en del af kodeblokken som udgør komponenten. Desuden har de hver en kodeblok tilknyttet (også med { og }).

Disse to kodeblokke (til Start og Update) er lige nu tomme, men det er to af de steder, hvor man typisk vil skrive meget af sin kode til nye komponenter.

Kodenblokken efter void Start() bliver kørt så snart komponenten bliver oprettet (f.eks. hver gang du laver et GameObject med komponenten på), men kun denne ene gang.

Kodenblokken efter void Update() bliver kørt hver gang Unity renderer (tegner) et billede på skærmen, f.eks. 60 gange i sekundet hvis man kører med en framerate (FPS) på 60. Koden her bliver altså gentaget mange gange.

Bemærk også indrykningen ("luft" foran dele af koden); typisk forøger man indrykningen efter hver {, og nedsætter den efter }. Dette gør det tydeligt at se hvilken { der passer til hvilken }, og hvilken kodeblok de forskellige dele af koden står i.

Brug scriptet på en kasse

Lad os lave en kasse ved at højreklikke i scene-hierarkiet og vælge 3D Object -> Cube, kald den "Kasse", og flyt den et sted hen i banen hvor den kan stå ovenpå gulvet (men ikke nede i gulvet). Gå til Materials-mappen i Project-panelet, højreklik og vælg
Create -> Material, kald det "Kasse" og giv det en passende texture. Træk det nye material ud på kassen.

Lav en kasse (3D Object -> Cube) med et material med en passende texture. Her har jeg også ændret Scale i Properties-panelet.

Vi kan nu bruge vores script på kassen: Vælg kassen i scene-hierarkiet og scroll ned i Properties-panelet så du kan se "Add Component"-knappen. Nu kan du enten bare trække scriptet fra Project-panelet over på "Add Component"-knappen, eller bruge "Add Component"-knappen til at finde scriptet vi kaldte Kasse.

Du skulle nu kunne se en komponent kaldet "Kasse" i Properties-panelet:

Træk Kasse-scriptet fra Project-panelet over i Properties-panelet (på Add Component-knappen, eller mellem to andre komponenter).

Du har nu lavet en komponent, og brugt den på et objekt i din scene! Nu skal vi bare have skrevet lidt kode, så komponenten faktisk gør noget.

Flyt kassen rundt

Lad os starte med at skrive lidt kode i kodeblokken til void Update(), som gør os i stand til at flytte kassen:

		if(Input.GetKey(KeyCode.E))
		{
			Vector3 position = transform.position;
			position.x += 5.0f * Time.deltaTime;
			transform.position = position;
		}

Dvs. vi har nu:

Koden til Kasse.cs i Visual Studio, som den ser ud nu.

Når du har skrevet koden ind og gemt filen, så gå tilbage i Unity. Nu vil Unity bruge et øjeblik på kompilering af koden, dvs. omdanne C#-koden til nogle instruktioner, som Unity/spillet kan køre.

Bemærk: Hvis du har fejl i koden

Hvis du har lavet en fejl i koden, kan du ikke køre spillet:

Eksempel på fejl i koden - i Project-panelet kan vi gå til Console-fanen og se information om fejlen (her et glemt semikolon på linie 19).

Her er der glemt et semikolon, ;, i linie 19 (dvs. efter position.x += 5.0f * Time.deltaTime).

I Project-panelet er der en fane kaldet Console, hvor du kan se alle fejlmeddelelser. Her kan vi se, at der er en fejl i Kasse.cs (kodefilen) i linie 19 (og kolonne 39). Der står endda ; expected i fejlmeddelelsen.

Prøv at køre spillet nu, gå hen til kassen, og prøv at trykke E. Kassen flytter sig! Lige nu bliver kassen altid skubbet i samme retning, uanset hvor spilleren er i forhold til kassen (og hvor langt væk), og kassen kan skubbes lige igennem murene!

Inden vi fikser dette, skal vi lige se på hvordan koden virker.

Først har vi if(Input.GetKey(KeyCode.E)), der tjekker om spilleren har trykket på E. Hvis dette er sandt, dvs. hvis (if) der er trykket på E, så køres den efterfølgende kodeblok mellem { og }. Er der ikke trykket på E bliver kodeblokken sprunget over (ikke kørt).

Inde i kodeblokken har vi tre linier, som ændrer positionen (transform.position). Læg mærke til at transform.position refererer til den position, som også kan sættes på et objekt under Transform øverst i Properties-panelet.

Desværre kan vi ikke ændre de enkelte x-, y- og z-komponenter direkte, derfor laver vi en kopi: transform.position er af typen Vector3, så vi laver en ny variabel af typen Vector3, som vi kalder position, og sætter til samme værdi som transform.position.

Koncept: Variabler

Variabler er et helt centralt begreb i programmering. Du kan tænke på en variabel som en skuffe: Når vi skal gemme noget data, som vi behandler i vores program, så laver vi en variabel, dvs. vi beder computeren om en skuffe, som vi kan lægge vores data i.

Du kan forestille dig at computeren har mange forskellige slags skuffer - med forskellige former og størrelser. Vi skal altså også fortælle hvilken slags skuffe vi skal bruge, dvs. vi skal vide hvilken slags data, som skal gemmes i den.

Til dette angiver vi en variabel-type, som f.eks. int (heltal), float (decimaltal), double (decimaltal, større skuffe end til float), eller mere komplekse typer som f.eks. Vector3 (Unity-specifik type, indeholder tre float-variabler).

Variabler skal også have et navn: du skal altså sætte et navn på skuffen, så du kan finde den igen senere.

Læs mere om variabler og hvordan de bruges her.

Derefter ændrer vi x-komponenten af vores variabel med 5.0f * Time.deltaTime (når vi bruger += lægger vi højresiden til variablen på venstresiden). Det lille f efter 5.0 angiver at vi bruger C#-typen float (32-bit decimaltal, standard i Unity) - uden det lille f vil C# angtage, at vi i stedet bruger typen double (64-bit decimaltal), som ikke passer sammen med værdierne i vores Vector3.

Bemærk: Brug af Time.deltaTime i void Update()

Bemærk at koden som flytter kassen ligger i kodeblokken til void Update(), som bliver kørt hver der der tegnes et billede på skærmen. Så hvad ville der ske, hvis vi bare skrev position.x += 5.0f;?

En god computer vil kunne køre dit spil med over 100 FPS (frames per second, billeder i sekundet), mens en gammel/dårlig computer f.eks. vil køre med 30 FPS eller mindre. På den hurtige computer vil koden blive kørt mange flere gange end på den langsomme, og kassen vil derfor skubbes meget hurtigere!

Kort sagt bruger vi altså Time.deltaTime til at kompensere for forskelle i framerate, så spillet fungerer ens på alle computere.

Til sidst sættes transform.position til samme værdi som vores variabel (position). Selvom vi ikke direkte kan ændre de enkelte x-, y- og z-komponenter af transform.position, kan vi godt sætte hele vektoren lig med en anden vektor.

Flyt kassen i flere retninger

Vi kan udvide koden:

	void Update()
	{
		if(Input.GetKey(KeyCode.E))
		{
			Vector3 position = transform.position;
			position.x += 5.0f * Time.deltaTime;
			transform.position = position;
		}

		if (Input.GetKey(KeyCode.Q))
		{
			Vector3 position = transform.position;
			position.x -= 5.0f * Time.deltaTime;
			transform.position = position;
		}
	}

Nu kan du bruge E og Q til at flytte kassen frem og tilbage med. Bemærk at der er brugt -= til at trække fra, i modsætning til +=, som lægger til.

Opgave: Flyt kassen i alle retninger

Prøv selv at tilføje flere if-sætninger (med andre taster) samt kodeblokke, således at du kan flytte kassen i alle retninger.

Du kan ændre position.y og position.z for at flytte kassen i andre retninger. Prøv f.eks. også at få kassen til at bevæge sig langsommere langs y-retningen.

Skub kassen væk fra spilleren

I stedet for bare at skubbe kassen langs x-, y- og z-akserne, vil vi gerne skubbe kassen væk fra spilleren. For at vi kan bruge spillerens position i koden, er kassen nødt til at vide, hvem spilleren er - den skal have en reference til spilleren.

Øverst i kodeblokken til public class Kasse : MonoBehaviour skal vi tilføje den linie, der er markeret her:

public class Kasse : MonoBehaviour
{
	public GameObject player;

	... resten af koden kommer her ...

Gem kodefilen og gå over i Unity - nu skulle der komme et "Player"-felt på Kasse-komponenten. Træk FPSController-objektet fra scene-hierarkiet over på feltet i Kasse-komponenten hvor der står "None", som vist her:

Vi har tilføjet et player-felt på vores Kasse-komponent. Træk player-objektet over på player-feltet.

Hver gang du bruger Kasse-komponenten skal du huske at trække player-objektet over på "Player"-feltet på denne måde.

Nu kan vi ændre koden i void Update(), så den ser således ud:

		if (Input.GetKey(KeyCode.E))
		{
			Vector3 playerTilKasse = transform.position - player.transform.position;
			playerTilKasse.Normalize();

			transform.position += playerTilKasse * Time.deltaTime;
		}

Her laver vi en variabel med navnet playerTilKasse, som indeholder en vektor, der går fra spillerens position til kassens position. Det kræver lidt vektorregning, men beregningen er illustreret her:

Ved at trække spillerens position fra kassens position får vi en vektor fra spilleren til kassen.

Variablen playerTilKasse bliver altså en vektor der har retningen fra spilleren til kassen, og med en længde svarende til afstanden mellem kassen og spilleren.

Vi vil ikke have at man skal skubbe kassen hurtigere jo længere man er væk fra den, derfor sætter vi længden af vektoren playerTilKasse til 1 ved at bruge playerTilKasse.Normalize(); (normering af vektorer med Normalize-metoden sætter deres længde til 1).

Da vi ikke sætter x-, y- og z-komponenterne enkeltvist, men bare vil lægge en vektor til positionen, kan vi ændre transform.position direkte. Ligesom da vi forøgede x-komponenten tidligere kan vi bruge += til at lægge en vektor til positionen.

Du skulle nu kunne skubbe kassen væk fra spilleren ved at trykke på E! Bemærk at det ikke virker (og du får en fejl i Console-fanen af Project-panelet) hvis du har glemt at trække FPSController-objektet over på kasse-komponenten - dette skal gøres for alle objekter, som bruger scriptet!

Koden til Kasse.cs i Visual Studio, som den ser ud nu.
Opgave: Hastighed på kassen

Hvordan kan du ændre den hastighed, som vi skubber kassen med? Løsningen kommer længere nede i artiklen, men prøv selv!

Prøv også at se hvad der sker, hvis du bruger en negativ hastighed.

Collision detection

Vi kan nu skubbe kassen væk fra spilleren, men den bliver stadig skubbet gennem mure (eller gennem gulvet, hvis spilleren står ovenpå den).

Dette problem løses med collision detection, og Unity har allerede nogle færdige værktøjer vi kan bruge til dette.

Vi skal tilføje en Character Controller-komponent til kassen, dvs. vælg kassen i scene-hierarkiet, og tryk på "Add Component"-knappen nederst i Properties-panelet. Søg på Character Controller og vælg den, for at tilføje denne komponent til kassen.

Tilføj en Character Controller-komponent til kassen.

Vi kan nu lave et par ændringer i koden, her er hele kodeblokken til public class Kasse : MonoBehaviour:

public class Kasse : MonoBehaviour
{
	public GameObject player;

	private CharacterController controller;

	// Start is called before the first frame update
	void Start()
	{
		controller = GetComponent<CharacterController>();
	}

	// Update is called once per frame
	void Update()
	{
		if (Input.GetKey(KeyCode.E))
		{
			Vector3 playerTilKasse = transform.position - player.transform.position;
			playerTilKasse.Normalize();

			controller.Move(playerTilKasse * Time.deltaTime);
		}
	}
}

Først har vi tilføjet to linier, som laver en variabel, controller, af typen CharacterController, og bruger void Start() til at sætte den til at pege på Character Controller-komponenten, som du tilføjede til kassen.

Derefter ændrer vi den linie der flytter kassen, så vi nu bruger Character Controller-komponentens Move-metode, i stedet for direkte at ændre transform.position.

Nu kan du flytte kassen rundt uden at den ryger igennem murene!

CharacterController er ikke altid det bedste valg

Generelt bør man bruge RigidBody i stedet for CharacterController, men det er nemmere at arbejde med CharacterController.

Du bruger RigidBody.AddForce til at påvirke (flytte) på RigidBody, i modsætning til CharacterController.Move.

En af ulemperne ved CharacterController er, at den altid bruger en "capsule-Collider", som ikke passer til alle 3D-modeller. Derfor vil mange modeller f.eks. se ud som om de svæver, hvis de bruger en CharacterController. Til gengæld bliver den ikke påvirket af kræfter som RigidBody, og flytter sig altså kun når vi beder den om det med Move-metoden.

Læs mere her.

Tyngdekraft

Hvis du skubber kassen udover en kant, vil du se, at den kan svæve! Dette kan du gøre noget ved på denne måde:

	void Update()
	{
		if (Input.GetKey(KeyCode.E))
		{
			Vector3 playerTilKasse = transform.position - player.transform.position;
			playerTilKasse.Normalize();

			controller.Move(playerTilKasse * Time.deltaTime);
		}

		controller.Move(Vector3.down * 0.1f);
	}

Nu flyttes kassen hele tiden nedad, men pga. collision detection gør det ingen forskel når den allerede står nede på gulvet. Den vil også kunne rutsje ned ad bakke.

Læg mærke til at denne linie, der får kassen til at falde nedad, står udenfor kodeblokken til if-sætningen! Dette er fordi kassen altid skal kunne falde nedad, ikke kun når man trykker på E!

Bemærk: Selvom dette kan give en illusion af tyngdekraft, så bruger vi her konstant hastighed nedad - tyngdekraft har i stedet konstant acceleration, så det er altså ikke helt det samme. Men det fungerer fint til de fleste formål her!

Skub kun kassen, når man er tæt på

Hvis du har flere kasser i banen, bliver de alle skubbet hver gang du trykker E, uanset hvor i banen de står. Man skal kun kunne skubbe en kasse når spilleren er tæt på.

Vi har allerede en vektor fra spilleren til kassen, playerTilKasse, så hvis vi tager længden (magnitude) af denne vektor inden vi normerer den, får vi afstanden. Afstanden kan derefter tjekkes i en if-sætning:

	void Update()
	{
		if (Input.GetKey(KeyCode.E))
		{
			Vector3 playerTilKasse = transform.position - player.transform.position;
			float afstandTilKasse = playerTilKasse.magnitude;

			if (afstandTilKasse < 2.5f)
			{
				playerTilKasse.Normalize();
				controller.Move(playerTilKasse * Time.deltaTime);
			}
		}

		controller.Move(Vector3.down * 0.1f);
	}

Nu skal man tæt på kassen, for at kunne skubbe den - prøv at ændre tallet 2.5f for at finde en passende afstand.

Forskellige kasser - indstillinger i Unity

Hvad nu hvis vi vil have forskellige slags kasser i banen - med forskellig størrelse, og hvor nogle er tungere at skubbe end andre. Til dette kan vi lave "parametre i Unity" ved at lave nogle variabler (public felter i klassen Kasse) således:

public class Kasse : MonoBehaviour
{
	public GameObject player;
	public float pushSpeed = 1.0f;
	public float fallSpeed = 0.1f;
	public float interactionDistance = 2.5f;

	private CharacterController controller;

	// Start is called before the first frame update
	void Start()
	{
		controller = GetComponent<CharacterController>();
	}

	// Update is called once per frame
	void Update()
	{
		if (Input.GetKey(KeyCode.E))
		{
			Vector3 playerTilKasse = transform.position - player.transform.position;
			float afstandTilKasse = playerTilKasse.magnitude;

			if (afstandTilKasse < interactionDistance)
			{
				playerTilKasse.Normalize();
				controller.Move(playerTilKasse * pushSpeed * Time.deltaTime);
			}
		}

		controller.Move(Vector3.down * fallSpeed);
	}
}

I Properties-panelet i Unity ser det nu således ud for vores komponent:

Public felter i klassen Kasse kan ses som parametre på komponenten i Unity.

Så er vi færdige! Du har nu en kasse, som spilleren kan skubbe rundt i banen, og evt. bruge til at komme steder hen, man ellers ikke ville kunne hoppe op til. Eller du kan f.eks. skjule en hemmelig indgang bag en kasse! Du kan også sagtens bruge scriptet på andet end kasser!

Relaterede ressourcer
Unity Engine

Game-engine til udvikling af 3D-spil.

Se mere her

C# Grundbog

Niels Hilmar Madsen og Michell Cronberg,
Libris, 2003

×