JavaScript 游戏——Ping-Pong

学习了一段时间 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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

1
2
3
4
5
* {
padding: 0;
margin: 0;
overflow: hidden;
}

参考

  1. Canvas Web API 接口|MDN
  2. window.requestAnimationFrame|MDN
  3. CSS3动画那么强,requestAnimationFrame还有毛线用?

本文作者:刘志宇

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

Donate comment here