Vòng Lặp Game - Các Code Mẫu

Vòng Lặp Game - Các Code Mẫu

Với một màn mở đầu không thể nào dài hơn, thì phần code cho một vòng lặp game thực ra khá là dễ hiểu. Chúng ta sẽ điểm qua một vài cách triển khai sau đó sẽ bàn tới những điểm tốt cũng như là không tốt của chúng

Vòng lặp game sẽ điều khiển AI, xuất hình ảnh, và các phần khác của hệ thống trò chơi, nhưng chúng lại không phải điểm mấu chốt của pattern này, và vì lẽ đó nên chúng ta sẽ gọi chúng bằng những hàm tượng chưng chức năng. Và cách để triển khai các hàm render(), update() và những hàm khác sẽ dành cho các bạn tự nghĩ ra cách giải quyết(khá là thử thách đấy ^^)

Chạy, chạy càng nhanh càng tốt!!!

Chúng ta đã từng nhìn thấy vòng lặp game này ở ngay phần trước:

while(true){
	xuLyDauVao();
	capNhat();
	xuatDoHoa();
}

Vấn đề của nó ở đây chính là việc bạn sẽ không thể kiểm soát được trò chơi sẽ chạy với tốc độ như thế nào. Với một thiết bị với phần cứng khủng, cái vòng lặp này sẽ chạy nhanh đến độ người dùng có khi còn không thể nhìn thấy được cái gì đang diễn ra. Ở phía ngược lại, thì trò chơi sẽ chạy như tốc độ crush phản hồi bạn vậy (với tôi là bị chặn rồi, khỏi phản hồi T_T). Nếu có phần nào của trò chơi mà cần xử lý nhiều phần nặng hoặc sử dụng nhiều AI hoặc tương tác vật lý, thì lúc đó trò chơi sẽ thực sự chơi chậm hơn bình thường.

Đi đâu mà vội mà vàng, nghỉ một chút đi em

Biến thể (variation) đầu tiên mà chúng ta sẽ cùng nhìn qua ở đây sẽ là một cách sửa đơn giản của vấn đề trên. Hãy đặt giả sử trò chơi của bạn sẽ chạy ở tốc độ 60FPS. Điều đó sẽ cho chúng ta một khoảng 16 mili giây / khung hình.

1000ms / FPS = ms per frame(mili giây trên khung hình)

Miễn là bạn vẫn tự tin rằng bạn có thể làm được tất cả mọi thứ từ tiến trình trò chơi đến xuất đồ họa với khoảng thời gian ngắn hơn cái 16ms đó, thì bạn sẽ có một tốc độ khung hình(frame rate) ổn định. Tất cả những gì bạn làm là xử lý khung hình và đợi đến khi bắt đầu thời gian cho khung hình tiếp theo, như thế này:

Hình tượng hóa quá trình game loop xử lý theo biến thể 1

Phần code sẽ trông hơi hơi giống như thế này:

while (true)
{
  double batDau = layThoiGianHienTai();
  xyLyDauVao(); //process input
  capNhat(); //update game
  xuatDoHoa(); //render

  ngu(batDau + MS_PER_FRAME - layThoiGianHienTai()); //MS_PER_FRAME là mili giây mỗi khung hình
}

ngu() ở đây sẽ đảm bảo rằng trò chơi sẽ không chạy quá nhanh nếu như nó xử lý được một khung hình trong thời gian ngắn. Nhưng nó sẽ không thể giúp trong trường hợp trò chơi của bạn chạy quá chậm. Nếu như nó chạy lâu hơn 16ms để cập nhật và xuất đồ họa khung hình, thì cái thời gian ngủ kia sẽ trở thành giá trị âm. Trong trường hợp chiếc máy của bạn có khả năng du hành thời gian, thì chắc hẳn cái hàm này sẽ tiện lợn lắm, nhưng tiếc là không có (。﹏。*).

