用jQuery和Canvas打造2D版“我的世界+超级玛丽“游戏
引言
在游戏开发的世界里,融合不同游戏元素创造新体验一直是一种有趣的尝试。今天,我将介绍如何使用jQuery和HTML5 Canvas技术,将"我的世界"的方块世界与"超级玛丽"的平台跳跃玩法相结合,打造一个有趣的2D平台游戏。
这个项目不需要任何游戏引擎,只需要基本的Web前端技术就能实现。非常适合想要学习游戏开发基础的初学者,或者想要快速制作原型的开发者。
游戏截图
游戏概念
我们的游戏将结合以下两款经典游戏的元素:
- 我的世界(Minecraft) - 方块构建的世界、不同类型的方块(泥土、草方块、石头等)
- 超级玛丽(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"我的世界超级玛丽"游戏具有以下特点:
- 方块世界:使用不同类型的方块(泥土、草方块、石头、砖块等)构建游戏世界
- 物理系统:实现了重力、跳跃和碰撞检测
- 敌人AI:敌人会自动移动并在遇到障碍时改变方向
- 金币收集:玩家可以收集金币增加分数
- 问号方块:撞击问号方块会产生金币
- 敌人互动:玩家可以踩踏敌人消灭它们
- 生命系统:玩家有多条生命,掉落或被敌人碰到会失去生命
- 相机系统:相机会跟随玩家移动,实现更大的游戏世界
- 程序化生成:每次游戏都会生成不同的关卡布局
总结
通过结合"我的世界"和"超级玛丽"的游戏元素,我们创建了一个有趣的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);
}