Bỏ qua điều hướng

Làm game thể thao 2D trên trình duyệt: input, physics và "juice"

Bài chia sẻ từ Viblo phân tích cách một game bóng bầu dục arcade 2D trên trình duyệt tạo cảm giác chơi mượt: xử lý bàn phím, fixed timestep, hitbox và các hiệu ứng phản hồi.

Administrator18 phút đọc2 lượt xem
Làm game thể thao 2D trên trình duyệt: input, physics và "juice"

Làm game 2D trên trình duyệt mượt hơn: Tối ưu Input, Physics và hiệu ứng bằng JavaScript

Một trò chơi 2D không cần đồ họa phức tạp để mang lại trải nghiệm hấp dẫn. Điều quyết định cảm giác chơi thường nằm ở độ nhạy điều khiển, hệ thống va chạm chính xác và các hiệu ứng phản hồi đúng thời điểm. Vậy làm thế nào để tối ưu những yếu tố này khi phát triển game bằng JavaScript và HTML5 Canvas?

Game chạy trực tiếp trên trình duyệt đang trở thành lựa chọn phù hợp cho nhiều dự án, từ trò chơi giải trí, ứng dụng giáo dục đến mini-game trong các chiến dịch marketing.

Nhờ sự phát triển của JavaScript, HTML5 Canvas và các Web API, lập trình viên có thể xây dựng trò chơi 2D mà không nhất thiết phải sử dụng một game engine lớn.

Tuy nhiên, một vấn đề thường xuất hiện khi phát triển game trình duyệt là trò chơi chạy được nhưng cảm giác điều khiển không thực sự tốt.

Nhân vật đôi khi phản hồi chậm, chuyển động không nhất quán trên các thiết bị, va chạm thiếu tự nhiên hoặc hiệu ứng không đủ rõ ràng.

Một bài phân tích kỹ thuật trên Viblo, sử dụng game bóng bầu dục arcade Football Bros làm ví dụ tham chiếu, đã đề cập đến ba thành phần cốt lõi để cải thiện trải nghiệm:

  • Input: Cách nhận và xử lý thao tác từ người chơi.
  • Physics: Cách cập nhật chuyển động và tính toán va chạm.
  • Juice: Các hiệu ứng hình ảnh, âm thanh và chuyển động giúp hành động có sức nặng.

Đây đều là những kỹ thuật có thể áp dụng cho nhiều thể loại game 2D khác nhau.

1. Vì sao game có FPS cao vẫn có thể gây khó chịu?

Nhiều lập trình viên cho rằng chỉ cần đạt 60 FPS hoặc cao hơn là trò chơi đã đủ mượt.

Trên thực tế, FPS chỉ phản ánh một phần trải nghiệm.

Một game chạy ở 120 FPS vẫn có thể tạo cảm giác thiếu chính xác nếu thao tác của người chơi không được xử lý đúng.

Ví dụ, trong một trò chơi bóng bầu dục arcade, người chơi bấm phím để thực hiện cú tackle. Nếu nhân vật đang trong trạng thái chưa thể hành động, lần bấm đó có thể bị bỏ qua.

Ngay sau đó, nhân vật trở lại trạng thái bình thường nhưng không thực hiện cú tackle.

Người chơi sẽ cảm thấy trò chơi không nhận phím, dù FPS vẫn ổn định.

Tương tự, nếu chuyển động được tính trực tiếp theo số khung hình, nhân vật có thể di chuyển nhanh hơn trên màn hình 144 Hz so với màn hình 60 Hz.

Do đó, để xây dựng một game 2D chất lượng, lập trình viên cần giải quyết đồng thời ba vấn đề:

  1. Đảm bảo thao tác người chơi được ghi nhận chính xác.
  2. Giữ mô phỏng và va chạm nhất quán giữa các thiết bị.
  3. Tạo phản hồi rõ ràng khi hành động diễn ra.

2. Xử lý Input: Nền tảng của một game điều khiển tốt