Thay vào đó, trò chơi sẽ chậm đi (lại chả hiển nhiên quá). Chúng ta có thể xử lý tình huống bằng cách cắt giảm lượng công việc phải làm mỗi khung hình - như là giảm đồ họa hay cái gì đó hoặc làm cho AI của bạn ngu đi tẹo. Nhưng mà cái đó sẽ tác động đến chất lượng phần gameplay với tất cả người dùng, ngay cả với những chiếc máy xịn xò.

Bước thấp, bước cao

Hãy cùng thử một thứ đó một chút tinh vi hơn. Vấn đề mà chúng ta đang vướng mắc có thể phân tích thành:

  1. Mỗi lần cập nhật thúc đẩy (advance) thời gian trò chơi lên một khoảng nhất định
  2. Sẽ mất một khoảng thời gian thực để thực thi nó

Nếu như bước hai diễn ra lâu hơn bước một, thì trò chơi sẽ theo đó mà chậm lại. Nếu nó cần nhiều hơn 16ms xử lý để thúc đẩy thời gian trong trò chơi một khoảng 16ms, thì nó gần như không thể bắt kịp. Nhưng nếu chúng ta có thể thúc đấy trò chơi một khoảng thời gian trò chơi nhiều hơn 16ms trong một bước đơn, thì chúng ta có thể không cần phải cập nhật trò chơi quá thường xuyên mà vẫn bắt kịp được.

Ý tưởng ở đây chính là việc chọn ra một bước nhảy thời gian để thúc đẩy dựa vào khoảng thời gian thực tế đã đi qua tính từ khung hình trước đó. Khung hình mất càng nhiều thời gian, thì bước nhảy trò chơi càng lớn. Nó sẽ luôn luôn bắt kịp với thời gian thực tế vởi vì nó sẽ thực thi một bước ngày càng lớn hơn để tới đích. Ta sẽ gọi nó là bước nhảy thời gian dễ thay đổi(variable). Nó sẽ trông như thế này:

double thoiGianCuoi = layThoiGianHienTai();
while (true)
{
  double hienTai = layThoiGianHienTai();
  double troiQua = hienTai - thoiGianCuoi;
  xyLyDauVao();
  capNhat(troiQua);
  xuatDoHoa();
  thoiGianCuoi = hienTai;
}

Mỗi khung hình, chúng ta sẽ xác định khoảng thời gian thực tế đã trôi qua tính từ lần cuối trò chơi cập nhật(troiQua). Nếu như chúng ta cập nhật trạng thái game, chúng ta sẽ truyền nó vào. Engine sau đó sẽ phản hồi bằng cách thúc đẩy thế giới trò chơi về phía trước bằng với khoảng thời gian đó.

Thử liên tưởng rằng bạn có một viên đạn bắn ra ngang qua màn hình. Với một bước nhảy thời gian cố định, bạn sẽ tỉ lệ cái vecto vận tốc đó theo khoảng thời gian trôi qua. Với những bước nhảy thời gian trở nên lớn hơn, thì viên đạn theo đó sẽ di chuyển xa hơn trên mỗi khung hình. Viên đạn đó sẽ đi ngang qua màn hình với cùng một khoảng thời gian cho nó có là 20 bước nhỏ nhanh hay 4 bước to chậm. Ai-da, có vẻ chúng ta đã tìm được nhà vô địch bởi phát kiến vĩ đại này:

  • Trò chơi sẽ có thể chơi được ở một tốc độ thích hợp trên các phần cứng khác nhau
  • Người chơi với những cố máy khủng long sẽ được thưởng thức gameplay theo đó mà mượt mà hơn

Dù vậy, than ôi, có một vấn đề nghiêm trọng đang lẩn trốn (lurking) ở phía trước: Chúng ta khiến cho trò chơi không còn ổn định và "non-deterministic" - bất định

"Deterministic"- Tính xác định có nghĩa là mỗi khi bạn chạy chương trình, nếu như bạn đưa cho nó cùng một đầu vào, thì bạn sẽ nhận lại được chính xác cùng một loại đầu ra. Và bạn cũng có thể tưởng tượng được là, sẽ dễ hơn nhiều để có thể theo dõi được bug trong một chương trình xác định - nếu tìm ra được đầu vào khiến bug diễn ra lần đầu, thì bạn có thể khiến nõ diễn ra mọi lần khác.

Máy tính thì theo bản chất sẽ có tính xác định; nó đi theo chương trình cài sẵn một cách máy móc. Bất định sẽ xảy ra khi yếu tố thế giới thực nhớp nhúa trà trộn vào. Ví dụ như là mạng, đồng hồ hệ thống, hay lập lịch luồng đều sẽ phụ thuộc vào các bit của thế giới mở rộng bên ngoài tầm kiểm soát của chương trình.

Đây là một ví dụ nho nhỏ của chiếc bẫy mà chúng ta tự đặt cho chính mình:

Hãy nghĩ tới việc chúng ta có một trò chơi 2 người thông qua mạng với 1 bên là Tùng zới một con quái vật chuyên trị game và bên còn lại là Tú là con máy dùng lại của nhà mua chắc phải từ đời nhà Tống. Trên máy của Tùng, trò chơi chạy siêu nhanh, siêu mượt với mỗi bước thời gian nhỏ tí xíu. Cho là khoảng 50 FPS để khiến viên đạn bay qua màn hình đi. Còn cái máy của Tú thì chật vật mãi viên đạn nó mới nhích được với khoảng 5 khung hình.

Điều đó có nghĩa là là trên chiếc máy của Tùng, cái engine vật lý sẽ cập nhật vị trí viên đạn 50 lần, nhưng của Tú chỉ làm được điều đó 5 lần. Đa số các trò chơi theo đó đều dùng số thực(số với dấu chấm động), và đây thường là chủ đề gây ra các lỗi làm tròn. Mỗi lần bạn thêm vào 2 số thực, thì câu trả lời bạn nhận lại đôi khi sẽ có một sự đổi khác một chút. Chiếc máy của Tùng thực hiện được gấp mười lần số phép tính ấy, vì thế khi tính tổng lại sẽ tạo ra số lỗi lớn hơn với máy Tú. Dẫn đến kết quả cùng một viên đạn nhưng vị trí theo đó lại khác nhau giữa hai chiếc máy.

Đây chỉ là một ví dụ xấu xa mà biến thời gian có thể gây ra, nhưng còn nhiều lắm. Để có thể chạy được với thời gian thực, thì engine vật lý của trò chơi nên là những phép tính gần đúng của các định luật cơ học thực. Và để giữ cho các phép xấp xỉ đó không tăng lên, thì việc hãm lại là cần thiết. Những cái hãm đó được cẩn thận biến thành các bước nhảy thời gian xác đinh. Thay đổi nó, và phần vật lý sẽ không còn ổn định.

"Bùng lên" chính xác sẽ diễn ra ở đây. Khi một engine vật lý bị lỗi, thì các đối tượng có thể nhận về một vecto vận tốc sai hoàn toàn và theo đó bắn một phát bay lên tận trời xanh(Như cách các bạn thấy lỗi trong mấy trò chơi kiểu GTA,...)

Tính bất ổn này đủ tồi tệ đến mức cái ví dụ này chỉ ở đây như một câu chuyện cảnh báo và dẫn lối chúng ta đến những thứ tốt đẹp hơn...

Chơi tiếp sức

Một phần của engine mà thường sẽ không bị ảnh hưởng nhiều bởi biến thời gian chính là phần xuất đồ họa. Lý do của nó là bởi engine xuất đồ họa sẽ bắt lại một khoảng khắc trong thời gian, và nó sẽ chẳng thèm đoái hoài đến việc bao nhiêu thời gian đã trôi qua từ lần trước. Nó sẽ xuất thứ cần xuất bất kì chỗ nào nó được gọi đến mà thôi.

