You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Canvas僵尸射击游戏添加昵称输入与重新开始功能?

僵尸射击游戏功能扩展实现

需求

我通过YouTube教程制作了一款Canvas僵尸射击游戏,目前运行正常,希望实现以下功能:

  • 游戏开始前支持输入昵称,点击开始按钮后启动游戏,并在屏幕中上侧显示昵称;
  • 游戏结束后弹出包含分数与重新开始按钮的窗口,点击按钮可重新开始游戏。

修改后的完整代码

HTML 文件

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>僵尸射击游戏</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- 开始界面 -->
    <div class="start-screen">
        <h1>僵尸射击游戏</h1>
        <input type="text" id="nicknameInput" placeholder="请输入你的昵称" required>
        <button id="startBtn">开始游戏</button>
    </div>

    <!-- 游戏画布 -->
    <canvas id="canvas1"></canvas>
    <canvas id="collisionCanvas"></canvas>

    <!-- 自定义光标 -->
    <div class="inner-cursor"></div>
    <div class="outer-cursor"></div>

    <!-- 游戏结束弹窗 -->
    <div class="game-over-modal" id="gameOverModal">
        <div class="modal-content">
            <h2>游戏结束</h2>
            <p>你的昵称: <span id="modalNickname"></span></p>
            <p>最终得分: <span id="modalScore"></span></p>
            <button id="restartBtn">重新开始</button>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

CSS 文件

*{
    cursor: none;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
canvas{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-image: url('board-bg.jpg');
}

#collisionCanvas{
    opacity: 0;
}

.inner-cursor{
    position: fixed;
    width: 15px;
    height: 15px;
    transform: translate(-50%, -50%);
    background-color: white !important;
    mix-blend-mode: difference;
    border-radius: 50%;
    pointer-events: none;
}

.outer-cursor{
    position: fixed;
    width: 70px;
    height: 70px;
    transform: translate(-50%, -50%);
    border: 5px solid white;
    mix-blend-mode: difference;
    border-radius: 50%;
    pointer-events: none;
}

