Spielentwicklung

Ein Webspiel in einer Stunde

Michael Oneppo

Laden Sie die Codebeispiele herunter

Für die Spieleentwicklung sind keine komplett neuen Fähigkeiten erforderlich. Ihre aktuellen Kenntnisse in Bezug auf die Webentwicklung in HTML, JavaScript, CSS usw. sind für eine Vielzahl von Spielen komplett ausreichend. Wenn Sie ein Spiel mit Webtechnologien erstellen, funktioniert es auf praktisch jedem Gerät mit einem Browser.

Um Ihnen das zu beweisen, werde ich mithilfe von Webtechnologien und nur zwei externen Bibliotheken ein Spiel von Grund auf neu erstellen. Das gelingt in weniger als einer Stunde. Dabei werde ich mich mit mehreren Themen zur Spieleentwicklung befassen. Es geht um das grundlegende Design und Layout, um Sprites und um künstliche Intelligenz für einen einfachen Gegner. Ich werde das Spiel sogar so entwickeln, dass es auf PCs, Tablets und Smartphones gleichermaßen funktioniert. Wenn Sie als Webentwickler oder in einer anderen Entwicklungsdomäne bereits über Programmiererfahrung verfügen, aber noch niemals ein Spiel geschrieben haben, erfahren Sie hier, wie Sie loslegen. Ich zeige Ihnen alle Grundlagen in einer Stunde.

Erste Schritte

Ich werde die gesamte Entwicklung in Visual Studio vornehmen. Das erlaubt mir ein schnelles Ausführen der Webanwendung, wenn ich Änderungen vornehme. Zum Mitmachen benötigen Sie unbedingt die neueste Version von Visual Studio. Sie können Sie unter bit.ly/1xEjEnX herunterladen. Ich verwende Visual Studio 2013 Pro, aktualisiere den Code aber mit Visual Studio 2013 Community.

Diese App erfordert keinen Servercode. Daher beginne ich mit dem Erstellen eines neuen, leeren Websiteprojekts in Visual Studio. Ich verwende die leere C#-Vorlage für eine Website, indem ich die Visual C#-Option auswähle, nachdem ich auf "Datei | Neu | ASP.NET Leere Website" geklickt habe.

Die HTML-Indexdatei benötigt nur drei Ressourcen: jQuery, ein Haupt-Stylesheet sowie eine Haupt-JavaScript-Datei. Ich füge dem Projekt eine leere CSS-Datei mit dem Namen style.css sowie eine leere JavaScript-Datei namens ping.js hinzu, um Fehler beim Laden der Seite zu vermeiden:

<!DOCTYPE html>
<html>
<head>
  <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.min.js"></script>
  <script src="ping.js"></script>
  <link rel="stylesheet" href="style.css"></script>
</head>
<body>
</body>
</html>

Grundlegender Entwurf

Das Spiel, das ich erstellen werde, ist eine Variante von Pong, die ich Ping nenne. Ping verfügt über dieselben Regeln wie Pong mit einer Ausnahme: Jeder Spieler kann den Ball festhalten, wenn er ihn fängt und anschließend direkt oder in einem Winkel nach oben oder unten zurückschießen. Am besten ist oft, wenn Sie das Erscheinungsbild des Spiels vor dem Entwickeln zeichnen. Bei meinem Spiel sehen Sie das erwünschte Gesamtlayout in Abbildung 1.

Das Erscheinungsbild von Ping
Abbildung 1: Das Erscheinungsbild von Ping

Sobald ich das Designlayout des Spiels entwickelt habe, müssen die einzelnen Elemente lediglich dem HTML-Code hinzugefügt werden, um das Spiel zu erstellen. Beachten Sie bitte, dass die Anzeigetafel und die Steuerelemente gruppiert werden sollten, damit sie beieinander bleiben. In Abbildung 2 sehen Sie, wie ich die Elemente nach und nach hinzugefügt habe.

Abbildung 2: Anfängliches HTML-Layout

<div id="arena">
  <div id="score">
    <h1>
      <span id="playerScore">0</span>
      -
      <span id="opponentScore">0</span>
    </h1>
  </div>
  <div id="player"></div>
  <div id="opponent"></div>
  <div id="ball"></div>
  <div id="controls-left">
    <div id="up"></div>
    <div id="down"></div>
  </div>
  <div id="controls-right">
    <div id="left"></div>
    <div id="right"></div>
  </div>
