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

用jQuery和Canvas打造2D版“我的世界+超级玛丽“游戏

引言

在游戏开发的世界里,融合不同游戏元素创造新体验一直是一种有趣的尝试。今天,我将介绍如何使用jQuery和HTML5 Canvas技术,将"我的世界"的方块世界与"超级玛丽"的平台跳跃玩法相结合,打造一个有趣的2D平台游戏。

这个项目不需要任何游戏引擎,只需要基本的Web前端技术就能实现。非常适合想要学习游戏开发基础的初学者,或者想要快速制作原型的开发者。

游戏截图

在这里插入图片描述

游戏概念

我们的游戏将结合以下两款经典游戏的元素:

  1. 我的世界(Minecraft) - 方块构建的世界、不同类型的方块(泥土、草方块、石头等)
  2. 超级玛丽(Super Mario) - 侧滚式平台跳跃、收集金币、踩踏敌人

游戏将保持简单的2D侧视图,但使用我的世界风格的方块和纹理。玩家可以在方块世界中跳跃、收集金币并与敌人战斗。

技术栈

  • HTML5 - 提供基本的页面结构
  • CSS3 - 样式和布局
  • jQuery - DOM操作和事件处理
  • Canvas API - 游戏渲染和绘图

游戏实现

1. 基本结构

首先,我们需要创建一个基本的HTML结构,包含Canvas元素和游戏UI:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2D Minecraft Mario</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="game-container">
        <canvas id="game-canvas"></canvas>
        <div id="game-ui">
            <div id="score">分数: 0</div>
            <div id="lives">生命: 3</div>
        </div>
        <div id="game-controls">
            <p>控制: 方向键移动, 空格键跳跃, Tab键切换鼠标捕获</p>
        </div>
    </div>
    
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="game.js"></script>
</body>
</html>

2. 游戏常量和状态

在JavaScript中,我们首先定义游戏的常量和状态变量:

// 游戏常量
const BLOCK_SIZE = 32;
const GRAVITY = 0.2;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;

// 游戏状态
let score = 0;
let lives = 3;
let gameRunning = true;

// 方块类型
const BLOCK_TYPES = {
    AIR: 0,
    DIRT: 1,
    GRASS: 2,
    STONE: 3,
    BRICK: 4,
    QUESTION: 5,
    COIN: 6,
    ENEMY: 7
};

3. 方块渲染系统

我的世界的核心是方块系统。我们需要创建一个函数来绘制不同类型的方块及其纹理:

// 方块纹理图案
function drawBlockTexture(x, y, type) {
    ctx.fillStyle = BLOCK_COLORS[type] || 'transparent';
    ctx.fillRect(x, y, BLOCK_SIZE, BLOCK_SIZE);
    
    // 添加纹理细节
    switch(type) {
        case BLOCK_TYPES.GRASS:
            // 草方块顶部绿色
            ctx.fillStyle = '#7CFC00';
            ctx.fillRect(x, y, BLOCK_SIZE, BLOCK_SIZE / 4);
            break;
        case BLOCK_TYPES.BRICK:
            // 砖块纹理
            ctx.strokeStyle = '#8B0000';
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(x, y + BLOCK_SIZE / 2);
            ctx.lineTo(x + BLOCK_SIZE, y + BLOCK_SIZE / 2);
            ctx.stroke();
            ctx.beginPath();
            ctx.moveTo(x + BLOCK_SIZE / 2, y);
            ctx.lineTo(x + BLOCK_SIZE / 2, y + BLOCK_SIZE);
            ctx.stroke();
            break;
        // 其他方块类型...
    }
    
    // 添加方块边框
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 1;
    ctx.strokeRect(x, y, BLOCK_SIZE, BLOCK_SIZE);
}

4. 程序化关卡生成

我们使用程序化生成技术创建游戏关卡,而不是手动设计每个方块的位置:

function generateLevel() {
    // 首先清空关卡
    for (let y = 0; y < levelHeight; y++) {
        for (let x = 0; x < levelWidth; x++) {
            level[y][x] = BLOCK_TYPES.AIR;
        }
    }
    
    // 设置地面
    for (let x = 0; x < levelWidth; x++) {
        level[levelHeight - 1][x] = BLOCK_TYPES.DIRT;
        level[levelHeight - 2][x] = BLOCK_TYPES.GRASS;
        
        // 添加一些地下的泥土
        if (x < 15 || Math.random() < 0.5) {
            level[levelHeight - 3][x] = BLOCK_TYPES.DIRT;
        }
    }
    
    // 添加平台和障碍
    for (let i = 0; i < 20; i++) {
        const platformLength = Math.floor(Math.random() * 5) + 3;
        const platformX = Math.floor(Math.random() * (levelWidth - platformLength - 10)) + 10;
        const platformY = Math.floor(Math.random() * 5) + 10;
        
        for (let j = 0; j < platformLength; j++) {
            level[platformY][platformX + j] = BLOCK_TYPES.BRICK;
        }
        
        // 在平台上添加问号方块和金币
        if (Math.random() < 0.5) {
            level[platformY - 3][platformX + Math.floor(platformLength / 2)] = BLOCK_TYPES.QUESTION;
        }
    }
    
    // 添加敌人
    for (let i = 0; i < 10; i++) {
        const enemyX = Math.floor(Math.random() * (levelWidth - 20)) + 15;
        level[levelHeight - 3][enemyX] = BLOCK_TYPES.ENEMY;
    }
}

