Mở bát:

Trong lúc deadline dự án dí sát mông vẫn có những lúc rảnh rỗi thư giãn làm ly trà, ngồi trước hiên nhà ngắm hoàng hôn, hóng gió, không có gì nghịch nên lôi máy ra code game.

Lật lại vấn đề, trước giờ làm game thì người ta sẽ nghĩ ngay đến C++, đồ họa 3D các thứ, nhưng nghĩ lại thân là một web dev sợ C++ hơn sợ bố thì việc này có vẻ bất khả thi. Thế thì sao không làm WEB GAME nhở 😄 ?? Nhưng không phải Đột Kid, Truy Kid, Võ Lâm các thứ đâu, nghịch thử một game nhỏ nhỏ trong dăm ba chục phút với sự hỗ trợ của thư viện P5.js.

Bài viết về P5.js là gì bạn có thể tìm hiểu ở đây: https://viblo.asia/p/cai-nhin-co-ban-ve-p5js-djeZ10aQKWz và ở đây: https://p5js.org/

Game mà mình sẽ làm trong 20p đó là Bắn gà phiên bản không có kinh phí \ :v /

Nếu ai lười đọc lười làm thì có thể kéo xuống phần chơi thử để trải nghiệm trước =))

Và đừng quên like share and subscribe cho kênh của mình nhé 😉

Thân bài:

Chuẩn bị:

Để làm được game này bạn cần chuẩn bị thư viện p5.js: http://p5js.org/libraries/

Tiếp tục tạo một thư mục với cấu trúc như sau:

Trong đó:

  • Libraries: chứa các file của thư viện p5.js
  • index.html: import các file js và thư viện p5
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>p5.js example</title>
    <style> body {padding: 0; margin: 0;} </style>
    <script src="libraries/p5.min.js"></script>
    <script src="libraries/addons/p5.dom.min.js"></script>
    <script src="libraries/addons/p5.sound.min.js"></script>
    <script src="sketch.js"></script>
  </head>
  <body>
  </body>
</html>
  • my.css: file css
  • sketch.js: Coi như đây là hàm main như trong Java hay C++ cũng được, vì nơi đây là nơi khởi tạo các đối tượng cho chương trình, code trong này sẽ dần dần được xây dựng bên dưới

Game này theo ý tưởng của mình sẽ gồm 3 đối tượng: Phi thuyền, , Đạn bắn

Xây dựng các đối tượng:

Trước khi đi vào xây dựng các đối tượng (object), mình sẽ tạo phông nền cho game trước như sau. Mở file sketch.js:

function setup() {
    createCanvas(600, 400); // kích thước khung hình trò chơi
}

function draw() {
    background(51); //màu nền
}

Giờ sẽ lần lượt đi xây dựng các object cho trò chơi

Phi thuyền:

Tạo hình của phi thuyền trông sẽ như thế này vì hết tiền thuê người vẽ

Các bạn tạo file ship.js trong thư mục game:

function Ship() {
    this.x = width / 2;
    
    show() {
        fill(255);
        rectMode(CENTER);
        // vẽ phi thuyền
        push();
        noStroke();
        translate(this.x, height - 10);
        scale(0.8, 0.8);
        fill('#c32929');
        rect(0, 0, 40, 40);
        fill('#d5d589');
        rect(0, -25, 10, 25);
        pop();
    }
}

Sau đó import vào index.html

  <head>
    <script src="libraries/p5.min.js"></script>
    <script src="libraries/p5.dom.min.js"></script>
    <script src="libraries/p5.sound.min.js"></script>
    <script src="sketch.js"></script>
    <script src="ship.js"></script>
  </head>

Mở sketch.js và thêm:

var ship;

function setup() {
    createCanvas(600, 400);
    ship = new Ship();
}

function draw() {
    ship.show();
}

Quay ra trình duyệt F5 một phát bạn sẽ thấy phi thuyền của mình đang nằm chềnh ềnh giữa khung trò chơi.

Bây giờ mình muốn phi thuyền của mình phải chạy chạy theo chiều ngang mỗi khi mình nhấn phím mũi tên di chuyển sang trái phải.

May quá p5.js đã cung cấp một bộ thư viện để kiểm soát các sự kiện như thế này, chúng ta sẽ làm như sau trong file sketch.js

