《canvas》之第16章 碰撞检测

  • 第16章 碰撞检测
  • 16.1 碰撞检测简介
  • 16.2 外接矩形判定法
  • 16.3 外接圆判定法
  • 16.4 多物体碰撞
  • 16.4.1 排列组合
  • 16.4.2 多物体碰撞


第16章 碰撞检测

16.1 碰撞检测简介

检测物体与物体间是否发生碰撞。

16.2 外接矩形判定法

将物体抽象成矩形来检测碰撞。

//外接矩形判定法(碰撞检测)
window.tools.checkRect = function (rectA, rectB) {
    return !(rectA.x + rectA.width < rectB.x ||
             rectB.x + rectB.width < rectA.x ||
             rectA.y + rectA.height < rectB.y ||
             rectB.y + rectB.height < rectA.y);
}
Ball.prototype = {
    getRect:function() {//获取包含小球的最小矩形
        var rect = {
                x: this.x - this.radius,
                y: this.y - this.radius,
                width: this.radius * 2,
                height: this.radius * 2
        }
        return rect;
    }
}
  • 外接矩形碰撞检测
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="js/tools.js"></script>
    <script src="js/ball.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");
            var msg = document.getElementById("msg");

            //定义一个位置固定的小球ballA
            var ballA = new Ball(cnv.width/2, cnv.height/2, 30);
            //获取ballA的外接矩形
            var rectA = ballA.getRect();
			
            var mouse = tools.getMouse(cnv);
            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                //绘制ballA以及它的外接矩形
                ballA.fill(cxt);
                cxt.strokeRect(rectA.x, rectA.y, rectA.width, rectA.height);

                //定义一个位置不固定的小球ballB,小球追随鼠标
                var ballB = new Ball(mouse.x, mouse.y, 30);
                //获取ballB的外接矩形
                var rectB = ballB.getRect();

                //绘制ballB以及它的外接矩形
                ballB.fill(cxt);
                cxt.strokeRect(rectB.x, rectB.y, rectB.width, rectB.height);

                //碰撞检测
                if (tools.checkRect(rectA, rectB)) {
                    msg.innerHTML = "撞上了";
                } else {
                    msg.innerHTML = "没撞上";
                }
            })();
        }
    </script>
</head>
<body>
    <canvas id="canvas" width="270" height="200" style="border:1px solid silver;"></canvas>
    <p id="msg"></p>
</body>
</html>
  • box.js
function Box(x, y, width, height, color) {
    //小球中心的x坐标,默认值为0
    this.x = x || 0;
    //小球中心的y坐标,默认值为0
    this.y = y || 0;
    //小球宽度,默认值为80
    this.width = width || 80;
    //小球高度,默认值为40
    this.height = height || 40;

    this.color = color || "red";
    //x和y速度
    this.vx = 0;
    this.vy = 0;
}
Box.prototype = {
    //绘制“描边”矩形
    stroke: function (cxt) {
        cxt.save();
        cxt.beginPath();
        cxt.rect(this.x, this.y, this.width, this.height);
        cxt.closePath();
        cxt.strokeStyle = this.color;
        cxt.stroke();
        cxt.restore();
    },
    //绘制“填充”矩形
    fill: function (cxt) {
        cxt.save();
        cxt.beginPath();
        cxt.rect(this.x, this.y, this.width, this.height);
        cxt.closePath();
        cxt.fillStyle = this.color;
        cxt.fill();
        cxt.restore();
    }
}
  • 俄罗斯方块
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="js/tools.js"></script>
    <script src="js/box.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");

            //定义一个用来存放方块的数组boxes
            var boxes = [];
            //定义一个“当前活动”的方块
            var activeBox = createBox();
            //定义方块的Y轴速度
            var vy = 2;

            //定义一个函数createBox(),用于创建一个新的方块
            function createBox() {
                var x = Math.random() * cnv.width;
                var y = 0;
                var width = Math.random() * 40 + 10;
                var height = Math.random() * 40 + 10;
                var color = tools.getRandomColor();
                var box = new Box(x, y, width, height, color);
                //添加到数组boxes中
                boxes.push(box);
                return box;
            }

            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                activeBox.y += vy;

                //边界检测,如果到达底部,则创建一个新的box
                if (activeBox.y > cnv.height - activeBox.height) {
                    activeBox.y = cnv.height - activeBox.height;
                    activeBox = createBox();
                }
                //遍历数组boxes,以便单独处理每一个box
                boxes.forEach(function (box) {
                    /*如果当前遍历的box不是“活动方块(activeBox)”,并且当前遍历的方块与“活动方块(activeBox)”碰上了,则创建新的方块*/
                    if (activeBox !== box && tools.checkRect(activeBox, box)) {
                        activeBox.y = box.y - activeBox.height;
                        activeBox = createBox();
                    }
                    box.fill(cxt);
                });
            })();

        }
    </script>
</head>
<body>
    <canvas id="canvas" width="270" height="200" style="border:1px solid silver;"></canvas>
