Cocos Creator 미니게임 프로젝트 구조: Boot, 씬, 결과 화면 나누는 법

화면에 뜬 “scene not found” 오류를 바라보다가, 이 미니게임 프로젝트의 디렉터리 구조가 완전히 통제 불능이 되었다는 사실을 깨달았습니다.
3개월 전에는 자신만만하게 시작했습니다. 메뉴는 씬 하나, 게임 메인 화면도 씬 하나, 결과 화면도 또 하나. 꽤 합리적으로 들리죠? 그런데 실제 결과는 달랐습니다. 씬을 전환할 때 플레이어 점수가 알 수 없이 0으로 초기화되고, loading 애니메이션은 프레젠테이션처럼 끊기고, 최악의 경우 어떤 스테이지는 로딩에 5초가 걸렸습니다. 사용자는 게임 화면을 보기도 전에 절반이 떠나 버렸습니다.
그때 알았습니다. 미니게임 아키텍처는 scene 파일 몇 개를 대충 쌓아 올린다고 끝나는 문제가 아니었습니다. 이후 전체 프로젝트를 단일 씬 + 네 개 Layer 구조로 리팩터링했고, 로딩 시간은 5초에서 2초 이내로 줄었으며 씬 전환도 훨씬 부드러워졌습니다.
미니게임에 단일 씬 아키텍처를 권장하는 이유
솔직히 처음 미니게임을 만들 때는 “아키텍처”라는 말을 거의 생각하지 않았습니다. 메뉴, 게임, 결과 화면. 각 화면을 별도 scene 파일로 두면 단순하고 거칠지만 빠르게 만들 수 있을 것 같았습니다.
막상 실행해 보니 문제가 줄줄이 드러났습니다.
전환 비용. director.loadScene()을 호출할 때마다 엔진은 기존 씬을 파괴하고 새 씬을 만들며 리소스를 다시 로드합니다. 이 과정은 미니게임에서는 치명적입니다. 모바일 사용자 53%는 로딩이 3초를 넘는 페이지를 포기하는데, 다중 씬 전환의 대기 시간은 이 임계값을 쉽게 넘습니다.
상태 손실. 플레이어가 한 스테이지를 끝내고 점수 1200점을 얻은 뒤 결과 화면으로 넘어갔는데 데이터가 사라집니다. 왜 그럴까요? 씬 전환은 모든 노드를 파괴하기 때문입니다. 데이터를 전역 변수나 상주 노드에 저장하지 않았다면 그대로 없어집니다. 뒤에서 이 문제를 어떻게 처리하는지 설명하겠습니다.
애니메이션 중단. 메뉴의 멋진 logo 애니메이션이 아직 끝나기 전에 사용자가 시작 버튼을 누르면 씬이 전환됩니다. 애니메이션은 강제로 끊기고 화면 경험은 크게 나빠집니다.
단일 씬 아키텍처의 장점은 분명합니다. 모든 UI 계층이 같은 씬 안에 있고, 전환은 Layer의 active 속성을 바꾸는 일에 가깝습니다. 파괴와 재생성 비용이 없고, 상태는 자연스럽게 유지되며, 애니메이션도 끊기지 않습니다. 미니게임은 보통 화면 수가 많지 않으므로 이 구조만으로도 충분합니다. 물론 대형 게임은 다릅니다. 스테이지가 많고 리소스가 크다면 다중 씬과 분할 패키지 로딩이 필요합니다. 하지만 미니게임이라면 단일 씬으로 충분합니다.
프로젝트 디렉터리 구조 템플릿
좋습니다. 단일 씬 아키텍처를 왜 써야 하는지 알았으니 이제 프로젝트 디렉터리를 어떻게 정리할지 보겠습니다.
아래는 여러 번 시행착오를 겪은 뒤 정리한 템플릿입니다. Cocos Creator 3.x를 기준으로 합니다.
assets/
├── Scenes/
│ └── Main.scene # 유일한 메인 씬
├── Scripts/
│ ├── managers/
│ │ ├── GameManager.ts # 게임 상태와 데이터 저장
│ │ ├── UIManager.ts # Layer 전환 로직
│ │ └── AudioManager.ts # 사운드 관리
│ ├── layers/
│ │ ├── BootLayer.ts # 시작 화면 로직
│ │ ├── MenuLayer.ts # 메인 메뉴 로직
│ │ ├── GameLayer.ts # 게임 메인 화면
│ │ └── SettlementLayer.ts # 결과 화면
│ └── components/
│ ├── PlayerController.ts
│ └── EnemyAI.ts
├── Prefabs/
│ ├── UI/
│ │ ├── BootPanel.prefab # 시작 화면 UI 프리팹
│ │ ├── MenuPanel.prefab # 메뉴 UI
│ │ ├── SettlementPanel.prefab
│ ├── Game/
│ │ ├── Player.prefab
│ │ ├── Obstacle.prefab
│ └── Effects/
│ └── Explosion.prefab
├── Resources/
│ ├── textures/ # 동적으로 로드하는 이미지
│ ├── audio/ # 사운드 파일
│ └── fonts/
└── bundles/ # Asset Bundle 분할 패키지(WeChat 미니게임 최적화)
├── core/ # 핵심 리소스 패키지
└── levels/ # 스테이지 리소스 패키지
몇 가지 핵심 포인트가 있습니다.
Scenes 디렉터리에는 파일을 하나만 둡니다. Main.scene은 유일한 메인 씬이며, 그 안에는 Canvas, GameManager, 각 Layer 등의 노드가 들어갑니다. 이 디렉터리에 다른 scene을 계속 넣지 않는 편이 좋습니다.
managers에는 관리 클래스 스크립트를 둡니다. GameManager는 game.addPersistRootNode()로 상주 노드로 설정해 점수와 스테이지 진행도 같은 데이터를 저장합니다. UIManager는 Layer 간 전환 로직을 담당합니다.
layers 디렉터리에는 각 UI 계층의 로직 스크립트를 둡니다. 각 Layer는 Prefab 하나와 대응됩니다. Prefab은 Prefabs/UI 디렉터리에 두고, 스크립트를 Prefab에 붙인 다음 Main.scene의 Canvas 노드 아래로 끌어다 놓습니다.
bundles 디렉터리는 WeChat/Douyin 미니게임을 위한 것입니다. 미니게임에는 첫 패키지 용량 제한이 있으므로 4MB를 넘는 부분은 Asset Bundle로 분할 로드해야 합니다. 핵심 리소스는 core bundle에 넣고 스테이지 리소스는 필요할 때 로드합니다.
당신의 프로젝트 디렉터리도 이렇게 정리되어 있나요? 아니라면 한 번 조정해 볼 만합니다.
Boot 씬: 시작 화면을 어떻게 설계할까
먼저 숫자 하나를 보겠습니다. Cocos Creator 엔진 자체의 크기는 약 1.9MB입니다. 여기에 게임 리소스를 더하면 첫 화면 로딩 시간은 기본적으로 3~5초가 됩니다. 미니게임에서는 너무 긴 시간입니다. 사용자가 게임을 눌렀는데 검은 화면이나 빈 페이지를 3초 동안 보고 있으면 많은 사람이 바로 닫아 버립니다.
Boot Layer는 이 문제를 해결하기 위한 계층입니다. 역할은 단순합니다.
로딩 진행률 표시. 사용자가 게임이 멈춘 것이 아니라 로딩 중임을 알 수 있게 합니다. 진행률 막대나 퍼센트 숫자 정도면 충분합니다. 너무 화려하게 만들 필요는 없습니다. 이 시점에는 이미지 리소스도 아직 다 로드되지 않았기 때문입니다.
핵심 리소스 사전 로드. resources.preload()나 Asset Bundle의 loadBundle()을 사용해 곧 필요한 이미지와 오디오를 먼저 로드합니다.
브랜드 노출. logo를 배치하고 간단한 애니메이션을 더합니다. 예를 들어 logo가 확대되며 서서히 나타나는 효과만으로도 충분합니다. 동시에 브랜드를 자연스럽게 보여줄 수 있습니다.
시작 흐름은 대략 다음과 같습니다.
엔진 초기화 → Boot Layer 표시 → 핵심 리소스 사전 로드 → 로딩 완료 → MenuLayer로 전환
코드 예시입니다. BootLayer.ts입니다.
import { _decorator, Component, Node, resources, ProgressBar } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('BootLayer')
export class BootLayer extends Component {
@property(ProgressBar)
progressBar: ProgressBar | null = null;
start() {
// 핵심 리소스 사전 로드
this.preloadCoreAssets();
}
preloadCoreAssets() {
resources.preloadDir('textures', (err, items) => {
if (err) {
console.error('사전 로드 실패:', err);
return;
}
// 로딩 완료 후 메뉴로 전환
this.switchToMenu();
});
}
updateProgress(current: number, total: number) {
if (this.progressBar) {
this.progressBar.progress = current / total;
}
}
switchToMenu() {
// UIManager에 MenuLayer로 전환하라고 알림
// 코드는 뒤에서 보여줍니다
}
}
WeChat 미니게임 첫 패키지 최적화: 게임을 WeChat 미니게임 플랫폼에 올릴 예정이라면 첫 패키지 제한은 4MB입니다. 초과분은 Asset Bundle로 분할해야 합니다. Boot Layer에서 이런 bundle을 로드할 수 있습니다.
assetManager.loadBundle('levels', (err, bundle) => {
if (err) return;
console.log('스테이지 패키지 로딩 완료');
});
H5 플랫폼 커스텀 시작 화면: Cocos Creator는 build-templates 메커니즘을 제공하므로 H5 배포 후의 시작 화면을 직접 커스터마이즈할 수 있습니다. 프로젝트 루트에 build-templates/web-mobile 디렉터리를 만들고 커스텀 index.html을 넣으면 기본 검은색 시작 화면을 교체할 수 있습니다. loading 애니메이션이나 브랜드 이미지를 추가해도 좋습니다.
구체적인 방법은 공식 포럼 튜토리얼을 참고하세요. Creator | H5 커스텀 시작 화면.
Boot Layer를 설계했다면 다음은 전체 아키텍처의 핵심입니다. 네 개 Layer를 어떻게 전환할까요?
4계층 아키텍처 구현: 메뉴에서 결과 화면까지
가장 중요한 부분입니다. 전체 메인 씬의 구조는 다음과 같습니다.
Main.scene
├── Canvas (UI 컨테이너)
│ ├── BootLayer → 로딩 진행률, 브랜드 노출
│ ├── MenuLayer → 시작 화면, 스테이지 선택
│ ├── GameLayer → 게임 메인 화면
│ └── SettlementLayer → 결과 화면(점수, 시간, 계속하기/재시도)
├── GameManager (상주 노드)
│ └─ 저장: 점수, 스테이지 진행도, 플레이어 설정
└── AudioRoot (오디오 관리 노드)
보시다시피 네 개 Layer는 모두 같은 Canvas 아래에 있습니다. 전환할 때는 특정 Layer의 active 속성만 바꿉니다. 다른 Layer는 계속 존재하므로 상태가 사라지지 않고 애니메이션도 끊기지 않습니다.
Layer 전환의 핵심 로직은 UIManager에 둡니다.
// UIManager.ts
import { _decorator, Component, Node, tween, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('UIManager')
export class UIManager extends Component {
private layers: Map<string, Node> = new Map();
private currentLayer: string = 'BootLayer';
onLoad() {
// 모든 Layer 노드 수집
const canvas = this.node.getChildByName('Canvas');
if (!canvas) return;
canvas.children.forEach(child => {
if (child.name.endsWith('Layer')) {
this.layers.set(child.name, child);
child.active = false; // 처음에는 모두 숨김
}
});
// BootLayer 표시
this.switchLayer('BootLayer');
}
switchLayer(targetLayer: string) {
// 현재 계층 숨김
const current = this.layers.get(this.currentLayer);
if (current) {
current.active = false;
}
// 대상 계층 표시
const target = this.layers.get(targetLayer);
if (target) {
target.active = true;
// 페이드인 애니메이션 추가(선택)
this.playFadeIn(target);
}
this.currentLayer = targetLayer;
}
playFadeIn(node: Node) {
// 간단한 스케일 페이드인 애니메이션
node.setScale(new Vec3(0.9, 0.9, 1));
tween(node)
.to(0.2, { scale: new Vec3(1, 1, 1) })
.start();
}
}
핵심은 Map으로 모든 Layer 참조를 저장하는 것입니다. 이렇게 하면 전환할 때마다 getChildByName()으로 찾을 필요가 없어 효율이 더 좋습니다.
결과 화면으로 데이터 전달하기: 플레이어가 한 스테이지를 끝내고 점수 1200점, 플레이 시간 45초를 얻었다고 해 봅시다. 이 데이터는 결과 화면에 어떻게 전달할까요?
답은 GameManager입니다.
GameManager는 상주 노드이며 생명주기가 게임 전체에 걸쳐 유지됩니다. 플레이어가 GameLayer에서 스테이지를 끝내면 점수와 시간을 GameManager에 저장합니다. SettlementLayer로 전환될 때는 GameManager에서 데이터를 꺼내 표시합니다.
// GameManager.ts (간단한 버전)
import { _decorator, Component, game } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
public currentScore: number = 0;
public currentLevel: number = 1;
public playTime: number = 0;
onLoad() {
// 상주 노드로 설정
game.addPersistRootNode(this.node);
}
// GameLayer가 이 메서드를 호출해 데이터 저장
saveResult(score: number, time: number) {
this.currentScore = score;
this.playTime = time;
}
// SettlementLayer가 이 메서드를 호출해 데이터 읽기
getResult() {
return {
score: this.currentScore,
time: this.playTime
};
}
}
SettlementLayer가 표시될 때는 GameManager의 데이터를 바로 읽습니다.
// SettlementLayer.ts
onEnable() {
const gameManager = find('GameManager')?.getComponent(GameManager);
if (!gameManager) return;
const result = gameManager.getResult();
this.scoreLabel.string = `점수: ${result.score}`;
this.timeLabel.string = `소요 시간: ${result.time}초`;
}
이렇게 하면 데이터 전달 문제가 해결됩니다. 복잡한 이벤트 시스템이 꼭 필요한 것은 아닙니다. GameManager가 중심을 잡아 주면 충분합니다.
상주 노드: Layer 간 데이터 전달
위에서 이미 상주 노드를 언급했지만, 여기서 조금 더 자세히 보겠습니다.
game.addPersistRootNode()는 Cocos Creator가 공식 제공하는 API입니다. 특정 노드가 씬 전환 시 파괴되지 않게 하는 역할을 합니다. 그런데 우리는 단일 씬 아키텍처를 쓰고 있으니 씬이 전환되지 않습니다. 그렇다면 왜 상주 노드가 필요할까요?
사실 상주 노드는 단일 씬 아키텍처에서도 유용합니다. 전역 데이터 센터와 이벤트 센터로 사용할 수 있기 때문입니다.
GameManager의 역할:
플레이어 데이터 저장. 점수, 스테이지 진행도, 최고 기록, 잠금 해제된 스테이지 목록을 보관합니다.
설정 데이터 저장. 효과음 켜기/끄기, 음악 켜기/끄기, 언어 선택을 보관합니다.
전역 이벤트 제공. 예를 들어 “스테이지 완료” 이벤트를 두면 각 Layer가 이 이벤트를 감지해 반응할 수 있습니다.
주의할 점:
상주 노드는 Canvas 아래에 두면 안 됩니다. Canvas는 UI 컨테이너이며 Layer의 active 상태 변화에 따라 조작될 수 있습니다. 상주 노드는 독립적으로 씬 루트 계층에 두어야 합니다.
상주 노드는 직접 만들어야 합니다. Main.scene에 빈 노드를 만들고 이름을 “GameManager”로 지정한 뒤 GameManager 스크립트를 붙입니다. 그리고 스크립트의 onLoad()에서 game.addPersistRootNode(this.node)를 호출합니다.
상주 노드는 신중하게 사용해야 합니다. 모든 것을 다 집어넣지 말고, 정말 화면 간 공유가 필요한 데이터만 저장하세요. 각 Layer만의 UI 상태, 예를 들어 버튼 표시 여부 같은 것은 GameManager에 넣지 말고 해당 Layer가 직접 관리하게 두는 편이 낫습니다.
완성된 데이터 구조 예시는 다음과 같습니다.
// GameManager.ts (완성 버전)
interface PlayerData {
highestScore: number;
unlockedLevels: number[];
currentLevel: number;
}
interface GameSettings {
soundEnabled: boolean;
musicEnabled: boolean;
language: 'zh' | 'en';
}
@ccclass('GameManager')
export class GameManager extends Component {
private playerData: PlayerData = {
highestScore: 0,
unlockedLevels: [1],
currentLevel: 1
};
private settings: GameSettings = {
soundEnabled: true,
musicEnabled: true,
language: 'zh'
};
onLoad() {
game.addPersistRootNode(this.node);
// 로컬 저장소에서 데이터 읽기
this.loadData();
}
loadData() {
const saved = localStorage.getItem('playerData');
if (saved) {
this.playerData = JSON.parse(saved);
}
}
saveData() {
localStorage.setItem('playerData', JSON.stringify(this.playerData));
}
// getters와 setters...
}
여기에는 localStorage 읽기와 쓰기도 추가해 플레이어 데이터를 로컬에 영속화했습니다. 미니게임 플랫폼은 대부분 localStorage를 지원합니다. WeChat 미니게임에서는 wx.setStorageSync를 사용하지만, 래핑된 localStorage도 사용할 수 있습니다.
정리
지금까지 꽤 많은 이야기를 했습니다. 마지막으로 자신의 프로젝트와 대조해 볼 수 있는 아키텍처 의사결정 체크리스트를 정리합니다.
미니게임을 만들고 있다면:
- 단일 씬 아키텍처를 사용합니다. 즉, 하나의 Main.scene을 둡니다.
- 모든 UI 계층은 Canvas 아래에 두고 Layer로 전환합니다.
- BootLayer는 로딩 진행률과 사전 로드를 담당합니다.
- GameManager는 상주 노드로 두고 데이터를 저장합니다.
- 디렉터리 구조는 assets/Scripts/managers/layers/Prefabs 기준으로 정리합니다.
대형 게임을 만들고 있다면:
- 다중 씬은 필요합니다. 스테이지가 많고 리소스가 크기 때문입니다.
- 다만 UI 계층에는 단일 씬 + Layer의 사고방식을 적용할 수 있습니다.
- Asset Bundle로 분할 패키지 로딩을 사용합니다.
이 구조는 제가 여러 미니게임 프로젝트에서 사용하며 시행착오를 거쳐 다듬은 방식이라 꽤 성숙한 편이라고 봅니다. 궁금한 점이 있다면 댓글을 남기거나 GitHub에서 예시 프로젝트를 찾아 비교해 보세요.
다음 글에서는 Layer 전환 애니메이션을 다뤄 보려고 합니다. 페이드인/페이드아웃, 슬라이드 전환, scale 확대와 축소 같은 효과로 전환을 더 부드럽게 만드는 방법입니다.
Cocos Creator 미니게임 단일 씬 아키텍처 구축하기
Boot, 메인 화면, 결과 화면으로 구성된 미니게임 프로젝트 아키텍처를 처음부터 구축합니다.
⏱️ Estimated time: 30 min
- 1
Step 1: 프로젝트 디렉터리 구조 만들기
assets 아래에 Scenes, Scripts/managers, Scripts/layers, Scripts/components, Prefabs/UI, Prefabs/Game, Resources, bundles 등의 디렉터리를 만들고 역할별로 파일을 분리합니다. - 2
Step 2: Main.scene과 GameManager 만들기
유일한 메인 씬 Main.scene을 만들고 루트 계층에 GameManager 노드를 생성해 스크립트를 붙입니다. onLoad()에서 game.addPersistRootNode(this.node)를 호출해 상주 노드로 설정합니다. - 3
Step 3: 네 개의 Layer 프리팹 만들기
BootLayer, MenuLayer, GameLayer, SettlementLayer 네 개의 Prefab을 만들고 각 Prefab에 대응하는 로직 스크립트를 붙인 뒤 Prefabs/UI 디렉터리에 모아 둡니다. - 4
Step 4: UIManager 전환 로직 구현하기
UIManager.ts에서 Map으로 모든 Layer 참조를 저장하고 switchLayer(targetLayer) 메서드를 구현합니다. active 속성을 제어해 화면을 전환하고 페이드인 애니메이션을 더해 경험을 개선합니다. - 5
Step 5: Boot Layer 로딩 흐름 구현하기
BootLayer.ts에서 resources.preloadDir()를 호출해 핵심 리소스를 사전 로드하고 진행률 막대를 갱신합니다. 로딩이 끝나면 UIManager를 호출해 MenuLayer로 전환합니다.
FAQ
미니게임에 단일 씬 아키텍처를 권장하는 이유는 무엇인가요?
씬 전환 시 데이터는 어떻게 전달하나요?
상주 노드는 어느 계층에 두어야 하나요?
Boot Layer의 역할은 무엇인가요?
WeChat 미니게임 첫 패키지가 4MB를 넘으면 어떻게 하나요?
3분 읽기 · 게시일: 2026년 5월 19일 · 수정일: 2026년 10월 21일
AI 기반 Cocos 미니게임 개발
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
인디 개발자의 미니게임 개발: 시스템을 쌓기 전에 플레이부터 검증하기(MVP 실전 가이드)
인디 개발자가 미니게임을 만들 때 가장 쉽게 빠지는 함정은 시스템을 다 쌓고 나서야 재미가 없다는 사실을 깨닫는 것입니다. 이 글은 핵심 Loop 식별부터 프로토타입 테스트까지, 최소 비용으로 게임을 계속 만들 가치가 있는지 확인하는 MVP 검증 경로를 설명합니다.
21편 중 2편
다음
미니게임 상태 머신 설계: 홈 화면부터 전투, 정산까지 이어지는 전체 흐름
미니게임을 위한 3계층 상태 머신 아키텍처를 게임 흐름 계층부터 전투 세부 계층까지 설명합니다. TypeScript 인터페이스 구현, Cocos Creator 실전 사례, 상태 드리프트와 동시성 충돌 같은 문제 해결책을 함께 다룹니다.
21편 중 4편



댓글
GitHub로 로그인하여 댓글을 남기세요