5. 玩家角色

玩家角色是游戏的核心,我们需要实现移动、跳跃和碰撞检测:

const player = {
    x: BLOCK_SIZE * 2,
    y: (levelHeight - 3) * BLOCK_SIZE - BLOCK_SIZE * 1.5,
    width: BLOCK_SIZE - 4,
    height: BLOCK_SIZE * 1.5,
    velocityX: 0,
    velocityY: 0,
    isJumping: false,
    facingRight: true,
    onGround: false,
    
    // 绘制玩家
    draw: function() {
        // 绘制玩家身体
        ctx.fillStyle = '#FF0000'; // 红色衣服
        ctx.fillRect(this.x, this.y, this.width, this.height);
        
        // 绘制头部
        ctx.fillStyle = '#FFA07A'; // 肤色
        ctx.fillRect(this.x, this.y - BLOCK_SIZE / 2, this.width, BLOCK_SIZE / 2);
        
        // 绘制眼睛和帽子
        // ...
    },
    
    // 更新玩家位置
    update: function() {
        // 应用重力
        if (!this.onGround) {
            this.velocityY += GRAVITY;
        }
        
        // 更新位置
        this.x += this.velocityX;
        this.y += this.velocityY;
        
        // 检测碰撞
        this.checkCollisions();
        
        // 限制在画布内
        // ...
    },
    
    // 碰撞检测
    checkCollisions: function() {
        // 获取玩家周围的网格位置
        const gridX1 = Math.floor(this.x / BLOCK_SIZE);
        const gridX2 = Math.floor((this.x + this.width) / BLOCK_SIZE);
        const gridY1 = Math.floor(this.y / BLOCK_SIZE);
        const gridY2 = Math.floor((this.y + this.height) / BLOCK_SIZE);
        
        // 向下碰撞检测(地面)
        const feetY = Math.floor((this.y + this.height + 1) / BLOCK_SIZE);
        
        for (let x = gridX1; x <= gridX2; x++) {
            if (x >= 0 && x < levelWidth && feetY >= 0 && feetY < levelHeight) {
                const blockBelow = level[feetY][x];
                
                if (blockBelow !== BLOCK_TYPES.AIR && blockBelow !== BLOCK_TYPES.COIN) {
                    const blockTop = feetY * BLOCK_SIZE;
                    
                    if (this.y + this.height >= blockTop - 2 && this.y + this.height <= blockTop + 8) {
                        this.y = blockTop - this.height;
                        this.velocityY = 0;
                        this.onGround = true;
                        this.isJumping = false;
                        break;
                    }
                }
            }
        }
        
        // 其他方向的碰撞检测
        // ...
        
        // 收集金币和敌人碰撞
        // ...
    },
    
    // 跳跃
    jump: function() {
        if (this.onGround) {
            this.velocityY = JUMP_FORCE;
            this.isJumping = true;
            this.onGround = false;
        }
    }
};

6. 敌人AI

敌人是游戏的挑战部分,我们需要实现简单的AI行为:

function initEnemies() {
    for (let y = 0; y < levelHeight; y++) {
        for (let x = 0; x < levelWidth; x++) {
            if (level[y][x] === BLOCK_TYPES.ENEMY) {
                enemies.push({
                    x: x * BLOCK_SIZE,
                    y: y * BLOCK_SIZE,
                    width: BLOCK_SIZE,
                    height: BLOCK_SIZE,
                    velocityX: -1,
                    
                    draw: function() {
                        ctx.fillStyle = '#8B008B'; // 紫色敌人
                        ctx.fillRect(this.x, this.y, this.width, this.height);
                        
                        // 眼睛
                        ctx.fillStyle = '#FFF';
                        ctx.fillRect(this.x + 5, this.y + 5, 5, 5);
                        ctx.fillRect(this.x + this.width - 10, this.y + 5, 5, 5);
                    },
                    
                    update: function() {
                        this.x += this.velocityX;
                        
                        // 简单的AI:碰到障碍物就转向
                        const gridX = this.velocityX > 0 ? 
                            Math.floor((this.x + this.width + 2) / BLOCK_SIZE) : 
                            Math.floor((this.x - 2) / BLOCK_SIZE);
                        const gridY = Math.floor(this.y / BLOCK_SIZE);
                        
                        // 检查前方是否有方块或边缘
                        if (gridX < 0 || gridX >= levelWidth || 
                            level[gridY][gridX] !== BLOCK_TYPES.AIR && 
                            level[gridY][gridX] !== BLOCK_TYPES.ENEMY) {
                            this.velocityX *= -1;
                        }
                    }
                });
                
                // 移除关卡中的敌人标记
                level[y][x] = BLOCK_TYPES.AIR;
            }
        }
    }
}