Input là điểm tiếp xúc trực tiếp giữa người chơi và trò chơi.

Nếu hệ thống input hoạt động không ổn định, những yếu tố còn lại rất khó bù đắp được.

Sử dụng KeyboardEvent.code thay vì key

JavaScript cung cấp hai thuộc tính phổ biến để xác định phím được nhấn: event.key và event.code.

event.key phản ánh ký tự hoặc giá trị phím theo bố cục bàn phím hiện tại.

Trong khi đó, event.code xác định vị trí vật lý của phím.

Đối với các game sử dụng WASD để di chuyển, event.code thường phù hợp hơn khi muốn giữ nguyên vị trí điều khiển giữa những bố cục bàn phím khác nhau.

Ví dụ:

const pressedKeys = new Set();

window.addEventListener("keydown", (event) => {
  if (["Space", "ArrowUp", "ArrowDown"].includes(event.code)) {
    event.preventDefault();
  }

  pressedKeys.add(event.code);
});

window.addEventListener("keyup", (event) => {
  pressedKeys.delete(event.code);
});

window.addEventListener("blur", () => {
  pressedKeys.clear();
});

Đoạn mã lưu trạng thái các phím đang được giữ trong một Set.

Nhờ vậy, game có thể kiểm tra trạng thái bàn phím tại mỗi bước cập nhật thay vì phụ thuộc hoàn toàn vào từng sự kiện riêng lẻ.

Việc xóa trạng thái khi cửa sổ mất focus giúp giảm lỗi kẹt phím sau khi người chơi chuyển sang ứng dụng khác.

Input Buffering: Không bỏ lỡ thao tác của người chơi

Một kỹ thuật quan trọng khi phát triển game hành động là Input Buffering.

Thay vì bỏ qua hành động khi nhân vật chưa sẵn sàng, game có thể tạm lưu thao tác trong một khoảng thời gian ngắn.

Ví dụ:

  • Người chơi nhấn nút tấn công.
  • Nhân vật đang ở cuối hoạt ảnh trước đó.
  • Hệ thống giữ lệnh tấn công trong khoảng 100 mili giây.
  • Khi nhân vật có thể hành động, lệnh được thực hiện nếu vẫn còn hiệu lực.

Với mô phỏng 60 bước mỗi giây, khoảng 100 mili giây tương đương xấp xỉ 6 bước cập nhật.

const BUFFER_TICKS = 6;
const pressedAt = new Map();

let tick = 0;

function bufferPress(action) {
  pressedAt.set(action, tick);
}

function consumePress(action) {
  const pressedTick = pressedAt.get(action);

  if (pressedTick === undefined) return false;

  const isValid = tick - pressedTick <= BUFFER_TICKS;

  pressedAt.delete(action);
  return isValid;
}

Trong game loop, tick được tăng sau mỗi bước mô phỏng.

Khi nhân vật sẵn sàng, game kiểm tra xem có thao tác còn hiệu lực trong bộ đệm hay không.

Kỹ thuật này đặc biệt hữu ích cho game platformer, đối kháng, thể thao và hành động tốc độ cao.

Tuy nhiên, thời lượng buffer cần được tinh chỉnh. Nếu quá dài, game có thể thực hiện những hành động mà người chơi không còn mong muốn.

Lưu ý về bàn phím nhiều người chơi

Nếu game cho phép hai người chơi sử dụng chung một bàn phím, cần lưu ý hiện tượng keyboard ghosting.

Một số bàn phím không thể ghi nhận chính xác nhiều phím được nhấn đồng thời.

Đây là giới hạn phần cứng, không thể giải quyết hoàn toàn bằng JavaScript.

Nhà phát triển nên bố trí các cụm phím điều khiển cách xa nhau, hỗ trợ thay đổi key binding và kiểm thử trên nhiều loại bàn phím.

3. Fixed Timestep: Giữ chuyển động ổn định trên mọi thiết bị

