引言:学习了一段时间 Web 前端后,就想写个项目练练手,后来就想到了这个 Ping-Pong 游戏,因为记得以前看有关电子游戏的纪录片时里面说到这个游戏是世界上第一款电子游戏,加上觉得项目难易度挺合适的,就撸起袖子加油干。游戏支持单人、双人玩家,所以欢迎各位无聊时拿来消消遣,搞搞基甚至撩撩妹,顺便找找 bug (逃)......
项目效果
在线游戏:PingPongGame(http://barryliu1995.studio/PingPongGame/)
GitHub 仓库:BarryLiu1995/PingPongGame
项目详情请查阅 README 文件,也欢迎各位 star,fork!
项目情况
本项目使用 JavaScript 在 Canvas 作画,同时使用 window.requestAnimationFrame()
方法告诉浏览器逐帧更新画面,以形成动画效果。这是这个项目的基本原理。而使用 JavaScript 更新 canvas 上的内容就是该项目的重点难点。scripts 目录下的 game.js 是单人游戏的业务逻辑代码,double-game.js 是依赖于 game.js 的双人游戏业务逻辑代码。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="styles/index.css">
<meta charset="UTF-8">
<title>Ping-Pong</title>
</head>
<body>
<canvas id="canvas"></canvas>
<audio preload="true" id="collide">
<source src="sound/PingPong.mp3" />
<source src="sound/PingPong.ogg" />
</audio>
<script src="scripts/game.js"></script>
<script src="scripts/double-game.js"></script>
</body>
</html>
game.js
// RequestAnimationFrame(): a browser API for getting smooth animations
requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
return window.setTimeout(callback, 1000 / 60);
};
})();
cancelRequestAnimFrame = (function () {
return window.cancelAnimationFrame ||
window.webkitCancelRequestAnimationFrame ||
window.mozCancelRequestAnimationFrame ||
window.oCancelRequestAnimationFrame ||
window.msCancelRequestAnimationFrame ||
clearTimeout
})();
// Initialize canvas and required variables
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"), // Create canvas context
W = window.innerWidth, // Window's width
H = window.innerHeight, // Window's height
particles = [], // Array containing particles
ball = {}, // Ball object
paddles = [2], // Array containing two paddles
mouse = {}, // Mouse object to store it's current position
points = 0, // variable to store points
particlesCount = 20, // Number of sparks when ball strikes the paddle
flag = 0, // Flag variable which is changed on collision
particlePos = {}, // Object to contain the position of collision
multiplier = 0, // variable to control the direction of sparks
startBtn = {}, // Start button object
restartBtn = {}, // Restart button object
over = 0, // flag variable, changed when the game is over
init, // variable to initialize animation
paddleHit, // variable about which paddle was hit
gameMode = 0; // variable about how many gamer are playing
// Add mousemove and mousedown events to the canvas
canvas.addEventListener("mousemove", trackPosition, true);
canvas.addEventListener("mousedown", btnClick, true);
// Initialise the collision sound
collision = document.getElementById("collide");
// Set the canvas's height and width to full screen
canvas.width = W;
canvas.height = H;
// Function to paint canvas
function paintCanvas() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, W, H);
}
// Function for creating paddles
function Paddle(pos) {
this.name = pos;
this.vx = 16;
// Height and width
this.h = 8;
this.w = 150;
// Paddle's position
this.x = W / 2 - this.w / 2;
this.y = (this.name == "top") ? 0 : H - this.h;
}
// Push two new paddles into the paddles[] array
paddles.push(new Paddle("bottom"));
paddles.push(new Paddle("top"));
// Ball object
ball = {
x: 20,
y: 20,
r: 9,
c: "white",
vx: 4,
vy: 8,
// Function for drawing ball on canvas
draw: function () {
ctx.beginPath();
ctx.fillStyle = this.c;
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
ctx.fill();
}
};
// Start Button object
startBtn = {
w: 125,
h: 50,
x: W / 2,
y: H / 2 - 25,
draw: function () {
ctx.strokeStyle = "white";
ctx.lineWidth = "2";
ctx.strokeRect(this.x - 150, this.y, this.w, this.h); // single player game start button
ctx.strokeRect(this.x + 25, this.y, this.w, this.h); // double player game start button
ctx.font = "18px Arial, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStlye = "white";
ctx.fillText("Single Player", W / 2 - 87.5, H / 2);
ctx.fillText("Double Player", W / 2 + 87.5, H / 2);
}
};
// Restart Button object
restartBtn = {
w: 125,
h: 50,
x: W / 2,
y: H / 2 - 25,
draw: function () {
ctx.strokeStyle = "white";
ctx.lineWidth = "2";
ctx.strokeRect(this.x - 150, this.y, this.w, this.h); // single player game restart button
ctx.strokeRect(this.x + 25, this.y, this.w, this.h); // double player game restart button
ctx.font = "18px Arial, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStlye = "white";
ctx.fillText("Single Player", W / 2 - 87.5, H / 2);
ctx.fillText("Double Player", W / 2 + 87.5, H / 2);
}
};
// Draw everything on canvas
function draw() {
paintCanvas();
//draw Paddles on canvas
for (var i = 1; i < paddles.length; i++) {
p = paddles[i];
ctx.fillStyle = "white";
ctx.fillRect(p.x, p.y, p.w, p.h);
}
ball.draw();
update();
}
// Function to update positions, score and everything.
// Basically, the main game logic is defined here
function update() {
// Update scores
updateScore();
// Move the paddles on mouse move
if (mouse.x && mouse.y) {
for (var i = 1; i < paddles.length; i++) {
p = paddles[i];
p.x = mouse.x - p.w / 2;
}
}
// Move the ball
ball.x += ball.vx;
ball.y += ball.vy;
// Collision with paddles
p1 = paddles[1];
p2 = paddles[2];
// If the ball strikes with paddles,
// invert the y-velocity vector of ball,
// increment the points, play the collision sound,
// save collision's position so that sparks can be
// emitted from that position, set the flag variable,
// and change the multiplier
if (collides(ball, p1)) {
collideAction(ball, p1);
}
else if (collides(ball, p2)) {
collideAction(ball, p2);
}
else {
// Collide with walls, If the ball hits the top/bottom walls, run gameOver() function
if (ball.y + ball.r > H) {
ball.y = H - ball.r;
gameOver();
}
else if (ball.y < 0) {
ball.y = ball.r;
gameOver();
}
// If ball strikes the vertical walls, invert the
// x-velocity vector of ball
if (ball.x + ball.r >= W) {
ball.vx = -ball.vx;
ball.x = W - ball.r;
}
else if (ball.x - ball.r < 0) {
ball.vx = -ball.vx;
ball.x = 0 + ball.r;
}
}
// If flag is set, push the particles
if (flag == 1) {
for (var k = 0; k < particlesCount; k++) {
particles.push(new Particles(particlePos.x, particlePos.y, multiplier));
}
}
// Emit particles/sparks
emitParticles();
// reset flag
flag = 0;
}
// Function for creating particles object
function Particles(x, y, m) {
this.x = x;
this.y = y;
this.radius = 1.2;
this.vx = -1.5 + Math.random() * 3;
this.vy = m * Math.random() * 1.5;
}
// Function for updating score
function updateScore() {
console.log("ball.vx: " + ball.vx);
console.log("ball.vy: " + ball.vy);
console.log("points: " + points);
ctx.fillStlye = "white";
ctx.font = "16px Arial, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText("Score: " + points, 20, 40);
}
// Function for emitting particles
function emitParticles() {
for (var j = 0; j < particles.length; j++) {
var par = particles[j];
ctx.beginPath();
ctx.fillStyle = "white";
if (par.radius > 0) {
ctx.arc(par.x, par.y, par.radius, 0, Math.PI * 2, false);
}
ctx.fill();
par.x += par.vx;
par.y += par.vy;
// Reduce radius so that the particles die after a few seconds
par.radius = Math.max(par.radius - 0.05, 0.0);
}
}
//Function to check collision between ball and one of
//the paddles
function collides(b, p) {
if (b.x >= p.x && b.x <= p.x + p.w) {
if (b.y >= (p.y - ball.r) && p.y > 0) {
paddleHit = 1;
return true;
}
else if (b.y <= p.h + ball.r && p.y == 0) {
paddleHit = 2;
return true;
}
else return false;
}
}
//Do this when collides == true
function collideAction(ball, p) {
ball.vy = -ball.vy;
if (paddleHit == 1) {
ball.y = p.y - ball.r;
particlePos.y = ball.y + ball.r;
multiplier = -1;
}
else if (paddleHit == 2) {
ball.y = p.h + ball.r;
particlePos.y = ball.y - ball.r;
multiplier = 1;
}
// This variable relates to the increase in the speed of the ball,
// so no matter how many player have will calculate this variable
points++;
// When there are two players,
// will be based on the game to calculate their respective scores
if (gameMode === 2) {
if (paddleHit === 1) {
bottomScore++;
} else if (paddleHit === 2) {
topScore++;
}
}
increaseSpd();
// Collision sound will be made
if (collision) {
if (points > 0)
collision.pause();
collision.currentTime = 0;
collision.play();
}
particlePos.x = ball.x;
flag = 1;
}
// Function to increase speed after every 5 points
function increaseSpd() {
if ((points + 1) % 5 == 0) {
if (Math.abs(ball.vx) < 15) {
ball.vx += (ball.vx < 0) ? -1 : 1;
ball.vy += (ball.vy < 0) ? -2 : 2;
}
}
}
// Track the position of mouse cursor
function trackPosition(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
}
// Function to run when the game overs
function gameOver() {
ctx.fillStlye = "white";
ctx.font = "20px Arial, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// According to the number of different players show the current score
if (gameMode === 1) {
ctx.fillText("Game Over - You scored " + points + " points!", W / 2, H / 2 + 50);
} else if (gameMode === 2) {
if (topScore > bottomScore) {
ctx.fillText("Player 1 Win!!! - You scored " + topScore + " points!", W / 2, H / 2 + 50);
} else if (topScore < bottomScore) {
ctx.fillText("Player 2 Win!!! - You scored " + bottomScore + " points!", W / 2, H / 2 + 50);
} else {
ctx.fillText("Both are Winner!!! - You scored " + topScore + " points!", W / 2, H / 2 + 50);
}
}
ctx.fillStlye = "white";
ctx.font = "35px Arial, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Restart", W / 2, H / 2 - 100);
// Stop the Animation
cancelRequestAnimFrame(init);
// Set the over flag
over = 1;
// Show the restart button
restartBtn.draw();
reset();
}
// Function for running the whole animation
function animloop() {
init = requestAnimFrame(animloop);
if (gameMode === 1) {
draw();
} else if (gameMode === 2) {
paint();
}
}
// On button click (Restart and start)
function btnClick(e) {
// Variables for storing mouse position on click
var mx = e.pageX,
my = e.pageY;
// Click Single Player start button
if (mx >= startBtn.x - 150 && mx <= startBtn.x - 25 &&
my >= startBtn.y && my <= startBtn.y + startBtn.h) {
gameMode = 1;
animloop();
}
// Click Double Player start button
if (mx >= startBtn.x + 25 && mx <= startBtn.x + 150 &&
my >= startBtn.y && my <= startBtn.y + startBtn.h) {
gameMode = 2;
animloop();
}
// If the game is over, and the restart button is clicked
if (over == 1) {
// Click Single Player restart button
if (mx >= restartBtn.x - 150 && mx <= restartBtn.x - 25 &&
my >= restartBtn.y && my <= restartBtn.y + restartBtn.h) {
gameMode = 1;
animloop();
}
// Click Double Player restart button
if (mx >= restartBtn.x + 25 && mx <= restartBtn.x + 150 &&
my >= restartBtn.y && my <= restartBtn.y + restartBtn.h) {
gameMode = 2;
animloop();
}
}
}
// Show the start screen
startScreen();
// Function to execute at startup
function startScreen() {
draw();
startBtn.draw();
}
// Reset the variable when the game is over
function reset() {
ball.x = 20;
ball.y = 20;
points = 0;
over = 0;
ball.vx = 4;
ball.vy = 8;
topScore = 0;
bottomScore = 0;
topLeft = false;
topRight = false;
bottomLeft = false;
bottomRight = false;
paddles[1].x = W / 2 - paddles[1].w / 2;
paddles[2].x = W / 2 - paddles[2].w / 2;
}
此处主要内容就是根据一定逻辑更新球的运动轨迹,根据事件处理更新挡板的位置,还有碰撞发生后的一系列处理逻辑。大家可以根据注释阅读理解此处代码
double-game.js
var topScore = 0, // variable to record Player1's score
bottomScore = 0, // variable to record Player2's score
keyNum, // variable to get keyCode
topLeft = false, // variable to record whether the corresponding button is pressed
topRight = false, // variable to record whether the corresponding button is pressed
bottomLeft = false, // variable to record whether the corresponding button is pressed
bottomRight = false; // variable to record whether the corresponding button is pressed
// Set the variable when the corresponding button is pressed
window.document.onkeydown = function (ev) {
var event = ev || window.event;
keyNum = event.keyCode;
if (keyNum === 65) {
topLeft = true;
} else if (keyNum === 68) {
topRight = true;
} else if (keyNum === 37) {
bottomLeft = true;
} else if (keyNum === 39) {
bottomRight = true;
}
};
// Set the variable when the corresponding button to bounce up
window.document.onkeyup = function (ev) {
var event = ev || window.event;
keyNum = event.keyCode;
if (keyNum === 65) {
topLeft = false;
} else if (keyNum === 68) {
topRight = false;
} else if (keyNum === 37) {
bottomLeft = false;
} else if (keyNum === 39) {
bottomRight = false;
}
};
function paint() {
paintCanvas();
// Draw the top paddle
ctx.fillStyle = "#ff4949";
ctx.fillRect(paddles[2].x, paddles[2].y, paddles[2].w, paddles[2].h);
// Draw the bottom paddle
ctx.fillStyle = "white";
ctx.fillRect(paddles[1].x, paddles[1].y, paddles[1].w, paddles[1].h);
ball.draw();
Update();
}
function Update() {
// Update the score
updateGrade();
// Use the relevant variables to record whether
// or not the two keys on the keyboard are pressed
if (topLeft) {
if (paddles[2].x >= -16) {
paddles[2].x -= paddles[2].vx;
}
}
if (topRight) {
if (paddles[2].x <= W - paddles[2].w + 16) {
paddles[2].x += paddles[2].vx;
}
}
if (bottomLeft) {
if (paddles[1].x >= -16) {
paddles[1].x -= paddles[1].vx;
}
}
if (bottomRight) {
if (paddles[1].x <= W - paddles[1].w + 16) {
paddles[1].x += paddles[1].vx;
}
}
ball.x += ball.vx;
ball.y += ball.vy;
// Collision with paddles
pa1 = paddles[1];
pa2 = paddles[2];
if (collides(ball, pa1)) {
collideAction(ball, pa1);
} else if (collides(ball, pa2)) {
collideAction(ball, pa2);
} else {
// Collide with walls, If the ball hits the top/bottom walls, run gameOver() function
if (ball.y + ball.r > H) {
ball.y = H - ball.r;
gameOver();
}
else if (ball.y < 0) {
ball.y = ball.r;
gameOver();
}
// If ball strikes the vertical walls, invert the
// x-velocity vector of ball
if (ball.x + ball.r >= W) {
ball.vx = -ball.vx;
ball.x = W - ball.r;
}
else if (ball.x - ball.r < 0) {
ball.vx = -ball.vx;
ball.x = 0 + ball.r;
}
}
if (flag == 1) {
for (var k = 0; k < particlesCount; k++) {
particles.push(new Particles(particlePos.x, particlePos.y, multiplier));
}
}
emitParticles();
flag = 0;
}
function updateGrade() {
ctx.fillStyle = "#ff4949";
ctx.font = "16px Arial, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillText("Player1 Score: " + topScore, 20, 40);
ctx.fillStyle = "white";
ctx.textBaseline = "bottom";
ctx.fillText("Player2 Score: " + bottomScore, 20, H - 40);
}
依赖于 game.js 的双人游戏业务逻辑代码,阅读完 game.js 后便可易于理解此处代码
index.css
* {
padding: 0;
margin: 0;
overflow: hidden;
}
参考
最后是广告时间,我的原创博文将同步更新在三大平台上,欢迎大家点击阅读!谢谢
网友评论