Giới thiệu
Bạn đang phát triển trò chơi Phaser và bỗng dưng... BOOM! Một lỗi không thể theo dõi. Trạng thái của người chơi biến mất giữa các cảnh, các listener không được gỡ bỏ và bắt đầu xuất hiện rò rỉ bộ nhớ. Nghe có quen không?
Hãy chuẩn bị để khám phá cách một thư viện nhỏ đã cách mạng hóa mã của tôi và sự tỉnh táo của tôi.
Vấn đề: Sự hỗn loạn trong quản lý trạng thái
Nếu bạn đã từng phát triển trò chơi với Phaser, bạn có lẽ đã phải đối mặt với nỗi frustation trong việc quản lý trạng thái. Hệ thống native của Phaser, mặc dù hoạt động, nhưng có nhiều cạm bẫy có thể biến một dự án đơn giản thành một cơn ác mộng với những lỗi khó theo dõi.
1. Tính không nhất quán trong tên gọi
Ai đã từng trải qua những khoảnh khắc “trống rỗng” trong khi lập trình? Tôi thú nhận: tôi rất tệ trong việc nhớ tên biến và cú pháp. Và điều tồi tệ nhất là hệ thống native của Phaser không cung cấp validation để cứu chúng ta khỏi những cạm bẫy này.
javascript
// Cảnh 1
this.registry.set('user-state', { name: 'Người chơi', level: 5 });
// Cảnh 2 - Ôi không! Lỗi chính tả chỉ gặp phải khi chạy
this.registry.set('userState', { name: 'Người chơi', level: 6 });
Kết quả? Giờ mất để săn lùng một lỗi vô hình. Bạn thay đổi giữa camelCase trong một cảnh và kebab-case trong cảnh khác, tạo ra các trạng thái trùng lặp mà không nhận ra. Điều tồi tệ nhất là loại lỗi này không phát ra âm thanh - trò chơi không bị hỏng, nó chỉ không hoạt động như mong đợi.
2. Hệ thống sự kiện khó hiểu
Phaser tự động tạo ra các sự kiện theo định dạng changedata-, mà:
- Khó nhớ
- Dễ mắc lỗi chính tả
- Không có hỗ trợ TypeScript đúng cách
javascript
// Hệ thống native - khó hiểu và dễ mắc lỗi
this.registry.events.on('changedata-player-health', (parent, key, value) => {
// Logic callback
});
// Vấn đề: làm thế nào để dọn dẹp mà không bị rò rỉ bộ nhớ?
// Nếu bạn sử dụng arrow function, bạn không thể gọi .off() sau đó
3. Nhầm lẫn giữa trạng thái địa phương và toàn cầu
javascript
// Trạng thái toàn cầu - tồn tại giữa các cảnh
this.registry.set('global-score', 100);
// Trạng thái địa phương - bị xóa khi thay đổi cảnh
this.data.set('local-ui', { menuOpen: false });
Mặc dù cả hai đều có cùng hợp đồng (.set(), .get()), bạn cần phải nhớ liên tục: "Đây là trạng thái registry hay data?". Sự khác biệt giữa toàn cầu và địa phương là khái niệm, nhưng bạn phải ghi nhớ đối tượng nào để sử dụng cho mỗi tình huống. Một điều nữa làm tốn không gian tinh thần mà đáng lẽ nên tập trung vào logic của trò chơi của bạn.
4. Không có hỗ trợ TypeScript
Nếu không có kiểu đúng cách, bạn hoàn toàn mất khả năng intellisense và an toàn kiểu mà TypeScript cung cấp.
Giải pháp: Phaser-Hooks
Lấy cảm hứng từ sự thanh lịch của React Hooks, phaser-hooks là một thư viện mang lại một API nhất quán, an toàn kiểu và trực quan cho quy trình làm việc của tôi với Phaser. Sau khi sử dụng nó trong sản xuất, tôi có thể nói: thật không thể quay lại hệ thống native.
Cài đặt
bash
$ npm i phaser-hooks
API đơn giản và quen thuộc
Nếu bạn đã sử dụng React, bạn sẽ cảm thấy như ở nhà. API rất trực quan và hoàn toàn được gõ kiểu:
javascript
// Định nghĩa loại trạng thái
type PlayerUI = {
health: number;
mana: number;
menuOpen: boolean;
};
// Tạo trạng thái với destructuring (rất giống React)
const { get, set, on } = withLocalState<PlayerUI>(this, 'player-ui', {
health: 100,
mana: 50,
menuOpen: false
});
// Sử dụng với đầy đủ an toàn kiểu
set({ health: 80, mana: 30, menuOpen: true }); // ✅ Được TypeScript xác thực
set({ helth: 80 }); // ❌ Lỗi biên dịch - phát hiện lỗi chính tả!
// Lấy giá trị đã gõ kiểu
const currentHealth = get().health; // IntelliSense hoàn chỉnh
// Nghe các thay đổi
on('change', (oldValue) => {
console.log(`Health: ${oldValue.health} → ${get().health}`);
});
Thư viện cung cấp hai hooks chính với cùng một hợp đồng:
withLocalState: Trạng thái bị cô lập theo cảnh - hoàn hảo cho UI, trạng thái tạm thời, hoặc dữ liệu cần "đặt lại" khi thay đổi cảnhwithGlobalState: Trạng thái chia sẻ giữa tất cả các cảnh - lý tưởng cho điểm số, cài đặt người chơi, tiến trình trò chơi
Dù bạn chọn cái nào, API là giống nhau. Không cần phải ghi nhớ xem nó là registry hay data nữa - chỉ cần nghĩ về phạm vi bạn muốn và sử dụng hook tương ứng.
Một ví dụ đơn giản
Hãy tạo một hook tùy chỉnh để quản lý UI của người chơi:
javascript
import { withLocalState, type HookState } from 'phaser-hooks';
// Định nghĩa loại trạng thái
type PlayerUI = {
health: number;
mana: number;
menuOpen: boolean;
};
// Tạo một hook tùy chỉnh
export type PlayerUIHook = HookState<PlayerUI> & {
takeDamage: (damage: number) => void;
useMana: (cost: number) => void;
toggleMenu: () => void;
};
export const withPlayerUI = (scene: Phaser.Scene): PlayerUIHook => {
const { get, set, ...rest } = withLocalState<PlayerUI>(scene, 'player-ui', {
health: 100,
mana: 50,
menuOpen: false
});
return {
...rest,
get,
set,
takeDamage: (damage: number) => {
const current = get();
set({ ...current, health: Math.max(0, current.health - damage) });
},
useMana: (cost: number) => {
const current = get();
set({ ...current, mana: Math.max(0, current.mana - cost) });
},
toggleMenu: () => {
const current = get();
set({ ...current, menuOpen: !current.menuOpen });
},
};
};
// Trong cảnh của bạn
class GameScene extends Phaser.Scene {
create() {
const playerUI = withPlayerUI(this);
// Sử dụng với đầy đủ an toàn kiểu
playerUI.takeDamage(20); // ✅ Phương thức đã gõ kiểu
// Nghe các thay đổi
playerUI.on('change', (oldValue) => {
console.log(`Health: ${oldValue.health}`);
});
}
}
Tính năng nâng cao
1. Hệ thống sự kiện sạch
javascript
// Listener vĩnh viễn
const unsubscribe = state.on('change', (oldValue) => {
console.log('Trạng thái đã thay đổi:', get());
});
// Listener một lần
state.once('change', (oldValue) => {
console.log('Thay đổi đầu tiên phát hiện:', get());
});
// Dọn dẹp dễ dàng
unsubscribe(); // hoặc
state.clearListeners(); // Gỡ bỏ tất cả listener
Quan trọng: Phương thức on('change') trả về một hàm gỡ bỏ mà nên được gọi trước khi rời khỏi cảnh để tránh rò rỉ bộ nhớ. Một thực tiễn phổ biến là sử dụng sự kiện hủy của cảnh:
javascript
create() {
const playerState = withLocalState(this, 'player', initialValue);
const unsubscribe = playerState.on('change', (oldValue) => {
console.log('Trạng thái đã thay đổi từ:', oldValue, 'đến:', playerState.get());
});
// Dọn dẹp tự động
this.events.once('destroy', () => {
unsubscribe();
});
}
2. Hook tùy chỉnh nâng cao
Bạn có thể tạo các hook phức tạp bao gồm logic trò chơi cụ thể:
javascript
// Hook hẹn giờ với kiểm soát hoàn toàn
export const withTimer = (scene: Phaser.Scene): TimerHook => {
const { get, set, ...rest } = withGlobalState<Timer>(scene, 'TIMER', { seconds: 0 });
let timer: Phaser.Time.TimerEvent | null = null;
return {
...rest,
get,
set,
start: () => {
if (!timer) {
timer = scene.time.addEvent({
delay: 1000,
loop: true,
callback: () => set({ seconds: get().seconds + 1 }),
});
}
},
pause: () => timer?.paused && (timer.paused = true),
reset: () => {
set({ seconds: 0 });
timer?.remove(false);
timer = null;
},
};
}
// Hook điểm số với các phương thức tiện lợi
export const withScore = (scene: Phaser.Scene): ScoreHook => {
const { get, set, ...rest } = withGlobalState<Score>(scene, 'SCORE', { home: 0, away: 0 });
return {
...rest,
get,
set,
reset: () => set({ home: 0, away: 0 }),
addHomeGoal: () => set({ ...get(), home: get().home + 1 }),
addAwayGoal: () => set({ ...get(), away: get().away + 1 }),
};
}
Tùy chọn nâng cao
Chế độ gỡ lỗi
javascript
const state = withLocalState(scene, 'debug-state', initialValue, {
debug: true // Bật log chi tiết
});
Xác thực tùy chỉnh
javascript
const healthState = withLocalState<number>(scene, 'health', 100, {
validator: (value) => {
const health = value as number;
return health >= 0 && health <= 100 ? true : 'Sức khỏe phải nằm trong khoảng 0-100';
}
});
Lợi ích của Phaser-Hooks
-
✅ An toàn kiểu hoàn toàn
IntelliSense đầy đủ trong VSCode
Lỗi được phát hiện tại thời điểm biên dịch
Giao diện tùy chỉnh cho mỗi trạng thái -
✅ API nhất quán
Hợp đồng giống nhau cho trạng thái địa phương và toàn cầu
Đặt tên tiêu chuẩn hóa và trực quan
Hệ thống sự kiện thống nhất -
✅ Ngăn ngừa rò rỉ bộ nhớ
Phương thức clearListeners() để dọn dẹp dễ dàng
Các hàm gỡ bỏ được trả về bởi các listener
Tài liệu rõ ràng về các thực hành tốt nhất -
✅ Khả năng mở rộng
Hook tùy chỉnh cho logic đặc biệt
Tổ hợp tính năng
Tái sử dụng giữa các dự án -
✅ Trải nghiệm lập trình
Chế độ gỡ lỗi với log chi tiết
Xác thực giá trị tùy chỉnh
Thông báo lỗi rõ ràng
Hiệu suất
Thư viện chỉ là một lớp trừu tượng trên hệ thống native của Phaser (registry và data). Không có chi phí hiệu suất đáng kể, duy trì tất cả hiệu quả của động cơ gốc.
Kết luận
Phaser-hooks giải quyết những vấn đề thực sự mà mọi nhà phát triển Phaser đều gặp phải, cung cấp:
- Năng suất: Ít lỗi hơn, phát triển nhanh hơn
- Bảo trì: Mã sạch hơn và có tổ chức hơn
- Khả năng mở rộng: Hook tùy chỉnh cho logic phức tạp
- Độ tin cậy: An toàn kiểu và ngăn ngừa rò rỉ bộ nhớ
Cá nhân tôi đã sử dụng thư viện trong các dự án sản xuất của mình — cả trong Coin Flick Soccer (đang phát triển) và Smart Dots Reloaded. Sự khác biệt trong trải nghiệm phát triển là biến đổi.
Nếu bạn phát triển trò chơi với Phaser và muốn có trải nghiệm phát triển chuyên nghiệp và năng suất hơn, hãy thử phaser-hooks trong dự án tiếp theo của bạn.
Liên kết
GitHub: phaser-hooks
NPM: phaser-hooks
Phát triển với ❤️ cho cộng đồng Phaser