Một trong những lỗi phổ biến khi làm game JavaScript là gắn trực tiếp tốc độ mô phỏng với tốc độ render.

Các màn hình hiện đại có thể hoạt động ở 60 Hz, 120 Hz hoặc 144 Hz.

Nếu mỗi lần render đều cập nhật chuyển động với một lượng cố định, tốc độ game sẽ phụ thuộc vào tần số quét.

Giải pháp phổ biến là Fixed Timestep.

Fixed Timestep hoạt động như thế nào?

Thay vì cập nhật vật lý theo số lần trình duyệt vẽ khung hình, hệ thống chia thời gian thành các bước cố định.

Ví dụ, với mục tiêu mô phỏng 60 lần mỗi giây:

const STEP = 1000 / 60;
let accumulator = 0;
let lastTime = null;

function gameLoop(timestamp) {
  if (lastTime === null) {
    lastTime = timestamp;
  }

  const delta = Math.min(timestamp - lastTime, 250);
  lastTime = timestamp;

  accumulator += delta;

  while (accumulator >= STEP) {
    update(STEP / 1000);
    accumulator -= STEP;
  }

  render(accumulator / STEP);

  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

Ở đây:

  • requestAnimationFrame đồng bộ việc vẽ với lịch render của trình duyệt.
  • accumulator tích lũy thời gian chưa được mô phỏng.
  • update() xử lý logic với bước thời gian cố định.
  • render() vẽ trạng thái trò chơi và có thể sử dụng giá trị nội suy.

Việc giới hạn delta ở 250 mili giây giúp tránh phải thực hiện quá nhiều bước cập nhật sau khi tab bị tạm dừng hoặc thiết bị gặp tình trạng giật lag.

Đây là giá trị khởi đầu để thử nghiệm, không phải cấu hình tối ưu cho mọi game.

Nội suy vị trí giúp chuyển động mượt hơn

Khi tốc độ render cao hơn tốc độ mô phỏng, có thể sử dụng interpolation để tạo chuyển động liên tục.

Công thức phổ biến:

const renderX = previousX
  + (currentX - previousX) * alpha;

Trong đó, alpha là tỷ lệ thời gian còn lại giữa hai bước mô phỏng.

Kỹ thuật này giúp tránh hiện tượng giật hình khi vẽ chuyển động trên màn hình có tần số quét cao.

Cần lưu trạng thái trước và sau mỗi bước cập nhật để nội suy chính xác.

4. Collision Detection: Tối ưu hệ thống va chạm 2D

Không phải game 2D nào cũng cần một physics engine phức tạp.

Đối với các game arcade đơn giản, hệ thống va chạm dựa trên hình chữ nhật thường đủ để xử lý các tình huống phổ biến.

Một phương pháp được sử dụng rộng rãi là AABB, viết tắt của Axis-Aligned Bounding Box.

AABB là gì?

AABB biểu diễn một đối tượng bằng hình chữ nhật có các cạnh song song với trục tọa độ.

Hai đối tượng được xem là va chạm nếu các vùng hình chữ nhật giao nhau.

Ví dụ:

function isColliding(a, b) {
  return (
    a.x < b.x + b.width &&
    a.x + a.width > b.x &&
    a.y < b.y + b.height &&
    a.y + a.height > b.y
  );
}

Phương pháp này có ưu điểm là dễ triển khai, chi phí tính toán thấp và dễ kiểm tra khi xảy ra lỗi.

Tuy nhiên, AABB không phù hợp với mọi trường hợp, đặc biệt khi đối tượng có hình dạng phức tạp hoặc xoay nhiều.

Phân biệt Hitbox và Hurtbox

Trong các trò chơi hành động, lập trình viên thường tách riêng hai vùng va chạm.

Hitbox là vùng mà một hành động có thể gây tác động, chẳng hạn cú đấm, cú đá hoặc cú tackle.

Hurtbox là vùng mà nhân vật có thể nhận tác động từ hành động của đối phương.

Ví dụ, khi nhân vật thực hiện cú tackle, hitbox có thể chỉ xuất hiện trong một số bước cập nhật nhất định.

Nhờ đó, game có thể phân biệt ba giai đoạn của hành động:

  • Startup: Chuẩn bị hành động.
  • Active: Hành động có thể gây va chạm.
  • Recovery: Nhân vật trở về trạng thái có thể điều khiển.

Việc phân chia các giai đoạn giúp cân bằng độ khó và tạo cảm giác tương tác chính xác.

Một lưu ý quan trọng là không nên tạo quá nhiều đối tượng va chạm mới ở mỗi khung hình nếu có thể tái sử dụng dữ liệu sẵn có.

5. Juice: Bí quyết khiến trò chơi có cảm giác hấp dẫn

Trong phát triển game, thuật ngữ Juice thường được sử dụng để chỉ những hiệu ứng bổ sung làm cho hành động trở nên sống động và thỏa mãn hơn.

Đây không nhất thiết là các hiệu ứng đồ họa đắt tiền.

Đôi khi, chỉ cần một chuyển động camera ngắn hoặc vài khung hình đóng băng cũng đủ làm một cú va chạm có cảm giác mạnh hơn.

Hit-stop: Tạo sức nặng cho va chạm

Hit-stop là kỹ thuật tạm dừng hoặc làm chậm một phần mô phỏng trong thời gian rất ngắn khi có tác động quan trọng.

Ví dụ, khi nhân vật tung ra một cú đánh mạnh, trò chơi có thể dừng chuyển động tương ứng trong khoảng 3–5 bước mô phỏng.

Điều này giúp người chơi cảm nhận rõ thời điểm va chạm.

Tuy nhiên, nên kiểm soát phạm vi tạm dừng để không làm hỏng việc xử lý input, âm thanh hoặc các cơ chế thời gian thực quan trọng.

Screen Shake: Rung màn hình đúng lúc

Screen shake là hiệu ứng dịch chuyển camera trong một khoảng thời gian ngắn.

Hiệu ứng này thường được sử dụng khi nhân vật bị tấn công, xảy ra vụ nổ hoặc một sự kiện quan trọng diễn ra.

Một cách triển khai là sử dụng biến trauma để đại diện cho cường độ rung.

let trauma = 0;

function addShake(amount) {
  trauma = Math.min(1, trauma + amount);
}

function updateShake(dt) {
  trauma = Math.max(0, trauma - dt * 1.5);
}

function getShakeOffset() {
  const strength = trauma * trauma;

  return {
    x: (Math.random() * 2 - 1) * 8 * strength,
    y: (Math.random() * 2 - 1) * 8 * strength
  };
}

Khi cường độ giảm dần, hiệu ứng rung cũng nhẹ đi.

Lập trình viên có thể kết hợp với easing để tạo chuyển động tự nhiên hơn.

Dù vậy, không nên lạm dụng screen shake. Rung liên tục có thể gây khó chịu và ảnh hưởng đến khả năng quan sát của người chơi.

Nên cung cấp tùy chọn giảm hoặc tắt hiệu ứng rung, đồng thời cân nhắc thiết lập prefers-reduced-motion của hệ điều hành.

Những hiệu ứng nhỏ khác

Ngoài hit-stop và screen shake, các game 2D có thể sử dụng particle effect, squash and stretch, âm thanh va chạm và hiệu ứng thay đổi màu sắc.

Điểm quan trọng là hiệu ứng phải phản ánh đúng hành động.

Một cú va chạm nhẹ không nên có hiệu ứng giống một vụ nổ lớn.

6. Tối ưu game 2D trên điện thoại

Việc hỗ trợ trình duyệt di động là yếu tố quan trọng khi phát triển game HTML5.

Không giống desktop, điện thoại sử dụng màn hình cảm ứng, có mật độ điểm ảnh cao và giới hạn tài nguyên phần cứng khác nhau.

Sử dụng Pointer Events

Pointer Events cho phép quản lý thao tác từ chuột, bút cảm ứng và ngón tay thông qua một mô hình sự kiện chung.

Với game có joystick ảo và nút hành động, lập trình viên có thể theo dõi từng ngón tay bằng pointerId.

Điều này giúp tránh tình trạng một ngón tay đang điều khiển nhân vật bị ảnh hưởng khi ngón khác nhấn nút tấn công.

Khi cần kiểm soát thao tác cảm ứng trong vùng game, có thể sử dụng:

.game-canvas {
  touch-action: none;
}

Nên chỉ áp dụng thuộc tính này cho vùng tương tác của game thay vì toàn bộ trang, nhằm tránh vô hiệu hóa những thao tác cuộn hoặc phóng to không cần thiết.

Hiển thị rõ nét trên màn hình HiDPI

Canvas có thể trông mờ nếu kích thước vùng vẽ không tương ứng với mật độ điểm ảnh của màn hình.

Để xử lý, có thể sử dụng devicePixelRatio.

function resizeCanvas(canvas, ctx, width, height) {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);

  canvas.width = Math.round(width * dpr);
  canvas.height = Math.round(height * dpr);

  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;

  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

Giới hạn DPR ở mức 2 là một phương án cân bằng giữa độ sắc nét và khối lượng xử lý đồ họa.

Giá trị phù hợp cần được kiểm thử trên thiết bị thực tế.

7. Giảm giật lag bằng cách hạn chế tạo dữ liệu trong Game Loop

Một nguyên nhân gây giật hình trong game JavaScript là tạo quá nhiều đối tượng tạm thời trong vòng lặp cập nhật.

Khi bộ nhớ cần được thu hồi, Garbage Collector có thể tạo ra các khoảng dừng ảnh hưởng đến thời gian xử lý khung hình.

Một số biện pháp nên áp dụng gồm:

  • Tái sử dụng các đối tượng vector, hitbox và dữ liệu tạm.
  • Sử dụng object pooling cho particle hoặc những đối tượng được tạo và hủy thường xuyên.
  • Hạn chế các phép tính hoặc thao tác DOM không cần thiết trong mỗi khung hình.
  • Vẽ trước các thành phần tĩnh vào canvas phụ khi phù hợp.
  • Giảm số lần thay đổi trạng thái render của Canvas.
  • Tạm dừng hoặc điều chỉnh vòng lặp khi tab không còn hiển thị.

Không phải mọi lần tạo đối tượng đều gây vấn đề về hiệu năng. Nhà phát triển nên sử dụng công cụ Performance trong trình duyệt để xác định nguyên nhân thực tế trước khi tối ưu.

8. Checklist xây dựng game 2D bằng JavaScript

Nếu đang phát triển một game mới, bạn có thể triển khai các kỹ thuật trên theo thứ tự sau:

Giai đoạnCông việcMục tiêu
1Input và key mappingĐiều khiển chính xác
2Input bufferingGiảm thao tác bị bỏ lỡ
3Fixed timestepMô phỏng nhất quán
4InterpolationChuyển động mượt
5AABB và hitboxVa chạm rõ ràng
6Hit-stop và screen shakeCải thiện phản hồi
7Pointer EventsHỗ trợ cảm ứng
8DPR và tối ưu bộ nhớHoạt động tốt trên mobile
9Kiểm thử hiệu năngPhát hiện nghẽn thực tế

Nên thử nghiệm ở các mức tần số quét khác nhau, trên thiết bị có cấu hình thấp và trong các tình huống trình duyệt bị mất focus.

Đặc biệt, hãy kiểm tra độ trễ đầu vào, tốc độ mô phỏng, thời gian xử lý mỗi frame và phản hồi của người chơi thay vì chỉ dựa vào FPS trung bình.

9. Những lỗi thường gặp khi phát triển game HTML5

Lỗi 1: Nhân vật di chuyển nhanh hơn trên màn hình có tần số quét cao.

Nguyên nhân thường là tốc độ chuyển động phụ thuộc trực tiếp vào số lần render. Cần sử dụng thời gian mô phỏng phù hợp, chẳng hạn fixed timestep.

Lỗi 2: Nhân vật tiếp tục di chuyển sau khi người chơi thả phím.

Nguyên nhân có thể do sự kiện keyup bị bỏ lỡ. Nên xóa trạng thái bàn phím khi cửa sổ mất focus.

Lỗi 3: Người chơi nhấn nút nhưng hành động không diễn ra.

Có thể do nhân vật chưa kết thúc hoạt ảnh hoặc đang ở trạng thái không cho phép hành động. Input buffering có thể cải thiện tình huống này.

Lỗi 4: Game mượt trên máy tính nhưng giật trên điện thoại.

Nguyên nhân có thể liên quan đến kích thước Canvas, mật độ điểm ảnh, số lượng đối tượng cần vẽ hoặc khối lượng xử lý JavaScript.

Lỗi 5: Game có nhiều hiệu ứng nhưng vẫn thiếu cảm giác điều khiển.

Các hiệu ứng hình ảnh không thể thay thế cho input chính xác và hệ thống va chạm ổn định. Cần hoàn thiện cơ chế điều khiển trước khi bổ sung hiệu ứng.

10. Có cần sử dụng game engine để xây dựng game 2D trên web?

Không phải lúc nào cũng cần.

Đối với mini-game đơn giản, JavaScript kết hợp HTML5 Canvas có thể đáp ứng tốt yêu cầu phát triển.

Cách tiếp cận này phù hợp khi muốn kiểm soát trực tiếp vòng lặp game, hệ thống input và cơ chế render.

Tuy nhiên, với những dự án lớn hơn, sử dụng engine hoặc framework có thể giảm thời gian xây dựng các thành phần nền tảng.

Ví dụ, Phaser là một lựa chọn phổ biến cho game 2D dựa trên JavaScript hoặc TypeScript, cung cấp nhiều công cụ hỗ trợ quản lý scene, animation, physics và input.

Lựa chọn phụ thuộc vào quy mô dự án, yêu cầu hiệu năng, thời gian phát triển và kinh nghiệm của nhóm kỹ thuật.

Kết luận

Để xây dựng một game 2D hấp dẫn trên trình duyệt, đồ họa đẹp chỉ là một phần của trải nghiệm.

Điều tạo ra sự khác biệt thường nằm ở những kỹ thuật khó nhận thấy: xử lý phím chính xác, lưu thao tác đúng thời điểm, mô phỏng vật lý ổn định, va chạm hợp lý và hiệu ứng phản hồi có chủ đích.

Các kỹ thuật như Input Buffering, Fixed Timestep, AABB Collision, Hit-stop và Screen Shake không quá phức tạp để triển khai, nhưng khi kết hợp đúng cách có thể cải thiện đáng kể cảm giác chơi.

Đối với lập trình viên frontend hoặc các nhóm phát triển game indie, đây là những kiến thức nền tảng đáng đầu tư ngay từ giai đoạn prototype.

Thay vì bắt đầu bằng những hiệu ứng đồ họa phức tạp, hãy ưu tiên làm cho một thao tác di chuyển, một cú nhảy hoặc một pha va chạm trở nên chính xác và dễ chịu.

Một trò chơi điều khiển tốt ngay từ bản thử nghiệm sẽ có nền tảng thuận lợi hơn để phát triển thành sản phẩm hoàn chỉnh.


Nguồn tham khảo

Bài viết được biên tập và bổ sung các ví dụ triển khai để phục vụ mục đích học tập. Các đoạn mã là ví dụ minh họa, không phải mã nguồn được xác nhận của Football Bros. Các thông số và hiệu năng cần được kiểm thử theo từng dự án.

Chia sẻ:FacebookXTelegram

Bài viết liên quan

Bình luận

Đang tải bình luận…