Game loop – nhịp đập con tim của game

Không game nào có thể chạy nếu thiếu game loop, và cũng ko thể chạy tốt nếu game loop ko được cài đặt một cách cẩn thận.

The game loop

Tất cả các game đều gồm một dãy các công việc sau được lặp đi lặp lại: lấy input từ user, cập nhật trạng thái game (game state), xử lý trí tuệ nhân tạo (AI), phát âm thanh, và vẽ lên màn hình đồ họa. Các công việc đó được thực hiện bên trong game loop. Tuy nhiên, chỉ có 2 công việc chính quan trọng nhất: update game state và render graphic.

Đây là đoạn code mẫu đơn giản cho một kiểu game loop đơn giản:

bool game_is_running = true;
while (game_is_running)
{
    update();
    render();
}

Vấn đề của đoạn code trên là nó không thỏa mãn tính “real time”, vốn là tính chất quan trọng của game. Trên phần cứng mạnh, nó chạy nhanh, và ngược lại. Thế nên, chúng ta sẽ xem xét một vài cách cài đặt game loop tốt hơn.

Trước hết, có 2 khái niệm cơ bản cần được giải thích:

  • FPS: là Frames Per Second, là số lần hàm render() được gọi trong 1 giây.
  • Game Speed: là số lần gọi hàm update() trong 1 giây.

FPS phụ thuộc vào Game Speed cố định trước

Một giải pháp rất dễ là chúng ta cố định số FPS, ví dụ là 25, tức là sau khi cả update() và render() được gọi, cho dù là xong sớm, ta vẫn phải nghỉ (sleep) một chút cho phù hợp rồi mới lặp tiếp. Điều này đảm bảo cho game loop chạy được đúng 25 lần trong 1 giây.

const int FRAMES_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;

DWORD next_game_tick = GetTickCount();
/* GetTickCount() là hàm trên Win32 trả về số milliseconds đếm được kể từ khi máy tính bật lên */

int sleep_time = 0;
bool game_is_running = true;

while (game_is_running)
{
    update();
    render();
    next_game_tick += SKIP_TICKS;
    sleep_time = next_game_tick - GetTickCount();
    if (sleep_time >= 0) {
        Sleep(sleep_time);
    } else
        // Shit, ko được nghỉ tí nào
    }
}

Kiểu game loop này có 1 ưu điểm lớn: dễ cài đặt. Do số lần update() là biết trước nên việc viết code không có gì khó khăn. Ví dụ, trong game có tính năng replay, đơn giản ta chỉ cần log lại input của user và replay khi cần sau này (tất nhiên là game ko có giá trị ngẫu nhiên được sinh ra).

Tùy vào phần cứng, ta cần chọn FPS cho phù hợp.

Slow hardware: nếu sleep_time luôn >= 0 thì ko có vấn đề gì, nhưng nếu thỉnh thoảng ko được như thế, game loop ko được nghỉ, và game sẽ có lúc chạy chậm, lúc chạy nhanh, ko ổn định.

Fast hardware: ko có vấn đề gì ngoại trừ việc ta hơi tiếc khi game có thể chạy ở mức FPS cao hơn nhiều so với 25. Tuy nhiên, điều này lại phù hợp với các thiết bị di động, vì như thế sẽ đỡ tốn pin.

Kết luận: kiểu game loop này thì code dễ nhưng cần cân nhắc khi chọn FPS.

Game Speed phụ thuộc vào biến FPS

Hàm update() có thể có 2 loại: 1 loại là cứ thế update(), không quan tâm bây giờ là thời điểm nào, chỉ quan tâm đến game state, đây là loại nãy giờ ta nhắc đến; loại thứ 2 thì ngược lại, nó quan tâm xem kể từ lần update() cuối cùng cho đến bây giờ đã là bao lâu, để từ đó biến đổi game state cho phù hợp. Với loại thứ 2 này, ta có kiểu game loop sau:

DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount();

bool game_is_running = true;
while (game_is_running)
{
    prev_time_tick = curr_time_tick();
    curr_frame_tick = GetTickCount();
    update(curr_frame_tick - prev_frame_tick);
    render();
}

Dĩ nhiên lúc này code bên trong hàm update() sẽ phức tạp hơn do ta phải tùy cơ ứng biến với tham số delta_time nhận vào. Đây là 1 kiểu game loop rất phổ biến. Nhưng vẫn có nhược điểm.

Slow hardware: làm cho render() chạy quá lâu suy ra delta_time sẽ lớn, và update() sẽ cho ra lò 1 game state trong tương lai khá xa. Game sẽ giật là cái chắc.

Fast hardware: render() chạy rất nhanh, delta_time cực nhỏ. Trông thì có vẻ tốt đấy, nhưng việc tính toán số float, double trên máy tính vốn có nhiều sai số. Việc chia nhỏ quá mức các phép tính toán sẽ xảy ra sai số lớn. Ví dụ thay vì a = a + 1 luôn một lần thì lại a = a + 0.01 trong một trăm lần. Không gì đảm bảo kết quả sẽ chính xác tuyệt đối.

Kết luận: kiểu này ko được tốt cho lắm, nhưng trong vài game ít tính toán số thực, thì dùng được.

Cố định Game Speed với FPS lớn nhất có thể được

Trong kiểu game loop thứ nhất, FPS phụ thuộc vào Game Speed cố định, sẽ có vấn đề với slow hardware. Ta thử cải tiến nó, tức là khi update() xong mà quá muộn, ko được nghỉ tí nào, thì thôi khỏi cần gọi render() chi nữa, đã muộn lại càng muộn, thay vào đó, ta lại update() tiếp ngay. Điều này hy vọng vớt vát chút ít cho game khi chạy với slow hardware.

const int TICK_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICK_PER_SECOND;
const int MAX_FRAMESKIP = 10;

DWORD next_game_tick = GetTickCount();
int loops;

bool game_is_running = true;
while (game_is_running)
{
    loops = 0;
    while (GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
        update();
        next_game_tick += SKIP_TICKS;
        loops++;
    }
    render();
}

Game sẽ được update() đúng 25 lần trong 1 giây, qua kinh nghiệm cho thấy 25 là quá đủ. Trong khi render() sẽ được gọi nhiều lần nhất có thể. Tuy nhiên sẽ có thể thỉnh thoảng render() lại 1 vài hình ảnh như cũ.

Slow hardware: cũng có thể hy vọng game chạy ko chậm đến mức tệ lắm.

Fast hardware: giống kiểu game loop đầu tiên.

Kết luận: đây là kiểu game loop tôi hay dùng nhất khi lập trình game cho mobile. Tôi thấy khá ổn.

Tháp Hà Nội

Rèn luyện tư duy đệ quy với game Tháp Hà Nội. Lúc thăng hoa tôi có thể click chuột liên tiếp.

So long, old friend…

Posted in Java. Tags: , . Leave a Comment »

How to write an image to file – ImageIO

The “javax.imageio.ImageIO” is a handy tool to let you read and save the image to your local file in a platform independent format.

ImageIO Example

In this example, you will use “ImageIO” to read an image from an URL and save it into three files – “jpg“, “gif” and “png” format.

package com.mkyong.image;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;

public class WriteImage
{
    public static void main( String[] args )
    {
    	BufferedImage image = null;
        try {

            URL url = new URL("http://www.mkyong.com/image/mypic.jpg");
            image = ImageIO.read(url);

            ImageIO.write(image, "jpg",new File("C:\\out.jpg"));
            ImageIO.write(image, "gif",new File("C:\\out.gif"));
            ImageIO.write(image, "png",new File("C:\\out.png"));

        } catch (IOException e) {
        	e.printStackTrace();
        }
        System.out.println("Done");
    }
}