7. 相机系统

为了让游戏世界可以比屏幕更大,我们实现了一个跟随玩家的相机系统:

// 相机位置
let cameraX = 0;

// 更新相机位置
if (player.x > canvas.width / 2) {
    cameraX = player.x - canvas.width / 2;
}

// 限制相机范围
const maxCameraX = (levelWidth * BLOCK_SIZE) - canvas.width;
cameraX = Math.max(0, Math.min(cameraX, maxCameraX));

// 绘制时考虑相机偏移
ctx.save();
ctx.translate(-cameraX, 0);
// 绘制游戏元素
ctx.restore();

8. 输入处理

我们使用jQuery来处理键盘输入:

// 键盘控制
const keys = {};

$(document).keydown(function(e) {
    keys[e.which] = true;
    
    // 空格键跳跃
    if (e.which === 32) {
        player.jump();
        e.preventDefault();
    }
    
    // Tab键切换鼠标捕获
    if (e.which === 9) {
        if (document.pointerLockElement === canvas) {
            document.exitPointerLock();
        } else {
            canvas.requestPointerLock();
        }
        e.preventDefault();
    }
});

$(document).keyup(function(e) {
    keys[e.which] = false;
});

9. 游戏循环

最后,我们需要一个游戏循环来不断更新和渲染游戏:

function gameLoop() {
    if (!gameRunning) return;
    
    // 清除画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // 处理键盘输入
    if (keys[37] || keys[65]) { // 左箭头或A
        player.velocityX = -MOVE_SPEED;
        player.facingRight = false;
    } else if (keys[39] || keys[68]) { // 右箭头或D
        player.velocityX = MOVE_SPEED;
        player.facingRight = true;
    } else {
        player.velocityX = 0;
    }
    
    // 更新玩家位置
    player.update();
    
    // 更新相机位置
    // ...
    
    // 绘制背景
    ctx.fillStyle = '#87CEEB'; // 天空蓝
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // 绘制云朵
    // ...
    
    // 绘制关卡
    // ...
    
    // 更新并绘制敌人
    // ...
    
    // 绘制玩家
    // ...
    
    // 继续游戏循环
    requestAnimationFrame(gameLoop);
}

// 初始化游戏
function initGame() {
    generateLevel();
    initPlayerPosition();
    initEnemies();
    gameLoop();
}

// 启动游戏
initGame();

游戏特点

我们的2D"我的世界超级玛丽"游戏具有以下特点:

  1. 方块世界:使用不同类型的方块(泥土、草方块、石头、砖块等)构建游戏世界
  2. 物理系统:实现了重力、跳跃和碰撞检测
  3. 敌人AI:敌人会自动移动并在遇到障碍时改变方向
  4. 金币收集:玩家可以收集金币增加分数
  5. 问号方块:撞击问号方块会产生金币
  6. 敌人互动:玩家可以踩踏敌人消灭它们
  7. 生命系统:玩家有多条生命,掉落或被敌人碰到会失去生命
  8. 相机系统:相机会跟随玩家移动,实现更大的游戏世界
  9. 程序化生成:每次游戏都会生成不同的关卡布局

总结

通过结合"我的世界"和"超级玛丽"的游戏元素,我们创建了一个有趣的2D平台游戏。这个项目展示了如何使用基本的Web技术(HTML5、CSS3、jQuery和Canvas)来实现游戏开发的核心概念,如物理系统、碰撞检测、敌人AI和程序化关卡生成。

这个游戏不仅是一个有趣的编程练习,也是学习游戏开发基础的好方法。通过理解这些核心概念,你可以进一步扩展游戏,或者将这些知识应用到其他游戏项目中。

源代码

Directory Content Summary

Source Directory: ./minecraft-mario

Directory Structure

minecraft-mario/
  game.js
  index.html
  styles.css

File Contents

game.js

