Spelinvoer vereenvoudigd

Stel je een gamekarakter voor met de naam "Bob the Butcher" die op zichzelf staat in een verduisterde kamer terwijl hordes mutante worstzombies binnenstromen door deuren en gebroken ramen. Op dit moment zou het een goed idee zijn voor Bob om de worstzombies in kleine stukjes vlees te schieten, maar hoe doet Bob dat in een platformoverschrijdend spel? Zal de spelspeler op een of meer toetsen op een toetsenbord moeten drukken, de muis aanklikken, op het scherm tikken of op een knop op een gamepad drukken??

Bij het programmeren van een platformoverschrijdend spel, is dit het soort ding waar je waarschijnlijk veel tijd mee doorbrengt als je er niet op voorbereid bent. Als je niet oppast, zou je massaal, spaghetti-achtig kunnen eindigen als verklaringen of schakelaar uitspraken om met alle verschillende invoerapparaten om te gaan.

In deze zelfstudie gaan we dingen veel eenvoudiger maken door een enkele klasse te maken die meerdere invoerapparaten verenigt. Elke instantie van de klasse vertegenwoordigt een specifieke spelactie of -gedrag (zoals "schieten", "uitvoeren" of "springen") en kan worden verteld om naar verschillende toetsen, knoppen en aanwijzers op meerdere invoerapparaten te luisteren.

Notitie: De programmeertaal die in deze zelfstudie wordt gebruikt, is JavaScript, maar de techniek die wordt gebruikt om meerdere invoerapparaten te verenigen, kan eenvoudig worden overgedragen naar elke andere platformonafhankelijke programmeertaal die API's voor invoerapparaten biedt.

De worsten schieten

Voordat we beginnen met het schrijven van de code voor de les die we in deze zelfstudie gaan maken, laten we even kijken hoe de klas daadwerkelijk kan worden gebruikt.

// Maak een invoer voor de actie "schieten". shoot = nieuwe GameInput (); // Vertel de input waarop moet worden gereageerd. shoot.add (GameInput.KEYBOARD_SPACE); shoot.add (GameInput.GAMEPAD_RT); // Controleer tijdens elke game-update de invoer. functie update () if (shoot.value> 0) // Vertel Bob om de mutante worstzombies te schieten!  else // Vertel Bob om te stoppen met fotograferen. 

GameInput is de klasse die we zullen creëren, en je kunt zien hoeveel eenvoudiger het zal worden. De shoot.value eigenschap is een getal en zal een positieve waarde zijn als het spatiebalk op een toetsenbord wordt ingedrukt of de juiste trigger op een gamepad is ingedrukt. Als noch de spatiebalk noch de rechter trigger wordt ingedrukt, is de waarde nul.

Ermee beginnen

Het eerste dat we moeten doen is een functiesluiting maken voor de GameInput klasse. Het grootste deel van de code die we schrijven, is niet echt een onderdeel van de klas, maar moet wel toegankelijk zijn vanuit de klas, maar verborgen blijven voor de rest. Een functiesluiting stelt ons in staat om dat in JavaScript te doen.

