Voorwoord Met bijzondere dank aan Coded Illusions, Koert van der Veer en Thijs Kruithof, presenteer ik hier mijn verslag over mijn afstudeerstage en opdracht bij Coded Illusions. Dit verslag is vertrouwelijk. Er staan verschillende beelden van het spel in dit verslag. Deze beelden zijn van zeer vroege interne testversies en zijn niet representatief voor het uiteindelijk product. Alles op de beelden is voor verandering vatbaar, of zou helemaal niet in het spel terecht kunnen komen. Het level uit de in-game en editor screenshots heb ik zelf gemaakt en komt niet voor in het spel. De screenshot op de voorkant is van het spel Assassin’s Creed van Ubisoft. Sijmen Mulder.
1
Inhoudsopgave Voorwoord .............................................................................. 1
4–Code on the moves............................................................ 15
Inhoudsopgave ........................................................................ 2
Haven, de game .................................................................. 5
Een nieuw systeem ............................................................ 16 Stapelen en invoer............................................................. 18 Met animaties.................................................................... 20 Eigen physics..................................................................... 23 Ontwerpdoelen ................................................................. 25
2–Special Moves ..................................................................... 6
5–Aanpak en implementatie .................................................. 28
Moves in games................................................................... 6 Soorten moves ..................................................................... 7
Ladder ............................................................................... 29 Leap of Faith...................................................................... 32 Wallhop ............................................................................ 34
1–Coded Illusions.................................................................... 3
3–Unreal Engine...................................................................... 9 UnrealScript ...................................................................... 10 Animaties .......................................................................... 11 Physics .............................................................................. 13 Pawns en controllers.......................................................... 14
6–Afsluiting ........................................................................... 36 Wat er minder ging............................................................ 36 Wat er goed ging ............................................................... 37 Conclusie .......................................................................... 38 Summary in English ............................................................... 39
2
1–Coded Illusions Voor mijn stage was ik op zoek naar een bedrijf in de game sector. Dit primair vanwege de major die ik volg, Game Technology. Ik was al bekend met een aantal bedrijven. Eén daarvan is Guerilla Games. Dit bedrijf werkt aan games voor Sony spelcomputers, en is de maker van de bestseller Killzone. Het bedrijf is na de release van Killzone opgegaan in Sony, maar werkt wel verder als zelfstandig ontwikkelaar in Nederland. Binnen dit bedrijf kende ik iemand, waarbij ik wel eens voorzichtig geïnformeerd heb naar een stageplek. Helaas bleek dit niet mogelijk. PlayLogic is een andere grote, bekende spelontwikkelaar. Behalve dat is het ook nog een internationale uitgever van spellen. Het manifesteert zich uitgebreid op gaming gerelateerde evenementen in Nederland, en is zelfs betrokken bij specifieke game-gerelateerde opleidingen in Nederland. De ontwikkelaar is echter gevestigd in Breda—terwijl ik in Purmerend woon, en behalve dat, spraken de games mij niet aan. In een nummer van het Ronduit Insite tijdschrift stond een artikel over Coded Illusions. Het artikel ging wat meer over het algemene spelontwikkelingsproces, maar mijn interesse was gewekt. Het bedrijf maakt net als Guerilla en PlayLogic zogenaamde AAA (triple-A) games. Dit zijn spellen met budgetten die vaak in de miljoenen lopen, en meestal worden uitgegeven en gepromoot door grote uitgevers. Wat Coded Illusions uniek maakt is de christelijke achtergrond van het bedrijf. Het hoopt door de games niet alleen een goede spelervaring, maar ook een boodschap over te brengen op de gamers. Deze boodschap is echter niet prominent aanwezig, het is een achterliggende gedachte. Het be3
drijf probeert de game die het in ontwikkeling heeft niet als ‘christelijke game’ in de markt te zetten. Coded Illusions is gevestigd in Rotterdam. Het heeft ongeveer 40 werknemers, waaronder een aantal stagairs. Het heeft ongeveer dezelfde organisatiestructuur als de meeste spelontwikkelaars: De afdeling Productie zorgt voor de facilitaire zaken, en is verantwoordelijk voor het eindproduct. Deze afdeling verdeelt de middelen binnen het bedrijf, en zorgt voor een goed verloop van het ontwikkelproces als geheel. Het verzorgt de zakelijke kant van het bedrijf. Game Design bedenkt de spelelementen, het achtergrondverhaal, en alles wat daar verder nog bij komt kijken. Alle ontwerpbeslissingen in het spel worden uiteindelijk door deze afdeling genomen. Voorbeelden van wat Game Design bedenkt zijn hoe wapens werken, de opbouw van levels in het spel, en de look & feel die het spel ongeveer moet hebben. Art zorgt voor al het artistieke werk. Zij maken de karaktermodellen, beschilderen de objecten uit het spel, maken levels, etc. Art maakt ook concept art. Dit zijn tekeningen die een sfeerimpressie moeten geven van het spel, zodat iedereen uit het bedrijf hetzelfde beeld heeft bij de te maken content. ‘Content’ is de verzamelnaam voor alles uit het spel wat zichtbaar en hoorbaar is. Tenslotte is er nog de afdeling Coding, die de code van het spel schrijft. Dit zijn de instructies voor de spelcomputer die ervoor zorgen dat het spel werkt. Alle interacties, alle bewegingen, het tekenen van alle objecten, het afspelen van geluiden—uiteindelijk wordt dit allemaal door de code gedaan. Dit is de afdeling waar ik werkte. 4
Zelda
Haven, de game Coded Illusions werkt aan het spel met de werktitel Haven. Deze werktitel is aangekondigd op Games in Concert. Hierbij werden er ook wat beelden uit de game en concept art getoond. Het spel is een zogenaamde action-adventure. Dit genre is een breed begrip, en betekent ongeveer zoveel als: ‘spellen waar teveel actie in zit om het een adventure game te noemen, maar te weinig voor een action game’. Spellen in dit genre hebben vaak een uitgebreid verhaal dat in de loop van het spel uitgediept wordt, onder andere door gesprekken met andere spelkarakters en tussenfilmpjes. Voorbeelden van bekende action-adventures zijn Zelda, Prince of Persia, en Metroid. Elk van deze spellen heeft een totaal andere stijl, maar een gedeelde factor is het prominent aanwezig zijn van een verhaallijn en actie-elementen. Deze drie spelreeksen hebben ook allemaal in beperkte mate platformelementen. Platformgames zijn spellen waarbij de speler met het spelkarakter springt, hangt, en rent over en op verschillende platforms in de wereld, om uiteindelijk het eindpunt te halen. Mario is zo’n spel. De genoemde action-adventures hebben dit allemaal ook een beetje: op bepaalde plekken moet je je behendigheid gebruiken om over smalle randjes te lopen, of om moeilijk toegankelijke plekken te bereiken. Over Haven zelf kan nog niet zo veel gezegd worden omdat er officieel nog weinig over bekend is. De site vermeldt dat het gaat om een spel dat zich afspeelt in de toekomst, in een verhaal geïnspireerd op het bijbelboek Openbaringen, dat gaat over het einde van de wereld. Prince of Persia 5
Wall jump in Metroid
2–Special Moves De opdracht van de stage is: “Bedenk een systeem om special moves af te handelen, en maak hiervan een prototype. Implementeer twee bestaande en twee nieuwe moves. Een systeem voor de AI om de moves te gebruiken is optioneel.” In dit verslag schrijf ik hoe ik deze opdracht heb aangepakt. Eerst zal ik beschrijven wat special moves inhouden. Voordat ik daarna overga tot een behandeling van het ontwerp en de implementatie, is het belangrijk om eerst wat achtergrondinformatie te geven over de gebruikte technologie.
Moves in games Een move is ruim te omschrijven als een ‘speciale handeling’. Special move systemen zitten al in een groot aantal spellen, in de een meer dan in de ander. Er zijn een aantal spellen die er bekend om staan. Een spel dat om zijn rijke traditie van moves en daarbij horende gedetailleerde animaties bekend staat is een spel dat ik al eerder heb genoemd, Prince of Persia. In platformers zijn special moves wat minder prominent aanwezig. Vaak bestaan ze wel, maar zijn ze transparant in de game geïmplementeerd—je merkt niet eens dat het ‘special moves’ zijn. Zo kun je in Mario springen met de A knop, maar je kunt je daarmee ook afzetten tegen een muurtje. Dat is de wall jump. Het is een special move, maar wordt niet zo duidelijk gepositioneerd als special move. Wall hop in Prince of Persia 6
Pro Evolution Soccer
Het idee van special moves is niet gebonden aan platformers en action adventure games. Het is in meer of mindere mate in veel andere soorten games terug te vinden waar een spelkarakter bestuurd wordt. Denk hierbij aan vechtspellen als de klassieker Street Fighter, of zelfs voetbalspellen als Pro Evolution Soccer. Een spel dat zeker niet mag ontbreken in een bespreking van games met special moves is het spel Assassin’s Creed. Dit spel werd in de pers geprezen om de bewegingsvrijheid en het fantastische klimsysteem. In het spel is het mogelijk om alles vast te grijpen wat maar een beetje uit de muur steekt, en daaraan op te klimmen. Ook kan de speler over daken rennen, en er was één bijzondere move, de Leap of Faith: hierbij kan de speler vanaf een aantal hoge punten naar beneden springen in het vertrouwen goed terecht te komen—heel toevallig altijd in hooibaal.
Soorten moves Er zijn twee soorten moves te onderscheiden:
Assassin’s Creed
De eerste soort zal ik atomaire moves noemen. Dit zijn moves die geïnitieerd worden door de context of door een speler, en vervolgens tijdelijk de controle over het spelkarakter (bijna) volledig overnemen. Hier is dus geen sprake van interactie na het initiëren van de move. Een paar voorbeelden hiervan zijn slidings in voetbalspellen, het oppakken van wapens in schietspellen en de Leap of Faith uit Assassin’s Creed.
7
Belangrijk om hier op te merken is dat hoewel de moves zelf over het algemeen kort duren, ze wel het spelkarakter in een aparte state1 kunnen zetten. Denk hierbij aan een move waarbij het spelkarakter zich tegen de muur aan drukt. De eigenlijke beweging waarbij dat gebeurt zou de move zijn, en het resultaat een nieuwe state voor het spelkarakter. Voor de speler lijkt het een grote doorlopende move. Ik zal hier later nog op terugkomen. De tweede soort zijn de niet-atomaire moves. Dit zijn moves die over een wat langere periode actief blijven, en waar de speler invloed op heeft tijdens het uitvoeren. Voorbeelden hiervan zijn het (nietautomatisch) beklimmen van een ladder, slingeren aan een touw, of over muren rennen. Bij de stage opdracht werd er een aantal voorbeelden gegeven van moves die het systeem moest ondersteunen. Dit waren onder andere rollen, ladder klimmen, en over een muurtje springen. Dit is een mengeling van atomaire moves en niet-atomaire moves. Bij het ontwerp zou ik rekening moeten houden met deze verschillende soorten moves.
1
Een state is een bepaalde modus. Voorbeelden zijn ‘gewoon’, ‘hangend aan een randje’, en ‘gebukt’.
8
3–Unreal Engine Voordat ik uitleg hoe ik te werk ben gegaan met de moves, is het belangrijk om de technische achtergrond van het project te kennen. De begrippen en technologieën die hier uitgelegd komen later in het verslag terug. Voor het ontwikkelen van Haven wordt er een engine gebruikt. Een game engine is een basis waarop een spel gebouwd kan worden. Het bevat een aantal onderdelen die vaak terugkomen in de code van spellen. Dit is meteen het belangrijkste voordeel van de engine—doordat deze onderdelen niet opnieuw geschreven hoeven te worden, kan er meteen begonnen worden aan het programmeren van het spel. Een aantal zaken waar de engine voor zorgt zijn geavanceerde grafische effecten, gameplay mechanics2, artificiële intelligentie, physics, ondersteuning voor verschillende spelcomputers zoals de Xbox 360 en Playstation 3. Ook belangrijk zijn de meegeleverde tools om de engine te kunnen gebruiken, zoals een level editor. Onderdelen die uniek zijn voor het spel moeten wel nog zelf geïmplementeerd worden. Special moves, het onderwerp van mijn afstudeeropdracht, is zo’n onderdeel.
2
Gameplay mechanics zijn de eigenlijke spelelementen
Gears of War (UE 3) 9
De engine die gebruikt wordt voor Haven is Unreal Engine 3 van Epic Games. Dit is een van de meest bekende, en wordt in veel andere, bekende spellen gebruikt. Voorbeelden hiervan zijn Gears of War, Unreal Tournament 3 en Mass Effect. Unreal Engine 3 is primair geschreven in C++. Dit omdat het een taal is die zich in de spelontwikkeling bewezen heeft, en het werkt op alle belangrijke platformen. Voor verschillende platformen (spel- en ‘gewone’ computers) heeft UE een aparte backend. Hierdoor werkt het op een aantal verschillende spelcomputers, terwijl de verschillen hiertussen zoveel mogelijk op de achtergrond worden gehouden voor de gebruiker van de engine. Dit hoofdstuk zal een aantal van de technische aspecten van de engine behandelen, omdat deze nodig zijn om een beeld te geven van de opdracht. Later zal blijken hoe deze onderdelen allemaal samenkomen in het special move systeem.
UnrealScript Het eigenlijke spel wordt geschreven in UnrealScript. Dit wordt gecompileerd en geïnterpreteerd door de engine. UnrealScript is een objectgeoriënteerde taal met een C-achtige syntax. Het is specifiek bedoeld voor gebruik in games. Vanwege het belang voor mijn project, en dus dit verslag, zal ik hier wat dieper op in gaan. Een klasse kan gedeeltelijk in UnrealScript, en voor het andere deel in C++ geschreven zijn. Dit is mogelijk door het native attribuut dat op een klasse gezet kan worden. Als een klasse native gedeclareerd is in UnrealScript zullen er headers voor C++ gegenereerd worden. Het is ook mogelijk om functies te declareren als een native functie. Dan staat BioShock (UE3) 10
deze wel in UnrealScript gedeclareerd, maar wordt de eigenlijke implementatie geschreven in C++. Voordelen van UnrealScript zijn onder andere de korte compilatietijd, en de mogelijkheden tot reflectie. Zo kan er bijvoorbeeld een lijst van variabelen opgevraagd worden. Hier wordt gebruikt van gemaakt in de editor.
BioShock (UE 3)
UnrealScript ondersteunt ook zogenaamde states. Een klasse kan een state definiëren. Wanneer deze state als actief wordt gezet, wordt er bij een functieaanroep eerst gekeken of de state die functie definieert. Zo ja, dan wordt die aangeroepen in plaats van het origineel. Dit is vergelijkbaar met subklassen, behalve dat er direct tussen states gewisseld kan worden. De scheidingslijn tussen wat in C++ en wat in UnrealScript geschreven wordt is niet duidelijk getekend. In ieder geval worden onderdelen waar snelheid cruciaal is, of er platformafhankelijkheid is, geschreven in C++. Zoals alle andere gameplay elementen, is UnrealScript de aangewezen taal voor special moves. Een aantal onderdelen, zoals de animaties en physics moesten wel in C++ geschreven worden om conventie- en snelheidsredenen.
Animaties Omdat de geloofwaardigheid van special moves sterk beïnvloed wordt door de kwaliteit van de animaties, moet er goede ondersteuning voor animaties in het movesysteem zitten. Mijn testlevel in de editor 11
Een stuk van een AnimTree
Unreal editor met eigen testlevel
De animator maakt voor spelkarakters een zogenaamde animatieboom. Aan de hand van een hiërarchische lijst van keuzes wordt beslist welke pose of animatie er wordt gebruikt. Dit is niet exclusief—een spelkarakter kan bijvoorbeeld 80% lopen en 20% rollen. In dit geval berekent de engine een tussenpose. Dit heet blending. Dit kan door het gebruik van een skeletal animatiesysteem. Hoe het precies werkt valt buiten het bestek van dit verslag, maar het komt er ongeveer op neer dat een spelkarakter door het animatiesysteem gezien wordt als een skelet waarvan de ‘botten’ apart geanimeerd kunnen worden. Deze hiërarchische lijst van beslissingen vormt de animatieboom. Voor de animator wordt dit gevisualiseerd als aan elkaar verbonden blokken. Een voorbeeld van zo’n blok is er bijvoorbeeld een die een animatie selecteert op basis van positie of huidige actie op een ladder, bijvoorbeeld ‘omhoogklimmend’, ‘omlaagklimmend’, of ‘stil’. Het idee van de boom is dat er op meerdere niveaus gezocht kan worden. Zo kan er bijvoorbeeld eerst gekeken worden of er gesprongen wordt, en zo ja, of dat was met een wapen in de hand. Deze animatieboom wordt de animation tree (of AnimTree) genoemd. De aparte blokken heten animation nodes (of AnimNodes). Een losse animatie of pose is hier een speciaal geval van en wordt een animation sequence (of AnimSequence) genoemd.
Enkele AnimNodes
In dit project zou één specifieke mogelijkheid van het animatie systeem in het bijzonder van belang blijken. Dit is wat verplaatsingsanimatie genoemd wordt. Om dit de begrijpen, is het nodig te weten hoe het skelet van het spelkarakter is opgebouwd. Dit begint bij de root bone. De rest van het skelet is hier vandaan opgebouwd 12
Bij verplaatsingsanimatie wordt dit bot van plaats verandert. Daarmee verandert de positie van het spelkarakter in de wereld. Hierdoor is het mogelijk om naast andere poses, ook een animatie met een beweging te maken. Een voorbeeld hiervan zou bijvoorbeeld rollen kunnen zijn. Daarbij maakt het karakter een koprol, en verplaatst daarbij een stuk naar voren. Dit zou met verplaatsingsanimatie gedaan kunnen worden.
Physics Het physics systeem zorgt voor een natuurgetrouw gedrag van objecten in het spel. Het zorgt voor zaken als zwaartekracht en realistische botsingen. Voor de physics maakt UE3 gebruik van de AGEIA PhysX library. Een library is een verzameling voorgeprogrammeerde functionaliteit. Het physics subsysteem kan een aantal verschillende modi afhandelen, zoals bijvoorbeeld ‘vallend’ of ‘lopend’. Ook wordt de modus aangepast mocht de speler bijvoorbeeld landen na een sprong. Dan gaat de modus over naar ‘lopend’. Deze functionaliteit is belangrijk omdat dat het ook mogelijk maakt om eigen modi toe te voegen. Dit kan bijvoorbeeld worden gebruikt voor een spin die op een muur kan lopen. Dat krijgt dan een aparte physics modus.
AEIGA PhysX in actie 13
Pawns en controllers De twee belangrijkste klassen voor de opdracht zijn HPawn en HPlayerController3. HPawn is een subklasse van Pawn, wat een actor is. Een actor is elk spelobject. Denk hierbij aan spelers, maar ook wapens, bewegende platformen, en triggers. Pawns zijn een speciaal type actors, dit zijn namelijk spelkarakters. HPlayerController is een subklasse van Controller—die overigens ook een actor is. Een controller ‘bestuurt’ eigenlijk een pawn. Het geeft de pawn opdrachten om dingen te doen. In het geval van de PlayerController leest deze acties als het indrukken van knoppen op de gamepad af, en geeft aan de hand hiervan opdrachten aan de pawn, zoals met de methode DoJump van Pawn. Tenslotte moet nog opgemerkt worden dat de pawn ook de verschillende physics modi afhandelt. In pawn zit een methode die de huidige physics modus bekijkt, en afhankelijk daarvan een methode aanroept. Is de huidige physics modus bijvoorbeeld ‘lopend’, dan wordt de methode physWalking() aangeroepen.
3
De H prefix wordt gebruikt door Coded Illusions voor hun eigen klassen.
14
4–Code on the moves Er was in de engine geen apart systeem voor moves beschikbaar. Een move, zoals het beklimmen van een ladder, bestond meestal uit verschillende onderdelen. Vaak was er een state voor in de PlayerController, en soms ook in Pawn. Verder werden er een aantal animatie nodes geschreven speciaal voor die move, en niet zelden een aparte physics modus, zodat de bewegingen apart afgehandeld konden worden. Er was ook geen centraal systeem dat het wisselen tussen verschillende moves kan afhandelen. Een move moest dus altijd zelf beslissen welke andere move de volgende zou zijn. Er is wel één methode, maar deze is niet helemaal toereikend. Hierbij wordt de nieuwe move ‘op de stapel gelegd’, dat heet een stack. Wanneer deze afgelopen is wordt de move er weer afgehaald, en wordt dat wat eronder lag weer actief. Zo wordt dus altijd de laatste state actief, maar ook dat is niet altijd gewenst. Wat is als grootste nadeel heb ondervonden van de originele methode is dat een move versplinterd raakt, met stukjes van de move in verschillende delen van het programma. Ter illustratie heb ik een aantal klassen genomen, en daaruit een hele kleine selectie van variabelen en states genomen. Hierbij heb ik alles wat betrekking heeft op het beklimmen van de ladder aangegeven. Zoals zichtbaar zal zijn uit het voorbeeld, is het grootste probleem hier dat alles nogal verspreid is. Door de moves worden HPawn en HPlayerController snel onnodig groot. Een nieuwe move toevoegen kost veel werk en tijd.
15
Een nieuw systeem Ik redeneerde dat de meeste moves bestaan uit een opeenstapeling van atomaire moves. Zo kan de ladder move opgedeeld worden in: omhoogklimmen, omlaagklimmen en omlaagglijden. De tussenstaat, waar de speler aan de ladder hangt, zou door de overkoepelende move worden afgehandeld. Om dit verder uit te werken heb ik verschillende moves, die mijn bedrijfsbegeleider me genoemd heeft, ontleed in zo klein mogelijke eenheden. De bedoeling was om een boomstructuur te krijgen. In zo’n structuur staat er één move bovenaan, en bestaat deze weer uit verschillende moves. Deze worden de kinderen genoemd. In dit geval staat er aan de top bijvoorbeeld LadderMove, met als kinderen LadderClimbUpMove en LadderClimbDownMove. Alle kinderen aan het einde zijn de atomaire moves. Met een systeem dat op deze manier opgebouwd zou zijn zou het mogelijk zijn om met een aantal basisbouwstenen op een simpele manier een groot aantal verschillende moves te kunnen maken. Om deze basisblokken te identificeren heb ik atomaire moves die ik gevonden had vergeleken op functionaliteit. Twee soorten kwam ik daarbij veel tegen. De eerste was een move die slechts bestaat uit een animatie, eventueel met beweging. Omhoogklimmen op een ladder is hiervan een voorbeeld. Dit is waar de eerder genoemde verplaatsingsanimatie handig bleek te zij. Door hiervan gebruik te maken zou het mogelijk worden om dit soort moves heel makkelijk te implementeren. Een andere steeds terugkerende atomaire move was die van een verplaatsing van de pawn over korte afstand. Een voorbeeld hiervan is het 16
‘goed voor een ladder gaan staan’ voor de ladder move. Het toeval kwam mij te hulp, want juist toen ik aan het ontwerpen was werd de generic mount feature afgerond. Hiermee is het mogelijk om de pawn opdracht te geven om zich volautomatisch naar een opgegeven positie te begeven. Animatie wordt daarbij meteen geregeld. Het diagram hiernaast was een van de eerste die ik heb gemaakt. Achter de naam ‘blocking anim’ zit nog wat meer dan wat ik tot nu toe heb uitgelegd. Mijn idee was namelijk dat het standaardgedrag van een MultiMove—de superklasse van nodes in die tree die meerdere kinderen hebben—in principe sequentieel uitgevoerd zou worden. Dan zou het programmeerwerk voor een move zich alleen hoeven te beperken tot het instellen van de juiste parameters, en de overkoepelende controller. Dat zou op het voorbeeld dus slechts ‘ladder’ zijn. Nadeel van dit systeem is dat het veel van de (spel)computer zou eisen. Er zouden veel objecten nodig zijn, voor elk onderdeel van de move één. Dit zou meer geheugen gebruiken dan acceptabel was. Daarom heb ik dit idee genomen, maar op een andere manier geïmplementeerd. In de Move klasse heb ik een enkele functie, StartBlockingAnim gemaakt. Deze start zo’n blokkerende animatie, eventueel met beweging. Wanneer de animatie afgelopen is wordt OnBlockingAnimEnd() aangeroepen. In deze functie kijkt de move wat er als volgende gedaan moet worden. Dit systeem is iets minder mooi dan een bijna volautomatische boom van move onderdelen, maar wel veel efficiënter. Bovendien maakt het het overzetten van bestaande moves naar het nieuwe systeem veel makkelijker—eerst kan de code uit de states van de player controller en pawn worden overgezet naar een eigen move, en wanneer dit werkt
17
kunnen onderdelen stuk voor stuk vervangen worden door blokkerende animaties. Dit is hoe ik de ladder heb geïmplementeerd. De move heeft twee Tick functies: TickPawn() en TickController(). Zoals de namen impliceren, worden deze functies respectievelijk vanuit Pawn en een Controller aangeroepen op de huidige move. In principe is het zo dat in TickController() de eigenlijke logica zit met betrekking tot wat er gedaan moet worden. TickPawn() voert die dan uit.
Stapelen en invoer Nu zijn de moves wel omschreven, maar er is nog steeds geen manier om te regelen welke move wanneer actief wordt. Sommige moves worden actief bij een hele specifieke actie van de gebruiker. Hierbij wordt er op een knop gedrukt, en de move wordt actief. Alhoewel, dit is wat voorbarig—het zou natuurlijk kunnen zijn dat je een gegeven move niet kan doen vanaf een bepaalde status. Een voorbeeld is rollen: het spelkarakter kan niet rollen terwijl deze aan het vallen is. Een ander probleem is dat sommige acties leiden tot verschillende resultaten afhankelijk van wat er op het moment gebeurt. Springen van een ladder werkt bijvoorbeeld anders dan het springen vanaf de vloer. De vraag is hier dus hoe er gekozen wordt welke specifieke actie (‘springen vanaf ladder’) er wordt uitgevoerd bij een opdracht voor een abstracte actie (‘spring’). Ook kan opgemerkt worden dat er voor de speler geen onderscheid is tussen het doen van een actie zoals springen, waar de move op reageert, en het direct starten van een nieuwe move, bijvoorbeeld rollen. Daarom was ik er van overtuigd dat er ook in de code weinig onderscheid tussen gemaakt zou moeten worden. 18
In het ontwerp dat ik hiervoor maakte voegde ik een methode toe aan HMove, HandleAction(). Deze neemt als enkel argument een naam, en geeft ‘true’ terug indien de actie afgehandeld is, of ‘false’ indien niet. In HPlayerController kwam er een extra methode die de invoer afvangt en doorgeeft aan de move. Dit werkt zo: in een .ini bestand staat aangegeven welke methode van PlayerController wordt aangeroepen, eventueel met een argument. In dit geval werd dus die nieuwe methode aangeroepen. Deze geeft vervolgens de opdracht door aan de huidige move, die kijkt of die er wat mee kan. Zo niet, dan kijkt de PlayerController of het toevallig de naam van een move is, en maakt in dat geval zo mogelijk die move actief. Moves kunnen zelf aangeven of ze actief willen worden. Eerst had ik daar een methode CanBecomeActive() voor. Als de move dan geactiveerd werd, werd Activate() aangeroepen. Het bleek echter dat er veel dubbele code zat in die twee functies, daarom heb ik de eerste iets anders genoemd: PrepareForActivation(). Dat impliceert dat dit voorbereidende werk niet meer in Activate() gedaan hoeft te worden. Er is echter nog een andere manier waarop een move actief kan worden, namelijk passief. Een voorbeeld is als de speler valt en op de grond terecht komt. Tijdens het vallen zou de ‘val’ move actief kunnen zijn. Bij het landen zou dan automatisch de ‘loop’ move actief worden. Het systeem dat ik hiervoor heb gemaakt was een van de meest complexe in het special move systeem, maar dat had niet veel simpeler gekund zonder de functionaliteit ervan aan te tasten. In de code is er een prioriteitsvolgorde van moves gedefinieerd. Een move kan vervolgens aangeven of hij de actieve move wenst te worden, of anders kan worden—dit onderscheid is belangrijk. 19
Elk frame wordt er een check gedaan of een nieuwe move actief wenst te worden. Als er een move met een hogere prioriteit actief wenst te worden, wordt deze dat. Een voorbeeld hiervan is de ladder move. Als de ladder move ziet dat het spelkarakter tegen een ladder aanloopt, zal deze aangeven te wensen om de nieuwe move te worden. Zo niet, dan wordt er geen andere move geactiveerd. Het is ook mogelijk om een geforceerde overgang naar een nieuwe move te doen. In dit geval wordt er net zoals hiervoor eerst gekeken of er een move is die wenst geactiveerd te worden. Als geen van de moves actief wenst te worden, wordt de lijst achterstevoren teruggelopen om te kijken of er één is die activatie niet wenst, maar wel accepteert. De reden dat er dan achterstevoren gezocht wordt, is omdat de dan logische moves zoals ‘lopen’ achteraan in de prioriteitenlijst staan. Een geforceerde overgang naar een nieuwe move wordt bijvoorbeeld gebruikt als een move afgelopen is, en er een nieuwe geactiveerd moet worden.
Met animaties Juist bij een special move systeem is het belangrijk dat er goede animaties gemaakt kunnen worden. Zoals eerder genoemd zijn er de blokkerende animaties, maar dat alleen is niet genoeg: ook wanneer er geen blokkerende animatie wordt afgespeeld moet het spelkarakter een bepaalde pose of animatie hebben. Daarom moet de animator ook informatie uit het systeem kunnen halen over wat er precies gaande is. Een heel belangrijk onderwerp hier zijn de overgangen tussen moves. Bij het bespreken van mijn opdracht werd er een scenario voorgesteld, waarin er vloeiend gewisseld kon worden tussen verschillende moves. 20
Het voorbeeld was het spelkarakter dat aan een randje hangt, en opzij klimt langs de muur in de richting van een ladder. De onderkant van de ladder is op dezelfde hoogte als het randje. Daar aangekomen, klimt het spelkarakter met een vloeiende beweging op naar de ladder. Dit voorbeeld illustreert een veel voorkomende soort overgang tussen moves, van de een naar de ander. Eén van de doelen van het animatiesysteem was het mogelijk maken om dit soort overgangen goed te kunnen animeren. Ik heb verschillende animatiesystemen overwogen. Bij het eerste ontwerp ging ik (te veel) uit van een directe overzetting van de eerder genoemde ‘move tree’. Het idee was dat er een AnimNode zou komen, waarbij je kon opgeven voor welke submove deze van toepassing was. Daar kon je vervolgens een lijstje van animaties opgeven die die move zou kunnen doen. Dat werden dan de outlets van die node. Er waren drie problemen met deze methode: ten eerste zou de animator voor elke submove een node moeten toevoegen in de animatieboom. Dit zou veel te veel zijn, en problemen geven met de workflow en het systeem trager maken. Het tweede probleem is dat de animator bekend zou moeten zijn met de precieze opbouw van de hele animatieboom. Tenslotte forceert dit systeem de hiërarchie van de moves op de animatieboom. Het eerste animatie ontwerp. Moves boven, voorbeeld AnimNodes onder. (De animator kan de move kiezen in de node, en voegt de ‘outlets’ toe.)
Alle drie de problemen hebben een gedeelde oorzaak. De node is beperkt tot één move. Het tweede ontwerp ontstond uit het verwijderen van die beperking, en er zo een ontwerp van te maken waarbij de animator outlets kan aanmaken voor elke animatie, uit welke move dan ook. Eventueel zou de animator er voor kunnen kiezen meerdere nodes
21
aan te maken met elk een verschillende set animaties er in, om het zo beter in zijn AnimTree te passen. Dit was een goed idee, maar er was een beperking—ik had nog geen rekening gehouden met de overgangen tussen de moves. Mijn eerste ingeving was om de animator ook overgangsscenario’s in te laten voeren in dezelfde node. Dit bleek niet zo’n goed idee. Het paste niet goed in de denkwijze van de animator, en de nodes zouden wel heel erg groot worden door een exponentiële toename van het aantal mogelijke combinaties tegenover het aantal moves. Het was duidelijk dat de overgangen niet in die node konden, er moest dus een andere komen.
Het tweede animatie ontwerp. (Hierbij kan de animator alle move animaties en overgangen benoemen vanuit één node.)
De volgende stap was het bedenken hoe deze AnimNode eruit zou gaan zien. Het meest voor de hand lag een node waarbij de animator outlets zou kunnen aanmaken voor de verschillende mogelijke overgangen. Maar dit zou het probleem alleen maar uitstellen, want ook de grootte of het aantal van deze nodes zou explosief toenemen met het toevoegen van nodes. Als oplossing bedacht ik dat deze nieuwe node uit zou kunnen gaan van de huidige move. In deze node geeft de animator dan aan of hij wil kijken naar het in- of uitblenden4 naar of van een andere move. Daarna kan hij outlets maken voor de verschillende moves. Zo kan de animator zeggen: ‘als er nu overgegaan wordt naar een andere move, kijk dan welke, en selecteer aan de hand daarvan een animatie’.
4
Blenden betekent hier ‘geleidelijk overgaan naar’ 22
Bij dit ontwerp zal misschien opvallen dat er hier gekozen kan worden voor het inblenden vanaf één move, en het uitblenden naar een andere toe. Bij de eerdere ontwerpen ging ik er vanuit dat eerst move A actief is, daarna een overgang AB, en daarna B. Die gedachtegang vormde de basis van het probleem dat hier ontstaan was, omdat het aantal combinaties tussen moves groot zou zijn. Met dit aparte in- en uitblendsysteem ziet het er anders uit: eerst is move A actief, die gaat dan naar uitblendmodus, dan wordt move B actief in inblendmodus, en dan gaat de inblendmodus uit, zodat move B ‘gewoon’ actief is. Tijdens het blenden is voor de animator het gegeven beschikbaar naar of van welke move er geblend wordt. Het is ook nog belangrijk om de vraag te beantwoorden hoe de nodes aan de animatienamen komen. Dit gebeurt op de volgende manier: eerst wordt er gekeken of er een blokkerende animatie actief is. Als dat het geval wordt deze gebruikt. Wanneer er geen blokkerende animatie actief is, wordt de ‘normale’ animatienaam gebruikt. Deze kan vrij worden ingesteld door de move.
Eigen physics Het uiteindelijke animatie ontwerp. (Het opgeven van overgangen gebeurt nu in een apart node.)
Het laatste element van de moves zijn de physics. Dit lijkt een ingewikkeld en imponerend onderwerp, en dat is niet onterecht. Physics engines zijn lastig om goed te maken, en vereisen veel kennis, vooral in de natuur- en wiskunde. Gelukkig was het bij de moves niet zo’n heel groot probleem. In het Unreal Engine hoofdstuk heb ik al uitgelegd hoe het Unreal Engine physics systeem ongeveer werkt. Het harde werk wordt gedaan door de achterliggende physics engine, en de moves hoeven in de meeste gevallen niets meer te doen dan deze aansturen. 23
Moves die eigen physics nodig hadden in het oude systeem, hadden een eigen physics modus. Daarbij hoorde een physNaamVanModus() functie die physics dan aanstuurt. Dit systeem was onhandig, omdat het zorgde voor nog meer verspreiding van move code. Ik zou een manier moeten bedenken om de physics code van de moves naar de move klasse zelf te verplaatsen, zonder het oude systeem hiervoor om te bouwen. Die laatste beperking was opgelegd door de aard van de opdracht: de engine moest zoveel mogelijk onveranderd blijven, zodat er geen problemen met engine-updates zouden ontstaan. Dit physics systeem is onderdeel van de engine, en daarom moet de physics-implementatie voor de moves er naadloos op aansluiten. Gelukkig vond ik snel een oplossing. In de Pawn klasse bestaat er een functie handlePhysics(). Deze kijkt welke physics mode actief is, en roept de desbetreffende functie aan. Ik voegde een functie HandlePhysics() toe aan de move klasse. Deze geeft een booleanse waarde terug, die aangeeft of de move hem afhandelt. Standaard gebeurt dat niet, en wordt er false teruggegeven. Voordat handlePhysics() naar de physics state gaat kijken, roept deze eerst HandlePhysics() aan van de huidige move. Als deze hem afhandelt, wordt de physics state genegeerd. Anders wordt deze zoals gebruikelijk afgehandeld. Op deze manier hebben moves de optie om de physics af te handelen, zonder dat er extra code geschreven hoeft te worden mocht de move dat niet wensen, of dat er code ergens anders in het spel geplaatst moet worden. Overigens is de physics code in C++ geschreven, vanwege snelheidsredenen.
24
Ontwerpdoelen Voordat ik overga naar de implementatie van het ontwerp in het volgende hoofdstuk, wil ik nog iets vertellen over de verschillende ontwerpdoelen die ik voor ogen had tijdens de implementatie. Deze heb ik niet vanaf het begin gehanteerd—ik ondervond de noodzaak hiervoor tijdens de ontwikkeling zelf. Ten eerste was het mijn bedoeling alles zo simpel mogelijk te doen. Dit was vooral in het begin bij het ontwerpen van de code lastig, omdat het moeilijk is om goed gebruik te maken van de mogelijkheden die al geboden worden bij een beperkt aan inzicht in het bestaande ontwerp en bestaande mogelijkheden. Door dit gebrek aan inzicht dreigt al snel het gevaar om veel te uitgebreide of ingewikkelde ontwerpen te maken. Een manier die hielp bij het ‘keep it simple’ doel is door mijzelf voor te houden dat wat ik toevoegde, niet iets heel bijzonders, of groots was. Daardoor had ik ook niet meer de neiging om iets te maken dat ingewikkelder of complexer was dan andere onderdelen. Dit doel werd mij duidelijk bij het werken aan de move tree, zoals ik eerder in dit hoofdstuk beschreef. Eerst had ik een complex systeem, met een hele boom van moves. Uiteindelijk is de essentie teruggevoerd tot één enkele functieaanroep in de HMove klasse. Op dat moment werd mij duidelijk hoe belangrijk het is om het simpel te houden™. Het tweede ontwerpidee waar ik mij op wilde concentreren was het gebruik van push in plaats van poll. Het verschil hiertussen zal ik uitleggen met een voorbeeld:
25
De speler drukt op een knop om te springen. Het spel registreert deze invoer, en begrijpt dat de speler wil springen. Daarom zet het spel de flag bWantsToJump. Een flag geeft aan of er iets aan de hand is, in dit geval dus of de speler wil springen. Tijdens het bijwerken van het spelkarakter, wat elk beeldje opnieuw gebeurt, kan een stukje ladder-gerelateerde code zien dat bWantsToJump aan staat, terwijl dat op dat moment niet kan, bijvoorbeeld omdat de speler nog bezig is met het omlaagglijden langs de ladder. Daarom beslist de code om bWantsToJump weer uit te zetten. Op die manier zitten er nog veel meer checks in, door de code verspreid. Aan het eind van de rit wordt gekeken of bWantsToJump nog aan staat, en zo ja, dan wordt er ‘echt’ gesprongen. Het nadeel hiervan zal duidelijk zijn: het wordt zo ontzettend moeilijk om te achterhalen wat er in bepaalde gevallen gebeurt, of waarom iets gebeurt zoals het gebeurt, omdat de code die daar invloed op heeft door verschillende functies in het spel verspreid kan zijn. Om die reden wilde ik met zo weinig mogelijk statusvariabelen werken, zoals die flag. In plaats daarvan zou er in het voorbeeld van springen één functie zijn die alle checks doet. Die functie zou dan al op het moment van indrukken van de ‘spring’ knop aangeroepen worden— push dus. Op die manier zijn er geen statusvariabelen meer nodig om bedoelingen tussen verschillende onderdelen van het spel te communiceren.
26
VANWEGEHETGEBREKAA NCONTENTOPDERECHTE RKANTVANDEZEPAGINA WASHETNIETMOGELIJK DEZEKANTTEVOORZIEN VANEENZINNIGGRAPPI GOFZELFSNUTTELOZEA FBEELDINGINPLAATSD AARVANHEBIKERVOORG EKOZENOMDEZEKANTTE VULLENMETDEZETEKST UELEDECORATIEOVERI GENSNOGHARTELIJKBE DANKTVOORHETLEZENV ANMIJNSTAGEVERSLAG
Op sommige punten was het echter onvermijdelijk om wat statusvariabelen voor communicatie te hebben. Dit was dan vooral het geval bij zaken waar eerst een functie uitgevoerd wordt die de opdracht geeft, en pas later, onafhankelijk, een andere functie de taak uitvoert indien nodig.
27
5–Aanpak en implementatie In het voorgaande hoofdstuk heb ik vooral beschreven hoe ik het systeem ontworpen heb. Dit was ook het primaire doel van de opdracht. Daarnaast was er nog vereist dat ik een werkend prototype zou maken, met daarin voorbeeldimplementaties van bestaande moves, en twee nieuwe. In dit hoofdstuk leg ik uit hoe ik de verschillende moves heb geïmplementeerd voor het prototype. Ik heb het project iteratief aangepakt. Ik maakte steeds een ontwerpje, en implementeerde het dan als een concept om te kijken of het goed werkte, en wat er beter kon. Bij sommige ontwerpen bleek al op voorhand dat ze niet afdoende waren, bijvoorbeeld uit overleg met de bedrijfsbegeleider. In die gevallen ben ik meteen terug gegaan naar de ontwerptafel. Tijdens het implementeren ben ik steeds uitgegaan van de ladder move. Deze move had alles in zich wat het systeem nodig zou hebben: standaard bouwblokken in de vorm van blokkerende animaties voor het klimmen, eigen stukjes logic voor bijvoorbeeld het naar beneden glijden langs een ladder en de besturing, en eigen physics. Het was oorspronkelijk de bedoeling dat ik twee bestaande moves zou implementeren. Enkele moves die als tweede showcase in overweging zijn genomen waren rollen, en het klimmen over een randje. Rollen leek een goed voorbeeld van hoe een animatie-met-verplaatsing move zou werken. Dit bleek alleen wat ingewikkelder dan eerst gedacht, omdat er nog extra dingen bij zaten zoals padcorrectie als er schuin tegen een muur aan gerold wordt. Het alternatief was om de be28
staande code om te bouwen tot een move. Omdat alle aspecten die hierbij van belang waren al waren gedemonstreerd met de ladder move, was het een beter idee om de tijd de besteden aan de implementatie van nieuwe moves. Hetzelfde gold voor het klimmen over een randje, deze lijkt wat opbouw betreft heel veel op de ladder. De twee nieuwe moves waren snel bedacht. De eerste was vergelijkbaar met de Leap of Faith uit Assassin’s Creed, welke genoemd is in het hoofdstuk ‘Special moves’. Hierbij springt het spelkarakter van grote hoogte naar beneden, om daar precies goed uit te komen. Mijn bedrijfsbegeleider stelde een tweede nieuwe move voor, de wallhop. Deze is onder andere bekend uit Gears of War en Call of Duty. Hierbij kan de speler snel over een muurtje heen springen.
Ladder De ladder move bestaat uit het beklimmen en afdalen van een ladder. Deze ladder wordt door de leveldesigner in het level neergezet. Wanneer de speler tegen de ladder aan loopt, zal het spelkarakter de ladder een stukje opklimmen. Daarna is er vrije controle op de ladder—de speler kan omhoog en omlaag klimmen, omlaag glijden, en van de ladder af springen. Er is een duidelijk verschil tussen de technische ladder, en dat wat het speler ziet in het spel. Dit zijn twee verschillende objecten. Wat de speler ziet in de level is niets anders dan zichtbare geometrie, het heeft geen effect op de werking. De technische ladder is een apart object dat de designer neerzet om aan te geven dat daar een ladder beklommen kan worden. Dit is zo gedaan zodat de designer niet beperkt is tot een Centraal Station Rotterdam 29
kleine set bruikbare levels, maar alle levelgeometrie kan gebruiken als ladder. Het beklimmen van de ladder wordt voor een groot deel afgehandeld met animaties. Het omhoog- en het omlaagklimmen en het op- en afstappen zijn blokkerende animaties met beweging. Ze worden door de ladder move gestart, ze zorgen zelf voor de verplaatsing van het karakter, en wanneer ze klaar zijn neemt de ladder move het weer over. Het naar beneden glijden wordt wel helemaal door de code afgehandeld. Dit wordt overigens ook gebruikt bij het vastgrijpen van een ladder vanuit een val of sprong. De kans is groot dat de het spelkarakter daar niet precies met zijn handen en voeten op een spaak uitkomt. Daarom wordt er een stukje omlaag gegleden zodat de handen en voeten precies goed uitlijnen met de spaken, wat er realistisch uitziet. De physics van het ladder gebeuren wordt door de move zelf afgehandeld. Dit omdat het spelkarakter wanneer het zich op de ladder bevind bijvoorbeeld niet (zichtbaar) onderhevig is aan zwaartekracht. Bij de Unreal Engine werd er een werkende ladder move meegeleverd, maar dit werkte niet zoals de bedoeling was in Haven. Om die reden is deze move herschreven, speciaal voor het spel, met als resultaat het laddersysteem dat ik hierboven beschreven heb. Het uitwerken van de ladder naar een aparte move was een interessante onderneming. Omdat de ladder zoveel aspecten omvat, was de code door een aantal klassen verspreid. Zo was er een state in de PlayerController, een gigantische animatie node in de AnimTree, en een HLadder klasse voor de ladder zelf.
30
Ik ben begonnen door de state code uit PlayerController over te zetten naar een aparte move. In tegenstelling tot wat ik verwachtte, heb ik hier veel tijd aan moeten besteden. Er waren veel kleine probleempjes met dingen die zich vanuit een aparte move klasse net iets anders gedroegen, door net iets ander timing bijvoorbeeld. Ook moest ik de logica herschrijven die regelt wanneer de ladder move actief wordt, zodat deze gebruik zou maken van het prioriteitensysteem zoals ik in een eerder hoofdstuk beschreef. Nadat de foutjes eruit waren die bij het overzetten er in geslopen waren, werkte het goed. Maar eigenlijk was het nog niet een echte move, maar gewoon de oude implementatie in een aparte klasse. Omdat het een directe overzetting was van de oude code, gebruikte het ook nog niet de animatiemogelijkheden en de physics van het special move systeem. Deze heb ik gaandeweg toegevoegd. Eerst ben ik alle animaties over gaan zetten om gebruik te maken van het blocking anim systeem. Omdat al deze functionaliteit eerst handmatig was geïmplementeerd in de ladder move, en al deze stukken vervangen werden door slechts enkele functiecalls naar StartBlockingAnim, werd de code een stuk compacter. Terwijl ik bezig was heb ik de code nog een stuk opgeschoond door stukjes code apart te zetten in eigen functies, of op andere manieren wat heen en weer te verplaatsen. Ook dit overzetten naar de animaties kostte veel werk. Er waren veel uitzonderingssituaties, zeker met betrekking tot het naar beneden glijden langs de ladder, hiervoor werden blokkerende animaties afgebro-
31
ken. Voor de communicatie met de physics moesten er ook nog veel variabelen in de Pawn blijven staan. Op dat moment werkte de ladder move goed, met één uitzondering: de physics stonden nog apart. Deze heb ik zonder problemen overgezet naar de move. Daarbij kon ik ook nog bijna alle overgebleven ladder variabelen verplaatsen naar de ladder move, zodat de eerste compleet geïmplementeerde move een feit was.
Leap of Faith De Leap of Faith is, zoals in het ‘Special moves’ hoofdstuk beschreven, gebaseerd op de gelijknamige move uit Assassin’s Creed waarbij de speler vanaf een grote hoogte naar beneden kan springen, en met perfecte precisie goed neerkomt. Hoe de move er ongeveer uit zou zien voor de leveldesigner stond eigenlijk al vanaf het begin vast: er zou een plek in de level komen waarvandaan er gesprongen zou kunnen worden, en een plek waar deze sprong dan zou kunnen eindigen. Er hoefde geen apart object te komen om de eindplek aan te geven, er zijn al een aantal andere objecten beschikbaar die puur dienen om een punt in de level te markeren. Voor het startpunt heb ik een nieuw object gemaakt. In de leveleditor wordt deze weergeven met een pijltje, dat symbool staat voor de sprong. Bij het object kan worden aangegeven wat het doelpuntobject is. Dit is alles wat de leveldesigner hoeft te doen.
32
De Leap of Faith in actie. (In beweging is het fantastisch mooi, echt )
Als extraatje heb ik ondersteuning voor meerdere startpunten rond dezelfde plek ingebouwd. Wanneer de speler in het bereik van meerdere startpunten staat, wordt het punt gekozen waar het spelkarakter het meeste naartoe gericht is (de kleinste hoek). Dit voegt wat extra realisme toe omdat de speler vanaf één punt meerdere kanten op kan springen. Voor de eigenlijke beweging heb ik verschillende ideeën overwogen. Ik was eerst van plan om de hele sprong handmatig te animeren/interpoleren met eigen physics code. Dit bleek echter nogal veel werk te zijn, vanwege bijvoorbeeld de collision detection. Om dit te voorkomen, heb ik ervoor gekozen om het spelkarakter een impuls te geven en hem vervolgens over te geven aan de zwaartekracht. Het grootste vraagstuk bij deze move was dus eigenlijk hoe de impuls het beste berekend kon worden. Er zijn bij het geven van de impuls twee variabelen: de horizontale en verticale impulssnelheid. Het is verreweg het meest eenvoudig om één daarvan constant te houden, zodat alleen de ander berekend hoeft te worden. In eerste instantie wilde ik de horizontale snelheid constant maken. Zo hoefde ik alleen de opwaartse snelheid te berekenen, wat tamelijk eenvoudig was omdat dit een lineaire vergelijking is. Het bleek echter dat voor mikpunten die iets verder lagen het spelkarakter onrealistisch hoog moest springen, wat stoorde bij het spelen van het spel. Een ander probleem was dat bij dichtbij gelegen punten, de sprong vaak niet hoog genoeg was om tot over het randje te komen.
Leap punten in de editor. (Vanaf deze plek kan er in drie verschillende richtingen gesprongen worden.)
Als alternatief moest ik uitgaan van een constante opwaartse snelheid, iets harder dan de normale sprong, zodat het er ook een beetje bijzon33
der uit zou zien. De vergelijking hier was een stuk lastiger, omdat er zaken als zwaartekracht bij de berekening kwamen. Met de bekende ABC formule bleek het gelukkig goed op te lossen. Voor de animatie heb ik 3 verschillende statussen gemaakt: voor het opspringen, het toppunt, en het vallen. Als test heb ik deze in de AnimTree aangesloten op respectievelijk springen, rollen, en vallen. De timing van de rol in het toppunt laat het lijken alsof het spelkarakter een salto maakt, een leuk effect in een prototype.
Wallhop De wallhop is de naam die ik heb gegeven aan de move waarbij de speler over een muurtje kan springen/klimmen, in één snelle vloeiende animatie. Deze move zou het voorbeeld moeten worden van een move die snel te implementeren is met het special move systeem. De move werkt ongeveer zo: de speler loopt naar een muurtje toe dat er op een bepaalde manier uitziet alsof er snel overheen te springen of klimmen is. Dit is iets dat de leveldesigner bedenkt. Wanneer het spelkarakter ervoor staat, drukt de speler op de spring knop om snel over het muurtje te springen of klimmen. Hier gebruik ik springen en klimmen door elkaar, omdat dat alleen een kwestie van animatie is. De move zelf is namelijk niet veel meer dan één blokkerende animatie. Technisch werkt het zo: er is een object beschikbaar in de leveleditor, een HWallHopPoint. De leveldesigner plaatst deze bij een muurtje, en zorgt dat hij precies goed uitgelijnd staat met het muurtje. Het object tekent een aantal hulplijnen om dit te vereenvoudigen.Wanneer de speEen wallhop in de editor. (Daarachter zichtbaar zijn twee leap punten op een lift.)
34
ler in het spel op springen drukt vanuit de ‘loop’ move, wordt er gekeken of het spelkarakter zich op een HWallHopPoint bevindt, en of het spelkarakter in de goede richting staat. Als dat zo is, wordt de wall hop gestart. Omdat er nog geen animatie beschikbaar was voor deze move, is het in het prototype geen visueel spektakel. Om het ontbreken van een animatie met verplaatsing af te vangen, heeft StartBlockingAnim() een handige mogelijkheid die hier gebruikt wordt: het opgeven van een time-out en handmatige verplaatsing. Als de time-out verstrijkt en er is geen animatie gestart in de AnimTree, wordt de verplaatsing handmatig toegepast. Dit noemde we ‘ploppen’. Met de wallhop heb ik laten zien dat het met special move in ieder geval tot op zekere hoogte mogelijk is om snel nieuwe moves te maken. In totaal heeft de wallhop move me ongeveer twee dagen gekost, vanaf overleg met mijn bedrijfsbegeleider tot een werkende versie.
35
6–Afsluiting Het hele project is voor mij een leerzame ervaring geweest. Ik heb met succes een special move systeem ontworpen en geïmplementeerd. Ik heb met de animator overlegd hoe hij het liefst het special move systeem zou zien, en een systeem gemaakt waar ook hij zich goed in kan vinden. Behalve het special move project heb ik ook in de vorm van bugfixes en een aantal features bijgedragen aan het spel. Als laatste onderdeel in dit document kijk ik nog terug naar de stage, en vooruit, naar de toekomst en potentie van het special move systeem, en zelfs special move systemen in het algemeen.
Wat er minder ging Over het ontwerp van het systeem ben ik in het algemeen tevreden. Het doet wat het ongeveer moet doen. Wat jammer is, is dat ik het nog niet heb kunnen inzetten in wat meer realistische scenario’s: de implementaties van de special moves zoals in het prototype gaat uit van de best case. Er is met veel dingen nog geen rekening gehouden, zoals hoe er gereageerd wordt op onverwachte situaties. Dit zou bijvoorbeeld kunnen zijn wanneer er op een spelkarakter geschoten wordt terwijl deze de wallhop aan het doen is. De implementatie is ook nog niet helemaal compleet. Er is een ontwerp voor overgangen tussen moves—zoals in dit verslag gedocumenteerd, maar voor de implementatie hiervan ontbrak het mij aan tijd. 36
Verder is er wel een mogelijkheid voor integratie voor de AI, maar in de praktijk is dit nog in geen van de moves geïmplementeerd. Bij de originele moves was de code van de move sterk verwikkeld met de PlayerController. Hierdoor stond veel code in TickPlayerController(), en weinig in TickPawn(). Als ik dit nog beter had gescheiden, was de interface met een AI mogelijk geweest. Ook hier was tijd de beperkende factor. Persoonlijk viel mijn eigen productiviteit me tegen. Dit komt voornamelijk komt door de lange compilatietijden. Elke keer als er iets in de declaratie een ‘native’ object verandert wordt moet de code bijna helemaal opnieuw gecompileerd worden. Dit kost heel veel tijd en haalde me telkens uit mijn werkritme. Als alternatief probeerde ik zo min mogelijk te compileren, maar dit kon niet altijd.
Wat er goed ging Bovenal ben ik tevreden met wat ik heb opgeleverd. Voordat ik begon aan dit project, waren juist dit soort dingen als special moves magie voor me, een ‘black box’. In de loop van de stage ben ik steeds meer gaan leren over de engine en de (on)mogelijkheden daarvan. Ondanks dat het ontwerp nog niet allesomvattend is en nog niet alles is geïmplementeerd, ben ik tevreden met dat wat ik heb opgeleverd in deze maanden. De opgedane kennis is duidelijk van waarde voor het bedrijf, dat nu een duidelijk beeld heeft van wat er wel en niet mogelijk is, en waar de complicaties liggen. Blokkerende animaties met de StartBlockingAnim() functies zijn uiterst nuttig.
37
Het animatiesysteem is goed uitgewerkt, past bij de werkwijze van de animator, en werkt goed. Met dit animatiesysteem is het eenvoudig om de special moves te integreren in de animatieboom.
Conclusie Mijn uiteindelijke conclusie is dat mijn special move systeem nog niet genoeg uitontwikkeld is om gebruikt te worden in een productieomgeving. De potentie is er zeker—met wat verdere ontwikkeling zou een systeem als deze goed bruikbaar zijn. Een aantal onderdelen uit het special move systeem kunnen direct gebruikt worden, in het bijzonder de aansturing van blokkerende animaties en de AnimNodes. Hoewel Coded Illusions uiteindelijk heeft besloten om het special move systeem niet te gaan, is het voor het bedrijf en mij een bijzonder leerzame ervaring geweest.
–Sijmen Mulder
38
Summary in English Assignment: “Design and build a system which handles non-standard moves. Implement two existing, and two new moves. Optionally, design a way for the AI to use the system” The internship described in this document was at a game development company called Coded Illusions, which is currently producing its first title, the action-adventure Haven. To be able to more easily create new ‘moves’, a system for special move is required.
be (re)written in a more generic way. Because all code related to moves is now gathered at one place, debugging and maintenance is easier. This new special move system also supplies support for automatic move selection, animation, and even custom physics. What went wrong: •
Unexpected events during moves are not handled in a standardized way. Custom code is required;
Examples of special moves include climbing ladders and rolling. During the course of the development, the scope widened to ‘pretty much anything that temporarily affects a game character’s state’, so even ‘walking’ was considered a move.
•
Not everything is fully implemented due to time constraints;
•
AI support marginal, not yet implemented. Again, time constraints to blame;
The development platform is Unreal Engine, a well-known commercial game engine. Most code is written in UnrealScript or C++.
•
Working with such a large code base slows down development severely, especially to someone new to it.
Before the special move system, moves were scattered throughout the source code. A move did not consist of a concise set of classes to define and implement it. To solve this, I have created an HMove base class, of which all special moves now derive. The idea of the HMove class is that every move has similarities. By implementing the common denominators, the moves could
What went right: •
Three moves successfully implemented. One was skipped; this was the second re-implementation for an existing move. This was deemed unnecessary as the other re-implementation, the ladder move, had already proven it possible;
39
•
Valuable knowledge was acquired;
•
Blocking animations with displacement (‘root motion’) proved very usable throughout multiple moves;
•
The animation support was great, and was well received by the animator.
My final conclusion is that due to the relatively short timeframe, the current design is not mature enough to be considered useful in a production environment. While it would certainly improve readability and scalability of the move code, it does not outweigh the time and work needed to implement it throughout the game. Certain features however, such as the blocking animation, are very usable and could be ported over with relative ease. Coded Illusions thus decided not to use the system, but for both the company and for me, it has proven a valuable learning experience.
Sijmen Mulder.
40