- Wat vooraf ging
- Gebruikte terminologie
- Inleiding
- Objecten
- Klassen
- Overerving
- De onderdelen van een klasse
- Een aantal typische kenmerken van object georiënteerd programmeren
- Alternatieve werkwijze voor het maken van een klasse
- Praktische voorbeelden
Wat vooraf ging
- U hebt reeds kennis gemaakt met Blueprint Visual Scripting in een Level Blueprint.
- U kunt variabelen gebruiken.
- U kunt functies aanmaken.
- U begrijpt de Control flow.
- U weet wat een Blueprint Class is.
Gebruikte terminologie
Objects
The base building blocks in the Unreal Engine are called Objects and contain a lot of the essential “under the hood” functionality for your game assets. Just about everything in Unreal Engine 4 inherits (or gets some functionality) from an Object.
Actors
An Actor is any object that can be placed into a level. Actors are a generic Class that support 3D transformations such as translation, rotation, and scale. Actors can be created (spawned) and destroyed through gameplay code (C++ or Blueprints).
Classes
A Class defines the behaviors and properties of a particular Object used in the creation of an Unreal Engine game. Classes are hierarchical, meaning a Class inherits information from its parent Classes (the Classes it was derived or “sub-classed” from) and passes that information to its children. Classes can be created in C++ code or in Blueprints.
Blueprints
The Blueprints Visual Scripting system in Unreal Engine is a complete gameplay scripting system based on the concept of using a node-based interface to create gameplay elements from within Unreal Editor. As with many common scripting languages, it is used to define object-oriented (OO) classes or objects in the engine. As you use UE4, you’ll often find that objects defined using Blueprint are colloquially referred to as just “Blueprints.”
Components
A Component is a piece of functionality that can be added to an Actor. Components cannot exist by themselves, however when added to an Actor, the Actor will have access to and can use functionality provided by the Component. For example, a Spot Light Component will allow your Actor to emit light like a spot light, a Rotating Movement Component will make your Actor spin around, or an Audio Component will make your Actor able to play sounds.
Inleiding
C++, de achterliggende programmeertaal van Unreal Engine, is een objectgeoriënteerde programmeertaal. Het programmeren in C++, en bij uitbreiding dus ook in Unreal Engine en in Blueprint Visual Scripting, is dus object georiënteerd.
Laten we dus eerst even stilstaan bij een aantal kenmerken van object georiënteerd programmeren en vervolgens hoe dit wordt toegepast in Unreal Engine Blueprint Visual Scripting.
Situering van deze handleiding binnen Unreal Engine
Objecten
In een objectgeoriënteerde programmeertaal staan objecten centraal.
Een object is de basisbouwsteen bij object georiënteerd programmeren. Zowat alles is opgebouwd uit objecten.
In een 3D-omgeving is dit nog duidelijker omdat de objecten, meestal, visueel te zien zijn: muren, deuren, ramen, stoelen, tafels, bomen,… dit zijn allemaal objecten. Maar ook de karakters, lichten,… zijn in wezen objecten. Objecten die we kunnen plaatsen en transformeren in level worden ook actors genoemd.
Al deze objecten hebben eigenschappen en functionaliteiten.
- Eigenschappen zijn kenmerken zoals hoogte, breedte, kleur,… die een waarde kunnen hebben.
- Functionaliteiten (functies) zijn acties die met, op of door het object kunnen worden uitgevoerd. Denk aan lopen, springen, instorten, licht geven,… Functionaliteiten kunnen ook gebruikt worden om de correcte, gewenste waarden toe te wijzen aan de eigenschappen.
Eens het project opgestart ziet u dat het Level reeds gevuld is met een aantal objecten, in Unreal Engine noemen we objecten die u in een level kunt plaatsen actors. Objecten of actors, in wezen zijn ze hetzelfde en ik durf ze dan ook wel eens door mekaar te gebruiken.
In onderstaande afbeelding ziet u deze actors met de actor CubeMesh geselecteerd.
Een overzicht van alle actors in een level vindt u in de World Outliner. De geselecteerde actor is ook hier geselecteerd.
Al deze objecten hebben eigenschappen en functionaliteiten.
De eigenschappen van de geselecteerde actor vindt u in het Details-panel. Hier kunt u ook waarden toekennen aan deze eigenschappen.
Functionaliteiten worden gebruikt om de waarden van eigenschappen via programmeercode te wijzigen.
We hebben dit reeds in een vorige post gedaan. We hebben toen de functies Set Visibility en Toggle Visibility gebruikt om de eigenschap Visibility van het object PointLight te wijzigen.
Klassen
Maar waar komen nu al die eigenschappen en functionaliteiten van deze objecten vandaan?
Ieder object dat we gebruiken in ons programma, iedere actor in het level, is in wezen een instantie van een specifieke klasse (Class in het Engels). De belangrijkste reden om een klasse aan te maken is de herbruikbaarheid van de klasse.
Een Class is een verzameling van eigenschappen en functionaliteiten die samen één geheel vormen.
U kunt een Class Muur aanmaken. Deze Class Muur zal de eigenschappen bevatten die de muur kenmerken. Denk aan hoogte, breedte, diepte en materiaal. Deze eigenschappen kunnen, binnen een Blueprint Class, aangemaakt worden als variabelen.
De Class Muur zal ook de functionaliteiten bevatten die van toepassing zijn op deze muur. Bv. een muur kan instorten. De programmeercode die nodig is om de muur te laten instorten wordt geplaatst in een functie Instorten. (Sommige object georiënteerde programmeertalen spreken van methoden als ze verwijzen naar functies binnenin een klasse. In C++, en bij uitbreiding dus in Unreal Engine, spreken we gewoon van functies en wordt het begrip methoden niet gebruikt). We hebben reeds gezien hoe u functies kunt aanmaken.
Wanneer we nu een object, op basis van een specifieke klasse, aanmaken (bv. we willen een muur op basis van de klasse Muur toevoegen aan een level) dan wordt een instantie van deze klasse gemaakt. De muur in ons level is dus een instantie van de klasse Muur.
Deze instantie, deze specifieke muur, heeft toegang tot eigenschappen en functionaliteiten van de klasse. Met andere woorden, we kunnen specifieke waarden geven aan de eigenschappen hoogte, breedte, diepte en materiaal, specifiek voor deze muur (voor dit object van de klasse Muur) en onze muur heeft de functionaliteit om in te storten.
Unreal Engine biedt een hele reeks ingebouwde klassen die we eventueel zelf kunnen wijzigen via de World Outliner in de Level Designer.
U herkent de klassen aan de mogelijkheid om ze te “editen”. Het gaat hem in dit voorbeeld om BP_Sky_Sphere (het object dat de “lucht” toevoegt aan het level) en ThirdPersonCharacter (ons “karakter” dat we besturen). Door de Edit … aan te klikken komt u in de Blueprint.
De StarterContent komt met een aantal Blueprints Classes.
- U vindt ze in de Content Browser onder Content – StarterContent – Blueprints.
- U kunt eventueel ook filteren op Blueprint Class.
Open onderstaande klassen en plaats ze in tabbladen naast elkaar zodat we ze gemakkelijk kunnen vergelijken.
Om er geen warboel van te maken neem ik de Blueprint_CeilingLight als basis voor de onderstaande uitleg (al zal ik geregeld specifiek naar een andere Blueprint verwijzen).
Blueprint Klasse
Binnen Unreal Engine wordt een klasse aangemaakt als een Blueprint klasse, Blueprints die eveneens werken volgens het principe van OOP (Object-Oriented Programming).
Voor we zelf een Blueprint klasse kunnen aanmaken moet u vertrouwd zijn met het begrip Overerving.
Overerving
Een klasse kan een andere klasse als parent hebben en zo de eigenschappen en functionaliteiten overerven van deze parent klasse (inheritance).
Bijvoorbeeld, u wilt een middeleeuws spelletje aanmaken bevolkt door boeren, soldaten en tovenaars. U zou nu 3 klassen kunnen aanmaken, een klasse Boer, een klasse Soldaat en een klasse Tovenaar. Maar moest u dit doen dan zal u al snel merken dat u dezelfde eigenschappen (lengte, breedte, gewicht,…) en dezelfde functionaliteiten (lopen, springen, zwemmen,…) hebben. Uiteindelijk zijn al boeren, soldaten en tovenaars allen mensen.
U kunt dus beter een klasse Mens aanmaken. Deze klasse bevat dan alle eigenschappen (lengte, breedte, gewicht,…) en alle functionaliteiten (lopen, springen, zwemmen,…) eigen aan de mens.
Nadien kan u een klasse Tovenaar aanmaken die de klasse Mens als parent klasse heeft en dus alle eigenschappen en functionaliteiten van de klasse Mens overerft en aanvult met typische eigenschappen en functionaliteiten voor een Tovenaar (bv. de functionaliteit Toveren).
Hetzelfde kunnen we doen met voor de klasse Boer en de klasse Soldaat.
En misschien wilt u wel verschillende klassen van soldaten maken, een boogschutter, een zwaardvechter, een lansier,… U kunt dus voor elk van deze “soldatenklasse” een klasse maken die de Klasse Soldaat als parent heeft (dat op zijn beurt overerft van de klasse Mens).
Let op, we moeten onderscheid maken tussen de eigenschappen en de waarde die we toekennen aan de eigenschappen. Bijvoorbeeld, lengte is een eigenschap die eigen is aan een mens en de eigenschap Lengte wordt dan ook aangemaakt in de klasse Mens en overgeërfd door de klasse Boer, Soldaat, Tovenaar,…. We kunnen nu elk mens, boer, soldaat, tovenaar hun eigen specifieke lengte geven zonder daarvoor een nieuwe klasse te moeten aanmaken.
Een nieuwe, overgeërfde, klasse is dus niet nodig wanneer u enkel andere waarden wilt geven. Een nieuwe, overgeërfde, klasse is enkel nodig wanneer u andere eigenschappen of functionaliteiten wilt toevoegen.
Om te weten wat de Parent Class is van een bestaande Blueprint Class.
- Open de Blueprint Class door te dubbelklikken (bv. de Blueprint_CeilingLight).
- Duid linksboven de naam van de Blueprint Class aan.
U krijgt een informatievenstertje waar de Parent Class aangegeven is. De Parent Class van Blueprint_CeilingLight is Actor.
Nu we weten dat een nieuwe klasse kan gebouwd worden op basis van een bestaande klasse kunnen we bekijken hoe klassen worden aangemaakt in Unreal Engine.
Een nieuwe klasse aanmaken
Tip, voor u begint nieuwe klassen aan te maken kunt u het best eerst een nieuwe folder aanmaken waarin u al uw eigen aangemaakte klassen bewaart.
- In de Content Browser, klik op Add New – New Folder.
Er zijn verschillende manieren om een nieuwe klasse aan te maken.
- In de Content Browser, klik op Add New – Blueprint Class (of klik met de rechtermuisknop in de net aangemaakte folder en klik op Blueprint Class).
- Of, klik in het menu op Blueprints – New Empty Blueprint Class….
U komt in onderstaand scherm waar u een Parent Class kunt selecteren. Bovenaan vindt u de meest gebruikte Parent Classes, onderaan kunt u eventueel een specifieke klasse zoeken (nadien verschijnt een knop Select om deze specifieke klasse als Parent Class te selecteren).
- Klik op de gewenste Parent Class.
Een actor converteren in een herbruikbare Blueprint Class
Een andere mogelijkheid is een bestaande actor te converteren naar een Blueprint Class.
- Selecteer de actor, eventueel in de World Outliner en klik op Blueprint/Add Script.
Gevorderde technieken
Er zijn een paar gevorderde technieken om klassen aan te maken, u vindt die onder de knop Blueprints.
De onderdelen van een klasse
Een klasse bestaat uit:
- Componenten
- Events
- Constructor
Componenten
Componenten is iets dat eigen is aan een Unreal Engine Blueprint Class en dat u niet in alle andere object georiënteerde programmeertalen terugvindt. Componenten zijn de onderdelen (objecten) waarmee de Blueprint class is gebouwd.
Zo zal onze “hangende lamp”-blueprint componenten bevatten voor de “hangende lamp” zelf (een StaticMesh) en een lichtbron (PointLight).
- Dubbelklik Blueprint_CeilingLight om deze te openen. Deze klasse kan gebruikt worden om “hangende lamp”-objecten toe te voegen.
- U komt wellicht in tabblad Construction Script, maar laten we dit even negeren. Klik op het tabblad Viewport. Hier vindt u de onderdelen, de componenten, waaruit deze klasse bestaat.
In het Components-panel ziet u eveneens de componenten en kunt u er nog anderen aan toevoegen.
Een belangrijk component is Scene1 of DefaultSceneRoot, of welk ander component er zich ook als eerste aan de basis bevindt. Het is dit basiscomponent dat de locatie (transformatie) van het object in de wereld bepaalt. De andere componenten hebben een locatie (transformatie) relatief ten opzichte van dit basiscomponent.
Componenten van andere Blueprints bekijken
De componenten van Blueprint_Wallsconce zijn bijna identiek aan de constructor van Blueprint_Ceilinglight. Er wordt gewoon een andere StaticMesh gebruikt die tegen een muur kan worden geplaatst.
De componenten van ThirdPersonCharacter zijn wel anders.
U ziet er het CapsuleComponent als basis dat een Mesh (het eigenlijke karaktermodel (de Mannequin)) en een ArrowComponent (die blauwe pijl die de kijkrichting van het karakter aanduidt en niet te verwarren is met een ) bevat. Verder is er ook een FollowCamera toegevoegd die zich achter het karakter bevindt (en zo voor de “ThirdPersonCharacter”-view zorgt). De camera hangt vast aan een zogenaamde CameraBoom.
Omdat ons ThirdPersonCharacter moet kunnen bewegen heeft het ook nog een CharacterMovement-component.
Events
Object georiënteerd programmeren is vaak Event-driven. Events zijn gebeurtenissen.
De instructies, programmeercode, worden geplaatst binnen deze events via de Event Graph.
Weet dat deze events, binnen de klasse, enkel kunnen werken met de componenten toegevoegd aan deze klasse.
Om de beschikbare events te zien, die beschikbaar zijn voor ene specifiek component:
- Selecteer onder My Blueprint – Components het gewenste component (bv. SM_Lamp_Ceiling).
- Rechts ziet u in het Details-panel de beschikbare events.
- Als u een event aanklikt wordt deze toegevoegd aan de Event Graph en kan het programmeren beginnen.
Merk op dat er in onze voorbeeldklasse Blueprint_Ceilinglight geen events aanwezig zijn.
Moest u een nieuwe Blueprint Class aanmaken met de Actor Class als Parent dan ziet u dat er reeds een aantal events klaar staan (zij het in het grijs omdat ze momenteel nog niet verbonden zijn.
Constructor
De constructor is een speciale functie die in elke klasse aanwezig moet zijn. De constructor stelt de beginwaarden in op het moment dat een object, een instantie van de klasse, wordt aangemaakt.
De Constructor maakt het mogelijk dat aan bepaalde eigenschappen door iedere instantie van de klasse (ieder object van de klasse) eigen specifieke waarden (voor die specifieke instantie) worden toegekend.
Concreet in ons voorbeeld, iedere keer we een nieuw “hangende lamp”-object toevoegen aan een level, wordt een nieuwe instantie van de klasse Blueprint_CeilingLight genomen en worden een aantal eigenschappen (de intensiteit, kleur en radius van het licht) ingesteld.
In de Blueprint Class vindt u de constructor in de ConstructionScript.
- Klik op het tabblad Construction Script om de constructor te bekijken.
Hieronder ziet u indentiek dezelfde constructor maar gewoon anders geordend.
Hoe is deze constructor gebouwd?
De Construction Script start bij de Construction Script-node.
Vandaaruit worden drie functies aangeroepen Set Intensity – Set Light Color – Set Source Radius. Set betekent dat een waarde aan een eigenschap wordt toegewezen/ingesteld (geSet). Merk hoe deze functies allen met elkaar verbonden zijn.
Deze 3 functies hebben allen dezelfde Target, namelijk het object PointLight1. De Target is dus het object waarvoor de eigenschap een waarde wordt toegewezen. Het is de intensiteit, kleur en radius van het object PointLight1 dat wordt ingesteld.
Om de waarden aan de eigenschap toe te wijzen worden initieel de Default Values (standaardwaarden) van de variabelen Brightness, Color, Source Radius gebruikt. Merk op dat deze variabelen een andere kleur hebben, dat is omdat ze van een andere type zijn (Brightness en Source Radius van het type Float (decimale getallen), Color van het type Linear Color).
De initieel toegekende standaardwaarden van deze variabelen kunnen eventueel per instantie gewijzigd worden, het zijn immer variabelen, en vervangen door meer specifieke waarde voor die specifieke instantie van onze “hangende lamp”-klasse. Dit maakt dat, ondanks de standaardwaarden, iedere instantie van onze “hangende lamp” een andere intensiteit, kleur en straal kan hebben.
Hoe werkt een constructor?
Als u de klasse Blueprint_CeilingLight sleept in het level dan wordt een instantie van die klasse, een “hangende lamp”-object (of nog correcter, een “hangende lamp”-actor) aan het level toegevoegd. Deze actor wordt gebouwd met de toegewezen componenten maar, door de constructor, heeft deze actor ook al meteen een intensiteit, kleur en radius meegekregen (op basis van de standaardwaarden van de specifieke variabelen).
Deze variabelen zijn echter, als eigenschappen, aanwezig in het Detail-panel en kunt u daar wijzigen om zo deze instantie van onze “hangende lamp” specifieke waarden voor de intensiteit, kleur en radius te geven.
Constructors van andere Blueprints bekijken
De constructor van Blueprint_Wallsconce is bijna identiek aan de constructor van Blueprint_Ceilinglight. Er zijn wel bijkomende settings (en bijhorende variabelen) nodig voor de Inner Cone Angle en Outer Cone Angle.
De constructor van BP_LightStudio is duidelijk veel complexer (breek er momenteel uw hoofd nog maar niet over).
Lege constructor
Iedere klasse (Blueprint) moet een Constructor hebben maar er moet niet altijd iets mee te gebeuren.
Neem maar eens een kijkje in de Blueprint van ThirdPersonCharacter (door het aan te klikken in de World Outliner in de Level Designer).
De Constructor Script-node is aanwezig maar bevat geen verdere verbindingen. Eigenlijk is dit logisch. Ons ThirdPersonCharacter is immer uniek, er hoeven dus geen andere instanties, met eigen specifieke waarden, genomen te worden.
Een aantal typische kenmerken van object georiënteerd programmeren
Encapsulation
Bepaalde eigenschappen en functionaliteiten binnen een klasse zullen enkel gekend zijn binnen deze klasse en zijn niet toegankelijk van buiten de klasse, vanuit andere klassen of het level. We spreken dan van inkapselen (Encapsulation).
Inkapselen is bepalen hoe men toegang krijgt tot een bepaalde klasse. Welke eigenschappen en functionaliteiten zijn van buitenaf toegankelijk (de interface) en welke zijn afgeschermd voor de “buitenwereld” en zijn enkel gekend binnen de klasse.
- Keer terug naar Blueprint_Ceilinglight.
De onderdelen waaruit de Blueprint Class bestaat ziet u in het My Blueprint-panel.
U ziet hier alle onderdelen waaruit de Blueprint Class bestaat.
- EventGraph – de Events (zie hieronder).
- Functions – met momenteel enkel de speciale ConstructionScript (zie hieronder).
- Macros – een Macro is variant op functies (zie later)
- Variables – variabelen zijn als het ware “opslagplaatsen” waar u waarden kunt bijhouden die u kunt (her)gebruiken. Deze waarden zijn niet constant maar kunnen variëren. De variabelen zijn hier onderverdeeld in twee groepen, de componenten en variabelen die vallen onder “Light”. Ze zijn, binnen een Blueprint Class, te vergelijken met eigenschappen.
- Event Dispatchers – Event dispatchers bundelen events om deze gezamenlijk uit te voeren (zie later).
De communicatie met de Blueprint Class verloopt via één van deze wegen.
Variabelen zijn standaard Public, dit betekent dat de waarde van de variabele ook van buiten de Blueprint Class kan gewijzigd worden.
Wilt u dit niet, wilt u niet dat de waarde van de variabele van buiten de Blueprint Class kan gewijzigd worden, dan selecteert u de variabele en vinkt u Private aan in het Details-panel.
Merk op dat een variabele een Default Value heeft (een standaardwaarde). De variabele Brightness heeft een standaardwaarde van 1700.0. Uiteraard kunt u deze veranderen, het is immer een variabele.
Polymorphisme en Function Override
Polymorphisme is de mogelijkheid om meerdere varianten van eenzelfde functie, met behoud van de naam van de functie, te programmeren. Het onderscheid tussen de varianten wordt bepaald door de “interface”, de parameters die worden meegegeven aan de functie.
Ok, dit is op dit ogenblik nogal abstract, onthoud gewoon het begrip.
Function Override is de mogelijkheid om een functionaliteit, een functie, uit een parent class te overschrijven met een nieuwe invulling van deze functie.
Bijvoorbeeld, de klasse Tovenaar erft de functionaliteit Lopen van de parent klasse Mens. Maar stel nu dat onze tovenaar op een geheel eigenzinnige manier loopt, zodanig zelfs dat de instructies, programmeercode, uit de functionaliteit Lopen van de parent klasse Mens, moeten gewijzigd worden. Dan kan men deze functionaliteit Lopen, binnen de klasse Tovenaar herprogrammeren met het behoudt van de naam Lopen. De originele functionaliteit Lopen uit de parent klasse Mens wordt overschreven door een gelijknamige functionaliteit Lopen in de klasse Tovenaar.
Iedere mens, instantie van de klasse Mens, iedere boer, instantie van de klasse Boer, iedere soldaat, instantie van de klasse Soldaat, zal nog steeds lopen op de originele manier zoals beschreven in de functionaliteit Lopen uit de parent klasse Mens, door de Boer en Soldaat overgeërfd. Maar de tovenaar, instantie van de klasse Tovenaar zal lopen volgens de hergeprogrammeerde functionaliteit Lopen uit de klasse Tovenaar.
In een Blueprint Class kunt u een functie van een parent class overschrijven door in het My Blueprint-panel te klikken op Add New – Override Function en vervolgens de gewenste functie aan te klikken die u wenst te overschrijven.
De ConstructionScript is op zich ook telkens een “Override” (u herkent het aan het pijltje voor de F in het icoontje). Hieronder ziet u een voorbeeld komende uit BP_LightStudio waar u het verschil kunt opmerken tussen de “gewone” functies (zonder pijltje) en de “overschreven” functie (met pijltje).
Alternatieve werkwijze voor het maken van een klasse
Onderstaande video toont een alternatieve werkwijzevoor het aanmaken van Blueprint Classes vertrekkende van reeds aanwezige actors binnen het level.
Praktische voorbeelden
Triggerable Light
Interactieve muurlamp
Onderstaande video’s tonen hoe u een klasse kunt maken van een muurlamp die geactiveerd wordt door het drukken op een toets aangegeven via een tekst.
Lichtschakelaar
Onderstaande video gebruikt een lichtschakelaar om meerdere lichten aan en uit te doen.
Zaklamp op batterij
Onderstaande video voegt een “zaklamp” toe aan uw karakter. De batterij ontlaadt bij gebruik en moet weer opgeladen worden.
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 BC247 – kan de bouwstenen van een specifieke ontwikkelomgeving gebruiken
- IC BC249 – kan de instellingen van een specifieke ontwikkelomgeving wijzigen
- IC BC250 – kan bij het programmeren in functie van een specifieke ontwikkelomgeving, een juiste logica volgen
- IC BC288 – kan ICT-problemen oplossen