function keyPressed() {
    if (keyCode === RIGHT_ARROW) {
        ship.move(1);
    } else if (keyCode === LEFT_ARROW) {
        ship.move(-1)
    }
}

Như vậy, chúng ta đang bắt lấy sự kiện khi người dùng nhấn phím, ở đây là kiểm tra xem có phải 2 phím mũi tên di chuyển trái phải không, nếu là bấm sang phải thì sẽ cho phi thuyền di chuyển sang phải bằng phương thức move().

Tiến hành xây dựng phương thức move() nào:

ship.js

function Ship() {
    this.x = width / 2;

    this.show = function() {
        //...
    }

    this.move = function(dir) {
        this.x += dir*5; // di chuyển với tốc độ là 5
    }
}

Just like that =))

Nhưng có một vấn đề nho nhỏ là mỗi lần bạn bấm phím phi thuyền mới chịu di chuyển làm bạn cứ phải bấm liên tục -_- thêm nữa là trông nó chạy giật cục rất khó chịu =.=

Bây giờ tôi muốn khi tôi nhấn giữ phím thì nó cũng phải chạy.

Không khó lắm, thêm code sau: sketch.js

function draw() {
    //...
    ship.move();
}
//...

function keyPressed() {
    if (keyCode === RIGHT_ARROW) {
        ship.setDir(1);
    } else if (keyCode === LEFT_ARROW) {
        ship.setDir(-1)
    }
}
function keyReleased() {
    if (key !== ' ') {
        ship.setDir(0)
    }
}

ship.js

function Ship() {
    this.x = width / 2;
    this.xdir = 0; // hướng di chuyển

    this.show = function() {
        //...
    }

    setDir(dir) {
        this.xdir = dir; // đặt lại hướng di chuyển mỗi khi có sự kiện nhấn nút
    }

    move() {
        this.x += this.xdir*5; // lúc này phi thuyền sẽ di chuyển sang  phải nếu xdir = 1 và ngược lại với xdir = -1
    }
}

Ý tưởng ở đây là hãy để cho phi thuyền luôn luôn chạy ngay từ khi khởi tạo đối tượng, nhưng bạn sẽ không thấy nó di chuyển vì xdir đang bằng 0, tức là giá trị this.x của phi thuyền lúc này không thay đổi.

Khi sự kiện nhấn phím xảy ra, hướng di chuyển của phi thuyền sẽ được đặt thông qua phương thức setDir() và vị trí x của phi thuyền sẽ liên tục được thay đổi cho đến khi phím được nhả.

Tiếp đến chúng ta dùng phương thức keyReleased() để bắt lấy sự kiện khi ta nhà phím ra, ta sẽ muốn phi thuyền dừng lại, không di chuyển nữa bằng việc set cho xdir bằng 0.

Như vậy phi thuyền sẽ di chuyển liên tục và mượt mà hơn, giống với game thật hơn như thế này:

Đến vậy là tạm xong phi thuyền, tiếp tục đi xây dựng đối tượng =))

Gà:

Tạo hình:

Đúng rồi đó là đàn gà đó, phiên bản super low cost =))

Tiếp tục tạo file chicken.js trong thư mục game:

class Chicken(x, y) {
       this.x = x;
       this.y = y;
       this.rotate = 1.5559;
       
   show() {
        push();
        noStroke();
        translate(this.x, this.y);
        scale(0.8, 0.8);
        rotate(this.rotate);
        // vẽ gà
        fill('yellow');
        ellipse(0, 0, 40, 40);
        fill('blue');
        ellipse(5, 0, 25, 25);
        fill('red');
        ellipse(17, 0, 20, 20);
        pop();
    }
}

Sau đó import vào index.html rồi mở sketch.js và khởi tạo các đối tượng gà, vì có nhiều gà và mình muốn gà của mình nhiều lên theo thời gian nên mình sẽ dùng setInterval() để tạo ra các con gà chạy lòng vòng trên màn hình.

Dữ liệu những con gà này sẽ được lưu hết vào một mảng các đối tượng gà như sau:

var ship;
var chickens = [];

function setup() {
    createCanvas(600, 400);
    ship = new Ship();
    
    setInterval(() => {
        chickens.push(new Zombie(Math.floor(Math.random() * 600), Math.floor(Math.random() * 200))) // mình muốn gà của mình khởi tạo ở các vị trí ngẫu nhiên nên sẽ dùng hàm random()
    }, 1000)
}

