Starten met programmeren van een 3D omgeving in Unreal Engine – Gevorderd – Functies, Macros en Custom Events

print
Deze handleiding maakt deel uit van het programmeertraject:


Inhoud


Wat vooraf ging

Deze handleiding bouwt verder op de basishandleiding over functies en breidt deze uit met Macros en Custom Events.


Inleiding

U hebt reeds kennis gemaakt met functies als toepassing van het DRY-principe.

We gaan nu dieper in op het gebruik van Functies, maar ook aanverwanten als Macros en Custom Events.

Ik ga er in deze handleiding van uit dat u al degelijke basiskennis hebt in het werken met Unreal Engin zodat ik niet meer elke stap hoef te beschrijven maar me kan focussen op wat typisch is voor Functies, Macros en Custom Events.

Ze hebben alle drie hetzelfde doel: de programmeercode minder complex en beheersbaarder (wijzigingen slechts op één plaats doorvoeren) te maken. Het verschil tussen Functies, Macros en Custom Events zit hem in de details.

  • Functies kunnen bv. geen “latente” acties (zoals een Delay) bevatten, Macros en Customs Events wel.
  • Macros hebben altijd een invoer-pin en uitvoer-pin.
  • Bij Functies is een uitvoer-pin optioneel.
  • Customs Events hebben geen uitvoer-pin.

Onderstaande video bespreekt en toont de verschillen.


DRY – Don’t Repeat Yourself

Eventjes wat algemene herhaling. Wat is DRY?

DRY is simpel gezegd: schrijf geen twee keer dezelfde code.

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Bovenstaande quote gebruikt twee (nieuwe) begrippen: piece of knowledge en representation.

Neem ons vorig programma, u wilt een lamp doen branden. Om de lamp te doen branden hebt u programmeercode (Blueprint Visual Scripting) nodig. U plaatst deze programmeercode in een eigen functie (functie, procedure, module, deelprogramma,… vele programmeertalen gebruiken een andere benaming voor wat vaak, ongeveer, hetzelfde is. In C++, en bij uitbreiding Unreal Engine wordt de term function gebruikt).

Deze functie krijgt een specifieke naam. De code in de functie is de piece of knowledge. De naam van deze functie is de representation waarmee de piece of knowledge wordt aangeroepen.

De functie, en de bijhorende code, wordt slechts eenmaal geschreven. De representation die deze functie aanroept wordt iedere keer hergebruikt waar u de piece of knowledge, in dit geval de lamp doen branden, nodig hebt.

Waarom is het DRY-principe belangrijk?

Als u tegen dit principe zondigt, en u herhaalt dezelfde code herhaaldelijk, dan zal u bij iedere potentiële wijziging aan deze code, deze wijziging moeten herhalen op alle plaatsen waar u de code gebruikt hebt.

Stel, u wilt een specifieke foutboodschap weergeven. Deze foutboodschap zal op meerdere plaatsen gebruikt worden. U kunt zondigen tegen het DRY-principe en deze foutboodschap telkens waar u ze nodig heeft opnieuw intypen. Maar stel dat u de foutboodschap wilt wijzigen. Dan moet u overal waar u deze foutboodschap ingetypt hebt de wijzigingen aanbrengen (en hopelijk vergeet u er geen te wijzigen).

Past u het DRY-principe toe dan hebt u die foutboodschap (piece of knowledge) aan een representation toegekend (gekoppeld) die u nadien herhaaldelijk gebruikt hebt. Als u nadien de foutboodschap (piece of knowledge) wilt wijzigen moet u enkel maar op die ene plek, binnen de functie, de wijziging aanbrengen.

Door DRY te programmeren:

  • wint u tijd (omdat u sneller fouten kunt verbeteren vermits u ze maar op één plek moet verbeteren).
  • beperkt u de kans op fouten.
  • maakt u de code ook leesbaarder.