</body>
</html>
  • 键盘控制
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="js/tools.js"></script>
    <script src="js/box.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");

            //定义一个用来存放方块的数组boxes
            var boxes = [];
            //定义一个“当前活动”的方块
            var activeBox = createBox();
            //定义方块的Y轴速度
            var vy = 1.5;

            //加入鼠标控制
            var key = tools.getKey();
            window.addEventListener("keydown", function () {
                switch (key.direction) {
                    case "down":
                        activeBox.y += 5;
                        break;
                    case "left":
                        activeBox.x -= 10;
                        break;
                    case "right":
                        activeBox.x += 10;
                        break;
                }
            }, false);

            //定义一个函数createBox(),用于创建一个新的方块
            function createBox() {
                var x = Math.random() * cnv.width;
                var y = 0;
                var width = Math.random() * 40 + 10;
                var height = Math.random() * 40 + 10;
                var color = tools.getRandomColor();
                var box = new Box(x, y, width, height, color);
                //添加到数组boxes中
                boxes.push(box);
                return box;
            }

            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                activeBox.y += vy;

                //边界检测,如果到达底部,则创建一个新的box
                if (activeBox.y > cnv.height - activeBox.height) {
                    activeBox.y = cnv.height - activeBox.height;
                    activeBox = createBox();
                }
                //遍历数组boxes,以便单独处理每一个box
                boxes.forEach(function (box) {
                    /*如果当前遍历的box不是“活动方块(activeBox)”,并且当前遍历的方块与
                    “活动方块(activeBox)”碰上了,则创建新的方块*/
                    if (activeBox !== box && tools.checkRect(activeBox, box)) {
                        activeBox.y = box.y - activeBox.height;
                        activeBox = createBox();
                    }
                    box.fill(cxt);
                });
            })();

        }
    </script>
</head>
<body>
    <canvas id="canvas" width="270" height="200" style="border:1px solid silver;"></canvas>
</body>
</html>

16.3 外接圆判定法

物体看成圆来判定是否碰撞。

//外接圆判定法(碰撞检测)
window.tools.checkCircle = function (circleB, circleA) {
    var dx = circleB.x - circleA.x;
    var dy = circleB.y - circleA.y;
    var distance = Math.sqrt(dx * dx + dy * dy);
    if (distance < (circleA.radius + circleB.radius)) {
        return true;
    } else {
        return false;
    }
}
  • 外接圆判断
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="js/tools.js"></script>
    <script src="js/ball.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");
            var txt = document.getElementById("txt");

            //定义一个位置固定的小球
            var ballA = new Ball(cnv.width / 2, cnv.height / 2, 20, "#FF6699");
            var mouse = tools.getMouse(cnv);

            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                //定义一个位置不固定的小球,小球追随鼠标
                var ballB = new Ball(mouse.x, mouse.y, 20, "#66CCFF");

                //碰撞检测
                if (tools.checkCircle(ballB, ballA)) {
                    txt.innerHTML = "撞上了";
                } else {
                    txt.innerHTML = "没撞上";
                }

                ballA.fill(cxt);
                ballB.fill(cxt);
            })();
        }
    </script>
</head>
<body>
    <canvas id="canvas" width="200" height="150" style="border:1px solid silver;"></canvas>
    <p id="txt"></p>
</body>
</html>
  • 小球碰撞
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="js/tools.js"></script>
    <script src="js/ball.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");

            //定义两个小球:ballA和ballB
            var ballA = new Ball(0, cnv.height / 2, 12, "#FF6699");
            var ballB = new Ball(cnv.width, cnv.height / 2, 12, "#66CCFF");
            //定义小球X轴速度
            var vx = 2;

            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                ballA.x += vx;
                ballB.x += -vx;

                //如果发生碰撞,则速度取反
                if (tools.checkCircle(ballB, ballA)) {
                    vx = -vx;
                }

                ballA.fill(cxt);
                ballB.fill(cxt);
            })();
        }
    </script>
</head>
<body>
    <canvas id="canvas" width="270" height="200" style="border:1px solid silver;"></canvas>
</body>
</html>

16.4 多物体碰撞

16.4.1 排列组合

n个物体,n×(n-1)/2种碰撞情况。

16.4.2 多物体碰撞

