본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 5. · 11 Views
Phaser 메뉴와 게임 UI 완벽 가이드
게임 개발에서 가장 중요한 사용자 인터페이스 요소들을 다룹니다. 타이틀 화면부터 게임 오버 화면까지, 플레이어가 게임과 상호작용하는 모든 UI를 Phaser로 구현하는 방법을 배웁니다.
목차
1. 타이틀 Scene 만들기
김개발 씨가 드디어 첫 번째 게임을 완성했습니다. 그런데 게임을 실행하자마자 바로 플레이가 시작되어 버립니다.
"어, 타이틀 화면이 없네?" 선배 박시니어 씨가 웃으며 말했습니다. "게임의 첫인상은 타이틀 화면에서 결정돼요."
타이틀 Scene은 게임의 얼굴과도 같습니다. 마치 책의 표지처럼, 플레이어가 게임을 시작할 때 가장 먼저 만나는 화면입니다.
이곳에서 게임의 분위기를 전달하고, 플레이어가 게임을 시작할 준비를 할 수 있도록 안내합니다.
다음 코드를 살펴봅시다.
class TitleScene extends Phaser.Scene {
constructor() {
super({ key: 'TitleScene' });
}
create() {
// 배경 이미지 추가
this.add.image(400, 300, 'title-bg');
// 게임 타이틀 텍스트
this.add.text(400, 200, 'My First Game', {
fontSize: '64px',
fontFamily: 'Arial',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 6
}).setOrigin(0.5);
// 시작 안내 텍스트 (깜빡임 효과)
const startText = this.add.text(400, 400, 'Press SPACE to Start', {
fontSize: '32px',
color: '#ffff00'
}).setOrigin(0.5);
// 깜빡임 애니메이션
this.tweens.add({
targets: startText,
alpha: 0,
duration: 500,
yoyo: true,
repeat: -1
});
// 스페이스바 입력 감지
this.input.keyboard.once('keydown-SPACE', () => {
this.scene.start('GameScene');
});
}
}
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 회사에서 처음으로 작은 웹 게임 프로젝트를 맡게 되었습니다.
열심히 게임 로직을 구현하고 테스트를 해보는데, 뭔가 허전합니다. 게임을 실행하면 바로 캐릭터가 움직이기 시작하는 것이 어색했습니다.
선배 개발자 박시니어 씨가 옆에서 지켜보다가 한마디 합니다. "게임에 타이틀 화면이 없으면 플레이어가 준비할 틈이 없어요.
마치 영화가 예고편 없이 바로 시작하는 것과 같죠." 그렇다면 타이틀 Scene이란 정확히 무엇일까요? 쉽게 비유하자면, 타이틀 Scene은 마치 레스토랑의 입구와 같습니다.
손님이 들어오면 바로 테이블에 앉히는 것이 아니라, 먼저 환영 인사를 하고 메뉴판을 건네며 준비할 시간을 줍니다. 게임도 마찬가지입니다.
플레이어에게 게임의 분위기를 전달하고, 마음의 준비를 할 시간을 제공하는 것이 타이틀 화면의 역할입니다. Phaser에서 Scene은 게임의 각 화면을 담당하는 독립적인 단위입니다.
타이틀 화면, 게임 플레이 화면, 게임 오버 화면 등을 각각 별도의 Scene으로 만들어 관리합니다. 이렇게 하면 코드가 깔끔하게 정리되고, 각 화면의 역할이 명확해집니다.
위의 코드를 살펴보겠습니다. 먼저 **super({ key: 'TitleScene' })**에서 Scene의 고유 이름을 지정합니다.
이 이름은 나중에 다른 Scene에서 이 Scene을 호출할 때 사용됩니다. create 메서드는 Scene이 시작될 때 한 번 실행되며, 여기서 화면에 표시할 요소들을 배치합니다.
this.add.text를 사용하여 게임 타이틀을 화면에 표시합니다. setOrigin(0.5)는 텍스트의 기준점을 중앙으로 설정하여, 지정한 좌표가 텍스트의 정중앙이 되도록 합니다.
stroke와 strokeThickness 옵션으로 텍스트에 외곽선을 추가하면 배경과 구분이 잘 되어 가독성이 높아집니다. 시작 안내 텍스트에는 Tween 애니메이션을 적용했습니다.
Tween은 Phaser에서 부드러운 애니메이션을 만드는 도구입니다. alpha 속성을 0으로 변경하면 투명해지고, yoyo: true 옵션으로 왔다 갔다 반복하며, repeat: -1은 무한 반복을 의미합니다.
이렇게 하면 "Press SPACE to Start" 텍스트가 계속 깜빡이며 플레이어의 주의를 끕니다. 마지막으로 this.input.keyboard.once를 사용하여 스페이스바 입력을 감지합니다.
once를 사용하면 한 번만 실행되므로, 실수로 여러 번 눌러도 중복 실행되지 않습니다. 입력이 감지되면 **this.scene.start('GameScene')**으로 실제 게임 화면으로 전환합니다.
실제 현업에서는 타이틀 화면에 더 많은 요소를 추가합니다. 배경 음악을 재생하고, 로고 애니메이션을 넣고, 여러 메뉴 버튼을 배치합니다.
하지만 기본 구조는 동일합니다. 중요한 것은 플레이어가 게임의 분위기를 느끼고, 자연스럽게 게임을 시작할 수 있도록 안내하는 것입니다.
주의할 점도 있습니다. 타이틀 화면에서 너무 많은 리소스를 한꺼번에 로드하면 로딩 시간이 길어집니다.
무거운 리소스는 별도의 Preload Scene에서 미리 로드하고, 타이틀 화면에서는 필요한 것만 표시하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언을 듣고 타이틀 화면을 추가한 김개발 씨는 만족스러운 미소를 지었습니다. 이제 게임이 한결 완성도 있어 보입니다.
실전 팁
💡 - 타이틀 화면의 배경 음악은 create 메서드에서 시작하고, Scene 전환 시 페이드아웃하면 자연스럽습니다
- setOrigin(0.5)를 활용하면 중앙 정렬이 쉬워집니다
2. 메뉴 버튼 구현
타이틀 화면을 만든 김개발 씨에게 새로운 과제가 주어졌습니다. "게임 시작만 있으면 안 되지.
설정, 크레딧 같은 메뉴도 필요해요." 박시니어 씨의 말에 김개발 씨는 고민에 빠졌습니다. 클릭할 수 있는 버튼은 어떻게 만들까요?
메뉴 버튼은 플레이어가 게임과 상호작용하는 가장 기본적인 UI 요소입니다. 마치 엘리베이터의 층수 버튼처럼, 누르면 원하는 곳으로 이동할 수 있게 해줍니다.
Phaser에서는 이미지나 텍스트에 인터랙티브 속성을 부여하여 버튼으로 활용합니다.
다음 코드를 살펴봅시다.
class MenuScene extends Phaser.Scene {
create() {
// 버튼 생성 함수
this.createButton(400, 250, 'Start Game', () => {
this.scene.start('GameScene');
});
this.createButton(400, 330, 'Settings', () => {
this.scene.start('SettingsScene');
});
this.createButton(400, 410, 'Credits', () => {
this.scene.start('CreditsScene');
});
}
createButton(x, y, text, callback) {
// 버튼 배경 생성
const button = this.add.rectangle(x, y, 200, 60, 0x4a4a4a)
.setInteractive({ useHandCursor: true });
// 버튼 텍스트
this.add.text(x, y, text, {
fontSize: '24px',
color: '#ffffff'
}).setOrigin(0.5);
// 마우스 오버 효과
button.on('pointerover', () => {
button.setFillStyle(0x6a6a6a);
});
button.on('pointerout', () => {
button.setFillStyle(0x4a4a4a);
});
// 클릭 이벤트
button.on('pointerdown', callback);
return button;
}
}
김개발 씨는 웹 개발 경험이 있어서 HTML 버튼을 떠올렸습니다. 하지만 Phaser는 캔버스 기반이라 HTML 요소를 직접 사용할 수 없습니다.
그렇다면 게임 내에서 버튼을 어떻게 만들 수 있을까요? 박시니어 씨가 힌트를 줍니다.
"Phaser에서는 어떤 게임 오브젝트든 setInteractive를 호출하면 클릭할 수 있게 돼요. 이미지든, 도형이든, 텍스트든 상관없어요." setInteractive는 마치 마법의 주문과 같습니다.
이 메서드를 호출하면 평범한 게임 오브젝트가 사용자 입력에 반응하는 인터랙티브 요소로 변신합니다. useHandCursor: true 옵션을 추가하면 마우스를 올렸을 때 손가락 모양 커서로 바뀌어 클릭할 수 있다는 것을 시각적으로 알려줍니다.
위 코드에서는 createButton이라는 재사용 가능한 함수를 만들었습니다. 같은 스타일의 버튼을 여러 개 만들어야 할 때, 매번 같은 코드를 반복하는 것은 비효율적입니다.
함수로 만들어 두면 버튼의 위치, 텍스트, 클릭 시 동작만 인자로 전달하면 됩니다. 버튼의 시각적 피드백도 중요합니다.
pointerover 이벤트는 마우스가 버튼 위에 올라왔을 때 발생하고, pointerout은 마우스가 버튼을 벗어났을 때 발생합니다. 이 이벤트들을 활용하여 버튼 색상을 변경하면 플레이어는 자신의 행동에 대한 피드백을 받을 수 있습니다.
pointerdown 이벤트는 실제 클릭이 발생했을 때 실행됩니다. 여기에 콜백 함수를 연결하면 버튼을 클릭했을 때 원하는 동작을 수행할 수 있습니다.
위 예제에서는 각 버튼이 해당하는 Scene으로 전환하도록 설정했습니다. 실제 게임에서는 버튼에 이미지를 사용하는 경우가 많습니다.
this.add.image로 버튼 이미지를 추가하고 setInteractive를 호출하면 됩니다. 호버 상태와 클릭 상태의 이미지를 별도로 준비해서 setTexture로 교체하면 더욱 풍성한 피드백을 제공할 수 있습니다.
주의할 점이 있습니다. 버튼이 겹쳐 있을 때는 위에 있는 버튼만 클릭됩니다.
Phaser는 기본적으로 나중에 추가된 오브젝트가 위에 표시되므로, 버튼의 추가 순서에 주의해야 합니다. setDepth 메서드로 표시 순서를 명시적으로 지정할 수도 있습니다.
김개발 씨는 createButton 함수를 만들어 두니 버튼 추가가 한결 수월해졌습니다. 새로운 메뉴가 필요할 때마다 한 줄만 추가하면 되니까요.
코드의 재사용성을 높이는 것이 좋은 프로그래밍 습관이라는 것을 다시 한번 깨달았습니다.
실전 팁
💡 - 버튼 클릭 시 효과음을 추가하면 더욱 만족스러운 피드백을 제공합니다
- 버튼 스타일을 클래스로 분리하면 프로젝트 전체에서 일관된 UI를 유지할 수 있습니다
3. 일시정지 기능
게임 테스트 중에 전화가 왔습니다. 김개발 씨는 급하게 전화를 받느라 게임 캐릭터가 죽는 것을 지켜볼 수밖에 없었습니다.
"일시정지 기능이 있었으면..." 이때 박시니어 씨가 말했습니다. "모든 게임에는 일시정지가 필수예요."
일시정지 기능은 게임의 시간을 멈추는 것입니다. 마치 리모컨의 일시정지 버튼처럼, 플레이어가 잠시 자리를 비우거나 메뉴를 확인할 때 게임 상태를 그대로 유지합니다.
Phaser에서는 Scene의 pause와 resume 메서드로 이를 구현합니다.
다음 코드를 살펴봅시다.
class GameScene extends Phaser.Scene {
create() {
// ESC 키로 일시정지 토글
this.input.keyboard.on('keydown-ESC', () => {
this.togglePause();
});
// 일시정지 UI (처음에는 숨김)
this.pauseOverlay = this.add.rectangle(400, 300, 800, 600, 0x000000, 0.7)
.setVisible(false);
this.pauseText = this.add.text(400, 250, 'PAUSED', {
fontSize: '64px',
color: '#ffffff'
}).setOrigin(0.5).setVisible(false);
this.resumeButton = this.add.text(400, 350, 'Resume', {
fontSize: '32px',
color: '#ffff00'
}).setOrigin(0.5)
.setInteractive({ useHandCursor: true })
.setVisible(false)
.on('pointerdown', () => this.togglePause());
}
togglePause() {
if (this.scene.isPaused()) {
// 게임 재개
this.scene.resume();
this.pauseOverlay.setVisible(false);
this.pauseText.setVisible(false);
this.resumeButton.setVisible(false);
} else {
// 게임 일시정지
this.scene.pause();
this.pauseOverlay.setVisible(true);
this.pauseText.setVisible(true);
this.resumeButton.setVisible(true);
}
}
}
김개발 씨는 생각했습니다. 게임을 일시정지한다는 것은 무엇일까요?
단순히 화면을 멈추는 것일까요, 아니면 그 이상의 무언가가 필요할까요? 박시니어 씨가 설명합니다.
"일시정지는 게임의 모든 업데이트 로직을 멈추는 거예요. 캐릭터의 움직임, 적의 AI, 타이머 등 모든 것이 정지해야 해요.
하지만 일시정지 메뉴는 여전히 반응해야 하죠." Phaser에서 **scene.pause()**를 호출하면 해당 Scene의 update 메서드가 더 이상 실행되지 않습니다. 물리 엔진도 멈추고, 타이머도 정지합니다.
마치 시간이 멈춘 것처럼 모든 것이 그 자리에 얼어붙습니다. 여기서 중요한 점이 있습니다.
Scene이 일시정지되어도 입력 이벤트는 여전히 작동합니다. 그래서 ESC 키를 다시 누르거나 Resume 버튼을 클릭하여 게임을 재개할 수 있는 것입니다.
만약 입력까지 멈춰버린다면 플레이어는 영원히 일시정지 상태에 갇히게 됩니다. 위 코드에서는 togglePause 함수를 만들어 일시정지와 재개를 하나의 함수로 처리했습니다.
**scene.isPaused()**로 현재 상태를 확인하고, 상태에 따라 적절한 동작을 수행합니다. 이렇게 토글 패턴을 사용하면 코드가 깔끔해집니다.
일시정지 UI도 중요합니다. 반투명한 검은색 오버레이를 게임 화면 위에 덮어 게임이 멈췄다는 것을 시각적으로 표현합니다.
alpha 값 0.7은 70%의 불투명도를 의미하며, 게임 화면이 살짝 비치면서도 일시정지 상태임을 명확히 알 수 있습니다. 실제 게임에서는 일시정지 메뉴에 더 많은 옵션을 추가합니다.
설정 화면으로 이동하거나, 메인 메뉴로 돌아가거나, 게임을 저장하는 버튼 등을 배치합니다. 이때 별도의 PauseScene을 만들어 현재 Scene 위에 오버레이로 표시하는 방법도 있습니다.
한 가지 주의할 점이 있습니다. **scene.pause()**는 해당 Scene만 멈춥니다.
만약 배경 음악이 다른 Scene에서 재생되고 있다면 음악은 계속 흘러나옵니다. 게임 전체를 완전히 멈추려면 관련된 모든 Scene을 일시정지하거나, 전역 상태를 관리해야 합니다.
김개발 씨는 일시정지 기능을 추가하고 다시 테스트해봤습니다. 전화가 와도 ESC 키 하나로 게임을 멈출 수 있게 되었습니다.
작은 기능이지만 플레이어 경험에 큰 차이를 만든다는 것을 느꼈습니다.
실전 팁
💡 - 일시정지 중에는 배경 음악 볼륨을 낮추면 더 자연스럽습니다
- 모바일 게임에서는 브라우저 탭이 비활성화될 때 자동 일시정지를 구현하세요
4. 게임 오버 화면
김개발 씨의 게임에서 캐릭터가 죽으면 그냥 화면이 멈춰버립니다. 플레이어는 무엇을 해야 할지 모릅니다.
"게임 오버 화면이 없으면 플레이어가 답답해해요." 박시니어 씨의 지적에 김개발 씨는 고개를 끄덕였습니다.
게임 오버 화면은 플레이어의 도전이 끝났음을 알리고, 다음 행동을 안내합니다. 마치 경기가 끝난 후 결과를 보여주는 전광판처럼, 최종 점수를 표시하고 재도전 또는 메뉴로 돌아가는 선택지를 제공합니다.
다음 코드를 살펴봅시다.
class GameOverScene extends Phaser.Scene {
constructor() {
super({ key: 'GameOverScene' });
}
init(data) {
// 이전 Scene에서 전달받은 데이터
this.finalScore = data.score || 0;
}
create() {
// 어두운 배경
this.add.rectangle(400, 300, 800, 600, 0x000000, 0.8);
// 게임 오버 텍스트
this.add.text(400, 150, 'GAME OVER', {
fontSize: '72px',
color: '#ff0000',
fontStyle: 'bold'
}).setOrigin(0.5);
// 최종 점수
this.add.text(400, 280, `Final Score: ${this.finalScore}`, {
fontSize: '48px',
color: '#ffffff'
}).setOrigin(0.5);
// 재시작 버튼
this.createButton(400, 400, 'Try Again', () => {
this.scene.start('GameScene');
});
// 메인 메뉴 버튼
this.createButton(400, 480, 'Main Menu', () => {
this.scene.start('TitleScene');
});
}
createButton(x, y, text, callback) {
const btn = this.add.text(x, y, text, {
fontSize: '32px',
color: '#ffffff',
backgroundColor: '#333333',
padding: { x: 20, y: 10 }
}).setOrigin(0.5)
.setInteractive({ useHandCursor: true })
.on('pointerover', () => btn.setColor('#ffff00'))
.on('pointerout', () => btn.setColor('#ffffff'))
.on('pointerdown', callback);
}
}
게임에서 실패는 피할 수 없습니다. 오히려 적절한 난이도의 실패가 있어야 성공의 기쁨도 커집니다.
중요한 것은 실패 후에 플레이어를 어떻게 안내하느냐입니다. 박시니어 씨가 말합니다.
"좋은 게임 오버 화면은 플레이어에게 세 가지를 알려줘야 해요. 첫째, 게임이 끝났다는 것.
둘째, 얼마나 잘했는지. 셋째, 다음에 무엇을 할 수 있는지." 위 코드에서 주목할 부분은 init 메서드입니다.
Scene이 시작될 때 가장 먼저 호출되는 이 메서드에서 이전 Scene으로부터 데이터를 전달받습니다. 게임 Scene에서 this.scene.start('GameOverScene', { score: this.score })처럼 호출하면, 점수 데이터가 게임 오버 화면으로 전달됩니다.
데이터 전달은 Scene 간 통신의 핵심입니다. 게임 Scene에서 획득한 점수, 클리어한 스테이지, 플레이 시간 등의 정보를 게임 오버 화면에서 표시해야 합니다.
init 메서드의 data 매개변수를 통해 이런 정보를 깔끔하게 전달할 수 있습니다. this.finalScore = data.score || 0 구문에서 || 0은 방어적 프로그래밍입니다.
만약 점수 데이터가 전달되지 않았을 경우를 대비하여 기본값 0을 설정합니다. 이런 작은 습관이 예상치 못한 오류를 방지합니다.
게임 오버 화면의 시각적 디자인도 신경 써야 합니다. 빨간색 "GAME OVER" 텍스트는 실패를 직관적으로 전달합니다.
하지만 너무 부정적인 느낌만 주면 플레이어가 재도전 의욕을 잃을 수 있습니다. 적절한 유머나 격려 문구를 추가하는 것도 좋은 방법입니다.
버튼은 두 가지를 제공합니다. Try Again은 바로 게임을 다시 시작하고, Main Menu는 타이틀 화면으로 돌아갑니다.
플레이어에게 선택권을 주는 것이 중요합니다. 무조건 재시작만 가능하다면 잠시 쉬고 싶은 플레이어는 답답함을 느낄 것입니다.
실제 게임에서는 최고 점수 기록, 업적 달성 여부, SNS 공유 버튼 등을 추가합니다. 리더보드와 연동하여 다른 플레이어와 점수를 비교하는 기능도 플레이어의 재도전 의욕을 높이는 좋은 방법입니다.
김개발 씨는 게임 오버 화면을 추가한 후 직접 플레이해봤습니다. 캐릭터가 죽었을 때 점수가 표시되고, 바로 다시 도전할 수 있는 버튼이 나타났습니다.
게임의 완성도가 한층 높아진 느낌이었습니다.
실전 팁
💡 - 게임 오버 시 효과음과 함께 화면 전환 효과를 주면 더욱 드라마틱합니다
- 최고 점수 갱신 시 축하 애니메이션을 추가하면 플레이어의 성취감을 높일 수 있습니다
5. 점수 표시 HUD
게임을 플레이하는데 현재 점수가 얼마인지 알 수가 없습니다. 김개발 씨는 게임 오버 화면에서야 점수를 확인할 수 있었습니다.
"실시간으로 점수를 보여줘야 플레이어가 성취감을 느끼죠." 박시니어 씨가 HUD의 중요성을 설명합니다.
**HUD(Head-Up Display)**는 게임 화면 위에 항상 표시되는 정보 영역입니다. 마치 자동차 계기판처럼, 플레이어가 게임 중에 알아야 할 중요한 정보를 실시간으로 보여줍니다.
점수, 생명력, 타이머, 아이템 등이 여기에 표시됩니다.
다음 코드를 살펴봅시다.
class GameScene extends Phaser.Scene {
create() {
this.score = 0;
this.lives = 3;
// HUD 컨테이너 (카메라 이동에 영향받지 않음)
this.hudContainer = this.add.container(0, 0);
this.hudContainer.setScrollFactor(0);
this.hudContainer.setDepth(100);
// 점수 표시
this.scoreText = this.add.text(20, 20, 'Score: 0', {
fontSize: '28px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 4
});
this.hudContainer.add(this.scoreText);
// 생명력 표시 (하트 아이콘)
this.livesGroup = this.add.group();
for (let i = 0; i < this.lives; i++) {
const heart = this.add.image(700 + (i * 40), 30, 'heart');
heart.setScale(0.5);
this.hudContainer.add(heart);
this.livesGroup.add(heart);
}
}
addScore(points) {
this.score += points;
this.scoreText.setText(`Score: ${this.score}`);
// 점수 증가 애니메이션
this.tweens.add({
targets: this.scoreText,
scale: 1.2,
duration: 100,
yoyo: true
});
}
loseLife() {
if (this.lives > 0) {
this.lives--;
const hearts = this.livesGroup.getChildren();
hearts[this.lives].setVisible(false);
}
if (this.lives <= 0) {
this.scene.start('GameOverScene', { score: this.score });
}
}
}
김개발 씨는 점수를 어디에 표시해야 할지 고민했습니다. 게임 화면 한쪽 구석에 텍스트를 추가하면 될 것 같은데, 카메라가 움직이면 점수 텍스트도 함께 움직여 버립니다.
어떻게 해야 점수를 화면의 고정된 위치에 표시할 수 있을까요? 박시니어 씨가 핵심을 짚어줍니다.
"**setScrollFactor(0)**을 사용하면 돼요. 이 설정은 해당 오브젝트가 카메라 스크롤에 영향받지 않도록 만들어줘요." setScrollFactor는 Phaser에서 매우 유용한 메서드입니다.
기본값 1은 카메라와 함께 움직인다는 의미이고, 0은 카메라가 어디로 이동하든 화면의 같은 위치에 고정된다는 의미입니다. HUD 요소들은 모두 setScrollFactor(0)을 설정해야 합니다.
위 코드에서는 Container를 사용하여 HUD 요소들을 그룹화했습니다. Container에 setScrollFactor(0)을 설정하면 그 안에 포함된 모든 요소가 함께 고정됩니다.
이렇게 하면 각 요소마다 개별적으로 설정할 필요가 없어 관리가 편해집니다. **setDepth(100)**은 표시 순서를 설정합니다.
숫자가 클수록 화면의 위쪽에 표시됩니다. HUD는 항상 다른 게임 요소 위에 표시되어야 하므로 충분히 큰 값을 지정합니다.
게임의 폭발 효과나 파티클이 HUD를 가리면 안 되니까요. addScore 함수에서는 점수를 추가하고 텍스트를 업데이트합니다.
단순히 숫자만 바꾸는 것이 아니라 작은 애니메이션을 추가했습니다. 점수가 올라갈 때 텍스트가 살짝 커졌다가 돌아오는 효과는 플레이어에게 즉각적인 피드백을 제공합니다.
이런 작은 디테일이 게임의 재미를 높입니다. 생명력은 텍스트 대신 하트 아이콘으로 표시했습니다.
시각적인 표현이 숫자보다 직관적입니다. 생명을 잃을 때 하트가 사라지는 것은 플레이어에게 위기감을 전달합니다.
setVisible(false)로 하트를 숨기면 나중에 다시 보이게 할 수도 있어서, 1UP 아이템을 먹었을 때 하트를 되살리는 것도 가능합니다. 실제 게임에서는 HUD에 더 많은 정보를 표시합니다.
스테이지 번호, 남은 시간, 특수 능력 쿨다운, 미니맵 등이 있습니다. 하지만 너무 많은 정보는 오히려 플레이어를 혼란스럽게 합니다.
정말 필요한 정보만 선별하여 깔끔하게 배치하는 것이 좋습니다. 김개발 씨는 HUD를 추가한 후 게임이 훨씬 완성도 있어 보인다고 느꼈습니다.
점수가 올라갈 때마다 작은 애니메이션이 뿌듯함을 느끼게 해줍니다. 이제 플레이어는 실시간으로 자신의 성과를 확인할 수 있습니다.
실전 팁
💡 - HUD는 가능한 한 심플하게 유지하세요. 정보 과부하는 플레이어를 피곤하게 만듭니다
- 점수 증가 시 팝업 텍스트(+100 같은)를 추가하면 더욱 만족스러운 피드백을 제공합니다
6. Scene 전환 효과
게임이 거의 완성되었습니다. 그런데 김개발 씨는 한 가지가 마음에 걸립니다.
Scene이 바뀔 때 화면이 뚝뚝 끊기는 느낌입니다. "전환 효과를 넣으면 훨씬 세련되어 보여요." 박시니어 씨가 마지막 터치를 알려줍니다.
Scene 전환 효과는 화면이 바뀔 때 부드러운 애니메이션을 추가하는 것입니다. 마치 영화에서 장면이 바뀔 때 페이드 아웃되는 것처럼, 자연스러운 전환은 플레이어의 몰입감을 높이고 게임의 완성도를 드높입니다.
다음 코드를 살펴봅시다.
class BaseScene extends Phaser.Scene {
// 페이드 아웃 후 Scene 전환
fadeToScene(targetScene, data = {}) {
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start(targetScene, data);
});
}
// Scene 시작 시 페이드 인
create() {
this.cameras.main.fadeIn(500, 0, 0, 0);
}
}
class GameScene extends BaseScene {
create() {
super.create(); // 페이드 인 효과 적용
// 게임 오브젝트 초기화
this.initGame();
}
goToGameOver() {
// 페이드 아웃 + 화면 흔들림
this.cameras.main.shake(300, 0.02);
this.time.delayedCall(300, () => {
this.fadeToScene('GameOverScene', { score: this.score });
});
}
}
class TitleScene extends BaseScene {
create() {
super.create();
// 슬라이드 인 애니메이션
const title = this.add.text(400, -100, 'My Game', {
fontSize: '64px',
color: '#ffffff'
}).setOrigin(0.5);
this.tweens.add({
targets: title,
y: 200,
duration: 1000,
ease: 'Bounce.easeOut'
});
}
}
김개발 씨는 Scene 전환이 너무 갑작스럽다고 느꼈습니다. 게임 오버 화면으로 넘어갈 때 화면이 순식간에 바뀌어 버리니 플레이어가 당황할 수 있습니다.
어떻게 하면 더 자연스럽게 전환할 수 있을까요? 박시니어 씨가 조언합니다.
"영화를 떠올려 보세요. 장면이 바뀔 때 갑자기 확 바뀌지 않잖아요.
페이드, 와이프, 디졸브 같은 전환 효과를 사용하죠. 게임도 마찬가지예요." Phaser의 Camera 객체는 다양한 전환 효과를 제공합니다.
fadeOut은 화면을 서서히 어둡게 만들고, fadeIn은 어두운 화면에서 서서히 밝아지게 합니다. shake는 화면을 흔들어 충격을 표현하고, flash는 화면을 번쩍이게 합니다.
위 코드에서는 BaseScene이라는 부모 클래스를 만들었습니다. 모든 게임 Scene이 이 클래스를 상속받으면 일관된 전환 효과를 적용할 수 있습니다.
fadeToScene 메서드는 페이드 아웃이 완료된 후에 다음 Scene으로 전환합니다. 이것이 핵심입니다.
애니메이션이 끝나기 전에 Scene을 바꾸면 효과가 잘리기 때문입니다. camerafadeoutcomplete 이벤트를 사용하여 페이드 아웃이 완전히 끝난 시점을 감지합니다.
once를 사용하면 이벤트가 한 번만 실행되고 자동으로 리스너가 제거됩니다. 메모리 누수를 방지하는 좋은 습관입니다.
게임 오버 시에는 단순히 페이드 아웃만 하는 것이 아니라 shake 효과를 먼저 적용했습니다. 캐릭터가 죽었을 때 화면이 흔들리면 충격과 긴장감을 전달할 수 있습니다.
time.delayedCall로 약간의 딜레이를 준 후 페이드 아웃으로 이어지는 연출입니다. 타이틀 화면에서는 Tween을 활용한 슬라이드 인 애니메이션을 적용했습니다.
제목 텍스트가 화면 위에서 아래로 떨어지며 통통 튀는 Bounce.easeOut 효과는 게임의 경쾌한 분위기를 전달합니다. Phaser는 다양한 이징(Easing) 함수를 제공하므로 상황에 맞게 선택할 수 있습니다.
전환 효과를 사용할 때 주의할 점이 있습니다. 너무 긴 전환 시간은 플레이어를 지루하게 만듭니다.
일반적으로 300ms에서 500ms 정도가 적당합니다. 또한 모든 Scene 전환에 같은 효과를 사용하면 지루해질 수 있으니, 상황에 맞게 다양한 효과를 적용해 보세요.
김개발 씨는 전환 효과를 추가한 후 게임을 다시 테스트했습니다. 화면이 부드럽게 전환되니 게임이 훨씬 세련되어 보입니다.
작은 디테일이지만 전체적인 완성도에 큰 차이를 만든다는 것을 느꼈습니다. 이제 정말 남에게 보여줘도 부끄럽지 않은 게임이 되었습니다.
실전 팁
💡 - 전환 효과의 길이는 500ms 이하로 유지하세요. 플레이어는 빠르게 게임으로 돌아가고 싶어합니다
- 상황에 맞는 전환 효과를 선택하세요. 게임 오버는 shake, 스테이지 클리어는 flash가 어울립니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
CloudFront CDN 완벽 가이드
AWS CloudFront를 활용한 콘텐츠 배포 최적화 방법을 실무 관점에서 다룹니다. 배포 생성부터 캐시 설정, HTTPS 적용까지 단계별로 알아봅니다.