function draw() {
    //...
    
    for (var i = 0; i < chickens.length; i++) {
        chickens[i].show();
        chickens[i].move();
    }
}

Giờ quay ra F5 lại một cái, các bạn sẽ thấy gà của bạn được khởi tạo trên đầy màn hình, giờ vấn đề tiếp theo là tôi muốn những con gà này di chuyển qua lại.

Chúng ta tạo một phương thức move() trong chicken.js như sau:

class Chicken(x, y) {
       this.x = x;
       this.y = y;
       this.rotate = 1.5559;
       this.xdir = 1;
       
   show() {
       // ...
    }
}

move() {
    this.x = this.x + this.xdir;
}

Với xdir tương tự như phi thuyền, là hướng di chuyển của các con gà qua trái hoặc qua phải. Nhưng tạm thời chúng vẫn chưa làm vậy được đâu, quay lại với ý tưởng chút.

Giờ tôi muốn những con gà này vừa di chuyển, khi con gà ngoài cùng nhất hoặc trong cùng nhất chạm vào viền khung trò chơi thì tôi sẽ cho những con gà chuyển động ngược lại hướng di chuyển của chúng, và tất cả con gà sẽ dịch chuyển gần hơn đến phi thuyền một khoảng cách nào đó.

Đầu tiên là với sketch.js

function draw() {
    //...
    var edge = false; // nhận biết xem gà đã chạm viền hay chưa
    for (var i = 0; i < chickens.length; i++) {
        chickens[i].show();
        chickens[i].move();
        
        if (chickens[i].x > width - 10 || chickens[i].x < 10) {
            edge = true;
        }
    }
    // nếu gà có bất cứ con gà nào va vào viền khung hình thì tất cả gà sẽ chạy một method shiftDown()
    if (edge) {
        for (var i = 0; i < chickens.length; i++) {
            chickens[i].shiftDown();
        }
    }
}

chicken.js

class Chicken(x, y) {
       this.x = x;
       this.y = y;
       this.rotate = 1.5559;
       this.r = 30;
       this.xdir = 1;
       
   show() {
           // ...
        }
    }

    move() {
        this.x = this.x + this.xdir;
    }
    
    shiftDown() {
        this.xdir *= -1;
        this.y += this.r;
    }
}

Nếu phát hiện gà đã chạm vào khung hình, chạy phương thức shiftDown() cho gà chạy ngược lại và tăng y lên một đơn vị r cũng chính là khoảng dịch chuyển của gà đến phi thuyền.

Xem thử kết quả:

Smooth =))

Xong con gà rồi, giờ cái quan trọng nhất ta cần nữa là làm cho phi thuyền bắn đạn =))

Đạn

Đầu tiên là làm viên đạn cho phi thuyền bắn ra, cũng chả biết mô tả trông nó như nào nên thôi làm luôn vậy =)) Tạo file drop.js trong thu mục game và thêm code: drop.js

function Drop(x, y) {
    this.x = x;
    this.y = y;
    this.r = 8;
    this.rotate = -1.5559;
    this.speed = 10;

    this.show = function() {
        push()
        noStroke();
        translate(this.x, this.y + 30);
        rotate(this.rotate);

        for(let i = 0; i > -5; i--) {
          fill(color(255, 255, 255, 255 + i * 12));
          rect(i * this.speed * 0.5 + 60 + this.speed * 3, 0, 5, 5 + i * 0.1);
        }

        pop();
    }
    
    this.move = function() {
        this.y = this.y - this.speed;
    }
}

Đó là đối tượng đạn, bây giờ muốn nhìn thấy nó, ta sẽ làm đồng thời luôn cả hành động bắn nó ra từ phi thuyền.

sketch.js

//...
var ship;
var chickens = [];
var drops = []; // thêm array đạn

//...
function draw() {
    //...
    for (var i = 0; i < drops.length; i++) {
        drops[i].show();
        drops[i].move();
    }
    //...
}

//...
function keyPressed() {
    if (key === ' ') {
        var drop = new Drop(ship.x, height);
        drops.push(drop);
    }
    //...
}

Ở đây tôi sẽ khởi tạo một viên đạn và làm nó bắn ra mỗi lần phím cách được gõ và khởi tạo ngay tại vị trí của phi thuyền đang đứng

Sắp xong rồi =)) cố lên nào

Game play