Voorbereiding

  • Start een nieuw Third Person Desktop/Console project van Maximum Quality en geef het een passende naam (Starters Content is niet noodzakelijk (zo bespaart u wat ruimte op de harde schijf).
  • Ga naar Level Blueprint. Ik zou dit vanuit gelijk welke Blueprint kunnen doen maar kies voor het gemak de Level Blueprint.
  • Links ziet u in het MyBlueprint-panel al het nodige al staan (Event Dispatchers zijn voor later).

U maakt 2 variabelen aan:

  • HealthFloatDefault Value = 100
  • DamageFloatDefault Value = 10

Bij het drukken op de F-toets en op de G-toets moet telkens hetzelfde gebeuren, namelijk het berekenen van de Health – Damage en moet dit worden uitgeprint. Bij het drukken op de H-toets wordt gewoon de Health getoond.

Hieronder ziet u de code.

Merk op dat dezelfde code zich herhaalt, dit is strijdig met het DRY-principe, we gaan deze code dus moeten plaatsen in een Functie, Macro of Custom Event.

  • Compile, Save en Start.

Probeer dit uit. Druk eerst eens op de F-toets, dan op de G-toets en tenslotte op de H-toets.

Merk op dat de waarde voor van de variabele Health, ook na de berekeningen, nog steeds op 100 staat.

We gaan nu dit voorbeeld herwerken gebruikmakend van een Functie, Macro en Custom Event.


Functies

Ik herneem nog even onze startcode.

U ziet dat de F-event en de G-event identiek dezelfde code uitvoert, we kunnen dit dus volledig in een functie steken zodat we de code maar één keer moeten programmeren maar meerdere keren (hier 2 keer) kunnen hergebruiken.

De eerste vraag is, wat plaatsen we in de functie. Hoewel het niet aan te raden is, en ik dit straks ga herwerken, zouden we alles in de functie kunnen plaatsen.

  • Selecteer alle nodes na de F-event (u zou evengoed de G-event kunnen nemen).
  • We zouden deze kunnen kopiëren en plakken in een nieuw aan te maken functie maar u kunt ook de selectie aanklikken met de rechtermuisknop ingedrukt en kiezen voor Collapse to Function.

  • Geef deze functie vervolgens een gepaste naam (bv. Health-Damage).

  • Druk op Enter en u ziet de nieuwe functie staan in plaats van de oorspronkelijke code.

We kunnen nu deze functie hergebruiken voor de G-event.

  • Selecteer de nodes achter de G-event en verwijder deze door op Delete te klikken.
  • Zoek vervolgens naar de naam van onze functie, u ziet dat deze gevonden wordt.

  • Klik op onze functie en verbindt de functie met de Pressed-pin van de G-event. We noemen dit een Function Call.

U merkt nu dat onze Blueprint al veel overzichtelijker is.

Maar waar staat nu de code van de functie?

  • Dubbelklik op de functienaam, tenzij in het My Blueprint-panel, tenzij op in de Level Blueprint zelf.
  • U ziet nu de functie Health-Damage staan en kunt u hier eventuele wijzigingen aanbrengen.

Functie Details-panel

Laten we even een blik werpen op het Details-panel van de functie.

Laten we de eigenschappen van het Graph-luik eens overlopen.

Description – voegt een omschrijving toe aan de functie (bv. Bereken de Health minus Damage).

Category – voeg de functie aan een specifieke categorie toe.

Keywords – Keywords helpen bij het zoeken naar de functie.

Compact Node Title – is de titel die in de compacte weergave verschijnt.

Access Specifier – bepaalt het bereik, de toegankelijkheid, van de functie.

Public De functie kan aangeroepen worden vanuit andere blueprints.
Protected De functie kan enkel aangeroepen worden in de huidige blueprint en in iedere blueprint die afgeleid is van de huidige (via overerving).
Private De functie kan enkel aangeroepen worden in de huidige blueprint.

Pure – een pure functie laat geen wijzigingen toe aan elementen binnen de functie.

Onderstaande Array-functies tonen duidelijk het verschil aan (negeer de groen geselecteerde woordjes Array, dit komt door de zoekopdracht).

 

  • De blauwe functies zijn niet Pure functies en zullen de array zelf (kunnen) wijzigen via functionaliteiten als Add, Clear, Insert,….
  • De groene functies zijn Pure functies en zullen de array zelf niet beïnvloeden of wijzigen. Pure functies zullen enkel gegevens opvragen via functionaliteiten als Contains Item, Find item, Get, Length,….

Call in editor – de functie kan vanaf de editor uitgevoerd worden.

Inputs en Outputs

Onder de eigenschappen van het Graph-luik ziet u de mogelijkheid om Inputs en Outputs (parameters of argumenten genoemd) toe te voegen.

Onze huidige functie kent geen Input– of Outputparameters. Dit betekent dat onze functies steeds hetzelfde doet. Om onze functie flexibeler te maken kunnen we het best Input– en Outputparameters toevoegen.

Als u de functie van naderbij bekijkt in functie van Input en Output dan ziet u dat:

  • links – de input (Health en Damage)
  • Midden – de berekening die herhaald kan/zal worden, de eigenlijke reden van bestaan van de functie.
  • Rechts – de output (het printen op het scherm via de functionaliteit Print String).

Misschien eerst even opnemen dan het opnemen van een vaste vorm van output, zoals hier via een vaste afdruk op het scherm via Print String, niet aan te raden is. Het ontneemt immers de flexibiliteit om de uitvoer in een andere vorm weer te geven.

  • We kunnen dus 2 Input-parameters toevoegen (voor Health en Damage).
  • En 1 Output-parameter (ter vervanging van de Print String).

Om parameters toe te voegen klikt u op het +-knopje.

  • Klik op het +-knopje om een nieuwe Input-parameter toe te voegen.
  • Geef het vervolgens de gewenste naam en het gewenste datatype. Om een Health-parameter aan te maken kan eveneens de naam Health gebruikt worden maar moet u hetzelfde datatype kiezen als de verwachtte invoer, hier dus Float.

  • Herhaal dit voor Damage.

  • Let op, ik koos nu, toevallig of voor het gemak zo u wilt, dezelfde namen als de variabele namen. Dit kan, mag maar hoeft uiteraard niet.
  • Met het kruisje achteraan kan de parameter verwijderd worden, de pijltjes worden gebruikt om de volgorde te bepalen van de parameters.

Laat ons al eens in de Blueprint kijken wat we hebben.

Merk de 2 nieuwe pins (Health en Damage) op als deel van Healt-Damage functie.

We hebben ook nog een Output-parameter nodig.

  • Klik op het +-knopje om een nieuwe Output-parameter toe te voegen.
  • Geef het vervolgens de gewenste naam en het gewenste datatype. Omdat we 2 Float-datatypen van mekaar aftrekken resulteert dit opnieuw in een Float. Als naam heb ik gewoon Result (van resultaat) gekozen.

Aan de Blueprint is een nieuwe Return node toegevoegd die de Output-parameter bevat.

  • Verwijder nu de variabelen Health en Damage en de functionaliteit Print String (en de bijhorende conversie), we gaan deze immers allen vervangen door de parameters.

  • Compile en Save.
  • Keer terug naar de Event Graph.
  • U ziet hier de gewijzigde functie die vraagt om invoer en uitvoer. Merk op de pin Target met als waarde Self (standaard dus een verwijzing naar de eigen Blueprint (we laten dit voor nu ongewijzigd).

We verbinden nu de Parameterpins met de respectievelijke variabelen Health en Damage en verzorgen de uitvoer via Print String. Zodat de waarde van de variabelen worden toegewezen aan de parameters en deze parameters worden dan intern in de functie gebruikt om de berekening te maken.

Oké, u kunt u afvragen of al deze moeite nu wel nodig is voor een eenvoudige aftrekking, het eindresultaat ziet er nog steeds even complex uit! Misschien niet voor deze eenvoudige aftrekking, maar uiteraard kunnen functies ook complexere berekeningen bevatten.

Hieronder ziet u een voorbeeld dat de afstand tussen 2 punten berekent en merk op dat hier een functie zeker op zijn plaats is.

Hieronder ziet u het aanroepen van de functie. De winst is nu veel duidelijker!

Standaardwaarde toekennen aan parameter

Stel dat u standaard altijd wilt 20 Damage aftrekken maar dat u nog steeds wilt andere Damage-waarden toestaan.

  • Keer terug naar de Blueprint van de functie (dat kan door gewoon bovenaan op het tabblad van de functie te klikken).
  • Zet de Default Value van de Input-parameter Damage op 20,0 (omdat het een float is geven we een decimale waarde in).

  • Compile en Save.
  • Keer terug naar de Event Graph.
  • Verwijder de variabele Damage bij de G-event.
  • Compile, Save en Play.

Als u op de F-toets drukt wordt de waarde van de variabele Damage (10) afgetrokken, drukt u op de G-toets dan wordt de Default Value van de parameter (20) afgetrokken.

Een waarde als Referentie meegeven

Standaard wordt de waarde van de variabele die wordt meegegeven aan de parameter van de functie als een kopie meegegeven.

Concreet betekent dit dat als we de waarde wijzigen binnen de functie (wat in ons voorbeeld effectief gedaan wordt met de waarde van de paramater Health), deze wijziging geen invloed heeft op de variabele die we meegegeven hebben.

U kunt dit testen door in het bovenstaande voorbeeld op de H-toets te drukken. U zult zien dat de gezondheid steeds op 100 blijft staan, ook na herhaaldelijk klikken op de F– of G-toets.

Stel dat u toch wenst dat de waarde van de variabele die u meegeeft aan een parameter, bij wijziging van de waarde van de parameter binnen de functie, ook de waarde van de variabele wijzigt (buiten de functie) dan moet u de waarde bij referentie meegeven.

Concreet gaat u niet langer meer een kopie van de waarde meegeven, maar een referentie naar de waarde, u zou kunnen zeggen, de variabele zelf!

We gaan dit instellen voor de Health, als de parameter Health wijzigt binnen de functie moet die ook de waarde van de variabele Health wijzigen.

  • Keer terug naar de Blueprint van de functie (dat kan door gewoon bovenaan op het tabblad van de functie te klikken).
  • Vink Pass-by-Reference van de Input-parameter Health aan.

Merk op dat de pin een Ruit-vorm gekregen heeft en geen bolletje meer is.

Het ware leuk geweest had dit alles geweest dat er moest gebeuren maar we zijn er nog niet. We moeten de gewijzigde waarde nog toekennen.

  • Trek een verbindingslijn vanuit de Health-parameter en zoek naar Set By-Ref.

  • Er wordt een Set Float (by ref)-node aangemaakt (merk op dat hij automatisch het datatype herkende). De parameter Health is verbonden met de Target-pin.

  • Verbind het resultaat van de berekening met de Value-pin en verbind de de Exec-pins opnieuw zodat de Set Float (by ref)-node wordt opgenomen in de instructiereeks.

  • Compile, Save en Play.
  • Druk eerst eens op de F-toets (en/of de G-toets) om de functie Health-Damage aan te roepen en de berekening te maken.
  • Druk dan op de H-toets, merk op dat de gezondheid niet langer meer op 100 staat, maar gezakt is ten gevolge van het aanroepen van de functie Health-Damage.

Onderstaande video toont het verschil tussen de string functies Replace en Replace Inline waarbij Replace Inline By ref werkt.

Lokale variabelen

Merk op dat u binnen een functie ook lokale variabele kunt aanmaken. Deze variabelen zijn enkel gekend binnen deze functie zelf en zijn niet toegankelijk van buiten de functie.

Het geheugen die deze variabelen innemen wordt weer vrijgegeven van zodra u de functie beëindigt is. Dit zorgt voor een efficiënter geheugenbeheer.

Functions Override

Een klasse is vaak gebaseerd op een andere klasse (overerving). Deze basisklasse kan reeds functies bevatten. Als u de functionaliteit van deze basisfuncties wilt wijzigen, kunt u deze overschrijven.

U vindt ze naast het knopje om nieuwe functies aan te maken.


Macros

Macros hebben hetzelfde nut als Functies namelijk: de code leesbaarder en beter onderhoudbaar te maken door code die bij mekaar hoort en herhaalt zal/kan worden bij mekaar te plaatsen.

De keuze tussen het gebruiken van Macros of Functies hangt een beetje van uw persoonlijke “stijl”. Macros is een begrip dat u misschien kent uit Microsoft Office producten en misschien bent u daardoor beter vertrouwd met dit begrip. Een meer “klassiek” geschoolde programmeur zal dan eerder vertrouwd zijn met het begrip Functies.

Zijn er dan geen verschillen?

Toch wel, bv. een Functie kan geen Delay bevatten, een Macro wel. Een Macro heeft altijd een invoer en een uitvoer, we hebben gezien dat u de uitvoer, de Return Node, kan, maar niet moet toevoegen bij Functies.

Oké, laten we nu teruggaan naar onze beginsituatie, voor we Functies hebben toegevoegd, en deze nu opnieuw maken maar met Macros (omdat er veel gelijkenis is met Functies ga ik er iets sneller over).

Hieronder ziet u de begincode nog eens.

We kunnen een Macro vanaf 0 beginnen door in het My Blueprint-panel te kiezen voor +Macro, of u kunt een nieuwe macro maken door de gewenste noden te selecteren, rechts klikken en Collapse to Macro te kiezen (analoog met Collapse to Function).

Laten we nu eens starten vanaf een nieuwe Macro.

  • Klik in het My Blueprint-panel op +Macro.

  • Geef de Macro een naam, let op deze mag nog niet bestaan!

  • Ik heb er dan maar M_Health-Damage van gemaakt.

Merk op dat de Macro al meteen een node bevat voor Inputs en Outputs.

Bekijk het Details-panel en merkt gelijkaardige eigenschappen op. Enkel de eigenschappen Access Specifier, Macros zijn beschikbaar in het gehele project, en Pure ontbreken. U kunt wel een kleurtje via Instance Color toekennen aan de Macro zo u wilt.

U kunt op dezelfde manier als bij Functies Input– en Output-parameters toevoegen. Laten we dit doen.

  • Voeg Input– en Output-parameters toe analoog als bij de bovenstaande Functie (zie hieronder).

Merk op dat u net als bij Functies kunt een Default Value en een Pass-by-Reference. We hebben dit hierboven besproken.

Merk ook al op dat u aan Macros geen lokale variabele kunt toevoegen!

  • Voeg nu de bewerking toe.

  • Ga naar de Event Graph en voeg de Macro toe.

  • Compile, Save en Play.

Merk dat het resultaat identiek is an het gebruik van Functies. Aan u dus de keuze hoe u het wilt programmeren.

Meerdere Exec-pins

In tegenstelling tot Functies kunnen Macros wel meerdere Exec-pinnen bevatten, zowel aan de invoerkant als aan de uitvoerkant en kan zo mee het verloop (Control Flow) van het programma bepalen.

Ik neem het voorbeeld uit de officiële handleiding over.

  • Voeg parameters toe van het type Exec (deze staan helemaal bovenaan in de keuzelijst).

Dit ziet er als volgt uit in de Blueprint.

  • Voeg de gewenste code toe, bv. om te zien of u gewonnen hebt of niet.


Custom Events

Een andere variant op hetzelfde thema, code overzichtelijker maken en herhaling vermijden, zijn Customs Events.

De voornaamste verschillen zijn dat Custom Events rechtstreeks aan de Blueprint worden toegevoegd, de code blijft dus gewoon zichtbaar in de Blueprint en staat niet op zich zoals bij Functies en Macros, en dat Custom Events geen Output-pin hebben.

We bouwen gewoon verder op onze huidige Blueprint.

We gaan het afdrukken van de gezondheid via een Custom Event herwerken.

Een Custom Event maakt rechtstreeks in de Blueprint.

  • Klik met de rechtermuisknop ingedrukt op de Blueprint en zoek naar Add Custom Event.

  • Geef de Custom Event een gepaste naam (bv.  PrintGezondheid).

  • Gebruik knippen en plakken om de code die onder de H-event staat te plaatsen onder de Custom Event.

  • Zoek naar de Custom Event PrintGezondheid en maak de verbinden met de H-event.

  • Compile, Save en Play.

Alles werkt zoals het hoort.


Project Level Creation – Meshing pass

Er wordt wat decoratie toegevoegd aan het level.


Masterclass – Night Time

Onderstaande Masterclass legt uit hoe u een nachtscene kunt maken.


Sidekick – Materialen

Bij het bouwen van een 3D omgeving komt veel meer kijken dan enkel maar het programmeren. Denk aan belichting, AI (Artificiële Intelligentie), animaties, cinematics, geluid, particle effects, materialen,….

Het is teveel gevraagd om gespecialiseerd te zijn in al deze materie, specialisatie dringt zich op. Ik leg in deze handleidingen de nadruk op het leren programmeren en zal al heel tevreden zijn als u dit onder de knie krijgt. Maar toch wil ik u ook, via deze Sidekicks, laten kennismaken met de andere aspecten nodig om een 3D omgeving te bouwen. Ik doe dit aan de hand van videoreeksen die ik gespreid aanbied als aanvulling bij de eigenlijke handleidingen. Deze Sidekicks maken geen deel uit van de eigenlijke “leerstof”, voel u vrij ze al dan niet te bekijken.

Using Masks within Materials continue


Behandelde Basiscompetenties uit de module ICT Programmeren – Specifieke ontwikkelomgeving: eenvoudige functionaliteiten

  • IC BC017 – kan ICT veilig en duurzaam gebruiken
  • IC BC234 – kan de basisprincipes van programmeren in een specifieke ontwikkelomgeving toepassen
  • IC BC236 – kan eenvoudige wijzigingen aan een programma aanbrengen
  • IC BC241 – kan een programma in een specifieke ontwikkelomgeving maken
  • IC BC250 – kan bij het programmeren in functie van een specifieke ontwikkelomgeving, een juiste logica volgen

Geef een reactie

  • Abonneer je op deze website d.m.v. e-mail

    Voer je e-mailadres in om je in te schrijven op deze website en e-mailmeldingen te ontvangen van nieuwe berichten.