Mục lục
ToggleTrong phần trước chúng ta đã cùng nhau tìm hiểu về cách sử dụng P5js và đã vẽ được hình nền của game Plinko. Trong bài viết này, chúng ta sẽ tiếp tục phát triển game Plinko js. Các bạn sẽ được học về các sử dụng matter js, vẽ các điểm chốt trong game plinko.
Đây là một bài viết quan trọng trong việc triển khai game plinko bằng javascript. Nếu có bất kì vấn đề nào chưa rõ bạn có thể comment để được trợ giúp bạn nhé.
Các bạn có thể kết hợp bài viết với xem nội dung sau:
Trong bài viết này chúng ta sẽ thực hiện vẽ các chốt – Peg. Đây là bước khởi đầu trong việc vẽ các đối tượng trong game.
Sau khi hoàn thành phần 4 của seri bài viết làm game đơn giản với Javascript này cấu trúc thư mục dự án sẽ như sau:
Đối với 1 game điều quan trọng đầu tiên là FPS của game, nếu FPS mà quá thấp thì trải nghiệm người dùng sẽ không được tốt.
Đặc biệt trong quá trình phát triển game, chúng ta không thể đảm bảo rằng mình làm không có lỗi lầm gì. Và các lỗi tạo ra này có ảnh hưởng tới FPS của game hay không?
Một phần quan trọng nữa là do việc các đối tượng trong game cần phải chính xác. Chính vì điều này, chúng ta cần phải có lưới tọa độ rõ ràng để kiểm chứng cho các hoạt động vẽ trong game.
Trong game Plinko javascript này Code Tu Tam có vẽ thêm 1 số thông tin mang tính chất hiển thị thông tin game bao gồm: FPS của game, Trục tọa độ của game.
Đầu tiên trong file app.ts chúng ta cập nhật thêm 2 cấu hình cho game:
public static APP: any = { WIDTH: 550, HEIGHT: 580, TOPBUFFER: 50, DEBUG: true, SHOW_BODY: false, };
Chúng ta bổ sung thêm cấu hình DEBUG: true, SHOW_BODY = false. Với chế độ DEBUG = true chúng ta sẽ hiển thông tin info của game và ngược lại. SHOW_BODY sẽ được trình bày trong phần sau của bài viết khi sử dụng thư viện Matter Js.
Trong file P5Wrapper.ts chúng ta thêm nội dung các hàm như sau:
debug() { if (!AppConfig.APP.DEBUG) return; this.debugLine(); this.debugFrameRate(); } debugLine() { this.P5.push(); this.P5.stroke("#f7a9a9"); this.P5.line( AppConfig.APP.WIDTH / 2, 0, AppConfig.APP.WIDTH / 2, AppConfig.APP.HEIGHT ); this.P5.line( 0, AppConfig.APP.HEIGHT / 2, AppConfig.APP.WIDTH, AppConfig.APP.HEIGHT / 2 ); this.P5.pop(); } debugFrameRate() { { this.P5.push(); this.P5.fill("#fff"); this.P5.text(this.P5.frameRate(), 10, 10); this.P5.pop(); } } }
Hàm debug: Trong hàm này có kiểm tra cờ trạng thái DEBUG, nếu là true mới thực hiện tiếp các công việc tiếp theo.
Đầu tiên sẽ hiển thị trục tọa độ thông qua hàm debugLine, và hiển thị FPS thông qua hàm debugFrameRate.
Trong hàm debugLine, sẽ có các lệnh sau bạn cần chú ý, hãy nhớ rằng các lệnh này sẽ lặp lại rất nhiều trong các đoạn mã tiếp theo.
Hàm P5 push() có nhiệm vụ lêu các thông tin cấu hình về kiểu vẽ, kiểu transform hiện tại. Hàm pop() có nhiệm vụ restore (lấy lại) các thông tin đã lưu trên.
Tại sao chúng ta cần sử dụng push và pop trong P5js? Điều này đảm bảo rằng việc chúng ta thay đổi style (với các lệnh như fill, stroke… hay lệnh transform, stranslate…) sẽ chỉ có tác động lên thành phần chúng ta xác định. Nếu không push và pop thì style được vẽ ra có thể bị chồng lấn và lẫn lộn với nhau.
Ví dụ như trong hàm debugLine bên trên, stroke màu F7a9a9 chỉ được áp dụng cho 2 đường line vẽ trước khi pop mà thôi.
Tìm hiểu thêm về hàm push P5js tại đây: https://p5js.org/reference/#/p5/push
Trong debugLine chúng ta có sử dụng hàm P5 line, hàm này có mục tiêu vẽ 1 đường thẳng từ tọa độ x1, y1, tới x2, y2.
Ví dụ như trong đoạn mã trên là vẽ 1 đường line từ (WIDTH/2, 0) tới (WIDTH/2, height) – nói cách khác chính là vẽ tung độ. Đường line vẽ từ (0,HEIGHT/2) tới (WIDTH, HEIGHT/2) chính là vẽ hoành độ.
Đối với hàm debugFrameRate chỉ đơn thuần là cà đặt màu của chữ hiển thị là màu trắng, và hiện ra FPS của chính đối tượng
Đương nhiên để hàm debug hoạt động, chúng ta cần gọi hàm này trong hàm draw của P5Wrapper.
draw() { let background = Loader.getImage("bg"); if (background) this.P5.background(background); this.debug(); }
Như bạn thấy trong hình chụp bên trên, chúng ta thấy được FPS của game đang làm được duy trì ở mức 59~60. Điều này chứng tỏ code game của chúng ta đang ổn, mọi thứ đang tốt.
Đầu tiên trước khi vẽ các peg trong game của chúng ta, chúng ta cùng xem lại hình ảnh game cần thực hiện.
Theo như hình vẽ chúng ta sẽ thấy rằng chúng ta cần vẽ 16 hàng, với hàng đầu tiên là có 3 chốt (peg), hàng cuối cùng có 18 Peg. Các Peg được được hiển thị đan xen nhau giữa hàng trên và hàng dưới.
Hum… Nhìn vậy nhưng cũng không dễ dàng để vẽ phải không nào?
Để vẽ được như vậy chúng ta có thể coi toàn bộ các Peg cần vẽ là 1 ma trận với kích cỡ là 35 x 35. Khi đó chúng ta chỉ cần xác định các vị trí Peg được vẽ ra mà thôi.
Ví dụ ở hàng đầu tiên sẽ là các Peg có index là 15 17 19 sẽ được vẽ ra.
Tiếp đến là hàng số 2, các Peg có index lần lượt là 47 49 51 53
Ok, tiếp đến chúng ta sẽ viết code cho Peg này nhé.
Chúng ta ngoài việc vẽ các Peg sẽ còn 1 số đối tượng khác cùng cần vẽ như là quả Bóng, hay ô giải thưởng… Để thuận tiện cho việc phát triển code, chúng ta cần xây dựng lớp Base Component.
Mã nguồn của BaseComponent.ts như sau:
import AppConfig from "../configs/app"; export default abstract class BaseComponent { protected world: any; private _body: any; public get body(): any { return this._body; } public set body(value: any) { this._body = value; } protected P5: any; private _x: number; public get x(): number { return this._x; } public set x(value: number) { this._x = value; } private _y: number; public get y(): number { return this._y; } public set y(value: number) { this._y = value; } constructor(world: any, P5: any) { this.world = world; this.P5 = P5; } getBody() { return this.body; } abstract show(): void; showBody() { if (!AppConfig.APP.SHOW_BODY) return; this.P5.rect( this.body.bounds.min.x, this.body.bounds.min.y, this.body.bounds.max.x - this.body.bounds.min.x, this.body.bounds.max.y - this.body.bounds.min.y ); } }
Trong class BaseComponent có một số thuộc tính:
World: Trong các phiên bản được ghi trên trang https://brm.io/matter-js/docs/classes/World.html thì World được thay thế bằng Composite. Tuy nhiên về tư tưởng vẫn giống nhau, đây giống như thùng chứa, chứa các đối tượng của game – một thế giới game.
Body: Đối tượng cơ thể đại diện cho 1 đối tượng trong game. Ví dụ như trong game của chúng ta mỗi 1 Peg sẽ có 1 body riêng biệt.
Ngoài ra chúng ta có các thuộc tính x, y là tọa độ của các body của chúng ta trong game plinko này.
Chúng ta cũng có 1 hàm showBody – hàm này dùng để vẽ ra phạm vi body đang chiếm trong game. Chúng ta sử dụng hàm rect để vẽ ra 1 ô chữ nhật để đại diện cho body của đối tượng hiện tại.
Như các bạn thấy trong đoạn code, chúng ta sẽ lấy bounds của body hiện tại và vẽ. Việc này cũng khá hữu ích trong việc muốn kiểm tra xem khung body hiện tại đang ở đâu.
Vậy là xong class base rồi, tiếp đến vào class Peg.ts thôi.
Nội dung mã nguồn của Peg.ts như sau:
import * as Matter from "matter-js"; import BaseComponent from "./BaseComponent"; export class Peg extends BaseComponent { private r: number; private _name: string; public get name(): string { return this._name; } public set name(value: string) { this._name = value; } private _indexPeg: number; public get indexPeg(): number { return this._indexPeg; } public set indexPeg(value: number) { this.body.game_idx_peg = value; this._indexPeg = value; } private options: any = { isStatic: true, restitution: 1, friction: 0, game_idx_peg: 0, game_active: 0, }; constructor(world: any, P5: any, x: number, y: number, r: number) { super(world, P5); this.r = r; this.x = x; this.y = y; this.body = Matter.Bodies.circle(x, y, r, this.options); this.body.label = "peg"; Matter.Composite.add(world, this.body); } public show() { this.P5.push(); this.showGameItem(); this.showBody(); this.P5.pop(); } protected showGameItem() { this.P5.fill("#fff"); this.P5.noStroke(); const pos = this.body.position; this.P5.translate(pos.x, pos.y); this.P5.ellipse(0, 0, this.r * 2); } }
Class Peg được kế thừa từ BaseComponent, nên các thuộc tính của class Base đều được kế thừa. Trong class Peg này chúng ta sẽ có 1 số thông tin.
indexPeg: Như phần mô tả cách vẽ các Peg bên trên thì chúng ta sẽ có các index cụ thể mà ở đó Peg sẽ được vẽ, ví dụ như ở vị trí 15 17 19. (Thay vì sử dụng mảng 2 chiều, Code Tu Tam đang sử dụng mảng 1 chiều để quy định các index của Peg)
Thuộc tính options: đây là thông tin cấu hình cho đối tượng trong Matterjs. Chúng ta có các thuộc tính con như sau:
+ isStatic: thuộc tính khai báo Peg của chúng ta là tĩnh, đứng im 1 chỗ và không chuyển động. Giống như 1 cái đinh đóng vào tường vậy, Peg của chúng ta sẽ đứng yên 1 chỗ mà thôi.
+ restitution: Đây là chỉ số được đặt từ 0 tới 1, đại diện cho độ đàn hồi của body. Ví dụ với 0.8 thì có nghĩa là vật thể có thể bật trở lại với 80% động năng của nó. Các bạn có thể tham khảo thêm chi tiết tại đây: https://brm.io/matter-js/docs/classes/Body.html#property_restitution
+ friction: chỉ số dao động từ 0 tới 1, là chỉ số đại diện cho hệ số ma sát
Các thông số này các bạn có thể chủ động căn chỉnh để game đạt hiệu quả như mong muốn của mình.
Ngoài trong này có thêm 2 thuộc tính do Code Tu Tam tự định nghĩa ra là game_active, và game_idx_peg
Trong hàm khởi tạo của Peg ngoài việc gán giá trị cho thuộc tính, chúng ta sẽ khởi tạo 1 body mới.
Như trong đoạn mã:
this.body = Matter.Bodies.circle(x, y, r, this.options); this.body.label = "peg"; Matter.Composite.add(world, this.body);
Chúng ta khở tạo 1 body với hình dáng là hình tròn, bán kính r, tại vị trí x, y.
Sau đó chúng ta đặt tên cho body này là “peg”
Tiếp đến chúng ta thêm body này vào world – thế giới game của chúng ta tạo ra.
Chúng ta có 2 phương thức, trong đó phương thức show là phương thức bắt buộc phải có. Nếu các bạn để ý thì trong BaseComponent có 1 hàm abstract là show rồi.
Trong hàm này chúng ta thực hiện thể hiện body ra dưới dạng hình ảnh, chúng ta sử dụng p5js để vẽ đối tượng.
Với hàm push và pop thì đã được nhắc trong bài trước đó, bài này mình sẽ không nhắc lại nữa.
Chúng ta có hàm fill để fill màu với màu trong mã nguồn đang thực hiện là màu trắng. Chúng ta dùng hàm translate để di chuyển tới vị trí x, y của body, sau đó thực hiện vẽ hình tròn với bán kính r.
Như đã mô tả trong phần thuật toán vẽ Peg, chúng ta sẽ có nhiều Peg chứ không chỉ có 1 Peg trong game Plinko javascript này.
Do vậy để thuận tiện cho việc code, chúng ta sẽ khởi tạo 1 class để quản lý các Peg đó.
Mã nguồn của PegContainer.ts như sau:
import { Peg } from "../components/Peg"; export default class PegContainer { private world: any; private P5: any; private _pegs: any = []; public get pegs(): any { return this._pegs; } public set pegs(value: any) { this._pegs = value; } constructor(world: any, P5: any) { this.world = world; this.P5 = P5; } public makePegs( width: number, topBuffer: number, options: any = { radius: 1, spacing: 30, rows: 17, } ) { let centerWidth = width / 2; let xSpacing = options.spacing * 1; let ySpacing = options.spacing * 0.8; const startX = centerWidth - xSpacing; var count = 0; let pegs = []; let idxPeg = 15; //Index của Peg đầu tiên let add = 28; // Index Peg đầu tiên hàng tiếp theo for (let i = 1; i < options.rows; i++) { count++; for (let j = 1; j < i + 3; j++) { let x = startX - 0.5 * (i + 1) * xSpacing + j * xSpacing; let y = topBuffer + i * ySpacing; let p = new Peg(this.world, this.P5, x, y, options.radius); p.indexPeg = idxPeg; pegs.push(p); idxPeg += 2; } idxPeg += add; add -= 2; } return (this.pegs = pegs); } public show() { this.pegs.forEach((peg: Peg) => { peg.show(); }); } }
Trong class này chúng ta có 1 mảng pegs để lưu danh sách các peg đã được tạo ra.
Chúng ta sẽ đi sâu vào hàm makePegs, đây có thể gọi là linh hồn của class PegContainer trong game plinko js này.
Hàm makePegs nhận 3 tham số đầu vào:
+ width: độ rộng của canvas hay màn hình chơi game
+ topBuffer: vị trí mép trên để vẽ vẽ Peg (hay nói cách khác là tọa độ y để vẽ Peg hàng đầu tiên)
+ options: tham số đàu vào cho việc vẽ peg, bao gồm: bán kính của 1 peg, khoảng cách giữa các peg, và số dòng pegs.
public makePegs( width: number, topBuffer: number, options: any = { radius: 1, spacing: 30, rows: 17, } ) { let centerWidth = width / 2; let xSpacing = options.spacing * 1; let ySpacing = options.spacing * 0.8; const startX = centerWidth - xSpacing; var count = 0; let pegs = []; let idxPeg = 15; //Index của Peg đầu tiên let add = 28; // Index Peg đầu tiên hàng tiếp theo for (let i = 1; i < options.rows; i++) { count++; for (let j = 1; j < i + 3; j++) { let x = startX - 0.5 * (i + 1) * xSpacing + j * xSpacing; let y = topBuffer + i * ySpacing; let p = new Peg(this.world, this.P5, x, y, options.radius); p.indexPeg = idxPeg; pegs.push(p); idxPeg += 2; } idxPeg += add; add -= 2; } return (this.pegs = pegs); }
Đầu tiên chúng ta xác định vị trí giữa của màn hình game theo chiều x.
Tiếp đến chúng ta tính toán lại khoảng cách theo chiều x và y của các peg với nhau. Như trong mã nguồn thì khoảng cách chiều y của 2 peg liền kề là 0.8 của spacing truyền vào.
Như đã mô tả trong phần trước, ở hàng 1 chúng ta vẽ peg ở index = 15, do vậy idxPeg sẽ bắt đầu bằng 15. Chúng ta cũng nhận ra rằng từ peg cuối cùng của hàng trên đến peg đầu tiên của hàng tiếp theo cách nhau 28 đơn vị. Chúng ta cũng lưu lại con số này để thực hiện việc vẽ các Peg.
Như trong mã nguồn, chúng ta cần 2 vòng lặp, vòng lặp 1 để khởi tạo ra số dòng chứa peg, vòng lặp số 2 chạy từ 1 cho tới i+3 để khởi tạo các peg.
Sau khi thực hiện hàm chạy này chúng ta có 1 mảng các Peg được khởi tạo. Chúng ta khởi tạo trước và sau này chỉ cần sử dụng lại, như vậy không cần phải khởi tạo nhiều gây lãng phí không cần thiết bộ nhớ.
Hàm này sẽ được gọi trong hàm setup để chạy 1 lần duy nhất, nếu bạn đặt trong hàm draw, rất nhanh thôi FPS của bạn sẽ tụt về 0 do việc khởi tạo liên tục đối tượng Peg mới. Hãy chú ý điều này nếu bạn là người mới để tránh các lỗi không cần thiết khi làm game plinko js.
Hàm cuối cùng là hàm show, hàm này đơn giản chỉ là vòng lặp để gọi hàm show của Peg ra mà thôi. Hàm này sẽ được gọi trong hàm draw của P5Wrapper để thị các Peg ra màn hình game plinko.
Với class này chúng ta sẽ khởi tạo các đối tượng của game plinko sử dụng như Peg…
Mã nguồn như sau:
import * as Matter from "matter-js"; import AppConfig from "./configs/app"; import PegContainer from "./containers/PegContainer"; export default class MatterWrapper { private engine: any; private world: any; private P5: any; private pegContainer: PegContainer; constructor(P5: any) { this.P5 = P5; this.engine = Matter.Engine.create(); this.world = this.engine.world; } public init() { this.createPegs(); this.initEvents(); } protected initEvents() {} protected createPegs() { this.pegContainer = new PegContainer(this.world, this.P5); this.pegContainer.makePegs( AppConfig.APP.WIDTH, AppConfig.APP.TOPBUFFER, AppConfig.PEG ); } public drawPegs() { this.pegContainer.show(); } public update() { Matter.Engine.update(this.engine); } }
Trong class này chúng ta có thuộc tính pegContainer, chính là đối tượng của class PegContainer chúng ta vừa tạo bên trên
Trong hàm khởi tạo của MatterWrapper chúng ta khởi tạo engine game mới, đồng thời tạo 1 world – thế giới trò chơi.
Tiếp đến chúng ta quan tâm tới 2 hàm là hàm createPegs
Với hàm này, chúng ta đang gọi tới hàm makePeg, tham số đầu vào là các giá trị config được viết trong file config/app.ts
Hàm drawPegs gọi tới hàm draw của pegContainer.
Hàm update là lời gọi thông báo cho matterjs update engine của mình. Bởi vì chúng ta đã sử dụng vòng lặp chính của P5js do vậy nên chúng ta sẽ dụng hàm update này. Trong trường hợp bạn không có vòng lặp giống như draw, các bạn có thể phải gọi hàm render của matterjs. Vui lòng xem thêm chi tiết nội dung trên trang chủ của matterjs.
Trong class MatterWrapper chúng ta đã sử dụng cấu hình từ app.ts. Với app.ts chúng ta cập nhật 1 số cấu hình cho Peg ở đây:
public static PEG: any = { spacing: 30, rows: 17, radius: 2, };
Như thông số cấu hình ở đây, chúng ta có 16 row được vẽ (vẽ từ 1 tới nhỏ hơn 17), với bán kính 1 peg là 2, khoảng cách là 30.
Với P5Wrapper, chúng ta sẽ cập nhật 2 phần, 1 là thêm thuộc tính matterWrapper là instance của MatterWrapper cũng như chỉnh sửa lại 2 hàm setup, draw của P5Wrapper.
Chúng ta khởi tạo matterWrapper trong hàm khởi tạo của P5Wrapper:
private matterWrapper: MatterWrapper; constructor(P5: any, options: any = {}) { this.P5 = P5; this.options = { ...this.options, ...options }; this.loader = new Loader(P5); this.matterWrapper = new MatterWrapper(P5); }
Cập nhật hàm khởi tạo MatterWrapper trong hàm setup của P5Wrapper:
setup() { this.mCanvas = this.P5.createCanvas( AppConfig.APP.WIDTH, AppConfig.APP.HEIGHT ); this.mCanvas.parent(this.options.parent); this.matterWrapper.init(); }
Và cập nhật hàm draw của P5Wrapper, trong hàm này chúng ta gọi hàm update của matterWrapper và hàm drawPegs.
draw() { let background = Loader.getImage("bg"); if (background) this.P5.background(background); this.matterWrapper.update(); this.matterWrapper.drawPegs(); this.debug(); }
Kết quả demo game Plinko javascript P4
Sau khi cài đặt toàn bộ mã nguồn, chúng ta sẽ có kết quả như hình sau:
Như đã thấy trong hình, chúng ta đã vẽ thành công danh sách các Peg trong game plinko js theo cấu hình được truyền vào.
Trong bài tiếp theo chúng ta sẽ thực hiện vẽ các ô phần thưởng. Hi vọng với bài viết này các bạn có giúp các bạn làm quen với P5js và Matter js trong việc xây dựng một game plinko bằng js cơ bản.
Bình luận: