Chào mọi người, ở bài viết trước, chúng ta đã thực hiện setup game board, hiển thị điểm, khởi tạo đối tượng food, snake và hiển thị các đối tượng đó ở trên giao diện. Trong bài viết này chúng ta sẽ thực hiện các chức năng còn lại để hoàn thiện game Snake bao gồm:

  • Di chuyển snake
  • Kiểm tra va chạm của snake với thân và với thành game board
  • Xử lý ăn điểm
  • ...

OK, Let's go 😊😊

class Block {
    // ...

    equal(otherBlock) {
        return this.col === otherBlock.col && this.row === otherBlock.row;
    }
}

Trong class Block, chúng ta định nghĩa thêm method equal(), method giúp chúng ta xác định được 2 block có trùng vị trí nhau hay không?

Phương thức này rất quan trọng để về sau chúng ta có thể kiểm tra xem snake và food có vạ chạm với nhau hay không? Hay snake có tự va chạm vào chính nó hay không?

Random vị trí Food

class Food {
    // ...

    move() {
        let randomCol = Math.floor(Math.random() * (widthInBlocks - 2) + 1);
        let randomRow = Math.floor(Math.random() * (heightInBlocks - 2) + 1);

        this.position = new Block(randomCol, randomRow);
    }
}

Trong class Food, bổ sung thêm method move(). Khi snake và food va chạm nhau, lúc này chúng ta cần random lại vị trí của đối tượng food và method move() sẽ giúp chúng ta điều này.

Đơn giản là chúng ta chỉ cần random row và col và sau đó cập nhật lại thuộc tính position theo row và col đã được random trước đó

Di chuyển Snake

class Snake {
    constructor(color) {
        ...
        this.direction = "right";
    }
}

Trong contructor của class Snake chúng ta bổ sung thêm thuộc tính direction, thuộc tính này dùng để xác định hướng mà snake đang di chuyển (left, right, bottom, top). Mặc định ban đầu là "right"

move() {
    // Lấy ra vị trí đầu snake
    let head = this.segments[0];
    let newHead;

    // Khi snake di chuyển, ví dụ sang bên phải (right)
    // Tạo newHead ở vị trí mới bằng cách tăng col lên 1 và giữ nguyên row
    // Tương tự với các hướng khác
    if(this.direction == 'right') {
            newHead = new Block(head.col + 1, head.row);
    } else if(this.direction == 'down') {
            newHead = new Block(head.col, head.row + 1);
    } else if(this.direction == 'left') {
            newHead = new Block(head.col - 1, head.row);
    } else if(this.direction == 'up') {
            newHead = new Block(head.col, head.row - 1);
    }

    // Kiểm tra va chạm
    if(this.checkCollision(newHead)) {
            gameOver();
            return;
    }

    // Thêm newHead vào đầu mảng segments
    this.segments.unshift(newHead);

    // Trường hợp di chuyển mà va chạm với food
    // Tăng score và random vị trí mới cho food
    if(newHead.equal(food.position)) {
            score++;
            food.move();
    } else {
            // Di chuyển mà không va chạm với food thì xóa phần tử ở cuối mảng segments
            this.segments.pop();
    }
}

Để di chuyển snake rất đơn giản, đầu tiên chúng ta xác định vị trí head của snake, tiếp theo tùy vào hướng của direction là gì chúng ta sẽ tạo ra head mới và thêm vào đầu mảng segments và đồng thời xóa phần tử cuối cùng trong mảng đi (đây là trường hợp di chuyển bình thường)

Tuy nhiên trong quá trình snake di chuyển sẽ gặp một số trường hợp sau

TH1 : Va chạm với food

if(newHead.equal(food.position)) {
    score++;
    food.move();
}

Nếu snake va chạm với food thì chúng ta tăng điểm lên 1 đồng thời random food ở vị trí mới

TH2 : Va chạm với cạnh của game board hoặc va chạm với chính bản thân snake

if(this.checkCollision(newHead)) {
    gameOver();
    return;
}

Trong trường hợp này chúng ta sẽ gọi method checkCollision() để kiểm tra khả năng va chạm của snake với cạnh game board hoặc với chính nó, chúng ta sẽ định nghĩa method này như sau

checkCollision(head) {
    // Kiểm tra va chạm với thành game Board
    let left = head.col < 0;
    let top = head.row < 0;
    let right = head.col > widthInBlocks - 1;
    let bottom = head.row > heightInBlocks - 1;
    let wallCollision = left || top || right || bottom

    // Kiểm tra va chạm với thân snake
    let selfCollision = false;
    for(let i=0; i<this.segments.length; i++) {
            if(head.equal(this.segments[i])) {
                    selfCollision = true;
            }
    }

    return wallCollision || selfCollision;
}

Nếu trường hợp va chạm xảy ra chúng ta sẽ gọi gameOver() để hiển thị thông tin kết thúc game và return để kết thúc hàm

function gameOver() {
    console.log("Game Over");
    clearInterval(interval);
}

Di chuyển snake theo các hướng

Bên trên chúng ta mới thực hiện việc cho snake di chuyển sang bên phải (direction = 'right'), vậy muốn snake di chuyển sang các hướng còn lại thì làm như thế nào

let directions = {
    37: "left",
    38: "up",
    39: "right",
    40: "down"
}

document.addEventListener('keydown', function(e) {
    let newDirection = directions[e.keyCode];
    if(newDirection) {
        snake.setDirection(newDirection);
    }
})

Ở trên chúng ta định nghĩa directions chứa thông tin về các hướng di chuyển của snake dựa trên keycode của các phím mũi tên (bạn nào chưa biết keycode của các phím có thể tham khảo link sau https://keycode.info/)

Tiếp đến chúng ta sẽ lắng nghe sự kiện khi người chơi bấm vào các phím mũi tên (keydown là sự kiện nhấn phím xuống)

OK, khi người chơi bấm phím chúng ta sẽ tiến hành lấy thông tin về newDirection dựa vào keycode mà người chơi đã ấn. Sau đó cập nhật lại direction mới cho đối tượng snake bằng method setDirection

Bây giờ chúng ta sẽ định nghĩa method setDirection trong class Snake

setDirection(newDirection) {
    if(
        this.direction == "up" && newDirection == "down" ||
        this.direction == "left" && newDirection == "right" ||
        this.direction == "down" && newDirection == "up" ||
        this.direction == "right" && newDirection == "left"
    ) return

    this.direction = newDirection;
}

Đơn giản là chúng ta chỉ việc cập nhật this.direction = newDirection là xong

Nhưng có điều đáng chú ý là với các trường hợp directionnew direction có cùng phương thì chúng ta sẽ bỏ qua (ví dụ : snake đang di chuyển sang phải mà chuyển hướng đi sang trái hoặc snake đang di chuyển lên trên mà muốn chuyển hướng xuống dưới, ...)

Tạo game loop cho game

Phần quan trong cuối cùng để chúng ta có thể nhìn thấy thành quả chính là tạo game loop cho game

Có 2 cách phổ biến để tạo game loop là sử dụng setInterval() hoặc requestAnimationFrame(). Trong game này thì chúng ta sẽ sử dụng setInterval cho nó đơn giản

Bây giờ chúng ta sẽ refactor lại function init() một chút

let score;
let snake;
let food;
let interval;

//Start Game function
function init() {
    // Khởi tạo điểm
    score = 0;

    // Khởi tạo đối tượng snake + food
    snake = new Snake('yellow');
    food = new Food('red');

    // Tạo game loop cho game
    interval = setInterval(function() {
        ctx.clearRect(0, 0, width, height);
        drawScore();

        snake.draw();
        snake.move();
        
        food.draw();
    },100)
}

Để tạo game loop thì chúng ta sử dụng hàm setInterval(), ở đây mình sử dụng là 10 frame / 1s

Đối với mỗi frame chúng ta sẽ thực hiện các công việc sau:

  • Xóa toàn bộ canvas
  • Vẽ lại điểm
  • Vẽ lại snake
  • Cập nhật vị trí cho snake
  • Vẽ lại food

Xong xuôi cả rồi thì cùng chơi thử game thôi

Các bạn có thể bấm vào link dưới đây để chơi game thử sau khi đã hoàn thành đến đây nhé (ở đây mình có mông má lại với 1 chút css, các bạn có thể tham khảo link source code bên dưới nhé)

Link chơi game : https://snake-ix0z3jtik-buihien.vercel.app/


Source code của phần này mình để ở đây nhé : https://github.com/buihien0109/snake/tree/master/part-2

Các bạn có thể tham khảo thêm khóa học này nhé:

  • Javascript căn bản - Tổng hợp 12 game huyền thoại - tại đây.
  • Lập trình Game Javascript (trực tuyến có tương tác) - tại đây.