balls.forEach(function(ballA, i) {
	for (var j = i + 1; j < balls.length; j++) {
		var ballB = balls[j];
		if (tools.checkCircle(ballB, ballA)) {
			//...
		}
	}
});
  • 存在小球重叠bug
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="js/tools.js"></script>
    <script src="js/ball.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");

            var n = 8;
            var balls = [];

            //生成n个小球,小球的x、y、color、vx、vy属性取的都是随机值
            for (var i = 0; i < n; i++) {
                ball = new Ball();
                ball.x = Math.random() * cnv.width;
                ball.y = Math.random() * cnv.height;
                ball.radius = 10;
                ball.color = tools.getRandomColor();
                ball.vx = Math.random() * 6 - 3;
                ball.vy = Math.random() * 6 - 3;
                //添加到数组balls中
                balls.push(ball);
            }

            //碰撞检测(小球与小球)
            function checkCollision(ballA, i) {
                for (var j = i + 1; j < balls.length; j++) {
                    var ballB = balls[j];
                    //如果两个小球碰撞,则碰撞后vx、vy都取相反值
                    if (tools.checkCircle(ballB, ballA)) {
                        ballA.vx = -ballA.vx;
                        ballA.vy = -ballA.vy;
                        ballB.vx = -ballB.vx;
                        ballB.vy = -ballB.vy;
                    }
                }
            }

            //边界检测(小球与边界)
            function checkBorder(ball) {
                //碰到左边界
                if (ball.x < ball.radius) {
                    ball.x = ball.radius;
                    ball.vx = -ball.vx;
                    //碰到右边界
                } else if (ball.x > canvas.width - ball.radius) {
                    ball.x = canvas.width - ball.radius;
                    ball.vx = -ball.vx;
                }
                //碰到上边界
                if (ball.y < ball.radius) {
                    ball.y = ball.radius;
                    ball.vy = -ball.vy;
                    //碰到下边界
                } else if (ball.y > canvas.height - ball.radius) {
                    ball.y = canvas.height - ball.radius;
                    ball.vy = -ball.vy;
                }
            }

            //绘制小球
            function drawBall(ball) {
                ball.fill(cxt);
                ball.x += ball.vx;
                ball.y += ball.vy;
            }

            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                //碰撞检测
                balls.forEach(checkCollision);
                //边界检测
                balls.forEach(checkBorder);
                //绘制小球
                balls.forEach(drawBall);

            })();
        }
    </script>
</head>
<body>
    <canvas id="canvas" width="200" height="150" style="border:1px solid silver;"></canvas>
</body>
</html>
  • 碰撞小球加上偏移量
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="js/tools.js"></script>
    <script src="js/ball.js"></script>
    <script type="text/javascript">
        function $$(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            var cnv = $$("canvas");
            var cxt = cnv.getContext("2d");

            var n = 8;
            var balls = [];

            //生成n个小球,小球的x、y、color、vx、vy属性取的都是随机值
            for (var i = 0; i < n; i++) {
                ball = new Ball();
                ball.x = Math.random() * cnv.width;
                ball.y = Math.random() * cnv.height;
                ball.radius = 10;
                ball.color = tools.getRandomColor();
                ball.vx = Math.random() * 6 - 3;
                ball.vy = Math.random() * 6 - 3;
                //添加到数组balls中
                balls.push(ball);
            }

            //碰撞检测(小球与小球)
            function checkCollision(ballA, i) {
                for (var j = i + 1; j < balls.length; j++) {
                    var ballB = balls[j];
                    //如果两个小球碰撞,则碰撞后vx、vy都取相反值
                    if (tools.checkCircle(ballB, ballA)) {
                        ballA.vx = -ballA.vx;
                        ballA.vy = -ballA.vy;
                        ballB.vx = -ballB.vx;
                        ballB.vy = -ballB.vy;

                        //每次碰撞,小球的x、y都加入偏移量,避免相互重叠
                        if (ballA.vx > 0) {
                            ballA.x += 5;
                        } else {
                            ballA.x -= 5;
                        }
                        if (ballA.vy > 0) {
                            ballA.y += 5;
                        } else {
                            ballA.y -= 5;
                        }
                        if (ballB.vx > 0) {
                            ballB.x += 5;
                        } else {
                            ballB.x -= 5;
                        }
                        if (ballB.vy > 0) {
                            ballB.y += 5;
                        } else {
                            ballB.y -= 5;
                        }
                    }
                }
            }

            //边界检测(小球与边界)
            function checkBorder(ball) {
                //碰到左边界
                if (ball.x < ball.radius) {
                    ball.x = ball.radius;
                    ball.vx = -ball.vx;
                    //碰到右边界
                } else if (ball.x > canvas.width - ball.radius) {
                    ball.x = canvas.width - ball.radius;
                    ball.vx = -ball.vx;
                }
                //碰到上边界
                if (ball.y < ball.radius) {
                    ball.y = ball.radius;
                    ball.vy = -ball.vy;
                    //碰到下边界
                } else if (ball.y > canvas.height - ball.radius) {
                    ball.y = canvas.height - ball.radius;
                    ball.vy = -ball.vy;
                }
            }

            //绘制小球
            function drawBall(ball) {
                ball.fill(cxt);
                ball.x += ball.vx;
                ball.y += ball.vy;
            }

            (function frame() {
                window.requestAnimationFrame(frame);
                cxt.clearRect(0, 0, cnv.width, cnv.height);

                //碰撞检测
                balls.forEach(checkCollision);
                //边界检测
                balls.forEach(checkBorder);
                //绘制小球
                balls.forEach(drawBall);

            })();
        }
    </script>
</head>
<body>
    <canvas id="canvas" width="200" height="150" style="border:1px solid silver;"></canvas>
</body>
</html>