</div>

Spielen mit Stil

Wenn Sie diese Seite laden, sehen Sie gar nichts, weil ihr kein Stylesheet zugewiesen wurde. Ich habe in meinem HTML-Code bereits einen Link zu einer Datei mit dem Namen "main.css" eingerichtet. Daher kopiere ich mein gesamtes CSS in eine neue Datei mit diesem Namen. Zunächst platziere ich die Elemente auf dem Bildschirm. Der Hauptteil der Seite muss den ganzen Bildschirm einnehmen. Daher richte ich diesen zuerst ein:

body {
  margin: 0px;
  height: 100%;
}

Dann muss das Hintergrundbild des Sportplatzes den gesamten Bildschirm einnehmen (siehe Abbildung 3):

#arena {
  background-image: url(arena.png);
  background-size: 100% 100%;
  margin: 0px;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

Der Sportplatzhintergrund
Abbildung 3: Der Sportplatzhintergrund

Anschließend platziere ich die Anzeigetafel. Ich möchte, dass sie oben in der Mitte über den anderen Elementen angezeigt wird. Der Befehl Position: Absolute lässt mich es platzieren wo ich will und Links: 50%" erscheint sie in der Mitte des Fensters, jedoch ausgehend von der linken Seite des Anzeigetafelelements. Ich zentriere sie komplett mittig, indem ich die transform-Eigenschaft verwende. Die z-index-Eigenschaft garantiert, dass sie stets über den anderen Elementen angezeigt wird:

#score {
  position: absolute;
  z-index: 1000;
  left: 50%;
  top: 5%;
  transform: translate(-50%, 0%);
}

Ich möchte, dass die Schriftart für den Text zum Retro-Design passt. Bei den meisten modernen Browsern kann ich meine eigenen Schriftarten verwenden. Bei codeman38 von zone38.net fand ich die passende Schriftart: Press Start 2P. Damit ich die Schriftart der Anzeigetafel hinzufügen kann, muss ich eine neue Schriftart erstellen:

@font-face {
  font-family: 'PressStart2P';
  src: url('PressStart2P.woff');
}

Jetzt befinden sich die Spielergebnisse in einem h1-Tag. Ich kann also die Schriftart für alle h1-Tags festlegen. Für den Fall, dass die Schriftart fehlt, biete ich ein paar Ausweichlösungen:

h1 {
  font-family: 'PressStart2P', 'Georgia', serif;
}

Für die anderen Elemente verwende ich ein Sprite-Sheet für Bilder. Ein Sprite-Sheet enthält alle für das Spiel erforderlichen Bilder in einer Datei (siehe Abbildung 4).

Das Sprite-Sheet für Ping
Abbildung 4: Das Sprite-Sheet für Ping

Jedes Element, das über ein Bild auf diesem Sheet verfügt, hat auch eine zugewiesene Sprite-Klasse. Für jedes Element verwende ich "background-position", um festzulegen, welcher Teil des Sprite-Sheets angezeigt werden soll:

.sprite {
  background-image: url("sprites.png");
  width: 128px;
  height: 128px;
}

Als Nächstes füge ich die Sprite-Klasse allen Elementen hinzu, die das Sprite-Sheet verwenden. Dazu muss ich kurz zu HTML zurückkehren:

<div id="player" class="sprite"></div>
<div id="opponent" class="sprite"></div>
<div id="ball" class="sprite"></div>
<div id="controls-left">
  <div id="up" class="sprite"></div>
  <div id="down" class="sprite"></div>
</div>
<div id="controls-right">
  <div id="left" class="sprite"></div>
  <div id="right" class="sprite"></div>
</div>

Jetzt muss ich die Positionen jedes Sprites auf dem Sheet für jedes Element angeben. Dazu verwende ich wieder "background-position", wie in Abbildung 5 gezeigt.

Abbildung 5: Hinzufügen von Offsets für das Sprite-Sheet

