Pong mit SVG.js

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Jeder liebt das klassische Spiel Pong, oder? Wir auf jeden Fall. Was macht mehr Spaß? Es selbst zu bauen!

Deshalb haben wir beschlossen, eines mit SVG.js zu erstellen – um einige Aspekte unserer Bibliothek hervorzuheben. Es mag wie eine komplexe Idee für ein kleines Tutorial erscheinen, aber wie Sie sehen werden, ist es einfacher als es klingt. Tauchen wir ein!

Hier ist das fertige Produkt

Siehe den Pen Voll funktionsfähiges Pong-Spiel mit Effekten von Wout Fierens (@wout) auf CodePen.

Erste Schritte

SVG.js ist über Github, npm, bower oder CDN.js verfügbar. Es gibt viele Optionen, um SVG.js in die Hände zu bekommen, also verwenden Sie, was Ihnen am bequemsten ist.

Beginnen Sie mit der Erstellung eines neuen HTML-Dokuments und binden Sie die Bibliothek ein. Erstellen Sie ein leeres <div> als Wrapper für das SVG-Dokument und geben Sie ihm ein id-Attribut. Etwas wie pong sollte für dieses Projekt geeignet sein

<div id="pong"></div>

Initialisieren Sie als Nächstes die SVG.js-Instanz, indem Sie auf den Wrapper verweisen. Zu diesem Zeitpunkt ist es auch eine gute Idee, eine width und height für das Spiel zu definieren, was die spätere Änderung erleichtert.

// define width and height
var width = 450, height = 300

// create SVG document and set its size
var draw = SVG('pong').size(width, height)

Jetzt sind Sie bereit, mit dem Aufbau des Spiels zu beginnen.

Zeichnen von Spielelementen

Der Hintergrund

Der Hintergrund sollte das gesamte Dokument abdecken, daher verwenden wir ein <rect> und geben ihm eine neutrale graue Farbe. Zuerst zeichnen wir den linken Spieler in Grün. Dann zeichnen wir den rechten, indem wir den linken klonen und ihn rosa einfärben.

// draw background
var background = draw.rect(width, height).fill('#E3E8E6')

Wir benötigen auch eine vertikale, gestrichelte Linie in der Mitte, um die Spielfelder zu unterscheiden.

// draw line
var line = draw.line(width/2, 0, width/2, height)
line.stroke({ width: 5, color: '#fff', dasharray: '5,5' })

Siehe den Pen Pong Hintergrund von Wout Fierens (@wout) auf CodePen.

Paddles und der Ball

Pong wäre nicht Pong ohne Paddles und einen Ball. Zuerst zeichnen wir den linken Spieler in Grün. Dann zeichnen wir den rechten, indem wir den linken klonen und ihn rosa einfärben.

var paddleWidth = 20, paddleHeight = 100

// create and position left paddle
var paddleLeft = draw.rect(paddleWidth, paddleHeight)
paddleLeft.x(0).cy(height/2).fill('#00ff99')

// create and position right paddle
var paddleRight = paddleLeft.clone()
paddleRight.x(width-paddleWidth).fill('#ff0066')

Für den Ball werden wir einen Kreis mit einem Durchmesser von 20 verwenden und ihn in der Mitte des Spielfelds platzieren

// define ball size
var ballSize = 20

// create ball
var ball = draw.circle(ballSize)
ball.center(width/2, height/2).fill('#7f7f7f')

Siehe den Pen Pong Paddles und Ball von Wout Fierens (@wout) auf CodePen.

Anzeigetafel

Schließlich benötigen wir eine Anzeigetafel, die wir oben auf dem Spielfeld hinzufügen werden.

// define initial player score
var playerLeft = playerRight = 0

// create text for the score, set font properties
var scoreLeft = draw.text(playerLeft+'').font({
  size: 32,
  family: 'Menlo, sans-serif',
  anchor: 'end',
  fill: '#fff'
}).move(width/2-10, 10)

