美文网首页编程Web前端之路程序员
JavaScript 游戏——Ping-Pong

JavaScript 游戏——Ping-Pong

作者: BarryLiu1995 | 来源:发表于2017-08-31 13:12 被阅读133次

引言:学习了一段时间 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;
}

参考

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

最后是广告时间,我的原创博文将同步更新在三大平台上,欢迎大家点击阅读!谢谢

刘志宇的新天地

简书

稀土掘金

相关文章

网友评论

    本文标题:JavaScript 游戏——Ping-Pong

    本文链接:https://www.haomeiwen.com/subject/khrqjxtx.html