Phaser Baduk Metaverse 프로젝트에서 사용되는 Phaser.js 게임 엔진의 구현 내용에 대해 자세히 설명합니다.
Phaser.js는 HTML5 게임 개발에 특화된 강력한 JavaScript 프레임워크입니다. 이 프레임워크는 바둑 게임의 시각적 요소를 구현하고, 사용자 인터랙션 및 실시간 통신 기능을 통합하여 몰입감 있는 게임 경험을 제공하는 데 핵심적인 역할을 합니다.
HTML5 기반: 웹 브라우저에서 직접 실행되어 별도의 설치 없이 접근성이 높습니다.게임 개발 특화: 스프라이트, 애니메이션, 물리 엔진 등 게임 개발에 필요한 다양한 기능을 내장하고 있습니다.유연한 구조: 씬(Scene) 기반의 아키텍처를 통해 게임의 각 부분을 모듈화하여 관리할 수 있습니다.
Phaser.js 게임 엔진의 핵심 진입점인 game.js 파일은 게임의 전반적인 설정과 초기화를 담당합니다. 이 파일은 게임의 캔버스 크기, 배경색, 사용할 씬 목록 등 필수적인 구성 요소를 정의합니다.
game.js는 Phaser 게임 인스턴스를 생성하고, 게임의 전역 설정을 정의하는 파일입니다. 이곳에서 게임의 해상도, 렌더링 방식, 포함될 씬 목록 등을 지정합니다.
import Phaser from 'phaser'; import BadukScene from './scenes/BadukScene'; import MenuScene from './scenes/MenuScene'; // 1. 게임 설정 객체 정의 const config = { type: Phaser.AUTO, // 렌더링 방식 (WebGL 또는 Canvas 자동 선택) width: 1200, // 게임 화면 너비 (픽셀) height: 800, // 게임 화면 높이 (픽셀) parent: 'game-container', // 게임 캔버스가 삽입될 HTML 요소의 ID backgroundColor: '#2c3e50', // 게임 배경색 scene: [MenuScene, BadukScene], // 게임에 포함될 씬 목록 (순서 중요) physics: { // 물리 엔진 설정 default: 'arcade', // 기본 물리 엔진으로 Arcade 물리 엔진 사용 arcade: { gravity: { y: 0 }, // y축 중력 없음 debug: false // 물리 객체 디버그 정보 비활성화 } }, scale: { // 스케일링 설정 mode: Phaser.Scale.FIT, // 화면에 맞게 게임 크기 조절 autoCenter: Phaser.Scale.CENTER_BOTH // 가로/세로 중앙 정렬 } }; // 2. Phaser 게임 인스턴스 생성 const game = new Phaser.Game(config); // 3. 게임 인스턴스 내보내기 (다른 파일에서 사용 가능하도록) export default game;
코드 설명:
임포트 (Import): Phaser 라이브러리와 게임에서 사용할 MenuScene, BadukScene 씬들을 불러옵니다.config 객체: 게임의 전반적인 설정을 담고 있습니다. 주요 속성은 다음과 같습니다.type: 게임 렌더링 방식을 결정합니다. Phaser.AUTO는 브라우저가 지원하는 최적의 렌더링 방식(WebGL 또는 Canvas)을 자동으로 선택합니다.width, height: 게임 화면의 해상도를 픽셀 단위로 지정합니다.parent: 게임 캔버스가 삽입될 HTML 문서 내의 특정 DOM 요소 ID를 지정합니다. 이 경우, id=“game-container”를 가진 요소에 게임이 표시됩니다.backgroundColor: 게임의 기본 배경색을 지정합니다.scene: 게임에서 사용할 모든 씬(Scene)들의 배열입니다. 배열의 순서에 따라 씬들이 로드되지만, 게임 시작 시에는 첫 번째 씬이 기본으로 시작됩니다.physics: 물리 엔진 설정을 포함합니다. 여기서는 바둑 게임에 필요하지 않은 중력을 비활성화하고 디버그 모드를 꺼두었습니다.scale: 게임이 다양한 화면 크기에 맞춰 어떻게 조절될지 정의합니다. FIT 모드는 화면 비율을 유지하며 가능한 한 크게 표시하고, CENTER_BOTH는 게임을 화면 중앙에 배치합니다.Phaser.Game 인스턴스 생성: 위에서 정의한 config 객체를 사용하여 새로운 Phaser 게임 인스턴스를 생성합니다. 이 인스턴스가 실제 게임을 실행하는 주체입니다.export default game: 이 게임 인스턴스를 다른 JavaScript 파일에서 불러와 사용할 수 있도록 내보냅니다.
Phaser.js에서 씬(Scene)은 게임의 특정 상태나 화면을 나타내는 독립적인 단위입니다. 예를 들어, 게임의 메뉴 화면, 실제 게임 플레이 화면, 게임 오버 화면 등이 각각의 씬으로 구현될 수 있습니다. 씬은 게임의 로드, 생성, 업데이트, 종료 등 생명주기를 가집니다.
MenuScene.js는 게임이 시작될 때 가장 먼저 사용자에게 보여지는 메뉴 화면을 담당합니다. 여기서는 게임 시작 버튼, 설정 버튼 등을 배치하고, 사용자의 입력에 따라 다른 씬으로 전환하는 기능을 구현합니다.
import Phaser from 'phaser'; export default class MenuScene extends Phaser.Scene { constructor() { super({ key: 'MenuScene' }); // 씬의 고유 키 정의 } preload() { // 1. 필요한 이미지 에셋 로드 this.load.image('menu-bg', 'assets/menu-background.png'); this.load.image('play-button', 'assets/play-button.png'); this.load.image('settings-button', 'assets/settings-button.png'); } create() { // 2. 배경 이미지 추가 (화면 중앙에 배치) this.add.image(600, 400, 'menu-bg'); // 3. 게임 제목 텍스트 추가 this.add.text(600, 200, '바둑 메타버스', { // x, y, 텍스트, 스타일 fontSize: '48px', fill: '#ffffff', // 흰색 글자 fontFamily: 'Arial' }).setOrigin(0.5); // 텍스트의 기준점을 중앙으로 설정 // 4. 플레이 버튼 생성 및 상호작용 설정 const playButton = this.add.image(600, 350, 'play-button') .setInteractive(); // 클릭 가능하도록 설정 playButton.on('pointerdown', () => { // 버튼 클릭 시 이벤트 this.scene.start('BadukScene'); // 'BadukScene'으로 전환 }); // 5. 설정 버튼 생성 및 상호작용 설정 const settingsButton = this.add.image(600, 450, 'settings-button') .setInteractive(); settingsButton.on('pointerdown', () => { console.log('설정 메뉴 열기'); // 설정 기능은 콘솔 로그로 대체 }); } }
코드 설명:
constructor: 씬의 고유한 키(여기서는 'MenuScene')를 정의합니다. 이는 다른 씬에서 이 씬을 참조할 때 사용됩니다.preload(): 이 씬이 시작되기 전에 필요한 모든 이미지, 오디오 등의 에셋을 미리 로드하는 함수입니다. 게임 로딩 시간을 단축하고, 씬이 준비되었을 때 즉시 사용할 수 있도록 합니다.create(): 씬의 에셋 로딩이 완료된 후, 게임 오브젝트(배경, 버튼, 텍스트 등)를 생성하고 배치하는 함수입니다.this.add.image(): 배경 이미지와 버튼 이미지를 화면에 추가합니다.this.add.text(): 게임 제목 텍스트를 추가하고 스타일을 적용합니다. setOrigin(0.5)는 텍스트의 중앙을 기준으로 위치를 잡도록 합니다.setInteractive(): 버튼 이미지를 클릭 가능한 상호작용 객체로 만듭니다.on('pointerdown', …): 버튼이 클릭(또는 터치)되었을 때 실행될 콜백 함수를 정의합니다. this.scene.start('BadukScene')을 통해 실제 바둑 게임 씬으로 전환합니다.
BadukScene.js는 바둑 게임의 핵심 로직과 사용자 인터랙션이 구현되는 씬입니다. 바둑판, 바둑돌의 배치, 클릭 이벤트 처리, 턴 관리, 그리고 서버와의 통신 등을 담당합니다.
import Phaser from 'phaser'; import BadukBoard from '../sprites/BadukBoard'; // 바둑판 스프라이트 임포트 import BadukStone from '../sprites/BadukStone'; // 바둑돌 스프라이트 임포트 export default class BadukScene extends Phaser.Scene { constructor() { super({ key: 'BadukScene' }); this.board = null; // 바둑판 객체 this.stones = []; // 놓여진 바둑돌 배열 this.currentPlayer = 'black'; // 현재 턴 플레이어 ('black' 또는 'white') this.gameId = null; // 현재 게임 ID } preload() { // 1. 바둑판 및 돌 이미지 로드 this.load.image('board', 'assets/baduk-board.png'); this.load.image('black-stone', 'assets/black-stone.png'); this.load.image('white-stone', 'assets/white-stone.png'); this.load.image('grid', 'assets/grid-lines.png'); // 바둑판 그리드 이미지 } create() { // 2. 바둑판 생성 및 그리드 라인 추가 this.board = new BadukBoard(this, 600, 400); // BadukBoard 스프라이트 생성 this.add.existing(this.board); // 씬에 바둑판 추가 this.add.image(600, 400, 'grid'); // 그리드 라인 이미지 추가 // 3. 클릭 이벤트 설정: 바둑판 클릭 시 착수 처리 this.input.on('pointerdown', (pointer) => { this.handleBoardClick(pointer); }); // 4. 현재 플레이어 표시 UI 생성 this.createPlayerIndicator(); // 5. (옵션) 게임 시작 시 서버로부터 게임 ID 수신 if (window.socket) { window.socket.on('game-start', (data) => { console.log('게임이 시작되었습니다. 게임 ID:', data.gameId); this.gameId = data.gameId; // 게임 상태 동기화 및 시작 로직 추가 가능 }); window.socket.on('game-update', (data) => { // 다른 플레이어의 착수 정보 수신 및 처리 if (data.player !== this.currentPlayer) { // 자신의 턴이 아닌 경우에만 처리 this.placeStone(data.x, data.y, data.player); } }); } } // 6. 바둑판 클릭 처리 함수 handleBoardClick(pointer) { // 클릭된 픽셀 좌표를 바둑판의 상대 좌표로 변환 const boardX = pointer.x - this.board.x; const boardY = pointer.y - this.board.y; // 상대 좌표를 그리드(바둑판 칸) 좌표로 변환 const gridX = Math.round(boardX / this.board.gridSize) + 9; // 0~18 범위로 조정 const gridY = Math.round(boardY / this.board.gridSize) + 9; // 유효한 착수 위치인지 확인 후 돌 놓기 if (this.isValidMove(gridX, gridY)) { this.placeStone(gridX, gridY, this.currentPlayer); } } // 7. 착수 위치 유효성 검사 isValidMove(x, y) { // 바둑판 범위 (0~18) 내인지 확인 if (x < 0 || x > 18 || y < 0 || y > 18) { return false; } // 이미 돌이 놓인 위치인지 확인 return !this.stones.some(stone => stone.gridX === x && stone.gridY === y ); // TODO: 자충, 코 규칙 등 추가적인 바둑 규칙 검사 필요 } // 8. 바둑돌 놓기 (시각적 처리 및 상태 업데이트) placeStone(x, y, playerColor) { // 현재 턴 플레이어의 돌 이미지 선택 const stoneImage = playerColor === 'black' ? 'black-stone' : 'white-stone'; // BadukStone 스프라이트 생성 (애니메이션 포함) const stone = new BadukStone(this, x, y, playerColor); this.stones.push(stone); // 놓여진 돌 목록에 추가 // 플레이어 턴 변경 (다음 턴) this.currentPlayer = this.currentPlayer === 'black' ? 'white' : 'black'; // UI 업데이트 (현재 플레이어 표시) this.updatePlayerIndicator(); // 서버에 착수 정보 전송 this.sendMoveToServer(x, y, playerColor); } // 9. 현재 플레이어 표시 UI 생성 createPlayerIndicator() { this.playerText = this.add.text(50, 50, '현재 플레이어: 흑돌', { fontSize: '24px', fill: '#ffffff' }); } // 10. 현재 플레이어 표시 UI 업데이트 updatePlayerIndicator() { const playerName = this.currentPlayer === 'black' ? '흑돌' : '백돌'; this.playerText.setText(`현재 플레이어: ${playerName}`); } // 11. 서버에 착수 정보 전송 sendMoveToServer(x, y, player) { if (window.socket && this.gameId) { window.socket.emit('move', { // 'move' 이벤트로 서버에 데이터 전송 x: x, y: y, player: player, gameId: this.gameId // 현재 게임 ID 포함 }); } } }
코드 설명:
constructor: 씬의 고유 키를 정의하고, 바둑판 객체, 놓인 돌들을 저장할 배열, 현재 턴 플레이어, 게임 ID와 같은 게임 상태 변수들을 초기화합니다.preload(): 바둑판, 흑돌, 백돌 이미지 등 게임 플레이에 필요한 모든 에셋을 로드합니다.create():this.board = new BadukBoard(…): 커스텀 BadukBoard 스프라이트를 생성하여 시각적인 바둑판을 화면에 추가합니다.this.input.on('pointerdown', …): 마우스 클릭(또는 터치) 이벤트를 감지하여 handleBoardClick 함수를 호출하도록 설정합니다.createPlayerIndicator(): 현재 턴을 표시하는 UI 텍스트를 생성합니다.window.socket.on(…): Socket.IO를 통해 서버로부터 게임 시작 및 다른 플레이어의 착수 정보를 실시간으로 수신하여 처리합니다.handleBoardClick(pointer): 사용자가 바둑판을 클릭했을 때 호출됩니다. 클릭된 화면 픽셀 좌표를 바둑판의 그리드 좌표(예: (0,0)에서 (18,18)까지)로 변환하고, isValidMove를 통해 유효성 검사 후 placeStone을 호출합니다.isValidMove(x, y): 주어진 그리드 좌표가 바둑판 범위 내에 있는지, 그리고 해당 위치에 이미 돌이 놓여있는지 확인하여 착수 가능 여부를 반환합니다. (TODO에 언급된 것처럼 실제 바둑 규칙은 더 복잡합니다.)placeStone(x, y, playerColor):new BadukStone(…): BadukStone 스프라이트를 생성하여 해당 위치에 시각적으로 돌을 놓습니다. 이 과정에서 돌이 나타나는 애니메이션이 포함됩니다.this.stones.push(stone): 놓여진 돌을 stones 배열에 추가하여 게임 상태를 관리합니다.this.currentPlayer = …: 현재 플레이어의 턴을 흑돌에서 백돌로, 또는 백돌에서 흑돌로 전환합니다.updatePlayerIndicator(): UI를 업데이트하여 현재 턴 플레이어를 표시합니다.sendMoveToServer(): 놓인 돌의 정보를 서버로 전송하여 다른 플레이어와 게임 상태를 동기화합니다.createPlayerIndicator(), updatePlayerIndicator(): 현재 턴 플레이어를 화면에 텍스트로 표시하고 업데이트하는 함수입니다.sendMoveToServer(x, y, player): Socket.IO를 사용하여 착수 정보를 서버의 'move' 이벤트로 전송합니다. 게임 ID를 함께 보내 특정 게임 세션에 대한 이동임을 알립니다.
Phaser.js에서 스프라이트(Sprite)는 게임 내에서 움직이거나 상호작용할 수 있는 시각적인 객체를 의미합니다. 바둑 게임에서는 바둑판과 바둑돌이 각각의 스프라이트로 구현되어 게임 세계 내에서 독립적으로 존재하며 특정 기능을 수행합니다.
BadukBoard.js는 바둑판 이미지를 나타내는 스프라이트입니다. 단순히 이미지를 표시하는 것을 넘어, 바둑판의 그리드 시스템을 관리하고 픽셀 좌표와 그리드 좌표 간의 변환을 처리하는 중요한 역할을 합니다.
import Phaser from 'phaser'; export default class BadukBoard extends Phaser.GameObjects.Sprite { constructor(scene, x, y) { super(scene, x, y, 'board'); // Phaser.GameObjects.Sprite 생성자 호출 this.setOrigin(0.5); // 스프라이트의 기준점을 중앙으로 설정 this.setInteractive(); // 상호작용 가능하도록 설정 (클릭 등) this.setScale(1.0); // 바둑판 크기 (스케일) 설정 // 바둑판 그리드 좌표 시스템 설정 this.gridSize = 40; // 한 칸의 픽셀 크기 this.boardSize = 19; // 19x19 바둑판 } // 1. 그리드 좌표(0~18)를 게임 화면의 픽셀 좌표로 변환 gridToPixel(gridX, gridY) { // 바둑판의 중앙이 (0,0)이라고 가정하고, 그리드 좌표를 픽셀로 변환 // 예: gridX 9는 바둑판의 중앙 x축에 해당 const pixelX = this.x + (gridX - (this.boardSize - 1) / 2) * this.gridSize; const pixelY = this.y + (gridY - (this.boardSize - 1) / 2) * this.gridSize; return { x: pixelX, y: pixelY }; } // 2. 게임 화면의 픽셀 좌표를 그리드 좌표(0~18)로 변환 pixelToGrid(pixelX, pixelY) { // 픽셀 좌표를 바둑판 중앙을 기준으로 한 상대 좌표로 변환 후 그리드 크기로 나누어 그리드 좌표 얻기 const gridX = Math.round((pixelX - this.x) / this.gridSize) + (this.boardSize - 1) / 2; const gridY = Math.round((pixelY - this.y) / this.gridSize) + (this.boardSize - 1) / 2; return { x: gridX, y: gridY }; } }
코드 설명:
constructor(scene, x, y):super(scene, x, y, 'board'): 부모 클래스인 Phaser.GameObjects.Sprite의 생성자를 호출하여, 이 객체가 'board'라는 키로 로드된 이미지를 사용하도록 설정합니다. x와 y는 바둑판 스프라이트의 화면상 위치입니다.this.setOrigin(0.5): 스프라이트의 원점(기준점)을 중앙으로 설정합니다. 이렇게 하면 x, y 좌표가 스프라이트의 중앙을 가리키게 되어 위치 계산이 편리해집니다.this.setInteractive(): 이 스프라이트가 마우스 클릭 같은 사용자 입력 이벤트를 감지할 수 있도록 설정합니다.this.gridSize, this.boardSize: 바둑판의 한 칸(점과 점 사이)의 픽셀 크기와 바둑판의 가로/세로 칸 수를 정의합니다. 이는 픽셀 좌표와 그리드 좌표 간 변환에 사용됩니다.gridToPixel(gridX, gridY): 바둑판의 그리드 좌표(예: (0,0)부터 (18,18)까지)를 게임 화면 상의 실제 픽셀 좌표로 변환합니다. 이는 바둑돌을 정확한 위치에 놓을 때 사용됩니다.pixelToGrid(pixelX, pixelY): 게임 화면 상의 픽셀 좌표를 바둑판의 그리드 좌표로 변환합니다. 이는 사용자가 마우스를 클릭했을 때 어느 칸을 클릭했는지 알아낼 때 사용됩니다.
BadukStone.js는 게임 내에서 흑돌과 백돌을 나타내는 스프라이트입니다. 이 클래스는 돌의 시각적인 표현뿐만 아니라, 착수 시 애니메이션 효과를 부여하여 사용자 경험을 향상시킵니다.
<file javascript> import Phaser from 'phaser';
export default class BadukStone extends Phaser.GameObjects.Sprite {
constructor(scene, gridX, gridY, color) {
// 돌 이미지 키 선택 ('black-stone' 또는 'white-stone')
const imageKey = color === 'black' ? 'black-stone' : 'white-stone';
// 바둑판 객체의 gridToPixel 함수를 사용하여 그리드 좌표를 픽셀 좌표로 변환
const { x, y } = scene.board.gridToPixel(gridX, gridY);
super(scene, x, y, imageKey); // Phaser.GameObjects.Sprite 생성자 호출
this.gridX = gridX; // 돌의 그리드 X 좌표 저장
this.gridY = gridY; // 돌의 그리드 Y 좌표 저장
this.color = color; // 돌의 색상 저장
this.setOrigin(0.5); // 돌의 기준점을 중앙으로 설정
this.setScale(0.8); // 돌의 크기(스케일) 설정
// 1. 착수 시 돌이 커지는 애니메이션 효과 (스케일 0 -> 0.8)
this.setScale(0); // 처음에는 크기를 0으로 설정
scene.tweens.add({ // 트윈(애니메이션) 추가
targets: this, // 애니메이션 적용 대상
scaleX: 0.8, // X축 스케일을 0.8로
scaleY: 0.8, // Y축 스케일을 0.8