// cloning rocks!
var scoreRight = scoreLeft.clone()
  .text(playerRight+'')
  .font('anchor', 'start')
  .x(width/2+10)

Das war's! Jetzt haben wir alle Spielelemente, kommen wir zur Spiellogik.

Siehe den Pen Pong Anzeigetafel von Wout Fierens (@wout) auf CodePen.

Spiellogik

Wir beginnen mit dem Schreiben einer Update-Funktion, die den Zustand unseres Spiels und der Spielelemente aktualisiert.

// random velocity for the ball at start
var vx = Math.random() * 500 - 250
  , vy = Math.random() * 500 - 250

// update is called on every animation step
function update(dt) {
  // move the ball by its velocity
  ball.dmove(vx*dt, vy*dt)

  // get position of ball
  var cx = ball.cx()
    , cy = ball.cy()

  // check if we hit top/bottom borders
  if ((vy < 0 && cy <= 0) || (vy > 0 && cy >= height)) {
    vy = -vy
  }

  // check if we hit left/right borders
  if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
    vx = -vx
  }
}

Wenn wir dies ausführen, passiert nichts, da wir die update-Funktion noch nicht aufgerufen haben. Dies geschieht mithilfe der nativen requestAnimationFrame-Funktion von JavaScript, die uns flüssige Animationen ermöglicht. Damit dies funktioniert, wird ein Handler registriert, der unsere update-Funktion periodisch aufruft

var lastTime, animFrame;

function callback(ms) {
  // we get passed a timestamp in milliseconds
  // we use it to determine how much time has passed since the last call

  if (lastTime) {
    update((ms-lastTime)/1000) // call update and pass delta time in seconds
  }

  lastTime = ms
  animFrame = requestAnimationFrame(callback)
}

callback()

Juhu! Der Ball springt herum! Aber unsere Paddles sind im Moment noch ziemlich nutzlos. Machen wir also etwas dagegen und fügen eine Kollisionserkennung für die Paddles hinzu. Wir brauchen sie nur auf der x-Achse

var paddleLeftY = paddleLeft.y()
  , paddleRightY = paddleRight.y()

// check if we hit the paddle
if ((vx < 0 && cx <= paddleWidth && cy > paddleLeftY && cy < paddleLeftY + paddleHeight) ||
   (vx > 0 && cx >= width - paddleWidth && cy > paddleRightY && cy < paddleRightY + paddleHeight)) {
  // depending on where the ball hit we adjust y velocity
  // for more realistic control we would need a bit more math here
  // just keep it simple
  vy = (cy - ((vx < 0 ? paddleLeftY : paddleRightY) + paddleHeight/2)) * 7 // magic factor

  // make the ball faster on hit
  vx = -vx * 1.05
} else ...

Besser, jetzt ist der Ball sich der Paddles bewusst. Aber ein paar andere Dinge fehlen noch

  • Der Punktestand wird nicht aktualisiert, wenn die Bande getroffen wird
  • Die Paddles bewegen sich nicht
  • Der Ball sollte zurückgesetzt werden, wenn ein Punkt erzielt wurde

Arbeiten wir diese Liste von oben nach unten ab.

Siehe den Pen Pong springender Ball von Wout Fierens (@wout) auf CodePen.

Aktualisiere den Punktestand

Um unseren Punktestand zu aktualisieren, müssen wir die Kollisionserkennung an die linke oder rechte Bande koppeln

// check if we hit left/right borders
if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
  // when x-velocity is negative, its a point for player 2, else player 1
  if (vx < 0) { ++playerRight }
  else { ++playerLeft }

  vx = -vx

  scoreLeft.text(playerLeft + '')
  scoreRight.text(playerRight + '')
}

Siehe den Pen Pong springender Ball von Wout Fierens (@wout) auf CodePen.

Bewegen des vom Benutzer gesteuerten Paddles

Das rechte Paddle wird über die Tastatur gesteuert, und das ist mit SVG.js ein Kinderspiel