Chúng ta hoàn toàn có thể tận dụng nó làm lợi thế. Chúng ta sẽ cập nhật trò chơi sử dụng một bước thời gian cố định bởi vì nó sẽ khiến mọi thứ trở nên dễ dàng và ổn định hơn cho phần vật lý và AI. Nhưng chúng ta sẽ cho phép khả năng uyển chuyển khi chúng ta kiết xuất để có thể giải phóng một ít thời gian xử lý.

Chúng ta sẽ làm như thế này: Một khoảng thời gian cố định nào đó đã trôi qua kẻ từ lần vòng cuối cùng của vòng lặp thời gian. Đây chính là khoảng thời gian chúng ta cần để giả lập nó vào khoảng thời gian trò chơi "hiên tại" để bắt kịp với người chơi. Chúng ta thực hiện điều đó sử dụng một chuỗi các bước thời gian cố định. Code sẽ nhìn như thế này:

double truocDo= layThoiGianHienTai();
double lag = 0.0;
while (true)
{
  double hienTai = layThoiGianHienTai();
  double troiQua = hienTai - truocDo;
  truocDo= hienTai ;
  lag += troiQua ;

  xuLyDauVao();

  while (lag >= MS_MOI_CAPNHAT) //MS_MOI_CAPNHAT là mili giây trên mỗi cập nhật
  {
    capNhat();
    lag -= MS_MOI_CAPNHAT;
  }

  xuatDoHoa();
}

Một vài mảnh cần phân tích ở đây. Mỗi khi bắt đầu một khung hình, chúng ta sẽ cập nhật lag dựa vào bao nhiêu thời gian thực đã trôi qua. Nó sẽ tính toán hiệu số giữa đồng hồ trò chơi so với đồng hồ thực. Chúng ta sau đó có một vòng lặp lồng trong đó để cập nhật trò chơi, một bước thời gian cố định mỗi lần, cho đến khi nào nó bắt kịp. Và khi nó có thể bắt kịp, thì chúng ta xuất đồ họa và lặp lại vòng lặp. Chúng ta có thể hình tượng quá bằng hình vẽ như thế này:

Hình tượng hóa game loop xử lý theo biến thể 3

Một chút ghi chú ở đây là mỗi bước thời gian ở đây không còn là những tốc độ khung hình bằng số cụ thể nữa. MS_MOI_CAPNHAT chính là mức độ chi tiết chúng ta sử dụng để cập nhật trò chơi. Cái bước này càng ngắn, thì thời gian xử lý sẽ theo đó mà tăng lên để bất kịp với thời gian thực tế. Nó càng dài thì gameplay càng giật cục hơn. Nên là muốn lý tưởng á, thì bạn nên cho nó ngắn thôi, thường nên là nhanh hơn 60FPS, để trò chơi có thể mô phỏng lại trung thực hơn trên những cỗ máy quái vật phần cứng

Tuy nhiên cũng nên cẩn thận không nên làm nó quá ngắn. Bạn cần phải đảm bảo rằng mỗi bước thời gian phải lớn hơn thời gian nó cần để xử lý cái capNhat(), ngay cả trên những chiếc máy chậm nhất. Nếu không, trò chơi của bạn đơn giản là không còn có thể bắt kịp.

May mắn thay, chúng ta cũng kịp chuẩn bị cho mình một không gian thoáng đãng ở đây. Mẹo ở đây chính là việc chúng ta kéo việc xuất đồ họa ra ngoài vòng cập nhật. Nó sẽ giải phóng một lố thời gian CPU. Và kết quả cuối cùng sẽ là trò chơi có thể mô phỏng lại theo một tốc độ ổn định bằng việc sử dụng bước thời gian cố định xuyên suốt phạm vi phần cứng. Chỉ là cửa sổ của người chơi ở trong trò chơi sẽ trở nên giật cục hơn ở những chiếc máy chậm chạp

Kẹt lại ở chính giữa

