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