先放效果图
编辑
🎮 游戏玩法设计
功能说明:
双人对战:两个玩家在同一键盘上对战
移动系统:左右移动 + 跳跃(带重力物理)
攻击系统: 近战攻击,有冷却时间和范围判定
防御系统:开启护盾减少50%伤害
胜负判定:血量先归零的一方失败
操作说明
玩家1(绿色):WASD移动,F攻击,G防御
玩家2(红色):方向键移动,L攻击,K防御
🎨 像素风格美术方案
游戏采用程序化像素渲染,无需外部素材:
元素和实现方式 :
机甲角色:使用Canvas矩形拼接成像素风格机体
动画系统:4帧循环动画(待机/行走/攻击/防御/受伤)
场景背景: 渐变夜空 + 像素星星 + 远景城市剪影
特效:粒子爆炸效果 + 浮动伤害数字
UI:复古像素风格血条 + 操作提示面板
🏗️ 核心代码结构
Mecha Class(机甲类)
├── 物理属性(位置、速度、重力)
├── 战斗属性(血量、攻击力、防御状态)
├── 动画系统(状态机管理)
├── update() - 更新逻辑
├── drawPixelMecha() - 像素渲染
└── attack/defend/move - 行为方法
Particle Class(粒子特效)
FloatingText Class(浮动文字)
游戏主循环
├── 输入处理
├── 物理更新
├── 碰撞检测
├── 胜负判定
└── 渲染绘制
代码部分:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>像素风机甲对战</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Courier New', monospace;
color: #fff;
}
h1 {
margin-bottom: 10px;
text-shadow: 2px 2px 0 #e94560;
font-size: 24px;
}
#gameCanvas {
border: 4px solid #4a4a6a;
box-shadow: 0 0 20px rgba(233, 69, 96, 0.5);
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.controls {
margin-top: 15px;
display: flex;
gap: 40px;
background: #2a2a4a;
padding: 15px 30px;
border-radius: 10px;
}
.player-controls {
text-align: center;
}
.player-controls h3 {
margin-bottom: 8px;
font-size: 14px;
}
.player1 h3 { color: #4ecca3; }
.player2 h3 { color: #e94560; }
.keys {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 5px;
max-width: 150px;
}
.key {
background: #3a3a5a;
border: 2px solid #5a5a7a;
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
min-width: 40px;
}
.instructions {
margin-top: 10px;
font-size: 12px;
color: #888;
}
#gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 30px 50px;
border-radius: 15px;
text-align: center;
display: none;
border: 3px solid #e94560;
}
#gameOver h2 {
font-size: 32px;
margin-bottom: 15px;
text-shadow: 2px 2px 0 #e94560;
}
#restartBtn {
margin-top: 15px;
padding: 10px 30px;
font-size: 16px;
background: #e94560;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-family: 'Courier New', monospace;
}
#restartBtn:hover {
background: #ff6b6b;
}
</style>
</head>
<body>
<h1>⚔️ 像素风机甲对战 ⚔️</h1>
<canvas id="gameCanvas" width="800" height="450"></canvas>
<div class="controls">
<div class="player-controls player1">
<h3>🟢 玩家1 (左侧)</h3>
<div class="keys">
<span class="key">W</span>
<span class="key">A</span>
<span class="key">S</span>
<span class="key">D</span>
<span class="key">F</span>
<span class="key">G</span>
</div>
<div class="instructions">移动: WASD | 攻击: F | 防御: G</div>
</div>
<div class="player-controls player2">
<h3>🔴 玩家2 (右侧)</h3>
<div class="keys">
<span class="key">↑</span>
<span class="key">←</span>
<span class="key">↓</span>
<span class="key">→</span>
<span class="key">L</span>
<span class="key">K</span>
</div>
<div class="instructions">移动: 方向键 | 攻击: L | 防御: K</div>
</div>
</div>
<div id="gameOver">
<h2 id="winnerText"></h2>
<p>按下方按钮重新开始</p>
<button id="restartBtn">重新开始</button>
</div>
<script>
// 游戏画布和上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// 游戏常量
const GRAVITY = 0.6;
const GROUND_Y = 380;
const GAME_WIDTH = 800;
const GAME_HEIGHT = 450;
// 像素艺术风格配置
const PIXEL_SIZE = 4;
// 游戏状态
let gameRunning = true;
let particles = [];
let projectiles = [];
// 机甲类
class Mecha {
constructor(x, y, color, isPlayer1) {
this.x = x;
this.y = y;
this.width = 48;
this.height = 64;
this.color = color;
this.isPlayer1 = isPlayer1;
// 移动属性
this.vx = 0;
this.vy = 0;
this.speed = 4;
this.jumpPower = -12;
this.onGround = false;
// 战斗属性
this.maxHp = 100;
this.hp = 100;
this.attackDamage = 15;
this.isDefending = false;
this.defenseReduction = 0.5;
// 动画状态
this.facing = isPlayer1 ? 1 : -1;
this.animFrame = 0;
this.animTimer = 0;
this.state = 'idle'; // idle, walk, attack, defend, hurt
// 攻击冷却
this.attackCooldown = 0;
this.attackCooldownMax = 30;
// 受击闪烁
this.hitFlash = 0;
}
update() {
// 应用重力
this.vy += GRAVITY;
// 更新位置
this.x += this.vx;
this.y += this.vy;
// 地面碰撞
if (this.y + this.height >= GROUND_Y) {
this.y = GROUND_Y - this.height;
this.vy = 0;
this.onGround = true;
} else {
this.onGround = false;
}
// 边界限制
if (this.x < 0) this.x = 0;
if (this.x + this.width > GAME_WIDTH) this.x = GAME_WIDTH - this.width;
// 更新动画
this.animTimer++;
if (this.animTimer >= 8) {
this.animTimer = 0;
this.animFrame = (this.animFrame + 1) % 4;
}
// 更新状态
if (this.attackCooldown > 0) this.attackCooldown--;
if (this.hitFlash > 0) this.hitFlash--;
// 自动恢复防御状态
if (this.state === 'defend' && !this.isDefending) {
this.state = 'idle';
}
// 攻击状态恢复
if (this.state === 'attack' && this.attackCooldown <= 20) {
this.state = 'idle';
}
// 受伤状态恢复
if (this.state === 'hurt' && this.hitFlash <= 0) {
this.state = 'idle';
}
}
move(dx, dy) {
this.vx = dx * this.speed;
if (dx !== 0) {
this.facing = dx > 0 ? 1 : -1;
if (this.onGround && this.state !== 'attack' && this.state !== 'defend') {
this.state = 'walk';
}
} else if (this.onGround && this.state === 'walk') {
this.state = 'idle';
}
if (dy < 0 && this.onGround) {
this.vy = this.jumpPower;
this.onGround = false;
createParticles(this.x + this.width/2, this.y + this.height, 5, '#888');
}
}
attack(target) {
if (this.attackCooldown > 0 || this.isDefending) return;
this.state = 'attack';
this.attackCooldown = this.attackCooldownMax;
// 检测攻击命中
const attackRange = 80;
const distance = Math.abs((this.x + this.width/2) - (target.x + target.width/2));
if (distance < attackRange && Math.abs(this.y - target.y) < 40) {
// 创建攻击特效
const hitX = target.x + target.width/2;
const hitY = target.y + target.height/2;
createParticles(hitX, hitY, 10, '#ff6b6b');
// 计算伤害
let damage = this.attackDamage;
if (target.isDefending) {
damage *= (1 - target.defenseReduction);
createFloatingText(hitX, hitY - 20, 'BLOCK!', '#4ecca3');
} else {
target.state = 'hurt';
target.hitFlash = 10;
createFloatingText(hitX, hitY - 20, Math.floor(damage), '#ff6b6b');
}
target.hp = Math.max(0, target.hp - damage);
// 击退效果
const knockback = this.facing * 5;
target.vx = knockback;
target.vy = -3;
}
}
defend(active) {
this.isDefending = active;
if (active) {
this.state = 'defend';
this.vx = 0;
} else if (this.state === 'defend') {
this.state = 'idle';
}
}
draw() {
ctx.save();
// 受击闪烁效果
if (this.hitFlash > 0 && Math.floor(this.hitFlash / 2) % 2 === 0) {
ctx.globalAlpha = 0.5;
}
// 绘制机甲(像素风格)
this.drawPixelMecha();
// 绘制防御护盾
if (this.isDefending) {
this.drawShield();
}
ctx.restore();
// 绘制血条
this.drawHealthBar();
}
drawPixelMecha() {
const x = Math.floor(this.x);
const y = Math.floor(this.y);
const w = this.width;
const h = this.height;
const facing = this.facing;
// 身体颜色
const bodyColor = this.color;
const darkColor = this.darkenColor(bodyColor, 30);
const lightColor = this.lightenColor(bodyColor, 30);
// 动画偏移
let bobOffset = 0;
if (this.state === 'walk') {
bobOffset = Math.sin(this.animFrame * Math.PI / 2) * 3;
} else if (this.state === 'attack') {
bobOffset = -5;
}
// 绘制阴影
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x + 8, GROUND_Y - 5, w - 16, 8);
// 腿部(像素风格)
ctx.fillStyle = darkColor;
const legOffset = this.state === 'walk' ? Math.sin(this.animFrame * Math.PI / 2) * 8 : 0;
// 左腿
ctx.fillRect(x + 12 + (facing === 1 ? 0 : legOffset), y + h - 20 + bobOffset, 10, 20);
// 右腿
ctx.fillRect(x + w - 22 - (facing === 1 ? legOffset : 0), y + h - 20 + bobOffset, 10, 20);
// 身体
ctx.fillStyle = bodyColor;
ctx.fillRect(x + 8, y + 20 + bobOffset, w - 16, 30);
// 身体细节
ctx.fillStyle = lightColor;
ctx.fillRect(x + 12, y + 25 + bobOffset, w - 24, 8);
ctx.fillRect(x + 12, y + 38 + bobOffset, w - 24, 8);
// 驾驶舱/头部
ctx.fillStyle = '#2a2a4a';
ctx.fillRect(x + 16, y + 8 + bobOffset, w - 32, 16);
// 眼睛/传感器
ctx.fillStyle = this.state === 'attack' ? '#ff6b6b' : '#4ecca3';
const eyeX = facing === 1 ? x + w - 24 : x + 16;
ctx.fillRect(eyeX, y + 12 + bobOffset, 8, 6);
// 手臂
ctx.fillStyle = darkColor;
const armOffset = this.state === 'attack' ? facing * 15 : 0;
// 左臂
ctx.fillRect(x - 4, y + 22 + bobOffset, 12, 20);
// 右臂(攻击时有动作)
ctx.fillRect(x + w - 8 + armOffset, y + 22 + bobOffset, 12, 20);
// 武器(右臂)
ctx.fillStyle = '#888';
const weaponX = x + w - 6 + armOffset;
ctx.fillRect(weaponX, y + 30 + bobOffset, 8, 25);
// 武器发光效果
ctx.fillStyle = this.state === 'attack' ? '#ff6b6b' : '#aaa';
ctx.fillRect(weaponX + 2, y + 32 + bobOffset, 4, 15);
// 肩部装甲
ctx.fillStyle = lightColor;
ctx.fillRect(x, y + 18 + bobOffset, 12, 12);
ctx.fillRect(x + w - 12, y + 18 + bobOffset, 12, 12);
}
drawShield() {
const x = this.x - 10;
const y = this.y - 5;
const w = this.width + 20;
const h = this.height + 10;
// 护盾发光效果
ctx.strokeStyle = `rgba(78, 204, 163, ${0.5 + Math.sin(Date.now() / 200) * 0.3})`;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.roundRect(x, y, w, h, 10);
ctx.stroke();
// 护盾内部
ctx.fillStyle = 'rgba(78, 204, 163, 0.15)';
ctx.fill();
// 护盾网格
ctx.strokeStyle = 'rgba(78, 204, 163, 0.3)';
ctx.lineWidth = 1;
for (let i = 10; i < w; i += 15) {
ctx.beginPath();
ctx.moveTo(x + i, y);
ctx.lineTo(x + i, y + h);
ctx.stroke();
}
}
drawHealthBar() {
const barWidth = 60;
const barHeight = 8;
const x = this.x + (this.width - barWidth) / 2;
const y = this.y - 15;
// 背景
ctx.fillStyle = '#333';
ctx.fillRect(x, y, barWidth, barHeight);
// 血量
const hpPercent = this.hp / this.maxHp;
const hpColor = hpPercent > 0.5 ? '#4ecca3' : hpPercent > 0.25 ? '#ffa500' : '#e94560';
ctx.fillStyle = hpColor;
ctx.fillRect(x, y, barWidth * hpPercent, barHeight);
// 边框
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, barWidth, barHeight);
// 血量数字
ctx.fillStyle = '#fff';
ctx.font = '10px Courier New';
ctx.textAlign = 'center';
ctx.fillText(`${Math.ceil(this.hp)}/${this.maxHp}`, this.x + this.width/2, y - 3);
}
darkenColor(color, percent) {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = Math.max((num >> 16) - amt, 0);
const G = Math.max((num >> 8 & 0x00FF) - amt, 0);
const B = Math.max((num & 0x0000FF) - amt, 0);
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}
lightenColor(color, percent) {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = Math.min((num >> 16) + amt, 255);
const G = Math.min((num >> 8 & 0x00FF) + amt, 255);
const B = Math.min((num & 0x0000FF) + amt, 255);
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}
}
// 粒子类
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 8;
this.vy = (Math.random() - 0.5) * 8;
this.life = 30;
this.color = color;
this.size = Math.random() * 4 + 2;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.3;
this.life--;
this.size *= 0.95;
}
draw() {
ctx.fillStyle = this.color;
ctx.globalAlpha = this.life / 30;
ctx.fillRect(this.x, this.y, this.size, this.size);
ctx.globalAlpha = 1;
}
}
// 浮动文字类
class FloatingText {
constructor(x, y, text, color) {
this.x = x;
this.y = y;
this.text = text;
this.color = color;
this.life = 40;
this.vy = -1;
}
update() {
this.y += this.vy;
this.life--;
}
draw() {
ctx.fillStyle = this.color;
ctx.globalAlpha = this.life / 40;
ctx.font = 'bold 16px Courier New';
ctx.textAlign = 'center';
ctx.fillText(this.text, this.x, this.y);
ctx.globalAlpha = 1;
}
}
// 创建粒子
function createParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
particles.push(new Particle(x, y, color));
}
}
// 创建浮动文字
let floatingTexts = [];
function createFloatingText(x, y, text, color) {
floatingTexts.push(new FloatingText(x, y, text, color));
}
// 绘制背景
function drawBackground() {
// 天空渐变
const gradient = ctx.createLinearGradient(0, 0, 0, GAME_HEIGHT);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(0.5, '#2a2a4a');
gradient.addColorStop(1, '#3a3a5a');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// 像素星星
ctx.fillStyle = '#fff';
for (let i = 0; i < 50; i++) {
const x = (i * 137) % GAME_WIDTH;
const y = (i * 73) % (GAME_HEIGHT / 2);
const size = (i % 3) + 1;
ctx.fillRect(x, y, size, size);
}
// 远景城市轮廓
ctx.fillStyle = '#1a1a3a';
for (let i = 0; i < 20; i++) {
const x = i * 45;
const height = 50 + (i * 17) % 80;
ctx.fillRect(x, GROUND_Y - height, 40, height);
}
// 地面
ctx.fillStyle = '#2a2a3a';
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, GAME_HEIGHT - GROUND_Y);
// 地面像素纹理
ctx.fillStyle = '#3a3a4a';
for (let i = 0; i < GAME_WIDTH; i += 20) {
for (let j = GROUND_Y; j < GAME_HEIGHT; j += 15) {
if ((i + j) % 40 === 0) {
ctx.fillRect(i, j, 12, 8);
}
}
}
// 地面边框线
ctx.strokeStyle = '#4ecca3';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, GROUND_Y);
ctx.lineTo(GAME_WIDTH, GROUND_Y);
ctx.stroke();
}
// 输入处理
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
// 玩家1攻击
if (e.key.toLowerCase() === 'f' && gameRunning) {
player1.attack(player2);
}
// 玩家1防御
if (e.key.toLowerCase() === 'g' && gameRunning) {
player1.defend(true);
}
// 玩家2攻击
if (e.key.toLowerCase() === 'l' && gameRunning) {
player2.attack(player1);
}
// 玩家2防御
if (e.key.toLowerCase() === 'k' && gameRunning) {
player2.defend(true);
}
});
document.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
// 玩家1停止防御
if (e.key.toLowerCase() === 'g') {
player1.defend(false);
}
// 玩家2停止防御
if (e.key.toLowerCase() === 'k') {
player2.defend(false);
}
});
// 处理移动输入
function handleInput() {
// 玩家1 (WASD)
let p1Dx = 0;
let p1Dy = 0;
if (keys['a']) p1Dx = -1;
if (keys['d']) p1Dx = 1;
if (keys['w']) p1Dy = -1;
player1.move(p1Dx, p1Dy);
// 玩家2 (方向键)
let p2Dx = 0;
let p2Dy = 0;
if (keys['arrowleft']) p2Dx = -1;
if (keys['arrowright']) p2Dx = 1;
if (keys['arrowup']) p2Dy = -1;
player2.move(p2Dx, p2Dy);
}
// 检查游戏结束
function checkGameOver() {
if (player1.hp <= 0 || player2.hp <= 0) {
gameRunning = false;
const winner = player1.hp > 0 ? '玩家1 获胜!' : '玩家2 获胜!';
const winnerColor = player1.hp > 0 ? '#4ecca3' : '#e94560';
document.getElementById('winnerText').textContent = winner;
document.getElementById('winnerText').style.color = winnerColor;
document.getElementById('gameOver').style.display = 'block';
}
}
// 重置游戏
function resetGame() {
player1 = new Mecha(150, GROUND_Y - 64, '#4ecca3', true);
player2 = new Mecha(GAME_WIDTH - 200, GROUND_Y - 64, '#e94560', false);
particles = [];
floatingTexts = [];
gameRunning = true;
document.getElementById('gameOver').style.display = 'none';
}
document.getElementById('restartBtn').addEventListener('click', resetGame);
// 初始化游戏对象
let player1 = new Mecha(150, GROUND_Y - 64, '#4ecca3', true);
let player2 = new Mecha(GAME_WIDTH - 200, GROUND_Y - 64, '#e94560', false);
// 游戏主循环
function gameLoop() {
// 清空画布
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// 绘制背景
drawBackground();
if (gameRunning) {
// 处理输入
handleInput();
// 更新玩家
player1.update();
player2.update();
// 检查游戏结束
checkGameOver();
}
// 绘制玩家
player1.draw();
player2.draw();
// 更新和绘制粒子
particles = particles.filter(p => p.life > 0);
particles.forEach(p => {
p.update();
p.draw();
});
// 更新和绘制浮动文字
floatingTexts = floatingTexts.filter(t => t.life > 0);
floatingTexts.forEach(t => {
t.update();
t.draw();
});
requestAnimationFrame(gameLoop);
}
// 启动游戏
gameLoop();
</script>
</body>
</html>