๋ชฉ์ฐจ
๐จ UI/UX ๊ตฌํ
Phaser Baduk Metaverse ํ๋ก์ ํธ์ ์ฌ์ฉ์ ์ธํฐํ์ด์ค์ ์ฌ์ฉ์ ๊ฒฝํ ๊ตฌํ์ ๋ํด ์ค๋ช ํฉ๋๋ค.
1. UI/UX๋ ๋ฌด์์ธ๊ฐ์?
UI
(User Interface
)๋ ์ฌ์ฉ์๊ฐ ํ๋ก๊ทธ๋จ๊ณผ ์ํธ์์ฉํ๋ ํ๋ฉด์ ์๋ฏธํ๊ณ , UX
(User Experience
)๋ ์ฌ์ฉ์๊ฐ ํ๋ก๊ทธ๋จ์ ์ฌ์ฉํ ๋ ๋๋ผ๋ ์ ์ฒด์ ์ธ ๊ฒฝํ์ ์๋ฏธํฉ๋๋ค.
UI/UX์ ์ค์์ฑ:
- ์ฌ์ฉ์๊ฐ ์ฝ๊ฒ ์ดํดํ ์ ์๋ ์ธํฐํ์ด์ค.
- ์ง๊ด์ ์ธ ์กฐ์ ๋ฐฉ๋ฒ.
- ์ผ๊ด๋ ๋์์ธ ์ธ์ด.
- ์ ๊ทผ์ฑ๊ณผ ์ฌ์ฉ์ฑ ๊ณ ๋ ค.
๋ฐ๋ ๊ฒ์์์์ UI/UX:
- ๋ช ํํ ๋ฐ๋ํ ํ์.
- ๋์ ๋๋ ๋ฐฉ๋ฒ์ด ์ง๊ด์ .
- ๊ฒ์ ์ํ ์ ๋ณด๊ฐ ๋ช ํ.
- ๋ชจ๋ฐ์ผ๊ณผ ๋ฐ์คํฌํฑ ๋ชจ๋ ์ง์.
2. ๋์์ธ ์์น
1) ์ง๊ด์ฑ
- ๋ฐ๋ํ์ ์ ํต์ ์ธ 19×19 ๊ฒฉ์ ๋ ์ด์์.
- ๋ช ํํ ์์ ๊ตฌ๋ถ (ํ๋ฐฑ ๋).
- ์ง๊ด์ ์ธ ๋ฒํผ ๋ฐฐ์น์ ์์ด์ฝ.
- ์ฌ์ฉ์๊ฐ ํ๋์ ์ดํดํ ์ ์๋ ์ธํฐํ์ด์ค.
2) ๋ฐ์ํ
- ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์ ๋์.
- ํฐ์น ์ธํฐํ์ด์ค ์ต์ ํ.
- ํค๋ณด๋ ๋จ์ถํค ์ง์.
- ๋ชจ๋ฐ์ผ๊ณผ ๋ฐ์คํฌํฑ ๋ชจ๋ ์ต์ ํ.
3) ์ ๊ทผ์ฑ
- ๊ณ ๋๋น ์์ ๋ชจ๋.
- ํค๋ณด๋ ๋ค๋น๊ฒ์ด์ .
- ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ง์.
- ๋ชจ๋ ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ ์ ์๋๋ก ์ค๊ณ.
3. UI ๊ตฌ์กฐ ์ดํดํ๊ธฐ
1) ๊ธฐ๋ณธ UI ํด๋์ค ๊ตฌ์กฐ
class BadukUI { constructor(scene) { this.scene = scene; this.board = null; this.stoneLayer = null; this.uiLayer = null; this.infoPanel = null; this.controlPanel = null; this.initUI(); } initUI() { // ๋ฐ๋ํ ์์ฑ this.createBoard(); // UI ๋ ์ด์ด ์์ฑ this.createUILayer(); // ์ ๋ณด ํจ๋ ์์ฑ this.createInfoPanel(); // ์ปจํธ๋กค ํจ๋ ์์ฑ this.createControlPanel(); } }
์ค๋ช :
scene
-Phaser.js
์ ๊ฒ์ ์ฌ ๊ฐ์ฒดboard
- ๋ฐ๋ํ ๊ฐ์ฒดstoneLayer
- ๋๋ค์ ํ์ํ๋ ๋ ์ด์ดuiLayer
- UI ์์๋ค์ ํ์ํ๋ ๋ ์ด์ดinfoPanel
- ๊ฒ์ ์ ๋ณด๋ฅผ ํ์ํ๋ ํจ๋controlPanel
- ๋ฒํผ๋ค์ ํ์ํ๋ ํจ๋
2) ๋ ์ด์ด ๊ตฌ์กฐ
Phaser.js์ ๋ ์ด์ด ์์คํ :
- ๋ฐฐ๊ฒฝ ๋ ์ด์ด - ๋ฐ๋ํ ๊ฒฉ์
- ๋ ๋ ์ด์ด - ๋ฐ๋๋๋ค
- UI ๋ ์ด์ด - ๋ฒํผ, ํ ์คํธ, ํจ๋
- ํจ๊ณผ ๋ ์ด์ด - ์ ๋๋ฉ์ด์ , ํจ๊ณผ
4. ๋ฐ๋ํ ๋ ๋๋ง
1) ๊ธฐ๋ณธ ๊ฒฉ์ ์์ฑ
createBoard() { // 19x19 ๊ฒฉ์ ์์ฑ const boardSize = 18 * 40; // 40px ๊ฐ๊ฒฉ const startX = (800 - boardSize) / 2; const startY = (600 - boardSize) / 2; // ๊ฒฉ์ ๊ทธ๋ฆฌ๊ธฐ const graphics = this.scene.add.graphics(); graphics.lineStyle(1, 0x8B4513, 1); for (let i = 0; i < 19; i++) { // ์ธ๋ก์ graphics.moveTo(startX + i * 40, startY); graphics.lineTo(startX + i * 40, startY + boardSize); // ๊ฐ๋ก์ graphics.moveTo(startX, startY + i * 40); graphics.lineTo(startX + boardSize, startY + i * 40); } graphics.strokePaths(); }
์ค๋ช :
boardSize
- ๋ฐ๋ํ์ ์ ์ฒด ํฌ๊ธฐ (18 * 40 = 720px)startX, startY
- ๋ฐ๋ํ์ ์์ ์์น (ํ๋ฉด ์ค์)graphics.lineStyle()
- ์ ์ ์คํ์ผ ์ค์ (๋๊ป, ์์)graphics.moveTo()
- ์ ์ ์์์ graphics.lineTo()
- ์ ์ ๋์ graphics.strokePaths()
- ๊ทธ๋ฆฐ ์ ๋ค์ ์ค์ ๋ก ํ์
2) ๋ณ์ ์์น ํ์
drawStarPoints(graphics, startX, startY) { const starPoints = [ [3, 3], [3, 9], [3, 15], [9, 3], [9, 9], [9, 15], [15, 3], [15, 9], [15, 15] ]; graphics.fillStyle(0x8B4513, 1); starPoints.forEach(([x, y]) => { graphics.fillCircle( startX + x * 40, startY + y * 40, 3 ); }); }
์ค๋ช :
starPoints
- ๋ฐ๋ํ์ ๋ณ์ ์์น๋ค (3,3,9,9,15,15 ๋ฑ)graphics.fillStyle()
- ์์ ์์ ์ค์ graphics.fillCircle()
- ์ ๊ทธ๋ฆฌ๊ธฐ (x, y, ๋ฐ์ง๋ฆ)
3) ๋ฐ๋ํ ๋ฐฐ๊ฒฝ
createBoardBackground() { const boardSize = 18 * 40; const startX = (800 - boardSize) / 2; const startY = (600 - boardSize) / 2; // ๋ฐ๋ํ ๋ฐฐ๊ฒฝ (๋๋ฌด ์์) const background = this.scene.add.rectangle( startX + boardSize / 2, startY + boardSize / 2, boardSize + 20, boardSize + 20, 0xD2B48C ); background.setDepth(0); // ๊ฐ์ฅ ๋ค์ชฝ์ ๋ฐฐ์น }
์ค๋ช :
rectangle
- ์ฌ๊ฐํ ์์ฑ (x, y, width, height, color)setDepth(0)
- ๋ ์ด์ด ์์ ์ค์ (์ซ์๊ฐ ์์์๋ก ๋ค์ชฝ)
5. ๋ ๋ ๋๋ง
1) ๋ ์์ฑ ํจ์
createStone(x, y, color) { const stoneSize = 18; const startX = (800 - 18 * 40) / 2; const startY = (600 - 18 * 40) / 2; const stone = this.scene.add.circle( startX + x * 40, startY + y * 40, stoneSize, color === 'black' ? 0x000000 : 0xFFFFFF ); // ํฐ ๋์ ํ ๋๋ฆฌ ์ถ๊ฐ if (color === 'white') { stone.setStrokeStyle(1, 0x000000); } stone.setDepth(1); // ๊ฒฉ์๋ณด๋ค ์์ชฝ์ ๋ฐฐ์น return stone; }
์ค๋ช :
circle
- ์ ์์ฑ (x, y, ๋ฐ์ง๋ฆ, ์์)setStrokeStyle()
- ํ ๋๋ฆฌ ์คํ์ผ ์ค์ setDepth(1)
- ๊ฒฉ์๋ณด๋ค ์์ชฝ์ ๋ฐฐ์น
2) ๋ ํด๋ฆญ ์ด๋ฒคํธ
addStoneClickHandler(stone, x, y) { stone.setInteractive(); stone.on('pointerdown', () => { // ๋ ํด๋ฆญ ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ this.onStoneClick(x, y); }); // ํธ๋ฒ ํจ๊ณผ stone.on('pointerover', () => { stone.setScale(1.1); }); stone.on('pointerout', () => { stone.setScale(1.0); }); }
์ค๋ช :
setInteractive()
- ํด๋ฆญ ๊ฐ๋ฅํ๊ฒ ์ค์ on('pointerdown')
- ๋ง์ฐ์ค ํด๋ฆญ ์ด๋ฒคํธon('pointerover')
- ๋ง์ฐ์ค ํธ๋ฒ ์ด๋ฒคํธsetScale()
- ํฌ๊ธฐ ์กฐ์ (1.1 = 10% ํ๋)
6. UI ํจ๋ ์์ฑ
1) ์ ๋ณด ํจ๋
createInfoPanel() { const panel = this.scene.add.rectangle( 650, 100, 120, 200, 0xF5F5F5 ); panel.setStrokeStyle(2, 0xCCCCCC); // ๊ฒ์ ์ ๋ณด ํ ์คํธ this.gameInfoText = this.scene.add.text( 590, 20, '๊ฒ์ ์ ๋ณด', { fontSize: '16px', fill: '#333' } ); // ํ์ฌ ํ๋ ์ด์ด ํ์ this.currentPlayerText = this.scene.add.text( 590, 50, 'ํ์ฌ: ํ', { fontSize: '14px', fill: '#000' } ); // ์บก์ฒ๋ ๋ ์ ํ์ this.capturedText = this.scene.add.text( 590, 80, 'ํ: 0๊ฐ, ๋ฐฑ: 0๊ฐ', { fontSize: '12px', fill: '#666' } ); }
์ค๋ช :
rectangle
- ํจ๋ ๋ฐฐ๊ฒฝ ์์ฑsetStrokeStyle()
- ํ ๋๋ฆฌ ์ค์ add.text()
- ํ ์คํธ ์ถ๊ฐ (x, y, ๋ด์ฉ, ์คํ์ผ)
2) ์ปจํธ๋กค ํจ๋
createControlPanel() { // ํจ์ค ๋ฒํผ this.passButton = this.scene.add.text( 590, 250, 'ํจ์ค', { fontSize: '16px', fill: '#333', backgroundColor: '#DDD' } ); this.passButton.setPadding(10, 5); this.passButton.setInteractive(); this.passButton.on('pointerdown', () => { this.onPass(); }); // ํญ๋ณต ๋ฒํผ this.surrenderButton = this.scene.add.text( 590, 300, 'ํญ๋ณต', { fontSize: '16px', fill: '#FFF', backgroundColor: '#D32F2F' } ); this.surrenderButton.setPadding(10, 5); this.surrenderButton.setInteractive(); this.surrenderButton.on('pointerdown', () => { this.onSurrender(); }); }
์ค๋ช :
setPadding()
- ํ ์คํธ ์ฃผ๋ณ ์ฌ๋ฐฑ ์ค์ setInteractive()
- ํด๋ฆญ ๊ฐ๋ฅํ๊ฒ ์ค์ on('pointerdown')
- ํด๋ฆญ ์ด๋ฒคํธ ์ฒ๋ฆฌ
7. ๋ฐ์ํ ๋์์ธ
1) ํ๋ฉด ํฌ๊ธฐ ๊ฐ์ง
handleResize() { const width = this.scene.game.config.width; const height = this.scene.game.config.height; // ํ๋ฉด ํฌ๊ธฐ์ ๋ฐ๋ผ UI ์กฐ์ if (width < 768) { // ๋ชจ๋ฐ์ผ ๋ ์ด์์ this.adjustForMobile(); } else { // ๋ฐ์คํฌํฑ ๋ ์ด์์ this.adjustForDesktop(); } } adjustForMobile() { // ๋ฐ๋ํ ํฌ๊ธฐ ์ถ์ this.board.setScale(0.8); // UI ํจ๋ ์์น ์กฐ์ this.infoPanel.setPosition(300, 50); this.controlPanel.setPosition(300, 200); }
์ค๋ช :
game.config.width/height
- ๊ฒ์ ํ๋ฉด ํฌ๊ธฐsetScale()
- ํฌ๊ธฐ ์กฐ์ (0.8 = 80% ํฌ๊ธฐ)setPosition()
- ์์น ๋ณ๊ฒฝ
2) ํฐ์น ์ธํฐํ์ด์ค
setupTouchInterface() { // ํฐ์น ์ด๋ฒคํธ ์ค์ this.scene.input.on('pointerdown', (pointer) => { const x = Math.floor((pointer.x - this.boardStartX) / 40); const y = Math.floor((pointer.y - this.boardStartY) / 40); if (x >= 0 && x < 19 && y >= 0 && y < 19) { this.onBoardClick(x, y); } }); }
์ค๋ช :
input.on('pointerdown')
- ํฐ์น/ํด๋ฆญ ์ด๋ฒคํธpointer.x/y
- ํฐ์นํ ์์น- ์ขํ ๊ณ์ฐ์ผ๋ก ๋ฐ๋ํ ์์น ํ์ธ
8. ์ ๋๋ฉ์ด์ ํจ๊ณผ
1) ๋ ๋๊ธฐ ์ ๋๋ฉ์ด์
animateStonePlacement(x, y, color) { const stone = this.createStone(x, y, color); // ์ฒ์์๋ ์๊ฒ ์์ stone.setScale(0.1); // ์ ๋๋ฉ์ด์ ์ผ๋ก ํฌ๊ธฐ ์กฐ์ this.scene.tweens.add({ targets: stone, scaleX: 1, scaleY: 1, duration: 300, ease: 'Back.easeOut' }); }
์ค๋ช :
setScale(0.1)
- ์ฒ์์๋ 10% ํฌ๊ธฐtweens.add()
- ์ ๋๋ฉ์ด์ ์ถ๊ฐduration: 300
- 300ms ๋์ ์ ๋๋ฉ์ด์ ease: 'Back.easeOut
' - ํ์ฑ ํจ๊ณผ
2) ๋ ์ ๊ฑฐ ์ ๋๋ฉ์ด์
animateStoneRemoval(stone) { this.scene.tweens.add({ targets: stone, scaleX: 0, scaleY: 0, alpha: 0, duration: 200, onComplete: () => { stone.destroy(); } }); }
์ค๋ช :
alpha: 0
- ํฌ๋ช ๋ 0์ผ๋ก ์ค์ onComplete
- ์ ๋๋ฉ์ด์ ์๋ฃ ํ ์คํstone.destroy()
- ๋ ๊ฐ์ฒด ์ ๊ฑฐ
9. ์ ๊ทผ์ฑ ๊ธฐ๋ฅ
1) ๊ณ ๋๋น ๋ชจ๋
enableHighContrast() { // ๊ณ ๋๋น ์์ ์ค์ this.boardBackground.setFillStyle(0x000000); this.gridLines.setStrokeStyle(1, 0xFFFFFF); // ํ ์คํธ ์์ ์กฐ์ this.gameInfoText.setColor('#FFFFFF'); this.currentPlayerText.setColor('#FFFFFF'); }
์ค๋ช :
setFillStyle()
- ๋ฐฐ๊ฒฝ์ ์ค์ setStrokeStyle()
- ์ ์์ ์ค์ setColor()
- ํ ์คํธ ์์ ์ค์
2) ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
setupKeyboardNavigation() { this.scene.input.keyboard.on('keydown', (event) => { switch(event.key) { case 'ArrowUp': this.moveSelection(0, -1); break; case 'ArrowDown': this.moveSelection(0, 1); break; case 'ArrowLeft': this.moveSelection(-1, 0); break; case 'ArrowRight': this.moveSelection(1, 0); break; case 'Enter': this.placeStone(); break; } }); }
์ค๋ช :
input.keyboard.on('keydown')
- ํค๋ณด๋ ์ด๋ฒคํธ- ๋ฐฉํฅํค๋ก ์ ํ ์์น ์ด๋
- Enter ํค๋ก ๋ ๋๊ธฐ
10. ์ค์ต ์์
1) ๊ฐ๋จํ ๋ฐ๋ํ UI
HTML ํ์ผ:
<!DOCTYPE html> <html> <head> <title>๋ฐ๋ ๊ฒ์</title> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script> </head> <body> <div id="game"></div> <script src="game.js"></script> </body> </html>
JavaScript ํ์ผ (game.js
):
const config = { type: Phaser.AUTO, width: 800, height: 600, parent: 'game', scene: { create: create } }; const game = new Phaser.Game(config); function create() { // ๋ฐ๋ํ ๋ฐฐ๊ฒฝ const background = this.add.rectangle(400, 300, 720, 720, 0xD2B48C); // ๊ฒฉ์ ๊ทธ๋ฆฌ๊ธฐ const graphics = this.add.graphics(); graphics.lineStyle(1, 0x8B4513, 1); for (let i = 0; i < 19; i++) { const x = 40 + i * 40; const y = 40 + i * 40; // ์ธ๋ก์ graphics.moveTo(x, 40); graphics.lineTo(x, 760); // ๊ฐ๋ก์ graphics.moveTo(40, y); graphics.lineTo(760, y); } graphics.strokePaths(); // ๋ณ์ ๊ทธ๋ฆฌ๊ธฐ const starPoints = [[3,3], [3,9], [3,15], [9,3], [9,9], [9,15], [15,3], [15,9], [15,15]]; graphics.fillStyle(0x8B4513, 1); starPoints.forEach(([x, y]) => { graphics.fillCircle(40 + x * 40, 40 + y * 40, 3); }); }
2) ์คํ ๋ฐฉ๋ฒ
# ๋ก์ปฌ ์๋ฒ ์คํ python -m http.server 8000 # ๋๋ npx http-server
ํ ์คํธ:
- ๋ธ๋ผ์ฐ์ ์์
http://localhost:8000
์ ์ - ๋ฐ๋ํ์ด ํ์๋๋์ง ํ์ธ
11. ๋ค์ ๋จ๊ณ
UI/UX
๊ธฐ๋ณธ์ ๋ฐฐ์ ๋ค๋ฉด ๋ค์์ ํ์ตํด๋ณด์ธ์:
- ๊ณ ๊ธ ์ ๋๋ฉ์ด์ - ๋ณต์กํ ์ ๋๋ฉ์ด์ ํจ๊ณผ
- ์ฌ์ด๋ ๋์์ธ - ํจ๊ณผ์๊ณผ ๋ฐฐ๊ฒฝ์์
- ๊ฒ์ ์ํ ๊ด๋ฆฌ - UI ์ํ ๊ด๋ฆฌ
- ์ฌ์ฉ์ ํ ์คํธ - ์ค์ ์ฌ์ฉ์ ํผ๋๋ฐฑ
- ์ฑ๋ฅ ์ต์ ํ - UI ๋ ๋๋ง ์ต์ ํ
์ถ์ฒ ํ์ต ์์:
โ
์นดํ ๊ณ ๋ฆฌ: ๋ฉํ๋ฒ์ค | ๊ด๋ จ ๊ธฐ์ : Phaser.js, UI/UX, ๋ฐ์ํ ๋์์ธ