Inspirasjon:
SwingingWarrior

Øyvind Ahlstrøm
HAHHalvor Ahlstrøm

18.02.24

Swinging Warrior

Programmering på fritiden

Jeg la merke til at etter noen få år som betalt utvikler så mistet jeg gløden og motivasjonen til å programmere på fritiden. Forklaringen er enkel, jeg ikke har overskudd til å gjøre det samme etter arbeidstid som jeg har brukt hele arbeidsdagen på. Jeg er komfortabel med denne situasjonen da det tillater meg å ha flere aspekter i livet enn programmering, men det er samtidig en intern konflikt som brygger. Det er en del av meg som har behov for å skape noe med den kompetansen jeg har. Skape noe fra min egen fantasi og kunne uttrykke meg selv via mitt eget håndverk.

Fra tid til annen så opplever jeg en eksplosjon av inspirasjon. Jeg kan ikke forklare hvorfor eller hvordan, men det er veldig spennende. Det å skape noe (trenger ikke være programmering) vekker en følelse av nysgjerrighet og en tilfredstillelse som jeg minnes å ha mye som barn. En form for iboende modus i meg som jeg skulle ønske jeg kunne tappe inn i oftere. I juleferien opplevde jeg nettopp dette etter en samtale med min bror om World of Warcraft.

20 år med World of Warcraft

For snart 20 år siden så kjøpte jeg og min bror spillet World of Warcraft på GameStop på Vinterbro. På den tiden ble spillene solgt i større forpakninger som skapte enorm forventning. Esken var designet slik at det var mulig å åpne en flapp med bilder fra spillet som vi beundret i baksetet på bilen hele veien hjem. På et tidspunkt måtte vi legge det fra oss for å unngå å bli bilsyke. Dette visste vi fra erfaring. Esken inneholdt også en manual som vi fikk god tid til å lese ettersom spillet ble installert via fire CDer som på den tiden tok flere timer.

I noen år levde både jeg og broren min oss helt inn i spillets verden i likhet med millioner av andre mennesker. Vi byttet på å spille på den eneste datamaskinen vi hadde i huset. Etterhvert fikk vi andre interesser, men vi har nok begge to hatt et sporadisk forhold til World of Warcraft siden den gang. I jula fortalte min bror om sin prestasjon å oppnå høyeste nivå med en "warrior" uten å dø en eneste gang. Han kunne videre fortelle at det viktigste verktøyet han brukte var en timer som kunne visuelt vise deg neste gang karakteren din kunne svinge våpenet i et angrep. En såkalt 'SwingTimer'. Verktøyet bidro til at han kunne løpe bort fra fiender i tiden mellom angrepene og bytte slag med fienden så effektivt som mulig. Denne teknikken skalerer i effektivitet basert på hvor raskt fienden angriper. Det bør man kunne klare å lage selv.

Verken jeg eller broren min hadde erfaring med hvordan man programmerte en addon til World of Warcraft. Vi kranglet oss igjennom et ekstremt fragmentert informasjonsgrunnlag og endte til slutt opp med en fungerende utgave. Denne artikkelen vil gi en kort introduksjon til hvordan man kommer i gang for å bygge en addon til World of Warcraft, basert på vårt arbeid med addonen vi døpte SwingingWarrior.

Hvordan henger det sammen?

World of Warcraft sin motor er designet slik at det tillater å kjøre kode på klienten. Selskapet selv bruker denne metoden for sitt eget brukergrensesnitt. Spillets motor fyrer av hendelser (events) for alt som foregår i spillet. Når en hendelse oppstår så vil kode knyttet til hendelsen også kjøres. For å tilby sin egen kode så plasserer man dette i mappen Interface/AddOns. Ved oppstart vil motoren scanne denne mappen og lete etter alle filer som ender med .toc (table of contents). Denne filen inneholder metadata og informasjon om hvilke andre filer motoren skal laste inn. Dette kan bestå av for eksempel XML-definisjoner, LUA-kode, bilder og lydfiler. LUA-koden trenger ikke være kompilert på forhånd da motoren selv vil kompilere disse ved innlasting. Ved kjøring vil LUA-koden ha tilgang på det samme APIet som spillet selv kjører på. APIet gir blant annet tilgang til å hente informasjon fra spillet og lage nye eller utvide eksisterende grensesnitt.

Utviklingsmiljø

De to største utfordringene man møter når man skal skrive en addon til World of Warcraft er fragmentert informasjon og tilgang til å teste koden sin. Det er dessverre ikke utgitt et utviklingsverktøy for å skrive addons mot deres API. Derfor ble vi avhengige av en kombinasjon av reverse engineering av andre addons samt hente dokumentasjon på WoWwiki for å forstå hva som er tilgjengelig av funksjoner. Sistnevnte er en nettside som er ekstremt nyttig og kanskje eneste ordentlige kilden til APIet. Dessverre er denne nettsiden proppfull av reklame og pop-up bokser. En person med brukernavnet Ketho har laget en Visual Studio Code plugin med navn WoW API som henter informasjon fra denne nettsiden og tilbyr IntelliSense ved utvikling.

