【前端】Matter实战:HTML游戏”命悬一线“

在本教程中,我们将使用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>
相关推荐
前端郭德纲6 分钟前
深入浅出ES6 Promise
前端·javascript·es6
就爱敲代码11 分钟前
ES6 运算符的扩展
前端·ecmascript·es6
王哲晓32 分钟前
第六章 Vue计算属性之computed
前端·javascript·vue.js
究极无敌暴龙战神X38 分钟前
CSS复习2
前端·javascript·css
风清扬_jd1 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5
Ellie陈1 小时前
Java已死,大模型才是未来?
java·开发语言·前端·后端·python
想做白天梦2 小时前
双向链表(数据结构与算法)
java·前端·算法
有梦想的咕噜2 小时前
Electron 是一个用于构建跨平台桌面应用程序的开源框架
前端·javascript·electron
yqcoder2 小时前
electron 监听窗口高端变化
前端·javascript·vue.js
Python私教2 小时前
Flutter主题最佳实践
前端·javascript·flutter