0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Quản lý trạng thái trong Phaser: Cách cách mạng với Phaser-Hooks

Đăng vào 1 tuần trước

• 9 phút đọc

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 Copy
// 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 Copy
// 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 Copy
// 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 Copy
$ 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 Copy
// Đị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ảnh
  • withGlobalState: 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 Copy
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 Copy
// 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 Copy
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 Copy
// 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 Copy
const state = withLocalState(scene, 'debug-state', initialValue, {
  debug: true  // Bật log chi tiết
});

Xác thực tùy chỉnh

javascript Copy
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

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào