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

JS贪吃蛇游戏中SnakeBody类oldDirection始终为undefined问题

SnakeBody类oldDirection始终为undefined的问题

我在JavaScript贪吃蛇游戏中尝试将oldDirection设置为当前direction的值,用来实现蛇身U型转弯的精灵动画,但目前this.direction能获取到新值,而oldDirection始终是undefined,导致无法根据方向切换蛇身精灵。

SnakeBody类代码

class SnakeBody {
    constructor(x, y, ctx) {
        this.x = x
        this.y = y
        this.oldX
        this.oldY
        this.ctx = ctx
        this.direction
        this.oldDirection
        this.sprite = new Image()
    }

    draw(headDirection) {
        this.direction = headDirection
        this.sprite.onload = () => {
            this.ctx.drawImage(this.sprite, this.x, this.y, 20, 20)
        }
    }

    update(newX, newY, newDirection) {
        this.oldDirection = this.direction
        this.direction = newDirection
        this.ctx.clearRect(this.x, this.y, 20, 20)

        console.log('This.Direction:', this.direction, 'This.oldDirection:', this.oldDirection);

        this.oldX = this.x
        this.oldY = this.y
        this.x = newX;
        this.y = newY;
        
        this.sprite.onload = () => {
            this.ctx.drawImage(this.sprite, this.x, this.y, 20, 20)
        }
    }

    setDirection(nDirection) {
        this.oldDirection = this.direction
        this.direction = nDirection
    }

    getDirection() {
        return this.direction
    }

    setSnakeSpriteDirection(newDirection) {
        switch(true) {
            case this.direction == 'DOWN' && newDirection == 'LEFT':
                this.sprite.src = 'src/assets/img/snakeBody_rotateDownLeft.png'
                break
            case this.direction == 'UP' || this.direction == 'UP' 
            && newDirection == 'DOWN' || this.direction == 'DOWN':
                this.sprite.src = 'src/assets/img/snakeBody_down.png'
                break;
            case this.direction == 'RIGHT' || this.direction == 'RIGHT' 
            && newDirection == 'LEFT' || this.direction == 'LEFT':
                this.sprite.src = 'src/assets/img/snakeBody_left.png'
                break;
            default:
                this.sprite.src = 'src/assets/img/snakeBody_left.png'
                break;
        }

        this.draw()
    }
}

Player类代码

class Player {
    constructor(x, y, name, ctx) {
        this.x = x;
        this.y = y;
        this.oldX;
        this.oldY;
        this.name = name;
        this.ctx = ctx;
        this.direction
        this.oldDirection
        this.body = []
        this.sprite = new Image()
        this.counter = 0;
    }

    draw() {
        switch (this.getDirection()) {
            case 'UP':
                this.sprite.src = 'src/assets/img/snakeHead.png'
                break;
            case 'DOWN':
                this.sprite.src = 'src/assets/img/snakeHead_down.png'
                break;
            case 'RIGHT':
                this.sprite.src = 'src/assets/img/snakeHead_right.png'
                break;
            case 'LEFT':
                this.sprite.src = 'src/assets/img/snakeHead_left.png'
                break;
            default: 
                this.sprite.src = 'src/assets/img/snakeHead.png'
                break;
        }

        this.sprite.onload = () => {
            this.ctx.drawImage(this.sprite, this.x, this.y, 20, 20)
        }
    }

    update(newX, newY) {
        this.ctx.clearRect(this.x, this.y, 20, 20)

        this.x = newX;
        this.y = newY;
    }

    updateBody() {
        if (this.body.length > 0) {
            this.body.forEach((bodyPart, index) => {
                
                if( index == 0 ) {
                    bodyPart.update(this.x, this.y, this.oldDirection)
                    bodyPart.setSnakeSpriteDirection(this.direction)
                } else {
                    bodyPart.update(
                        this.body[index - 1].oldX, 
                        this.body[index - 1].oldY, 
                        this.body[index - 1].oldDirection
                    )
                    bodyPart.setSnakeSpriteDirection(this.body[index - 1].getDirection())   
                }
            })
        }
    }

    
    movement() {
        this.updateBody()
        this.oldY = this.y
        this.oldX = this.x

        switch (this.direction) {
            case 'DOWN':
                this.y != 380
                    ? this.update(this.x, this.y + 20)
                    : this.update(this.x, 0)
                break;
            case 'UP':
                this.y == 0
                    ? this.update(this.x, 380)
                    : this.update(this.x, this.y - 20)
                break;
            case 'LEFT':
                this.x == 0
                    ? this.update(380, this.y)
                    : this.update(this.x - 20, this.y)
                break;
            case 'RIGHT':
                this.x != 380
                    ? this.update(this.x + 20, this.y)
                    : this.update(0, this.y)
                break;
            default:
                break;
        }

        this.oldDirection = this.direction
    }

    setDirection(direction) {
        this.oldDirection = this.direction
        this.direction = direction
    }

    getDirection() {
        return this.direction
    }

    checkCollision(block) {
        if (
            this.x < block.x + 20 &&
            this.x + 20 > block.x &&
            this.y < block.y + 20 &&
            this.y + 20 > block.y
            ) {
                let newBody = new SnakeBody(this.x, this.y, this.ctx)
                newBody.setDirection()
                this.body.push(newBody)
            }
    }
    
}

游戏主逻辑代码

document.addEventListener('DOMContentLoaded', (e) => {
    const canvasGame = document.querySelector('canvas')
    const ctx = canvasGame.getContext('2d')

    const 
    cHeight = canvasGame.height = 400,
    cWidth = canvasGame.width = 400;

    let cells = 20;

    const drawGrid = () => {
        ctx.lineWidth = 1.1
        ctx.strokeStyle = '#232332'
        ctx.shadowBlur = 0

        for (let i = 0; i < cells; i++) {
            let f = (cHeight / cells) * i

            ctx.beginPath()
            ctx.moveTo(f, 0);
            ctx.lineTo(f, cHeight)
            ctx.stroke()

            ctx.beginPath();
            ctx.moveTo(0, f)
            ctx.lineTo(cHeight, f)
            ctx.stroke()

            ctx.closePath()
            
        }
    }

    const drawPlayer = () => {        
        let playerX = Math.floor(Math.random() * 19 - 0 + 1) * 20
        let foodX = Math.floor(Math.random() * 19 - 0 + 1) * 20
        let foodY = Math.floor(Math.random() * 19 - 0 + 1) * 20
        let playerY = Math.floor(Math.random() * 19 - 0 + 1) * 20

        let food = new Food(foodX, foodY, 'red', ctx)
        let player = new Player(playerX, playerY, 'Moises', ctx)
        
        player.draw()
        food.draw()

        document.addEventListener('keydown', (e) => {
            switch (e.keyCode) {
                case 37:
                    player.setDirection('LEFT')
                    break;
                case 39:
                    player.setDirection('RIGHT')
                    break
                case 38:
                    player.setDirection('UP')
                    break
                case 40:
                    player.setDirection('DOWN')
                    break;
            }
        })

        setInterval(() => {
            player.movement()
            player.checkCollision(food)
            food.collision(player)
            player.draw()
            food.draw()
            drawGrid()
        }, 450)
    }

    drawGrid()
    drawPlayer()
})

Food类代码

class Food {
    constructor(x, y, color, ctx) {
        this.x = x
        this.y = y
        this.color = color
        this.ctx = ctx
    }

    draw() {
        let img = new Image()

        img.src = 'src/assets/img/apple.png'
        img.onload = () => {
            this.ctx.drawImage(img, this.x, this.y, 20, 20);
            // this.ctx.fillStyle = this.color
            // this.ctx.fillRect(this.x, this.y, 20, 20)
        }

    }

    update(newX, newY) {
        this.ctx.clearRect(this.x, this.y, 20, 20)

        this.x = newX;
        this.y = newY;

        // this.ctx.fillRect(newX, newY, 20, 20)
    }

    collision(player) {
        let newX = (Math.floor(Math.random() * 19 - 0 + 1)) * 20;
        let newY = (Math.floor(Math.random() * 19 - 0 + 1)) * 20;

        if (
            player.x < this.x + 20 &&
            player.x + 20 > this.x &&
            player.y < this.y + 20 &&
            player.y + 20 > this.y
            ) {
                this.update(newX, newY)
            }
    }
}

问题原因分析

  1. 未初始化默认方向:SnakeBody和Player类的direction、oldDirection在构造函数中仅声明未赋值,初始值为undefined,第一次赋值时oldDirection会继承这个空值。
  2. 新身体块初始化错误:Player的checkCollision方法中,创建新SnakeBody时调用newBody.setDirection()但未传参,导致新身体块的direction始终为undefined。
  3. movement方法赋值顺序错误:movement中先执行updateBody再设置this.oldDirection = this.direction,第一次移动时oldDirection还未赋值。
  4. draw方法覆盖方向:SnakeBody的draw方法直接将direction设置为传入的headDirection,会覆盖之前的方向值,导致oldDirection无法保存上一次状态。

修复方案

1. 初始化默认方向

在构造函数中给方向属性设置初始值:

// Player构造函数
this.direction = 'RIGHT';
this.oldDirection = 'RIGHT';

// SnakeBody构造函数,新增初始方向参数
constructor(x, y, ctx, initialDirection) {
    this.x = x;
    this.y = y;
    this.oldX = x;
    this.oldY = y;
    this.ctx = ctx;
    this.direction = initialDirection;
    this.oldDirection = initialDirection;
    this.sprite = new Image();
}

2. 修复新身体块初始化

修改Player的checkCollision方法,创建身体块时传入当前头部方向:

checkCollision(block) {
    if (
        this.x < block.x + 20 &&
        this.x + 20 > block.x &&
        this.y < block.y + 20 &&
        this.y + 20 > block.y
    ) {
        let newBody = new SnakeBody(this.x, this.y, this.ctx, this.direction);
        this.body.push(newBody);
    }
}

3. 调整movement方法赋值顺序

先保存旧方向,再执行移动和身体更新:

movement() {
    this.oldDirection = this.direction;
    this.oldY = this.y;
    this.oldX = this.x;

    // 移动逻辑
    switch (this.direction) {
        case 'DOWN':
            this.y != 380
                ? this.update(this.x, this.y + 20)
                : this.update(this.x, 0)
            break;
        // 其他方向逻辑...
    }

    this.updateBody();
}

4. 修改draw方法避免覆盖方向

移除draw方法中对this.direction的赋值:

draw() {
    this.sprite.onload = () => {
        this.ctx.drawImage(this.sprite, this.x, this.y, 20, 20);
    }
}

5. 优化方向判断逻辑

修复setSnakeSpriteDirection中的条件错误,补充完整转弯场景:

setSnakeSpriteDirection(newDirection) {
    switch(true) {
        case this.direction === 'DOWN' && newDirection === 'LEFT':
            this.sprite.src = 'src/assets/img/snakeBody_rotateDownLeft.png';
            break;
        case this.direction === 'UP' && newDirection === 'DOWN':
            this.sprite.src = 'src/assets/img/snakeBody_down.png';
            break;
        case this.direction === 'RIGHT' && newDirection === 'LEFT':
            this.sprite.src = 'src/assets/img/snakeBody_left.png';
            break;
        case this.direction === 'LEFT' && newDirection === 'RIGHT':
            this.sprite.src = 'src/assets/img/snakeBody_right.png';
            break;
        case this.direction === 'DOWN' && newDirection === 'UP':
            this.sprite.src = 'src/assets/img/snakeBody_up.png';
            break;
        case ['UP', 'DOWN'].includes(this.direction):
            this.sprite.src = 'src/assets/img/snakeBody_down.png';
            break;
        case ['LEFT', 'RIGHT'].includes(this.direction):
            this.sprite.src = 'src/assets/img/snakeBody_left.png';
            break;
        default:
            this.sprite.src = 'src/assets/img/snakeBody_left.png';
            break;
    }
    this.draw();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 05:45:56