(In een programmeertaal zoals ActionScript of C # kunt u eenvoudig privéclass-leden gebruiken, maar dat is helaas geen luxe in JavaScript.)

(function () // code goes here) ();

De rest van de code in deze zelfstudie vervangt de opmerking "code gaat hier".

De variabelen

De code heeft slechts een handvol variabelen nodig om buiten de functies te worden gedefinieerd, en die variabelen zijn als volgt.

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, identifier: 0, moved: false, pressed: false; var __keyboard = ; var __inputs = []; var __channels = []; var __mouseDetected = false; var __touchDetected = false;

Het constante-achtige TOETSENBORD, WIJZER, GAMEPAD, APPARAAT en CODE waarden worden gebruikt om te definiëren invoerapparaatkanalen, zoals GameInput.KEYBOARD_SPACE, en hun gebruik zal later in de tutorial duidelijk worden.

De __wijzer object bevat eigenschappen die relevant zijn voor invoerapparaten van de muis en het aanraakscherm, en de __toetsenbord object wordt gebruikt om de toetstoestanden van het toetsenbord bij te houden. De __inputs en __channels arrays worden gebruikt om op te slaan GameInput instanties en alle kanalen voor invoerapparaten die aan die instanties zijn toegevoegd. eindelijk, de __mouseDetected en __touchDetected aangeven of een muis of touchscreen is gedetecteerd.

Notitie: De variabelen hoeven niet vooraf te worden voorafgegaan door twee onderstrepingstekens; dat is gewoon de codeerconventie die ik in deze zelfstudie voor de code heb gekozen. Het helpt om ze te scheiden van variabelen gedefinieerd in functies.

De functies

Hier komt het grootste deel van de code, dus misschien wil je een kopje koffie pakken of iets voordat je dit gedeelte begint te lezen!

Deze functies worden gedefinieerd na de variabelen in het vorige gedeelte van deze zelfstudie en worden gedefinieerd in volgorde van weergave.

// Initialiseert het invoersysteem. function main () // Ontmasker de constructor van GameInput. window.GameInput = GameInput; // Voeg de gebeurtenislisteners toe. addMouseListeners (); addTouchListeners (); addKeyboardListeners (); // Sommige UI-acties die we in een game moeten voorkomen. window.addEventListener ("contextmenu", killEvent, true); window.addEventListener ("selectstart", killEvent, true); // Start de updatelus. window.requestAnimationFrame (update); 

De hoofd() De functie wordt aan het einde van de code aangeroepen, dat wil zeggen aan het einde van de code functiesluiting die we eerder hebben gemaakt. Het doet wat het zegt op het blik en zorgt ervoor dat alles loopt, dus de GameInput klasse kan worden gebruikt.

Een ding dat ik onder uw aandacht moet brengen is het gebruik van de requestAnimationFrame () functie, die deel uitmaakt van de W3C Animation Timing-specificatie. Moderne games en applicaties gebruiken deze functie om hun update- of rendering-loops uit te voeren, omdat het in de meeste webbrowsers hiervoor zeer geoptimaliseerd is.

// Werkt het invoersysteem bij. functie-update () window.requestAnimationFrame (update); // Werk eerst de aanwijzerwaarden bij. updatePointer (); var i = __inputs.length; var input = null; var channels = null; while (i -> 0) input = __inputs [i]; channels = __channels [i]; if (input.enabled === true) updateInput (invoer, kanalen);  else input.value = 0; 

De bijwerken() functie rolt door de lijst met actieve GameInput instances en updates die zijn ingeschakeld. Het volgende updateInput () functie is vrij lang, dus ik zal de code hier niet toevoegen; je kunt de code volledig bekijken door de bronbestanden te downloaden.

// Werkt een GameInput-instantie bij. functie updateInput (invoer, kanalen) // opmerking: zie de bronbestanden

De updateInput () functie kijkt naar de invoerapparaatkanalen die aan a zijn toegevoegd GameInput exemplaar en uitwerkt wat de waarde eigendom van de GameInput instantie moet worden ingesteld op. Zoals te zien in de De worsten schieten voorbeeldcode, de waarde eigenschap geeft aan of een ingangsapparaatkanaal wordt geactiveerd en dat een spel daarop kan reageren, misschien door Bob te vertellen de mutante worstzombies te schieten.

// Werkt de waarde van een instantie GameInput bij. functie updateValue (invoer, waarde, drempel) if (threshold! == undefined) if (waarde < threshold )  value = 0;   // The highest value has priority. if( input.value < value )  input.value = value;  

De updateValue () functie bepaalt of het waarde eigendom van een GameInput instantie moet worden bijgewerkt. De drempel wordt voornamelijk gebruikt om te voorkomen dat ingangskanalen van analoge apparaten, zoals gamepadknoppen en sticks, die zichzelf niet correct resetten, een constant triggerende GameInput aanleg. Dit gebeurt vrij vaak met defecte of smerige gamepads.

Zoals de updateInput () functie, het volgende updatePointer () functie is vrij lang, dus ik zal de code hier niet toevoegen. U kunt de code volledig bekijken door de bronbestanden te downloaden.

// Werkt de aanwijzerwaarden bij. function updatePointer () // note: bekijk de bronbestanden

De updatePointer () functie werkt de eigenschappen in de update bij __wijzer voorwerp. In een notendop klemt de functie de positie van de aanwijzer vast om er zeker van te zijn dat deze de vensterviewport van de webbrowser niet verlaat en berekent het de afstand die de aanwijzer sinds de laatste update heeft verplaatst.

// Wordt gebeld wanneer een muisinvoerapparaat wordt gedetecteerd. function mouseDetected () if (__mouseDetected === false) __mouseDetected = true; // Raak aanraakgebeurtenissen niet aan als een muis wordt gebruikt. removeTouchListeners ();  // Wordt gebeld wanneer een invoerapparaat met aanraakscherm wordt gedetecteerd. function touchDetected () if (__touchDetected === false) __touchDetected = true; // Negeer muisgebeurtenissen als een aanraakscherm wordt gebruikt. removeMouseListeners (); 

De mouseDetected () en touchDetected () functies vertellen de code om het ene invoerapparaat of het andere te negeren. Als een muis wordt gedetecteerd vóór een aanraakscherm, wordt het aanraakscherm genegeerd. Als vóór een muis een aanraakscherm wordt gedetecteerd, wordt de muis genegeerd.

// Wordt aangeroepen wanneer een aanwijzerachtig invoerapparaat wordt ingedrukt. functie pointerPressed (x, y, identifier) ​​__pointer.identifier = identifier; __pointer.pressed = true; pointerMoved (x, y);  // Wordt gebeld wanneer een aanwijzerachtig invoerapparaat wordt vrijgegeven. function pointerReleased () __pointer.identifier = 0; __pointer.pressed = false;  // Wordt aangeroepen wanneer een aanwijzerachtig invoerapparaat wordt verplaatst. functie pointerMoved (x, y) __pointer.currentX = x >>> 0; __pointer.currentY = y >>> 0; if (__pointer.moved === false) __pointer.moved = true; __pointer.previousX = __pointer.currentX; __pointer.previousY = __pointer.currentY; 

De pointerPressed (), pointerReleased () en pointerMoved () functies verwerken invoer van een muis of een aanraakscherm. Alle drie de functies updaten eenvoudig de eigenschappen in de __wijzer voorwerp.

Na deze drie functies hebben we een handvol standaardfuncties voor JavaScript-gebeurtenissenafhandeling. De functies spreken voor zich, dus ik zal de code hier niet toevoegen; je kunt de code volledig bekijken door de bronbestanden te downloaden.

// Voegt een invoerapparaat toe aan een GameInput-instantie. functie invoerVoeg toe (invoer, kanaal) var i = __inputs.indexOf (invoer); if (i === -1) __inputs.push (invoer); __channels.push ([kanaal]); terug te keren;  var ca = __channels [i]; var ci = ca.indexOf (kanaal); if (ci === -1) ca.push (kanaal);  // Hiermee verwijdert u een invoerapparaatkanaal naar een instantie GameInput. functie inputRemove (invoer, kanaal) var i = __inputs.indexOf (input); if (i === -1) terug;  var ca = __channels [i]; var ci = ca.indexOf (kanaal); if (ci! == -1) ca.splice (ci, 1); if (ca.length === 0) __inputs.splice (i, 1); __channels.splice (i, 1);  // Hiermee wordt een instantie GameInput opnieuw ingesteld. function inputReset (input) var i = __inputs.indexOf (input); if (i! == -1) __inputs.splice (i, 1); __channels.splice (i, 1);  input.value = 0; input.enabled = true; 

De inputAdd (), inputRemove () en inputReset () functies worden aangeroepen vanuit een GameInput instantie (zie hieronder). De functies wijzigen de __inputs en __channels matrices afhankelijk van wat er moet gebeuren.

EEN GameInput instance wordt als actief beschouwd en toegevoegd aan de __inputs array, wanneer een invoerapparaatkanaal is toegevoegd aan de GameInput aanleg. Als een actief GameInput instance heeft alle invoerapparaatkanalen verwijderd, de GameInput bijvoorbeeld beschouwd als inactief en verwijderd uit de __inputs rangschikking.

Nu komen we aan bij de GameInput klasse.

// Constructor van GameInput. function GameInput ()  GameInput.prototype = waarde: 0, ingeschakeld: true, // Voegt een invoerapparaat toe. toevoegen: functie (kanaal) inputAdd (this, channel); , // Hiermee verwijdert u een invoerapparaatkanaal. remove: function (channel) inputRemove (this, channel); , // Hiermee verwijdert u alle invoerapparaatkanalen. reset: function () inputReset (this); ;

Ja, dat is alles wat er is - het is een superlichtgewichtklasse die in wezen fungeert als een interface naar de hoofdcode. De waarde eigenschap is een getal dat varieert van 0 (nul) tot en met 1 (een). Als de waarde is 0, het betekent dat de GameInput instance ontvangt niets van alle invoerapparaatkanalen die eraan zijn toegevoegd.

De GameInput klasse heeft een paar statische eigenschappen, dus we zullen die nu toevoegen.

// De X-positie van de aanwijzer binnen het venstervenster. GameInput.pointerX = 0; // De Y-positie van de aanwijzer binnen het venstervenster. GameInput.pointerY = 0; // De afstand die de aanwijzer moet verplaatsen, in pixels per frame, om // de waarde van een instantie GameInput gelijk aan 1,0 te maken. GameInput.pointerSpeed ​​= 10;

Toetsenbord apparaatkanalen:

GameInput.KEYBOARD_A = TOETSENBORD << 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;

Pointer apparaatkanalen:

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-apparaatkanalen:

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;

Om de code te finaliseren, hoeven we alleen de. Te bellen hoofd() functie.

// Initialiseer het invoersysteem. hoofd();

En dat is alles van de code. Nogmaals, het is allemaal beschikbaar in de bronbestanden.

Weglopen!

Voordat we de tutorial met een conclusie inpakken, laten we nog een voorbeeld bekijken van hoe het GameInput klasse kan worden gebruikt. Deze keer geven we Bob het vermogen om te bewegen en te springen, omdat de hordes gemuteerde worstzombies misschien te veel worden om alleen te behandelen.

// Maak de invoer. var jump = new GameInput (); var moveLeft = new GameInput (); var moveRight = new GameInput (); // Vertel de ingangen waarop moet worden gereageerd. 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); // Controleer tijdens elke game-update de ingangen. functie-update () if (jump.value> 0) // Vertel Bob om te springen.  else // Zeg tegen Bob dat hij moet stoppen met springen.  if (moveLeft.value> 0) // Zeg tegen Bob om te bewegen / naar links rennen.  else // Vertel Bob om te stoppen met naar links te gaan.  if (moveRight.value> 0) // Zeg tegen Bob om te bewegen / ren naar rechts.  else // Vertel Bob om te stoppen met naar rechts te gaan. 

Leuk en gemakkelijk. Houd er rekening mee dat het waarde eigendom van GameInput instanties varieert van 0 door naar 1, dus we zouden iets kunnen doen als het veranderen van de bewegingssnelheid van Bob met die waarde als een van de kanalen van het actieve invoerapparaat analoog is.

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

Veel plezier!

Conclusie

Platformoverschrijdende games hebben allemaal één ding gemeen: ze moeten allemaal omgaan met een veelvoud aan game-invoerapparaten (controllers) en het omgaan met die invoerapparaten kan een hele klus worden. Deze zelfstudie heeft aangetoond dat er één manier is om meerdere invoerapparaten te verwerken met behulp van een eenvoudige, uniforme API.