Giờ chỉ còn lại một vấn đề còn tồn động, chính là phần lag bị dư. Chúng ta cập nhật trò chơi ở một bước nhảy cố định, nhưng chúng ta lại xuất đồ họa ở điểm bất kì(arbitrary) trong thời gian. Điều đó có nghĩa là đứng từ góc nhìn của người dùng, trò chơi thường sẽ chỉ hiển thị ở một thời điểm trong thời gian mà nằm ở giữa hai lần update.

Đường thời gian hiện tại

Như bạn có thể thấy, chúng ta cập nhật ở một khoảng thời gian cố định, chặt chẽ. Trong lúc đó, chúng ta lại xuất đồ họa lung tung xòe. Nó sẽ không còn thường xuyên như cập nhật, và nó cũng chả chắc chắn(steady) nữa. Cả hai đều vẫn khá là OK. Nhưng phần vớ vẩn ở đây chính là việc chúng ta không luôn luôn xuất đồ họa ngay vào thời điểm cập nhật. Nhìn vào lần xuất đồ họa thứ ba đi. Nó ở ngay giữa hai lần cập nhật:

Má nó xuất quả chủn hết hồn lun á @@

Vẫn quay lại ví dụ viên đạn "pằng chíu" qua màn hình. Ở lần cập nhật đầu tiên, nó ở bên trái. Cái cập nhật thứ hai nó sẽ ở bên phải. Trò chơi sẽ xuất đồ họa ở một điểm trong thời gian ở giữa hai lần cập nhật đó, theo lẽ đó người dùng sẽ mong chờ việc thấy viên đạn đó ở giữa màn hình. Nhưng mà với cách triển khai hiện tại của ta, nó vẫn đang ở bên trái. Nó sẽ làm cho chuyển động trông lắp bắp và giật cục.

Thuận tiện hay, chúng ta thực ra biết một cách chính xác hiệu số thời gian giữa hai lần cập nhật khung hình khi chúng ta xuất đồ họa: nó được lưu trữ trong lag. Chúng ta sẽ trợ giúp(bail out) vòng lặp thời gian khi nó nhỏ hơn bước thời gian cập nhật, chứ không phải khi nó bằng 0. Phần dư còn lại? Sẽ là khoảng cách đến với khung hình tiếp theo của chúng ta.

Và khi chúng ta đến với phần xuất đồ họa, chúng ta sẽ đưa nó vào:

xuatDoHoa(lag / MS_MOI_CAPNHAT);

Chúng ta chia cho MS_MOI_CAPNHAT ở đây để chuẩn hóa giá trị. Giá trị được truyền vào xuatDoHoa() sẽ chỉ ở trong khoảng từ 0(ngay từ khung hình trước đó) đến dưới 1.0(ngay khung hình tiếp theo), không còn phụ thuộc vào bước nhảy cập nhật nữa. Theo cách này, bộ kiết xuất không còn phải lo lắng về tốc độ khung hình. Nó chỉ phải chơi với các giá trị từ 0 đến 1.