Giờ chúng ta đã có đủ tất cả các đối tượng cần thiết, hãy nghĩ xem game play sẽ như thế nào hmmm :-?

Ý tưởng: tôi muốn mỗi viên đạn tôi bắn ra khi trúng gà sẽ làm gà mất một số máu, số máu của gà sẽ được thiết lập ngay trong đối tượng gà, con gà nào hết máu sẽ chết và biến mất khỏi màn hình game, viên đạn sau khi trúng gà cũng vậy.

Đó, ý tưởng chỉ đơn giản vậy thôi :v

Đầu tiên là xác định va chạm giữa đạn và gà. Hãy tưởng tượng mỗi viên đạn và mỗi con gà đều có một hình tròn với bán kính không đổi, hình tròn tưởng tượng này gọi là hit box của đối tượng, vậy việc cần làm là xác định xem khoảng cách của viên đạn và con gà đã nhỏ hơn tổng 2 giá trị bán kính này chưa, nếu nó nhỏ hơn thì tức là 2 đối tượng đã va chạm với nhau, ez. Khi đó ta sẽ thực hiện tác vụ xóa con gà và viên đạn đó khỏi màn hình.

Bắt tay vào làm nào. Đầu tiên sửa lại phần code hiện đạn bắn đã, chúng ta sẽ xử lý va chạm ở đây

sketch.js

for (var i = 0; i < drops.length; i++) {
    drops[i].show();
    drops[i].move();

    for (var j = 0; j < chickens.length; j++) {
        if (drops[i].hits(chickens[j])) {
            drops[i].remove();
        }
    }
}

for (var i = drops.length - 1; i >= 0; i--) {
    if (drops[i].toDel) {
        drops.splice(i, 1);
    }
}

Dùng phương thức hits() để kiểm tra viên đạn này đã trúng đối tượng gà nào chưa, nếu trúng thì remove viên đạn đó ra khỏi mảng, còn đây là phương thức hits()

drop.js

function Drop(x, y) {
    //...
    this.toDel = false;
    
    this.hits = function(chicken) {
        var d = dist(this.x, this.y, chicken.x, chicken.y);
        if (d < this.r + chicken.r + 30) {
            return true;
        }
        
        return false;
    }
    
    this.remove = function () {
        this.toDel = true;
    }
}

p5.js đã cung cấp cho chúng ta một phương thức dist() để xác định khoảng cách giữa 2 điểm, ở đây là tọa độ của đạn và gà, nếu khoảng cách này nhỏ hơn tổng bán kính của đạn và gà trong vòng 30 đơn vị thì trả về true ngược lại trả về false.

Và vậy nên ta có, nếu đạn hits gà thì remove viên đạn đó, còn con gà sẽ không remove ngay mà để tăng độ khó cho trò chơi, chúng ta sẽ có một thứ gọi là máu của gà =)) (tiết gà)

Số máu này được thiết lập trong mỗi đối tượng gà khởi tạo, tôi sẽ để gà có 3 máu mỗi con, mỗi phát bắn trúng sẽ làm gà mất 1 máu, vậy con gà nào trúng 3 viên đạn là tèo, ok làm nào.

chicken.js

class Chicken(x, y) {
       this.blood = 3;
       this.x = x;
       this.y = y;
       this.rotate = 1.5559;
       this.r = 30;
       this.xdir = 1;
       
       //...
       
       subBlood() {
            this.blood -= 1;
        }
}

sketch.js

for (var i = 0; i < drops.length; i++) {
    drops[i].show();
    drops[i].move();

    for (var j = 0; j < chickens.length; j++) {
        if (drops[i].hits(chickens[j])) {
            if (chickens[j].blood) {
                chickens[j].subBlood();
            } else {
                chickens.splice(j, 1);
            }
            drops[i].remove();
        }
    }
}

Chơi thử

Hình như là xong rồi đấy, giờ nếu các bạn làm đúng các bước ở trên thì game của các bạn đã cơ bản chơi được rồi, nhưng nó còn thiếu sót khá nhiều và có lẽ cũng còn nhiều bug nữa, nhưng mà anyway, vẫn gọi là dùng được =))

Còn với ai đọc đến đây mà lười làm thì tôi đã có sẵn 1 bản demo kèm source code đây, tất nhiên là đã được thêm một số thứ để làm nó giống trò chơi thật hơn, ví dụ như điểm số với điều kiện thua cuộc ✌️

Let's try