Referene

1. http://java.sun.com/j2se/1.4.2/docs/api/javax/imageio/ImageIO.html

Art of coding

International Obfuscated C Code Contest (IOCCC) là 1 cuộc thi thường niên dành cho các lập trình viên ngôn ngữ C, được bắt đầu từ năm 1984 đến nay, trừ các năm 1997, 1999, 2002, 2003 và 2006. Tiêu chí của cuộc thi là viết các đoạn mã C ngắn gọn mà lại rối rắm, khó hiểu. Nhưng quan trọng là phải sáng tạo và mang tính nghệ thuật.

Ví dụ 2 bài dự thi giải nhất năm 1988 và 1998 trích ra từ Wikipedia:

In ra số PI:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Format code hình cái máy bay:

#include                                     <math.h>
#include                                   <sys/time.h>
#include                                   <X11/Xlib.h>
#include                                  <X11/keysym.h>
                                          double L ,o ,P
                                         ,_=dt,T,Z,D=1,d,
                                         s[999],E,h= 8,I,
                                         J,K,w[999],M,m,O
                                        ,n[999],j=33e-3,i=
                                        1E3,r,t, u,v ,W,S=
                                        74.5,l=221,X=7.26,
                                        a,B,A=32.2,c, F,H;
                                        int N,q, C, y,p,U;
                                       Window z; char f[52]
                                    ; GC k; main(){ Display*e=
 XOpenDisplay( 0); z=RootWindow(e,0); for (XSetForeground(e,k=XCreateGC (e,z,0,0),BlackPixel(e,0))
; scanf("%lf%lf%lf",y +n,w+y, y+s)+1; y ++); XSelectInput(e,z= XCreateSimpleWindow(e,z,0,0,400,400,
0,0,WhitePixel(e,0) ),KeyPressMask); for(XMapWindow(e,z); ; T=sin(O)){ struct timeval G={ 0,dt*1e6}
; K= cos(j); N=1e4; M+= H*_; Z=D*K; F+=_*P; r=E*K; W=cos( O); m=K*W; H=K*T; O+=D*_*F/ K+d/K*E*_; B=
sin(j); a=B*T*D-E*W; XClearWindow(e,z); t=T*E+ D*B*W; j+=d*_*D-_*F*E; P=W*E*B-T*D; for (o+=(I=D*W+E
*T*B,E*d/K *B+v+B/K*F*D)*_; p<y; ){ T=p[s]+i; E=c-p[w]; D=n[p]-L; K=D*m-B*T-H*E; if(p [n]+w[ p]+p[s
]== 0|K <fabs(W=T*r-I*E +D*P) |fabs(D=t *D+Z *T-a *E)> K)N=1e4; else{ q=W/K *4E2+2e2; C= 2E2+4e2/ K
 *D; N-1E4&& XDrawLine(e ,z,k,N ,U,q,C); N=q; U=C; } ++p; } L+=_* (X*t +P*M+m*l); T=X*X+ l*l+M *M;
  XDrawString(e,z,k ,20,380,f,17); D=v/l*15; i+=(B *l-M*r -X*Z)*_; for(; XPending(e); u *=CS!=N){
                                   XEvent z; XNextEvent(e ,&z);
                                       ++*((N=XLookupKeysym
                                         (&z.xkey,0))-IT?
                                         N-LT? UP-N?& E:&
                                         J:& u: &h); --*(
                                         DN -N? N-DT ?N==
                                         RT?&u: & W:&h:&J
                                          ); } m=15*F/l;
                                          c+=(I=M/ l,l*H
                                          +I*M+a*X)*_; H
                                          =A*r+v*X-F*l+(
                                          E=.1+X*4.9/l,t
                                          =T*m/32-I*T/24
                                           )/S; K=F*M+(
                                           h* 1e4/l-(T+
                                           E*5*T*E)/3e2
                                           )/S-X*d-B*A;
                                           a=2.63 /l*d;
                                           X+=( d*l-T/S
                                            *(.19*E +a
                                            *.64+J/1e3
                                            )-M* v +A*
                                            Z)*_; l +=
                                            K *_; W=d;
                                            sprintf(f,
                                            "%5d  %3d"
                                            "%7d",p =l
                                           /1.7,(C=9E3+
                              O*57.3)%0550,(int)i); d+=T*(.45-14/l*
                             X-a*130-J* .14)*_/125e2+F*_*v; P=(T*(47
                             *I-m* 52+E*94 *D-t*.38+u*.21*E) /1e2+W*
                             179*v)/2312; select(p=0,0,0,0,&G); v-=(
                              W*F-T*(.63*m-I*.086+m*E*19-D*25-.11*u
                               )/107e2)*_; D=cos(o); E=sin(o); } }