Bộ xuất đồ họa biết mỗi đối tượng game cùng với vecto vận tốc hiện tại của nó. Nói rằng viên đạn đạng 20pixel khỏi phía bên trái màn hình và di chuyển sang phải 400pixel mỗi khung hình. Nếu chúng ta đang ở được một nửa giữa các khung hình, thì chúng ta sẽ cuối cùng truyền 0.5 vào xuatDoHoa(). Vì lẽ đó nó sẽ vẽ viên đạn một nửa khung hình trước, ở mốc 220pixel. ヾ(•ω•`)o, hoạt ảnh mượt mà.

Đương nhiên, sẽ có lúc tất cả phép đoán của chúng ta trở thành sai sót. Khi chúng ta tính toán cái khung hình tiếp theo, chúng ta có thể phát hiện ra là viên đạn đã bắn trúng một trướng ngại vật hay chậm lại hay gì đó. Chúng ta xuất vị trí của nó hoàn toàn dựa vào giả định ở giữa thời điểm nó ở trong khung hình trước và nơi mà chúng ta đoán nó sẽ ở trong khung hình tiếp theo. Nhưng thực sự chúng ta sẽ không thể biết chắc chắn nó ở đâu đến khi chúng ta thực thực làm hoàn thiện phần cập nhật với vật lý và AI.

Vậy phép tính toán ở đây sẽ là một phép thử và đôi khi nó sẽ không thể tránh khỏi việc sai sót. May thay, mấy cái kiểu sai đó thường sẽ khó mà nhận ra. Ít ra, nó sẽ khó nhận ra hơn việc giật cục bạn nhận được khi không đoán tí nào.

Những Lựa Chọn Thiết Kế

Một khi bạn đưa vào trò chơi của mình những thứ như đồng bộ với tốc độ làm mới màn hình(display's refresh rate), đa luồng, hay GPU, một vòng lặp game thực tế sẽ trở lên rối lắm đi nhiều. Ở mức độ cao hơn, vì thế, sẽ có một vài câu hỏi mà bạn sẽ lần phải trả lời:

Làm vòng lặp của riêng mình, hay làm dựa vào nền tảng?

Đây giống như kiểu lựa chọn nào dành cho bạn hơn là lựa chọn bạn đưa ra. Nếu bạn làm một trò chơi chạy trên trình duyệt web, bạn gần như không thể nào viết được một vòng lặp cơ bản của riêng bạn. Cái vòng lặp sự kiện của trình duyệt sẽ ngăn cản vòng lặp của bạn. Tương tự, nếu như bạn đang sử dụng một engine trò chơi cụ thể, thì gần như phải phu thuộc vào vòng lặp của nó thay vì tự tạo ra của riêng.

  • Sử dụng vòng lặp sự kiện của nền tảng:
    • Nó đơn giản. Bạn không còn phải lo lắng về việc viết và tối ưu hóa cái vòng lặp lõi của trò chơi.
    • Tương thích tốt với nền tảng hiện hành. Bạn không còn phải lo lắng về các vấn đề như cho máy tính chủ thời gian để xử lý sự kiện của chính nó, đệm hóa các sự kiện, hay là tìm cách quản lý sự sai sót phụ thuộc giữa các đầu vào của nền tảng và của bạn.
    • Bạn sẽ không thể kiểm soát được nhân tố thời gian. Nền tảng sẽ gọi code của bạn nếu như nó thấy hợp lý. Nếu như nó không được thường xuyên hay mượt mà như bạn muốn, không phải việc của nó. Tệ hơn thế, hầu hết các vòng lặp sự kiện của ứng dụng thường không được thiết kế để tương thích với các trò chơi trong tiềm thức và thường sẽ chậm chạp và giật cục.
  • Sử dụng vòng lặp của engine trò chơi:
    • Bạn không phải viết nó. Để tự viết vòng lặp có thể trở nên khá là khó khăn. Do vấn đề về code gốc được thực thi mỗi khung hình, các lỗi lặt vặt hay vấn đề hiệu năng có thể gây ra nhứng tác động lớn tới trò chơi của bạn. Một vòng lặp chặt chẽ(tight) là một trong những lý do nên cân nhắc sử dụng engine có sẵn.
    • Bạn không có khả năng viết nó. Đương nhiên, ở chiều ngược lại thì bạn sẽ không còn khả năng kiểm soát nó như ý bạn vì khả năng phù hợp của cách của bạn với engine bạn dùng là không hề hoàn hảo.
  • Tự viết vòng lặp của chính mình:
    • Toàn toàn tự chủ. Bạn thích làm gì thì làm, bạn có thể thiết kế nó phù hợp hoàn toàn với nhu cầu của trò chơi bạn đang phát triển.
    • Bạn phải tìm cách giao tiếp với nền tảng. Khung ứng dụng(Application frameworks) và hệ điều hành thường sẽ cần một khoảng thời gian để xử lý sự kiện và làm các việc khác). Nếu như bạn làm chủ vòng lặp của bạn, bạn sẽ không có cả hai thứ ở trên. Bạn sẽ cần phải tìm cách kiểm soát hoạt động một cách định kì để chắc chắn rằng cái khung của bạn không bị treo hoặc bị bối rối.

Làm thế nào để quản lý phần tiêu tốn điện năng?

Bạn sẽ cần phải cân nhắc nó đặc biệt trong thời buổi hiện nay, khi mà các thiết bị cầm tay, điện thoại, hay laptop đang lên ngôi trong việc sử dụng đài trà. Một chiếc game tuyệt vời chạy mượt mà mà biến cái điện thoại của người chơi thành cái lò nướng và vắt kiệt từng giọt năng lượng trong 30p chắc chắn không phải một trò chơi mà người chơi thích thú.

Bạn không chỉ cần phải nghĩ đến việc làm thế nào khiến trò chơi của bạn trở nên tuyệt vời, mà còn phải sử dụng càng ít CPU càng tốt. Có thể sẽ có giới hạn đối với hiệu suất nơi mà bạn để CPU nghỉ một chút nếu như bạn đã làm xong tất cả công việc bạn cần phải làm trong một khung hình.

  • Chạy càng nhanh càng tốt:

    Đây là thứ bạn thường làm với game trên PC(cho dù gần đây càng nhiều trò được chơi trên laptop hơn). Cái vòng lặp game của bạn sẽ không bào giờ cho OS được nghỉ ngơi, mọi khoảng trống sẽ được tận dụng để tăng cường FPS hay đồ họa lên một chút.

    Nó sẽ đem lại gameplay tuyệt nhất có thể, nhưng nó cũng ngốn tài nguyên từng đó. Nếu như người chơi ở trên laptop, tôi nghĩ chỗ này là chỗ ưa thích của lũ mèo cũng không phải lý do gì quá khó hiểu.

  • Giới hạn tốc độ khung hình:

    Trò chơi di động thường sẽ tập chung nhiều hơn vào chất lượng gameplay hơn là cố gắng tối đa hóa chi tiết đồ họa. Rất nhiều trò chơi trong số đó sẽ có một giới hạn trên giới hạn tốc độ khung hình(thường thường là 30 hoặc 60FPS). Nếu như vòng lặp game hoàn thiện xử lý trước khi phần thời gian kia được sử dụng, nó sẽ có thể nghỉ ngơi đến khi hết luôn quãng thời gian đó.

    Nó sẽ đưa cho người chơi một trải nghiệm vừa đủ và giúp cho phần pin cũng vì thế mà dễ thở hơn.

Cách nào để kiểm soát tốc độ gameplay?

Một vòng lặp trò chơi cần hai mảnh khóa cần để tâm: không chặn đầu vào người dùng và khả năng thích nghi với sự trôi dạt của thời gian. Đầu vào thì khá là rõ ràng. Nhưng phép màu ở này thì nằm ở cách bạn đối mặt với thời gian. Có gần như một số vô bạn các nền tảng mà một trò chơi có thể chạy trên và bất kì một trò chơi nào cần phải chạy trên ít nhất một vài trong số đó. Cách thích ứng sự đa dạng đó chính là chìa khóa.

  • Bước thời gian cố định mà không có đồng bộ:

    Đây là code mẫu đầu tiên của chúng ta, bạn chỉ cần chạy vòng lặp trò chơi càng nhanh càng tốt

    • Nó đơn giản. Đây là nhân tố chính((⊙_⊙;), duy nhất).
    • Tốc độ trò chơi bị ảnh hưởng trực tiếp do phần cứng và độ phức tạp trò chơi. Và tội chính của nó chính là việc nếu như có bất kì yếu tố đa dạng nào, nó sẽ ảnh hưởng trực tiếp đến tốc độ trò chơi.
  • Vòng lặp cố định với đồng bộ hóa:

    Bước tiến tiếp theo trên nấng thang phức tạp là chạy trò chơi ở một bước thời gian cố định nhưng thêm vào đó độ chễ hay điểm đồng bộ ở cuối vòng lặp để giữ cho trò chơi khỏi việc chạy quá nhanh.

    • Vẫn còn khá đơn giản. Nó gần như chỉ hơn 1 dòng code so với cái ví dụ có-lẽ-quá-giản-đơn-để-chạy-thực-tế(・∀・(・∀・(・∀・*). Ở đa số các vòng lặp game, bạn gần như đều sẽ làm công việc đồng bộ hóa cả. Có lẽ bạn sẽ nhân đôi bộ đệm hình ảnh của bạn và đồng bộ bộ đệm lật với tốc độ làm mới của màn hình.

    • Nó tiết kiệm điện năng. Điều này quan trọng một cách đáng ngạc nhiên nếu như cân nhắc cho các trò chơi di động. Bạn không hề muốn tiêu sạch pin của người dùng một cách không cần thiết. Nhưng chỉ bằng việc ngủ trong một vài mili giây thay vì cố lắng làm càng nhiều thứ càng tốt trong mỗi khoảng khắc, bạn đang tiết kiệm pin rồi đó.

    • Trò chơi sẽ không còn chạy quá nhanh. Nó đã sửa được một nửa nỗi lo về tốc độ của vòng lặp cố định.

    • Trò chơi có thể chơi chậm đi. Nếu như nó mất quá nhiều thời gian để cập nhật và xuất đồ họa một khung hình trò chơi, tốc độ chơi sẽ chậm đi. Bởi vì nó không chia cách phần cập nhật khỏi phần xuất đồ họa, việc chậm lại sẽ thường xảy ra nhanh hơn các lựa chọn nâng cao về sau. Thay vì việc chỉ giảm tốc độ xuất khung hình để bắt kịp, thì trò chơi sẽ chậm lại.

  • Thời gian bất định:

Tôi sẽ đặt nó ở đây như là một lựa chọn trong không gian giải pháp mặc cho việc gần như mọi nhà lập trình trò chơi hiện nay gần như đều khuyên không nên dùng nó. Sẽ là một điều tốt để nhớ đến việc vì sao đây lại là ý tưởng tồi tệ.

    • Nó thích nghi với việc chơi quá chậm hay quá nhanh. Nếu như trò chơi không thể bắt kịp với thời gian thực tế, nó chỉ cần lấy một bước thời gian lớn hơn mỗi lần đến khi bắt kịp thì thôi.

    • Nó khiến cho gameplay trở lên không xác định và bất ổn. Đây chính là vấn đề chính của nó. Vật lý hay mạng trong thực tế trở nên khó hơn nhiều khi liên quan đến biến thời gian.

  • Cập nhật bước thời gian cố đinh, xuất đồ họa bất định:

    Lựa chọn cuối cùng chúng ta bao quát ở phần code mẫu ở trên là phức tạp nhất, nhưng nó cũng dễ thích nghi nhất. Nó cập nhật với bước thời gian cố định, nhưng nó có thể giảm tốc độ khung hình nếu cần thiết để bắt kịp với đồng hồ của người chơi.

    • Nó thích nghi để cớ thể chơi được ở tốc độ quá chậm hay quá nhanh. Miễn là trò chơi có thể cập nhật theo thời gian thực, nó sẽ không bị bỏ lại phía sau. nếu như cỗ máy của người chơi thuộc hàng khủng, thì theo đó kết quả trả về là trải nghiệm gameplay mượt mà hơn.
    • Nó phức tạp hơn nhiều. Điểm yếu chính của nó sẽ là sẽ cần nhiều hơn để có thể triển khải. Bạn cần phải khiến cho việc cập nhật bước thời gian phải đảm bảo cả vừa càng nhỏ càng tốt cho những cố máy hàng khủng, lại không bị quá chậm chạp với các máy đời yếu.

Nguồn tham khảo:

Đây là một bản dịch không chuyên của mình từ một chủ đề trong cuốn gameprogrammingpattern, nếu thấy bài này khó hiểu và muốn đọc bản gốc bạn có thể tham khảo theo nguồn này: Game Loop · Sequencing Patterns · Game Programming Patterns

Rất cảm ơn mọi người đã đọc bài viết của mình