$(document).ready(function() {
    // 游戏常量
    const BLOCK_SIZE = 32;
    const GRAVITY = 0.2; // 进一步减小重力值
    const JUMP_FORCE = -12;
    const MOVE_SPEED = 5;
    
    // 游戏状态
    let score = 0;
    let lives = 3;
    let gameRunning = true;
    
    // 帧计数器用于调试
    let frameCount = 0;
    
    // 画布设置
    const canvas = document.getElementById('game-canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = 800;
    canvas.height = 600;
    
    // 方块类型
    const BLOCK_TYPES = {
        AIR: 0,
        DIRT: 1,
        GRASS: 2,
        STONE: 3,
        BRICK: 4,
        QUESTION: 5,
        COIN: 6,
        ENEMY: 7
    };
    
    // 方块颜色和纹理
    const BLOCK_COLORS = {
        [BLOCK_TYPES.DIRT]: '#8B4513',
        [BLOCK_TYPES.GRASS]: '#567D46',
        [BLOCK_TYPES.STONE]: '#808080',
        [BLOCK_TYPES.BRICK]: '#B22222',
        [BLOCK_TYPES.QUESTION]: '#FFD700',
        [BLOCK_TYPES.COIN]: '#FFD700'
    };
    
    // 方块纹理图案
    function drawBlockTexture(x, y, type) {
        ctx.fillStyle = BLOCK_COLORS[type] || 'transparent';
        ctx.fillRect(x, y, BLOCK_SIZE, BLOCK_SIZE);
        
        // 添加纹理细节
        switch(type) {
            case BLOCK_TYPES.GRASS:
                // 草方块顶部绿色
                ctx.fillStyle = '#7CFC00';
                ctx.fillRect(x, y, BLOCK_SIZE, BLOCK_SIZE / 4);
                break;
            case BLOCK_TYPES.BRICK:
                // 砖块纹理
                ctx.strokeStyle = '#8B0000';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(x, y + BLOCK_SIZE / 2);
                ctx.lineTo(x + BLOCK_SIZE, y + BLOCK_SIZE / 2);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(x + BLOCK_SIZE / 2, y);
                ctx.lineTo(x + BLOCK_SIZE / 2, y + BLOCK_SIZE);
                ctx.stroke();
                break;
            case BLOCK_TYPES.QUESTION:
                // 问号方块
                ctx.fillStyle = '#FFA500';
                ctx.font = '20px Arial';
                ctx.fillText('?', x + BLOCK_SIZE / 3, y + BLOCK_SIZE / 1.5);
                break;
            case BLOCK_TYPES.COIN:
                // 金币
                ctx.beginPath();
                ctx.arc(x + BLOCK_SIZE / 2, y + BLOCK_SIZE / 2, BLOCK_SIZE / 3, 0, Math.PI * 2);
                ctx.fill();
                ctx.strokeStyle = '#B8860B';
                ctx.lineWidth = 2;
                ctx.stroke();
                break;
        }
        
        // 添加方块边框
        ctx.strokeStyle = '#000';
        ctx.lineWidth = 1;
        ctx.strokeRect(x, y, BLOCK_SIZE, BLOCK_SIZE);
    }
    
    // 关卡设计 (0=空气, 1=泥土, 2=草方块, 3=石头, 4=砖块, 5=问号方块, 6=金币, 7=敌人)
    const levelWidth = 100; // 关卡宽度(方块数)
    const levelHeight = 18; // 关卡高度(方块数)
    let level = Array(levelHeight).fill().map(() => Array(levelWidth).fill(BLOCK_TYPES.AIR));
    
    // 生成关卡
    function generateLevel() {
        // 首先清空关卡
        for (let y = 0; y < levelHeight; y++) {
            for (let x = 0; x < levelWidth; x++) {
                level[y][x] = BLOCK_TYPES.AIR;
            }
        }
        
        // 设置地面 - 确保整个地面都是实心的
        for (let x = 0; x < levelWidth; x++) {
            // 最底层是泥土
            level[levelHeight - 1][x] = BLOCK_TYPES.DIRT;
            // 上面一层是草方块
            level[levelHeight - 2][x] = BLOCK_TYPES.GRASS;
            // 再上面一层也是泥土(确保起始区域有足够支撑)
            if (x < 15) {
                level[levelHeight - 3][x] = BLOCK_TYPES.DIRT;
            } else if (Math.random() < 0.5) {
                level[levelHeight - 3][x] = BLOCK_TYPES.DIRT;
            }
        }
        
        // 添加一些平台和障碍
        for (let i = 0; i < 20; i++) {
            const platformLength = Math.floor(Math.random() * 5) + 3;
            const platformX = Math.floor(Math.random() * (levelWidth - platformLength - 10)) + 10;
            const platformY = Math.floor(Math.random() * 5) + 10;
            
            for (let j = 0; j < platformLength; j++) {
                level[platformY][platformX + j] = BLOCK_TYPES.BRICK;
            }
            
            // 在平台上添加一些问号方块和金币
            if (Math.random() < 0.5) {
                level[platformY - 3][platformX + Math.floor(platformLength / 2)] = BLOCK_TYPES.QUESTION;
            }
            
            if (Math.random() < 0.3) {
                level[platformY - 4][platformX + 1] = BLOCK_TYPES.COIN;
            }
        }
        
        // 添加一些管道和障碍物
        for (let i = 0; i < 8; i++) {
            const pipeX = Math.floor(Math.random() * (levelWidth - 20)) + 15;
            const pipeHeight = Math.floor(Math.random() * 2) + 2;
            
            for (let y = 0; y < pipeHeight; y++) {
                level[levelHeight - 3 - y][pipeX] = BLOCK_TYPES.STONE;
                level[levelHeight - 3 - y][pipeX + 1] = BLOCK_TYPES.STONE;
            }
        }
        
        // 添加一些敌人
        for (let i = 0; i < 10; i++) {
            const enemyX = Math.floor(Math.random() * (levelWidth - 20)) + 15;
            level[levelHeight - 3][enemyX] = BLOCK_TYPES.ENEMY;
        }
        
        // 确保起始位置安全 - 清除玩家起始位置的方块
        for (let x = 1; x < 5; x++) {
            for (let y = levelHeight - 4; y > levelHeight - 10; y--) {
                level[y][x] = BLOCK_TYPES.AIR;
            }
        }
        
        // 确保起始位置下方有坚实的地面
        for (let x = 0; x < 10; x++) {
            level[levelHeight - 2][x] = BLOCK_TYPES.GRASS; // 顶层草方块
            level[levelHeight - 1][x] = BLOCK_TYPES.DIRT;  // 下面是泥土
        }
    }
    
    // 玩家对象
    const player = {
        x: BLOCK_SIZE * 2,
        y: (levelHeight - 3) * BLOCK_SIZE - BLOCK_SIZE * 1.5, // 调整初始高度确保站在地面上
        width: BLOCK_SIZE - 4,
        height: BLOCK_SIZE * 1.5,
        velocityX: 0,
        velocityY: 0,
        isJumping: false,
        facingRight: true,
        onGround: false, // 新增地面状态跟踪
        
        // 绘制玩家
        draw: function() {
            // 绘制玩家身体
            ctx.fillStyle = '#FF0000'; // 红色衣服
            ctx.fillRect(this.x, this.y, this.width, this.height);
            
            // 绘制头部
            ctx.fillStyle = '#FFA07A'; // 肤色
            ctx.fillRect(this.x, this.y - BLOCK_SIZE / 2, this.width, BLOCK_SIZE / 2);
            
            // 绘制眼睛
            ctx.fillStyle = '#000';
            if (this.facingRight) {
                ctx.fillRect(this.x + this.width - 10, this.y - BLOCK_SIZE / 3, 4, 4);
            } else {
                ctx.fillRect(this.x + 6, this.y - BLOCK_SIZE / 3, 4, 4);
            }
            
            // 绘制帽子
            ctx.fillStyle = '#0000FF'; // 蓝色帽子
            ctx.fillRect(this.x - 2, this.y - BLOCK_SIZE / 2, this.width + 4, BLOCK_SIZE / 6);
        },
        
        // 更新玩家位置
        update: function() {
            // 应用重力(只有不在地面时)
            if (!this.onGround) {
                this.velocityY += GRAVITY;
            }
            
            // 限制下落速度,防止穿过方块
            if (this.velocityY > BLOCK_SIZE / 2) {
                this.velocityY = BLOCK_SIZE / 2;
            }
            
            // 更新位置
            this.x += this.velocityX;
            this.y += this.velocityY;
            
            // 重置地面状态,让碰撞检测重新判断
            this.onGround = false;
            
            // 检测碰撞
            this.checkCollisions();
            
            // 限制在画布内
            if (this.x < 0) this.x = 0;
            if (this.x > canvas.width - this.width) this.x = canvas.width - this.width;
            
            // 如果掉出画布底部,失去生命
            if (this.y > canvas.height) {
                this.respawn();
                lives--;
                $('#lives').text('生命: ' + lives);
                
                if (lives <= 0) {
                    gameRunning = false;
                    alert('游戏结束! 最终分数: ' + score);
                    location.reload();
                }
            }
        },
        
        // 检测与方块的碰撞
        checkCollisions: function() {
            // 获取玩家周围的网格位置
            const gridX1 = Math.floor(this.x / BLOCK_SIZE);
            const gridX2 = Math.floor((this.x + this.width) / BLOCK_SIZE);
            const gridY1 = Math.floor(this.y / BLOCK_SIZE);
            const gridY2 = Math.floor((this.y + this.height) / BLOCK_SIZE);
            
            // 向下碰撞检测(地面)- 完全重写
            // 检查玩家脚下的方块
            const feetY = Math.floor((this.y + this.height + 1) / BLOCK_SIZE); // 脚下一个像素的位置
            
            for (let x = gridX1; x <= gridX2; x++) {
                if (x >= 0 && x < levelWidth && feetY >= 0 && feetY < levelHeight) {
                    const blockBelow = level[feetY][x];
                    
                    // 如果脚下有实心方块
                    if (blockBelow !== BLOCK_TYPES.AIR && blockBelow !== BLOCK_TYPES.COIN) {
                        // 计算方块的顶部Y坐标
                        const blockTop = feetY * BLOCK_SIZE;
                        
                        // 如果玩家的底部接触或略微穿过方块顶部
                        if (this.y + this.height >= blockTop - 2 && this.y + this.height <= blockTop + 8) {
                            // 将玩家放在方块顶部
                            this.y = blockTop - this.height;
                            this.velocityY = 0;
                            this.onGround = true;
                            this.isJumping = false;
                            break; // 找到地面就可以停止检查
                        }
                    }
                }
            }
            
            // 向上碰撞检测(天花板)
            if (this.velocityY < 0) {
                for (let x = gridX1; x <= gridX2; x++) {
                    const gridY = gridY1 - 1;
                    if (gridY >= 0 && x >= 0 && x < levelWidth) {
                        const block = level[gridY][x];
                        if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.COIN) {
                            if (this.y < (gridY + 1) * BLOCK_SIZE) {
                                this.y = (gridY + 1) * BLOCK_SIZE;
                                this.velocityY = 0;
                                
                                // 如果是问号方块,产生金币
                                if (block === BLOCK_TYPES.QUESTION) {
                                    level[gridY][x] = BLOCK_TYPES.BRICK;
                                    level[gridY - 1][x] = BLOCK_TYPES.COIN;
                                    score += 10;
                                    $('#score').text('分数: ' + score);
                                }
                            }
                        }
                    }
                }
            }
            
            // 水平碰撞检测
            const originalX = this.x;
            
            // 向右碰撞
            if (this.velocityX > 0) {
                for (let y = gridY1; y <= gridY2; y++) {
                    const gridX = gridX2 + 1;
                    if (y >= 0 && y < levelHeight && gridX >= 0 && gridX < levelWidth) {
                        const block = level[y][gridX];
                        if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.COIN) {
                            if (this.x + this.width > gridX * BLOCK_SIZE) {
                                this.x = gridX * BLOCK_SIZE - this.width;
                                this.velocityX = 0;
                            }
                        }
                    }
                }
            }
            
            // 向左碰撞
            if (this.velocityX < 0) {
                for (let y = gridY1; y <= gridY2; y++) {
                    const gridX = gridX1 - 1;
                    if (y >= 0 && y < levelHeight && gridX >= 0 && gridX < levelWidth) {
                        const block = level[y][gridX];
                        if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.COIN) {
                            if (this.x < (gridX + 1) * BLOCK_SIZE) {
                                this.x = (gridX + 1) * BLOCK_SIZE;
                                this.velocityX = 0;
                            }
                        }
                    }
                }
            }
            
            // 收集金币
            for (let y = gridY1; y <= gridY2; y++) {
                for (let x = gridX1; x <= gridX2; x++) {
                    if (y >= 0 && y < levelHeight && x >= 0 && x < levelWidth) {
                        if (level[y][x] === BLOCK_TYPES.COIN) {
                            level[y][x] = BLOCK_TYPES.AIR;
                            score += 100;
                            $('#score').text('分数: ' + score);
                        }
                        
                        // 敌人碰撞
                        if (level[y][x] === BLOCK_TYPES.ENEMY) {
                            // 如果从上方踩到敌人
                            if (this.velocityY > 0 && this.y + this.height < (y + 0.5) * BLOCK_SIZE) {
                                level[y][x] = BLOCK_TYPES.AIR;
                                this.velocityY = JUMP_FORCE / 2; // 小跳
                                score += 50;
                                $('#score').text('分数: ' + score);
                            } else {
                                // 被敌人碰到
                                this.respawn();
                                lives--;
                                $('#lives').text('生命: ' + lives);
                                
                                if (lives <= 0) {
                                    gameRunning = false;
                                    alert('游戏结束! 最终分数: ' + score);
                                    location.reload();
                                }
                            }
                        }
                    }
                }
            }
        },
        
        // 跳跃
        jump: function() {
            if (this.onGround) {
                this.velocityY = JUMP_FORCE;
                this.isJumping = true;
                this.onGround = false;
            }
        },
        
        // 重生
        respawn: function() {
            this.x = BLOCK_SIZE * 2;
            this.y = (levelHeight - 3) * BLOCK_SIZE - BLOCK_SIZE * 1.5;
            this.velocityX = 0;
            this.velocityY = 0;
            this.isJumping = false;
            this.onGround = false; // 重置地面状态
        }
    };
    
    // 敌人对象数组
    let enemies = [];
    
    // 初始化敌人
    function initEnemies() {
        for (let y = 0; y < levelHeight; y++) {
            for (let x = 0; x < levelWidth; x++) {
                if (level[y][x] === BLOCK_TYPES.ENEMY) {
                    enemies.push({
                        x: x * BLOCK_SIZE,
                        y: y * BLOCK_SIZE,
                        width: BLOCK_SIZE,
                        height: BLOCK_SIZE,
                        velocityX: -1,
                        
                        draw: function() {
                            ctx.fillStyle = '#8B008B'; // 紫色敌人
                            ctx.fillRect(this.x, this.y, this.width, this.height);
                            
                            // 眼睛
                            ctx.fillStyle = '#FFF';
                            ctx.fillRect(this.x + 5, this.y + 5, 5, 5);
                            ctx.fillRect(this.x + this.width - 10, this.y + 5, 5, 5);
                        },
                        
                        update: function() {
                            this.x += this.velocityX;
                            
                            // 简单的AI:碰到障碍物就转向
                            const gridX = this.velocityX > 0 ? 
                                Math.floor((this.x + this.width + 2) / BLOCK_SIZE) : 
                                Math.floor((this.x - 2) / BLOCK_SIZE);
                            const gridY = Math.floor(this.y / BLOCK_SIZE);
                            
                            // 检查前方是否有方块
                            if (gridX < 0 || gridX >= levelWidth || 
                                level[gridY][gridX] !== BLOCK_TYPES.AIR && level[gridY][gridX] !== BLOCK_TYPES.ENEMY) {
                                this.velocityX *= -1;
                            }
                            
                            // 检查下方是否有方块(防止掉落)
                            const gridXBelow = Math.floor((this.x + this.width / 2) / BLOCK_SIZE);
                            const gridYBelow = Math.floor((this.y + this.height + 2) / BLOCK_SIZE);
                            
                            if (gridYBelow < levelHeight && (gridXBelow < 0 || gridXBelow >= levelWidth || 
                                level[gridYBelow][gridXBelow] === BLOCK_TYPES.AIR)) {
                                this.velocityX *= -1;
                            }
                        }
                    });
                    
                    // 移除关卡中的敌人标记,因为已经创建了实际的敌人对象
                    level[y][x] = BLOCK_TYPES.AIR;
                }
            }
        }
    }
    
    // 相机位置
    let cameraX = 0;
    
    // 键盘控制
    const keys = {};
    
    $(document).keydown(function(e) {
        keys[e.which] = true;
        
        // 空格键跳跃
        if (e.which === 32) {
            player.jump();
            e.preventDefault();
        }
        
        // Tab键切换鼠标捕获(参考Minecraft的功能)
        if (e.which === 9) {
            if (document.pointerLockElement === canvas) {
                document.exitPointerLock();
            } else {
                canvas.requestPointerLock();
            }
            e.preventDefault();
        }
    });
    
    $(document).keyup(function(e) {
        keys[e.which] = false;
    });
    
    // 鼠标移动控制(类似Minecraft的视角控制)
    $(canvas).on('mousemove', function(e) {
        if (document.pointerLockElement === canvas) {
            // 水平移动视角
            cameraX -= e.originalEvent.movementX * 0.5;
            
            // 限制相机范围
            const maxCameraX = (levelWidth * BLOCK_SIZE) - canvas.width;
            cameraX = Math.max(0, Math.min(cameraX, maxCameraX));
        }
    });
    
    // 游戏循环
    function gameLoop() {
        if (!gameRunning) return;
        
        // 清除画布
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        // 处理键盘输入
        if (keys[37] || keys[65]) { // 左箭头或A
            player.velocityX = -MOVE_SPEED;
            player.facingRight = false;
        } else if (keys[39] || keys[68]) { // 右箭头或D
            player.velocityX = MOVE_SPEED;
            player.facingRight = true;
        } else {
            player.velocityX = 0;
        }
        
        // 更新玩家位置
        player.update();
        
        // 输出玩家状态(仅在前几帧)
        if (frameCount < 10) {
            console.log("帧:", frameCount, "玩家位置:", player.y, "速度:", player.velocityY, "地面状态:", player.onGround);
            frameCount++;
        }
        
        // 更新相机位置跟随玩家
        if (player.x > canvas.width / 2) {
            cameraX = player.x - canvas.width / 2;
        }
        
        // 限制相机范围
        const maxCameraX = (levelWidth * BLOCK_SIZE) - canvas.width;
        cameraX = Math.max(0, Math.min(cameraX, maxCameraX));
        
        // 绘制背景
        ctx.fillStyle = '#87CEEB'; // 天空蓝
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // 绘制云朵
        ctx.fillStyle = '#FFF';
        for (let i = 0; i < 5; i++) {
            const cloudX = (i * 200 + 50) - (cameraX * 0.2) % (levelWidth * BLOCK_SIZE);
            ctx.beginPath();
            ctx.arc(cloudX, 80, 30, 0, Math.PI * 2);
            ctx.arc(cloudX + 25, 70, 25, 0, Math.PI * 2);
            ctx.arc(cloudX - 25, 70, 25, 0, Math.PI * 2);
            ctx.fill();
        }
        
        // 绘制关卡
        const startX = Math.floor(cameraX / BLOCK_SIZE);
        const endX = startX + Math.ceil(canvas.width / BLOCK_SIZE) + 1;
        
        for (let y = 0; y < levelHeight; y++) {
            for (let x = startX; x < endX; x++) {
                if (x >= 0 && x < levelWidth && level[y][x] !== BLOCK_TYPES.AIR) {
                    drawBlockTexture(x * BLOCK_SIZE - cameraX, y * BLOCK_SIZE, level[y][x]);
                }
            }
        }
        
        // 更新并绘制敌人
        for (let i = 0; i < enemies.length; i++) {
            enemies[i].update();
            
            // 只绘制在视野内的敌人
            if (enemies[i].x + enemies[i].width > cameraX && 
                enemies[i].x < cameraX + canvas.width) {
                enemies[i].draw();
            }
        }
        
        // 绘制玩家(相对于相机位置)
        ctx.save();
        ctx.translate(-cameraX, 0);
        player.draw();
        ctx.restore();
        
        // 继续游戏循环
        requestAnimationFrame(gameLoop);
    }
    
    // 在游戏开始前确保玩家站在地面上
    function initPlayerPosition() {
        // 强制将玩家放在起始位置的地面上
        player.x = BLOCK_SIZE * 2;
        player.y = (levelHeight - 3) * BLOCK_SIZE - player.height;
        player.velocityY = 0;
        player.velocityX = 0;
        player.onGround = true;
        player.isJumping = false;
        
        // 确保地面存在
        level[levelHeight - 2][2] = BLOCK_TYPES.GRASS;
        level[levelHeight - 1][2] = BLOCK_TYPES.DIRT;
        
        // 执行一次碰撞检测以确保正确放置
        player.checkCollisions();
        
        // 输出调试信息
        console.log("初始化玩家位置:", player.x, player.y);
        console.log("地面方块位置:", (levelHeight - 2) * BLOCK_SIZE);
        console.log("玩家是否在地面:", player.onGround);
    }
    
    // 初始化游戏
    function initGame() {
        // 生成关卡
        generateLevel();
        
        // 初始化玩家位置
        initPlayerPosition();
        
        // 初始化敌人
        initEnemies();
        
        // 开始游戏循环
        gameLoop();
    }
    
    // 启动游戏
    initGame();
});

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2D Minecraft Mario</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="game-container">
        <canvas id="game-canvas"></canvas>
        <div id="game-ui">
            <div id="score">分数: 0</div>
            <div id="lives">生命: 3</div>
        </div>
        <div id="game-controls">
            <p>控制: 方向键移动, 空格键跳跃, Tab键切换鼠标捕获</p>
        </div>
    </div>
    
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="game.js"></script>
</body>
</html>

