欢迎光临
我们一直在努力

不会写游戏的前端不是好博主!用《植物大战僵尸》案例打通你的JS任督二脉 一个项目学会:DOM操作、事件处理、游戏循环、状态管理等核心技能

"别人的JavaScript教程:变量、函数、数组… 我的JavaScript教程:👽向日葵生产阳光 → 🧜‍♂️豌豆射手攻击 → 😍僵尸进攻 → 🏠保卫家园!这个超有趣的实战项目。通过开发一个完整游戏,让你在不知不觉中掌握JS核心概念。就等你了!"

游戏状态变量

let gameState = {
sun: 150,
wave: 1,
maxWaves: 5,
zombiesAlive: 0,
health: 5,
gameActive: false,
selectedPlant: null,
plants: [],
zombies: [],
projectiles: [],
suns: new Map(), // 使用Map来管理阳光,更容易查找和删除
nextZombieId: 1,
nextProjectileId: 1,
gameLoopId: null,
sunInterval: null,
waveInterval: null
};

gameState 是整个游戏的 "记忆库" 和 "控制中心",它记住游戏发生的所有事情:

四大核心功能

1. 数据存储中心(记住一切)

用户操作 → 更新gameState → UI显示
↑ ↓
更新gameState ← 游戏事件

2. 游戏逻辑依据(决策基础)

// 判断能否种植物:
function canPlacePlant(plantType) {
// 依据1:是否有足够的阳光? → 查看 gameState.sun
// 依据2:这个格子有没有植物? → 查看 gameState.plants
// 依据3:游戏是否在进行? → 查看 gameState.gameActive
}

3. UI显示来源(显示什么)

<!– UI从gameState获取数据显示 –>
<div id="sun-count">150</div> ← 显示 gameState.sun
<div id="wave-count">1/5</div> ← 显示 gameState.wave
<div id="health-count">5</div> ← 显示 gameState.health

4. 游戏同步协调(保持一致性)

// 当僵尸被豌豆击中时:
function zombieHit(zombieId, damage) {
// 1. 更新僵尸血量(游戏逻辑)
zombie.hp -= damage;

// 2. 更新UI显示(视觉效果)
updateZombieHpBar(zombieId);

// 3. 如果僵尸死亡,更新游戏状态
if (zombie.hp <= 0) {
gameState.zombiesAlive–; // 减少存活计数
removeZombieFromUI(zombieId); // 移除僵尸显示
}
}

DOM元素:

// DOM元素
const sunCountEl = document.getElementById('sun-count');
const waveCountEl = document.getElementById('wave-count');
const zombieCountEl = document.getElementById('zombie-count');
const healthCountEl = document.getElementById('health-count');
const lawnEl = document.getElementById('lawn');
const plantCards = document.querySelectorAll('.plant-card');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const messageEl = document.getElementById('message');
const messageTitleEl = document.getElementById('message-title');
const messageTextEl = document.getElementById('message-text');
const restartBtn = document.getElementById('restart-btn');

1. document

  • 作用:表示整个HTML文档

  • 含义:浏览器加载页面时会自动创建一个document对象,代表整个网页

  • 类比:就像是一本书的"目录"或"索引"

2. .getElementById('lawn')

  • 作用:在文档中查找具有指定id的元素

  • 分解:

    • getElement:获取元素

    • ById:通过id

    • ('lawn'):id值为"lawn"

  • 意思:在文档中查找id="lawn"的元素

  • 返回值:找到的元素对象,如果没找到返回null

3. const lawnEl =

  • 作用:将找到的元素赋值给变量

  • 分解:

    • const:声明一个常量(值不可改变)

    • lawnEl:变量名(自定义的,可改为其他名字)

    • =:赋值运算符

  • 意思:将找到的元素存储在lawnEl变量中

总结:"在HTML文档中找到id为'lawn'的元素,并将其引用存储到名为lawnEl的常量中"现在 lawnEl 就代表那个 <div id="lawn">

初始化草地

// 初始化草地
function initLawn() {
lawnEl.innerHTML = '';
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 9; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = row;
cell.dataset.col = col;
cell.addEventListener('click', () => placePlant(row, col));
lawnEl.appendChild(cell);
}
}
}

