내일배움캠프 Node.js 사전캠프 12일차
1. ZEP에서 이루어진 팀단위 JavaScript 스터디
(1) 스터디 계획
르탄이 슈팅 게임 (ppiok-owo.github.io)
1) HP 포션 아이템 추가
2) 피격 효과음, 이미지 추가
3) Math.cos, Math.PI 이용해서 대각선 이동하기
4) 코드 간결하게 정리해 보기
지난 시간에 이어서 게임 내 발생하던 버그를 수정하고 기능을 정확히 구현하기 위해 코드를 작성해 주었다.
(2) 코드 작성하기
타이머 변수 제대로 이용하기
어제 작성한 코드에서 타이머 기능을 더 명확히 표현하기 위해 생성되는 오브젝트에 대한 타이머들을 각각 만들어 주었다. enemyTimer, itemTimer, bulletTimer라는 변수명에 프레임당 걸리는 시간이 계속 누적이 될 것이고, 이 변수를 통해 생성 빈도를 조절하는 방향으로 코드 가독성을 높여주었다.
let lastFrameTime = 0;
function animate(frameTime) {
requestAnimationFrame(animate);
// animate(frameTime) 함수를 실행할 때 frameTime 인자에 페이지가 로드된 이후 경과된 시간을 반환해준다.
deltaTime = (frameTime - lastFrameTime) / 1000;
// frameTime - lastFrameTime : 1프레임 동안 걸리는 시간(밀리초)
// ((frameTime - lastFrameTime) / 1000): 1프레임당 걸린 시간을 초 단위로 변환
lastFrameTime = frameTime;
accumulatedTime += deltaTime; // 총 누적 시간 (게임이 실행된 시간)
enemyTimer += deltaTime; // 적 생성 타이머
itemTimer += deltaTime; // 아이템 생성 타이머
bulletTimer += deltaTime; // 총알 생성 타이머
}
적을 생성할 때 사용하는 예시
/** 적 생성 및 업데이트 */
if (enemyTimer >= ENEMY_FREQUENCY) {// 0.5초마다 적 생성
const enemy = new Enemy();
enemyArray.push(enemy);
gameTimer = 0;
}
이를 총알 객체에 적용시킨다면 다음과 같이 코드를 짤 수 있다.
// 스페이스바(공백)를 누를 시 총알 발사
// 스페이스바(공백)를 누를 시 총알 발사(0.3초 딜레이)
if (keyPresses[" "]) {
if (bulletTimer >= 0.4) {
const bullet = new Bullet();
bulletArray.push(bullet);
bulletSound.currentTime = 0;
bulletSound.play();
bulletTimer = 0;
// 폭주 모드일 때 총알 두 개 추가
if (rtan.Israge) {
const bullet2 = new Bullet2();
const bullet3 = new Bullet3();
bullet2.draw();
bullet2.update();
bullet3.draw();
bullet3.update();
bulletArray.push(bullet2);
bulletArray.push(bullet3);
bulletSound.currentTime = 0;
bulletSound.play();
bulletTimer = 0;
if (bullet.x > canvas.width) bulletArray.splice(bulletIndex, 1);
}
}
}
총알 클래스를 여러 개 생성하는 게 아니라 메서드로 총알의 개수를 제어하는 방식으로 코드를 수정해보려고 한다. 타이머 또한 각 객체의 메서드에 삽입하는 방식으로 바꿔볼 수 있을 것 같다.
편의성 향상시키기 - HP 포션
게임에 HP바를 추가하였으나, 정작 깎인 HP를 복구할 방법이 없었기 때문에 다소 아쉬웠다.
따라서 HP 포션을 아이템으로 삽입하고 체력을 다시 회복할 수 있게끔 코드를 작성해 보았다.
/** HP Potion 정의 */
const HPPOTION_FREQUENCY = 5; // 5초마다 생성되게 만들고 싶음
class HpPotion {
constructor() {
let HPPOTION_Y = Math.random() * (canvas.height - 50 - 30) + 30;
this.x = canvas.width;
this.y = HPPOTION_Y;
this.width = 50;
this.height = 50;
this.speed = 5;
this.healingValue = 10;
this.IsConsumed = false; // 이미 획득한 포션인지 확인하는 용도
}
draw() {
ctx.drawImage(HPpotionImage, this.x, this.y, this.width, this.height);
}
update() {
this.x -= this.speed;
}
}
/** HP 포션 생성 및 업데이트 */
if (itemTimer >= HPPOTION_FREQUENCY) { // 아이템 생성 타이머가 5초를 넘기면 객체 생성
const hppotion = new HpPotion();
hpPotionArray.push(hppotion);
itemTimer = 0; // 타이머 초기화
}
hpPotionArray.forEach((hppotion, hppotionIndex) => {
hppotion.draw();
hppotion.update();
if (hppotion.x < 0) {
hpPotionArray.shift();
}
// HP 포션 객체들 충돌 검사
if (collision(rtan, hppotion)) {
if (!hppotion.IsConsumed) { // 이미 획득한 물약은 충돌 검사에서 제외시킨다.
hppotion.IsConsumed = true;
hpPotionArray.splice(hppotionIndex, 1); // 포션을 획득하면 배열에서 없애주기
rtan.hp += hppotion.healingValue;
HP_bar.width += hppotion.healingValue * HP_BAR_WIDTH_COEFF;
if (rtan.hp > maxHp) { // 이미 피가 가득 차있으면 수치가 더 증가하지 않도록, 포션을 먹어도 획득 효과음이 들리지 않도록 설정해두었다.
rtan.hp = maxHp;
HP_bar.width = maxHp * HP_BAR_WIDTH_COEFF;
hpText.innerHTML = "HP : " + rtan.hp;
} else {
hpText.innerHTML = "HP : " + rtan.hp;
getItemSound.pause();
getItemSound.currentTime = 0;
getItemSound.play();
}
}
}
});
대각선 이동
그 동안 대각선 이동거리를 x 값 증가량, y값 증가량으로 코드를 작성한 바 있다.
엄밀히 따지자면 상하좌우의 이동거리보다 대각선의 이동거리가 더 길었던 터라 조작감이 그닥 균일하지 못했다. 마침 편의성을 향상시키기 위해 코드를 고치고 있었으므로, 겸사겸사 해당 부분을 수정하기로 결정했다.
/ 대각선으로 이동하기
if ((keyPresses.w || keyPresses.W) && (keyPresses.a || keyPresses.A)) {
rtan.x -= speed * Math.cos(Math.PI / 4) * deltaTime * 60;
rtan.y -= speed * Math.cos(Math.PI / 4) * deltaTime * 60;
if (rtan.x < -rtan.width) rtan.x = 0;
if (rtan.y < 20) rtan.y = 20;
} else if ((keyPresses.w || keyPresses.W) && (keyPresses.d || keyPresses.D)) {
rtan.x += speed * Math.cos(Math.PI / 4) * deltaTime * 60;
rtan.y -= speed * Math.cos(Math.PI / 4) * deltaTime * 60;
if (rtan.x > canvas.width) rtan.x = canvas.width - rtan.width;
if (rtan.y < 20) rtan.y = 20;
} else if ((keyPresses.s || keyPresses.S) && (keyPresses.a || keyPresses.A)) {
rtan.x -= speed * Math.cos(Math.PI / 4) * deltaTime * 60;
rtan.y += speed * Math.cos(Math.PI / 4) * deltaTime * 60;
if (rtan.x < -rtan.width) rtan.x = 0;
if (rtan.y > RTAN_Y) rtan.y = RTAN_Y;
} else if ((keyPresses.s || keyPresses.S) && (keyPresses.d || keyPresses.D)) {
rtan.x += speed * Math.cos(Math.PI / 4) * deltaTime * 60;
rtan.y += speed * Math.cos(Math.PI / 4) * deltaTime * 60;
if (rtan.x > canvas.width) rtan.x = canvas.width - rtan.width;
if (rtan.y > RTAN_Y) rtan.y = RTAN_Y;
}
A라는 오브젝트가 대각선 45도 방향으로 이동하려는 거리를 S라고 한다면, A가 X방향으로 이동한 거리는 S * cos(PI/4)
로 계산할 수 있다. 자바스크립트에서 PI값은 Math.PI로 구할 수 있고 Math.cos()로 코사인값을 계산할 수 있으므로 위와 같이 코드를 작성해보았다.
코드를 수정한 결과, 게임의 조작감이 훨씬 부드러워져서 바꾸길 잘했다는 생각이 든다. 단지 귀찮다는 이유로 변경을 보류하고 있던 코드였는데, 이제 보니 플레이 경험에 유의미한 영향을 끼치고 있었던 것 같다. 앞으로는 사소하다고 넘어가기보다는 좀 더 고민하며 코드를 작성해야겠다.
댓글