/* CZ原创水印 */
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2D 我的世界 - Web版</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
body {
margin: 0;
overflow: hidden;
background-color: #87CEEB; /* 天空蓝 */
font-family: 'VT323', monospace;
user-select: none;
}
#game-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
image-rendering: pixelated; /* 像素风格渲染 */
box-shadow: 0 0 20px rgba(0,0,0,0.5);
background-color: #87CEEB;
}
.ui-layer {
position: absolute;
pointer-events: none;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
}
.hud-panel {
pointer-events: auto;
background: rgba(0, 0, 0, 0.6);
border: 4px solid #333;
padding: 10px;
color: white;
border-radius: 4px;
}
.hotbar {
display: flex;
gap: 5px;
justify-content: center;
margin-top: 10px;
}
.slot {
width: 50px;
height: 50px;
background: rgba(0, 0, 0, 0.5);
border: 3px solid #555;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
position: relative;
transition: transform 0.1s;
}
.slot.active {
border-color: #fff;
transform: scale(1.1);
box-shadow: 0 0 10px rgba(255,255,255,0.5);
}
.slot img {
width: 32px;
height: 32px;
image-rendering: pixelated;
}
.slot-number {
position: absolute;
bottom: 2px;
left: 4px;
font-size: 12px;
color: #ddd;
text-shadow: 1px 1px 0 #000;
}
/* 简单的方块颜色模拟 */
.block-dirt { background-color: #5d4037; }
.block-grass { background: linear-gradient(to bottom, #4caf50 30%, #5d4037 30%); }
.block-stone { background-color: #757575; }
.block-wood { background-color: #795548; }
.block-leaves { background-color: #2e7d32; }
.block-brick { background-color: #a1887f; }
.block-sand { background-color: #fdd835; }
.block-glass { background-color: rgba(200, 240, 255, 0.6); border: 1px solid #fff; }
.controls-hint {
position: absolute;
top: 20px;
right: 20px;
text-align: right;
font-size: 14px;
line-height: 1.5;
color: rgba(255,255,255,0.8);
text-shadow: 1px 1px 2px black;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.8);
padding: 20px;
border-radius: 10px;
z-index: 100;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="loading">正在生成世界...</div>
<div class="ui-layer">
<div class="controls-hint hud-panel">
<div><span class="font-bold text-yellow-400">WASD</span> 移动 / 跳跃</div>
<div><span class="font-bold text-yellow-400">鼠标左键</span> 破坏方块</div>
<div><span class="font-bold text-yellow-400">鼠标右键</span> 放置方块</div>
<div><span class="font-bold text-yellow-400">1-9</span> 选择物品</div>
<div><span class="font-bold text-yellow-400">R</span> 重置世界</div>
</div>
<div class="hud-panel" style="align-self: center; margin-bottom: 20px;">
<div class="text-center text-sm mb-2 text-gray-300">物品栏</div>
<div class="hotbar" id="hotbar">
<!-- 由JS生成 -->
</div>
</div>
</div>
</div>
<script>
/**
* 2D Minecraft Core Logic
* 使用 Canvas API 实现基于 Tile 的沙盒游戏
*/
// --- 配置常量 ---
const TILE_SIZE = 40;
const CHUNK_WIDTH = 200; // 世界宽度(块)
const CHUNK_HEIGHT = 64; // 世界高度(块)
const GRAVITY = 0.5;
const TERMINAL_VELOCITY = 12;
const PLAYER_SPEED = 5;
const JUMP_FORCE = 9;
// 方块ID定义
const BLOCKS = {
AIR: 0,
DIRT: 1,
GRASS: 2,
STONE: 3,
WOOD: 4,
LEAVES: 5,
BRICK: 6,
SAND: 7,
GLASS: 8,
BEDROCK: 99
};
// 方块属性映射
const BLOCK_PROPS = {
[BLOCKS.AIR]: { color: null, solid: false },
[BLOCKS.DIRT]: { color: '#5d4037', solid: true, name: '泥土' },
[BLOCKS.GRASS]: { color: '#4caf50', solid: true, name: '草方块', topColor: '#4caf50' },
[BLOCKS.STONE]: { color: '#757575', solid: true, name: '石头' },
[BLOCKS.WOOD]: { color: '#795548', solid: true, name: '木头' },
[BLOCKS.LEAVES]:{ color: '#2e7d32', solid: true, name: '树叶' },
[BLOCKS.BRICK]: { color: '#a1887f', solid: true, name: '砖块' },
[BLOCKS.SAND]: { color: '#fdd835', solid: true, name: '沙子' },
[BLOCKS.GLASS]: { color: 'rgba(200, 240, 255, 0.3)', solid: true, name: '玻璃', transparent: true },
[BLOCKS.BEDROCK]:{ color: '#212121', solid: true, name: '基岩' }
};
// 玩家初始物品栏
const INVENTORY = [
BLOCKS.DIRT, BLOCKS.GRASS, BLOCKS.STONE, BLOCKS.WOOD,
BLOCKS.LEAVES, BLOCKS.BRICK, BLOCKS.SAND, BLOCKS.GLASS
];
// --- 游戏状态 ---
const state = {
canvas: null,
ctx: null,
world: [], // 二维数组 [x][y]
camera: { x: 0, y: 0 },
player: {
x: 0, y: 0,
vx: 0, vy: 0,
width: 20, height: 56, // 略小于2个格子高
grounded: false,
selectedSlot: 0
},
keys: {},
mouse: { x: 0, y: 0, leftDown: false, rightDown: false },
lastTime: 0
};
// --- 初始化 ---
window.onload = () => {
state.canvas = document.getElementById('gameCanvas');
state.ctx = state.canvas.getContext('2d');
resize();
window.addEventListener('resize', resize);
initInput();
generateWorld();
initUI();
document.getElementById('loading').style.display = 'none';
requestAnimationFrame(gameLoop);
};
function resize() {
state.canvas.width = window.innerWidth;
state.canvas.height = window.innerHeight;
state.ctx.imageSmoothingEnabled = false; // 保持像素风
}
// --- 世界生成 ---
function generateWorld() {
state.world = new Array(CHUNK_WIDTH).fill(null).map(() => new Array(CHUNK_HEIGHT).fill(BLOCKS.AIR));
// 简单的地形生成算法 (正弦波叠加)
const surfaceLevel = [];
for (let x = 0; x < CHUNK_WIDTH; x++) {
// 基础高度 + 噪声
let h = 20 + Math.sin(x * 0.05) * 5 + Math.sin(x * 0.1) * 2;
surfaceLevel[x] = Math.floor(h);
}
for (let x = 0; x < CHUNK_WIDTH; x++) {
let groundY = surfaceLevel[x];
for (let y = 0; y < CHUNK_HEIGHT; y++) {
if (y >= CHUNK_HEIGHT - 2) {
state.world[x][y] = BLOCKS.BEDROCK;
} else if (y > groundY + 5) {
// 深层石头
state.world[x][y] = Math.random() > 0.1 ? BLOCKS.STONE : BLOCKS.DIRT;
} else if (y > groundY) {
// 浅层泥土
state.world[x][y] = BLOCKS.DIRT;
} else if (y === groundY) {
// 地表
state.world[x][y] = BLOCKS.GRASS;
// 随机生成树木
if (x > 5 && x < CHUNK_WIDTH - 5 && Math.random() < 0.05) {
generateTree(x, y - 1);
}
}
}
}
// 设置玩家出生点
state.player.x = (CHUNK_WIDTH / 2) * TILE_SIZE;
state.player.y = (surfaceLevel[Math.floor(CHUNK_WIDTH / 2)] - 3) * TILE_SIZE;
}
function generateTree(x, y) {
const height = 3 + Math.floor(Math.random() * 3);
// 树干
for (let i = 0; i < height; i++) {
if (y - i >= 0) state.world[x][y - i] = BLOCKS.WOOD;
}
// 树叶
for (let lx = x - 2; lx <= x + 2; lx++) {
for (let ly = y - height - 2; ly <= y - height; ly++) {
if (lx >= 0 && lx < CHUNK_WIDTH && ly >= 0 && Math.abs(lx - x) + Math.abs(ly - (y - height)) < 4) {
if (state.world[lx][ly] === BLOCKS.AIR) {
state.world[lx][ly] = BLOCKS.LEAVES;
}
}
}
}
}
// --- 输入处理 ---
function initInput() {
window.addEventListener('keydown', e => {
state.keys[e.code] = true;
// 物品栏快捷键 1-9
if (e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key) - 1;
if (idx < INVENTORY.length) {
state.player.selectedSlot = idx;
updateHotbarUI();
}
}
if (e.code === 'KeyR') {
generateWorld();
}
});
window.addEventListener('keyup', e => state.keys[e.code] = false);
state.canvas.addEventListener('mousemove', e => {
const rect = state.canvas.getBoundingClientRect();
state.mouse.x = e.clientX - rect.left;
state.mouse.y = e.clientY - rect.top;
});
state.canvas.addEventListener('mousedown', e => {
if (e.button === 0) state.mouse.leftDown = true;
if (e.button === 2) state.mouse.rightDown = true;
handleInteraction();
});
state.canvas.addEventListener('mouseup', e => {
if (e.button === 0) state.mouse.leftDown = false;
if (e.button === 2) state.mouse.rightDown = false;
});
// 阻止右键菜单
state.canvas.addEventListener('contextmenu', e => e.preventDefault());
}
// --- 游戏逻辑 ---
function update(dt) {
// 1. 玩家物理
// 水平移动
if (state.keys['KeyA'] || state.keys['ArrowLeft']) state.player.vx = -PLAYER_SPEED;
else if (state.keys['KeyD'] || state.keys['ArrowRight']) state.player.vx = PLAYER_SPEED;
else state.player.vx = 0;
// 跳跃
if ((state.keys['KeyW'] || state.keys['ArrowUp'] || state.keys['Space']) && state.player.grounded) {
state.player.vy = -JUMP_FORCE;
state.player.grounded = false;
}
// 重力
state.player.vy += GRAVITY;
if (state.player.vy > TERMINAL_VELOCITY) state.player.vy = TERMINAL_VELOCITY;
// 应用速度并检测碰撞
movePlayer(state.player.vx, 0); // X轴
movePlayer(0, state.player.vy); // Y轴
// 2. 相机跟随
// 目标相机位置
const targetCamX = state.player.x - state.canvas.width / 2 + state.player.width / 2;
const targetCamY = state.player.y - state.canvas.height / 2 + state.player.height / 2;
// 平滑插值
state.camera.x += (targetCamX - state.camera.x) * 0.1;
state.camera.y += (targetCamY - state.camera.y) * 0.1;
// 限制相机范围
state.camera.x = Math.max(0, Math.min(state.camera.x, CHUNK_WIDTH * TILE_SIZE - state.canvas.width));
state.camera.y = Math.max(0, Math.min(state.camera.y, CHUNK_HEIGHT * TILE_SIZE - state.canvas.height));
// 3. 持续交互 (按住鼠标时)
if (state.mouse.leftDown || state.mouse.rightDown) {
handleInteraction();
}
}
function movePlayer(vx, vy) {
const p = state.player;
p.x += vx;
checkCollision(p, 'x');
p.y += vy;
p.grounded = false; // 假设在空中,除非碰撞检测证明在地面
checkCollision(p, 'y');
}
function checkCollision(p, axis) {
// 计算玩家占据的网格坐标范围
const startX = Math.floor(p.x / TILE_SIZE);
const endX = Math.floor((p.x + p.width - 0.1) / TILE_SIZE);
const startY = Math.floor(p.y / TILE_SIZE);
const endY = Math.floor((p.y + p.height - 0.1) / TILE_SIZE);
for (let y = startY; y <= endY; y++) {
for (let x = startX; x <= endX; x++) {
if (x < 0 || x >= CHUNK_WIDTH || y < 0 || y >= CHUNK_HEIGHT) continue;
const block = state.world[x][y];
if (BLOCK_PROPS[block].solid) {
if (axis === 'x') {
if (p.vx > 0) { // 向右撞
p.x = x * TILE_SIZE - p.width;
} else if (p.vx < 0) { // 向左撞
p.x = (x + 1) * TILE_SIZE;
}
p.vx = 0;
} else { // axis === 'y'
if (p.vy > 0) { // 向下落
p.y = y * TILE_SIZE - p.height;
p.grounded = true;
p.vy = 0;
} else if (p.vy < 0) { // 向上顶
p.y = (y + 1) * TILE_SIZE;
p.vy = 0;
}
}
return; // 解决了一个方向的碰撞就返回,防止卡住
}
}
}
}
function handleInteraction() {
// 计算鼠标在世界中的坐标
const worldMX = state.mouse.x + state.camera.x;
const worldMY = state.mouse.y + state.camera.y;
const tx = Math.floor(worldMX / TILE_SIZE);
const ty = Math.floor(worldMY / TILE_SIZE);
// 检查边界
if (tx < 0 || tx >= CHUNK_WIDTH || ty < 0 || ty >= CHUNK_HEIGHT) return;
// 距离检查 (只能操作附近方块)
const dist = Math.hypot((state.player.x + state.player.width/2) - (tx * TILE_SIZE + TILE_SIZE/2),
(state.player.y + state.player.height/2) - (ty * TILE_SIZE + TILE_SIZE/2));
if (dist > TILE_SIZE * 4) return;
if (state.mouse.leftDown) {
// 破坏方块
if (state.world[tx][ty] !== BLOCKS.BEDROCK) {
state.world[tx][ty] = BLOCKS.AIR;
}
} else if (state.mouse.rightDown) {
// 放置方块
if (state.world[tx][ty] === BLOCKS.AIR) {
// 检查是否与玩家重叠
const p = state.player;
const blockRect = { x: tx * TILE_SIZE, y: ty * TILE_SIZE, w: TILE_SIZE, h: TILE_SIZE };
if (!(p.x < blockRect.x + blockRect.w &&
p.x + p.width > blockRect.x &&
p.y < blockRect.y + blockRect.h &&
p.y + p.height > blockRect.y)) {
const blockToPlace = INVENTORY[state.player.selectedSlot];
state.world[tx][ty] = blockToPlace;
}
}
}
}
// --- 渲染 ---
function draw() {
// 清空画布
state.ctx.fillStyle = '#87CEEB';
state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height);
state.ctx.save();
state.ctx.translate(-Math.floor(state.camera.x), -Math.floor(state.camera.y));
// 视锥剔除:只绘制屏幕内的方块
const startCol = Math.floor(state.camera.x / TILE_SIZE);
const endCol = startCol + (state.canvas.width / TILE_SIZE) + 1;
const startRow = Math.floor(state.camera.y / TILE_SIZE);
const endRow = startRow + (state.canvas.height / TILE_SIZE) + 1;
for (let x = startCol; x <= endCol; x++) {
for (let y = startRow; y <= endRow; y++) {
if (x >= 0 && x < CHUNK_WIDTH && y >= 0 && y < CHUNK_HEIGHT) {
const blockId = state.world[x][y];
if (blockId !== BLOCKS.AIR) {
drawBlock(x, y, blockId);
}
}
}
}
// 绘制玩家
drawPlayer();
// 绘制鼠标高亮框
const worldMX = state.mouse.x + state.camera.x;
const worldMY = state.mouse.y + state.camera.y;
const tx = Math.floor(worldMX / TILE_SIZE);
const ty = Math.floor(worldMY / TILE_SIZE);
state.ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
state.ctx.lineWidth = 2;
state.ctx.strokeRect(tx * TILE_SIZE, ty * TILE_SIZE, TILE_SIZE, TILE_SIZE);
state.ctx.restore();
}
function drawBlock(x, y, id) {
const prop = BLOCK_PROPS[id];
const px = x * TILE_SIZE;
const py = y * TILE_SIZE;
state.ctx.fillStyle = prop.color;
state.ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
// 简单的纹理效果
if (id === BLOCKS.GRASS) {
state.ctx.fillStyle = '#5d4037'; // 泥土部分
state.ctx.fillRect(px, py + 10, TILE_SIZE, TILE_SIZE - 10);
state.ctx.fillStyle = '#4caf50'; // 草皮
state.ctx.fillRect(px, py, TILE_SIZE, 10);
} else if (id === BLOCKS.BRICK) {
state.ctx.fillStyle = 'rgba(0,0,0,0.1)';
state.ctx.fillRect(px, py + 10, TILE_SIZE, 2);
state.ctx.fillRect(px + 20, py, 2, 10);
state.ctx.fillRect(px + 10, py + 12, 2, 28);
} else if (id === BLOCKS.WOOD) {
state.ctx.fillStyle = 'rgba(0,0,0,0.1)';
state.ctx.fillRect(px + 5, py, 4, TILE_SIZE);
state.ctx.fillRect(px + 25, py, 4, TILE_SIZE);
} else if (id === BLOCKS.LEAVES) {
state.ctx.fillStyle = 'rgba(0,0,0,0.1)';
state.ctx.fillRect(px+5, py+5, 10, 10);
state.ctx.fillRect(px+20, py+20, 10, 10);
}
// 边框
state.ctx.strokeStyle = 'rgba(0,0,0,0.1)';
state.ctx.lineWidth = 1;
state.ctx.strokeRect(px, py, TILE_SIZE, TILE_SIZE);
}
function drawPlayer() {
const p = state.player;
// 身体
state.ctx.fillStyle = '#3b82f6'; // 蓝色衣服
state.ctx.fillRect(p.x, p.y + 20, p.width, p.height - 20);
// 头
state.ctx.fillStyle = '#fca5a5'; // 肤色
state.ctx.fillRect(p.x + 2, p.y, p.width - 4, 20);
// 眼睛
state.ctx.fillStyle = 'white';
state.ctx.fillRect(p.x + 4, p.y + 6, 4, 4);
state.ctx.fillRect(p.x + 12, p.y + 6, 4, 4);
state.ctx.fillStyle = 'black';
state.ctx.fillRect(p.x + 6, p.y + 6, 2, 2);
state.ctx.fillRect(p.x + 14, p.y + 6, 2, 2);
// 腿
state.ctx.fillStyle = '#1e3a8a'; // 深色裤子
state.ctx.fillRect(p.x + 2, p.y + p.height - 15, 6, 15);
state.ctx.fillRect(p.x + 12, p.y + p.height - 15, 6, 15);
}
// --- UI 管理 ---
function initUI() {
const hotbar = document.getElementById('hotbar');
hotbar.innerHTML = '';
INVENTORY.forEach((blockId, index) => {
const slot = document.createElement('div');
slot.className = `slot ${index === 0 ? 'active' : ''}`;
slot.onclick = () => {
state.player.selectedSlot = index;
updateHotbarUI();
};
// 创建方块预览
const preview = document.createElement('div');
preview.style.width = '32px';
preview.style.height = '32px';
preview.className = `block-${getBlockName(blockId).toLowerCase()}`;
if(blockId === BLOCKS.GRASS) preview.className = 'block-grass'; // 特殊处理
const num = document.createElement('span');
num.className = 'slot-number';
num.innerText = index + 1;
slot.appendChild(preview);
slot.appendChild(num);
hotbar.appendChild(slot);
});
}
function updateHotbarUI() {
const slots = document.querySelectorAll('.slot');
slots.forEach((slot, idx) => {
if (idx === state.player.selectedSlot) slot.classList.add('active');
else slot.classList.remove('active');
});
}
function getBlockName(id) {
return Object.keys(BLOCKS).find(key => BLOCKS[key] === id);
}
// --- 主循环 ---
function gameLoop(timestamp) {
const dt = timestamp - state.lastTime;
state.lastTime = timestamp;
update(dt);
draw();
requestAnimationFrame(gameLoop);
}
</script>
</body>
</html>