/* 开始界面样式 */
.start-screen{
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: rgba(0,0,0,0.8);
    padding: 40px;
    border-radius: 10px;
    text-align: center;
    z-index: 100;
}
.start-screen h1{
    color: white;
    margin-bottom: 20px;
    font-size: 3rem;
}
.start-screen input{
    padding: 10px 15px;
    font-size: 1.2rem;
    margin-bottom: 20px;
    border-radius: 5px;
    border: none;
    outline: none;
}
.start-screen button{
    padding: 12px 30px;
    font-size: 1.2rem;
    background-color: #ff4444;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
.start-screen button:hover{
    background-color: #cc0000;
}

/* 游戏结束弹窗样式 */
.game-over-modal{
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.9);
    display: none;
    justify-content: center;
    align-items: center;
    z-index: 100;
}
.modal-content{
    background-color: #333;
    padding: 40px;
    border-radius: 10px;
    text-align: center;
    color: white;
}
.modal-content h2{
    font-size: 2.5rem;
    margin-bottom: 20px;
    color: #ff4444;
}
.modal-content p{
    font-size: 1.5rem;
    margin: 10px 0;
}
.modal-content button{
    margin-top: 20px;
    padding: 12px 30px;
    font-size: 1.2rem;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
.modal-content button:hover{
    background-color: #45a049;
}

JavaScript 文件

const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const collisionCanvas = document.getElementById('collisionCanvas');
const collisionCtx = collisionCanvas.getContext('2d');
collisionCanvas.width = window.innerWidth;
collisionCanvas.height = window.innerHeight;

// DOM元素
const startScreen = document.querySelector('.start-screen');
const nicknameInput = document.getElementById('nicknameInput');
const startBtn = document.getElementById('startBtn');
const gameOverModal = document.getElementById('gameOverModal');
const modalNickname = document.getElementById('modalNickname');
const modalScore = document.getElementById('modalScore');
const restartBtn = document.getElementById('restartBtn');

//游戏状态变量
let gameOver = false;
let score = 0;
let lives = 3;
let hit = false;
let clicked = false;
let playerNickname = '';
let animationId = null;

//生成设置
let timeToNextZombie = 0;
let zombieInterval = 400;
let lastTime = 0;
let zombies =[];

//鼠标
let innerCursor = document.querySelector('.inner-cursor');
let outerCursor = document.querySelector('.outer-cursor');

document.addEventListener('mousemove', moveCursor)

function moveCursor(e){
    let x = e.clientX;
    let y = e.clientY;

    innerCursor.style.left = `${x}px`;
    innerCursor.style.top = `${y}px`;
    outerCursor.style.left = `${x}px`;
    outerCursor.style.top = `${y}px`;
}

class Zombie{
    constructor(){
        this.spriteWidth = 200; //僵尸帧宽度
        this.spriteHeight = 312; //僵尸帧高度
        this.sizeModifier = Math.random()* 0.8 + 0.6; //僵尸尺寸系数
        this.width = this.spriteWidth * this.sizeModifier;
        this.height = this.spriteHeight * this.sizeModifier;
        this.x = canvas.width;
        this.y =Math.random() * (canvas.height -this.height); //僵尸在屏幕上下范围内生成
        this.directionX = Math.random() * 4 + 1; //僵尸移动速度
        if(Math.floor(Math.random() * 20) == 0) this.directionX = 10; //5%概率生成快速奔跑的僵尸
        this.markedForDeletion = false; //标记是否可删除对象
        this.image = new Image();
        this.image.src = 'walkingdead.png';
        this.frame = 0;
        this.maxFrame = 8;
        this.timeSinceFlap = 0;
        this.flapInterval = Math.random()*50+50; //每帧切换间隔随机
        this.randomColors = [Math.floor(Math.random()*255), Math.floor(Math.random()*255), Math.floor(Math.random()*255)];
        this.color = 'rgb(' + this.randomColors[0] + ',' + this.randomColors[1] + ',' + this.randomColors[2] + ')';
    }

    update(deltatime){
        this.x -= this.directionX;
        this.timeSinceFlap +=deltatime;
        if (this.x < 0-this.width){
            this.markedForDeletion = true;
        }  //僵尸超出屏幕范围
        if (this.timeSinceFlap > this.flapInterval){
            if (this.frame > this.maxFrame) this.frame = 0;
            else this.frame++;
            this.timeSinceFlap = 0;
        } 
        if (this.x < 0 -this.width){
            lives-=1;
            if(lives == 0){
                gameOver = true;
                showGameOverModal();
            } 
        } 
    } 
    //绘制僵尸
    draw(){
        collisionCtx.fillStyle = this.color;
        collisionCtx.fillRect(this.x, this.y, this.width, this.height);
        ctx.drawImage(this.image, this.frame * this.spriteWidth, 0, this.spriteWidth, this.spriteHeight, this.x, this.y, this.width, this.height);
    }
}

function drawScore(){
    ctx.font = '50px Impact';
    ctx.fillStyle = 'white';
    // 绘制昵称
    ctx.textAlign = 'center';
    ctx.fillText(`玩家: ${playerNickname}`, canvas.width/2, 80);
    // 绘制分数和生命值
    ctx.textAlign = 'left';
    ctx.fillText('Score: ' + score, 55, 80);
    if(lives == 3) ctx.fillText('❤ ❤ ❤', 55, 130);
    if(lives == 2) ctx.fillText('❤ ❤', 55, 130);
    if(lives == 1) ctx.fillText('❤', 55, 130);
}

function showGameOverModal(){
    modalNickname.textContent = playerNickname;
    modalScore.textContent = score;
    gameOverModal.style.display = 'flex';
}

function resetGame(){
    // 重置所有游戏状态
    gameOver = false;
    score = 0;
    lives = 3;
    hit = false;
    clicked = false;
    zombies = [];
    timeToNextZombie = 0;
    lastTime = 0;
    // 隐藏弹窗
    gameOverModal.style.display = 'none';
    // 重启动画
    cancelAnimationFrame(animationId);
    animationId = requestAnimationFrame(animate);
}

window.addEventListener('click', function(e){
    if(gameOver) return;
    
    const detectPixelColor = collisionCtx.getImageData(e.x, e.y, 1, 1);
    const pc = detectPixelColor.data; //获取僵尸碰撞盒信息
    hit = false;
    zombies.forEach(object =>{
        if(object.randomColors[0] === pc[0] && object.randomColors[1] === pc[1] && object.randomColors[2] === pc[2]){
            object.markedForDeletion = true; //从地图中移除该对象
            hit = true;
        }
    })
    clicked = true;
});

//帧处理
function animate(timestamp) {
    if(clicked){
        if(hit) score+=12
        else score-=6;
        clicked = false;
        hit = false;
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    collisionCtx.clearRect(0, 0, canvas.width, canvas.height);
    let deltatime = timestamp - lastTime;
    lastTime = timestamp;
    timeToNextZombie += deltatime;
    if (timeToNextZombie > zombieInterval){
        zombies.push(new Zombie()); //生成新僵尸
        timeToNextZombie = 0;
        zombies.sort(function(a, b){
            return a.width - b.width; //排序使小僵尸在大僵尸后方
        })
    };
    drawScore();
    //为每个僵尸执行更新操作,object代表单个僵尸对象
    [...zombies].forEach(object => object.update(deltatime)); 
    [...zombies].forEach(object => object.draw());
    zombies = zombies.filter(object => !object.markedForDeletion); //移除超出屏幕的僵尸

    if(!gameOver) animationId = requestAnimationFrame(animate);
}

// 开始游戏按钮事件
startBtn.addEventListener('click', () => {
    const nickname = nicknameInput.value.trim();
    if(nickname){
        playerNickname = nickname;
        startScreen.style.display = 'none';
        animationId = requestAnimationFrame(animate);
    } else {
        alert('请输入昵称!');
    }
});

// 重新开始按钮事件
restartBtn.addEventListener('click', resetGame);

关键修改说明

  1. 新增开始界面:添加带昵称输入框和开始按钮的弹窗,游戏初始时显示,输入昵称并点击开始后隐藏,同时启动游戏动画。
  2. 昵称显示:在drawScore函数中新增屏幕中上侧的昵称绘制逻辑,居中显示玩家昵称。
  3. 游戏结束弹窗:创建包含昵称、最终分数和重新开始按钮的弹窗,游戏结束时自动弹出。
  4. 游戏重置功能:新增resetGame函数,重置所有游戏状态变量,关闭弹窗并重启动画循环。
  5. 事件逻辑优化:调整点击事件判断,避免游戏结束后仍响应点击;将动画启动逻辑移至开始按钮点击事件中,初始不自动运行游戏。

内容的提问来源于stack exchange,提问作者fantomx775

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 16:40:33