当前位置: 首页 > article >正文

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

在本教程中,我们将使用Matter.js构建一个简单的物理游戏,称为**“命悬一线”**(建议手机游玩),在文章末尾附完整代码。游戏的目标是控制一个小球,避免让连接在一起的线段掉到地面上。当线段的一部分接触到地面时,游戏结束。通过本教程,你将逐步了解如何在网页中使用Matter.js创建2D物理世界,并实现基本的用户交互。

准备工作

首先需要一个HTML文件来托管我们的游戏,并引入Matter.js库。Matter.js是一个JavaScript物理引擎,可以轻松创建真实的物理效果。

<!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引擎和渲染器。

// 引入 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),即它们不会移动。

// 创建地面、墙壁和天花板,确保它们是静止的
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]);

创建玩家可控制的球

我们创建一个静止的小球,玩家可以通过鼠标或触摸来控制它。球的质量很大,目的是使它具有足够的重量来压住物理链条。

// 创建玩家控制的小球
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(约束)来连接。

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);
    }
}

添加重力

为了使线段能够自然下垂并随时间移动,我们需要调整引擎的重力。我们将重力设置为较小的值,这样物体会缓慢下落。

// 调整引擎的重力,使链条缓慢下落,该参数适合手机版,电脑自行调节
engine.gravity.y = 0.1;

控制小球的移动

通过鼠标或触摸事件来控制小球的移动,当按下鼠标或触摸屏幕时,小球会跟随指针的位置。

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 });
    }
});

检测游戏失败

如果链条的任何部分触碰到地面,游戏将结束。

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();  // 重新加载页面以重置游戏
            }
        }
    });
});

计分系统

我们添加一个简单的计分系统,随着时间推移得分会增加,直到游戏结束。

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);

添加水印

最后,我们可以在游戏中添加水印,以标识制作者信息。

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文件中,运行它便可以体验这个小游戏了。

<!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>

http://www.kler.cn/news/356626.html

相关文章:

  • Python随机森林算法详解与案例实现
  • HTML(五)列表详解
  • k8s的微服务
  • PyCharm+ssh跳板机+服务器
  • 自闭症儿童能否适应学校生活:提供专业辅助,助力顺利融入
  • 使用人体关键点驱动FBX格式虚拟人原理【详解】
  • Linux——应用软件的生命周期
  • 基于SpringBoot+Vue+uniapp的诗词学习系统的详细设计和实现(源码+lw+部署文档+讲解等)
  • 自由学习记录(11)
  • pc轨迹回放制作
  • 【DevOps工具篇】Docker的DNS原理
  • Snowflake算法js(实现)
  • Python线性回归算法:面向对象的实现与案例详解
  • 牛客编程初学者入门训练——BC53 判断是元音还是辅音
  • 驱动开发系列21 - 编译内核模块的Makefile解释
  • Qt键盘按下事件和定时器事件及事件的接收和忽略
  • Javaweb基础-axios
  • ElasticSearch+Kibana 8.1.0安装部署
  • 今日总结10.18
  • 反向传播和优化 pytorch