#player {
  position: absolute;
  background-position: 0px 128px;
}
#opponent {
  position: absolute;
  background-position: 0px 0px;
}
#ball {
  position: absolute;
  background-position: 128px 128px;
}
#right {
  background-position: 64px 192px;
}
#left {
  background-position: 64px 0px;
}
#down {
  background-position: 128px 192px;
}
#up {
  background-position: 128px 0px;
}

Die Position: Mit der Eigenschaft "position: absolute" für Spieler, Gegner und Ball kann ich diese mit JavaScript bewegen. Wenn Sie jetzt auf die Seite blicken, werden Sie bemerken, dass die Steuerelemente und der Ball über zusätzliche Teile verfügen. Das kommt daher, weil die Sprite-Größen kleiner als die standardmäßigen 128 Pixel sind. Ich ändere diese also auf die korrekte Größe. Es gibt nur einen Ball, dessen Größe ich direkt festlege:

#ball {
  position: absolute;
  width: 64px;
  height: 64px;
  background-position: 128px 128px;
}

Es gibt vier Steuerelemente, also Schaltflächen mit denen der Benutzer den Spieler bewegen kann. Für diese sollte ich eine besondere Klasse erstellen. Ich füge auch einen Rand hinzu, damit außen herum ein wenig Platz entsteht:

.control {
  margin: 16px;
  width: 64px;
  height: 64px;
}

Nach dem Hinzufügen dieser Klasse sehen die Steuerelemente des Spiels schon viel besser aus:

<div id="controls-left">
  <div id="up" class="sprite control"></div>
  <div id="down" class="sprite control"></div>
</div>
<div id="controls-right">
  <div id="left" class="sprite control"></div>
  <div id="right" class="sprite control"></div>
</div>

Nun muss ich als Letztes die Steuerelemente so platzieren, dass sie sich in der Nähe der Daumen des Benutzers befinden, falls die Seite auf einem Mobilgerät ausgeführt wird. Ich setze sie in die unteren Ecken:

#controls-left {
  position: absolute;
  left: 0; bottom: 0;
}
#controls-right {
  position: absolute;
  right: 0; bottom: 0;
}

Praktisch ist bei diesem Design die Wahl relativer Positionen für alle Elemente. Das bedeutet, dass das Spiel auf Bildschirmen verschiedener Größen immer gut aussieht.

Folgen Sie dem hüpfenden Ball

Jetzt bringe ich den Ball dazu, sich zu bewegen. Für den JavaScript-Code habe ich einen Verweis auf eine Datei mit dem Namen ping.js in HTML erstellt, genau wie für das CSS. Diesen Code füge ich einer neuen Datei mit diesem Namen hinzu. Ich erstelle Objekte für den Ball und für jeden Spieler, aber ich verwende das Factorymuster für die Objekte.

Dies ist ein einfaches Konzept. Die Ballfunktion erstellt beim Aufrufen einen neuen Ball. Dazu ist keine Tastatur erforderlich. Durch das Muster entsteht keine Verwirrung um diese Variable, da die verfügbaren Objekteigenschaften deutlich werden. Und weil ich für die Entwicklung dieses Spiel nur eine Stunde zur Verfügung habe, muss ich alle verwirrenden Konzepte vermeiden.

Die Struktur dieses Musters beim Erstellen der einfachen Ball-Klasse sehen Sie in Abbildung 6.

Abbildung 6: Die Ball-Klasse