lawnEl.innerHTML = '':清空草坪
const cell = document.createElement('div'):创建一个"格子"(创建一个HTML的<div>元素(一个容器盒子))

cell.className = 'cell';给这个格子(div元素)起一个"类名"叫"cell",这样CSS就能给它添加样式了。

// 游戏状态变量
let gameState = {
sun: 150,
wave: 1,
maxWaves: 5,
zombiesAlive: 0,
health: 5,
gameActive: false,
selectedPlant: null,
plants: [],
zombies: [],
projectiles: [],
suns: new Map(), // 使用Map来管理阳光,更容易查找和删除
nextZombieId: 1,
nextProjectileId: 1,
gameLoopId: null,
sunInterval: null,
waveInterval: null
};

// 植物类型数据
const plantTypes = {
sunflower: {
name: "向日葵",
cost: 50,
emoji: "👽",
color: "#ffd700",
hp: 5,
maxHp: 5,
produceSun: true,
produceSunInterval: 10000,
attack: false,
attackInterval: 0
},
peashooter: {
name: "豌豆射手",
cost: 100,
emoji: "🧜‍♂️🧜‍♂️",
color: "#7cb879",
hp: 10,
maxHp: 10,
produceSun: false,
produceSunInterval: 0,
attack: true,
attackInterval: 2000
},
wallnut: {
name: "坚果墙",
cost: 50,
emoji: "🥜",
color: "#8b4513",
hp: 25,
maxHp: 25,
produceSun: false,
produceSunInterval: 0,
attack: false,
attackInterval: 0
}
};

// 僵尸类型数据
const zombieTypes = [
{ type: "normal", emoji: "😍", hp: 10, maxHp: 10, speed: 0.08, damage: 1 },
{ type: "cone", emoji: "😉😉", hp: 20, maxHp: 20, speed: 0.06, damage: 1 },
{ type: "bucket", emoji: "🤐", hp: 30, maxHp: 30, speed: 0.04, damage: 1 }
];

// DOM元素
const sunCountEl = document.getElementById('sun-count');
const waveCountEl = document.getElementById('wave-count');
const zombieCountEl = document.getElementById('zombie-count');
const healthCountEl = document.getElementById('health-count');
const lawnEl = document.getElementById('lawn');
const plantCards = document.querySelectorAll('.plant-card');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const messageEl = document.getElementById('message');
const messageTitleEl = document.getElementById('message-title');
const messageTextEl = document.getElementById('message-text');
const restartBtn = document.getElementById('restart-btn');

// ==================== 游戏初始化 ====================

// 初始化草地
function initLawn() {
lawnEl.innerHTML = '';
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 9; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = row;
cell.dataset.col = col;
cell.addEventListener('click', () => placePlant(row, col));
lawnEl.appendChild(cell);
}
}
}

// 初始化游戏
function initGame() {
gameState = {
sun: 150,
wave: 1,
maxWaves: 5,
zombiesAlive: 0,
health: 5,
gameActive: false,
selectedPlant: null,
plants: [],
zombies: [],
projectiles: [],
suns: new Map(),
nextZombieId: 1,
nextProjectileId: 1,
gameLoopId: null,
sunInterval: null,
waveInterval: null
};

updateUI();
initLawn();
clearIntervals();
hideMessage();

plantCards.forEach(card => {
card.classList.remove('selected', 'disabled');
});

startBtn.textContent = "开始游戏";
startBtn.disabled = false;
}

// ==================== 阳光系统 ====================

