Spillinngang Forenklet

Tenk deg at et spill karakter som heter "Bob the Butcher" står alene på et mørkt rom mens horder av mutant pølse zombier begynner å strømme inn gjennom dører og knuste vinduer. På dette tidspunktet vil det være en god idé for Bob å begynne å sprenge pølszombiene i små kjøttstykker, men hvordan vil Bob gjøre det i et kryssplattformsspill? Vil spilleren ha å trykke en eller flere taster på et tastatur, klikk musen, trykk på skjermen eller trykk på en knapp på en gamepad?

Når du programmerer et tversplattformspill, er dette den typen ting du sannsynligvis vil bruke mye tid på, hvis du er uforberedt på det. Hvis du ikke er forsiktig, kan du ende opp med massiv, spaghetti-lignende hvis uttalelser eller bytte om uttalelser for å håndtere alle de forskjellige inngangsenhetene.

I denne opplæringen skal vi gjøre ting mye enklere ved å lage en enkelt klasse som vil forene flere inngangsenheter. Hver forekomst av klassen vil representere en bestemt spillhandling eller -adferd (som "skyte", "løp" eller "hoppe") og kan bli fortalt å lytte til forskjellige taster, knapper og pekere på flere inngangsenheter.

Merk: Programmeringsspråket som brukes i denne opplæringen, er JavaScript, men teknikken som brukes til å forene flere inngangsenheter, kan enkelt overføres til et hvilket som helst annet programmeringssprog som gir APIer for inngangsenheter.

Skyt pølsene

Før vi begynner å skrive koden for klassen som vi skal skape i denne opplæringen, la oss se på hvordan klassen egentlig kunne brukes.

// Opprett et innspill for "skyt" -aksjonen. skyte = nytt GameInput (); // Fortell innspillet hva du skal reagere på. shoot.add (GameInput.KEYBOARD_SPACE); shoot.add (GameInput.GAMEPAD_RT); // Under hver spilloppdatering, kontroller du innspillingen. funksjon oppdatering () if (shoot.value> 0) // Fortell Bob å skyte mutant pølse zombier!  annet // Fortell Bob å slutte å skyte. 

GameInput er den klassen vi skal skape, og du kan se hvor mye enklere det vil gjøre ting. De shoot.value Eiendommen er et tall og vil være en positiv verdi hvis mellomromstasten På et tastatur trykkes eller høyre utløser På en gamepad trykkes. Hvis ikke mellomromstasten eller høyre utløseren trykkes, vil verdien være null.

Starter

Det første vi må gjøre er å opprette en funksjonslukning for GameInput klasse. Mesteparten av koden vi skal skrive, er egentlig ikke en del av klassen, men den må være tilgjengelig fra klassen, mens den fortsatt er skjult fra alt annet. En funksjonslukning gjør det mulig for oss å gjøre det i JavaScript.