var Ball = function( {
  // List of variables only the object can see (private variables).
  var velocity = [0,0];
  var position = [0,0];
  var element = $('#ball');
  var paused = false;
  // Method that moves the ball based on its velocity. This method is only used
  // internally and will not be made accessible outside of the object.
  function move(t) {
  }
  // Update the state of the ball, which for now just checks  
  // if the play is paused and moves the ball if it is not.  
  // This function will be provided as a method on the object.
  function update(t) {
    // First the motion of the ball is handled
    if(!paused) {
      move(t);
    }
  }
  // Pause the ball motion.
  function pause() {
    paused = true;
  }
  // Start the ball motion.
  function start() {
    paused = false;
  }
  // Now explicitly set what consumers of the Ball object can use.
  // Right now this will just be the ability to update the state of the ball,
  // and start and stop the motion of the ball.
  return {
    update:       update,
    pause:        pause,
    start:        start
}

Zum Erstellen eines neuen Balls rufe ich einfach diese bereits definierte Funktion auf:

var ball = Ball();

Jetzt möchte ich, dass sich der Ball auf dem Bildschirm bewegt und hüpft. Zuerst muss ich in bestimmten Intervallen die Aktualisierungsfunktion abrufen, damit der Ball animiert wird. Moderne Browser bieten eine Funktion für diesen Zweck. Sie heißt "requestAnimationFrame". Die Funktion wird hierbei als Argument verwendet und ruft beim nächsten Ausführen des Animationszyklusses die passed-in-Funktion auf. Dadurch bewegt sich der Ball flüssig, wenn der Browser für eine Aktualisierung bereit ist. Sobald die passed-in-Funktion aufgerufen wird, erhält diese die Zeit in Sekunden, die seit dem Laden der Seite verstrichen ist. Das ist wichtig, um sicherzustellen, dass die Animationen über längere Zeit hinweg konsistent erfolgen. Im Spiel erscheint die Nutzung von "requestAnimationFrame" wie folgt:

var lastUpdate = 0;
var ball = Ball();
function update(time) {
  var t = time - lastUpdate;
  lastUpdate = time;
  ball.update(t);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

Beachten Sie, dass "requestAnimationFrame" in der Funktion wieder aufgerufen wird, sobald der Ball die Aktualisierung beendet hat. So wird eine kontinuierliche Animation sichergestellt.

Dieser Code funktioniert, aber es kann zu einem Problem kommen, wenn das Skript die Ausführung beginnt, bevor die Seite vollständig geladen wurde. Das kann ich vermeiden, indem der Code gestartet wird, sobald die Seite geladen wird. Dazu verwende ich jQuery:

var ball;
var lastUpdate;
$(document).ready(function() {
  lastUpdate = 0;
  ball = Ball();
  requestAnimationFrame(update);
});

Da ich die Ballgeschwindigkeit und die Zeit seit seiner letzten Aktualisierung kenne, kann ich den Ball mit einem ganz einfachem Trick vorwärts bewegen:

var position = [300, 300];
var velocity = [-1, -1];
var move = function(t) {
  position[0] += velocity[0] * t;
  position[1] += velocity[1] * t;  
  element.css('left', position[0] + 'px');
  element.css('top', position[1] + 'px');
}

Versuchen Sie, den Code auszuführen. Sie sehen, dass der Ball sich im Winkel wegbewegt und vom Bildschirm verschwindet. Das ist zwar wunderbar, aber wenn der Ball weg ist, war es das auch schon. Daher ist der nächste logische Schritt: Der Ball muss von den Bildschirmrändern abprallen, wie in Abbildung 7 gezeigt. Wenn Sie diesen Code hinzufügen, zeigt die ausgeführte App einen konstant hüpfenden Ball.

Abbildung 7: Einfach hüpfender Ball

var move = function(t) {
  // If the ball hit the top or bottom, reverse the vertical speed.
  if (position[1] <= 0 || position[1] >= innerHeight) {
   velocity[1] = -velocity[1];
  }
  // If the ball hit the left or right sides, reverse the horizontal speed.
  if (position[0] <= 0 || position[0] >= innerWidth) {
   velocity[0] = -velocity[0];
  }
  position[0] += velocity[0] * t;
  position[1] += velocity[1] * t; 
  element.css('left', (position[0] - 32) + 'px');
  element.css('top', (position[1] - 32) + 'px');
}

Ein beweglicher Spieler

Jetzt müssen aus den Spielern Objekte werden. Die Spieler-Klasse wird "animiert" indem Sie die move-Funktion verwenden, um die Position des Spielers zu wechseln. Die side-Variable gibt an, auf welcher Seite des Sportplatzes sich der Spieler befindet. Sie gibt die horizontale Position des Spielers vor. Der an die move-Funktion weitergegebene y-Wert gibt an, um welche Distanz sich der Spieler auf und ab bewegen kann:

var Player = function (elementName, side) {
  var position = [0,0];
  var element = $('#'+elementName);
  var move = function(y) {
  }
  return {
    move: move,
    getSide:      function()  { return side; },
    getPosition:  function()  { return position; }
  }
}

Abbildung 8 zeigt die Bewegung des Spielers. Die Bewegung wird gestoppt, sobald das Spieler-Sprite den oberen oder unteren Rand des Fensters erreicht.

Ich kann jetzt zwei Spieler erstellen und diese sich auf die korrekte Seite des Bildschirms bewegen lassen:

player = Player('player', 'left');
player.move(0);
opponent = Player('opponent', 'right');
opponent.move(0);

Abbildung 8: Bewegungssteuerelemente für das Spieler-Sprite

var move = function(y) {
  // Adjust the player's position.
  position[1] += y;
  // If the player is off the edge of the screen, move it back.
  if (position[1] <= 0)  {
    position[1] = 0;
  }
  // The height of the player is 128 pixels, so stop it before any
  // part of the player extends off the screen.
  if (position[1] >= innerHeight - 128) {
    position[1] = innerHeight - 128;
  }
  // If the player is meant to stick to the right side, set the player position
  // to the right edge of the screen.
  if (side == 'right') {
    position[0] = innerWidth - 128;
  }
  // Finally, update the player's position on the page.
  element.css('left', position[0] + 'px');
  element.css('top', position[1] + 'px');
}

Tastatureingabe

Theoretisch können Sie den Spieler jetzt bewegen. Dazu ist jedoch eine Anweisung erforderlich. Fügen Sie für den Spieler links Steuerelemente hinzu. Der Spieler soll auf zwei Arten gesteuert werden: auf dem PC über die Tastatur und auf Tablets und Smartphones über das Tippen auf die Steuerelemente.

Damit auf verschiedenen Plattformen die Touch- und die Mauseingabe konsistent funktioniert, verwende ich das hervorragende einheitliche Framework Hand.js (handjs.codeplex.com). Zuerst füge ich das Skript dem HTML-Code im Bereich "head" hinzu:

 

<script src="hand.minified-1.3.8.js"></script>

Abbildung 9 zeigt die Nutzung von Hand.js und jQuery, um den Spieler über die Tasten A und Z zu steuern oder wenn Sie auf die Steuerelemente tippen.

Abbildung 9: Touch- und Tastatursteuerelemente

var distance = 24;  // The amount to move the player each step.
$(document).ready(function() {
  lastUpdate = 0;
  player = Player('player', 'left');
  player.move(0);
  opponent = Player('opponent', 'right');
  opponent.move(0);
  ball = Ball();
  // pointerdown is the universal event for all types of pointers -- a finger,
  // a mouse, a stylus and so on.
  $('#up')    .bind("pointerdown", function() {player.move(-distance);});
  $('#down')  .bind("pointerdown", function() {player.move(distance);});
  requestAnimationFrame(update);
});
$(document).keydown(function(event) {
  var event = event || window.event;
  // This code converts the keyCode (a number) from the event to an uppercase
  // letter to make the switch statement easier to read.
  switch(String.fromCharCode(event.keyCode).toUpperCase()) {
    case 'A':
      player.move(-distance);
      break;
    case 'Z':
      player.move(distance);
      break;
  }
  return false;
});

Fangen des Balls

Wenn der Ball hüpft, möchte ich, dass die Spieler ihn fangen können. Sobald er gefangen ist, hat der Ball einen Besitzer und folgt den Bewegungen dieses Besitzers. Abbildung 10 fügt der move-Methode des Balls eine Funktionalität hinzu, sodass der Ball einem Besitzer folgt.

Abbildung 10: Der Ball folgt seinem Besitzer

var move = function(t) {
  // If there is an owner, move the ball to match the owner's position.
  if (owner !== undefined) {
    var ownerPosition = owner.getPosition();
    position[1] = ownerPosition[1] + 64;
    if (owner.getSide() == 'left') {
      position[0] = ownerPosition[0] + 64;
    } else {
      position[0] = ownerPosition[0];
    }
  // Otherwise, move the ball using physics. Note the horizontal bouncing
  // has been removed -- ball should pass by a player if it
  // isn't caught.
  } else {
    // If the ball hits the top or bottom, reverse the vertical speed.
    if (position[1] - 32 <= 0 || position[1] + 32 >= innerHeight) {
      velocity[1] = -velocity[1];
    }
    position[0] += velocity[0] * t;
    position[1] += velocity[1] * t;  
  }
  element.css('left', (position[0] - 32) + 'px');
  element.css('top',  (position[1] - 32) + 'px');
}

Aktuell gibt es keine Möglichkeit, die Position eines Spielerobjekts abzurufen. Daher füge ich dem Spielerobjekt die Zugriffsmethoden "getPosition" und "getSide" hinzu:

return {
  move: move,
  getSide:      function()  { return side; },
  getPosition:  function()  { return position; }
}

Wenn der Ball jetzt einen Besitzer hat, folgt er diesem nach. Aber wie kann ich den Besitzer festlegen? Jemand muss den Ball fangen. Abbildung 11 zeigt, wie Sie festlegen, wann einer der Spieler-Sprites den Ball berührt. Wenn das passiert, wird dieser Spieler zum Besitzer des Balls.

Abbildung 11: Kollisionserkennung für Ball und Spieler

var update = function(t) {
// First the motion of the ball is handled.
if(!paused) {
    move(t);
}
// The ball is under control of a player, no need to update.
if (owner !== undefined) {
    return;
}
// First, check if the ball is about to be grabbed by the player.
var playerPosition = player.getPosition();
  if (position[0] <= 128 &&
      position[1] >= playerPosition[1] &&
      position[1] <= playerPosition[1] + 128) {
    console.log("Grabbed by player!");
    owner = player;
}
// Then the opponent...
var opponentPosition = opponent.getPosition();
  if (position[0] >= innerWidth - 128 &&
      position[1] >= opponentPosition[1] &&
      position[1] <= opponentPosition[1] + 128) {
    console.log("Grabbed by opponent!");
    owner = opponent;
}

Wenn Sie das Spiel jetzt versuchen zu spielen, wird der Ball oben am Bildschirm abprallen. Sie können aber den Spieler nicht bewegen, um den Ball zu fangen. Wie sollen Sie ihn dann werfen? Dafür sind die Steuerelemente der rechten Hand gedacht – zum Zielen. Abbildung 12 fügt dem Spieler eine Funktion zum Werfen und eine Zieleigenschaft hinzu.

Abbildung 12: Zielen und Werfen des Balls

var aim = 0;
var fire = function() {
  // Safety check: if the ball doesn't have an owner, don't not mess with it.
  if (ball.getOwner() !== this) {
    return;
  }
  var v = [0,0];
  // Depending on the side the player is on, different directions will be thrown.
  // The ball should move at the same speed, regardless of direction --
  // with some math you can determine that moving .707 pixels on the
  // x and y directions is the same speed as moving one pixel in just one direction.
  if (side == 'left') {
    switch(aim) {
    case -1:
      v = [.707, -.707];
      break;
    case 0:
      v = [1,0];
      break;
    case 1:
      v = [.707, .707];
    }
  } else {
    switch(aim) {
    case -1:
      v = [-.707, -.707];
      break;
    case 0:
      v = [-1,0];
      break;
    case 1:
      v = [-.707, .707];
    }
  }
  ball.setVelocity(v);
  // Release control of the ball.
  ball.setOwner(undefined);
}
// The rest of the Ball definition code goes here...
return {
  move: move,
  fire: fire,
  getSide:      function()  { return side; },
  setAim:       function(a) { aim = a; },
  getPosition:  function()  { return position; },
}

Abbildung 13 erweitert die Tastaturfunktion für das Zielen und Werfen des Spielers. Das Zielen funktioniert ein wenig anders. Sobald die Zieltaste losgelassen wird, kehrt das Ziel auf "gerade" zurück.

Abbildung 13: Einrichten der Zielfunktion des Spielers

$(document).keydown(function(event) {
  var event = event || window.event;
  switch(String.fromCharCode(event.keyCode).toUpperCase()) {
    case 'A':
      player.move(-distance);
      break;
    case 'Z':
      player.move(distance);
      break;
    case 'K':
      player.setAim(-1);
      break;
    case 'M':
      player.setAim(1);
      break;
    case ' ':
      player.fire();
      break;
  }
  return false;
});
$(document).keyup(function(event) {
  var event = event || window.event;
  switch(String.fromCharCode(event.keyCode).toUpperCase()) {
    case 'K':
    case 'M':
      player.setAim(0);
      break;
  }
  return false;
});

Zum Schluss wird für alle Steuerelemente eine Touchfunktion hinzugefügt. Die Steuerelemente auf der rechten Seite ändern nun das Zielen des Spielers. Ich richte es außerdem so ein, dass der Ball bei jeder Berührung des Bildschirms geworfen wird:

$('#left')  .bind("pointerdown", function() {player.setAim(-1);});
$('#right') .bind("pointerdown", function() {player.setAim(1);});
$('#left')  .bind("pointerup",   function() {player.setAim(0);});
$('#right') .bind("pointerup",   function() {player.setAim(0);});
$('body')   .bind("pointerdown", function() {player.fire();});

Aufzeichnen des Spielstands

Wenn ein Ball an einem Spieler vorbeifliegt, möchte ich den Spielstand ändern und diesem Spieler den Ball geben. Dazu verwende ich benutzerdefinierte Ereignisse. So kann ich den Spielstand von allen vorhandenen Objekten trennen. Die Aktualisierungsfunktion wird ein wenig lang. Daher füge ich eine neue private Funktion mit dem Namen "checkScored" hinzu:

function checkScored() {
  if (position[0] <= 0) {
    pause();
    $(document).trigger('ping:opponentScored');
  }
  if (position[0] >= innerWidth) {
    pause();
    $(document).trigger('ping:playerScored');
  }
}

Die Abbildung 14 zeigt den Code, der auf diese Ereignisse reagiert, den Spielstand aktualisiert sowie den Ball an den anderen Spieler abgibt. Fügen Sie diesen Code unten im JavaScript-Dokument ein.

Abbildung 14: Aktualisieren der Anzeigetafel

$(document).on('ping:playerScored', function(e) {
  console.log('player scored!');
  score[0]++;
  $('#playerScore').text(score[0]);
  ball.setOwner(opponent);
  ball.start();
});
$(document).on('ping:opponentScored', function(e) {
  console.log('opponent scored!');
  score[1]++;
  $('#opponentScore').text(score[1]);
  ball.setOwner(player);
  ball.start();
});

Wenn der Ball jetzt an Ihrem Gegner vorbeifliegt (was nicht schwer ist, da er sich nicht bewegt) steigt Ihr Spielstand und der Gegner erhält den Ball. Er hält den Ball jedoch einfach fest.

Was fehlt? Die Intelligenz!

Fast ist Ihr Spiel fertig. Sie brauchen nur noch einen Mitspieler. Als letzten Schritt zeige ich Ihnen, wie Sie den Gegner durch einfache künstliche Intelligenz steuern. Der Gegner wird versuchen, parallel zum Ball zu bleiben, wenn dieser sich bewegt. Sobald er den Ball fängt, bewegt er sich zufällig und wirft ihn in eine zufällige Richtung. Damit der Computergegner ein wenig menschlicher wirkt, füge ich bei allem eine Verzögerung hinzu. Sehr intelligent ist unser Gegner zwar nicht, aber man kann zumindest gegen ihn spielen.

Wenn Sie diese Art von System entwerfen, sollten Sie sich die verschiedenen Zustandsformen des Gegners vorstellen. Für den Computergegner gibt es drei Möglichkeiten: Er kann folgen, zielen/werfen und warten. Zwischen den Folgeaktionen füge ich eine kurze Verzögerung hinzu, damit der Gegner menschlicher wirkt. Ich beginne zunächst damit:

function AI(playerToControl) {
  var ctl = playerToControl;
  var State = {
    WAITING: 0,
    FOLLOWING: 1,
    AIMING: 2
  }
  var currentState = State.FOLLOWING;
}

Je nach seinem Zustand, möchte ich, dass der Gegner jeweils eine andere Aktion ausführt. Wie beim Ball erstelle ich eine Aktualisierungsfunktion, die ich in "requestAnimationFrame" abrufen kann, damit der Gegner entsprechend seinem Zustand handelt:

function update() {
  switch (currentState) {
    case State.FOLLOWING:
      // Do something to follow the ball.
      break;
    case State.WAITING:
      // Do something to wait.
      break;
    case State.AIMING:
      // Do something to aim.
      break;
  }
}

Das FOLGEN ist ganz einfach. Der Gegner bewegt sich in vertikaler Richtung mit dem Ball und wechselt zwischendurch in den Zustand WARTEN, um eine langsamere Reaktionszeit zu simulieren. Abbildung 15 zeigt diese beiden Zustände.

Abbildung 15: Computergegner FOLGT durch künstliche Intelligenz

function moveTowardsBall() {
  // Move the same distance the player would move, to make it fair.
  if(ball.getPosition()[1] >= ctl.getPosition()[1] + 64) {
    ctl.move(distance);
  } else {
    ctl.move(-distance);
  }
}
function update() {
  switch (currentState) {
    case State.FOLLOWING:
      moveTowardsBall();
      currentState = State.WAITING;
    case State.WAITING:
      setTimeout(function() {
        currentState = State.FOLLOWING;
      }, 400);
      break;
    }
  }
}

Durch den Code in Abbildung 15 wartet der Computergegner einen Sekundenbruchteil beim Folgen des Balls. Jetzt fügen sie der spieleweiten Aktualisierungsfunktion den Code hinzu:

function update(time) {
  var t = time - lastUpdate;
  lastUpdate = time;
  ball.update(t);
  ai.update();
  requestAnimationFrame(update);
}

Sobald Sie das Spiel ausführen, sehen Sie, dass der Gegner die Bewegungen des Balls verfolgt. Das ist gar nicht schlecht für eine künstliche Intelligenz in weniger als 30 Codezeilen. Wenn der Gegner den Ball fängt, passiert natürlich bis jetzt noch nichts. Zum Ende der Stunde müssen wir also noch die Aktionen für den Zustand ZIELEN hinzufügen. Ich möchte, dass sich der Gegner ein paar mal zufällig bewegt und den Ball dann in eine zufällige Richtung wirft. Abbildung 16 fügt eine private Funktion hinzu, die genau das tut. Wenn Sie der Case-Anweisung eine aimAndFire-Funktion hinzufügen, erstellen Sie einen voll funktionsfähigen Computergegner mit künstlicher Intelligenz, gegen den Sie spielen können.

Abbildung 16: Zielen und Werfen des Computergegners

function repeat(cb, cbFinal, interval, count) {
  var timeout = function() {
    repeat(cb, cbFinal, interval, count-1);
  }
  if (count <= 0) {
    cbFinal();
  } else {
    cb();
    setTimeout(function() {
      repeat(cb, cbFinal, interval, count-1);
    }, interval);
  }
}
function aimAndFire() {
  // Repeat the motion action 5 to 10 times.
  var numRepeats = Math.floor(5 + Math.random() * 5);
  function randomMove() {
    if (Math.random() > .5) {
      ctl.move(-distance);
    } else {
      ctl.move(distance);
    }
  }
  function randomAimAndFire() {
    var d = Math.floor( Math.random() * 3 - 1 );
    opponent.setAim(d);
    opponent.fire();
    // Finally, set the state to FOLLOWING.
    currentState = State.FOLLOWING;
  }
  repeat(randomMove, randomAimAndFire, 250, numRepeats);
}

Zusammenfassung

Jetzt haben Sie ein vollständiges Internetspiel, das auf PCs, Smartphones und Tablets funktioniert. Natürlich gibt es noch viele Verbesserungsmöglichkeiten für dieses Spiel. Beispielsweise wird es im Hochformat auf einem Smartphone etwas seltsam aussehen, daher muss das Smartphone im Querformat gehalten werden. Ich wollte Ihnen lediglich zeigen, was bei der Spieleentwicklung in und außerhalb des Internets möglich ist.


Michael Oneppo *ist Kreativtechnologe und früherer Programmmanager im Direct3D-Team bei Microsoft. In den letzten Jahren engagierte er sich als technischer Direktor in der gemeinnützigen Einrichtung "Library For All" und arbeitete an einem Master-Abschluss im "Interactive Telecommunications Program" der Universität von New York (NYU).*​

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Mohamed Ameen Ibrahim