[HTML5 Game] Shooting Down

Posted in 모바일/HTML5 // Posted at 2013. 10. 16. 22:37

앞서 전개한 글들에서 충돌처리, 총알발사, 오브젝트 이동 처리 등을 구현해 보았다.

이번 글에서는, 이러한 개별 개념을 조합하여 총알로 적(enemy) 비행체를 격추하는 샘플을 구현해 보자.

 

간략한 시나리오

- 적(enemy) 비행체는 캔버스 상단에서 좌/우로 방향을 바꿔가며 계속 이동한다.

- 키보드의 스페이스 키를 누르면 총알이 발사된다.

- 총알이 적 비행체에 명중하면 폭발한 듯한 이미지 효과를 준다.

 

격추라는 시나리오에만 충실하기 위해 아군 비행기는 등장시키지 않았으며, 총알의 출발 위치도 캔버스 하단 중앙에서만 출발하도록 한다. 또한 지속적인 테스트를 위하여 적 비행체가 격추되어도 충돌을 표현하는 이미지 효과만 줄 뿐 비행체는 계속 살아서 움직이도록 할 것이다.

 

먼저 적 비행체와 총알, 그리고 충돌 처리를 위한 객체 기반을 작성할 것인데 그전에, 주요 코드부터 간략히 살펴보자.

 

총알 발사

이전 글에서는, 과도한 연사 속도를 조절하기 위해 FPS를 기반의 조절값을 사용한 반면 여기서는 총알 발사의 전/후 시간을 속도 조절의 기준 값으로 사용했다. 즉 직전 총알 발사 시간과 현재 총알 발사 시간의 간격이 0.1초 이상 되어야만 새 총알이 배열에 추가되도록 처리한다. 그리고 총알의 발사 위치는 캔버스의 하단 중앙으로 설정한다.

if(Date.now() - this.lastShootTime >= 100){        
   this.bullets.push({x: (this.canvasSize.width / 2)
                       - (this.gameAssets.bullet.width / 2), y: this.canvasSize.height});
   this.lastShootTime = Date.now();  
  } 

 

 

오브젝트 이동

적 비행체와 총알의 이동을 위해 좌표값을 변경하는 코드이다. 게임 루프의 update() 메서드에서 매 프레임마다 호출하는 부분으로, 총발 위치는 위쪽 방향으로 이동하도록 Y 좌표값을 변경하고 적 비행체는 캔버스의 좌/우우를 계속해서 왔다갔다 하도록 방향값을 기준으로 X 좌표값을 변경시켜준다.

for(var i = 0; i < this.bullets.length; i++){    
   this.bullets[i].y -= this.speedBullet; 
}      

 

if(this.directionEnemy == 1
        && this.enemyPostion.x + this.gameAssets.enemy.width  > this.canvasClientRect.right)

{
   this.directionEnemy = -1;
}  


if(this.directionEnemy == -1
        && this.enemyPostion.x + 5 < this.canvasClientRect.left)

{   
   this.directionEnemy = 1;
}
    
this.enemyPostion.x += (this.directionEnemy) * 5; 

 

 

오브젝트 그리기 및 충돌처리

객체의 마지막 코드로, 게임 오브젝트들을 캔버스에 그린다. 게임 루프의 display() 메서드에서 매 프레임마다 호출하는 부분으로, 먼저 캔버스의 모든 내용을 지우고 적 비행체를 그린다. 다음으로 총알을 배열 수 만큼 그려주는데 총알이 캔버스 영역 밖으로 이동했다면 배열에서 제거하고 루프를 빠져나간다. 그리고 총알과 적 비행체와의 충돌처리를 통하여 격추되는 이미지를 그려줄지 판단한다. 

this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height); 
  
this.canvasContext.drawImage(this.gameAssets.enemy, this.enemyPostion.x, this.enemyPostion.y);
       
for(var i = 0; i < this.bullets.length; i++){
   if(this.bullets[i].y <= 0) {
      this.bullets.splice(i,1); continue;
   }
   
   this.canvasContext.drawImage(this.gameAssets.bullet, this.bullets[i].x, this.bullets[i].y);
   
   if(this.bullets[i].x +  this.gameAssets.bullet.width > this.enemyPostion.x 
       && this.bullets[i].x < this.enemyPostion.x + this.gameAssets.enemy.width    
       && this.bullets[i].y > this.enemyPostion.y 
       && this.bullets[i].y < this.enemyPostion.y + this.gameAssets.enemy.height)
    {           
      this.canvasContext.drawImage(this.gameAssets.pow, this.enemyPostion.x, this.enemyPostion.y);
    }                
}

 

 

여기까지 해서 객체기반 작성이 완료되었다. 나머지 코드는 각종 초기화 및 이미지 리소스 다운로드, 위의 객체를 기반으로 게임 루프를 구현하는 것이다. 다음 코드는 객체를 포함한 전체 소스이다.

 function ShootingDownObj(gameAssets,canvasElement){
    this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
    this.canvasContext = canvasElement.getContext('2d');
    this.canvasClientRect = canvasElement.getBoundingClientRect();     
    this.enemyPostion = {x: 10, y:10}; 
    this.gameAssets = gameAssets;
    this.bullets = [];
    this.speedBullet = 15;   
    this.directionEnemy = 1;
    this.lastShootTime = Date.now();
 
    this.addBullet = function(){   
        if(Date.now() - this.lastShootTime >= 100){        
         this.bullets.push({x: (this.canvasSize.width / 2)
             - (this.gameAssets.bullet.width / 2), y: this.canvasSize.height});
         this.lastShootTime = Date.now();  
        }  
    }
 
    this.movePosition = function(){
        for(var i = 0; i < this.bullets.length; i++){    
           this.bullets[i].y -= this.speedBullet;
        }      
        if(this.directionEnemy == 1
          && this.enemyPostion.x + this.gameAssets.enemy.width  > this.canvasClientRect.right){
            this.directionEnemy = -1;
        }  
        if(this.directionEnemy == -1
         && this.enemyPostion.x + 5 < this.canvasClientRect.left){   
            this.directionEnemy = 1;
        }
    
        this.enemyPostion.x += (this.directionEnemy) * 5;   
    }
   
    this.shotBullets = function(){  
       this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height); 
       this.canvasContext.drawImage(this.gameAssets.enemy,
                                                                        this.enemyPostion.x, this.enemyPostion.y);
       
      for(var i = 0; i < this.bullets.length; i++){
          if(this.bullets[i].y <= 0) {
             this.bullets.splice(i,1);
             continue;
          }
   
          if(this.bullets[i].x +  this.gameAssets.bullet.width > this.enemyPostion.x 
           && this.bullets[i].x < this.enemyPostion.x + this.gameAssets.enemy.width    
           && this.bullets[i].y > this.enemyPostion.y 
           && this.bullets[i].y < this.enemyPostion.y + this.gameAssets.enemy.height){           
            this.canvasContext.drawImage(this.gameAssets.pow,
                                                         this.enemyPostion.x, this.enemyPostion.y);
          }           
            this.canvasContext.drawImage(this.gameAssets.bullet, this.bullets[i].x, this.bullets[i].y);
       }    
    } 
}

var fps = 30;
var canvasElement;
var gameContext; 
var shootingDownObj;   
var gameAssets;  
var currentAssetImageLoadCount = 0;    
var isKeyDown = [];

 

function init(){
  canvasElement = document.getElementById('GameCanvas');
  gameContext = canvasElement.getContext('2d');
   
  var bulletImage = new Image();  
  bulletImage.src = 'image/bullet.png';       
  bulletImage.onload = onAssetImageLoadComplete; 
 
  var enemyImage = new Image();  
  enemyImage.src = 'image/enemy.png';       
  enemyImage.onload = onAssetImageLoadComplete; 
 
  var powImage = new Image();  
  powImage.src = 'image/pow.png';       
  powImage.onload = onAssetImageLoadComplete;  
 
  gameAssets = {bullet: bulletImage, enemy: enemyImage, pow: powImage};     
}

 

function onAssetImageLoadComplete(){ 
  if(++currentAssetImageLoadCount >= 3){  
    shootingDownObj = new ShootingDownObj(gameAssets,canvasElement);      
    setInterval(gameLoop, 1000 / fps);
  } 
}

 

function gameLoop(){
  update();
  display();
}

 

function update(){   
  if(isKeyDown[32]){      
    shootingDownObj.addBullet();
  } 
  shootingDownObj.movePosition();
}

 

function display(){
  shootingDownObj.shotBullets();

}

 

function onKeyDown(e){  
  isKeyDown[e.keyCode] = true;   
}

 

