본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 5. · 12 Views
게임 개발 생명력과 데미지 시스템 완벽 가이드
게임 개발에서 필수적인 생명력과 데미지 시스템을 Phaser.js로 구현하는 방법을 다룹니다. 체력 변수 관리부터 무적 시간, 하트 UI, 게임 오버, 리스폰까지 완전한 시스템을 구축합니다.
목차
1. 생명력 변수 관리
김개발 씨가 처음으로 게임을 만들기 시작했습니다. 캐릭터가 화면에서 움직이는 것까지는 성공했는데, 이제 적에게 맞으면 체력이 줄어드는 기능을 넣고 싶습니다.
"체력은 어떻게 관리해야 하지?" 간단해 보이지만, 제대로 설계하지 않으면 나중에 큰 문제가 될 수 있습니다.
생명력 변수 관리는 게임 캐릭터의 현재 체력과 최대 체력을 체계적으로 추적하는 것입니다. 마치 자동차의 연료 게이지처럼, 현재 얼마나 남았는지와 최대 용량이 얼마인지를 함께 관리해야 합니다.
이를 제대로 구현하면 체력 회복, 버프, 난이도 조절 등 다양한 기능을 쉽게 확장할 수 있습니다.
다음 코드를 살펴봅시다.
class Player extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'player');
scene.add.existing(this);
scene.physics.add.existing(this);
// 생명력 관련 변수 초기화
this.maxHealth = 3; // 최대 체력
this.currentHealth = this.maxHealth; // 현재 체력
this.isAlive = true; // 생존 여부
}
// 체력 확인 메서드
getHealthPercentage() {
return this.currentHealth / this.maxHealth;
}
}
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 회사에서 간단한 모바일 게임 프로젝트에 투입되었는데, 선배가 "캐릭터 체력 시스템 좀 만들어볼래?"라고 했습니다.
단순히 변수 하나 만들면 될 줄 알았는데, 기획서를 보니 체력 회복 아이템도 있고, 보스전에서는 최대 체력이 늘어나는 버프도 있었습니다. 선배 개발자 박시니어 씨가 다가와 조언합니다.
"체력 시스템은 처음부터 확장성을 고려해서 설계해야 해요. 나중에 바꾸려면 정말 골치 아프거든요." 그렇다면 생명력 변수 관리란 정확히 무엇일까요?
쉽게 비유하자면, 생명력 시스템은 마치 스마트폰의 배터리 관리와 같습니다. 스마트폰은 현재 배터리 잔량과 최대 용량을 따로 관리합니다.
배터리가 노후화되면 최대 용량이 줄어들기도 하고, 급속 충전으로 빠르게 채울 수도 있습니다. 게임의 체력도 마찬가지입니다.
현재 체력과 최대 체력을 분리해서 관리해야 다양한 상황에 대응할 수 있습니다. 만약 체력을 단순히 숫자 하나로만 관리한다면 어떤 문제가 생길까요?
회복 아이템을 먹었을 때 체력이 최대치를 넘어버리는 버그가 발생할 수 있습니다. "현재 체력 5인데 회복 물약으로 3 회복하면 8이 되어버리네?" 이런 상황을 막으려면 최대 체력이 얼마인지 알아야 합니다.
또한 보스전에서 체력 버프를 받을 때도 기준점이 필요합니다. 바로 이런 문제를 해결하기 위해 maxHealth와 currentHealth를 분리합니다.
위의 코드를 살펴보면, Player 클래스 안에서 세 가지 핵심 변수를 초기화합니다. maxHealth는 최대 체력의 기준점이 됩니다.
currentHealth는 현재 상태를 나타내며, 처음에는 최대 체력으로 시작합니다. isAlive는 캐릭터의 생존 여부를 명확하게 표시합니다.
getHealthPercentage 메서드는 현재 체력을 백분율로 반환합니다. 이 값은 나중에 체력바 UI를 만들 때 매우 유용하게 사용됩니다.
0.5라면 체력이 절반 남은 것이고, 1.0이라면 풀피 상태입니다. 실제 현업에서는 이 구조를 더 확장하기도 합니다.
예를 들어 RPG 게임에서는 레벨업할 때마다 maxHealth를 증가시키고, 현재 체력도 비율에 맞게 조정합니다. 액션 게임에서는 특정 스킬 사용 시 일시적으로 최대 체력이 늘어나는 버프를 구현할 수도 있습니다.
주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 currentHealth를 직접 수정하는 것입니다.
"this.currentHealth = 5"처럼 아무 데서나 직접 값을 바꾸면, 나중에 체력 변화를 추적하기 어려워집니다. 가능하면 전용 메서드를 통해서만 체력을 변경하는 습관을 들이세요.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 변수를 나눠서 관리하는 거군요!" 이제 체력 시스템의 기초가 탄탄하게 잡혔습니다.
실전 팁
💡 - 체력 변수는 항상 private하게 관리하고, getter/setter 메서드로 접근하세요
- 최대 체력 변경 시 현재 체력도 비율에 맞게 조정할지 결정해두세요
2. 데미지 처리
김개발 씨가 체력 변수를 만들었습니다. 이제 적에게 맞으면 체력이 줄어들게 해야 합니다.
"그냥 체력에서 빼면 되는 거 아닌가?" 하지만 막상 구현하려니 고려할 게 많습니다. 체력이 마이너스가 되면 어쩌지?
방어력은 어떻게 적용하지?
데미지 처리는 캐릭터가 피해를 입었을 때 체력을 감소시키고 그에 따른 후속 처리를 하는 것입니다. 마치 은행에서 출금할 때 잔액 확인, 출금 처리, 영수증 발급이 순서대로 일어나는 것처럼, 데미지 처리도 여러 단계를 거쳐야 합니다.
제대로 구현하면 방어력, 속성 저항 등 복잡한 전투 시스템의 기반이 됩니다.
다음 코드를 살펴봅시다.
// Player 클래스 내부 메서드
takeDamage(amount) {
if (!this.isAlive) return; // 이미 죽었으면 무시
// 실제 데미지 계산 (방어력 적용 가능)
const actualDamage = Math.max(1, amount - this.defense);
// 체력 감소 (0 미만으로 내려가지 않게)
this.currentHealth = Math.max(0, this.currentHealth - actualDamage);
// 피격 이벤트 발생
this.scene.events.emit('playerDamaged', this.currentHealth, this.maxHealth);
// 사망 체크
if (this.currentHealth <= 0) {
this.die();
}
}
김개발 씨가 게임을 테스트하다가 이상한 현상을 발견했습니다. 적에게 여러 번 맞았더니 체력이 -5가 되어 있었습니다.
화면에는 죽은 캐릭터가 여전히 돌아다니고 있었습니다. "어, 이게 뭐지?" 박시니어 씨가 코드를 보더니 한숨을 쉬었습니다.
"체력을 그냥 빼기만 하면 안 돼요. 데미지 처리는 생각보다 복잡한 과정이에요." 그렇다면 데미지 처리란 정확히 어떤 과정일까요?
쉽게 비유하자면, 데미지 처리는 마치 병원 응급실의 처치 과정과 같습니다. 환자가 들어오면 먼저 상태를 확인하고, 치료를 진행하고, 기록을 남기고, 위험한 상태인지 판단합니다.
그냥 무턱대고 치료부터 하지 않습니다. 게임에서도 마찬가지입니다.
단순히 "체력 -= 데미지"만 하면 어떤 문제가 생길까요? 첫째, 이미 죽은 캐릭터가 또 데미지를 받을 수 있습니다.
죽는 애니메이션 중에 적의 공격이 또 들어오면 죽음 처리가 여러 번 실행될 수 있습니다. 둘째, 체력이 음수가 됩니다.
-10 같은 값이 체력바에 표시되면 플레이어는 당황합니다. 셋째, 다른 시스템이 체력 변화를 알 수 없습니다.
UI 업데이트, 사운드 재생 등이 제때 일어나지 않습니다. 바로 이런 문제들을 해결하기 위해 체계적인 takeDamage 메서드를 만듭니다.
위의 코드를 단계별로 살펴보겠습니다. 먼저 isAlive 체크로 이미 죽은 캐릭터는 데미지 처리를 건너뜁니다.
이것이 첫 번째 방어선입니다. 다음으로 실제 데미지를 계산합니다.
여기서 방어력을 적용할 수 있습니다. **Math.max(1, amount - this.defense)**는 최소 1의 데미지는 항상 들어가게 합니다.
방어력이 아무리 높아도 무적은 아닌 거죠. 체력 감소 시 **Math.max(0, ...)**를 사용해서 음수가 되지 않도록 합니다.
이 한 줄이 많은 버그를 예방합니다. events.emit으로 피격 이벤트를 발생시킵니다.
이 이벤트를 구독하는 다른 시스템들, 예를 들어 UI, 사운드, 카메라 흔들림 등이 각자 반응할 수 있습니다. 이것이 바로 이벤트 기반 설계의 장점입니다.
마지막으로 체력이 0 이하가 되면 die 메서드를 호출합니다. 사망 처리는 별도 메서드로 분리하는 것이 좋습니다.
실제 현업에서는 이 구조를 더 확장합니다. 속성 데미지(불, 얼음, 독 등), 크리티컬 히트, 데미지 감소 버프, 쉴드 시스템 등을 추가할 수 있습니다.
기본 구조가 탄탄하면 이런 확장이 쉬워집니다. 주의할 점은 데미지 처리 중간에 다른 데미지가 들어오는 상황입니다.
무적 시간 없이 여러 적에게 동시에 맞으면 순식간에 죽을 수 있습니다. 이 문제는 다음 장에서 다루겠습니다.
김개발 씨는 코드를 수정한 후 다시 테스트했습니다. 이제 체력이 깔끔하게 0에서 멈추고, 화면의 하트도 정확하게 줄어들었습니다.
"이제 좀 게임다워졌네!"
실전 팁
💡 - 데미지 처리는 반드시 메서드로 캡슐화하세요
- 이벤트를 발생시켜서 UI와 로직을 분리하세요
3. 무적 시간 구현
김개발 씨가 게임을 테스트하는데 적 무리 속에 들어가니 순식간에 죽어버렸습니다. 체력 3칸이 0.1초 만에 사라진 겁니다.
"이건 너무 불공평한데?" 고전 게임들을 떠올려보니, 피격당하면 캐릭터가 깜빡이면서 잠시 무적이 되었던 기억이 났습니다.
무적 시간은 캐릭터가 피격당한 후 일정 시간 동안 추가 데미지를 받지 않는 시스템입니다. 마치 복싱에서 다운된 선수에게 10카운트를 세어주는 것처럼, 플레이어에게 회복할 기회를 주는 장치입니다.
이 시스템이 없으면 연속 피격으로 순식간에 게임 오버가 될 수 있습니다.
다음 코드를 살펴봅시다.
// Player 클래스 내부
constructor(scene, x, y) {
// ... 기존 코드 ...
this.isInvincible = false; // 무적 상태
this.invincibleDuration = 1500; // 무적 지속 시간 (ms)
}
takeDamage(amount) {
if (!this.isAlive || this.isInvincible) return;
// 데미지 처리 후 무적 시작
this.currentHealth = Math.max(0, this.currentHealth - amount);
this.startInvincibility();
if (this.currentHealth <= 0) this.die();
}
startInvincibility() {
this.isInvincible = true;
this.blinkEffect(); // 깜빡임 효과
this.scene.time.delayedCall(this.invincibleDuration, () => {
this.isInvincible = false;
this.alpha = 1; // 투명도 복원
});
}
김개발 씨는 어린 시절 즐겨했던 슈퍼마리오를 떠올렸습니다. 마리오가 적에게 맞으면 깜빡이면서 잠시 무적이 되었습니다.
그때는 당연하게 여겼는데, 직접 만들어보니 이게 정말 중요한 시스템이었습니다. 박시니어 씨가 설명합니다.
"무적 시간은 단순히 편의를 위한 게 아니에요. 게임의 공정성을 위한 핵심 장치예요." 무적 시간이란 정확히 무엇일까요?
쉽게 비유하자면, 무적 시간은 마치 교통사고 후 보험 처리 기간과 같습니다. 사고가 나면 일정 기간 동안 다른 절차 없이 치료에 집중할 수 있도록 보호받습니다.
그 기간 동안 또 다른 사고가 나더라도 이전 사고의 처리가 우선됩니다. 게임에서도 피격 후 회복할 시간을 보장하는 것입니다.
무적 시간이 없으면 어떤 문제가 생길까요? 가장 큰 문제는 연속 피격입니다.
적 여러 마리 사이에 끼이면 매 프레임마다 데미지를 받습니다. 1초에 60프레임이면 60번의 데미지입니다.
체력 100이어도 순식간에 0이 됩니다. 플레이어는 "뭐야, 손 쓸 틈도 없이 죽었잖아!"라고 불만을 토로하겠죠.
바로 이런 문제를 해결하기 위해 무적 시간 시스템을 도입합니다. 위의 코드를 살펴보면, isInvincible 플래그가 핵심입니다.
이 값이 true면 takeDamage 메서드 첫 줄에서 바로 리턴됩니다. 어떤 데미지도 통과하지 못합니다.
invincibleDuration은 무적 지속 시간입니다. 1500밀리초, 즉 1.5초로 설정했습니다.
이 값은 게임 장르와 난이도에 따라 조절합니다. 액션 게임은 짧게, 플랫포머는 길게 설정하는 경향이 있습니다.
startInvincibility 메서드에서 무적을 시작하고, delayedCall로 일정 시간 후 무적을 해제합니다. Phaser의 time.delayedCall은 setTimeout보다 게임에 최적화되어 있습니다.
게임이 일시정지되면 타이머도 함께 멈추기 때문입니다. blinkEffect는 깜빡임 효과입니다.
플레이어에게 "지금 무적이야"라는 신호를 보내는 것입니다. 시각적 피드백 없이 무적 시간만 있으면 플레이어는 자신의 상태를 알 수 없습니다.
실제 게임에서는 무적 시간 동안 다양한 효과를 넣습니다. 캐릭터 깜빡임, 반투명 처리, 무지개빛 효과, 실드 이펙트 등이 있습니다.
어떤 게임은 무적 시간 동안 적을 통과할 수 있게 하기도 합니다. 주의할 점은 무적 시간의 길이입니다.
너무 짧으면 의미가 없고, 너무 길면 게임이 쉬워집니다. 보통 1-2초가 적당합니다.
또한 보스의 특수 공격은 무적을 무시하게 만들기도 합니다. 이런 예외 처리도 고려해야 합니다.
김개발 씨가 무적 시간을 구현한 후 다시 테스트했습니다. 이제 적 무리 속에서도 깜빡이면서 빠져나올 수 있게 되었습니다.
"이제야 공정한 게임이 됐어!"
실전 팁
💡 - 무적 시간 동안 반드시 시각적 피드백을 제공하세요
- delayedCall 사용 시 씬 전환에 따른 타이머 정리를 잊지 마세요
4. 하트 UI 표시
김개발 씨가 내부 로직은 완성했는데, 화면에는 체력이 전혀 보이지 않았습니다. 플레이어가 자기 체력이 얼마나 남았는지 어떻게 알 수 있을까요?
"하트로 표시하면 직관적이겠다!" 젤다처럼 하트 아이콘으로 체력을 보여주기로 했습니다.
하트 UI는 플레이어의 현재 체력을 시각적으로 표시하는 인터페이스입니다. 마치 자동차 계기판의 연료 게이지처럼, 플레이어가 한눈에 자신의 상태를 파악할 수 있게 해줍니다.
숫자보다 아이콘이 직관적이고, 게임의 분위기에도 잘 어울립니다.
다음 코드를 살펴봅시다.
class HealthUI {
constructor(scene, player) {
this.scene = scene;
this.player = player;
this.hearts = [];
this.createHearts();
// 데미지 이벤트 구독
scene.events.on('playerDamaged', this.updateHearts, this);
}
createHearts() {
for (let i = 0; i < this.player.maxHealth; i++) {
const heart = this.scene.add.image(30 + i * 40, 30, 'heart-full');
heart.setScrollFactor(0); // UI는 화면에 고정
this.hearts.push(heart);
}
}
updateHearts(currentHealth) {
this.hearts.forEach((heart, index) => {
heart.setTexture(index < currentHealth ? 'heart-full' : 'heart-empty');
});
}
}
김개발 씨는 체력을 어떻게 표시할지 고민했습니다. 숫자로 "HP: 3/3" 이렇게 표시할까?
아니면 바 형태로? 고전 게임들을 찾아보니 하트 아이콘이 가장 직관적이었습니다.
특히 캐주얼 게임에서는 하트가 정석입니다. 박시니어 씨가 조언합니다.
"UI는 로직과 분리해서 만드는 게 좋아요. 이벤트 기반으로 연결하면 나중에 UI만 바꾸기도 쉽고요." 하트 UI란 정확히 어떻게 구현하는 걸까요?
쉽게 비유하자면, 하트 UI는 마치 신호등과 같습니다. 신호등은 내부의 교통 제어 시스템과 분리되어 있습니다.
제어 시스템이 "빨간불"이라고 신호를 보내면 신호등은 그에 맞게 표시만 합니다. 하트 UI도 마찬가지입니다.
체력 시스템이 "체력 변경됨"이라고 신호를 보내면 UI는 그에 맞게 하트를 업데이트합니다. UI를 왜 별도 클래스로 분리할까요?
첫째, 관심사의 분리입니다. Player 클래스는 게임 로직에만 집중하고, HealthUI 클래스는 화면 표시에만 집중합니다.
둘째, 유지보수가 쉬워집니다. 나중에 하트 대신 체력바로 바꾸고 싶다면 HealthUI만 수정하면 됩니다.
Player 클래스는 건드릴 필요가 없습니다. 위의 코드를 살펴보면, createHearts 메서드에서 최대 체력만큼 하트를 생성합니다.
각 하트의 x 좌표를 30 + i * 40으로 설정해서 일정한 간격으로 배치합니다. **setScrollFactor(0)**이 중요합니다.
이 설정이 없으면 캐릭터가 움직일 때 하트도 함께 스크롤됩니다. 0으로 설정하면 화면에 고정되어 항상 같은 위치에 보입니다.
게임 UI의 필수 설정입니다. **events.on('playerDamaged', ...)**로 이벤트를 구독합니다.
Player의 takeDamage에서 이벤트를 발생시키면 이 콜백이 호출됩니다. 이것이 바로 이벤트 기반 설계의 힘입니다.
Player는 UI의 존재를 몰라도 됩니다. updateHearts 메서드는 현재 체력에 따라 하트 텍스처를 변경합니다.
index가 currentHealth보다 작으면 채워진 하트, 크거나 같으면 빈 하트를 표시합니다. 간단하지만 효과적인 로직입니다.
실제 게임에서는 더 다양한 표현을 사용합니다. 반 칸짜리 하트, 하트가 깨지는 애니메이션, 체력 회복 시 반짝이는 효과 등을 추가하기도 합니다.
기본 구조가 잡혀 있으면 이런 확장은 어렵지 않습니다. 주의할 점은 씬 전환 시 이벤트 리스너 정리입니다.
events.on으로 등록한 리스너는 씬이 종료될 때 events.off로 해제해야 합니다. 그렇지 않으면 메모리 누수가 발생합니다.
김개발 씨는 화면 왼쪽 상단에 예쁜 하트 세 개가 표시되는 것을 보고 뿌듯해했습니다. 적에게 맞으니 하트가 하나씩 비워졌습니다.
"이제 진짜 게임 같아!"
실전 팁
💡 - UI 요소는 반드시 setScrollFactor(0)으로 화면에 고정하세요
- 씬 종료 시 이벤트 리스너를 정리하는 습관을 들이세요
5. 게임 오버 처리
김개발 씨가 테스트하다가 체력이 0이 되었습니다. 캐릭터가 멈췄는데...
그 다음은? 화면이 그대로였습니다.
"게임 오버 화면이 없네!" 플레이어가 죽었을 때 어떤 피드백을 주고 어떻게 처리해야 할까요?
게임 오버 처리는 플레이어가 패배했을 때의 일련의 과정을 담당합니다. 마치 영화의 엔딩 크레딧처럼, 게임에도 적절한 마무리가 필요합니다.
사망 애니메이션, 게임 오버 화면 표시, 재시작 옵션 제공까지 플레이어 경험을 완성하는 중요한 요소입니다.
다음 코드를 살펴봅시다.
// Player 클래스 내부
die() {
if (!this.isAlive) return; // 중복 호출 방지
this.isAlive = false;
this.body.enable = false; // 물리 비활성화
// 사망 애니메이션
this.scene.tweens.add({
targets: this,
alpha: 0,
y: this.y - 50,
duration: 800,
ease: 'Power2',
onComplete: () => {
this.scene.events.emit('gameOver');
}
});
}
// GameScene에서
this.events.on('gameOver', () => {
this.time.delayedCall(1000, () => {
this.scene.start('GameOverScene', { score: this.score });
});
});
김개발 씨는 캐릭터가 죽으면 바로 게임 오버 화면으로 가야 한다고 생각했습니다. 하지만 막상 그렇게 만드니 너무 갑작스러웠습니다.
이전에 즐겼던 게임들은 어땠지? 캐릭터가 쓰러지는 애니메이션이 있고, 잠시 후 게임 오버 화면이 나왔던 것 같습니다.
박시니어 씨가 설명합니다. "게임 오버도 연출이에요.
플레이어에게 '아, 죽었구나'라고 느낄 시간을 줘야 해요." 게임 오버 처리란 정확히 어떤 과정일까요? 쉽게 비유하자면, 게임 오버는 마치 연극의 막이 내리는 장면과 같습니다.
연극이 끝나면 배우들이 무대에서 갑자기 사라지지 않습니다. 마지막 대사, 조명 페이드아웃, 막이 천천히 내려오는 과정이 있습니다.
관객은 이 시간 동안 감정을 정리합니다. 게임도 마찬가지입니다.
바로 게임 오버 화면으로 전환하면 어떤 문제가 있을까요? 첫째, 플레이어가 자신이 왜 죽었는지 인지하지 못합니다.
화면이 갑자기 바뀌면 "어? 뭐야?" 하는 느낌입니다.
둘째, 감정적 몰입이 끊깁니다. 게임에 빠져 있다가 갑자기 현실로 돌아온 느낌이죠.
셋째, 기술적으로도 문제가 생길 수 있습니다. 진행 중인 애니메이션이나 타이머가 제대로 정리되지 않을 수 있습니다.
위의 코드를 살펴보면, die 메서드 첫 줄에서 isAlive 체크를 합니다. 여러 적에게 동시에 마지막 타격을 맞으면 die가 여러 번 호출될 수 있는데, 이를 방지합니다.
body.enable = false로 물리 엔진을 비활성화합니다. 죽은 캐릭터가 계속 적과 충돌하면 이상해지니까요.
tweens.add로 사망 애니메이션을 만듭니다. 캐릭터가 투명해지면서 위로 살짝 떠오르는 효과입니다.
duration 800은 0.8초 동안 진행된다는 뜻입니다. 이 짧은 연출이 플레이어 경험을 크게 향상시킵니다.
애니메이션이 끝나면 onComplete 콜백에서 gameOver 이벤트를 발생시킵니다. GameScene에서 이 이벤트를 받아서 1초 후 GameOverScene으로 전환합니다.
scene.start의 두 번째 인자로 데이터를 전달합니다. 점수, 플레이 시간, 도달한 스테이지 등을 게임 오버 화면에서 표시할 수 있습니다.
실제 게임에서는 더 화려한 연출을 넣습니다. 화면 흔들림, 슬로우 모션, 폭발 이펙트, 비장한 음악 등이 있습니다.
하지만 기본 구조는 동일합니다. 사망 확정 - 애니메이션 - 이벤트 - 씬 전환의 흐름입니다.
주의할 점은 게임 오버 처리 중에 다른 입력을 막는 것입니다. 죽는 도중에 점프 키를 누르면 이상한 동작이 될 수 있습니다.
입력 비활성화도 함께 처리하세요. 김개발 씨가 게임 오버 연출을 추가한 후, 죽을 때마다 캐릭터가 서서히 사라지고 게임 오버 화면이 나타났습니다.
"이제 죽어도 억울하지 않네!"
실전 팁
💡 - 게임 오버 전환에 1-2초의 여유를 두세요
- 사망 중에는 모든 입력을 비활성화하세요
6. 리스폰 시스템
김개발 씨가 게임 오버 화면에서 "다시 시작" 버튼을 눌렀습니다. 그런데 캐릭터가 죽은 자리에서 그대로 시작되었습니다.
체력은 0인 채로요. "이건 아닌데..." 제대로 된 리스폰 시스템이 필요했습니다.
리스폰 시스템은 캐릭터를 초기 상태로 되돌려서 게임을 다시 시작할 수 있게 하는 것입니다. 마치 체스 게임을 다시 시작할 때 모든 말을 원래 위치에 놓는 것처럼, 게임의 모든 요소를 리셋해야 합니다.
체력, 위치, 점수, 적 배치 등을 고려해야 합니다.
다음 코드를 살펴봅시다.
// Player 클래스 내부
respawn(x, y) {
// 상태 초기화
this.currentHealth = this.maxHealth;
this.isAlive = true;
this.isInvincible = false;
// 위치 초기화
this.setPosition(x, y);
this.body.enable = true;
// 시각 효과 초기화
this.alpha = 1;
this.setVisible(true);
// 리스폰 무적 (3초)
this.startInvincibility();
// 이벤트 발생
this.scene.events.emit('playerRespawned');
}
// GameScene에서 재시작 처리
restartGame() {
this.score = 0;
this.player.respawn(100, 300); // 시작 위치
this.enemies.clear(true, true); // 적 초기화
this.spawnEnemies();
}
김개발 씨는 리스폰이 단순히 체력만 채우면 되는 줄 알았습니다. 하지만 테스트해보니 문제가 한두 가지가 아니었습니다.
죽은 자리에서 다시 시작되니 바로 적에게 맞았고, 무적 상태가 유지되어 있기도 했고, 투명도가 그대로여서 반투명한 캐릭터가 되기도 했습니다. 박시니어 씨가 말합니다.
"리스폰은 완전한 초기화예요. 캐릭터의 모든 상태를 처음 상태로 되돌려야 해요." 리스폰 시스템이란 정확히 어떤 것일까요?
쉽게 비유하자면, 리스폰은 마치 컴퓨터 재부팅과 같습니다. 재부팅하면 메모리에 있던 모든 임시 데이터가 지워지고, 시스템이 처음 상태로 돌아갑니다.
실행 중이던 프로그램, 열려 있던 파일, 임시 설정 모두 초기화됩니다. 게임 캐릭터 리스폰도 마찬가지입니다.
모든 상태 변수를 초기값으로 되돌려야 합니다. 제대로 된 리스폰 없이 단순히 씬을 다시 시작하면 어떤 문제가 있을까요?
씬 전체를 다시 시작하는 것은 무겁습니다. 모든 게임 오브젝트를 파괴하고 다시 생성해야 합니다.
로딩 시간이 필요할 수도 있습니다. 또한 연속성이 끊깁니다.
같은 스테이지 내에서 죽었다 살아나는 느낌이 아니라, 게임을 껐다 켠 느낌이 됩니다. 위의 코드를 살펴보면, respawn 메서드에서 모든 상태를 하나씩 초기화합니다.
이것이 핵심입니다. 빠뜨리는 게 하나라도 있으면 버그가 됩니다.
currentHealth = maxHealth로 체력을 풀로 채웁니다. isAlive = true로 생존 상태를 복원합니다.
isInvincible = false로 무적 상태를 해제합니다. **setPosition(x, y)**로 시작 위치로 이동시킵니다.
죽은 자리가 아닌, 안전한 시작 지점으로 보내야 합니다. body.enable = true로 물리 엔진을 다시 활성화합니다.
alpha = 1과 **setVisible(true)**로 시각적 상태를 복원합니다. 사망 애니메이션으로 투명해졌던 캐릭터를 다시 보이게 합니다.
중요한 것이 startInvincibility 호출입니다. 리스폰 직후에도 무적 시간을 줍니다.
시작 위치에 적이 있을 수도 있으니까요. 이 무적 시간 동안 플레이어는 안전하게 상황을 파악하고 움직일 수 있습니다.
restartGame 메서드에서는 플레이어만 리스폰하는 게 아니라, 점수와 적들도 함께 초기화합니다. 게임 전체의 상태를 관리해야 합니다.
실제 게임에서는 리스폰 유형이 다양합니다. 체크포인트에서 리스폰, 목숨 시스템과 연동, 페널티(점수 감소, 아이템 드랍) 등이 있습니다.
기본 구조가 잘 되어 있으면 이런 확장은 쉽습니다. 주의할 점은 리스폰 이벤트를 발생시키는 것입니다.
UI나 다른 시스템도 리스폰 사실을 알아야 합니다. 하트 UI는 다시 채워져야 하고, 음악이 다시 시작될 수도 있습니다.
김개발 씨는 모든 시스템을 연결한 후 완전한 게임 루프를 완성했습니다. 시작 - 플레이 - 피격 - 사망 - 게임 오버 - 재시작이 매끄럽게 이어졌습니다.
"드디어 완성이다!"
실전 팁
💡 - 리스폰 시 초기화해야 할 모든 상태를 체크리스트로 관리하세요
- 리스폰 후에도 무적 시간을 제공해서 즉사를 방지하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
CloudFront CDN 완벽 가이드
AWS CloudFront를 활용한 콘텐츠 배포 최적화 방법을 실무 관점에서 다룹니다. 배포 생성부터 캐시 설정, HTTPS 적용까지 단계별로 알아봅니다.