Som en konsekvens av at man ikke har tilgang til et utviklingsmiljø så har man heller ikke tilgang til å teste koden sin uten å gå via spillet. Det betyr at man må starte spillet og sørge for at gitte hendelser forekommer for å verifisere at det fungerer. Både jeg og broren min opplevde store utfordringer, men det føltes desto bedre når vi løste dem.

Hvordan skrive en addon?

Det enkleste du kan lage er følgende:

local frame = CreateFrame("Frame");
frame:RegisterEvent("PLAYER_LOGIN");

Koden over vil opprette en frame som er et sentralt objekt i denne sammenhengen. En frame er et objekt som tillater å lytte til hendelser og kjøre kode dersom en hendelse oppstår. På neste linje forteller vi motoren at denne framen skal lytte på hendelsen "PLAYER_LOGIN". Dette betyr at hver gang spillet fyrer av hendelsen "PLAYER_LOGIN" så vil frame sin kode bli kjørt. En frame kan lytte på så mange hendelser den vil og det finnes i skrivende stund hundrevis av hendelser å lytte til.

En frame er også objektet som holder på informasjon om hvilken kode som skal kjøres. For å legge til kode brukes metoden frame:SetScript("handler", func). Førte argument representer en string som forteller motoren når koden skal kjøres. Dette kan eksempelvis være "OnEvent" når hendelsen oppstår eller "OnUpdate" hver gang brukergrensesnittet rendres. Det kan også være interaktive hendelser som "OnDragStart" og "OnDragStop" dersom man har åpnet for at framen kan flyttes.

frame:SetScript("OnEvent", function(self, event, arg1, ...)
    if event == "PLAYER_LOGIN" then
        print("DinAddon er lastet inn!")
    end
end)

"OnUpdate" vil kjøres omtrent like ofte som bildefrekvensen til spillet og brukes til animasjoner og lignende.

local function OnUpdateScript(self, elapsed)
    <din kode her>
end

mainhandFrame:SetScript("OnUpdate", OnUpdateScript);

Funksjonen du tilbyr "OnUpdate" vil få injisert flere argumenter som eksempelvis elapsed som gir deg informasjon om hvor lang tid det har gått siden siste gang "OnUpdate" metoden ble kjørt. I vår addon ble dette en kjernefunksjonalitet. Vi brukte argumentet til å kalkulere hvor lang tid som har gått siden sist slag og samtidig kalkulere og tegne en visuell indikator.

Noen eksempler fra SwingingWarrior

Vi ønsket at addonen skulle vises kun når karakteren befant seg i kamp og ellers holdes skjult. Samtidig holder vi styr på om karakteren har en eller to våpen og om disse blir byttet ut midt i en kamp. Dersom det skulle oppstå noe midt i en kamp som øker eller reduserer slagfrekvensen så vil dette også bli registrert. Her er et kort utdrag av noe av koden i SwingingWarrior som viser hvordan koden kan struktureres.

if event == "ADDON_LOADED" then
    if args[1] == "SwingingWarrior" then
        mainhandFrame:SetScript("OnUpdate", OnUpdateScript);
        mainhandFrame:RegisterEvent("PLAYER_REGEN_DISABLED");
        mainhandFrame:RegisterEvent("PLAYER_REGEN_ENABLED");
        mainhandFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        mainhandFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
    end
end
    if event == "PLAYER_REGEN_DISABLED" then
    mainhandFrame:Show();
end
if event == "PLAYER_REGEN_ENABLED" then
    mainhandFrame:Hide();
    offhandFrame:Hide();
end
local function OnUpdateScript(self, elapsed)
    mainhandSpeed, offhandSpeed = UnitAttackSpeed("player");
    if offhandSpeed then
        if not offhandFrame:IsShown() then
            offhandFrame:Show();
            offhandSwingTimer = offhandSpeed;
        end
        if offhandSwingTimer == nil then
            offhandSwingTimer = offhandSpeed;
        end
        offhandSwingTimer = updateSwingTimer(offhandSwingTimer, elapsed, offhandSpeed, offhandFrameWidth, offhandFrame);
    else
        if offhandFrame:IsShown() then
            offhandFrame:Hide();
        end
    end
    mainhandSwingTimer = updateSwingTimer(mainhandSwingTimer, elapsed, mainhandSpeed, mainhandFrameWidth, mainhandFrame);
end

Videre arbeid

Med kjernefunksjonaliteten på plass så har vi begynt å bruke addonen selv. Dette gir anledning til å teste koden i mange forskjellige situasjoner og identifisere eventuelle mangler. Det er fremdeles en en del som gjenstår i form av det estetiske som står på agendaen. Vi ønsker gjerne at dette er på plass før vi tilbyr koden vår til offentligheten. Vi sitter begge igjen med en god erfaring av å ha gjennomført et prosjekt som er annerledes enn det man møter i en arbeidshverdag. Følelsen av å uttrykke seg selv i form av eget håndverk er givende og fungerer som et drivstoff for videre arbeid.

Takk for at du leste hele artikkelen. Dersom du har noen innspill, spørsmål eller kritikk så kan du kontakte meg på oya@progit.no.