// define paddle direction and speed
var paddleDirection = 0  // -1 is up, 1 is down, 0 is still
  , paddleSpeed = 5      // pixels per frame refresh

// detect if up and down arrows are prssed to change direction
SVG.on(document, 'keydown', function(e) {
  paddleDirection = e.keyCode == 40 ? 1 : e.keyCode == 38 ? -1 : 0
});

// make sure the direction is reset when the key is released
SVG.on(document, 'keyup', function(e) {
  paddleDirection = 0
})

Was machen wir hier? Zuerst rufen wir SVG.on auf, was es uns erlaubt, einen Event-Listener an jeden Knoten zu binden (nicht nur an SVG.js-Objekte). Wir lauschen auf das keydown-Ereignis, um zu erkennen, ob die up-Taste (38) oder die down-Taste (40) gedrückt wird. Wenn das der Fall ist, wird paddleDirection entsprechend auf -1 oder 1 gesetzt. Wenn eine andere Taste gedrückt wird, ist paddleDirection 0. Schließlich wird bei jedem Tastendruck die paddleDirection auf 0 zurückgesetzt.

Die update-Funktion übernimmt die eigentliche Arbeit des Bewegens des Paddles, basierend auf der Benutzereingabe. Wir fügen also folgenden Code zur update-Funktion hinzu

// move player paddle
var playerPaddleY = paddleRight.y();

if (playerPaddleY <= 0 && paddleDirection == -1) {
  paddleRight.cy(paddleHeight / 2)
} else if (playerPaddleY >= height-paddleHeight && paddleDirection == 1) {
  paddleRight.y(height - paddleHeight)
} else {
  paddleRight.dy(paddleDirection * paddleSpeed)
}

Wir verhindern, dass das Paddle das Spielfeld verlässt, indem wir seine y-Position testen. Andernfalls wird das Paddle mit einer relativen Distanz über dy() bewegt.

Siehe den Pen Pong vom Benutzer gesteuertes Paddle von Wout Fierens (@wout) auf CodePen.

Bewegen des KI-Paddles

Ein guter Gegner macht das Spiel lohnenswert. Deshalb wird der KI-Spieler dem Ball folgen, mit einem vordefinierten Schwierigkeitsgrad. Je höher der Schwierigkeitsgrad, desto schneller reagiert das KI-Paddle.

Definieren Sie zuerst den Schwierigkeitsgrad, der die Geschwindigkeit der KI bestimmt

var difficulty = 2

Fügen Sie dann den folgenden Code zur update-Funktion hinzu

// get position of ball and paddle
var paddleRightCy = paddleRight.cy()

// move the left paddle in the direction of the ball
var dy = Math.min(difficulty, Math.abs(cy - paddleRightCy))
paddleRightCy += cy > paddleRightCy ? dy : -dy

// constraint the move to the canvas area
paddleRight.cy(Math.max(paddleHeight/2, Math.min(height-paddleHeight/2, paddleRightCy)))

Siehe den Pen Pong vom Benutzer gesteuertes Paddle von Wout Fierens (@wout) auf CodePen.

Punkte!

Moment mal, das stimmt nicht! Das Spiel geht weiter, auch nachdem einer der Spieler den Ball verfehlt hat. Zeit, eine reset-Funktion einzubauen, um alle Spielelemente mithilfe von Animationen in ihre Ausgangsposition zu bewegen

function reset() {
  // reset speed values
  vx = 0
  vy = 0

  // position the ball back in the middle
  ball.animate(100).center(width / 2, height / 2)

  // reset the position of the paddles
  paddleLeft.animate(100).cy(height / 2)
  paddleRight.animate(100).cy(height / 2)
}

Die reset-Funktion sollte aufgerufen werden, wenn einer der Spieler den Ball verfehlt. Um das zu erreichen, ändern Sie die Fehlererkennung, indem Sie die Zeile vx = -vx entfernen und den Aufruf von reset() hinzufügen