function onKeyUp(e){
  isKeyDown[e.keyCode] = false;
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);

 

 

 

브라우저를 통해 확인해보면, 다음 그림과 같이 총알이 명중하면 폭발한듯한 이미지 효과를 확인할 수 있다.

 


 


 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Sound Effect  (2) 2013.10.11
[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30

[HTML5 Game] Sound Effect

Posted in 모바일/HTML5 // Posted at 2013. 10. 11. 21:43

이번 글에서는 게임에 사운드(Sound) 효과를 추가해 볼 것이다.

 

이전 글(http://m.mkexdev.net/252)에서 스페이스 키를 누르면 총알이 발사되도록 해 봤으니, 여기서는 총알이 발사되는 소리를 구현해 보도록 하겠다.

 

사운드 처리는 HTML5 스펙에 새로이 추가된 Audio 요소를 이용할 것이다.(참고: http://m.mkexdev.net/63)

 

 

 

단순히 하나의 사운드를 재생하는 것은 매우 심플한 코드로 구현이 가능하다. 하지만 게임 사운드는 한 번에 하나의 사운드만 재생되는 것이 아니라 여러 사운드가 동시에 재생되어야 하는 경우가 많고 또한 하나의 사운드라 해도 그 사운드가 종료되기 전에 같은 사운드가 다시 재생되어야 하는 경우도 있다. 이러한 대표적인 경우가 총알이 발사되는 경우이다. 예를들어 총알 사운드의 플레이 타임이 3초인 경우, 첫번째 총알이 발새되고 3초가 지나기 전에 두 번째 총알이 발사되는 경우이다. 이를 위해서는 하나의 사운드를 여러개로 분리해서 각각 재생되도록 하는 기법이 필요하다.

 

바로 구현해 들어가 보자. 다음과 같이 총알 사운드를 추상화환 객체 기반을 준비한다.

여러 개의 총알 소리가 거의 동시에 같이 재생될 수 있도록 사운드 배열을 관리하고 플레이 가능한 사운드를 찾으면 그 사운드를 재생하고 바로 루프를 빠져 나간다. 또한 특정 사운드의 재생이 완료되면 명시적으로 paused(일시정지)처리를 하는데 이 함수에서는 이 속성값을 검사하여 재생가능한 사운드 파일을 탐색한다. 

function FireShot(sounds){
  this.sounds = sounds

  this.playSound = function(){   
    for(var i = 0; i < this.sounds.length; i++){  
      if(this.sounds[i].audio.paused){             
         this.sounds[i].audio.play();
         break;    
      }    
    }   
  }
}

 

이어지는 코드는 FireShort 객체를 이용하여 스페이스 키가 눌러졌을 때 사운드 재생을 처리하는 코드이다.

총알 소리로 사용되는 사운드 파일은 한개이지만, (앞서 설명한대로) 동시 재생을 위해 여러개의 Audio 요소를 동적으로 생성한다. 코드에서는 50개를 생성했는데 이것은 사운드 파일의 재생시간과 동시 재생 환경등을 고려하여 적절한 값을 지정해야 한다. 필자가 사용하는 (인터넷에서 다운받는) 사운드 파일의 재생시간은 대략 4초 가량 되며(너무 길다. 짧막한 총성소리에 비해 너무 긴 재생 시간이다. ㅡ,ㅡ;) 스페이스 키를 계속 누르고 있을 경우 소리가 끊임없이 나도록 해야 하기 때문에 50이라는 좀 과한(?) 값을 지정했다. (이렇게 해도 플레이타임이 길어서 매끄럽지 못하다.) 예제에서는 사운드 재생만이 목적이기 때문에 이러한 과도한 값을 지정했지만 실제 게임 개발시에는 자원의 낭비 및 성능의 저해가 없는 수준에서 적절한 선택을 해야 할 것이다.

 

참고로 크롬 브라우저의 경우 한번 재생한 Audio를 재사용 하기 위해서는 다시 로드해야 하는것을 주의하기 바라며 기타 코드는 HTML5의 Audio 요소를 사용하는 코드와 FireShot 객체를 호출하는 부분으로 이뤄져있다.

var sounds = []; 
var fireShot; 
var currentSoundLoadCount = 0;   


function init(){
 for(var i = 0; i < 50; i++){  
    var gunAudio = new Audio();
    gunAudio.src = 'audio/gun.mp3'
        
    gunAudio.addEventListener("canplaythrough", onSoundReadyComplete, false);  
    gunAudio.addEventListener("ended", function(){
       if(window.chrome) this.load();                    

         this.pause(); 
     },false); 
      
    document.body.appendChild(gunAudio);
  
    sounds.push({audio: gunAudio});  
 }               
}

 

function onSoundReadyComplete(){    
   if(++currentSoundLoadCount >= 50){         
     fireShot = new FireShot(sounds);             
 }
}

 

function onKeyDown(e){   
 if(e.keyCode = 32){        
    fireShot.playSound(); 
 }
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);

 

브라우저로 실행하여 사운드 효과를 감상(?)해 보자.

다시한번 말하지만, 사운드 파일의 재생시간과 동시 재생의 환경에 따라 적절히 수정을 가하며 테스트 해 보는 것이 좋다.

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Shooting Down  (0) 2013.10.16
[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30

[HTML5 Game] Firing Bullet

Posted in 모바일/HTML5 // Posted at 2013. 10. 8. 22:30

이전 글(http://m.mkexdev.net/247)에서 게임 루프를 기반으로 게임 오브젝트 이동에 대해 알아 보았다.

키보드의 방향키에 반응해 전투기가 이동하는 샘플을 작성해 봤는데 여기서 덧붙여 전투기에서 총알이 발사되도록 해 볼 것이다. 키보드의 스페이스 바를 누르면 총알이 발사되며 키를 계속 누르고 있으면 총알 다발이 연속해서 발사되는 시나리오이다.

 

총알 이미지

총알으로 사용할 이미지는 총알을 제외한 배경이 투명한 형태로 만드는 것이 좋다. 그러나 투명 이지미를 수급하기 쉽지 않아,  여기서는 캔버스 배경과 동일한 흰색 배경의 총알 이미지를 사용할 것이다. (ppt로 간단히 만들고 캡쳐해서 이미지로 변환하자 ㅎㅎ)

 

 

데모 실행 화면

실제 구현에 앞서 데모 실행화면을 먼저 확인해 보자. 이전 글의 예제와 같이 전투기는 방향키에 반응해 이동을 하며, 여기서 더해 스페이스 키를 누르면 총알이 위쪽 방향으로 발사된다.

 

 

 

구현 내용 정리

구현해야 할 내용을 큰 맥락에서만 정리해 보자.

- 스페이스 키를 누르면 총알 이미지가 비행기 중앙에서 출발하도록 한다

- 총알은 연속적으로 발사되기 때문에 배열로 관리한다.

- 발사된 총알이 위쪽 방향으로 계속 이동하게끔 하기 위해 Y 좌표 값을 업데이트 한다

 

대략 이 정도만 정리하고 자세한 건 구현하면서 살펴 보도록 하자.

 

이전 글에서는 전투기 객체를 Character로 명명했었는데, 맘에 들지 않아 Fighter로 변경하고 메서드 이름도 drawFighter 로 변경하였다. 그리고 총알을 그리기 위해 추가된 코드는 파란색으로 표시했다.

 

총알은 하나 이상 발사될 수 있기 때문에 배열로 관리하며 발사된 총알 수 만큼 배열 요소가 생성된다. drawFighter 메서드에서는 전투기를 그리기 전에 배열에 담긴 총알을 그려준다. 이때 캔버스의 맨 위쪽까지 도달했을 경우(Y 좌표가 '0'일 경우) 배열에서 제거하고 루프를 건너뛴다. 이렇게 하지 않으면 캔버스를 벗어난 총알도 배열에 계속 잔존하게 되고 이 배열은 게임 실행 중 계속 커져 버려서 성능에 좋지 못하다. 따라서 반드시 총알 제거를 해 주어야 한다.(참고로 캔버스의 위쪽 끝 Y 좌표가 오프셋 값 등으로 인해 0보다 클 수 있어나 큰 의미 없으므로 그냥 0 값을 기준으로 한다) 

function Fighter(assets, x, y, canvasElement){ 
  this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
  this.canvasContext = canvasElement.getContext('2d'); 
  this.assets = assets;
  this.position = {x: x, y: y}  
  this.bullets = [];
 
  this.drawFighter = function(){  
    this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);  
  
    //draw Bullet
    for(var i = 0; i < this.bullets.length; i++){
       if(this.bullets[i].y <= 0) {
        this.bullets.splice(i,1);
        continue;
       }       
      this.canvasContext.drawImage(this.assets.bulletAsset, this.bullets[i].x, this.bullets[i].y);
    }

  
   //draw Fighter
   this.canvasContext.drawImage(this.assets.fighterAsset, this.position.x, this.position.y);
  }
}

 

 

이어지는 코드 역시 이전 글의 샘플 코드에서 파란색 부분의 주요 변경이 이뤄졌다. 파란색으료 표시하지 않는 부분도 일부 변경되 되었는데 이것은 일부 변수명 변경, 이미지를 두 개(전투기, 총알) 다운 받기 위한 코드, 게임 루프 메서드 분리 등 총알 발사와 관련된 로직과 크게 관련되어 있지 않아 별도 표시를 하지 않았다.

 

변경된 부분을 대략 설명하면, 전투기와 총알 이미지 객체를 담기 위한 assets 객체와 스페이스 바를 누를때 전투기 객체의 총알 배열에 (총알이 발사될 최초 x,y 지점을 지정하여) 하나의 총알 요소를 추가하고 있다. 그리고 매 업데이트마다 Y 값을 점점 줄여나가면서 위쪽 방향으로 이동시킨다.

var fps = 30;
var canvasElement;
var gameContext;
var fighter;         
var assets;  
var currentAssetLoadCount = 0;    
var speedFighter = 10;
var speedBullet = 15;
var isKeyDown = [];
var bulletTime = 0;
   
function init(){
 canvasElement = document.getElementById('GameCanvas');
 gameContext = canvasElement.getContext('2d');
 
 var fighterImage = new Image();  
 fighterImage.src = 'image/fighter.png';        
 fighterImage.onload = onAssetLoadComplete;  
  
 var bulletImage = new Image();  
 bulletImage.src = 'image/bullet.png';        
 bulletImage.onload = onAssetLoadComplete; 
 
 assets = {fighterAsset: fighterImage, bulletAsset: bulletImage};    

}

 

function onAssetLoadComplete(){ 
 if(++currentAssetLoadCount >= 2){     
    fighter = new Fighter(assets, 200, 200, canvasElement);      
    fighter.drawFighter();   
    setInterval(gameLoop, 1000 / fps);
 } 
}

function gameLoop(){
  update();
  display();
}

 

function update(){  
  bulletTime += 1000 / fps;  
  if(bulletTime >= 100){
    if(isKeyDown[32]){ //space      
       fighter.bullets.push({x: fighter.position.x + 6, y: fighter.position.y});  
       bulletTime = 0;
    }
  }

  if(isKeyDown[37]){
    fighter.position.x -= speedFighter;
  }
  if(isKeyDown[38]){
    fighter.position.y -= speedFighter;
  }
  if(isKeyDown[39]){
    fighter.position.x += speedFighter;
  }
  if(isKeyDown[40]){
    fighter.position.y += speedFighter;
  }
 
  for(var i = 0; i < fighter.bullets.length; i++){    
    fighter.bullets[i].y -= speedBullet;
  }  
}

 

function display(){
 fighter.drawFighter();
}

 

function onKeyDown(e){  
 isKeyDown[e.keyCode] = true;  
}

 

function onKeyUp(e){
 isKeyDown[e.keyCode] = false;
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);

 

추가로 위의 코드에서 다음의 부분을 주의깊게 보자. 이 코드가 필요한 이유는 30프레임이라는 빠른 속도는 스페이스 키를 한번만 눌러도 두 세번 누른 것처럼 되기도 하고, 스페이스 키를 계속 누르고 있을 경우 그 속도가 너무 빨라 총알이 겹쳐서 발사되어 시각적으로 완전하기 못하게 된다. 따라서 총알이 한번 발사된 후 0.1초 정도 지나야만 다음 총알이 발사될 수 있도록 약간의 딜레이(delay) 시간을 주는 것이 좋다.


bulletTime += 1000 / fps;
if(bulletTime >= 100){
   if(isKeyDown[32]){ //space
      fighter.bullets.push({x: fighter.position.x + 6, y: fighter.position.y});
      bulletTime = 0;
   }
}

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Shooting Down  (0) 2013.10.16
[HTML5 Game] Sound Effect  (2) 2013.10.11
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30

[HTML5 Game] Moving Object Ⅱ

Posted in 모바일/HTML5 // Posted at 2013. 10. 2. 21:30

이전 글(http://m.mkexdev.net/246)에서 키보드에 반응해 게임 오브젝트가 이동하는 예를 살펴 보았다.

당시 제기했던 문제점을 해결해서 완성도를 높여 보자.

 

이전 글에서 제기한 문제점을 다시 가져와 본다.

브라우저로 결과를 확인해 봤다면 뭔가 완전하지 않다는 것을 느꼈을 것이다. 전투기가 방향키에 반응해 원하는 곳으로 이동 하는 것은 분명하지만 완성도 있는 게임으로써는 뭔가 부족하다.

 

먼저 제일 문제가 되는 것은 '동시 키 입력'이 되지 않는다는 것이다.

예를 들어 대각선 방향으로 이동하기 위해 '→, ↑' 두 키를 동시 누르고 있더라도 뒤에 눌러진 키에만 반응하여 한쪽 방향으로만 이동하게 된다. 이것은 PC에서 Sift, Alt 키와 같은 보조키를 제외하고는 동시키 입력을 지원하지 않기 때문에 발생하는 현상이다. 따라서 이에 대한 처리를 별도로 해 줘야 한다.

 

두 번째 문제는, 전투기 이동의 시작이 매끄럽지 못하다는 것이다. 전투기를 끊김 없이 이동 시키기위해 키를 떼지 않고 계속 누르고 있을 경우 처음 이동이 끊기는 현상을 볼 수 있다. 이것은 키 입력이 지속될 때 발생하는 최초 지연 현상 때문인데 실제로 키 입력의 초당 실행수를 측정해 보면 최초 시작시점에 1~5사의 값이 나오고 이후부터는 최대 30정도로 유지되는 것을 확인할 수 있었다.

 

 

두 가지 문제의 해결책은 바로 게임 루프이다.

키보드의 키 입력 이벤트가 발생할 때 UI를 갱신하는 것이 아니라 게임 루프 상에서 빠르고도 지속적으로 UI를 갱신하도록 하며 키 이벤트는 키 입력 상태 값을 업데이트 해 주는 용도로만 그치게 하는 것이 핵심이다.

이렇게 하면 동시 키 입력이나 초기 지연 현상을 극복할 수 있게 되는 것이다.

 

 

 

 

 

Character 객체는 이전 코드와 동일하다.

function Character(asset, x, y, canvasElement){ 
  this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
  this.canvasContext = canvasElement.getContext('2d');
 
   this.asset = asset;
   this.position = {x: x, y: y} 

   this.drawCharacter = function(){  
      this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);    
      this.canvasContext.drawImage(asset, this.position.x, this.position.y);    
  }
}

 

 

이어지는 코드는 이전 코드이 문제점을 개선한 것인데, 먼저 UI를 갱신시키는 부분을 키 이벤트가 아닌 게임 루프에서 처리하는 것을 주의깊게 보자. 키 이벤트에서는 isKeyDown이라는 배열에 키 입력 여부를 삽입하고 게임루프에서 이 값을 기반으로 오브젝트 이동을 처리한다. 키 입력을 해제하기 위해 keyup 이벤트도 사용하고 있다.

var fps = 30;
var canvasElement;
var gameContext;
var character;         
var asset;  
var speed = 10;
var isKeyDown = [];
   
function init(){
  canvasElement = document.getElementById('GameCanvas');
  gameContext = canvasElement.getContext('2d');
  
  asset = new Image(); 
  asset.src = 'image/fighter.png';        
  asset.onload = onAssetLoadComplete;      
}

function onAssetLoadComplete(){ 
  character = new Character(asset, 200, 200, canvasElement);
  character.drawCharacter();
 
  setInterval(gameLoop, 1000/fps);
}

 

function gameLoop(){ 
  if(isKeyDown[37]){ 
    character.position.x -= speed;
  }
  if(isKeyDown[38]){ 
    character.position.y -= speed;
  }
  if(isKeyDown[39]){ 
    character.position.x += speed;
  }
  if(isKeyDown[40]){ 
    character.position.y += speed;
  }
 
 character.drawCharacter();
}

 

function onKeyDown(e){ 
  isKeyDown[e.keyCode] = true;
}

 

function onKeyUp(e){
  isKeyDown[e.keyCode] = false;

}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown",onKeyDown,false);
window.addEventListener("keyup",onKeyUp,false);

 

브라우저로 결과를 확인해 보면 이전 샘플에서 발생했던 동시키 입력 문제나 초반 지연 현상이 개선된 것을 확인할 수 있다.

 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Sound Effect  (2) 2013.10.11
[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game State  (0) 2013.09.27

[HTML5 Game] Moving Object

Posted in 모바일/HTML5 // Posted at 2013. 10. 2. 11:08

이번 글에서는 게임 오브젝트를 움직여 이동시키는 방법에 대해 알아보자.

키보드의 방향키에 반응하여 상,하,좌,우로 움직여 볼텐데 뻔한 개념이니 바로 코드를 살표보자.

 

먼저 다음과 같이 Character 객체 기반을 작성한다. 지금까지 작성해오던 코딩 패턴과 유사하기에 별도의 설명은 생략하며, 위치 정보를 위한 position 객체 변수와 이 값을 기준으로 이미지를 그리고 있는 것을 확인하자.

function Character(asset, x, y, canvasElement){ 
  this.canvasSize = {width: canvasElement.width, height: canvasElement.height};
  this.canvasContext = canvasElement.getContext('2d');
 
  this.asset = asset;
  this.position = {x: x, y: y}; 
  
  this.drawCharacter = function(){  
      this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);    
      this.canvasContext.drawImage(asset, this.position.x, this.position.y);  
   }
}

 

이어지는 코드에서는 키보드의 방향키에 반응하여 앞서 생성한 Character 객체의 위치 값(position)을 변경하면서 그리기 작업을 호출하고 있다. speed라는 전역변수는 이동 속도 즉 간견을 위한 값이다. 여기서 사용한 캐릭터는 전투기 이미지인데 구글에서 다운 받아서 무단으로 사용한 것이다 ㅡ,ㅡ;

var fps = 30;
var canvasElement;
var gameContext;
var character;         
var asset;  
var speed = 10;
   
function init(){
  canvasElement = document.getElementById('GameCanvas');
  gameContext = canvasElement.getContext('2d');
  
  asset = new Image(); 
  asset.src = 'image/fighter.png';        
  asset.onload = onAssetLoadComplete;      
}

 

function onAssetLoadComplete(){
 var frameCounter = new FrameCounter();  
 character = new Character(asset, 200, 200, canvasElement,frameCounter);
 character.drawCharacter();
}

 

function onKeyDown(e){ 
 if(e.keyCode == 37){                //left
   character.position.x -= speed;

 }

 if(e.keyCode == 38){                //up
   character.position.y -= speed;

 }

 if(e.keyCode == 39){                //right
   character.position.x += speed;

 }
 if(e.keyCode == 40){                //down
   character.position.y += speed;

 }
  
 character.drawCharacter();
}

 

window.addEventListener("load", init, false);
window.addEventListener("keydown", onKeyDown, false);

 

코드는, 전형적인 자바스크립트 키 이벤트 처리 로직이며 브라우저로 실행해 보면 전투기가 방향키대로 움직이는 것을 확인할 수 있다.

 

 

문제점

브라우저로 결과를 확인해 봤다면 뭔가 완전하지 않다는 것을 느꼈을 것이다. 전투기가 방향키에 반응해 원하는 곳으로 이동 하는 것은 분명하지만 완성도 있는 게임으로써는 뭔가 부족하다.

 

먼저 제일 문제가 되는 것은 '동시 키 입력'이 되지 않는다는 것이다.

예를 들어 대각선 방향으로 이동하기 위해 '→, ↑'  두 키를 동시 누르고 있더라도 뒤에 눌러진 키에만 반응하여 한쪽 방향으로만 이동하게 된다. 이것은 PC에서 Sift, Alt 키와 같은 보조키를 제외하고는 동시키 입력을 지원하지 않기 때문에 발생하는 현상이다. 따라서 이에 대한 처리를 별도로 해 줘야 한다.

 

두 번째 문제는, 전투기 이동의 시작이 매끄럽지 못하다는 것이다. 전투기를 끊김 없이 이동 시키기위해 키를 떼지 않고 계속 누르고 있을 경우 처음 이동이 끊기는 현상을 볼 수 있다. 이것은 키 입력이 지속될 때 발생하는 최초 지연 현상 때문인데 실제로 키 입력의 초당 실행수를 측정해 보면 최초 시작시점에 1~5사의 값이 나오고 이후부터는 최대 30정도로 유지되는 것을 확인할 수 있었다.

 

결국 이 두가지 문제를 해결해야 정상적인 이동 처리가 가능하며 다음 글에서 해결된 내용을 살펴볼 것이다.

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Firing Bullet  (0) 2013.10.08
[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game State  (0) 2013.09.27
[HTML5 Game] Game Loop  (0) 2013.09.25

[HTML5 Game]Calculating FPS

Posted in 모바일/HTML5 // Posted at 2013. 9. 30. 23:30

게임 루프(http://m.mkexdev.net/242)를 다루는 글에서 초당 프레임 수인 FPS의 개념과 샘플을 작성해 보았다. 이번 글에서는 실제 초당 얼마의 프레임률을 보이는지 파악할 수 있는 초당 프레임 수 계산기를 작성해 보자.

 

초당 프레임 수는 게임 실행환경이나 가변적인 연산처리 등으로 일정하지 않거나, 목표한 프레임 률(framerate) 보장하지 못하는 등의 문제가 발생하기 때문에 게임 개발과 디버깅 시 관심있게 살펴봐야 하는 데이터이다.

 

특히 가변 프레임 방식이라면 초당 몇 프레임이 나오는지에 대한 실제적 자료가 필요하며 고정 프레임 방식일지라도 항상 고정값을 보장하지 않는 환경이 될 수도 있으니 실제 프레임 수를 살펴보는 것이 여전히 도움이 된다.

 

또한 만들고자 하는 게임이, 초당 몇 프레임에서 가장 원활한 동작을 보이는 지를 확인할 때도 유용하다 하겠다.

 

아래 사이트는 tree.js의 3D 샘플인데, 이 샘플에서도 좌측 상단에 초당 프레임 수를 보여주고 있다.

http://threejs.org/examples/ (이 사이트의 예제는 날 감탄스럽게 만들어 버렸다)

 

 

초당 프레임 수 계산 방식

초당 프레임수를 계산하기 위해서는 크게 두 가지 접근방식을 취할 수 있다.
먼저 밀리세컨드 단위의 이전 프레임의 실행 시간과 현재 프레임의 실행 시간을 비교해서 그 간격을 보고 프레임 수를 역으로 계산하는 방식과 실제 프레임 실행시마다 카운팅하는 방식이 그것이다. 두 방식 모두 큰 의미에서는 동일하다 할 수 있으나, 두 번째 방식이 좀 더 직관적이고 명료한 듯 하여 이 글에서도 두번째 방식으로 샘플을 작성해 볼 것이다.

 

예제가 복잡하지 않으니 바로 작성해 보자. 더 이상 HTML 파일은 언급하지 않겠다.(너무 심플하뉘...)

 

다음과 같이 초당 프레임 수를 계산하기 위한 객체 기반을 작성한다. 코드의 핵심은 현재 시간과 직전 시간을 (밀리세컨드 단위로) 비교하여 1000 즉, 1초가 지났을 경우 계속 증가시켜오던 callCount 값을 framePerSecond에 대입하는 부분이다. 참고로 자바스크립트의 Date.now()는 1970년 1월 1일 자정으로부터 현재 시간 사이의 밀리초를 반환하는 함수인데 new Date().getTime()으로도 동일한 결과를 받을 수 있으나 검색한 자료에 의하면 성능상 Date.now()가 더 좋은 효율을 보인다고 한다.

function FrameCounter(){
  this.callCount = 0;
  this.framePerSecond = 0; 
  this.beforeTime = 0; 
 
  this.countFps = function(){    
  //(Date.now() is returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC)  
  var nowTime =  Date.now(); //1970년 1월 1일 자정과 현재 날짜 및 시간 사이의 밀리초 값입니다
  
   //If one second has passed
   if(nowTime - this.beforeTime >= 1000){
      this.framePerSecond = this.callCount;        
      this.beforeTime = nowTime;

      this.callCount = 0; 
   }
  
   //Increase frame count per second
   this.callCount++;
 }
}

 

이어지는 코드는 앞서 생성한 FrameCounter 인스턴스를 생성해서 게임루프마다 fps를 증가시키기 위해
countFps 함수를 호출하는 것이다. 초당 프레임 수는 좌측 상단에 표시하며 전체 누적 프레임 수는 중앙에 표시하도록 한다.

var fps = 30;
var canvasElement;
var gameContext;
var totalFrameCount = 0;
var frameCounter;

 

function init(){
 canvasElement = document.getElementById('GameCanvas');
 gameContext = canvasElement.getContext('2d');
 
 frameCounter = new FrameCounter();
  
 setInterval(gameLoop, 1000/fps); 
}

 

function gameLoop(){ 
  update();
  display();
}

 

function update(){  
  frameCounter.countFps();  //Call countFps function
  ++totalFrameCount;              //Increase total frame count
}

function display(){ 
 gameContext.clearRect(0, 0, canvasElement.width, canvasElement.height); 
   
 gameContext.textBaseline = 'top'; 
 gameContext.font = '10pt Arial';           
 gameContext.fillText(frameCounter.framePerSecond + '/second' ,5, 5);
 
 gameContext.font = '18pt Arial';           
 gameContext.fillText(totalFrameCount ,130, 80); 
}

window.addEventListener('load', init, false);

 

 

브라우저로 실행하면 가운데 누적 프레임 수와 좌측 상단의 초당 프레임 수를 확인할 수 있다 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Moving Object Ⅱ  (1) 2013.10.02
[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game] Game State  (0) 2013.09.27
[HTML5 Game] Game Loop  (0) 2013.09.25
[HTML5 Game] Collision Detection  (2) 2013.09.23

[HTML5 Game] Game State

Posted in 모바일/HTML5 // Posted at 2013. 9. 27. 21:00

게임은 상태의 집합이라 할 수 있다. 보통 우리가 게임을 할 때 대략 다음과 같은 수순의 단계를 만나게 된다.

 

게임 준비 -> 게임 실행 -> 게임 종료

 

물론 복잡한 게임에서는 더 다양한 단계가 있을 수 있지만, 개념을 이해하는 수준에서 위와 같이 간단히 세 가지 단계이 있고 한 번에 하나의 단계가 활성화되어 게임이 실행된다는 것을 이해하는게 중요하다.

 

이러한 게임 흐름의 각 단계를 '게임 상태'라 하며 HTML5 Game 개발에서도 이러한 상태의 전이를 통해 전반적인 게임 흐름을 관리하게 된다.

 

다음 그림은 게임 루프상에서 실행되는 게임 상태를 표현한 것이다. 게임 상태는 특정한 이벤트에 의해 서로 교체되며 한 번에 하나의 상태가 게임 루프 상에서 동작하여 게임이 진행된다.

 

 

 

게임 상태를 잘 분리해서 독립화시켜 적절히 모듈화한다면 개발과 유지보수에 좋은 영향을 미치게 된다. 따라서 게임 개발 시 각각의상태를 객체화 시켜서 관리하는 것이 권장된다.

 

 

그럼 이제 게임 상태와 상태의 변경을 통한 게임 흐름을 체험해 볼만한 간단한 샘플을 작성해 보자.

총 3가지 게임상태(ready, running, end)를 객체로 관리하고 키보드의 스페이스 바를 누르면 다음 상태로 이동하는 예인데, running 상태에서는 캔버스에 랜덤한 사격형을 그리는 이전 글(Game Loop, http://m.mkexdev.net/242)의 샘플을 그대로 사용할 것이다.

 

먼저 다음과 같이 HTML 파일을 준비한다.

<!DOCTYPE html>
<html lang="en">
 <head>  
  <script type="text/javascript" src="js/gameState.js"></script>
 </head> 
 <body>  
  <canvas id="GameCanvas" width="500" height="400" style="border: 1px solid #000;">
    HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
  </canvas>        
 </body>
</html>

 

 

다음으로 '준비/실행/종료'에 해당하는 각각의 게임상태를 객체로 관리하기 위한 3개의 생성자 함수를 정의하는데 공통 속성을 재사용하기 위해 프로토타입의 상속을 활용한다. 3가지 상태는 모두 게임 루프상에서 실행되기 위한 update(), display() 함수를 가지고 있으며 각 상태에 맞는 업데이트와 화면 갱신을 처리하도록 한다.

 

/* Define Base Game State Class */
function GameState(canvasElement){
 if(canvasElement != undefined){
     this.canvasSize = {width: canvasElement.width, height: canvasElement.height};  
     this.canvasContext = canvasElement.getContext('2d');                                    
   }
}

 

/* Define Ready State Class */
function Ready(canvasElement){
   //Call Parent Constract Function
   GameState.call(this,canvasElement);
}

 

Ready.prototype = new GameState(); //inherit

 

Ready.prototype.update = function(){ 
  this.canvasContext.fillStyle = '#000000'; 
}

 

Ready.prototype.display = function(){ 
   this.canvasContext.font = '18pt Arial';        
   this.canvasContext.textBaseline = 'top';
   this.canvasContext.fillText("Ready..." ,200, 150);
}

 

/* Define Running State Class */
function Running(canvasElement){
  //Call Parent Constract Function
  GameState.call(this,canvasElement);

  this.position = {};
}

 

Running.prototype = new GameState(); //inherit

 

Running.prototype.update = function(){ 
  this.position.x = Math.floor(Math.random() * (canvasElement.width - 20));
  this.position.y = Math.floor(Math.random() * (canvasElement.height - 20));
  
  this.canvasContext.fillStyle = 'rgb(' + Math.floor(Math.random() * 255) + ','
                    + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')'; 
}

 

Running.prototype.display = function(){ 
   this.canvasContext.fillRect(this.position.x, this.position.y, 20, 20);
}

 

/* Define End Class */
function End(canvasElement){
   //Call Parent Constract Function
   GameState.call(this,canvasElement);
}

 

End.prototype = new GameState(); //inherit

 

End.prototype.update = function(){ 
   this.canvasContext.fillStyle = '#000000'; 
}

 

End.prototype.display = function(){ 
   this.canvasContext.font = '18pt Arial';        
   this.canvasContext.textBaseline = 'top';
   this.canvasContext.fillText("End!!!" ,200, 150);
}

 

 

 

게임 상태가 준비되었으니, 각 상태가 실행되도록 다음과 같이 작성한다. 게임상태를 저장하기 위한 gameState 배열과 이 배열에서 특정 상태를 선택하기 위한 인덱스 값을 저장하는 currentGameStateIndex 변수를 선언한다. 키보드의 스페이스 바를 누르면 이 변수 값을 차례대로(0~2) 변경되도록 하여 게임 상태가 순환되도록 한다. 나머지 코드는 지금까지 학습했던 내용과 동일하므로 설명을 생략한다.

 

var fps = 10;
var canvasElement;
var gameContext;
var gameState = [];
var currentGameStateIndex = 0;

 

function init(){
   canvasElement = document.getElementById('GameCanvas');
   gameContext = canvasElement.getContext('2d'); 
 
   //Create Game State Instance & Push in gameState Array
   gameState = [new Ready(canvasElement),new Running(canvasElement),new End(canvasElement)];
 
   setInterval(gameLoop, 1000/fps);
}

 

function gameLoop(){ 
   gameState[currentGameStateIndex].update();
   gameState[currentGameStateIndex].display();
}

 

function ChangeGameState(e){
   if(e.keyCode == 32){ //32: Space Key
      gameContext.clearRect(0, 0, canvasElement.width, canvasElement.height);  
      //Change Game State Index(0 ~ 2) 
      currentGameStateIndex = (currentGameStateIndex + 1) % 3; 
   }
}

 

window.addEventListener('load', init, false);
window.addEventListener("keydown", ChangeGameState, false);

 

샘플을 브라우저로 확인해 보면 스페이스 키로 인해 다음의 3 단계가 순환되는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Moving Object  (0) 2013.10.02
[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game Loop  (0) 2013.09.25
[HTML5 Game] Collision Detection  (2) 2013.09.23
[HTML5 Game] Parallxing Background Animation  (0) 2013.09.21

[HTML5 Game] Game Loop

Posted in 모바일/HTML5 // Posted at 2013. 9. 25. 01:30

모든 마법은 게임루프(Game Loop)에서 일어난다!!!

 

처음 HTML5 Game 개발에 대한 학습을 시작할 때 가장 충격적(?)이었던 것이 바로 게임루프라는 개념이었다. 좀 과장된 표현이긴 하지만, 서버 측 어플리케이션 개발에 대부분의 시간을 보낸 나에게는 꽤나 신선한 느낌을 주었다. 물론 서버 측 어플리케이션에도 무한 반복 실행을 위한 루프 로직이 없는 것은 아니다. TCP 서버에서 입력 소켓을 지속적으로 리스닝 하는 것도 일종의 루프라 할 수 있고 주기적인 폴링을 위한 루프 로직도 심심찮게 구현해왔다. 하지만 초당 프레임 수에 기반한 매우 빠른 속도의 루프와 이벤트 처리, 상태 업데이트, 랜더링 등 거의 모든 로직이 스스로 살아 움직이는 게임 루프상에서 이뤄지는 개념은 적어도 나에게는 입가에 미소를 짓게 만들만큼 신선한 느낌이었다.

 

게임루프는 비단 HTML5 게임개발에만 적용되는 개념이 아니다. 어떤 프로그래밍 환경의 게임이라도 게임루프가 구현되며 큰 맥락에서는 동일한 개념과 로직이라고 할 수 있다.

 

혹자는 게임루프를 게임의 심장박동이라고 표현한다. 심장이 계속 뛰어야 하는 것처럼 게임이 살아있기 위해서는 게임루프가 지속적으로 실행되어야 하며 분당 심박 수처럼 게임 루프도 초당 프레임 수에 기반하므로 꽤 그럴싸한 비유라 하겠다.

 

 

업데이트와 디스플레이

게임 루프에서 모든 마법이 일어나지만 개념을 단순화 시키면 크게 두 가지 마법(?)이 일어난다고 할 수 있다.

바로 업데이트와 디스플레이다. 게임은 실행 중에 캐릭터가 이동하거나 레벨이 업데이트 되거나 하는 여러가지 상태 값의 업데이트가 필요하며 이렇게 업데이트 된 데이터를 기반으로 화면을 갱신해 줘야 한다. 이러한 게임 루프의 로직을 의사코드로 표현하면 다음과 같다.

 

gameLoop(){

update()

display()

}

 

 

FPS

FPS는 Frame Per Second의 약자로 초당 프레임 수를 말한다. 1초 동안 보여주는 화면의 수를 일컫는데 보통 영상을 상영할 때 필름의 프레임이 교체되는 속도 즉 프레임률(frame rate)과 동일한 개념이다. 보통 인간은 초당 15프레임 이상이면 깜빡임 현상을 거의 느끼지 못한다고 하며 25프레임 이상이면 비교적 자연스러운 영상으로 인식한다고 한다. 60프레임이상이면 잔상을 느끼지 못하게 된다.

 

프레임 수가 높을수록 더 부드러운 처리가 가능하지만 과도하게 높은 프레임 수는 게임 실행에 지장을 주게 되므로 성능과 하드웨어 환경 및 게임 상황을 고려해 적절한 프레임 수를 지정할 필요가 있다.

 

1) 고정 프레임 방식

초당 프레임 수를 일정하게 고정시키는 것을 고정 프레임 방식이라 한다. 이 방식은 한 프레임이 수행되는 시간이 아니라 초당 프레임 수행 횟수에 의존하는 방식이며 이 프레임 수행 횟수를 매 초 마다 고정시키는 방식이다.자료에 의하면 고정 프레임 방식은 하드웨어 환경을 미리 파악할 수 있어서, 하나의 프레임에 걸리는 시간을 미리 결정할 수 있는 콘솔 게임들에서 흔히 사용되어 왔다고 한다. HTML5 게임 개발에서 고정 프레임 방식을 적용할 경우 자바스크립트의 setInterval() 함수를 사용할 수 있다.

 

2) 가변 프레임 방식

초당 프레임 수가 일정하지 않고 유동적으로 변하는 것을 가변 프레임 방식이라 한다. 가변 프레임 환경에서 한 프레임의 수행 시간은 그 프레임에서 그려야 할 화면의 디스플레이 시간에 의존적이기 때문에 게임 상황에 따라 달라질 수 있다. 즉 초당 프레임 횟수가 아니라 프레임 수행 시간에 의존적인 방식이다. 따라서 하드웨어의 성능이나 게임 상황에 따라 초당 프레임 수가 달라질 수 있다는 것이다. 대부분의 PC게임이 이 방식으로 구현된다고 한다. HTML5 게임 개발에서 고정 프레임 방식을 적용할 경우 자바스크립트의 setTimeout() 함수를 사용할 수 있다.

 

고정 프레임 방식의 게임 루프를 코드로 표현하면 대략 다음과 같다.

var fps = 10;

setInterval
(gameLoop, 1000/fps);

 

자바스크립트의 setInterval() 함수는, 두 번째 매개변수로 지정된 시간 간격으로 함수를 반복해서 실행한다. 이때 시간 단위는 1/1000초 즉, 밀리세컨드 단위로 지정하게 된다.

 

코드에서는 1000/fps = 1000/10은 1초에 10번 gameLoop() 함수를 실행하라는 의미가 된다. 다시말해 1000/10 = 100 즉, 100ms (0.1초) 마다 함수를 호출하도록 한다.

 

게임 루프 구현

게임 루프와 FPS의 개념을 알아봤으니 이 개념을 기반으로 간단한 게임 루프를 HTML5 기반으로 구현해보자.

여기서 구현해 볼 예제는 게임 루프 상에서 Canvas 영역안에 임의의 색상의 사각형을 랜덤하게 반복적으로 그려주는 예이다.

 

먼저 HTML 파일을 다음과 같이 작성한다.

<!DOCTYPE html>
<html lang="en">
 <head>  
  <script type="text/javascript" src="js/gameLoop.js"></script>
 </head> 
 <body>  
  <canvas id="GameCanvas" width="500" height="400" style="border: 1px solid #000;">
   HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
  </canvas>        
 </body>
</html>

 

그리고 다음과 같이 자바스크립트를 작성한다. 게임 루프가 어떤 식으로 구현되는지 자세히 살펴보기 바라며 사각형의 위치와 색상이 랜덤하게 지정되도록 했다.

var fps = 10;
var canvasElement;
var gameContext;
var position = {};

function init(){
   canvasElement = document.getElementById('GameCanvas');
   gameContext = canvasElement.getContext('2d'); 
   setInterval(gameLoop, 1000/fps);
}

 

function gameLoop(){ 
   update();
   display();
}

 

function update(){
   //Set Rectangle Position(Random Positioning In Canvas)
   position.x = Math.floor(Math.random() * (canvasElement.width - 20));  //0~480
   position.y = Math.floor(Math.random() * (canvasElement.height - 20)); //0~380
 
   //Set Random Coloring
   gameContext.fillStyle = 'rgb(' + Math.floor(Math.random() * 255) + ','
                       + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';  
}

 

function display(){  
   gameContext.fillRect(position.x, position.y, 20, 20);
}

 

window.addEventListener('load', init, false);

 

 

이제 예제를 브라우저로 실행해보면 다음과 같은 화면을 볼 수 있을 것이다.

fps를 10으로 지정했기 때문에 1초에 10번 사각형이 그려질 것이며 이를 무한 반복하게 되는 것을 확인할 수 있다.

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game]Calculating FPS  (0) 2013.09.30
[HTML5 Game] Game State  (0) 2013.09.27
[HTML5 Game] Collision Detection  (2) 2013.09.23
[HTML5 Game] Parallxing Background Animation  (0) 2013.09.21
[HTML5 Game] Background Animation  (1) 2013.09.20

[HTML5 Game] Collision Detection

Posted in 모바일/HTML5 // Posted at 2013. 9. 23. 21:30

거의 대부분의 게임은 충돌감지를 필요로 한다.

 

플레이어를 향해 달려오는 적(enemy), 적을 향해 쏘는 총알 등 게임 요소들간 충돌을 구현하기 위해서는 개별 요소들의 시각적 겹침을 처리해야 한다.

 

2D 게임에서의 충돌 감지는, 충돌 감지 대상이 되는 게임 요소의 형태에 따라 충돌박스, 충돌구, 충돌점 등 상황에 맞는 감지 로직을 구현해야 한다.

 

여기서는 가장 기본이 되는 충돌 박스에 대해 알아 보겠다.

 

* 간단한 개념

앞서 언급했듯이, 충돌 감지는 두 그래픽 요소의 시각적 겹침을 처리하면 된다. 겹침은 그래픽 요소의 X, Y 좌표와 요소의 크기(너비/높이) 값이 기준이 되는데 다음과 같이 크게 두 영역의 겹첨을 감지하면 된다.

 

1) X 좌표 겹침

player 와 enemy 라는 두 그래픽 요소가 있다고 가정하자.

player를 기준으로 보면, enemy가 player의 X좌표 영역 안에 있을 경우 충돌되었다고 할 수 있다.

 

아래 그림을 보면 이해가 쉬울 것이다.

먼저 enemy의 우측 끝 X좌표(enemy.x + enemy.width)가 player의 좌측 끝 X좌표(player.x)보다 크고,

enemy의 좌측 끝 X좌표(enemy.x)가 player의 우측 끝 X좌표(player.x + player.width)보다 작으면 서로 겹침 즉, 충돌되었다고 할 수 있다.

 

2) Y 좌표 겹침

같은 개념으로 Y 좌표 겹침을 감지할 수 있다.

먼저 enemy의 하단 끝 Y좌표(enemy.y + enemy.height)가 player의 상단 끝 Y좌표(player.y)보다 크고,

enemy의 상단 끝 Y좌표(enemy.y)가 player의 하단 끝 Y 좌표(player.y + player.height)보다 작으면 서로 겹침 즉, 충돌되었다고 할 수 있다.

 

결론적으로 두 요소의 X 좌표 겹침과 Y 좌표 겹침이 둘다 만족한다면 서로 충돌되었다고 감지하면 되는 것이다.이 개념을 코드화 하면 다음과 같다.

function IsCollision(enemy, player) {
  return enemy.x < player.x + player.width &&
           enemy.x + enemy.width > player.x &&
           enemy.y < player.y + player.height &&
           enemy.y + enemy.height > player.y;

}

 

 

* HTML

충돌 박스에 근거한 간단한 예제를 만들어 보겠다. 먼저 다음과 같이 HTML 파일을 구성한다. 캔버스와 마우스 좌표를 표시하기 위한 span 요소를 정의한다.

<!DOCTYPE html>
<html lang="en">
 <head>  
  <script type="text/javascript" src="js/collisionBox.js"></script>
 </head>
 <body>
   <canvas id="GameCanvas" width="400" height="300" style="border: 1px solid #000;">
      HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
   </canvas>
   <br />
   <span id="mousePositionDisplay"></span>
 </body>
</html>

 

 

* Javascript

먼저 Player 객체를 위한 Box 생성자 함수를 정의하는데, Player가 위치할 X,Y 좌표와 크기(너비/높이)를 매개변수로 전달받도록 한다.

function Box(x, y, width, height){
 this.x = x;
 this.y = y;
 this.width = width;
 this.height = height; 
}

 

다음으로 문서의 로딩 완료와 마우스 움직임을 처리하기 위한 이벤트 리스너를 등록하고 필요한 전역 변수를 정의한다. 예제의 단순함을 위해 enemy 요소는 따로 정의하지 않고 마우스 포인트를 사용할 것이다. 즉 Player 박스와 마우스 포인터의 겹침(충돌)을 감지하게 되므로 마우스 이벤트가 필요하다.

window.addEventListener("load", init, false);
window.addEventListener("mousemove", onMousemove, false);

 

var canvasClientRect;  //Canvas Client Bounding Rect
var gameContext;      //Canvas Context
var box;                   //Player Box

 

그리고 초기화 함수와 Player 박스를 그리는 함수를 정의한다. 여기서 주의깊게 봐야 할 것은 Canvas의 getBoundingClientRect() 함수이다. Canvas가 문서에 표시될 때 div와 같은 부모요소에 포함될 수도 있고 문서 자체의 margin, padding 값에 의해 문서의 좌측 상단에서 떨어져 있을 수 있다.

 

다시말해, Canvas의 좌측 상단의 좌표가(0,0)이 아닐 수 있다는 것이다. Canvas에 포함된 게임 요소들의 충돌 감지를 위해서는 Canvas의 좌측 상단의 좌표가 (0,0)이어야 하므로 문서와 Canvas의 간격을 좌표계산에서 제거해야 한다. 이를 위해 문서 자체의 margin값을 0으로 설정하거나 Canvas의 offSet값을 계산에 포함할 수도 있지만 좀더 범용적으로 적용하기 위해서 getBoundingClientRect()함수를 사용하는 것이 좋다. 이 함수를 이용하면 Canvas가 문서로부터 얼마나 떨어져 있는지 알 수 있으므로 쉽게 계산할 수 있게 된다.

function init(){ 
  var canvasElement = document.getElementById("GameCanvas");
  //Get Canvas Bounding ClientRect
  canvasClientRect = canvasElement.getBoundingClientRect();  
  gameContext = canvasElement.getContext('2d');  

  //Draw Player Box
  drawBox();
}

 

function drawBox(){
  //Create Player Box Instance
  box = new Box(150,100,100,100); 
  gameContext.fillStyle = '#000000';
  //Draw Player Box
  gameContext.fillRect(box.x,box.y,box.width,box.height);
}

 

이제 남은건 마우스 무브 이벤트와 충돌감지 로직이다. 이 코드가 핵심인데 다음과 같이 구현한다.

앞서 설명한대로 Player 요소와 마우스 포인터의 겹침을 감지하고 충돌 시 이를 표시해 준다.

function onMousemove(e){
 //Clear Canvas  
 gameContext.clearRect(0, 0, canvasClientRect.width, canvasClientRect.height);
  
 //Draw Player Box
 drawBox();
   
 //Calculate Mouse Position based on Canvas
 mouseXinCanvas = e.clientX - canvasClientRect.left;
 mouseYinCanvas = e.clientY- canvasClientRect.top;
     
 //Collision Detection 
 if(IsCollision(box,mouseXinCanvas,mouseYinCanvas)){
    gameContext.fillStyle = '#ffffff';  
    gameContext.font = '18pt Arial';        
    gameContext.textBaseline = 'top';
    gameContext.fillText("collision!", box.x, box.y);
 }
 
 //Display Mouse Position
 document.getElementById("mousePositionDisplay").innerText =
                                                                   "x:" + mouseXinCanvas + ", y:" + mouseYinCanvas;
}

function IsCollision(box, x, y){    
   return  x > box.x  && x < box.x + box.width &&
              y > box.y  && y < box.y + box.height      
}

 

 

* 실행 화면

예제를 브라우저로 실행해 보면 마우스 포인터가 Player 박스 안에 들어가면 충돌되었다고 표시하는 것을 확인할 수 있을 것이다. 물론 이 예제에서는 마우스 포인트가 enemy를 대신하기 때문에 enemy의 박스 계산(너비/높이 계산)이 필요없게 되지만 개념은 동일하다. 

 

 

 

 

 

 

 

 

 

 

 

'모바일 > HTML5' 카테고리의 다른 글

[HTML5 Game] Game State  (0) 2013.09.27
[HTML5 Game] Game Loop  (0) 2013.09.25
[HTML5 Game] Parallxing Background Animation  (0) 2013.09.21
[HTML5 Game] Background Animation  (1) 2013.09.20
[HTML5 Game] Character Animation Using Trident.js  (3) 2013.09.17

[HTML5 Game] Parallxing Background Animation

Posted in 모바일/HTML5 // Posted at 2013. 9. 21. 17:10

지난 포스팅에서 백그라운드 애니메이션 처리 기법을 알아 보았다.

=> http://m.mkexdev.net/239

 

이 글에서는 하나의 배경이미지를 사용해서 애니메이션을 구현했는데,

이번 글에서는 더욱 현실감있게 하기 위해 몇 개의 배경을 중첩시켜서 서로 다른 속도로 애니메이션이 동작하도록 구현해 볼 것이다.

 

여러개의 배경이 서로 중첩되어 각기 다른 속도로 움직이도록 처리하면 게임을 더욱 실감나게 만들 수 있는데, 이를 시차 스크롤 방식이라 한다. 이에 대한 깔끔한 설명은 'LEARNING HTML5 온라인 게임 개발 프로그래밍'의 설명을 인용한다.

 

 수퍼 마리오나 소닉과 같은 측면 스크롤 방식의 게임에서 3차원 공간감을 더욱 실감나게 만드는 기술의 하나가 바로 시차(Parallxing)다.

...

 

자동차를 타고 다리를 건너가면서 차창 밖으로 지나가는 풍경을 바라보는 것과 같다고 생각하면 쉽게 이해될 것이다. 저 멀리 보이는 산이라든가 도시의 스카이라인에 비해 자동차에서 가까운 다리의 기둥들은 훨씬 빠른 속도로 시야를 스쳐 지나간다.

 

즉 가까운 거리에 있는 배경은 상대적으로 빠르게 움직이고 멀리 있을 수록 천천히 움직이도록 배경에 시차 스크롤링 효과를 주면 2D게임의 3차원 효과를 줄 수 있게 된다.

 

* 간단한 개념

시차효과를 구현하기 위해서는 각각의 배경을 별도의 이미지로 만들고 이를 중첩시켜서 서로 다른 속도로 애니메이션이 동작하도록 구현해야 한다. 즉 Canvas에 배경 이미지가 계층을 이루어 그려져야 하는데 이때 다른 이미지를 덮게 되는 이미지는 투명효과를 지원하도록 하여 밑에 깔린 이미지를 가리지 않도록 해야한다. 이것은 이미지를 제작할 때 고려되어야 하는 부분으로 디자이너의 몫이라 하겠다. 이번 예제에서는 개인적으로 투명이미지를 만들 수 있는 디자인 역량이 없는 관계로 앞에서 언급한 책의 이미지를 사용할 것이다.

 

 

총 4개의 배경 이미지가 사용되는데 이 이미지들은 자신의 표현부분을 제외하고는 모두 투명효과를 지원해서 차례대로 중첩시키면 다음과 같은 모양이 나온다.

 

 

이 4개의 배경이미지를 서로 다른 속도로 동작하도록 구현하면 시차 효과가 적용된 배경 애니메이션이 완성되는 것이다. 이때 서로 다른 속도를 구현하기 위한 다음과 같은 방법이 사용될 수 있다.

 

1) fps 조절

초당 프레임수를 조정하여 가까이 있는 배경일수록 더 빠른 fps를 사용한다. 다만 그다지 권장하고 싶지는 않다. 배경 이미지 애니메이션을 위해 fps가 따로 관리되어야 한다면 배보다 배꼽이 더 크지는 느낌이다.

 

2) 이미지 자르기 위치 조절

이전 글에서 살펴본대로 배경 애니메이션은 게임 루프상에서 이미지 자르기와 두 번의 이미지 그리기 작업을 반복함으로써 구현할 수 있었다. 즉 배경 이미지의 이동 간격을 서로 다르게 하여 시차 효과를 구현할 수 있다.

이 방법도 다음과 같이 두 가지로 나눠볼 수 있겠다.

 

- 이미지 크기 조절

앞에서 언급한 책인, 'LEARNING HTML5 온라인 게임 개발 프로그래밍'에서 구현한 방식이다. 이미지 크기를 배수로 조절하여 계층간 이미지 이동 간격을 별도로 계산하지 않아도 되도록 한다. 즉 첫번째 계층은 320px, 두 번째는 640px, 세 번째는 960px 너비를 갖게 함으로써 동일 시간 내에 점점 더 많은 부분을 보여주도록 한다.

 

- 이동 간격 조절

앞서, 이미지 크기 조절과 개념적으로는 동일하지만 이미지 크기에 제약을 두지는 않는다. 이미지 크기 조절 방식이 편리한 측면이 있으나 예제의 포괄성(?)을 위해 이미지 크기를 직접 손대지 않고 이동 간격을 조절할 수 있도록 한다. 이번 글의 예제에서 사용하는 방식이다.

 

* HTML 

<!DOCTYPE html>
<html lang="en">
 <head>  
  <script type="text/javascript" src="js/animationParallxingBackground.js"></script>
 </head>
 <body>
  <canvas id="GameCanvas" width="320" height="200" style="border: 1px solid #000;">
   HTML5 Canvas를 지원하지 않습니다. 크롬 또는 사파리와 같은 HTML5 지원 브라우저를 이용해 주세요
  </canvas>
 </body>
</html>

 

 

* Javascript

'LEARNING HTML5 온라인 게임 개발 프로그래밍' 책의 예제 이미지를 사용했지만 코드는 이전 글들과의 맥락을 위해 사용하지 않겠다. 이 책에서는 Trident.js를 사용해 시차 효과를 구현하고 있다. 여기서는 이전 글들의 코드 구조를 유지하면서 라이브러리 없이 직접 구현하도록 한다.

 

1. 백그라운드, 생성자 함수와 메서드

/* Define Background Class */
function Background(assets,canvasElement){
   this.assets = assets; 
   this.canvasSize = {width: canvasElement.width, height: canvasElement.height}; //Canvas Size
   this.canvasContext = canvasElement.getContext('2d');                                   //Canvas Context
   this.spritesX = [];
   for(var i = 0; i < assets.length; i++){
      this.spritesX.push(0);
   }
}

 

Background.prototype.startAnimation = function(){ 
 //Clear Canvas  
 this.canvasContext.clearRect(0, 0, this.canvasSize.width, this.canvasSize.height);
 
 for(var i = 0; i < this.assets.length; i++){  
  //Draw Background Image
  var drawX = this.spritesX[i] * this.assets[i].bgImage.width;
  var drawWidth = this.assets[i].bgImage.width - drawX;
  this.canvasContext.drawImage(this.assets[i].bgImage,

                   drawX, 0, drawWidth, this.assets[i].bgImage.height,

                   0, 0, drawWidth, this.assets[i].bgImage.height);     
         
  //Fill Cut Out area     
  if(drawWidth < this.assets[i].bgImage.width) {
    var fillDrawWidth = this.assets[i].bgImage.width - drawWidth;
    this.canvasContext.drawImage(this.assets[i].bgImage,

                    0, 0, fillDrawWidth, this.assets[i].bgImage.height,

                    drawWidth, 0, fillDrawWidth, this.assets[i].bgImage.height);
  }      
 
  this.spritesX[i] = (this.spritesX[i] + this.assets[i].spritesRate) % 1;   
 } 
}

 

배경 이미지를 다뤘던 이전 글의 흐름과 거의 동일하다. 다만 이번 글에서는 중첩된 배경 이미지들이 사용될 것이기에 생성자 함수에 커스텀 객체 배열(assets)을 받는 부분과 이 배열을 기반으로 그리기 좌표 등이 계산되는 부분만 변경되었다. assets 배열의 개별 객체에는 배경 이미지 이외에도 이동 간격을 나타내는 속성인 spritesRate가 정의되어 있는데 이 값을 기준으로 자르기 X좌표(spritesX) 값의 수정이 이뤄진다. 생성자 함수에서는 spritesX를 배경 이미지 수 만큼 지정할 수 있도록 하였으며 처음에는 모두 0으로 초기화한다.

 

2. 기타 프로그램 로직

역시 이전 포스팅의 예제와 거의 동일하다. 중요한 것은 assets 배열에 담기는 객체의 spritesRate 속성과 그 값이다. 이 값은 이미지 자르기 x좌표를 계산하기 위한 값인데, 이 값의 크기를 배경 이미지마다 다르게 지정하여 애니메이션 속도를 조절하게 된다. 가장 밑에 깔리는 배경은 움직임이 없게 하기 위해 값을 '0'으로 지정하고 이어지는 배경이미지들은 직전 이미지보다 2배 빠른 속도로 애니에미션이 될 수 있도록 값을 지정했다.

var fps = 60;                           //frame per second
var background;                        //Character Instance
var canvasElement;                    //Canvas Element
var assetfiles;                            //Asset Image File Array
var assets = [];                        //Custom Asset Object Array  
var currentAssetLoadCount = 0;  //Asset Image File Load Count  

 

function init(){ 
 canvasElement = document.getElementById("GameCanvas"); 
 
 //Define Asset Image File Array
 assetfiles = ['image/Parallax0.gif', 'image/Parallax1.gif', 'image/Parallax2.gif', 'image/Parallax3.gif'];
 
 //Create Custom Literal Object(Define spritesRate Property) & Insert into Asset Image Array
 assets.push({spritesRate: 0});
 assets.push({spritesRate: 0.001});
 assets.push({spritesRate: 0.002});
 assets.push({spritesRate: 0.004});
  
 for (var i = 0; i < assetfiles.length; i++) {
    //Create Asset Image Ojbect
    var asset = new Image(); 
    asset.src = assetfiles[i];     
  
    //Assign Asset Image Object to the bgImage property that newly created
    assets[i].bgImage = asset;    
      
    //Assign Imgae Load Event
    asset.onload = onAssetLoadComplete;      
  }     
}

function onAssetLoadComplete(){ 
   //Check Load Complete of All Images
   if(++currentAssetLoadCount >= assetfiles.length){ 
     //Create Character Instance    
     background = new Background(assets,canvasElement);
     //Run Game Loop     
     setInterval(animationLoop, 1000 / fps);   
 } 
}

 

function animationLoop(){
   background.startAnimation();
}

window.addEventListener("load", init, false);

 

 

* 실행화면 

 

 

* 마무리하며...

두 번의 포스팅을 통해 배경 이미지 애니메이션과 시차 효과가 적용된 애니메이션 구현 기법에 대해 알아보았다. 캐릭터 애니메이션이나 배경 애니메이션 모두 게임 루프상에서 이미지 그리기를 지속적으로 업데이트 하는, 즉 궁극적으로는 동일한 매커니즘이 사용됨을 알 수 있었다. 이 지식은 어떠한 2차원 배경 이미지 애니메이션도 구현할 수 있는 초석이 될 것이다. 개념이 정립되었으면 각자 취향에 맞는 배경 애니메이션을 살을 붙여 가며 구현해 보기 바란다.