styles.css

body {
    margin: 0;
    padding: 0;
    background-color: #333;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    font-family: 'Arial', sans-serif;
}

#game-container {
    position: relative;
    width: 800px;
    height: 600px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}

#game-canvas {
    width: 100%;
    height: 100%;
    background-color: #87CEEB; /* 天空蓝色背景 */
}

#game-ui {
    position: absolute;
    top: 10px;
    left: 10px;
    color: white;
    font-size: 18px;
    text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
}

#game-ui div {
    margin-bottom: 5px;
}

#game-controls {
    position: absolute;
    bottom: 10px;
    left: 0;
    right: 0;
    text-align: center;
    color: white;
    font-size: 14px;
    text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}

相关文章:

  • 知名界面控件DevExpress v24.2.6全新可用|发布重要更改
  • 安卓一些接口使用
  • dbgpt7.0 docker部署
  • Unity工具—默认取消Image和RowImage的Raycast Target
  • Vue3入门
  • 智谱大模型(ChatGLM3)PyCharm的调试指南
  • AI大模型从0到1记录学习 day11
  • 三步构建企业级操作日志系统:Spring AOP + 自定义注解的优雅实践
  • Redis的一些高级指令
  • HBase安装与配置——单机版到完全分布式部署
  • 【蓝桥杯】回文字符串
  • 自己用python写的查询任意网络设备IP地址工具使用实测
  • 什么是 继续预训练、SFT(监督微调)和RLHF
  • 【Java/数据结构】Map与Set(图文版)
  • AllData数据中台商业版发布版本1.2.9相关白皮书发布
  • UML 4+1 视图:搭建软件架构的 “万能拼图”
  • zabbix“专家坐诊”第281期问答
  • Logstash开启定时任务增量同步mysql数据到es的时区问题
  • 淘宝搜索关键字与商品数据采集接口技术指南
  • 软考 中级软件设计师 考点知识点笔记总结 day09 操作系统进程管理
  • 庄语乐︱宋代历史是被“塑造”出来的吗?
  • 从孔雀尾巴到蒙娜丽莎,一个鸟类学博士眼中的“美”
  • 五一“拼假”催热超长假期,热门酒店民宿一房难求
  • 西班牙遭遇史上最严重停电,已进入国家紧急状态
  • 经济日报:多平台告别“仅退款”,规则调整有何影响
  • 我国已形成完整人工智能产业体系,专利申请量位居全球首位