// check if a player missed the ball
if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
  // when x-velocity is negative, its a point for player 2, else player 1
  if (vx < 0) {
    ++playerRight
  } else {
    ++playerLeft
  }

  // update score
  scoreLeft.text(playerLeft)
  scoreRight.text(playerLeft)

  reset()
}

Wir müssen auch sicherstellen, dass die anfänglichen Werte für vx und vy auf 0 gesetzt sind. So beginnt das Spiel nicht ohne unsere Eingabe. Um den ersten Aufschlag anzeigen zu können, fügen wir einen click-Listener zum SVG-Dokument hinzu

draw.on('click', function() {
  if (vx === 0 && vy === 0) {
    vx = Math.random() * 500 - 250
    vy = Math.random() * 500 - 250
  }
})

Siehe den Pen Pong mit Start und Reset von Wout Fierens (@wout) auf CodePen.

Zugabe

Natürlich gibt es noch viel zu verbessern am Spiel, aber der Zweck dieses Tutorials ist es, über SVG und insbesondere über SVG.js zu lernen. Wir möchten Ihnen einige visuelle Effekte hinterlassen, um das Spiel aufzupeppen.

Ballfarbe

Es wäre schön, wenn sich die Farbe des Balls beim Annähern des gegnerischen Gegners ändern würde. Dies geschieht durch die Nutzung der Kraft der morph-Methode der Klasse SVG.Color. Wir werden die Position des Balls erkennen und schrittweise die Farbe des gegnerischen Gegners zuweisen, basierend auf der Position des Balls auf der x-Achse.

Wir beginnen mit der Initialisierung einer neuen Instanz von SVG.Color

var ballColor = new SVG.Color('#ff0066')

Als Nächstes definieren wir die Zielfarbe, indem wir die Methode morph() aufrufen

ballColor.morph('#00ff99')

Dies setzt eine Startfarbe, die #ff0066 ist, und eine Endfarbe, die #00ff99 ist. Mit der Methode at() auf SVG.Color können wir die Farbe basierend auf einer gegebenen Position zwischen 0 und 1 überblenden. Indem wir also folgenden Code zu unserer update-Funktion hinzufügen, können wir die Farbe des Balls ändern, während er sich bewegt

ball.fill(ballColor.at(1/width*ball.x()))

Das war doch gar nicht schwer, oder?

Bum!

Stellen Sie sich eine riesige Farbexplosion vor, wenn der Gegner den Ball verfehlt. Das würde das Gewinnen eines Punktes noch mehr Spaß machen. Um dies zu erreichen, verwenden wir einen radialen Verlauf. Er erscheint dort, wo der Ball die Wand getroffen hat, und verblasst dann schnell. Sobald er verblasst ist, wird das Objekt, das den Verlauf trägt, aus der Szene gelöscht. Um dies zu erreichen, fügen wir eine weitere Funktion namens boom mit der erforderlichen Logik hinzu

function boom() {
  // detect winning player
  var paddle = vx > width/2 ? paddleLeft : paddleRight

  // create the gradient
  var gradient = draw.gradient('radial', function(stop) {
    stop.at(0, paddle.attr('fill'), 1)
    stop.at(1, paddle.attr('fill'), 0)
  })

  // create circle to carry the gradient
  var blast = draw.circle(300)
  blast.center(ball.cx(), ball.cy()).fill(gradient)

  // animate to invisibility
  blast.animate(1000, '>').opacity(0).after(function() {
    blast.remove()
  })
}

Siehe den Pen Voll funktionsfähiges Pong-Spiel mit Effekten von Wout Fierens (@wout) auf CodePen.

Fazit

Das ist alles! Sie haben gerade ein funktionierendes Pong-Spiel mit SVG.js erstellt. Im nächsten Tutorial erfahren Sie, wie Sie diesen Code-Blob in ein wiederverwendbares SVG.js-Plugin umwandeln, neue Funktionen hinzufügen und das Spiel einfach konfigurieren können.


Geschrieben von Ulrich-Matthias Schäfer & Wout Fierens.