Còn đây là đoạn code in ra số nguyên tố lớn nhất hiện tại loài người tìm được 243112609 – 1 số này có khoảng 13 triệu chữ số, đoạn code thì không dài tới nửa KB, đây thực chất là 1 ứng dụng tinh xảo của phép biến đổi nhanh Fourier:

#include <stdio.h>
#include <conio.h>

#define OUTFILE "biggest.txt"

int m=167772161,N=1,t[1<<25]={2},a,*p,i,e=34893349,s,c,U=1;

void g(int d,int h){
	for(i=s;i<1<<24;i*=2) d=d*1LL*d%m;
	for(p=t;p<t+N;p+=s)for(i=s,c=1;i;i--){
		a=p[s]*(h?c:1LL)%m;p[s]=(m+*p-a)*(h?1LL:c)%m;a+=*p;*p++=a%m;c=c*1LL*d%m;
	}
}

int _tmain(int argc, _TCHAR* argv[])
{
	printf("calculating...\n");
	while(e/=2){
		N*=2;U=U*1LL*(m+1)/2%m;
		for(s=N;s/=2;)g(17,0);
		for(p=t;p<t+N;p++)*p=*p*1LL**p%m*U%m;
		for(s=1;s<N;s*=2)g(29606852,1);
		for(a=0,p=t;p<t+N;){
			a+=*p<<(e&1);*p++=a%10;a/=10;
		}
	}
	while(!*--p);

	printf("now writing result to file...\n");
	FILE* f = fopen(OUTFILE, "w");
	long count = 0;
	for(t[0]--;p>=t;){
		fputc(48+*p--,f);
		if (!(++count % 100)) fputc('\n',f);
	}
	fclose(f);

	printf("\nfinish!");
	getch();
	return 0;
}

Java EE 6 Feature Overview

Source: http://www.taranfx.com/blog/java-ee-6-features-overview

Java EE 6 just got released yesterday, here is everything you need to know about, in brief.

A look back: Java EE 5 was a remarkable step towards a mature, widely deployed, well supported server-side development platform. EJB 3.0 was re-engineered to ease, the Entity Bean model was stripped and replaced by JPA as the persistence paradigm, JSF was introduced as the standard presentation tier framework and JAX-WS 2.0 replaced JAX-RPC as the SOAP web services API. The focus of Java EE 5 was squarely on reducing complexity by embracing the concepts of annotations, POJO programming, zero-configuration systems.

Read the rest of this entry »

State Pattern

State Pattern là một mẫu thiết kế dễ hiểu, dễ hiện thực nhưng tiện lợi thì vô cùng, nhất là trong việc lập trình game. Trong mẫu này, tính đa hình của lập trình hướng đối tượng đã được đẩy lên tới mức cực độ.

