Particles
슬롯 게임에서 사용되는 파티클 이펙트 모음. 각 파티클은 독립 모듈로 사용 가능합니다.
- Fire Effects 예시 —
flares아틀라스 + ADD 블렌드로 불꽃 3종 구현 - Particle Emitter 예시 전체 목록
- ParticleEmitter API 레퍼런스
cdn.phaserfiles.com/v385/assets/particles/)- 📁 ingame/ —
ParticleManager에서 실제 로드하는 에셋 flares.png+flares.json— Phaser 공식 soft-blob 아틀라스.white프레임이 fire·wisp에 사용됨- 필요 시 CDN에서 다운로드 가능한 추가 에셋
fire1~3.png,flame1~2.png— 실제 불꽃 프레임 텍스처muzzleflash3.png— 섬광·폭발 버스트용 텍스처smoke-puff.png,rising-smoke.png,white-smoke.png— 연기 효과sparkle1.png,star.png— 반짝이·별 형태leaf1~3.png— 나뭇잎 낙하 (petal_fall 대체 가능)snowflake.png— 눈송이 실제 이미지soft1~3.png,orb.png,gold.png— 범용 soft blob·마법 구체·금빛white.png,white-flare.png,blue-flare.png— 범용 flare
- Kenney Particle Pack — 80+ 스프라이트, fire·smoke·magic·hearts·sparks·electricity. CC0 라이선스
- OpenGameArt 미러 — 동일 팩, 직접 다운로드 가능
flares 아틀라스의 white 프레임(128×128 Gaussian soft blob)에 color 배열 + colorEase + blendMode: 'ADD' 조합으로 만든다. 직접 텍스처를 그리는 것보다 훨씬 자연스럽다.Win Effects
당첨 등급별 연출 시스템. Small Win부터 Grand Win까지 6단계 + Progressive 모드.
Win 연출 시스템 요약
- Progressive ON: Big → Super → Mega → Epic → Grand 순차 강화
- Progressive OFF: 최종 등급만 즉시 표시 (Static 모드)
- 파티클: 등급 상승 → 코인 분수 수 증가, 스파클/컨페티 강도 증가
- 코드:
WinPopupManager.showWinPopup(amount, bet, delay, reason, options)
Shaders
Phaser 3 PostFX Pipeline 기반 셰이더 모음. 모두 src/effects/ShaderPipelines.ts에 구현되어 있습니다.
공통 사용 방법
- 등록 (최초 1회):
registerAllPipelines(this.game)— Boot/Preload Scene에서 호출 - 카메라 전체 적용:
this.cameras.main.setPostPipeline('ShaderName') - 게임오브젝트에 적용:
gameObject.setPostPipeline('ShaderName') - 제거:
this.cameras.main.resetPostPipeline()/gameObject.resetPostPipeline() - Animated 셰이더는
uTime을 매 프레임 증가시켜야 합니다 (코드 예시 참고)
GameObject별 PostFX 지원 현황
| Image / Sprite | ✅ 완전 지원 — img.setPostPipeline('...') |
| Container | ✅ 지원 — 컨테이너 전체 렌더 결과에 PostFX 적용 (자식 오브젝트 포함) |
| RenderTexture | ✅ 지원 — Spine처럼 직접 지원 안 되는 오브젝트를 그린 뒤 PostFX 적용 |
| ParticleEmitter | ✅ 지원. 단, 파티클 수가 많으면 성능 주의 |
| Spine (SpineGameObject) | ⚠️ 직접 적용 불가 — RenderTexture에 draw한 뒤 RT에 PostFX 적용하는 우회 필요 |
| Text / BitmapText / Graphics | ✅ 지원 |
⚠️ 주의사항
- Canvas 렌더러에서는 동작하지 않습니다 — WebGL 필수
- Win 연출에 PreFX/PostFX Glow는 사용하지 않습니다 (성능 이슈)
- 여러 PostFX를 동시에 적용하면 성능이 저하될 수 있습니다 — 필요한 것만 활성화
화면 효과 (Camera PostFX)
카메라 전체에 적용하는 셰이더. 씬 분위기·테마 전환·인트로 연출에 사용합니다. Object Preview에서 단일 오브젝트 적용도 확인 가능합니다.
오브젝트 효과 (Object PostFX)
특정 심볼·이미지·RenderTexture에 개별로 적용하는 셰이더. 당첨 심볼 강조, 소환/소멸 연출, 테마 오브젝트 특수 처리에 사용합니다.
Phaser 3.60+ Built-in FX
별도 셰이더 코드 없이 gameObject.preFX.addXxx() / postFX.addXxx() 한 줄로 사용하는 내장 이펙트 14종 중 슬롯에 실용적인 8종.
preFX vs postFX — 성능 차이
| preFX | 오브젝트 크기 버퍼에서 동작 → 빠름. Image / Sprite / Text / RenderTexture에서만 사용 가능 |
| postFX | 풀스크린 버퍼에서 동작 → preFX보다 무거움. 모든 GameObject + Camera에서 사용 가능 |
| Glow 특수 주의 | preFX/postFX 모두 내부적으로 반복 blur pass → 비쌈. Win 연출에 사용 금지. Shine 또는 Bloom(steps≤2)으로 대체할 것 |
API 패턴
// 적용 const effect = gameObject.preFX.addShine(speed, lineWidth, gradient); const effect = gameObject.postFX.addBlur(quality, x, y, strength); const effect = this.cameras.main.postFX.addWipe(wipeWidth, direction, axis); // 일시 비활성화 / 재활성화 effect.setActive(false); effect.setActive(true); // 제거 (단일) gameObject.preFX.remove(effect); // 전체 제거 gameObject.preFX.clear(); gameObject.postFX.clear();
Symbols
Config
슬롯별 WizardSpecJSON — 위저드 UI 에서 작성한 spec 으로부터 런타임에 ISlotConfig 가 합성됩니다. 수학·레이아웃·사운드·에셋 정의를 한 곳에서 확인.
Spine
Phaser 3 + spine-phaser 플러그인으로 Spine 2D 애니메이션을 로드·재생하는 방법을 정리합니다. 심볼 Spine과 UI Spine(팝업 캐릭터) 두 가지 패턴을 다룹니다.
개요
이 프로젝트에서 Spine이 쓰이는 두 가지 맥락과 각각의 에셋 구조.
에셋 구성
| 심볼 Spine | 공유 atlas 1개 + 심볼별 skeleton JSON N개assets/games/{id}/symbols/Symbols.atlas & symbol_01_xxx.json |
| UI Spine | 전용 atlas + skeleton JSON 1쌍assets/spine/olivia/popup_olivia.atlas.txt & Popup_Olivia.json |
파일 확장자 주의
- 심볼 atlas:
.atlas—spineAtlas()로 로드 - UI atlas:
.atlas.txt— 동일하게spineAtlas()로 로드, 확장자만 다름 - skeleton:
.json—spineJson()으로 로드 (바이너리.skel은spineBinary())
플러그인 초기화
// index.ts (또는 Phaser.Game config)
import { SpinePlugin } from '@esotericsoftware/spine-phaser';
const config: Phaser.Types.Core.GameConfig = {
plugins: {
scene: [{ key: 'SpinePlugin', plugin: SpinePlugin, mapping: 'spine' }],
},
};
심볼 Spine (게임 내)
릴에 사용되는 심볼 Spine. 공유 atlas + 심볼별 JSON 구조. 라이브 애니메이션 프리뷰는 Symbols 탭에서 확인하세요.
// preload() 안에서 호출
// 공유 atlas 1개 + 심볼별 skeleton JSON N개
this.load.spineAtlas(
'jo-atlas', // 로드 키
'assets/games/jo/symbols/Symbols.atlas', // atlas 파일 경로
true // premultipliedAlpha (pma:true 일 때)
);
// 심볼 이름은 ISlotConfig.spine.symbols 배열 순서와 동일
this.load.spineJson('jo-sym-orbs', 'assets/games/jo/symbols/symbol_01_orbs.json');
this.load.spineJson('jo-sym-scatter', 'assets/games/jo/symbols/symbol_02_scatter.json');
this.load.spineJson('jo-sym-wild', 'assets/games/jo/symbols/symbol_03_wild.json');
// ... 나머지 심볼도 동일 패턴
// ⚠️ 실제로는 PreloadScene에서 ISlotConfig를 순회해 자동 로드
// src/scenes/PreloadScene.ts → loadSpineAssets(config)
// create() 또는 씬 내 아무 곳에서 const sym = (this.add as any).spine( x, y, // 배치 위치 (월드 좌표) 'jo-sym-orbs', // skeleton JSON key (spineJson으로 로드한 key) 'jo-atlas' // atlas key (spineAtlas로 로드한 key, 공유) ); sym.setScale(2.5); sym.setDepth(10); // Container에 추가하는 경우 container.add(sym); // ⚠️ spine() 팩토리 반환 타입이 any — TypeScript에서 캐스팅 필요 // const sym = (this.add as any).spine(...) as SpineGameObject;
// 공통 애니메이션 이름 (심볼마다 동일한 규칙)
// Idle — 대기 루프 (loop: true)
// Win — 당첨 연출 (loop: false)
// Spin — 릴 회전 중 (loop: true)
// NotWin — 비당첨 페이드 (loop: false, 일부 심볼만 존재)
// 단일 재생
sym.animationState.setAnimation(
0, // track index (보통 0)
'Idle', // 애니메이션 이름
true // loop
);
// 체이닝 — Win 후 Idle로 복귀
sym.animationState.setAnimation(0, 'Win', false);
sym.animationState.addAnimation(0, 'Idle', true, 0); // delay: 0
// 이벤트 콜백 (Win 끝난 시점 감지)
sym.animationState.addListener({
complete: (entry) => {
if (entry.animation?.name === 'Win') {
// Win 연출 완료
}
},
});
// src/config/JOConfig.ts — ISlotConfig.spine
spine: {
assetFolder: 'jo', // assets/games/jo/symbols/ 의 'jo' 부분
atlasFile: 'Symbols', // Symbols.atlas (확장자 제외)
symbols: [ // 배열 인덱스 = 서버 심볼 번호 순서
'orbs', // [0] → J-0 (symbol_01_orbs.json의 '01_orbs' 부분)
'scatter', // [1] → S-0
'wild', // [2] → W-0
'red7', // [3] → H-2
'blue7', // [4] → H-1
'3bar', // [5] → M-3
'2bar', // [6] → M-2
'1bar', // [7] → M-1
]
}
// symbolCodeMap의 서버코드 → 인덱스 변환:
// MathEngine.getSymbolIndex('J-0') === 0 → symbols[0] = 'orbs'
UI Spine — Olivia 팝업 캐릭터
게임 UI에 등장하는 캐릭터 애니메이션. 심볼 Spine과 에셋 구조가 같지만 atlas 확장자가 .atlas.txt.
// preload() 안에서 호출 // atlas 파일 확장자가 .atlas.txt 임에 주의 this.load.spineAtlas( 'olivia-atlas', // 로드 키 'assets/spine/olivia/popup_olivia.atlas.txt', // .atlas.txt 확장자 true // pma:true (atlas 파일 내 명시) ); this.load.spineJson( 'olivia', // 로드 키 'assets/spine/olivia/Popup_Olivia.json' // skeleton JSON );
// create() 안에서 const olivia = (this.add as any).spine( this.scale.width / 2, // 화면 중앙 X this.scale.height, // 화면 하단 기준 Y (캐릭터 발 위치) 'olivia', // skeleton key 'olivia-atlas' // atlas key ); olivia.setScale(0.8); olivia.setDepth(100); // UI 최상단 // 팝업 컨테이너에 포함시키는 경우 popupContainer.add(olivia);
// Popup_Olivia.json 내 애니메이션 이름 확인 방법:
// Spine Editor에서 열거나, JSON 파일의 "animations" 키를 조회
// 예시 — 일반적인 캐릭터 팝업 패턴
olivia.animationState.setAnimation(0, 'Idle', true);
// 팝업 등장 → Idle 루프
olivia.animationState.setAnimation(0, 'Appear', false);
olivia.animationState.addAnimation(0, 'Idle', true, 0);
// 특정 이벤트 감지 (Spine 이벤트 리스너)
olivia.animationState.addListener({
event: (entry, event) => {
if (event.data.name === 'sfx_appear') {
// 사운드 트리거 등
}
},
});
// popup_olivia.atlas.txt 구조 (일부) // atlas 파일 자체는 일반 텍스트 — 스프라이트 이름·좌표·크기 목록 popup_olivia.png ← 텍스처 파일명 (같은 폴더에 위치) size:1024,1024 filter:Linear,Linear pma:true ← premultipliedAlpha — spineAtlas() 3번째 인자 true 필수 LD_Line_Head ← 스프라이트 이름 (Spine 내부 슬롯명) bounds:467,734,240,284 ← x, y, width, height LD_Line_Body_Upper bounds:193,690,268,328 // ... 총 46개 부위 // ⚠️ .atlas.txt 와 .atlas 는 내용 형식이 동일 // 확장자만 다를 뿐 spineAtlas() 로드 방식은 완전히 동일
팁 & 주의사항
자주 겪는 문제
| 흰색 테두리 / 색 번짐 | premultipliedAlpha 미설정. atlas에 pma:true이면 반드시 spineAtlas(..., true) |
| 텍스처 로드 실패 | atlas 파일과 PNG가 동일 폴더에 있어야 함. atlas 내 첫 줄 파일명과 실제 PNG명이 일치해야 함 |
| skeleton key 충돌 | 동일 씬에서 같은 atlas를 공유하더라도 skeleton key는 고유해야 함 |
this.add.spine TypeScript 오류 |
(this.add as any).spine(...) 패턴 사용. 반환 타입은 SpineGameObject로 캐스팅 |
| 스핀 중 Win 애니메이션 깜빡임 | 릴 회전 중(Spin) setAnimation을 호출하면 프레임 튐 발생 — 릴 정지 콜백 이후에 호출할 것 |
참고 자료
- spine-phaser 공식 문서
- Spine Runtime API 레퍼런스
- 이 프로젝트 실제 사용:
src/scenes/PreloadScene.ts→loadSpineAssets() - 심볼 생성·교체:
src/engine/Reel.ts→createSpineSymbol()
Animation
Tween 기초
이동 · 회전 · 크기 · 투명도 · 색상 — 핵심 속성. 각 카드에서 라이브 데모를 확인하고 코드를 복사하세요.
Easing
대표 Easing 함수 비교. 같은 거리를 각각의 커브로 이동합니다. 전체 30종은 easings.net/ko를 참고하세요.
참고 자료
- easings.net/ko — 30종 이징 함수 시각화 + CSS cubic-bezier 값 치트 시트
- Bounce · Elastic은 CSS
cubic-bezier()로 표현 불가 →@keyframes직접 구현 필요 - Phaser 3 사용법:
ease: 'Cubic.easeOut'/ease: 'Back.easeOut'등 문자열 지정 - 커스텀 커브:
ease: [x1, y1, x2, y2]배열로 cubic-bezier 직접 전달 가능
게임 패턴
슬롯 게임에서 자주 쓰이는 합성 애니메이션 패턴. TweenHelper 유틸리티로 한 줄 호출.
TweenHelper — 공통 유틸리티
- 모든 패턴은
src/utils/TweenHelper.ts에 정의된 정적 메서드 scene: Phaser.Scene+target: Phaser.GameObjects.GameObject를 첫 두 인자로 받음- 반환값은
Phaser.Tweens.Tween— 외부에서.stop()/.resume()가능 - destroy() 시 자동으로 트윈도 정리됨 (Phaser 내부 처리)
Images
프로젝트 내 모든 이미지 리소스 목록. public/assets/ 기준.
Sounds
프로젝트 내 모든 사운드 리소스 목록. public/assets/ 기준.
UI Elements
IDebugInspectable을 구현하여 UIElementRegistry에 등록된 모든 UI 매니저/컨트롤러 목록. 각 카드의 섹션·프로퍼티 정보는 인게임 레이아웃 인스펙터 패널과 동일한 구조입니다.
IDebugInspectable 아키텍처
- 자기 등록: 각 매니저가 생성 시
UIElementRegistry.getInstance().register(this)호출 - SectionDescriptor: 접이식 섹션 하나 — name, color, properties[]
- PropertyDescriptor: 슬라이더 하나 — type, label, getValue(), setValue(), range, step
- 소비자: LayoutInspectorPanel (인게임), Catalog (이 페이지)
레이어 분류
| Engine | 릴 엔진, Spine 렌더링 등 코어 시스템 |
| UI | 버튼, 아바타, 헤더 등 사용자 인터페이스 |
| Effects | LED, 파티클, 릴 이펙트 등 시각 효과 |
| Managers | 캐비닛 레이아웃 등 레이아웃 관리자 |
Mini Games
슬롯에 바인딩 가능한 미니게임 목록. MiniGameRegistry에 등록된 게임만 표시됩니다. ISlotConfig.miniGame으로 바인딩하세요.
바인딩 방법
- miniGameId: 등록된 미니게임 ID를 Config에 지정
- trigger.type:
SYMBOL/WIN_TIER/RANDOM/SERVER - overrides: 이 슬롯에서만 적용할 옵션 덮어쓰기
새 미니게임 추가: src/minigame/ 폴더에 Scene 구현 → MiniGameRegistry.register() 호출 → catalog-minigame-data.ts에 항목 추가