Android의 SurfaceView를 이용하여 간단한 게임을 구현하고자 한다.

보통 다들 worker thread를 만들어서 holder에 lock을 걸었다가 풀어주면서 화면을 갱신해주는 방법을 쓰더라.

 

문제는, thread 내부의 무한 루프에서 발생한다.

무한 루프는, 계~~~~~~~~속 해서 돌며, CPU의 속도에 따라 도는 속도라 다르다.

따라서 빠른 CPU를 쓴다면 루프 속도가 빠를 것이고, 느린 CPU를 쓴다면 느릴 것이다.

그리고.. 무한 루프를 도는 동안에는 엄청나게 점유율이 높을 것이다..

 

사람의 눈은 30fps면 웬만한 동작을 인지하는데 무리가 없기 때문에, 나는 surfaceview로 만든 게임을 30fps로 맞추고자 했다.

그렇게 해서 나온 소스는 다음과 같다.

 

public class GameThread extends Thread{

	public static final long MILLIS_PER_FRAME = 33; // ~30fps // not guaranteed
	protected SurfaceHolder mHolder = null;
	
	private boolean isRunning = false; 
	
	private FirstStage mStage1;
	
	
	public GameThread(SurfaceHolder holder) {
		mHolder = holder;
		
		mStage1 = new FirstStage();
		mStage1.init();
		
	}
	
	@Override
	public synchronized void start() {
		isRunning = true;
		super.start();
	}
	
	public void setLoop(boolean loop) {
		isRunning = loop;
	}
	
	@Override
	public void run() {
		super.run();
		
		long now = 0, dt;
		long last = System.currentTimeMillis();
		
		while (isRunning) {
			
			if (mHolder == null) return;
			Canvas c = null;
			try {
				c = mHolder.lockCanvas();
				
				if (c == null)
					continue;
				
				synchronized (mHolder) {
					now = System.currentTimeMillis();
					dt = (now - last);
					while (dt < MILLIS_PER_FRAME) {
						Thread.sleep(1);
						now = System.currentTimeMillis();
						dt = (now - last);
					}
					
					mStage1.update(dt);
					mStage1.render(c, dt);
					Log.v("TEST","TEST"+dt);
				}
			}
			catch(Exception e) {
				
			}
			finally {
				if (c!=null && mHolder!=null)
					mHolder.unlockCanvasAndPost(c);
			}
			
			last = now;
		}
	}
}

 

가장 중요한 부분이 어디일까?

바로 이 부분이다.

 

now = System.currentTimeMillis();
dt = (now - last);
while (dt < MILLIS_PER_FRAME) {
	Thread.sleep(1);
	now = System.currentTimeMillis();
	dt = (now - last);
}

처음에는 경과 시간, 즉 Delta Time (DT) 을 고려하지 않을까 생각했는데, 그건 말이 안된다고 생각했다.

dt를 관리 안하고 무한 루프마다 update와 rendor 함수를 호출 한다면 무슨 일이 발생하냐면,

예를 들어 상,하,좌,우 로 움직이는 RPG 게임이 있는데, 방향키를 누르면 1초동안 주인공이 4칸 정도 움직여야 한다고 쳐보자.

이게, 빠른 CPU에서는 10칸 움직이고, 느린 cpu에서는 1칸 움직 일 수도 있는 것이다.

 

옛날 렉이 심한 컴퓨터로 게임을 했던 유저들은 알 것이다.

잠시 그래픽 카드가 버벅여서 화면상 캐릭터의 움직임들이 정지했다가, 화면 렉이 풀리면, 그동안 움직였어야 했던 거리를 한번에 후다다닥 움직이는 캐릭터들의 모습을 기억 할 것이다. 동기화 측면에서는 이게 맞다는 얘기다.

 

즉, dt 개념을 넣어야 하는데.. 처음에는 위와 같이 안짜고 아래와 같이 짰었다.

 

if (dt < MILLIS_PER_FRAME) {

 Thread.sleep(MILLIS_PER_FRAME - dt);

}

 

이렇게 할 경우 MILLIS_PER_FRAME와 dt 사이의 간격 만큼 android os가 sleep 후 다음 구문으로 진행 할 것이라 생각했는데...

실제 돌려보니 30fps가 나오는게 아니고 막 50fps가 나오더라... 그래서 레퍼런스를 뒤져보니....

Thread.sleep() 함수가 sleep의 시간을 정확히 보증하지 않는 다는 얘기가 있더라.

그래서 나온 결론이 위의 예제에 적은 코드이다. while로 돌면서 dt 검사하기. 중간데 Sleep(1)을 넣은 것은, 이것마저 안넣으면 해당 while문이 수백번 돌 수도 있기 때문에 최소한의 배려를 한 것이다.

 

사실 위 thread의 run 하단부도 썩 맘에 들진 않는다.

rendor와 update를 분리하고 싶긴 한데, 내가 만들 게임은 '간단한' 게임이기 때문에 굳이 이를 분리하지 않았다.

나중에 복잡한 게임을 만들 즈음이 되면 분리시켜서 짜야지.

 

고럼 이만..!

 

 

 

 

 

 

 

저작자 표시 비영리 변경 금지
신고
블로그 이미지

roter

JHB / Peripheral Programmer

Tag

댓글을 달아 주세요