// 创建阳光元素
function createSun(row, col, source = 'sky') {
const sunId = `sun-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

// 创建阳光DOM元素
const sunEl = document.createElement('div');
sunEl.id = sunId;
sunEl.className = 'sun';
sunEl.textContent = '☀️';
sunEl.style.left = `${col * (100 / 9) + 5}%`;
sunEl.style.top = `${row * 20 + 10}%`;

// 设置数据属性
sunEl.dataset.sunId = sunId;
sunEl.dataset.row = row;
sunEl.dataset.col = col;
sunEl.dataset.source = source;

// 添加点击事件 – 使用事件委托
sunEl.addEventListener('click', handleSunClick);

// 添加到草地
lawnEl.appendChild(sunEl);

// 存储阳光信息
gameState.suns.set(sunId, {
id: sunId,
element: sunEl,
row: row,
col: col,
source: source,
collected: false,
timeoutId: null
});

// 设置自动消失
const timeoutId = setTimeout(() => {
removeSun(sunId);
}, 10000);

// 存储timeoutId以便清理
const sunData = gameState.suns.get(sunId);
if (sunData) {
sunData.timeoutId = timeoutId;
}

return sunId;
}

// 处理阳光点击
function handleSunClick(event) {
event.stopPropagation(); // 阻止事件冒泡

const sunEl = event.currentTarget;
const sunId = sunEl.dataset.sunId;

if (!sunId) return;

collectSun(sunId);
}

// 收集阳光
function collectSun(sunId) {
const sunData = gameState.suns.get(sunId);

if (!sunData || sunData.collected) {
return;
}

// 标记为已收集
sunData.collected = true;

// 清除自动消失的timeout
if (sunData.timeoutId) {
clearTimeout(sunData.timeoutId);
}

// 增加阳光数量
gameState.sun += 25;

// 收集动画
const sunEl = sunData.element;
sunEl.style.transform = 'scale(1.5)';
sunEl.style.opacity = '0';
sunEl.style.transition = 'all 0.3s ease-out';

// 播放音效(如果有的话)
playSunSound();

// 更新UI
updateUI();

// 延迟移除元素
setTimeout(() => {
if (sunEl && sunEl.parentNode) {
sunEl.remove();
}
// 从Map中删除
gameState.suns.delete(sunId);
}, 300);
}

// 移除阳光(超时自动消失)
function removeSun(sunId) {
const sunData = gameState.suns.get(sunId);

if (!sunData || sunData.collected) {
return;
}

const sunEl = sunData.element;

// 淡出动画
sunEl.style.opacity = '0';
sunEl.style.transition = 'opacity 0.5s';

setTimeout(() => {
if (sunEl && sunEl.parentNode) {
sunEl.remove();
}
gameState.suns.delete(sunId);
}, 500);
}

// 播放阳光收集音效(占位函数)
function playSunSound() {
// 这里可以添加音效
console.log('阳光收集音效');
}

// 创建随机阳光(天空掉落)
function createRandomSun() {
const row = Math.floor(Math.random() * 5);
const col = Math.floor(Math.random() * 9);
createSun(row, col, 'sky');
}

// 向日葵生产阳光
function sunflowerProduceSun(plant) {
const now = Date.now();
if (now – plant.lastProducedSun > plantTypes.sunflower.produceSunInterval) {
plant.lastProducedSun = now;
// 在向日葵位置创建阳光
createSun(plant.row, plant.col, 'sunflower');
}
}

// ==================== 植物系统 ====================

// 更新UI
function updateUI() {
sunCountEl.textContent = gameState.sun;
waveCountEl.textContent = `${gameState.wave}/${gameState.maxWaves}`;
zombieCountEl.textContent = gameState.zombiesAlive;
healthCountEl.textContent = gameState.health;

plantCards.forEach(card => {
const plantType = card.dataset.plant;
const cost = parseInt(card.dataset.cost);

if (gameState.sun < cost) {
card.classList.add('disabled');
} else {
card.classList.remove('disabled');
}

if (gameState.selectedPlant === plantType) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
}

// 选择植物
plantCards.forEach(card => {
card.addEventListener('click', () => {
if (card.classList.contains('disabled')) return;

const plantType = card.dataset.plant;
if (gameState.selectedPlant === plantType) {
gameState.selectedPlant = null;
} else {
gameState.selectedPlant = plantType;
}

updateUI();
});
});

// 放置植物
function placePlant(row, col) {
if (!gameState.gameActive || !gameState.selectedPlant) return;

const existingPlant = gameState.plants.find(p => p.row === row && p.col === col);
if (existingPlant) return;

const plantType = plantTypes[gameState.selectedPlant];

if (gameState.sun < plantType.cost) {
alert(`阳光不足!需要${plantType.cost}阳光,你只有${gameState.sun}阳光。`);
return;
}

gameState.sun -= plantType.cost;

const plant = {
id: `plant-${Date.now()}`,
type: gameState.selectedPlant,
row: row,
col: col,
hp: plantType.hp,
maxHp: plantType.maxHp,
lastProducedSun: plantType.produceSun ? Date.now() : null,
lastAttacked: plantType.attack ? Date.now() : null
};

gameState.plants.push(plant);

const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
const plantEl = document.createElement('div');
plantEl.id = plant.id;
plantEl.className = 'plant';
plantEl.textContent = plantType.emoji;
plantEl.style.color = plantType.color;
cell.appendChild(plantEl);
cell.classList.add('with-plant');

const hpBar = document.createElement('div');
hpBar.className = 'hp-bar';
const hpFill = document.createElement('div');
hpFill.className = 'hp-fill';
hpFill.style.width = '100%';
hpBar.appendChild(hpFill);
plantEl.appendChild(hpBar);

gameState.selectedPlant = null;
updateUI();
}

// ==================== 僵尸系统 ====================

// 豌豆射手攻击
function peashooterAttack(plant) {
const now = Date.now();
if (now – plant.lastAttacked > plantTypes.peashooter.attackInterval) {
plant.lastAttacked = now;

const zombiesInRow = gameState.zombies.filter(z => z.row === plant.row && z.col > plant.col);
if (zombiesInRow.length === 0) return;

const projectile = {
id: `projectile-${gameState.nextProjectileId++}`,
row: plant.row,
col: plant.col + 0.5,
damage: 2,
speed: 0.15
};

gameState.projectiles.push(projectile);

const projectileEl = document.createElement('div');
projectileEl.id = projectile.id;
projectileEl.className = 'projectile';
projectileEl.style.top = `${plant.row * 20 + 10}%`;
projectileEl.style.left = `${(plant.col + 0.5) * (100 / 9)}%`;

lawnEl.appendChild(projectileEl);
}
}

// 移动抛射物
function moveProjectile(projectile) {
const projectileEl = document.getElementById(projectile.id);
if (!projectileEl) return;

projectile.col += projectile.speed * 0.05;
projectileEl.style.left = `${projectile.col * (100 / 9)}%`;

const hitZombie = gameState.zombies.find(z =>
z.row === projectile.row &&
Math.abs(z.col – projectile.col) < 0.3
);

if (hitZombie) {
hitZombie.hp -= projectile.damage;

const projectileIndex = gameState.projectiles.findIndex(p => p.id === projectile.id);
if (projectileIndex > -1) {
gameState.projectiles.splice(projectileIndex, 1);
}

if (projectileEl) {
projectileEl.style.backgroundColor = '#ff0000';
projectileEl.style.transform = 'scale(1.5)';

setTimeout(() => {
if (projectileEl) projectileEl.remove();
}, 100);
}

if (hitZombie.hp <= 0) {
removeZombie(hitZombie.id);
}

return;
}

if (projectile.col > 9) {
const projectileIndex = gameState.projectiles.findIndex(p => p.id === projectile.id);
if (projectileIndex > -1) {
gameState.projectiles.splice(projectileIndex, 1);
}

if (projectileEl) {
projectileEl.remove();
}
}
}

// 创建僵尸
function createZombie() {
const row = Math.floor(Math.random() * 5);
let zombieType;

if (gameState.wave > 4) {
zombieType = zombieTypes[2];
} else if (gameState.wave > 2) {
zombieType = zombieTypes[1];
} else {
zombieType = zombieTypes[0];
}

const zombie = {
id: `zombie-${gameState.nextZombieId++}`,
type: zombieType.type,
row: row,
col: 8.5,
hp: zombieType.hp,
maxHp: zombieType.maxHp,
speed: zombieType.speed,
damage: zombieType.damage,
isAttacking: false,
moveInterval: null
};

gameState.zombies.push(zombie);
gameState.zombiesAlive++;

const zombieEl = document.createElement('div');
zombieEl.id = zombie.id;
zombieEl.className = 'zombie';
zombieEl.textContent = zombieType.emoji;
zombieEl.style.top = `${row * 20}%`;
zombieEl.style.right = `0%`;

const hpBar = document.createElement('div');
hpBar.className = 'zombie-hp-bar';
const hpFill = document.createElement('div');
hpFill.className = 'zombie-hp-fill';
hpFill.style.width = '100%';
hpBar.appendChild(hpFill);
zombieEl.appendChild(hpBar);

lawnEl.appendChild(zombieEl);

startZombieMovement(zombie);
updateUI();
}

// 开始僵尸移动
function startZombieMovement(zombie) {
if (zombie.moveInterval) {
clearInterval(zombie.moveInterval);
}

zombie.moveInterval = setInterval(() => {
if (!gameState.gameActive || zombie.hp <= 0) {
clearInterval(zombie.moveInterval);
return;
}
moveZombieStep(zombie);
}, 100);
}

// 僵尸单步移动
function moveZombieStep(zombie) {
const zombieEl = document.getElementById(zombie.id);
if (!zombieEl) return;

const plantInFront = gameState.plants.find(p => {
return p.row === zombie.row &&
Math.abs(p.col – zombie.col) < 0.5;
});

if (plantInFront && !zombie.isAttacking) {
zombie.isAttacking = true;
clearInterval(zombie.moveInterval);
attackPlant(zombie, plantInFront);
} else if (!plantInFront) {
if (zombie.isAttacking) {
zombie.isAttacking = false;
startZombieMovement(zombie);
}

zombie.col -= zombie.speed * 0.05;

if (zombie.col < 0) {
clearInterval(zombie.moveInterval);
zombieReachedHouse(zombie);
return;
}

const rightPos = (8.5 – zombie.col) * (100 / 9);
zombieEl.style.right = `${rightPos}%`;

const plantAhead = gameState.plants.find(p => {
return p.row === zombie.row &&
Math.abs(p.col – zombie.col) < 0.5;
});

if (plantAhead && !zombie.isAttacking) {
zombie.isAttacking = true;
clearInterval(zombie.moveInterval);
attackPlant(zombie, plantAhead);
}
}
}

// 僵尸攻击植物
function attackPlant(zombie, plant) {
if (!gameState.gameActive || zombie.hp <= 0 || plant.hp <= 0) {
zombie.isAttacking = false;
startZombieMovement(zombie);
return;
}

plant.hp -= zombie.damage;
updatePlantHpDisplay(plant);

const plantEl = document.getElementById(plant.id);
if (plantEl) {
plantEl.style.transform = 'scale(1.1)';
setTimeout(() => {
if (plantEl) plantEl.style.transform = 'scale(1)';
}, 200);
}

if (plant.hp <= 0) {
removePlant(plant.id);
zombie.isAttacking = false;

setTimeout(() => {
if (gameState.gameActive && zombie.hp > 0) {
startZombieMovement(zombie);
}
}, 500);
return;
}

setTimeout(() => {
if (gameState.gameActive && zombie.hp > 0 && plant.hp > 0) {
attackPlant(zombie, plant);
} else {
zombie.isAttacking = false;
if (gameState.gameActive && zombie.hp > 0) {
startZombieMovement(zombie);
}
}
}, 1000);
}

// 更新植物血量显示
function updatePlantHpDisplay(plant) {
const plantEl = document.getElementById(plant.id);
if (!plantEl) return;

const hpBar = plantEl.querySelector('.hp-bar .hp-fill');
if (hpBar) {
const hpPercent = (plant.hp / plant.maxHp) * 100;
hpBar.style.width = `${hpPercent}%`;

if (hpPercent > 60) {
hpBar.style.backgroundColor = '#00ff00';
} else if (hpPercent > 30) {
hpBar.style.backgroundColor = '#ffff00';
} else {
hpBar.style.backgroundColor = '#ff0000';
}
}
}

// 僵尸到达房屋
function zombieReachedHouse(zombie) {
gameState.health–;
removeZombie(zombie.id);
updateUI();

if (gameState.health <= 0) {
gameOver(false);
}
}

// 移除植物
function removePlant(plantId) {
const plantIndex = gameState.plants.findIndex(p => p.id === plantId);
if (plantIndex === -1) return;

const plant = gameState.plants[plantIndex];
gameState.plants.splice(plantIndex, 1);

const plantEl = document.getElementById(plantId);
if (plantEl) {
plantEl.style.transform = 'scale(0.5)';
plantEl.style.opacity = '0.5';

setTimeout(() => {
if (plantEl) {
plantEl.remove();
const cell = document.querySelector(`.cell[data-row="${plant.row}"][data-col="${plant.col}"]`);
if (cell) {
cell.classList.remove('with-plant');
}
}
}, 300);
}
}

// 移除僵尸
function removeZombie(zombieId) {
const zombieIndex = gameState.zombies.findIndex(z => z.id === zombieId);
if (zombieIndex === -1) return;

const zombie = gameState.zombies[zombieIndex];

if (zombie.moveInterval) {
clearInterval(zombie.moveInterval);
}

gameState.zombies.splice(zombieIndex, 1);
gameState.zombiesAlive–;

const zombieEl = document.getElementById(zombieId);
if (zombieEl) {
zombieEl.style.transform = 'rotate(90deg)';
zombieEl.style.opacity = '0.5';

setTimeout(() => {
if (zombieEl) {
zombieEl.remove();
}
}, 500);
}

if (gameState.zombiesAlive === 0 && gameState.wave >= gameState.maxWaves) {
setTimeout(() => {
if (gameState.zombiesAlive === 0 && gameState.health > 0) {
gameOver(true);
}
}, 2000);
}

updateUI();
}

// ==================== 游戏控制 ====================

// 开始游戏
startBtn.addEventListener('click', () => {
if (!gameState.gameActive) {
startGame();
} else {
pauseGame();
}
});

// 开始游戏
function startGame() {
gameState.gameActive = true;
startBtn.textContent = "暂停游戏";

startSunProduction();
startWave();
gameLoop();
}

// 暂停游戏
function pauseGame() {
gameState.gameActive = false;
startBtn.textContent = "继续游戏";

clearIntervals();
if (gameState.gameLoopId) {
cancelAnimationFrame(gameState.gameLoopId);
gameState.gameLoopId = null;
}
}

// 开始产生天空阳光
function startSunProduction() {
if (gameState.sunInterval) clearInterval(gameState.sunInterval);

gameState.sunInterval = setInterval(() => {
if (!gameState.gameActive) return;
createRandomSun();
}, 8000);
}

// 开始一波僵尸
function startWave() {
if (gameState.wave > gameState.maxWaves) return;

const zombiesInWave = Math.min(2 + gameState.wave, 6);

for (let i = 0; i < zombiesInWave; i++) {
const delay = i * 3000 + Math.random() * 2000;
setTimeout(() => {
if (!gameState.gameActive) return;
createZombie();
}, delay);
}

if (gameState.wave < gameState.maxWaves) {
const nextWaveDelay = zombiesInWave * 4000 + 10000;
gameState.waveInterval = setTimeout(() => {
if (!gameState.gameActive) return;
gameState.wave++;
updateUI();
startWave();
}, nextWaveDelay);
}
}

// 游戏主循环
function gameLoop() {
if (!gameState.gameActive) return;

gameState.plants.forEach(plant => {
if (plant.type === 'sunflower') {
sunflowerProduceSun(plant);
} else if (plant.type === 'peashooter') {
peashooterAttack(plant);
}
});

gameState.projectiles.forEach(projectile => {
moveProjectile(projectile);
});

gameState.gameLoopId = requestAnimationFrame(() => gameLoop());
}

// 游戏结束
function gameOver(isWin) {
gameState.gameActive = false;
clearIntervals();

if (gameState.gameLoopId) {
cancelAnimationFrame(gameState.gameLoopId);
gameState.gameLoopId = null;
}

gameState.zombies.forEach(zombie => {
if (zombie.moveInterval) {
clearInterval(zombie.moveInterval);
}
});

if (isWin) {
messageTitleEl.textContent = "恭喜!";
messageTextEl.textContent = `你成功抵御了${gameState.maxWaves}波僵尸的攻击!`;
} else {
messageTitleEl.textContent = "游戏结束";
messageTextEl.textContent = "僵尸攻破了你的防线!";
}

showMessage();
}

// 显示消息
function showMessage() {
messageEl.style.display = 'block';
}

// 隐藏消息
function hideMessage() {
messageEl.style.display = 'none';
}

// 清除所有间隔
function clearIntervals() {
if (gameState.waveInterval) clearTimeout(gameState.waveInterval);
if (gameState.sunInterval) clearInterval(gameState.sunInterval);

// 清理所有阳光的timeout
gameState.suns.forEach(sunData => {
if (sunData.timeoutId) {
clearTimeout(sunData.timeoutId);
}
});

gameState.zombies.forEach(zombie => {
if (zombie.moveInterval) {
clearInterval(zombie.moveInterval);
}
});
}

// 重置游戏
resetBtn.addEventListener('click', initGame);

// 重新开始游戏
restartBtn.addEventListener('click', () => {
hideMessage();
initGame();
});

// 初始化游戏
initGame();

逐部分解析(从简单到复杂)

1. 游戏状态变量 (gameState)

let gameState = {
sun: 150, // 当前阳光数量
wave: 1, // 当前波数
health: 5, // 家园血量
gameActive: false, // 游戏是否进行中
// … 其他状态
};

比喻:就像一个记分牌,记录游戏的当前情况。

2. 变量声明 (let, const)

  • let:可以改变的值(如分数、血量)

  • const:固定不变的值(如植物类型数据)

3. DOM元素(连接HTML和JavaScript)

// 获取HTML元素
const sunCountEl = document.getElementById('sun-count');
// sunCountEl 现在代表HTML中的阳光计数显示

4. 基本的数据结构

对象(Object) – 描述一个事物的多个属性:

const plantTypes = {
sunflower: {
name: "向日葵",
cost: 50, // 种植需要50阳光
emoji: "👽", // 显示的表情
hp: 5 // 血量
}
};

数组(Array) – 列表:

const zombieTypes = [
{ type: "normal", emoji: "😍", hp: 10 }, // 第一项
{ type: "cone", emoji: "😉😉", hp: 20 } // 第二项
];

5. 函数(Function) – 可重复使用的代码块

定义一个函数:

function updateUI() {
sunCountEl.textContent = gameState.sun; // 更新显示
}

调用函数:

updateUI(); // 执行这个函数

6. 事件监听(点击按钮、选择植物等)

给按钮添加点击事件:

startBtn.addEventListener('click', () => {
// 当按钮被点击时,执行这里的代码
if (!gameState.gameActive) {
startGame();
}
});

核心游戏机制解析

1. 种植植物的流程:

点击植物卡片 → 选择植物 → 点击草地格子 → 检查阳光 → 扣除阳光 → 创建植物

对应代码:

function placePlant(row, col) {
// 1. 检查游戏是否进行中
if (!gameState.gameActive || !gameState.selectedPlant) return;

// 2. 检查这个格子是否有植物
const existingPlant = gameState.plants.find(p => p.row === row && p.col === col);
if (existingPlant) return;

// 3. 检查阳光是否足够
if (gameState.sun < plantType.cost) {
alert(`阳光不足!`);
return;
}

// 4. 扣除阳光,创建植物
gameState.sun -= plantType.cost;
// … 创建植物对象和显示元素
}

2. 游戏循环(核心)

游戏不断检查:

  • 植物是否需要生产阳光

  • 豌豆射手是否需要攻击

  • 抛射物是否需要移动

  • 僵尸是否需要移动

function gameLoop() {
if (!gameState.gameActive) return; // 如果游戏暂停,停止循环

// 检查所有植物
gameState.plants.forEach(plant => {
if (plant.type === 'sunflower') {
sunflowerProduceSun(plant); // 向日葵生产阳光
} else if (plant.type === 'peashooter') {
peashooterAttack(plant); // 豌豆射手攻击
}
});

// 继续下一帧循环
gameState.gameLoopId = requestAnimationFrame(() => gameLoop());
}

赞(0)
未经允许不得转载:171主机测评 » 不会写游戏的前端不是好博主!用《植物大战僵尸》案例打通你的JS任督二脉 一个项目学会:DOM操作、事件处理、游戏循环、状态管理等核心技能
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址