Effects

Particles

슬롯 게임에서 사용되는 파티클 이펙트 모음. 각 파티클은 독립 모듈로 사용 가능합니다.

📚 참고 자료 & 에셋 출처
Phaser 3 공식 문서 / 예시
Phaser CDN 에셋 (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
무료 에셋 (CC0)
핵심 패턴: Phaser 불꽃 이펙트는 flares 아틀라스의 white 프레임(128×128 Gaussian soft blob)에 color 배열 + colorEase + blendMode: 'ADD' 조합으로 만든다. 직접 텍스처를 그리는 것보다 훨씬 자연스럽다.
Effects

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)
Effects

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

Symbols

Config

Config

슬롯별 WizardSpecJSON — 위저드 UI 에서 작성한 spec 으로부터 런타임에 ISlotConfig 가 합성됩니다. 수학·레이아웃·사운드·에셋 정의를 한 곳에서 확인.

Spine

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: .atlasspineAtlas()로 로드
  • UI atlas: .atlas.txt — 동일하게 spineAtlas()로 로드, 확장자만 다름
  • skeleton: .jsonspineJson()으로 로드 (바이너리 .skelspineBinary())

플러그인 초기화

// 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.

Game View에서 재생됩니다 →
// 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을 호출하면 프레임 튐 발생 — 릴 정지 콜백 이후에 호출할 것

참고 자료

Animation

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/ 기준.

로딩 중…
Components

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버튼, 아바타, 헤더 등 사용자 인터페이스
EffectsLED, 파티클, 릴 이펙트 등 시각 효과
Managers캐비닛 레이아웃 등 레이아웃 관리자
Components

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에 항목 추가