美文网首页编程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