인터랙티브 멀티플레이어 게임을 개발하려면 실시간 동기화와 서버리스 함수를 사용하여 게임 로직을 관리해야 합니다. Firebase는 실시간 데이터베이스와 Firebase Functions을 통해 이러한 애플리케이션에 이상적인 플랫폼을 제공합니다. 이 기사에서는 멀티플레이어 수학 퀴즈 게임을 디자인할 것입니다. 주요 기능, 데이터베이스 스키마, Firebase 실시간 데이터베이스, Firebase Functions을 자세히 살펴볼 것입니다.
프로젝트의 전체 소스 코드는 여기에서 확인할 수 있습니다.
Firebase 실시간 데이터베이스
Firebase 실시간 데이터베이스는 클라우드 기반 NoSQL 데이터베이스로, 모든 클라이언트 간 데이터를 실시간으로 동기화합니다. 주요 기능은 다음과 같습니다:
- 실시간 동기화: 데이터 변경 사항이 모든 연결된 클라이언트에 즉시 반영됩니다.
- 오프라인 지원: 로컬 데이터 지속성을 통해 오프라인 상태에서도 원활한 사용자 경험을 제공합니다.
- 보안 규칙: 데이터 액세스 및 유효성 검사에 대한 세밀한 제어.
Firebase Functions
Firebase Functions은 이벤트에 응답하여 실행되는 서버리스 함수입니다. Firebase 기능을 확장하고 외부 서비스와 통합할 수 있습니다. 주요 장점은 다음과 같습니다:
- 확장성: 다양한 워크로드를 처리할 수 있도록 자동으로 확장됩니다.
- 트리거: 함수는 데이터베이스 업데이트 또는 인증 변경과 같은 다양한 Firebase 이벤트에 의해 트리거될 수 있습니다.
- 통합: 다른 Firebase 제품 및 서드파티 API와 쉽게 통합할 수 있습니다.
수학 퀴즈 게임의 특징
- 실시간 매칭: 매칭 대기 중인 플레이어를 자동으로 짝지어 줍니다.
- 다이내믹 수학 문제: 각 게임 세션마다 고유한 수학 방정식을 생성합니다.
- 게임 관리: 게임 세션을 생성하고 관리하며, 플레이어의 상태와 점수를 업데이트합니다.
- 즉시 업데이트: 게임 상태와 플레이어 상호작용에 대한 실시간 업데이트를 보장합니다.
데이터베이스 스키마 설계
저희 게임은 Firebase 실시간 데이터베이스에서 다음 스키마를 사용할 것입니다.
- /matchMaking: 매칭을 기다리는 플레이어 정보를 저장합니다.
- /inGame: 활성화된 게임 세션을 관리하며, 플레이어 데이터와 수학 문제를 포함합니다.
- /lock: 매칭 처리를 단 한 번에 한 인스턴스만 처리되도록 잠금 메커니즘을 구현합니다.
스키마 세부 정보:
{
"matchMaking": {
"player1Id": {
"info": {
"userName": "플레이어1",
"photoUrl": "url1"
},
"gameId": null
},
"player2Id": {
"info": {
"userName": "플레이어2",
"photoUrl": "url2"
},
"gameId": null
}
},
"inGame": {
"gameId": {
"players": {
"player1Id": {
"userName": "플레이어1",
"photoUrl": "url1",
"score": 0
},
"player2Id": {
"userName": "플레이어2",
"photoUrl": "url2",
"score": 0
}
},
"questionOptions": [
{
"numbers": [2, 3],
"operator": "+",
"correctAns": 5,
"options": [5, 6, 7, 8]
}
]
}
},
"lock": false
}
함수 구현하기
Firebase를 초기화 중: Firebase 서비스를 사용하려면 먼저 Firebase 앱을 초기화하고 Realtime Database에 대한 참조를 가져와야 합니다:
import { initializeApp } from "firebase-admin/app";
import { getDatabase } from "firebase-admin/database";
import { onValueUpdated } from "firebase-functions/v2/database";
import { logger } from "firebase-functions";
initializeApp();
const db = getDatabase();
매칭 메이킹 처리하기
핵심 함수 startGame은 매칭 큐에서 플레이어를 쌍으로 매치하여 처리합니다. 여기에 개괄적인 내용이 있습니다:
export const startGame = onValueUpdated("/matchMaking", async (event) => {
const matchMakingRef = db.ref("/matchMaking");
const lockRef = db.ref("/lock");
try {
const lockResult = await lockRef.transaction((lock) => {
if (!lock) {
return true;
}
return;
});
if (!lockResult.committed) {
logger.info("Lock already acquired by another instance");
return;
}
const snapshot = await matchMakingRef.once("value");
const matchMaking = snapshot.val();
const matchMakingKeys = Object.keys(matchMaking || {});
if (matchMakingKeys.length >= 2) {
let player1, player2;
for (let i = 0; i < matchMakingKeys.length; i++) {
const player = matchMaking[matchMakingKeys[i]];
if (!player1 && !player.gameId) {
player1 = player;
player1.id = matchMakingKeys[i];
} else if (!player2 && !player.gameId) {
player2 = player;
player2.id = matchMakingKeys[i];
}
if (player1 && player2) {
break;
}
}
if (player1 && player2) {
const firstPlayerId = player1.id;
const secondPlayerId = player2.id;
const firstPlayerInfo = {
userName: player1.info.userName,
photoUrl: player1.info.photoUrl,
score: 0,
};
const secondPlayerInfo = {
userName: player2.info.userName,
photoUrl: player2.info.photoUrl,
score: 0,
};
const playerKeyValue = {
[firstPlayerId]: firstPlayerInfo,
[secondPlayerId]: secondPlayerInfo,
};
const gameId = `${firstPlayerId}_${secondPlayerId}`;
const inGameRef = db.ref(`/inGame/${gameId}`);
const equations = generateEquations();
await inGameRef.set({
players: playerKeyValue,
questionOptions: equations,
});
const updates = {
[`/matchMaking/${firstPlayerId}`]: { gameId, opponentUuid: secondPlayerId },
[`/matchMaking/${secondPlayerId}`]: { gameId, opponentUuid: firstPlayerId },
};
await db.ref().update(updates);
}
}
} catch (error) {
logger.error("Error processing matchmaking:", error);
} finally {
await lockRef.set(false);
}
});
주요 단계:
- Lock Handling: /lock에서 race condition을 방지하기 위해 트랜잭션을 사용합니다.
- Matchmaking: 매칭 대기열에서 두 플레이어를 선택하고 짝을 지어줍니다.
- 게임 설정: 새로운 게임 세션을 초기화하고 플레이어 상태를 업데이트합니다.
수학 문제 생성하기
게임을 위해 동적 수학 문제를 생성하기 위해 generateEquations를 사용합니다. 각 문제는 두 숫자, 연산자 및 객관식 옵션으로 구성됩니다:
function generateEquations() {
const equations = [];
for (let i = 1; i <= 30; i++) {
const num1 = Math.floor(Math.random() * (i > 10 ? 100 : 10));
const num2 = Math.floor(Math.random() * (i > 10 ? 100 : 10));
const operators = ["+", "-", "*"];
const operator = operators[Math.floor(Math.random() * operators.length)];
const result = eval(`${num1} ${operator} ${num2}`);
const options = generateOptions(result);
equations.push({ numbers: [num1, num2], options, operator, correctAns: result });
}
return equations;
}
객관식 옵션 생성
게임을 더 도전적으로 만들기 위해 generateOptions를 사용하여 올바른 답변과 임의의 부정확한 옵션을 섞은 가능한 답안 목록을 생성합니다.
function generateOptions(correctAnswer) {
const options = [correctAnswer];
while (options.length < 4) {
const wrongOption = correctAnswer + (Math.floor(Math.random() * 5) - Math.floor(Math.random() * 5));
if (!options.includes(wrongOption)) options.push(wrongOption);
}
return options.sort(() => Math.random() - 0.5);
}
함수 배포하기
함수를 배포하려면 Firebase CLI를 사용하세요:
firebase deploy --only functions
결론
이 튜토리얼에서는 Firebase Functions와 Firebase 실시간 데이터베이스를 활용하여 멀티플레이어 수학 퀴즈 게임을 만들었습니다. 다음을 다루었습니다:
- 기능: 게임의 주요 기능을 설명했습니다.
- 데이터베이스 스키마: 실시간 데이터베이스에서 사용되는 구조와 노드를 상세히 설명했습니다.
- Firebase 실시간 데이터베이스: Firebase의 실시간 데이터 동기화를 사용하는 이점을 강조했습니다.
- Firebase Functions: 서버리스 함수가 게임 로직과 매칭을 처리하는 방법을 논의했습니다.
- 주요 코드 컴포넌트: 코드베이스의 중요한 부분을 살펴보았습니다.
이 프레임워크는 더 복잡한 방정식, 인증 및 게임 이력 추적과 같은 추가 기능으로 확장할 수 있습니다. 전체 소스 코드는 이 링크에서 확인할 수 있습니다.
다음 단계
- UI 개발: React 또는 Flutter와 같은 프레임워크를 사용하여이 백엔드와 상호 작용하는 프론트 엔드를 구축합니다.
- 고급 기능: 사용자 인증 및 개인화 된 게임 이력 지원 추가.
- 확장성: 대규모 플레이어 및 세션을 처리하기 위한 확장 전략 탐색.
코드를 실험하고 필요에 맞게 수정하는 것에 자유롭게 도전하세요. 즐거운 코딩 되세요!