Spillet kan spilles her.
Denne bloggposten er for deg som har jobbet med Unity en stund og ser etter litt teknisk påfyll, eller for deg som bare synes spillutvikling er interessant. Om ikke annet Jeg kunne gjerne tenkt meg å lese lignende bloggposter av andre norske utviklere.
Begynnelsen
Dette prosjektet begynte da vi tok kontakt med Adresseavisen med en idé om å utvikle et spill som kunne skape engasjement hos leserne deres. Tanken var å lage et lite, vanedannende spill der man bare "må ta én runde til". Etter litt brainstorming ble vi alle enige om at et straffespark-spill kunne passe godt for trønderske lesere.
Teamet vårt besto av to personer
- David Skaufjord (Hållingwood AS) som prosjektleder, grafiker og stemmeskuespiller
- Martin Lande (Lande Audio) som teknisk ansvarlig, lyddesigner og komponist
Begge fungerte som spilldesignere.
Konsept
Dette var målene våre
- Spillet skal kjøre i nettleser, både på mobil og desktop
- Gameplay som er lett å lære, men vanskelig å mestre
- Nettbasert highscoreliste
- Replay-system
- Keeper med litt personlighet, og mulighet til å endre utseende
Gjennomføring
Vi brukte Unity, FMOD og git. Ellers besto verktøyene av Blender, Photoshop og Affinity Designer. Musikk og lyddesign i Reaper. Samarbeidet mellom oss foregikk via Discord, da vi begge jobber hjemmefra.
Vi bestemte oss tidlig for å bruke et 3D-miljø med en flat 2D-keeper i mål, og å benytte oss av Unitys innebygde fysikksystem. For at dimensjonene skulle føles nogenlunde autentisk ut, tok vi utgangspunkt i FIFA sine offisielle retningslinjer da vi modellerte banen og målet. Unntaket her er tykkelsen på stengene, som ble økt for å gjøre det lettere å se hva som foregår, og lettere å score stang-inn.
Grunnleggende systemer
Kodemessig bruker vi en state machine for å holde styr på flyten i spillet. Denne ligger i et objekt kalt GameplayHandler, som fungerer som bindeledd mellom de fleste andre systemene i spillet. Dette er på sett og vis kjernen i koden, og brukes blant annet av UI-systemet for overganger mellom menyer.
For å kontrollere kameraet bruker vi Cinemachine, som er en av Unitys egne pakker tilgjengelig i Package Manager. Anbefales å sjekke ut for de som ikke har testen den.
Ballen
Ballen er fysikkbasert, så alt vi trenger å gjøre for å simulere et skudd er å påføre en gitt kraft. Vi legger til rotasjon (torque) basert på hvor mye spilleren skrur. Ballen beveger seg kontinuerlig i retningen den roterer, basert på hvilken retning den flyr. Så snart den spretter på noe opphører skru-effekten.
Vi hadde et problem i starten der ballen ikke ville rotere så fort som vi ville. Dette viste seg å være en innstilling i Project Settings > Physics. Default Max Angular Speed måtte settes opp til 100 i vårt tilfelle.
Keeperen
Keeperen består av 2D sprites, som vi animerer ved bruk av Unitys egen skjelettanimasjonspakke. Her bruker vi også PSD-importeringspakken, slik at vi kan eksportere alle kroppsdelene i én .psd-fil. Animasjonssystemet har verktøy som lar oss definere et skjelett og skape en mesh som gjør at kroppsdeler kan bøyes og vrenges.
Når skuddet går blir han helt slapp, og slenger seg omtrent mot der ballen ender opp. Ragdoll-fysikk er jo alltids gøy, og siden både ballen og keeper er fysikkbasert ender vi opp med litt dynamikk i gameplayet. Hendene blir også kontinuerlig dratt mot ballen en liten stund etter at han hopper.
Ett av målene våre var å gi keeperen personlighet. Før hver runde kan spilleren tilfeldiggjøre utseendet med forskjellige neser, øyne, drakt, osv. Teknisk sett har keeperen også stemmeskuespill med to stemmer; mann og kvinne. Begge spilt av Skaufjord.
Keeperen må vite omtrent hvor ballen havner med det samme skuddet tas. Dermed måtte vi skrive et lite fysikksystem som kan simulere banen til ballen momentant. Fordelen her er at Keeperen ikke trenger eksakt posisjon, og heldigvis har Unity en del snedige funksjoner for å teste kollisjon med en gitt form på et gitt punkt.
UI
Hver meny er sitt eget objekt, som styres av en MenuController. Denne animerer overgangene mellom dem direkte i koden, som gjør det veldig enkelt og kjapt å legge til nye menyer etter hvert som prosjektet vokser. Overgangsanimasjonene benytter seg av Robert Penner sine easing-funksjoner.
Spilleren får med en gang feedback på hvordan skuddet gikk. Deretter kommer en "breakdown" av poengene som ble gitt til skuddet. Vi forsøkte å gjøre opplevelsen så "juicy" som mulig, med forseggjort animasjon og lyddesign. Samtidig må dette foregå kjapt, slik at man ikke går lei etter hundre skudd. Både lyd og visuell stil er blant annet inspirert av Nintendo sin typiske UI-stil.
https://www.youtube.com/watch?v=YsUl_fpuGKw
Replay-system
Spilleren får se skuddet sitt på nytt rett etter at resultatet av skuddet er kjent. Dette speiler virkelig fotball, der man gjerne får se skudd i sakte film fra spenstige kameravinkler. Siden Unity sitt fysikksystem ikke er deterministisk, så måtte vi enten skrive vårt eget, eller bruke en metode der vi lagrer posisjoner og rotasjoner. Vi gikk for det siste, som beskrives som en state-basert metode.
Systemet vi endte opp med har en liste med transforms; ballen og alle kroppsdelene til keeperen. Systemet noterer ned posisjonene og rotasjonene til alle kontinuerlig, hver fjerde fixed frame. Vi begynner å lagre disse så fort spilleren kan sikte ballen, og slutter en liten stund etter at resultatet er kjent. Deretter kutter vi lengden ned til rett før skuddet ble gjort. Vi setter et maks antall frames for å hindre at man ender opp med veldig lange replays hvis noe uforutsett skulle skje.
Når replayen spilles av interpoleres posisjonene og rotasjonene mellom hver frame, slik at resultatet ser naturlig ut, og så bruker vi Time.timeScale for å få sakte film.
Highscore-system
Etter å spilt ferdig en hel runde får man muligheten til å skrive inn et navn og sende poengene sine på den offentlige ledertavla. En interessant utfordring her er at spillere kan skrive inn hva som helst, inkludert banneord. Navnet sjekkes opp mot en tekstfil som inneholder alle mulige tenkelige og utenkelige obskøniteter. Hvis et stygt ord blir oppdaget får man ikke til å sende inn.
En SQL-database brukes for å holde styr på scorene. Spillet kommuniserer med et par PHP-skript som i sin tur snakker med databasen og sender info tilbake til spillet.
Vi benytter oss av UnityWebRequest for å kommunisere med serveren. Under testing oppdaget vi at spesielle hensyn må tas på WebGL-plattformen. Ettersom spillet kjører i nettleser, må responsen fra serveren inneholde CORS-headere. Testserveren vår hadde ikke dette konfigurert, og vi hadde heller ikke tilgang til å konfigurere den. Heldigvis er det mulig å hardkode disse inn i responsene, og dette ble løsningen for oss under testing.
Implementering i nettleser
Addressa må ha muligheten til å endre på visse ting i spillet. For eksempel har spillet reklameplakater bak mål. Når man trykker på "del på facebook" så kan det hende at de vil endre URL. Disse må kunne endres på etter at vi har laget en build.
For å få til dette bruker vi StreamingAssets-mappen, som lar oss endre innhold etter at man har laget en build. Disse lastes inn i runtime. I en WebGL-build ligger denne mappen altså på serveren, og dermed må man faktisk laste innholdet inn via et WebRequest.
Oppsummering
Den tekniske delen av prosjektet tok totalt litt over 100 timer å ferdigstille, fordelt på omtrent to måneder. Dette gikk relativt greit, mye takket være et bevisst forhold til scope. Etter implementering på Adressa sine nettsider oppdaget vi noen bugs, og de ønsket et par endringer. Dette hadde vi forutsett, og var greit å svare ut.
Dette var de største tekniske utfordringene vi støtte på:
- Vi brukte Unity 2022.3, som ikke offisielt støtter WebGL på mobil. Dette førte til en del knotete trøbbel, spesielt med lyden. For eksempel så kutter lyden ut hvis man går til en annen tab på mobil, og vi har ingen pålitelig måte å sjekke om brukeren bytter tilbake. Unity 6 har offisiell støtte, som antakelig ville gjort dette enklere.
- Vi kom over en bug i 2D Animation-pakken der visse SpriteSkins ble sittende fast. For eksempel at hodet til keeperen sto helt i ro, mens resten av kroppen beveget på seg. Heldigvis kunne vi modifisere disse scriptene for å løse problemet.
- Spillet støtter både portrettmodus og landskapsmodus, som gjør at man må tenke over layout på UI, osv. For eksempel; hvis du har tre knapper som dekker bredden på skjermen så ser dette veldig greit ut i landskapsmodus, men det kan bli veldig trangt i portrettmodus. Hvis man setter Canvas-komponenten til å skalere UI like mye basert på bredde vs høyde så har man et godt utgangspunkt. I sjeldne tilfeller brukte vi et skript som sjekker hvilken modus vi er i som lar oss vise/gjemme elementer.
Det gjenstår å se om spillet blir en hit blant leserne, men vi tror og håper at spillere kan more seg med spillet en god stund!