
最近因为一个面试笔试题检测,写了一个H5小游戏,总结了一下关于两个物体的检测,如何判断两个物体的碰撞检测,希望能在业务上能带来一点帮助和思考
我用以下示例图解释了两个物体产生碰撞的条件

我们用一段伪代码来解释下这个过程
// 代表圆形物体
class FallingObject {
constructor(canvas){
...
this.x1 = canvas.width;
this.y1 = 0;
this.size = 10;
}
// 检测碰撞,character代表下面的一个物体
checkCollision(character) {
const bool1 = this.x1 < character.x2 + character.width;
const bool2 = this.x1 + this.size > character.x2;
const bool3 = this.y1 < character.y2 + character.height;
const bool4 = this.y1+this.size > character.y2 + character.height;
const is_collision = bool1 && bool2 && bool3 && bool4;
if (is_collision) {
return true
}
return false;
}
}
checkCollision就是检测两个物体产生碰撞的方法,如果产生了碰撞就返回true,否则返回false,这是一段非常简单的判断碰撞逻辑,基本可以这个万能公式解决碰撞问题
我们主要是使用了一个FallingObject的类,这个类代表了随机生成的障碍物
class FallingObject {
private canvas: any;
private ctx: any;
private x: number;
private y: number;
private size: number;
private speed: number;
private blockImage: any;
constructor(canvas: any) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
// 初始位置
this.x = Math.random() * canvas.width;
this.y = 0;
this.size = Math.random() * 30 + 20; // 随机物体大小
this.speed = Math.random() * 3 + 2; // 随机物体下落速度
this.blockImage = new Image();
this.blockImage.src = "/meme/SHIKOKU.png";
this.blockImage.onload = () => {
this.draw(); // 只有在图片加载完成后调用绘制
};
}
首先掉落物体这个类的基本属性就有
初始化位置,随机大小,以及下落的速度,以及canvas的上下文环境我们再看下如何下落,首先是从初识位置下落,不断更新Y轴纵向的距离
...
// 更新Y掉落的位置
update() {
this.y += this.speed;
}
//绘制掉落的物体
draw() {
this.ctx.drawImage(this.blockImage, this.x, this.y, this.size, this.size);
}
// 检测碰撞
checkCollision(character: {
x: number;
width: any;
y: number;
height: any;
}) {
if (
this.x < character.x + character.width &&
this.x + this.size > character.x &&
this.y < character.y + character.height &&
this.y + this.size > character.y
) {
return true; // 发生碰撞
}
return false;
}
由于我们需要一个角色躲避障碍物,因此我们得生成一个角色
// 角色类
export class Character {
private canvas: any;
private ctx: any;
private x: number;
private y: number;
private width: number;
private height: number;
private speed: number;
private velocityY: number;
private gravity: number;
lives: number;
private image: HTMLImageElement;
constructor(x: number, y: number, canvas: any) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.x = x;
this.y = y;
this.width = 50;
this.height = 50;
this.speed = 5;
this.velocityY = 0; // 重力的速度
this.gravity = 0.5; // 重力大小
this.lives = 3; // 生命数量
this.image = new Image();
this.image.src = "/role.png";
this.image.onload = () => {
this.draw(this.ctx);
};
}
...
}
这个角色初始化的属性主要有
自身的大小,初始位置,以及生命次数,还有重力速度以及大小角色主要是躲避,因此需更新x方向的位置即可
...
move(left: boolean, right: boolean) {
// 左移动
if (left) {
this.x -= this.speed;
// 确保角色不会移出画布
if (this.x < 0) this.x = 0;
if (this.x > this.canvas.width - 50) this.x = this.canvas.width - 50;
return;
};
// 右移动
if (right) {
this.x += this.speed;
// 确保角色不会移出画布
if (this.x < 0) this.x = 0;
if (this.x > this.canvas.width - 50) this.x = this.canvas.width - 50;
return;
};
}
角色还可以跳跃,左右移动
...
jump() {
if (this.y === this.canvas.height - this.height) {
// 确保只有在地面上时可以跳跃
this.velocityY = -15;
}
}
// y轴更新位置
update() {
this.y += this.velocityY;
if (this.y < this.canvas.height - this.height) {
this.velocityY += this.gravity;
} else {
this.y = this.canvas.height - this.height; // 防止角色掉出画面
this.velocityY = 0;
}
}
// 绘制角色
draw(ctx: any) {
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
在浏览器驱动页面动画,有三种方式,一种是css3的动画帧@keyframes,一种是定时器,还有一种是requestAnimationFrame,我们使用requestAnimationFrame递归调用,驱动动画
核心代码大概思路就是:
requestAnimationFrame递归调用自己,驱动动画
...
const is_pc_media = useMediaQuery("(min-width:1025px)");
const character_obj = useRef<any>(null);
// 按键状态
const [left, setLeft] = useState(false);
const [right, setRight] = useState(false);
useEffect(() => {
const canvas: any = canvasDom.current;
const ctx = canvas.getContext("2d");
canvas.width = is_pc_media ? 800 : window.innerWidth;
canvas.height = 600;
let character = new Character(
canvas.width / 2 - 25,
canvas.height - 50,
canvas
);
character_obj.current = character;
let fallingObjects: any = [];
let score = 0;
let gameOver = false;
let paused = false;
const gameInit = () => {
if (gameOver) {
console.log("game over");
message.alert({
type: "warning",
msg: "game over, please reset again!",
});
return;
}
if (!paused) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 根据按键状态控制角色移动
character.move(left, right);
character.update();
character.draw(ctx);
// 随机添加和更新掉落物体
if (Math.random() < 0.05) {
fallingObjects.push(new FallingObject(canvas, symbolData));
}
fallingObjects.forEach((object: any, index: any) => {
object.update();
object.draw(ctx);
// 检测每一个物体的碰撞
if (object.checkCollision(character)) {
character.lives -= 1;
setLifeTime(character.lives);
fallingObjects.splice(index, 1);
if (character.lives <= 0) {
gameOver = true;
}
}
});
score += 1;
setSource(score);
requestAnimationFrame(gameInit);
};
// 开始游戏
startGame.current = () => {
character = new Character(
canvas.width / 2 - 25,
canvas.height - 50,
canvas
);
fallingObjects = [];
score = 0;
gameOver = false;
setLeft(false);
setRight(false);
paused = true;
gameInit();
};
// 暂停/开始游戏
plauseGame.current = () => {
paused = !paused;
if (paused) {
setStartText("pause");
gameInit();
} else {
setStartText("start");
stopGameStart();
}
};
// 监听键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") {
setLeft(true); // 持续按下左键时,角色会向左移动
}
if (e.key === "ArrowRight") {
setRight(true); // 持续按下右键时,角色会向右移动
}
if ([" ", "ArrowUp"].includes(e.key)) character.jump();
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === "ArrowLeft") {
setLeft(false); // 松开左键时,停止向左移动
}
if (e.key === "ArrowRight") {
setRight(false); // 松开右键时,停止向右移动
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, [is_pc_media, left, right]);
核心代码基本差不多了,具体预览地址预览地址[1],源码[2]
后续再拓展新增一些好玩的功能了