[HTML5 Game] Firing Bullet
이전 글(http://m.mkexdev.net/247)에서 게임 루프를 기반으로 게임 오브젝트 이동에 대해 알아 보았다.
키보드의 방향키에 반응해 전투기가 이동하는 샘플을 작성해 봤는데 여기서 덧붙여 전투기에서 총알이 발사되도록 해 볼 것이다. 키보드의 스페이스 바를 누르면 총알이 발사되며 키를 계속 누르고 있으면 총알 다발이 연속해서 발사되는 시나리오이다.
총알 이미지
총알으로 사용할 이미지는 총알을 제외한 배경이 투명한 형태로 만드는 것이 좋다. 그러나 투명 이지미를 수급하기 쉽지 않아, 여기서는 캔버스 배경과 동일한 흰색 배경의 총알 이미지를 사용할 것이다. (ppt로 간단히 만들고 캡쳐해서 이미지로 변환하자 ㅎㅎ)
데모 실행 화면
실제 구현에 앞서 데모 실행화면을 먼저 확인해 보자. 이전 글의 예제와 같이 전투기는 방향키에 반응해 이동을 하며, 여기서 더해 스페이스 키를 누르면 총알이 위쪽 방향으로 발사된다.
구현 내용 정리
구현해야 할 내용을 큰 맥락에서만 정리해 보자.
- 스페이스 키를 누르면 총알 이미지가 비행기 중앙에서 출발하도록 한다
- 총알은 연속적으로 발사되기 때문에 배열로 관리한다.
- 발사된 총알이 위쪽 방향으로 계속 이동하게끔 하기 위해 Y 좌표 값을 업데이트 한다
대략 이 정도만 정리하고 자세한 건 구현하면서 살펴 보도록 하자.
이전 글에서는 전투기 객체를 Character로 명명했었는데, 맘에 들지 않아 Fighter로 변경하고 메서드 이름도 drawFighter 로 변경하였다. 그리고 총알을 그리기 위해 추가된 코드는 파란색으로 표시했다.
총알은 하나 이상 발사될 수 있기 때문에 배열로 관리하며 발사된 총알 수 만큼 배열 요소가 생성된다. drawFighter 메서드에서는 전투기를 그리기 전에 배열에 담긴 총알을 그려준다. 이때 캔버스의 맨 위쪽까지 도달했을 경우(Y 좌표가 '0'일 경우) 배열에서 제거하고 루프를 건너뛴다. 이렇게 하지 않으면 캔버스를 벗어난 총알도 배열에 계속 잔존하게 되고 이 배열은 게임 실행 중 계속 커져 버려서 성능에 좋지 못하다. 따라서 반드시 총알 제거를 해 주어야 한다.(참고로 캔버스의 위쪽 끝 Y 좌표가 오프셋 값 등으로 인해 0보다 클 수 있어나 큰 의미 없으므로 그냥 0 값을 기준으로 한다)
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 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;
}
}