(I et programmeringsspråk som ActionScript eller C #, kan du bare bruke private klassemedlemmer, men det er ikke en luksus vi har i JavaScript, dessverre.)

(funksjon () // kode går her) ();

Resten av koden i denne opplæringen erstatter "koden går her" kommentaren.

Variablene

Koden trenger bare en håndfull variabler som skal defineres utenfor funksjoner, og disse variablene er som følger.

var KEYBOARD = 1; var POINTER = 2; var GAMEPAD = 3; var DEVICE = 16; var CODE = 8; var __pointer = currentX: 0, currentY: 0, previousX: 0, previousY: 0, distanceX: 0, distanceY: 0, identifiserer: 0, flyttet: falskt, trykket: false; var __keyboard = ; var __inputs = []; var __channels = []; var __mouseDetected = false; var __touchDetected = false;

Den konstant-liknende TASTATUR, POINTER, GAMEPAD, ENHET og KODE verdier brukes til å definere Inndata Enhetskanaler, som for eksempel GameInput.KEYBOARD_SPACE, og deres bruk vil bli klar senere i opplæringen.

De __pointer Objektet inneholder egenskaper som er relevante for mus- og berøringsskjerminngangsenheter, og __tastatur Objekt brukes til å holde styr på tastaturnøkkelstatus. De __inputs og __channels arrays brukes til å lagre GameInput forekomster og eventuelle innmatede enhetskanaler lagt til disse tilfellene. Endelig, den __mouseDetected og __touchDetected Angi om en mus eller berøringsskjerm har blitt oppdaget.

Merk: Variablene trenger ikke å være prefiks med to understreker; det er ganske enkelt den kodende konvensjonen jeg har valgt å bruke for koden i denne opplæringen. Det hjelper å skille dem fra variabler definert i funksjoner.

Funksjonene

Her kommer hoveddelen av koden, slik at du kanskje vil ta en kaffe eller noe før du begynner å lese denne delen!

Disse funksjonene er definert etter variablene i forrige del av denne opplæringen, og de er definert i rekkefølge av utseende.

// Initialiserer inngangssystemet. funksjon main () // Expose GameInput konstruktøren. window.GameInput = GameInput; // Legg til hendelseslytterne. addMouseListeners (); addTouchListeners (); addKeyboardListeners (); // Noen brukergrensesnitt vi bør forhindre i et spill. window.addEventListener ("contextmenu", killEvent, true); window.addEventListener ("selectstart", killEvent, true); // Start oppdateringssløyfen. window.requestAnimationFrame (update); 

De hoved() funksjon kalles på slutten av koden-det vil si på slutten av funksjonslukning vi opprettet tidligere. Det gjør hva det står på tinnet og får alt til å løpe så GameInput klassen kan brukes.

En ting jeg bør bringe til din oppmerksomhet er bruken av requestAnimationFrame () funksjon, som er en del av W3C Animation Timing-spesifikasjonen. Moderne spill og programmer bruker denne funksjonen til å kjøre oppdateringen eller rendering av sløyfer fordi den har blitt svært optimalisert for det formålet i de fleste nettlesere.

// Oppdaterer inngangssystemet. funksjon oppdatering () window.requestAnimationFrame (update); // Oppdater pekerverdiene først. updatePointer (); var i = __inputs.length; var input = null; var kanaler = null; mens (i -> 0) input = __inputs [i]; kanaler = __kanaler [i]; hvis (input.enabled === true) updateInput (input, channels);  ellers input.value = 0; 

De Oppdater() funksjonsruller gjennom listen over aktive GameInput forekommer og oppdaterer de som er aktivert. Følgende updateInput () funksjonen er ganske lang, så jeg vil ikke legge til koden her; Du kan se koden i sin helhet ved å laste ned kildefilene.

// Oppdaterer et GameInput-eksempel. funksjonoppdateringInput (inngang, kanaler) // note: se kildefilene

De updateInput () funksjonen ser på inngangsenhetskanaler som er lagt til i en GameInput eksempel og utarbeider hva verdi eiendom av GameInput forekomst bør settes til. Som sett i Skyt pølsene Eksempelkode, verdi Egenskapen indikerer om en inngangsenhetskanal utløses, og det tillater et spill å reagere tilsvarende, kanskje ved å fortelle Bob å skyte mutantpølsezombiene.

// Oppdaterer verdien av en GameInput-forekomst. funksjon updateValue (input, value, threshold) hvis (terskel! == undefined) hvis (verdi < threshold )  value = 0;   // The highest value has priority. if( input.value < value )  input.value = value;  

De updateValue () funksjonen bestemmer om verdi eiendom av en GameInput forekomsten bør oppdateres. De terskel brukes primært til å forhindre analoge inngangskanaler, for eksempel gamepadknapper og pinner, som ikke tilbakestiller seg riktig fra å utløse en GameInput forekomst. Dette skjer ganske ofte med feil eller skitten gamepads.

Som updateInput () funksjon, følgende updatePointer () funksjonen er ganske lang, så jeg vil ikke legge til koden her. Du kan se koden i sin helhet ved å laste ned kildefilene.

// Oppdaterer pekerverdiene. funksjon updatePointer () // note: se kildefilene

De updatePointer () funksjon oppdaterer egenskapene i __pointer gjenstand. I et nøtteskall klemmer funksjonen pekeren sin posisjon for å sikre at den ikke forlater nettleserens vinduevisningsport, og det beregner avstanden pekeren har flyttet siden den siste oppdateringen.

// Kalt når en musinnmatingsenhet er oppdaget. funksjon mouseDetected () if (__mouseDetected === false) __mouseDetected = true; // Ignorer berøringshendelser hvis en mus blir brukt. removeTouchListeners ();  // Kalt når en inngangsenhet med berøringsskjerm er oppdaget. funksjon touchDetected () if (__touchDetected === false) __touchDetected = true; // Ignorer mushendelser hvis en berøringsskjerm brukes. removeMouseListeners (); 

De mouseDetected () og touchDetected () funksjoner forteller koden for å ignorere en inngangsenhet eller den andre. Hvis en mus oppdages før en berøringsskjerm, blir berøringsskjermen ignorert. Hvis en berøringsskjerm oppdages før en mus, ignoreres musen.

// Kalt når en peker-lignende inngangsenhet er trykket. funksjon pointerPressed (x, y, identifiserer) __pointer.identifier = identifier; __pointer.pressed = true; pointerMoved (x, y);  // Kalt når en peker-lignende inngangsenhet er utgitt. funksjon pointerReleased () __pointer.identifier = 0; __pointer.pressed = false;  // Kalt når en peker-lignende inngangsenhet er flyttet. funksjon pointerMoved (x, y) __pointer.currentX = x >>> 0; __pointer.currentY = y >>> 0; hvis (__pointer.moved === false) __pointer.moved = true; __pointer.previousX = __pointer.currentX; __pointer.previousY = __pointer.currentY; 

De pointerPressed (), pointerReleased () og pointerMoved () Funksjoner håndterer inngang fra en mus eller en berøringsskjerm. Alle tre funksjonene oppdaterer bare egenskaper i __pointer gjenstand.

Etter disse tre funksjonene har vi en håndfull standard JavaScript hendelseshåndteringsfunksjoner. Funksjonene er selvforklarende, så jeg vil ikke legge til koden her; Du kan se koden i sin helhet ved å laste ned kildefilene.

// Legger til en inngangsenhetskanal til en GameInput-forekomst. funksjon inngangAdd (inngang, kanal) var i = __inputs.indexOf (input); hvis (i === -1) __inputs.push (input); __channels.push ([kanal]); komme tilbake;  var ca = __kanaler [i]; var ci = ca.indexOf (kanal); hvis (ci === -1) ca.push (kanal);  // Fjerner en inngangsenhetskanal til en GameInput-forekomst. funksjonen inputRemove (input, channel) var i = __inputs.indexOf (input); hvis (i === -1) return;  var ca = __kanaler [i]; var ci = ca.indexOf (kanal); hvis (ci = = -1) ca.splice (ci, 1); hvis (ca.length === 0) __inputs.splice (jeg, 1); __channels.splice (jeg, 1);  // Tilbakestiller en GameInput-forekomst. funksjon inputReset (input) var i = __inputs.indexOf (input); hvis (i! == -1) __inputs.splice (jeg, 1); __channels.splice (jeg, 1);  input.value = 0; input.enabled = true; 

De inputAdd (), inputRemove () og inputReset () funksjoner kalles fra a GameInput eksempel (se nedenfor). Funksjonene endrer __inputs og __channels arrays avhengig av hva som må gjøres.

EN GameInput forekomsten anses som aktiv, og legges til __inputs array, når en inngangsenhetskanal er lagt til i GameInput forekomst. Hvis en aktiv GameInput forekomsten har alle sine inngangsenhetskanaler fjernet, den GameInput forekomsten anses inaktiv og fjernet fra __inputs matrise.

Nå kommer vi til GameInput klasse.

// GameInput-konstruktør. funksjon GameInput ()  GameInput.prototype = verdi: 0, aktivert: true, // legger til en innmatingsenhetskanal. legg til: funksjon (kanal) inputAdd (denne kanalen); , // Fjerner en innmatingsenhetskanal. fjern: funksjon (kanal) inputRemove (denne kanalen); , // Fjerner alle innkommende enhetskanaler. Tilbakestill: funksjon () inputReset (dette); ;

Ja, det er alt det er - det er en super lett klasse som i hovedsak fungerer som et grensesnitt til hovedkoden. De verdi Eiendommen er et nummer som spenner fra 0 (null) til og med 1 (en). Hvis verdien er 0, det betyr at GameInput forekomst mottar ikke noe fra en hvilken som helst inngangsenhetskanal som er lagt til den.

De GameInput klassen har noen statiske egenskaper, så vi legger til de nå.

// Poengets X-posisjon i vinduets visningsport. GameInput.pointerX = 0; // Y-posisjonen til pekeren i vinduevisningsporten. GameInput.pointerY = 0; // Avstanden pekeren må flytte, i piksler per ramme, til // fordi verdien av en GameInput-forekomst er lik 1,0. GameInput.pointerSpeed ​​= 10;

Tastatur-enhetskanaler:

GameInput.KEYBOARD_A = KEYBOARD << DEVICE | 65 << CODE; GameInput.KEYBOARD_B = KEYBOARD << DEVICE | 66 << CODE; GameInput.KEYBOARD_C = KEYBOARD << DEVICE | 67 << CODE; GameInput.KEYBOARD_D = KEYBOARD << DEVICE | 68 << CODE; GameInput.KEYBOARD_E = KEYBOARD << DEVICE | 69 << CODE; GameInput.KEYBOARD_F = KEYBOARD << DEVICE | 70 << CODE; GameInput.KEYBOARD_G = KEYBOARD << DEVICE | 71 << CODE; GameInput.KEYBOARD_H = KEYBOARD << DEVICE | 72 << CODE; GameInput.KEYBOARD_I = KEYBOARD << DEVICE | 73 << CODE; GameInput.KEYBOARD_J = KEYBOARD << DEVICE | 74 << CODE; GameInput.KEYBOARD_K = KEYBOARD << DEVICE | 75 << CODE; GameInput.KEYBOARD_L = KEYBOARD << DEVICE | 76 << CODE; GameInput.KEYBOARD_M = KEYBOARD << DEVICE | 77 << CODE; GameInput.KEYBOARD_N = KEYBOARD << DEVICE | 78 << CODE; GameInput.KEYBOARD_O = KEYBOARD << DEVICE | 79 << CODE; GameInput.KEYBOARD_P = KEYBOARD << DEVICE | 80 << CODE; GameInput.KEYBOARD_Q = KEYBOARD << DEVICE | 81 << CODE; GameInput.KEYBOARD_R = KEYBOARD << DEVICE | 82 << CODE; GameInput.KEYBOARD_S = KEYBOARD << DEVICE | 83 << CODE; GameInput.KEYBOARD_T = KEYBOARD << DEVICE | 84 << CODE; GameInput.KEYBOARD_U = KEYBOARD << DEVICE | 85 << CODE; GameInput.KEYBOARD_V = KEYBOARD << DEVICE | 86 << CODE; GameInput.KEYBOARD_W = KEYBOARD << DEVICE | 87 << CODE; GameInput.KEYBOARD_X = KEYBOARD << DEVICE | 88 << CODE; GameInput.KEYBOARD_Y = KEYBOARD << DEVICE | 89 << CODE; GameInput.KEYBOARD_Z = KEYBOARD << DEVICE | 90 << CODE; GameInput.KEYBOARD_0 = KEYBOARD << DEVICE | 48 << CODE; GameInput.KEYBOARD_1 = KEYBOARD << DEVICE | 49 << CODE; GameInput.KEYBOARD_2 = KEYBOARD << DEVICE | 50 << CODE; GameInput.KEYBOARD_3 = KEYBOARD << DEVICE | 51 << CODE; GameInput.KEYBOARD_4 = KEYBOARD << DEVICE | 52 << CODE; GameInput.KEYBOARD_5 = KEYBOARD << DEVICE | 53 << CODE; GameInput.KEYBOARD_6 = KEYBOARD << DEVICE | 54 << CODE; GameInput.KEYBOARD_7 = KEYBOARD << DEVICE | 55 << CODE; GameInput.KEYBOARD_8 = KEYBOARD << DEVICE | 56 << CODE; GameInput.KEYBOARD_9 = KEYBOARD << DEVICE | 57 << CODE; GameInput.KEYBOARD_UP = KEYBOARD << DEVICE | 38 << CODE; GameInput.KEYBOARD_DOWN = KEYBOARD << DEVICE | 40 << CODE; GameInput.KEYBOARD_LEFT = KEYBOARD << DEVICE | 37 << CODE; GameInput.KEYBOARD_RIGHT = KEYBOARD << DEVICE | 39 << CODE; GameInput.KEYBOARD_SPACE = KEYBOARD << DEVICE | 32 << CODE; GameInput.KEYBOARD_SHIFT = KEYBOARD << DEVICE | 16 << CODE;

Pekerenhetskanaler:

GameInput.POINTER_UP = POINTER << DEVICE | 0 << CODE; GameInput.POINTER_DOWN = POINTER << DEVICE | 1 << CODE; GameInput.POINTER_LEFT = POINTER << DEVICE | 2 << CODE; GameInput.POINTER_RIGHT = POINTER << DEVICE | 3 << CODE; GameInput.POINTER_PRESS = POINTER << DEVICE | 4 << CODE;

Gamepad-enhetskanaler:

GameInput.GAMEPAD_A = GAMEPAD << DEVICE | 0 << CODE; GameInput.GAMEPAD_B = GAMEPAD << DEVICE | 1 << CODE; GameInput.GAMEPAD_X = GAMEPAD << DEVICE | 2 << CODE; GameInput.GAMEPAD_Y = GAMEPAD << DEVICE | 3 << CODE; GameInput.GAMEPAD_LB = GAMEPAD << DEVICE | 4 << CODE; GameInput.GAMEPAD_RB = GAMEPAD << DEVICE | 5 << CODE; GameInput.GAMEPAD_LT = GAMEPAD << DEVICE | 6 << CODE; GameInput.GAMEPAD_RT = GAMEPAD << DEVICE | 7 << CODE; GameInput.GAMEPAD_START = GAMEPAD << DEVICE | 8 << CODE; GameInput.GAMEPAD_SELECT = GAMEPAD << DEVICE | 9 << CODE; GameInput.GAMEPAD_L = GAMEPAD << DEVICE | 10 << CODE; GameInput.GAMEPAD_R = GAMEPAD << DEVICE | 11 << CODE; GameInput.GAMEPAD_UP = GAMEPAD << DEVICE | 12 << CODE; GameInput.GAMEPAD_DOWN = GAMEPAD << DEVICE | 13 << CODE; GameInput.GAMEPAD_LEFT = GAMEPAD << DEVICE | 14 << CODE; GameInput.GAMEPAD_RIGHT = GAMEPAD << DEVICE | 15 << CODE; GameInput.GAMEPAD_L_UP = GAMEPAD << DEVICE | 16 << CODE; GameInput.GAMEPAD_L_DOWN = GAMEPAD << DEVICE | 17 << CODE; GameInput.GAMEPAD_L_LEFT = GAMEPAD << DEVICE | 18 << CODE; GameInput.GAMEPAD_L_RIGHT = GAMEPAD << DEVICE | 19 << CODE; GameInput.GAMEPAD_R_UP = GAMEPAD << DEVICE | 20 << CODE; GameInput.GAMEPAD_R_DOWN = GAMEPAD << DEVICE | 21 << CODE; GameInput.GAMEPAD_R_LEFT = GAMEPAD << DEVICE | 22 << CODE; GameInput.GAMEPAD_R_RIGHT = GAMEPAD << DEVICE | 23 << CODE;

For å fullføre koden, trenger vi bare å ringe hoved() funksjon.

// Initialiser inngangssystemet. hoved();

Og det er all koden. Igjen er det alt tilgjengelig i kildefilene.

Løpe bort!

Før vi pakker opp opplæringen med en konklusjon, la oss ta en titt på et eksempel på hvordan GameInput klassen kan brukes. Denne gangen vil vi gi Bob muligheten til å flytte og hoppe fordi horder av mutant pølse zombier kan bli for mye for at han skal håndtere alene.

// Opprett inngangene. var hoppe = ny GameInput (); var moveLeft = ny GameInput (); var moveRight = ny GameInput (); // Fortell innspillene hva du skal reagere på. jump.add (GameInput.KEYBOARD_UP); jump.add (GameInput.KEYBOARD_W); jump.add (GameInput.GAMEPAD_A); moveLeft.add (GameInput.KEYBOARD_LEFT); moveLeft.add (GameInput.KEYBOARD_A); moveLeft.add (GameInput.GAMEPAD_LEFT); moveRight.add (GameInput.KEYBOARD_RIGHT); moveRight.add (GameInput.KEYBOARD_D); moveRight.add (GameInput.GAMEPAD_RIGHT); // Under hver spilloppdatering, kontroller du inngangene. funksjon oppdatering () hvis (jump.value> 0) // Fortell Bob å hoppe.  ellers // Fortell Bob å slutte å hoppe.  hvis (moveLeft.value> 0) // Fortell Bob å flytte / kjøre til venstre.  annet // Fortell Bob å slutte å flytte til venstre.  hvis (moveRight.value> 0) // Fortell Bob å flytte / kjøre til høyre.  annet // Fortell Bob å slutte å flytte til høyre. 

Fint og enkelt. Husk at verdi tilhører GameInput forekomster varierer fra 0 gjennom til 1, slik at vi kunne gjøre noe som å endre Bobs bevegelseshastighet ved å bruke denne verdien hvis en av de aktive inngangsenhetskanaler er analog.

hvis (moveLeft.value> 0) bob.x - = bob.maxDistancePerFrame * moveLeft.value; 

Ha det gøy!

Konklusjon

Cross-platform spill har alle en ting til felles: de alle trenger å håndtere en rekke spillinngangsenheter (controllere), og å håndtere disse innspillingsenhetene kan bli en skremmende oppgave. Denne opplæringen har vist en måte å håndtere flere inngangsenheter ved hjelp av en enkel, enhetlig API.