在本教程中,我们将使用Matter.js构建一个简单的物理游戏,称为**"命悬一线"**(建议手机游玩),在文章末尾附完整代码。游戏的目标是控制一个小球,避免让连接在一起的线段掉到地面上。当线段的一部分接触到地面时,游戏结束。通过本教程,你将逐步了解如何在网页中使用Matter.js创建2D物理世界,并实现基本的用户交互。
准备工作
首先需要一个HTML文件来托管我们的游戏,并引入Matter.js库。Matter.js是一个JavaScript物理引擎,可以轻松创建真实的物理效果。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>命悬一线</title>
<style>
body {
margin: 0;
overflow: hidden; <!-- 避免滚动条出现 -->
}
canvas {
background: #f0f0f0;
display: block;
width: 100%; <!-- 适配屏幕宽度 -->
height: 100vh; <!-- 适配屏幕高度 -->
}
</style>
</head>
<body>
<!-- 引入Matter.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
<script>
初始化物理引擎
接下来,我们将创建一个物理引擎并渲染到页面中。这部分代码负责设置Matter.js引擎和渲染器。
js
// 引入 Matter.js 的关键模块
const { Engine, Render, Runner, World, Bodies, Events } = Matter;
// 创建引擎
const engine = Engine.create(); // Engine.create()初始化物理引擎
const world = engine.world; // 获取引擎的世界
// 创建渲染器,将物理世界显示在网页上
const render = Render.create({
element: document.body, // 将渲染器附加到HTML文档的body元素
engine: engine, // 绑定创建的物理引擎
options: {
width: window.innerWidth, // 设置渲染器宽度,动态适配浏览器窗口
height: window.innerHeight, // 设置渲染器高度,动态适配浏览器窗口
wireframes: false // 设置为false以禁用线框模式,使用实际的渲染
}
});
Render.run(render); // 启动渲染器
const runner = Runner.create(); // 创建物理引擎的运行循环
Runner.run(runner, engine); // 启动物理引擎的运行
添加游戏边界
为了让物体不飞出屏幕,我们需要在屏幕四周创建边界:地面、左右墙壁和天花板。这些物体是静止的(isStatic: true
),即它们不会移动。
js
// 创建地面、墙壁和天花板,确保它们是静止的
const ground = Bodies.rectangle(window.innerWidth / 2, window.innerHeight, window.innerWidth, 60, { isStatic: true });
const leftWall = Bodies.rectangle(0, window.innerHeight / 2, 60, window.innerHeight + 100, { isStatic: true });
const rightWall = Bodies.rectangle(window.innerWidth, window.innerHeight / 2, 60, window.innerHeight + 100, { isStatic: true });
const ceiling = Bodies.rectangle(window.innerWidth / 2, -40, window.innerWidth, 60, { isStatic: true });
// 将边界添加到世界中
World.add(world, [ground, leftWall, rightWall, ceiling]);
创建玩家可控制的球
我们创建一个静止的小球,玩家可以通过鼠标或触摸来控制它。球的质量很大,目的是使它具有足够的重量来压住物理链条。
js
// 创建玩家控制的小球
const followBall = Bodies.circle(window.innerWidth / 2, window.innerHeight - 300, 30, {
friction: 0, // 设置摩擦力为0,使小球容易滑动
mass: 10000, // 大质量使小球具有更大的影响力
render: {
fillStyle: 'rgba(0, 255, 255, 0.5)', // 设置小球为半透明蓝色
strokeStyle: 'white', // 边框颜色
lineWidth: 5 // 边框宽度
},
isStatic: true // 初始化为静止
});
World.add(world, followBall);
创建物理链条
这部分是游戏的核心,我们将创建一系列的小段,它们连接在一起形成链条。每个段之间使用Matter.js的Constraint
(约束)来连接。
js
const segments = []; // 存储所有线段
const segmentCount = 50; // 设置链条的段数
const segmentRadius = 2; // 每段的半径
const segmentSpacing = 4; // 段间距
// 生成链条段
for (let i = 0; i < segmentCount; i++) {
const xPosition = (window.innerWidth / 2) - (segmentCount / 2) * segmentSpacing + i * segmentSpacing;
const segment = Bodies.circle(xPosition, 100, segmentRadius, {
friction: 0,
restitution: 0.6, // 弹性设置
render: {
fillStyle: 'green' // 线段的颜色
}
});
segments.push(segment);
World.add(world, segment);
// 使用约束连接相邻的线段,形成刚性链条
if (i > 0) {
const constraint = Matter.Constraint.create({
bodyA: segments[i - 1], // 连接前一个线段
bodyB: segment, // 连接当前线段
length: segmentRadius * 2, // 约束长度
stiffness: 1 // 刚度设置为1,表示刚性连接
});
World.add(world, constraint);
}
}
添加重力
为了使线段能够自然下垂并随时间移动,我们需要调整引擎的重力。我们将重力设置为较小的值,这样物体会缓慢下落。
js
// 调整引擎的重力,使链条缓慢下落,该参数适合手机版,电脑自行调节
engine.gravity.y = 0.1;
控制小球的移动
通过鼠标或触摸事件来控制小球的移动,当按下鼠标或触摸屏幕时,小球会跟随指针的位置。
js
let isMousePressed = false; // 用于检测鼠标是否按下
// 当按下鼠标时,小球跟随指针
window.addEventListener('mousedown', () => {
isMousePressed = true;
});
window.addEventListener('mouseup', () => {
isMousePressed = false;
});
// 鼠标移动时更新小球位置
window.addEventListener('mousemove', (event) => {
if (isMousePressed) {
const mouseX = event.clientX;
const mouseY = event.clientY;
Matter.Body.setPosition(followBall, { x: mouseX, y: mouseY });
}
});
// 支持触摸设备
window.addEventListener('touchstart', (event) => {
isMousePressed = true;
});
window.addEventListener('touchend', () => {
isMousePressed = false;
});
window.addEventListener('touchmove', (event) => {
if (isMousePressed) {
const touchX = event.touches[0].clientX;
const touchY = event.touches[0].clientY;
Matter.Body.setPosition(followBall, { x: touchX, y: touchY });
}
});
检测游戏失败
如果链条的任何部分触碰到地面,游戏将结束。
js
let gameOver = false;
// 监听碰撞事件,判断是否有线段触碰到地面
Events.on(engine, 'collisionStart', (event) => {
event.pairs.forEach(pair => {
if ((segments.includes(pair.bodyA) && pair.bodyB === ground) ||
(segments.includes(pair.bodyB) && pair.bodyA === ground)) {
if (!gameOver) {
gameOver = true;
alert('游戏结束! 最终得分: ' + score);
location.reload(); // 重新加载页面以重置游戏
}
}
});
});
计分系统
我们添加一个简单的计分系统,随着时间推移得分会增加,直到游戏结束。
js
let score = 0; // 初始化分数
const scoreDiv = document.createElement('div
');
scoreDiv.style.position = 'absolute';
scoreDiv.style.top = '10px';
scoreDiv.style.left = '10px';
scoreDiv.style.fontSize = '20px';
scoreDiv.style.color = 'white'; // 分数显示为白色
scoreDiv.innerText = '分数: 0';
document.body.appendChild(scoreDiv);
// 每秒更新一次分数
setInterval(() => {
if (!gameOver) {
score++;
scoreDiv.innerText = '分数: ' + score;
}
}, 1000);
添加水印
最后,我们可以在游戏中添加水印,以标识制作者信息。
js
const watermark = document.createElement('div');
watermark.style.position = 'absolute';
watermark.style.bottom = '10px';
watermark.style.right = '15px';
watermark.style.fontSize = '14px';
watermark.style.color = 'rgba(255, 255, 255, 0.7)';
watermark.innerText = 'Made By Touken'; // 制作者信息
document.body.appendChild(watermark);
完整代码
最后,你可以将所有代码合并到一个HTML文件中,运行它便可以体验这个小游戏了。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>命悬一线</title>
<style>
body {
margin: 0;
overflow: hidden;
/* 防止出现滚动条 */
}
canvas {
background: #f0f0f0;
display: block;
margin: 0 auto;
width: 100%;
/* 适配手机 */
height: 100vh;
/* 全屏高度 */
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
<script>
// 引入 Matter.js
const { Engine, Render, Runner, World, Bodies, Events, MouseConstraint, Mouse } = Matter;
// 创建引擎
const engine = Engine.create();
const world = engine.world;
// 创建渲染器
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: window.innerWidth,
height: window.innerHeight,
wireframes: false
}
});
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
// 创建地面和左右墙壁
const ground = Bodies.rectangle(window.innerWidth / 2, window.innerHeight, window.innerWidth, 60, { isStatic: true });
const leftWall = Bodies.rectangle(0, window.innerHeight / 2, 60, window.innerHeight + 100, { isStatic: true });
const rightWall = Bodies.rectangle(window.innerWidth, window.innerHeight / 2, 60, window.innerHeight + 100, { isStatic: true });
World.add(world, [ground, leftWall, rightWall]);
// 创建顶部天花板
const ceiling = Bodies.rectangle(window.innerWidth / 2, -40, window.innerWidth, 60, { isStatic: true });
World.add(world, ceiling);
// 创建跟随的球,初始位置在线的下方
const followBall = Bodies.circle(window.innerWidth / 2, window.innerHeight - 300, 30, {
friction: 0,
mass: 10000,
render: {
fillStyle: 'rgba(0, 255, 255, 0.5)', // 使用半透明的蓝色
strokeStyle: 'white', // 添加边框颜色
lineWidth: 5, // 设置边框宽度
// 自定义图形,使用 `beforeRender` 来绘制渐变或阴影
sprite: {
texture: '', // 可以添加对应的纹理或图像路径
xScale: 1, // 调节纹理的缩放
yScale: 1
}
},
isStatic: true
});
World.add(world, followBall);
// 创建线,水平排列
const segments = [];
const segmentCount = 50; // 减少段数以适配小屏幕
const segmentRadius = 2;
const segmentSpacing = 4;
for (let i = 0; i < segmentCount; i++) {
const xPosition = (window.innerWidth / 2) - (segmentCount / 2) * segmentSpacing + i * segmentSpacing;
const segment = Bodies.circle(xPosition, 100, segmentRadius, {
friction: 0,
frictionAir: 0,
restitution: 0.6,
render: {
fillStyle: 'green'
}
});
segments.push(segment);
World.add(world, segment);
// 创建约束连接,去除弹性
if (i > 0) {
const constraint = Matter.Constraint.create({
friction: 0,
bodyA: segments[i - 1],
bodyB: segment,
length: segmentRadius * 2,
stiffness: 1
});
World.add(world, constraint);
}
}
// 设置较小的重力,使物体下落缓慢
engine.gravity.y = 0.1;
// 触摸控制:按下时小球跟随,松开时停止跟随
let isMousePressed = false;
const handleMouseStart = (event) => {
isMousePressed = true;
};
const handleMouseEnd = () => {
isMousePressed = false;
};
const handleMouseMove = (event) => {
if (isMousePressed) {
const mouseX = event.clientX;
const mouseY = event.clientY;
// 将鼠标位置映射到 canvas 中
const canvasBounds = render.canvas.getBoundingClientRect();
const mousePosition = {
x: mouseX - canvasBounds.left,
y: mouseY - canvasBounds.top
};
// 更新小球位置
Matter.Body.setPosition(followBall, mousePosition);
}
};
window.addEventListener('mousedown', handleMouseStart);
window.addEventListener('mouseup', handleMouseEnd);
window.addEventListener('mousemove', handleMouseMove);
// 支持移动设备触摸
window.addEventListener('touchstart', (event) => {
event.preventDefault(); // 防止默认的触摸事件
isMousePressed = true;
});
window.addEventListener('touchend', handleMouseEnd);
window.addEventListener('touchmove', (event) => {
event.preventDefault(); // 防止滚动
if (isMousePressed) {
const touchX = event.touches[0].clientX;
const touchY = event.touches[0].clientY;
const canvasBounds = render.canvas.getBoundingClientRect();
const touchPosition = {
x: touchX - canvasBounds.left,
y: touchY - canvasBounds.top
};
// 更新小球位置
Matter.Body.setPosition(followBall, touchPosition);
}
});
// 游戏失败检测
let gameOver = false;
Events.on(engine, 'collisionStart', function (event) {
event.pairs.forEach(function (pair) {
if (segments.includes(pair.bodyA) && pair.bodyB === ground ||
segments.includes(pair.bodyB) && pair.bodyA === ground) {
if (!gameOver) {
gameOver = true;
alert('游戏结束! 最终得分: ' + score);
location.reload();
}
}
});
});
// 游戏计分
let score = 0;
const scoreDiv = document.createElement('div');
scoreDiv.style.position = 'absolute';
scoreDiv.style.top = '10px';
scoreDiv.style.left = '10px';
scoreDiv.style.fontSize = '20px';
scoreDiv.style.color = 'white'; // 设置分数颜色为白色
scoreDiv.innerText = '分数: 0';
document.body.appendChild(scoreDiv);
// 计时更新
setInterval(() => {
if (!gameOver) {
score++;
scoreDiv.innerText = '分数: ' + score;
}
}, 1000);
// 启动引擎,运行 Matter.js
World.add(world, [ground, followBall]);
// 创建水印
const watermark = document.createElement('div');
watermark.style.position = 'absolute';
watermark.style.bottom = '10px'; // 距离底部 10 像素
watermark.style.right = '15px'; // 距离右侧 15 像素
watermark.style.fontSize = '14px'; // 字体大小
watermark.style.color = 'rgba(255, 255, 255, 0.7)'; // 设置为白色并带有透明度
watermark.innerText = 'Made By Touken'; // 水印文本
document.body.appendChild(watermark);
</script>
</body>
</html>