Ta thử lấy ví dụ khi làm game Football, với 1 cầu thủ, sẽ có những hành xử khác nhau tùy vào trạng thái của cầu thủ đó: nếu đang ở trạng thái Yếu, ta gọi hàm Chạy Chậm(), nếu ở trạng thái Khỏe, gọi hàm Chạy Nhanh(). Để thể hiện điều đó thì cần 1 đoạn code rẽ nhánh, “If Else”. Nhưng trong thực tế thì số lượng trạng thái của 1 cầu thủ là rất nhiều, khoảng vài chục cho đến 100, nếu cứ code theo kiểu trên, ta có 1 đoạn rẽ nhánh khổng lồ, rất xấu cho dù dùng “Switch case”.

State Pattern đưa ra cách tiếp cận sau: tách bạch tất cả các trạng thái của cầu thủ thành các lớp con riêng biệt, các lớp con này cùng thừa kế 1 lớp cha trừu tượng. Trong lớp cha có 1 phương thức ảo: Handle() để tất cả các lớp con phải hiện thực nó. Phương thức Handle() khi được gọi sẽ thực hiện 1 hành động nào đó của cầu thủ. Chúng ta ko cần quan tâm đối tượng tạo ra thực sự là thể hiện của lớp con nào, chỉ cần biết nó thuộc kiểu lớp cha, là có thể gọi Handle() một cách hợp lệ. Còn thực sự Handle() của lớp con nào được gọi thì sẽ được quyết định vào lúc run-time.

Code minh họa:

// Lớp cầu thủ: Player
class Player
{
public:
  // đối tượng cho biết hiện tại
  // đang ở trạng thái nào
  IState* currentState;
}

// Lớp trừu tượng cha: IState
class IState
{
public:
  virtual void Handle(); // hàm ảo
}

// Lớp con: StrongState
class StrongState : public IState
{
public:
  void Handle(); // hiện thực hàm ảo
}

// Lớp con: WeakState
class WeakState : public IState
{
public:
  void Handle(); // hiện thực hàm ảo
}

// Test
void main()
{
  Player player;

  // cầu thủ đang ở trạng thái Strong
  player.currentState = new StrongState();
  // thực hiện hành động
  player.currentState->Handle();

  // chuyển cầu thủ sang trạng thái Weak
  player.currentState = new WeakState();
  // thực hiện hành động
  player.currentState->Handle();
}

Rõ ràng tại mọi thời điểm ta chỉ cần gọi player.currentState->Handle(); còn thực sự hàm Handle() nào của lớp con được chạy thì phụ thuộc vào biến player.currentState đang trỏ vào đâu.

Trí tuệ nhân tạo trong đánh cờ (P.1)

Trí tuệ nhân tạo trong các trò chơi đánh cờ ko phải là cái gì quá cao siêu và hàn lâm, chỉ cần có kiến thức về cây, bạn hoàn toàn có thể lĩnh hội cách thức đưa ra chiến lược đánh cờ cho máy tính. Trong bài viết này tôi sẽ cố gắng trình bày một cách dễ hiểu nhất về vấn đề trên.

Read the rest of this entry »

Anathema 1995-1996

Tôi thích Anathema nhất là vào thời điểm 1995-1996. Và album [1995] The Silent Enigma xứng đáng là album hay nhất của Anathema.

Read the rest of this entry »

Dust – Mourning Beloveth – More Than Love, More Than Life (Sưu tầm)

Mourning Beloveth – cái tên không hề xa lạ với những người yêu doom, đặc biệt là kiểu doom/death của My Dying Bride, Paradise Lost thời kì đầu.

Thành lập năm 1992 tại đất nước Ireland nhỏ bé, nhưng phải đến gần 10 năm sau, khi album full-length đầu tiên của band – “Dust” được release thì Mourning Beloveth mới thoát ra khỏi bóng tối underground để cho thế giới metal 1 thứ doom/death đẹp như mơ, tinh tế như tranh…

Dust – một sự khởi đầu tuyệt vời..

Read the rest of this entry »

Posted in Doom. Tags: , , . Leave a Comment »