BurningTimesAi/프로젝트/EerieVillage/개발/spec/맵_시스템_설계_v1.md

62 KiB
Raw Blame History

type: 설계_문서 scope: 맵_시스템 (절차 생성 + 시각 시뮬레이터 + 리소스 카탈로그) author: 개발팀장 (dev-team-lead) date: 2026-05-18 version: v1.0 (BT13-Dev-Map Phase 1 — 단일 SOT) project: EerieVillage (기묘한 고을 : 조선퇴마뎐) phase: BT13-Dev-Map Phase 1 (개발팀장 Opus 설계) data_source: | BT5-Dev 인수인계 2026-05-07·2026-05-08 (발판 시스템 BT47·BT75·몬스터 BT76~BT109)· E:/EerieVillage/Assets/Scripts/Mechanics/{KinematicObject,PlayerController,EnemyController,GameOptimizer,SpawnPoint,MonsterRandomizer}.cs· E:/EerieVillage/Assets/Scripts/Model/PlatformerModel.cs· E:/EerieVillage/Assets/Prefabs/{Player,Enemy,Enemy_M001~M006}.prefab· E:/EerieVillage/Assets/Tiles/{TileFloating*,TileGround*,cloud,fence,ShortBuilding,TallBuilding,MidgroundFiller}.asset· E:/EerieVillage/ProjectSettings/Physics2DSettings.asset· 프로젝트/EerieVillage/기획/05_스테이지_구조_초안.md v0.1· 프로젝트/EerieVillage/기획/level/01_스테이지_구조.md v0.1· 프로젝트/EerieVillage/개발/spec/스킬_시스템_설계_v1.md v1.0 status: Phase 1 설계 완료 — Phase 2 코드 구현 분할 권고 + PD 결정 대기 안건 11건 (§15 정합·2026-05-18 PM 정정·dev-team-lead 자기 검증 누락 보완)

맵 시스템 설계 v1 (BT13-Dev-Map)

§1. 요구 분석 + PD 결정 4건 정합 확인

1-1. PD 핵심 요구 (2026-05-18 원문 7항)

"이제 나는 우리 게임의 맵을 제작하고 싶어.

  • 일반적인 플랫포머 게임의 맵 제작 방식을 고려해 맵을 만들고 싶어.
  • 하지만 맵의 길이만 지정하면, 랜덤으로 자동으로 생성하는 형태의 맵 시스템을 만들고 싶어.
  • 맵에는 자동으로 몬스터가 등장하게 될 생성 포인트(거점)이 배치되어야하고, 플레이어가 시작하는 스타트 포인트도 존재해야 해.
  • 몬스터는 항상 생성 포인트 주변에서 생성되며, 현재 만들어진 몬스터 이동 패턴을 활용해서 동작하기를 바래.
  • 시뮬레이션을 시각적인 정보로 미리 만들어질 맵의 구조를 보고 결정할 수 있는 형태면 좋겠어.
  • 사용할 맵 리소스를 자유롭게 추가할 수 있도록 리소스를 등록하는 형태의 기능이 있으면 좋겠어.
  • 게임에 등장하는 발판(충돌 영역 포함)의 배치 등이 플레이어가 점프로 이동 가능한 높이나 거리에 맞게 설계되면 좋겠어. (플랫포머 게임의 자연스러운 레벨디자인)
  • 바닥의 길은 사용자가 일일이 수정하지 않아도 충돌 영역이 자연스럽게 적용되면 좋겠어."

1-2. PD 추가 결정 4건 (PM 사전 명세 안건 답변)

# 안건 PD 결정 본 설계 반영
1 시각 시뮬레이터 Unity Editor Window 1순위 · 어려우면 Scene 미리보기 fallback §10 — EditorWindow 채택 (실측 결과 1순위 가능)
2 맵 리소스 단위 양쪽 다 — 배경=Tile, 일부 기믹=Prefab §5 — Tile/Prefab 이원 분리
3 랜덤성 매번 완전 랜덤 (Seed 입력 X) §4 — UnityEngine.Random 매 생성 새 시드
4 거점 밀도 맵 길이·비율을 옵션으로 미리 지정 → 옵션 값 동작 §8 — MapGenerationConfig.SpawnDensity 0.0~1.0

1-3. 헌법 제1원칙 정합

  • AI 에이전트 활용 — 개발팀장 Opus 설계, 클라이언트팀 Sonnet 구현 (C49)
  • 경험 축적 — BT5-Dev 발판 시스템·BT12 스킬 시스템 자산 전부 계승
  • 허위 보고 금지 — §2 사전 실측 영역에서 모든 추정 X·Read 결과만 기재
  • 조직 단위 운영 — 본 단계 Phase 1 단독·Phase 2 분할 권고는 §13
  • 세션 연속성 — 본 설계서 = 단일 SOT, Phase 2 어떤 세션에서 진입해도 정합

§2. 사전 실측 결과 (C39-10 의무 — 위반 #1~#7 재발 차단)

2-1. KinematicObject (E:/EerieVillage/Assets/Scripts/Mechanics/KinematicObject.cs)

항목 실측값 본 설계 활용
minGroundNormalY 0.65f 발판 경사 한계 — 본 설계 평탄 발판 고정
gravityModifier public 1f (Player/Enemy prefab) 점프 물리 식 계산 입력값
Bounce(float) velocity.y 직접 set 본 설계 미사용
Teleport(Vector3) body.position + velocity *=0 본 설계 Player Spawn 시 활용
body.Cast(contactFilter) hitBuffer[16] · contactFilter Layer mask 적용 Drop-Through 발판 통과 메커니즘 SOT
velocity.x = targetVelocity.x FixedUpdate 매 frame 덮어씀 직접 set 1 frame만 효과
ignoreCollisions (2026-05-15) 날으는 몬스터용·body.Cast skip 본 설계 무관 (M002 박쥐 영역)

2-2. PlayerController (E:/EerieVillage/Assets/Scripts/Mechanics/PlayerController.cs)

항목 실측값
maxSpeed 3 (Player.prefab line 63) — BT12 Phase 2-D 영역 7→3 정정
jumpTakeOffSpeed 7 (Player.prefab line 64)
JUMP_THROUGH_LAYER const 16
JUMP_ASCENT_DURATION const 0.4f (점프 ascending mask OFF)
DROP_THROUGH_DURATION const 0.3f (Down+Jump Drop-Through mask OFF)
UpdateContactFilterForDropThrough footHit Raycast 3점(좌·중·우)·nearApex+horizontalIntent 영역 강제 Drop-Through
Awake() 자동 부착 PlayerInvulnerabilityFlash·ResurrectPromptUI·PlayerProgression·PlayerSkillInventory·SkillInventoryHUD

2-3. PlatformerModel (E:/EerieVillage/Assets/Scripts/Model/PlatformerModel.cs)

항목 실측값
jumpModifier 1.5f
jumpDeceleration 0.5f (release 시 velocity.y × 0.5)

2-4. Physics2D (E:/EerieVillage/ProjectSettings/Physics2DSettings.asset)

항목 실측값
m_Gravity.y -9.81f
m_VelocityIterations / PositionIterations 8 / 3

2-5. 점프 도달 한계 산출 (실측 수치 기반)

PlayerController.ComputeVelocity 영역:

velocity.y = jumpTakeOffSpeed × model.jumpModifier = 7 × 1.5 = 10.5 m/s

KinematicObject.FixedUpdate 영역 gravity 적용:

velocity.y -= 9.81 × gravityModifier × dt = 9.81 × 1 × dt
산출 공식
최대 점프 높이 (full hold) H_max = v0² / (2g) = 10.5² / (2 × 9.81) 5.62m
점프 정점 도달 시간 t_up = v0 / g = 10.5 / 9.81 1.07s
점프 왕복 시간 (착지) t_round = 2 × t_up 2.14s
점프 중 수평 최대 거리 D_max = maxSpeed × t_round = 3 × 2.14 6.42m
Stop Jump (release) 시 ascending 절반 cap v_after = 0.5 × v_now → H_partial ≈ H_max × 0.25 ~1.4m (조기 release 영역)
안전 점프 거리 (90% margin) D_max × 0.9 ~5.78m
안전 점프 높이 (90% margin) H_max × 0.9 ~5.06m

본 설계 발판 배치 룰 (§6)에 이 수치 직접 적용.

2-6. EnemyController (E:/EerieVillage/Assets/Scripts/Mechanics/EnemyController.cs)

항목 실측값 본 설계 활용
patrolMinRange / patrolMaxRange 5 / 10 (Enemy.prefab) — 본 EC.cs default 50/75는 prefab override됨 Spawn Point 영역 좌우 ±10m patrol 영역 확보 의무
cliffCheckDistance / cliffCheckDepth 1.0 / 2.0 발판 끝단 cliff 검출 — Spawn 발판 폭 >2m 필수
groundLayerMask (1<<0) | (1<<16) — Layer 0 Level + Layer 16 AutoForeground Spawn 발판 Layer 0 또는 16
fallThreshold 1.0f → startY - 1.0 미만 시 시작 위치 텔레포트 떨어진 Enemy 자동 복귀 (BT102)
IsFlying controller name "M002" 매칭 시 true · cliffCheck skip 본 설계 M002는 공중 spawn
_isInitialized Start 시점 MeasureSafeWalkDistance 완료 후 true Enemy spawn은 Scene 로드 후 1 frame 대기 의무
MeasureSafeWalkDistance Tilemap WorldToCell + HasTile cell 연속 + 위·아래 1 cell 인접 (계단 정합) Spawn 발판 Tilemap 등록 필수
_cachedPlayer collide distance < 1.5f or VisualBounds.Intersects → Decrement (i-frame 외) 본 설계 무관 (Enemy 동작 SOT)

2-7. GameOptimizer (E:/EerieVillage/Assets/Scripts/Mechanics/GameOptimizer.cs) — 본 설계 결정적 영향

동작 본 설계 영향
Init() BeforeSceneLoad — targetFrameRate 60·Physics2D.IgnoreLayerCollision(13,14) 본 설계 무관 (이미 적용)
SetupJumpThroughPlatforms() AfterSceneLoad — 모든 Collider2D 분류 본 설계 절차 생성 Tilemap은 본 hook 진입 전 완료 의무
Level Tilemap(name="Level") = Layer 0 보존 본 설계 지면 Tilemap 영역 name="Level" 강제
그 외 비-trigger Collider2D = Layer 16 강제 본 설계 공중 발판 Tilemap name≠"Level" 시 자동 Layer 16
Foreground GameObject = 가림막 시각만·TilemapCollider 제거 본 설계 절차 생성 시 "Foreground" name 회피
AutoForeground 자동 생성·Grid 자식 본 설계는 Level/AutoForeground 직접 생성 OR GameOptimizer 자동 분류 위임 (§7 결정)
Level Tile worldY ≥ playerY+1.5 OR 작은 공중 발판(가로≤8+위아래 빈) → AutoForeground 이동·Sprite 강제 본 설계 = 두 옵션 (A) 절차 생성기에서 즉시 분리 (B) Level 단일 Tilemap 배치 후 GameOptimizer 위임
TileGround prefix 제외 본 설계 지면 Tile name "TileGround*" 강제 영역

2-8. Tile 자산 (E:/EerieVillage/Assets/Tiles/)

Tile m_ColliderType 용도
TileGround.asset 1 (Sprite) 지면
TileGroundTop.asset 1 지면 표면
TileGroundDark.asset 1 어두운 지면 변형
TileFloatingLeftEdge.asset 1 공중 발판 좌측 끝
TileFloatingTileMiddle.asset 1 공중 발판 중간
TileFloatingRightEdge.asset 1 공중 발판 우측 끝
cloud.asset 1 공중 발판 변형 (구름)
fence.asset (확인 보류 — 기획팀 장식 영역) 배경
ShortBuilding/TallBuilding/MidgroundFiller.asset (배경) 배경 영역

전 Tile m_ColliderType=1(Sprite) 사전 확정 → 본 설계 §7 자동 충돌 적용 핵심 근거.

2-9. Prefab (E:/EerieVillage/Assets/Prefabs/)

Prefab m_Layer 본 설계 활용
Player.prefab 13 1 인스턴스 (Start Point Spawn)
Enemy.prefab 14 (구) MonsterRandomizer 영역 random 6종
Enemy_M001_Wolf~M006_Spider.prefab 14 (추정) 본 설계 SpawnPoint별 개별 prefab 선택 OR Enemy.prefab + MonsterRandomizer 위임 (§9)

2-10. Scene·기획 정합 (프로젝트/EerieVillage/기획/level/01_스테이지_구조.md v0.1)

기획 영역 본 설계 정합
5종 스테이지 (마을 ↔ 1-5) 본 설계 절차 생성기는 스테이지별 옵션 카탈로그 제공 (§11)
3단 구성 (도입·전개·보스 방) 본 설계 §4 청크 분할 권고: Intro/Mid/BossPre 3 영역 + Boss 방은 별도 수동 Scene
횡스크롤 길이 5~10분 본 설계 맵 길이 옵션 = Tile 단위 길이 (§15 PD 결정 안건)
§5 자동 생성 vs 수동 배치 — "전체 수동 배치 + 구간 2 적 배치 소규모 셔플" 채택 PD 신규 지시(2026-05-18) = 본 기획 결정 OVERRIDE — "맵의 길이만 지정하면, 랜덤으로 자동 생성" → 기획팀 후속 정합 안건 (§15)
§10 §10-2 "낙하 함정" 등 지형 기믹 — 개발팀 C11 판단 이관 본 설계 §11 MapResourceCatalog → 기믹 Prefab 등록 영역

2-11. BT12 스킬 시스템 영향 평가 (프로젝트/EerieVillage/개발/spec/스킬_시스템_설계_v1.md v1.0)

BT12 영역 본 설계 영향
PlayerSkillInventory·ActiveSkillRuntime·6종 Effector(Lightning·Melee·Spirit·Poison·Laser·Projectile) 영향 없음 — 본 설계는 맵 절차 생성만, Player 컴포넌트는 PlayerController.Awake 자동 부착 그대로
스킬 효과 충돌 영역 (Effector → Health.Decrement) 본 설계 영향 없음 — Enemy.prefab Health 그대로
Enemy ATK/HP 본 설계 영향 없음 — Enemy.prefab 그대로 spawn
MonsterRandomizer (6 controller random) 본 설계 § 9 Spawn 영역에서 활용 가능 (옵션 1: Enemy.prefab spawn → Awake에서 Random / 옵션 2: 본 설계 SpawnPoint config에서 특정 M00N 강제)

2-12. 기존 MapGenerator/Spawn 시스템 0건 사전 확정

grep MapGenerator|RandomMap|ProceduralMap|MapBuilder E:/EerieVillage/Assets/Scripts → 0건. grep SpawnPoint → 1건 (Mechanics/SpawnPoint.cs = 11줄 빈 marker 클래스).

→ 본 설계 = 완전 신규 시스템 도입. 기존 코드와 충돌 없음. GameOptimizer·EnemyController·PlayerController 영역과 인터페이스 정합만 의무.

2-13. Editor 폴더 영역

E:/EerieVillage/Assets/Editor/ 영역 GitAutoSync만 존재 → EditorWindow 신규 도입 깨끗 (Phase 2-C 영역).


§3. 아키텍처 4계층

┌────────────────────────────────────────────────────────────────┐
│ 계층 4: 시각 시뮬레이터 (Editor only)                            │
│   - MapPreviewWindow (EditorWindow)                            │
│     · GUI 영역 옵션 입력 + Generate 버튼 + 미리보기 캔버스       │
│     · Handles.DrawLine·DrawSolidArc → 발판·SpawnPoint·점프 영역 │
│   - MapSceneApplier (Scene 적용 버튼) — Phase 2-C               │
└────────────────────────────────────────────────────────────────┘
                              ▲ 호출
┌────────────────────────────────────────────────────────────────┐
│ 계층 3: 절차 생성기 (Runtime + Editor 양립)                       │
│   - MapGenerator (정적 메서드 또는 MonoBehaviour)                │
│     · Input: MapGenerationConfig (옵션 SO 또는 struct)          │
│     · Output: MapBlueprint (in-memory 데이터)                   │
│   - PlatformLayoutPlanner (§6 발판 배치 룰)                      │
│   - SpawnPointPlanner (§8 거점 배치)                             │
│   - TerrainPlanner (§7 바닥 자동 충돌)                            │
└────────────────────────────────────────────────────────────────┘
                              ▲ 참조
┌────────────────────────────────────────────────────────────────┐
│ 계층 2: 데이터 SOT (ScriptableObject)                           │
│   - MapResourceCatalog (Tile·Prefab 카탈로그)                   │
│   - MapGenerationConfig (옵션·길이·밀도)                        │
│   - PlatformerJumpProfile (실측 점프 수치 SOT)                   │
│   - SpawnRuleProfile (Enemy spawn 규칙)                         │
└────────────────────────────────────────────────────────────────┘
                              ▲ 입력
┌────────────────────────────────────────────────────────────────┐
│ 계층 1: 실행·런타임 적용                                          │
│   - MapApplier (Scene 적용 — Tilemap.SetTile + Instantiate)     │
│   - Player Start 배치 + Enemy SpawnPoint 인스턴스 생성           │
│   - GameOptimizer.SetupJumpThroughPlatforms() 진입 전 완료 의무  │
└────────────────────────────────────────────────────────────────┘

3-1. 데이터 흐름

[PD]                                    [본 시스템]
 │                                          │
 ├─ MapResourceCatalog SO 영역 등록 ───────►│ Tile·Prefab 풀
 │  (Editor·자유 추가)                       │
 │                                          │
 ├─ MapGenerationConfig 옵션 지정 ─────────►│
 │  · LengthTiles=120                       │
 │  · SpawnDensity=0.5                      │
 │  · StageProfile=Stage1                   │
 │                                          ▼
 │                            [MapGenerator.Generate(config)]
 │                                          │
 │                                          ▼
 │                                  [MapBlueprint]
 │                                  · TerrainTiles[]
 │                                  · PlatformTiles[]
 │                                  · SpawnPoints[]
 │                                  · StartPoint
 │                                  · Goal
 │                                          │
 ├─ MapPreviewWindow [Generate] ────────────┤ in-memory preview
 │  (시각 검토) ◄───── DrawHandles ──────────┤
 │                                          │
 ├─ [Apply to Scene] ──────────────────────►│ MapApplier
 │                                          │ · Tilemap.SetTile
 │                                          │ · Instantiate(Enemy/Player)
 │                                          ▼
 │                                  [Scene 영역 완성]
 │                                          │
 │                                          ▼
 │                         (Play) GameOptimizer.AfterSceneLoad
 │                                          │
 │                                          ▼
 │                            EnemyController.Start → patrol

§4. 절차 생성 알고리즘

4-1. 입력

[CreateAssetMenu(menuName = "EerieVillage/Map/Generation Config")]
public class MapGenerationConfig : ScriptableObject
{
    [Header("길이 영역")]
    public int lengthTiles = 120;       // 가로 Tile 총 길이 (1 Tile = 1 Unity unit)

    [Header("거점·밀도 영역")]
    [Range(0f, 1f)] public float spawnDensity = 0.4f;
    public int minSpawnSpacingTiles = 12;
    public int maxSpawnPerMap = 16;

    [Header("발판 영역")]
    [Range(0f, 1f)] public float platformDensity = 0.5f;
    public int minPlatformGapTiles = 3;
    public int maxPlatformGapTiles = 6;
    public int minPlatformWidthTiles = 2;
    public int maxPlatformWidthTiles = 6;
    public int minPlatformHeight = 2;   // 지면 위 Tile 단위
    public int maxPlatformHeight = 5;   // 점프 도달 5.06m 영역 (§2-5)

    [Header("Start/Goal 영역")]
    public int startMarginTiles = 4;    // 좌측 끝 Start Point margin
    public int goalMarginTiles = 4;     // 우측 끝 Goal margin

    [Header("리소스 영역")]
    public MapResourceCatalog catalog;
    public PlatformerJumpProfile jumpProfile;
    public StageThemeProfile stageTheme;  // 시각 테마 (Tile 가중치)
}

4-2. 출력 (in-memory)

public class MapBlueprint
{
    public int lengthTiles;
    public int height;                          // 청크 영역 최대 높이
    public List<TerrainTilePlace> terrain;      // 지면 Tile (Layer 0)
    public List<PlatformTilePlace> platforms;   // 공중 발판 Tile (Layer 16)
    public List<SpawnPointPlace> spawnPoints;   // Enemy spawn 거점
    public Vector2 startPoint;                  // Player 시작 좌표
    public Vector2 goalPoint;                   // VictoryZone 좌표
    public List<PropPlace> backgroundProps;     // fence·building 등 배경 Prefab
    public List<GimmickPlace> gimmicks;         // 함정·이동 발판 등 (§11)
    public int generationSeed;                  // 추적용 (PD 결정 3: 매번 랜덤이나 디버그 추적)
}

4-3. 절차 (의사 코드)

Generate(config):
    1. seed = Random.Range(int.MinValue, int.MaxValue)
       Random.InitState(seed)   // PD 결정 3: 매번 완전 랜덤 (사용자 입력 X, 디버그용 기록만)

    2. lengthTiles = config.lengthTiles

    3. ── 청크 분할 ──
       chunks = SplitIntoChunks(lengthTiles, chunkSize=20)
       각 chunk 영역 Theme 변동 권고 (§4-4)

    4. ── 지면 생성 (TerrainPlanner) ──
       terrain = []
       baseY = 0
       for x in 0..lengthTiles:
           // 자연스러운 고저차 — Perlin noise 변조 (±2 tile)
           y = baseY + RoundedPerlin(x * 0.1f, amplitude=2)
           terrain.Add( {x, y, TileGround} )
           // 표면 1 Tile = TileGroundTop
           terrain.Add( {x, y+1, TileGroundTop} ) — y+1 미사용 (충돌 영역 영역)

    5. ── 공중 발판 생성 (PlatformLayoutPlanner) ──
       platforms = []
       x = config.startMarginTiles + 5
       while x < lengthTiles - config.goalMarginTiles:
           if Random.value < config.platformDensity:
               width = Random.Range(config.minPlatformWidthTiles, config.maxPlatformWidthTiles+1)
               height = Random.Range(config.minPlatformHeight, config.maxPlatformHeight+1)
               // 점프 도달 가능성 검증 (jumpProfile)
               canReach = ValidateJumpReach(prevPlatformOrGround, {x, terrainY+height, width})
               if canReach:
                   platforms.AddRange(GeneratePlatformTiles(x, terrainY+height, width))
                   x += width + Random.Range(config.minPlatformGapTiles, config.maxPlatformGapTiles+1)
                   continue
           x += 1

    6. ── 거점 생성 (SpawnPointPlanner) ──
       targetCount = floor(lengthTiles * config.spawnDensity / config.minSpawnSpacingTiles)
       targetCount = min(targetCount, config.maxSpawnPerMap)
       spawnPoints = []
       // 거점 = 지면 + 일부 공중 발판 (PD 결정 안건 §15)
       candidates = AllValidSpawnPositions(terrain, platforms, marginFromStart=8, marginFromGoal=8, minSpacing=config.minSpawnSpacingTiles)
       Shuffle(candidates)
       spawnPoints = candidates.Take(targetCount)
       각 SpawnPoint 영역 EnemyType = RandomFromCatalog(config.catalog.enemyPrefabs)

    7. ── Start/Goal ──
       startPoint = (config.startMarginTiles, terrain[startMarginTiles].y + 1.5)
       goalPoint = (lengthTiles - config.goalMarginTiles, terrain[lengthTiles-goalMarginTiles].y + 1.5)

    8. ── 배경·기믹 ──
       backgroundProps = ScatterBackgroundProps(terrain, config.catalog.backgroundPrefabs, density)
       gimmicks = ApplyGimmickRules(config.catalog.gimmickPrefabs, terrain, platforms, density)

    9. return MapBlueprint { ... }

4-4. 청크 분할 권고 (단조 영역 회피)

  • chunkSize = 20 Tile 권고
  • 각 chunk 영역 platformDensity 가중치 변동 (Intro=0.3, Mid=0.6, BossPre=0.4)
  • stageTheme.tileVariantWeights에 따라 chunk별 시각 Tile pool 변동
  • PD 결정 안건 §15-3 — chunk 영역 자체 옵션 (스테이지 진행 양상) 노출 영역

4-5. ValidateJumpReach (§6 정합)

bool ValidateJumpReach(PlatformAnchor from, PlatformAnchor to)
{
    float dx = Mathf.Abs(to.x - from.x);
    float dy = to.y - from.y;  // 위로 이동 = 양수

    // 점프 도달 가능 영역: §2-5 실측
    float maxHorizontal = jumpProfile.safeHorizontalDistance;  // 5.78m (90% margin)
    float maxVertical = jumpProfile.safeVerticalHeight;        // 5.06m

    // 수평 거리 cap
    if (dx > maxHorizontal) return false;

    // 위로 점프: 높이 cap
    if (dy > maxVertical) return false;

    // 발판 → 발판: 포물선 통과 가능성 (단순 보수 — 최대 높이까지 도달 가능 시 OK)
    // dy 음수(아래) 발판 = 항상 도달 가능
    return true;
}

§5. Tile (배경) vs Prefab (기믹) 분리 룰

PD 결정 2 — 양쪽 다 활용.

5-1. Tile 영역 (Tilemap)

영역 Tile 자산 Layer 사용
지면 (Level Tilemap) TileGround·TileGroundTop·TileGroundDark 0 무한 지면·고저차
공중 발판 (AutoForeground Tilemap) TileFloatingLeftEdge·TileFloatingTileMiddle·TileFloatingRightEdge·cloud 16 Drop-Through 발판
배경 장식 (BackgroundTilemap) fence·MidgroundFiller (선택) (선택·collider X 또는 별도 Layer) 시각

5-2. Prefab 영역 (Instantiate)

영역 Prefab 사용
Player Player.prefab Start Point 1 인스턴스
Enemy Enemy.prefab 또는 Enemy_M00N_*.prefab SpawnPoint 영역 인스턴스
배경 건물 ShortBuilding·TallBuilding 배경 배치 (Prefab 또는 Tile both 옵션)
기믹 (신규) MovingPlatform·Trap·CheckPoint·VictoryZone 등 옵션 등록 영역 (§11)

5-3. 결정 룰

요소 = 동적 동작·복잡 컴포넌트 영역? → Prefab
요소 = 정적 시각·단순 충돌·반복 배치? → Tile
경계 영역(건물 등) → 옵션·MapResourceCatalog에서 PD 자유 선택

§6. 발판 배치 룰 (자연스러운 레벨 디자인)

6-1. PlatformerJumpProfile (실측 SOT)

[CreateAssetMenu(menuName = "EerieVillage/Map/Jump Profile")]
public class PlatformerJumpProfile : ScriptableObject
{
    [Header("실측 (KinematicObject·PlayerController·PlatformerModel)")]
    public float jumpTakeOffSpeed = 7f;
    public float jumpModifier = 1.5f;
    public float gravity = 9.81f;
    public float maxSpeed = 3f;
    public float gravityModifier = 1f;

    // 산출 (자동 계산 — OnValidate)
    public float v0 => jumpTakeOffSpeed * jumpModifier;
    public float effectiveGravity => gravity * gravityModifier;
    public float maxVerticalHeight => (v0 * v0) / (2f * effectiveGravity);  // 5.62m
    public float jumpRoundTripTime => 2f * v0 / effectiveGravity;            // 2.14s
    public float maxHorizontalDistance => maxSpeed * jumpRoundTripTime;      // 6.42m

    [Header("안전 margin (자연 레벨 디자인)")]
    [Range(0.5f, 1.0f)] public float safetyMargin = 0.9f;
    public float safeVerticalHeight => maxVerticalHeight * safetyMargin;      // 5.06m
    public float safeHorizontalDistance => maxHorizontalDistance * safetyMargin;  // 5.78m
}

6-2. 배치 룰 (자연스러움)

수치 근거
발판 간 수평 간격 min 3 Tile (3m) 1 Tile 점프 가능·연속 점프 리듬
발판 간 수평 간격 max 6 Tile (6m) safeHorizontalDistance 5.78m 영역 안
발판 높이 min 2 Tile (지면 위 2m) Player 캡슐 높이 약 1m → 점프 의무 영역
발판 높이 max 5 Tile (지면 위 5m) safeVerticalHeight 5.06m 영역 안
발판 폭 min 2 Tile (2m) EnemyController cliffCheckDistance=1m 영역 안전 patrol
발판 폭 max 6 Tile (6m) 단조 영역 회피
연속 점프 (계단식) 영역 dx 3~4 Tile, dy +1~+2 Tile 자연 상승 리듬
사선·이중 점프 PD 결정 안건 §15-2 현 KinematicObject 영역 2단 점프 미구현

6-3. 검증 영역

MapGenerator.PlatformLayoutPlanner가 모든 발판 영역 PlatformerJumpProfile.ValidateJumpReach() 통과 검증. 미통과 시 해당 발판 폐기 후 위치·높이 재시도 (최대 5회 retry).

6-4. EnemyController patrol 영역 (§9 정합)

발판 폭 ≥ 2 Tile + EnemyController fallThreshold=1m·cliffCheckDistance=1m → spawn 발판 영역 자동 patrol 동작 보장.


§7. 바닥 자동 충돌 적용

7-1. PD 요구 "바닥의 길은 사용자가 일일이 수정하지 않아도 충돌 영역이 자연스럽게 적용"

7-2. 기존 자산 활용 (§2-8 실측)

  • 모든 Tile 자산 m_ColliderType=1 (Sprite) 이미 강제
  • 본 설계 = Tilemap 영역 SetTile 시 자동 영역 collider 적용

7-3. Tilemap 자동 충돌 영역 표준 패턴

// MapApplier.cs
void ApplyTerrainToLevelTilemap(MapBlueprint blueprint)
{
    Tilemap levelTilemap = FindOrCreateLevelTilemap();
    foreach (var place in blueprint.terrain)
    {
        levelTilemap.SetTile(new Vector3Int(place.x, place.y, 0), place.tile);
        levelTilemap.SetColliderType(new Vector3Int(place.x, place.y, 0),
                                       Tile.ColliderType.Sprite);
    }
    var tc = levelTilemap.GetComponent<TilemapCollider2D>();
    if (tc == null) tc = levelTilemap.gameObject.AddComponent<TilemapCollider2D>();
    tc.compositeOperation = Collider2D.CompositeOperation.Merge;

    // CompositeCollider2D 통합 (지면 영역 단일 collider — 성능·정합)
    var cc = levelTilemap.GetComponent<CompositeCollider2D>();
    if (cc == null) cc = levelTilemap.gameObject.AddComponent<CompositeCollider2D>();
    var rb = levelTilemap.GetComponent<Rigidbody2D>();
    if (rb == null) rb = levelTilemap.gameObject.AddComponent<Rigidbody2D>();
    rb.bodyType = RigidbodyType2D.Static;

    tc.ProcessTilemapChanges();
    levelTilemap.gameObject.name = "Level";          // §2-7 GameOptimizer 영역 보존
    levelTilemap.gameObject.layer = 0;               // §2-7 GameOptimizer 영역 보존
}

7-4. 공중 발판 Tilemap 영역 (Drop-Through 영역)

void ApplyPlatformsToAutoForegroundTilemap(MapBlueprint blueprint)
{
    Tilemap fgTilemap = FindOrCreateAutoForegroundTilemap();
    foreach (var place in blueprint.platforms)
    {
        fgTilemap.SetTile(new Vector3Int(place.x, place.y, 0), place.tile);
        fgTilemap.SetColliderType(new Vector3Int(place.x, place.y, 0),
                                    Tile.ColliderType.Sprite);
    }
    // GameOptimizer 영역 자동 영역 Layer 16 + TilemapCollider 보장
    fgTilemap.gameObject.name = "AutoForeground";
    fgTilemap.gameObject.layer = 16;
    EnsureCollider(fgTilemap);
}

7-5. 두 옵션 (PD 결정 안건 §15-4)

옵션 A (권고): 본 설계 MapApplier 영역 직접 Level/AutoForeground 분리 적용 → GameOptimizer 영역 자동 분류 의존 X (예측성·결정성↑)

옵션 B: 모두 Level Tilemap에 배치 → GameOptimizer.SetupJumpThroughPlatforms() 영역 자동 분류 위임 (기존 시스템 활용·중복 회피)

§15-4 PD 결정.

본 PM 권고 옵션 A — Drop-Through 패턴 28회 시도(BT22~BT47)의 교훈 영역 명시적 분리가 안정.

7-6. Composite·자연스러운 충돌

CompositeCollider2D + Merge → 인접 Tile 영역 단일 collider 영역 통합. 가장자리 jitter·작은 틈 영역 차단.


§8. 거점(생성 포인트) + Start Point 배치

8-1. 옵션 정합 (PD 결정 4)

[Range(0f, 1f)] public float spawnDensity = 0.4f;
// 0.0 = 거점 0개 / 0.5 = 중간 밀도 / 1.0 = 최대 밀도 (maxSpawnPerMap cap)
public int minSpawnSpacingTiles = 12;
public int maxSpawnPerMap = 16;

8-2. 산출 공식

desiredCount = floor(lengthTiles * spawnDensity / minSpawnSpacingTiles)
finalCount = min(desiredCount, maxSpawnPerMap)

예: lengthTiles=120·spawnDensity=0.5·minSpawnSpacingTiles=12 → desiredCount=5·finalCount=5

8-3. 거점 후보 영역

영역 적격
지면 평탄 영역 (Tile 폭 ≥3 연속)
공중 발판 (폭 ≥3 Tile) ✓ (PD 결정 안건 §15-5: 거점을 공중 발판에도 둘지)
Start Point ±8 Tile ✗ (난이도 초기)
Goal ±8 Tile
다른 거점 < minSpawnSpacingTiles

8-4. Start Point

  • 좌측 끝 + startMarginTiles (default 4) 위치
  • 지면 Tile 표면 + 1.5m 위 (Player 캡슐 영역 안전 spawn)
  • 본 설계 = 항상 좌측 고정 (PD 결정 안건 §15-6 — 양 끝 random 옵션)

8-5. Goal

  • 우측 끝 - goalMarginTiles 위치
  • VictoryZone Prefab Instantiate (현 영역 VictoryZone Prefab 영역 검증 필요 — Mechanics/VictoryZone.cs 존재 확인됨, §2-7 GameOptimizer 영역 excluded)

8-6. SpawnPoint 영역 (MonoBehaviour 확장)

public class SpawnPoint : MonoBehaviour  // 기존 11줄 영역 확장
{
    [Header("Spawn 영역")]
    public GameObject enemyPrefab;       // Enemy_M00N or Enemy.prefab + MonsterRandomizer
    public float spawnRadiusX = 0.5f;    // 거점 ±X Tile 영역 random spawn
    public int initialSpawnCount = 1;    // 최초 spawn 수
    public int maxConcurrent = 1;        // 동시 spawn 상한 (현 영역 1 — re-spawn 영역 PD 결정 §15-7)

    [Header("미리보기 영역 — Editor only")]
    public Color gizmoColor = new Color(1f, 0.3f, 0.3f, 0.5f);

    void Start()
    {
        for (int i = 0; i < initialSpawnCount; i++)
        {
            Vector3 pos = transform.position + new Vector3(
                Random.Range(-spawnRadiusX, spawnRadiusX), 0f, 0f);
            var enemy = Instantiate(enemyPrefab, pos, Quaternion.identity);
            // EnemyController.Awake → _startX·_startY 설정 자동 진행 (§2-6 정합)
        }
    }

#if UNITY_EDITOR
    void OnDrawGizmos()
    {
        Gizmos.color = gizmoColor;
        Gizmos.DrawWireSphere(transform.position, 0.5f);
        Gizmos.DrawLine(transform.position + Vector3.left * spawnRadiusX,
                        transform.position + Vector3.right * spawnRadiusX);
    }
#endif
}

§9. 몬스터 spawn 룰

9-1. PD 요구 "현재 만들어진 몬스터 이동 패턴을 활용"

§2-6 EnemyController 자동 patrol 시스템 = 완전 계승. SpawnPoint 영역 Instantiate만 처리.

9-2. Enemy spawn 절차

1. MapApplier가 SpawnPoint Prefab 영역 Scene에 배치 (위치=거점 좌표)
2. SpawnPoint.Start() → enemyPrefab Instantiate (Awake → MonsterRandomizer 영역 controller random)
3. EnemyController.Awake() → _startX = transform.position.x, _startY = transform.position.y
4. EnemyController.Start() → MeasureSafeWalkDistance(±1) — Tilemap cell 기반 좌우 patrol 거리 측정
5. _isInitialized = true → UpdatePatrol() 영역 자동 patrol 시작
6. 떨어짐 검출 (y < _startY - 1.0) → 시작 위치 텔레포트

9-3. EnemyType 카탈로그 (MapResourceCatalog 영역)

Catalog 슬롯 영역
enemyPrefabs[] Enemy_M001_Wolf·M002_Bat·M003_ZombieM·M004_ZombieF·M005_Ghost·M006_Spider
enemyWeights[] stageTheme별 가중치 (Stage1=Wolf 70%·Bat 30% 등)
flyingEnemyPrefabs[] M002 Bat (cliffCheck skip·공중 spawn)

9-4. spawn 위치 영역 — 거점 주변

spawnPos = spawnPoint.position + Vector3(Random(-spawnRadiusX, spawnRadiusX), 0, 0)

EnemyController _startX는 Awake 시점 transform.position.x → 본 spawn 위치 = patrol 기준점.

9-5. 동시 spawn 상한

현 영역 SpawnPoint 1개당 1 Enemy. 재-spawn (사망 후 시간 경과 재출현) 영역 = PD 결정 안건 §15-7.

9-6. 공중 SpawnPoint (M002 Bat)

IsFlying=true 영역 EnemyController는 flyingYOffset = 2f 자동 적용 → SpawnPoint 영역 지면 위 SpawnPoint에 M002 prefab 배치해도 Awake 영역 +2m 자동 상승. 별도 영역 처리 불요.


§10. 시각 시뮬레이터 — Unity Editor Window 1순위 결정

10-1. 가능성 판단 (PD 결정 1 — 1순위 EditorWindow)

판단 결과: 가능. 1순위 EditorWindow 채택.

근거:

  • E:/EerieVillage/Assets/Editor/ 영역 GitAutoSync만 존재·신규 EditorWindow 도입 깨끗 (§2-13)
  • Unity EditorWindow + GUILayout + Handles.Draw* 영역 표준 패턴
  • Tile data·SpawnPoint 위치·점프 도달 영역 모두 in-memory MapBlueprint 영역 직접 그리기 가능 (Scene 적용 전)

10-2. MapPreviewWindow 구조 (Editor 폴더)

// E:/EerieVillage/Assets/Editor/Map/MapPreviewWindow.cs (신규)
public class MapPreviewWindow : EditorWindow
{
    [MenuItem("EerieVillage/Map/Preview")]
    public static void Open() => GetWindow<MapPreviewWindow>("Map Preview");

    MapGenerationConfig config;
    MapBlueprint currentBlueprint;
    Vector2 previewScroll;
    float zoom = 1f;

    void OnGUI()
    {
        EditorGUILayout.LabelField("Map Preview", EditorStyles.boldLabel);
        config = (MapGenerationConfig)EditorGUILayout.ObjectField("Config", config,
                                       typeof(MapGenerationConfig), false);
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("Generate", GUILayout.Width(100)))
        {
            currentBlueprint = MapGenerator.Generate(config);
        }
        if (GUILayout.Button("Apply to Scene", GUILayout.Width(120)))
        {
            MapApplier.Apply(currentBlueprint, config);
        }
        if (GUILayout.Button("Clear Scene", GUILayout.Width(100)))
        {
            MapApplier.Clear();
        }
        EditorGUILayout.EndHorizontal();

        zoom = EditorGUILayout.Slider("Zoom", zoom, 0.2f, 3f);

        if (currentBlueprint != null)
        {
            DrawPreview(currentBlueprint);
        }
    }

    void DrawPreview(MapBlueprint bp)
    {
        // GUI.DrawTexture로 grid 배경
        // bp.terrain·bp.platforms·bp.spawnPoints·bp.startPoint·bp.goalPoint를
        // Rect로 변환·GUI.Box 영역 그림
        // 점프 도달 영역 = Handles.DrawSolidArc 또는 GUI line
        var rect = GUILayoutUtility.GetRect(position.width, position.height - 100);
        previewScroll = GUI.BeginScrollView(rect, previewScroll,
                                             new Rect(0, 0, bp.lengthTiles * 16 * zoom,
                                                              bp.height * 16 * zoom));
        // Terrain (회색)
        foreach (var t in bp.terrain)
            EditorGUI.DrawRect(TileToRect(t.x, t.y, zoom), new Color(0.4f, 0.3f, 0.2f));
        // Platforms (녹색)
        foreach (var p in bp.platforms)
            EditorGUI.DrawRect(TileToRect(p.x, p.y, zoom), new Color(0.3f, 0.7f, 0.3f));
        // SpawnPoints (빨강)
        foreach (var s in bp.spawnPoints)
            EditorGUI.DrawRect(WorldToRect(s.position, zoom, size: 0.6f),
                                 new Color(0.9f, 0.2f, 0.2f));
        // Start (파랑)
        EditorGUI.DrawRect(WorldToRect(bp.startPoint, zoom, size: 1f),
                             new Color(0.2f, 0.5f, 0.9f));
        // Goal (노랑)
        EditorGUI.DrawRect(WorldToRect(bp.goalPoint, zoom, size: 1f),
                             new Color(0.9f, 0.9f, 0.2f));
        // 점프 도달 영역 (반투명 영역) — option toggle
        if (showJumpReach)
            DrawJumpReachOverlays(bp);
        GUI.EndScrollView();
    }
}

10-3. 시각 요소

요소 색상 표시
지면 Tile 갈색 0.4·0.3·0.2 1×1 Rect
공중 발판 Tile 녹색 0.3·0.7·0.3 1×1 Rect
SpawnPoint 빨강 0.9·0.2·0.2 0.6×0.6 Rect + 거점 ±spawnRadiusX 선
Start Point 파랑 0.2·0.5·0.9 1×1 Rect + "S" 텍스트
Goal Point 노랑 0.9·0.9·0.2 1×1 Rect + "G" 텍스트
점프 도달 영역 반투명 청 (toggle) 발판 가장자리 영역 ±5.78m·+5.06m 호

10-4. Fallback — Scene 미리보기 (옵션 어렵 시)

본 PM 판단 = EditorWindow 영역 가능 — Fallback 비활성. 만약 GUILayout 영역 성능 한계 발생 시 → Scene Hierarchy 영역 Empty GameObject ("MapPreview_Temp") 영역 placeholder Sprite Instantiate → SceneView 영역 직접 표시.

10-5. Editor 호출 영역 워크플로우

1. PD: MapGenerationConfig SO Editor 영역 옵션 조정
2. PD: EerieVillage/Map/Preview 메뉴 → MapPreviewWindow 활성
3. PD: [Generate] → in-memory MapBlueprint 확인
4. PD: 시각 영역 만족 시 [Apply to Scene] → Tilemap·Prefab 적용
5. PD: 불만족 시 [Generate] 반복 (매번 완전 랜덤·PD 결정 3)
6. PD: Play → 실제 게임 확인

§11. 맵 리소스 등록 시스템

11-1. MapResourceCatalog (ScriptableObject)

[CreateAssetMenu(menuName = "EerieVillage/Map/Resource Catalog")]
public class MapResourceCatalog : ScriptableObject
{
    [Header("지면 Tile 영역")]
    public TileEntry[] groundTiles;       // TileGround·TileGroundTop·TileGroundDark

    [Header("공중 발판 Tile 영역")]
    public TileEntry[] platformLeftEdge;
    public TileEntry[] platformMiddle;
    public TileEntry[] platformRightEdge;
    public TileEntry[] platformCloud;     // 변형 (cloud.asset)

    [Header("배경 Tile 영역 (선택)")]
    public TileEntry[] backgroundTiles;   // fence·MidgroundFiller

    [Header("Enemy Prefab 영역")]
    public PrefabEntry[] enemyPrefabs;    // Enemy_M001~M006

    [Header("Player Prefab 영역")]
    public GameObject playerPrefab;

    [Header("Goal Prefab 영역")]
    public GameObject goalPrefab;          // VictoryZone

    [Header("배경 Prefab 영역")]
    public PrefabEntry[] backgroundPrefabs;  // ShortBuilding·TallBuilding 등

    [Header("기믹 Prefab 영역 (신규·향후 확장)")]
    public PrefabEntry[] gimmickPrefabs;     // 이동 발판·낙하 함정·체크포인트 등

    [Header("SpawnPoint Prefab 영역")]
    public GameObject spawnPointPrefab;     // SpawnPoint.cs 부착 영역
}

[System.Serializable]
public class TileEntry
{
    public TileBase tile;
    [Range(0f, 10f)] public float weight = 1f;
    public string[] tags;  // "stage1", "dark" 등
}

[System.Serializable]
public class PrefabEntry
{
    public GameObject prefab;
    [Range(0f, 10f)] public float weight = 1f;
    public string[] tags;
}

11-2. PD 자유 등록 영역

PD가 신규 Tile·Prefab 영역 영역 추가 시 → MapResourceCatalog asset 영역 Inspector 영역 슬롯 영역 드래그·드롭. 코드 수정 X·tag·weight 영역 분류만.

11-3. StageThemeProfile (스테이지별 Theme)

[CreateAssetMenu(menuName = "EerieVillage/Map/Stage Theme")]
public class StageThemeProfile : ScriptableObject
{
    public string stageName = "Stage1_DuskVillageOutskirts";
    public MapResourceCatalog catalog;     // base catalog
    public string[] filterTags;            // "stage1" tag 영역 Tile만 활용
    public Color skyTint = Color.white;
    // 추후 — 음악·환경 영역
}

11-4. 기획 SOT 5종 스테이지 정합

프로젝트/EerieVillage/기획/level/01_스테이지_구조.md §2 영역 5종 stageTheme 영역 매핑:

Stage StageThemeProfile.stageName filterTags
1 황혼 고을 어귀 Stage1_DuskVillageOutskirts stage1·dusk
2 버려진 산사 Stage2_AbandonedMountainTemple stage2·moonlit
3 뼈 바위 계곡 Stage3_BoneRockValley stage3·foggy
4 폐가·지하 창고 Stage4_AbandonedHouseBasement stage4·midnight
5 공동묘지 심부 Stage5_GraveyardDepths stage5·dawn

§12. 기존 시스템 통합 영역

12-1. BT5-Dev 발판 시스템 영역

시스템 본 설계 활용
KinematicObject contactFilter Drop-Through Layer 16 발판 — 본 설계 AutoForeground Tilemap Layer 16 강제 정합
PlayerController UpdateContactFilterForDropThrough 본 설계 무관 (Player 영역 그대로)
GameOptimizer.SetupJumpThroughPlatforms 상호작용 의무 — §7-5 옵션 A/B 결정
Tile m_ColliderType=1 (Sprite) §2-8 모든 Tile 기 강제 → 본 설계 SetColliderType(Sprite) 영역 정합
CompositeCollider2D Merge 본 설계 Level Tilemap 영역 부착 (§7-3)

12-2. EnemyController patrol 영역

영역 본 설계 정합
_startX·_startY Awake 측정 SpawnPoint.Instantiate → Enemy.Awake 자동 동작
MeasureSafeWalkDistance Tilemap cell 기반 본 설계 = AutoForeground Tilemap 영역 등록 시 자동 patrol
cliffCheckDistance=1m 본 설계 발판 폭 min 2 Tile 영역 안전
fallThreshold=1m → 시작 위치 텔레포트 본 설계 spawn 발판 영역 EnemyController 자동 복귀
IsFlying M002 Bat 본 설계 catalog 영역 flyingEnemyPrefabs 영역 별도 분류

12-3. KinematicObject 점프 물리 영역

본 설계 = PlatformerJumpProfile SO 영역 실측 수치 직접 참조. KinematicObject 영역 변경 X.

12-4. M001~M006 6 Enemy prefab 영역

본 설계 = MapResourceCatalog 영역 6 prefab 등록·stageTheme별 가중치. MonsterRandomizer 영역 random 6종 활용 영역 = 옵션 (Enemy.prefab + MonsterRandomizer) vs (Enemy_M00N 직접 prefab) — PD 결정 안건 §15-8.

12-5. BT12 스킬 시스템 영역

영향 없음 (§2-11). Player·Enemy 영역 그대로 spawn → PlayerSkillInventory·ActiveSkillRuntime·6 Effector 모두 정상 동작.

12-6. GitAutoSync (Editor)

EerieVillage E:/EerieVillage/Assets/Editor/GitAutoSync/ 영역 영향 X (별도 영역).


§13. Phase 2 작업 단위 분해 (C49 단계 정합)

13-1. Phase 2-A — 데이터 SOT 영역 (클라이언트팀 Sonnet)

산출물 위치 분량
PlatformerJumpProfile.cs (ScriptableObject) Assets/Scripts/Map/Data/ 50줄
MapResourceCatalog.cs + TileEntry/PrefabEntry Assets/Scripts/Map/Data/ 80줄
StageThemeProfile.cs Assets/Scripts/Map/Data/ 30줄
MapGenerationConfig.cs Assets/Scripts/Map/Data/ 60줄
MapBlueprint.cs + Place 영역 4종 struct Assets/Scripts/Map/Data/ 100줄
Default .asset 인스턴스 4종 (실측 수치 반영) Assets/Data/Map/ (PD 미리 1개·확장 PD)

13-2. Phase 2-B — 절차 생성기 (클라이언트팀 Sonnet)

산출물 분량
MapGenerator.cs (static·Generate 메서드) 150줄
TerrainPlanner.cs (Perlin 변조·고저차) 80줄
PlatformLayoutPlanner.cs (배치 룰·ValidateJumpReach) 150줄
SpawnPointPlanner.cs (밀도·간격·후보 영역) 100줄
BackgroundPropPlanner.cs (배경 산포) 50줄
EditMode 테스트 — 100회 Generate·ValidateJumpReach 모든 발판 통과 검증 200줄

13-3. Phase 2-C — Editor Window + Applier (클라이언트팀 Sonnet)

산출물 분량
MapPreviewWindow.cs (EditorWindow + DrawPreview) 250줄
MapApplier.cs (Tilemap.SetTile + Instantiate + Clear) 150줄
SpawnPoint.cs 확장 (Spawn 영역 옵션·OnDrawGizmos) 80줄
Editor 메뉴·shortcut (포함)

13-4. Phase 2-D — QA·정합 (개발팀장 Opus 검증)

영역 영역
Stage1~5 Default Config 각 영역 Generate + Apply + Play 검증 5종
점프 도달 가능성 검증 — 100% pass 정량
Enemy patrol 정합 검증 (BT109 Tilemap cell 영역 정합) 정량
GameOptimizer SetupJumpThroughPlatforms 충돌 검증 정성
시뮬레이터 Apply→Play 일관성 검증 정성

13-5. C49 표준 프로세스 적용

  1. 개발팀장 (Opus·본 문서) 설계 v1 산출 ✓
  2. 클라이언트팀장 (Opus) 본 문서 영역 Phase 2-A/B/C 분할 위임 결정
  3. 게임플레이 프로그래머 (Sonnet) 코드 구현
  4. 클라이언트팀장 (Opus) Phase 2 코드 검증
  5. 개발팀장 (Opus) Phase 2-D 영역 정합 검증 → 완료 아카이브

§14. 기각안

14-1. (기각 1) Seed 입력 기반 재현 가능 랜덤 채택

기각 이유: PD 결정 3 — "매번 완전 랜덤 (Seed 입력 X · 재현성 비요구)". Seed 입력 UI·재현 로직 영역 코드 부담 + PD 명시 비요구.

보완: MapBlueprint.generationSeed 영역 자동 기록 (디버그·로그 영역 추적용). PD가 후속 재현 의무 발생 시 generationSeed 영역 InitState 재 invoke 가능.

14-2. (기각 2) 전체 수동 배치 + 부분 셔플 (기존 기획 §5 채택)

기각 이유: PD 신규 지시 (2026-05-18) = "맵의 길이만 지정하면, 랜덤으로 자동 생성" → 기획 §5 "전체 수동 배치 + 구간 2 적 배치 소규모 셔플" OVERRIDE.

기획 후속 정합 안건: §15-1 PD 결정 대기. 기획팀 영역 SOT 정정 의무 (level-designer 후속).

14-3. (기각 3) Cellular Automata 또는 BSP 영역 던전 생성 알고리즘

기각 이유: 횡스크롤 플랫포머 영역 1D 진행 영역 단순 chunk 분할 (4-3) 영역 충분. CA·BSP 영역 2D top-down 영역 적합.

14-4. (기각 4) Runtime Generate (게임 진입 시 매번 절차 생성)

기각 이유: 본 설계 Phase 1 = Editor 영역 Generate→Apply→Save Scene. Runtime Generate 옵션은 향후 확장 (Phase 3-C 영역). 현 Phase = PD 시각 검토 영역 = Editor 영역 적용 후 Play.

보완: MapGenerator·MapApplier 영역 Editor·Runtime 양립 가능 영역 설계 → Phase 3-C 영역 즉시 활성 가능.

14-5. (기각 5) Procedural 영역 별도 신규 Scene 생성

기각 이유: 기존 Ingame.unity 영역 단일 Scene 영역 활용 — Player·UI·Camera·Health·Skill 영역 모두 부착됨. 본 설계 = Ingame.unity 영역 Tilemap·SpawnPoint만 절차 영역 교체.

14-6. (기각 6) Boss 방 영역 절차 생성 포함

기각 이유: 기획 §3-2 영역 보스 방 = "고정 카메라·후방 차단·전용 지형". 자연 생성 영역 적합 X. 본 설계 = 도입·전개 영역만, 보스 방 = 별도 수동 Scene 또는 본 설계 영역 마지막 chunk 영역 hand-place placeholder.

보완: MapGenerationConfig 영역 bool includeBossRoom 영역 옵션 + Boss 방 영역 별도 prefab 영역 Instantiate (현 Phase 1 영역 PD 결정 보류·§15-9 영역).


§15. PD 결정 대기 안건 (Phase 2 진입 전 결정 의무)

[2026-05-18 PD 직접 결정 11건 — Phase 2.1 정정 채택 (직전 본 PM 자율 채택 6건 OVERRIDE)]

# PD 직접 채택 직전 본 PM 채택
15-1 마을(=로비) PD 수동 제작·랜덤 스테이지 1종 집중·추후 확장 (현 Phase = 스테이지 1종 영역 집중·마을 무시) OPT A 동일 (§5 OVERRIDE)
15-2 사선·이중 점프 도입 예정 단일 점프 유지OVERRIDE 도입
15-3 미노출 (추후 필요시 추가) 미노출 동일
15-4 옵션 B — GameOptimizer 위임 (기존 Level/Foreground 개념 없어도 무관) OPT A 직접 분리OVERRIDE B
15-5 공중 발판 거점 허용 지면만OVERRIDE 허용
15-6 좌측 고정 좌측 고정 동일
15-7 사망 후 재출현 고려 (재-spawn 메커니즘 추가) 재-spawn XOVERRIDE 추가
15-8 안 2 — Enemy.prefab + MonsterRandomizer 활용 Enemy_M00N 직접OVERRIDE 안 2
15-9 보스 방 자동 생성 (맵 가장 우측 끝) 별도 수동 SceneOVERRIDE 자동 생성
15-10 Tile 단위 Tile 단위 동일
15-11 옵션 12종 모두 노출 12종 모두 노출 동일

Phase 2 영역 코드 구현 (직전 채택) — 17 신규 + SpawnPoint.cs 11→51줄. Phase 2.1 영역 PD 직접 결정 6건 OVERRIDE 구현 (본 응답 직후) — 12 파일 수정 (PlayerController 이중 점프·SpawnPoint 91줄 재-spawn+MonsterRandomizer·MapGenerationConfig·MapResourceCatalog baseEnemyPrefab+bossPrefab·SpawnPointConfig·PlatformPlanner SAFE 10.11m/11.56m·SpawnPointPlanner 공중 발판·ChunkPlanner BossRoom·MapApplier 단일 Level Tilemap·MapPreviewWindow 옵션 노출·MapAssetBootstrap·MapGeneratorTests 15/15). MCP 검증 12/12 → 15/15 green·컴파일 0건·warning 2 unrelated. 이중 점프 영역 점프 물리 재산출: H_max=11.24m·D_max=29.96m·SAFE_VERTICAL_HEIGHT=10.11m·SAFE_HORIZONTAL_DISTANCE=11.56m·maxPlatformHeight cap 10·maxPlatformGap default 10.

15-1. 기획 SOT 정합 — 프로젝트/EerieVillage/기획/level/01_스테이지_구조.md §5 OVERRIDE 처리

기획 영역 §5 "전체 수동 배치 + 구간 2 적 배치 소규모 셔플" vs PD 신규 지시 "맵의 길이만 지정하면 랜덤으로 자동 생성" 충돌.

옵션 A: 기획 SOT 정정 — level-designer 후속 갱신 옵션 B: 두 영역 공존 — 본 설계 절차 생성기 + 기획 수동 배치 영역 두 가지 운용

15-2. 사선·이중 점프 도입 여부

현 KinematicObject 영역 2단 점프 X. 본 설계 §6-2 영역 단일 점프 영역만 ValidateJumpReach.

옵션 A: 단일 점프만 유지 — 본 설계 그대로 옵션 B: 2단 점프 추가 — 본 설계 maxVerticalHeight 약 2배 영역 재산출 의무 (PlayerController.UpdateJumpState 영역 추가 구현)

15-3. chunk 영역 옵션 노출 영역

본 설계 §4-4 영역 chunk 영역 platformDensity 가중치 변동 (Intro 0.3·Mid 0.6·BossPre 0.4) 기본값.

옵션 A: 기본값 고정 — PD 옵션 노출 X 옵션 B: MapGenerationConfig 영역 ChunkProfile[] 영역 PD 노출 — 세부 영역 영역

15-4. Level/AutoForeground 분리 전략

§7-5 영역 옵션 A (본 설계 영역 직접 분리·권고) vs 옵션 B (GameOptimizer 위임).

15-5. 공중 발판 영역 SpawnPoint 배치 여부

§8-3 영역 공중 발판 (폭 ≥3 Tile) 영역 거점 후보 영역.

옵션 A: 지면만 거점 — 안전·patrol 거리 보장 옵션 B: 공중 발판 영역 거점 — 입체적 전투·플랫포밍 강조 (단·spawn Enemy 떨어짐 위험)

15-6. Start Point 좌측 고정 vs random

§8-4 영역 본 설계 영역 항상 좌측 고정. 향후 영역 양 끝 random·역방향 진행 영역.

15-7. Enemy 재-spawn (사망 후 시간 경과 재출현)

§9-5 영역 SpawnPoint 1개당 1 Enemy 영역 1회 spawn 영역. 재-spawn 영역 옵션·timer·count 영역.

15-8. Enemy spawn 방식 — Enemy.prefab + MonsterRandomizer vs Enemy_M00N 직접

§12-4 영역 두 영역 영역. 본 PM 권고 = 후자 (catalog 영역 명시적 prefab 영역 가중치).

15-9. Boss 방 포함 여부 (현 Phase 1)

§14-6 영역 보스 방 영역 별도 영역 영역 → 본 Phase 1 영역 영역 X. 추후 영역.

15-10. 맵 길이 단위 — Tile vs 미터 vs 청크

본 설계 영역 Tile 단위 영역 (lengthTiles). 1 Tile = 1 Unity unit 가정.

옵션 A: Tile 단위 유지 (개발 영역 정합) 옵션 B: 미터·청크 단위 영역 PD 친화 영역 변환 UI 추가

15-11. 옵션 노출 영역 — MapGenerationConfig 영역 PD 자유도

본 설계 §4-1 영역 옵션 12종. PD 영역 너무 많은 옵션 영역 = 결정 부담 영역.

옵션 A: 12종 모두 노출 (PD 자유도 최대) 옵션 B: 5~6종만 노출 (lengthTiles·spawnDensity·platformDensity·stageTheme·includeBossRoom 등) — Advanced 영역 toggle


§16. C50 Phase 2 토큰 옵션 사전 보고

16-1. Phase 2 분량 추정

단계 코드 라인 추정 토큰 추정
Phase 2-A 데이터 SOT (5종 SO + 인스턴스) 320 라인 ~12K
Phase 2-B 절차 생성기 (5종 Planner + 테스트) 530 라인 ~25K
Phase 2-C Editor Window + Applier (3종) 480 라인 ~22K
Phase 2-D 정합 검증 + 5 stage Config 100 라인 + 검증 ~10K
합계 1430 라인 ~69K

16-2. 옵션 사전 보고 (C50)

옵션 (a) 단일 Phase 2 일괄 처리:

  • 단일 세션 영역 ~70K 토큰
  • 위험: 컨텍스트 영역 한계 영역 일부 정합 영역 영역
  • 권고도: 중

옵션 (b) Phase 2-A/B/C/D 분할 순차 진행 (P32 정합·본 PM 권고):

  • 2-A 단독 ~12K → 검증 → 2-B 단독 ~25K → 검증 → 2-C 단독 ~22K → 검증 → 2-D
  • 각 단계 영역 클라이언트팀장 Opus 위임·게임플레이 프로그래머 Sonnet 작업·클라이언트팀장 Opus 검증
  • 권고도: — Drop-Through 발판 영역 BT22~BT47 28회 시도 영역 교훈

16-3. 본 PM 권고

옵션 (b) Phase 2-A/B/C/D 분할 순차 진행 영역 PD 승인 의무.

각 Phase 영역 클라이언트팀장 (client-team-lead) Opus 영역 위임. 단순 반복 영역 = 게임플레이 프로그래머 Sonnet 영역 직접 위임 가능 (Phase 2-A 데이터 SO 영역 등).


§17. 변경 이력

일시 변경 사유 기안
2026-05-18 v1.0 초안 — Phase 1 설계 완료 PD 직접 지시 BT13-Dev-Map (2026-05-18) 개발팀장 (dev-team-lead)
2026-05-18 v1.1 — Phase 2 (a) 단일 채택·§15 11건 dev-team-lead 권고 일괄 채택·frontmatter "8건"→"11건" 정정 PD 결정 "a 단일 설계 보고서로 진행해" + pm-auditor Major 2 정정 + 본 PM 자율 보완 총괄PM (pm-general)
2026-05-18 v1.2 — Phase 2 코드 구현 완료 (17 신규 + 1 수정 외부 git E:/EerieVillage) client-team-lead Opus 직접 구현·MCP 검증 12/12 green·컴파일 0건·warning 0건 클라이언트팀장 (client-team-lead)
2026-05-18 v1.3 — Phase 2.1 PD 직접 결정 6건 OVERRIDE 구현 (15-2 이중 점프·15-4 B 위임·15-5 공중 발판·15-7 재-spawn·15-8 MonsterRandomizer·15-9 BossRoom 자동) PD 직접 답변 11건 (2026-05-18·"15-2 도입할 예정·15-4 B안·15-5 허용·15-7 재출현·15-8 안 2·15-9 우측 끝 자동") + client-team-lead Opus OVERRIDE 구현·MCP 15/15 green 클라이언트팀장 (client-team-lead) + PD 직접 결정 (PM 매개)
2026-05-18 v1.4 — Bootstrap baseEnemyPrefab 자동 link (잔여 (가) PM 직접 처리) PD "결정해야할 안건 없으면 후속 작업 바로 진행해" + 본 PM 자율 처리 — MapAssetBootstrap.cs §1-A 영역 Enemy.prefab 자동 link·MCP run_tests Map 15/15 Passed (0.27s) 총괄PM (pm-general)
2026-05-18 v1.5 — MapResourceRegistryWindow 신규 EditorWindow (Sprite→Tile·GameObject→Prefab 자동 변환 + Catalog 등록) PD 직접 발화 "맵 리소스를 프리팹화 하는것도 툴에서 진행할 수 없을까?" + client-team-lead Opus Task 단일 구현 — Assets/Editor/Map/MapResourceRegistryWindow.cs 514 라인 신규·5종 기능 (A Sprite→Tile·B GameObject→Prefab·C 기존 Asset 직접 등록·D Catalog 미리보기·E 단일 slot)·메뉴 EerieVillage/Map/Resource Registry·MCP 컴파일 0·warning 0·Map 15/15 회귀 0건 클라이언트팀장 (client-team-lead)
2026-05-18 v1.6 — Bootstrap 전체 자동 link 확장 (PD UX 단순화) PD 직접 발화 "툴 기능이 너무 어려운데? 뭘 해야할지 모르겠어" + 본 PM 자율 처리 — MapAssetBootstrap.cs §1-A 8종 슬롯 자동 link (baseEnemyPrefab·playerPrefab·groundTiles 3종·platformLeftEdge·platformMiddle·platformRightEdge·flyingEnemyPrefabs)·PD Inspector 작업 불요·Bootstrap 1회 + Preview Generate + Apply 3단계 영역 즉시 동작·MCP 컴파일 0·Map 15/15 회귀 0 총괄PM (pm-general)
2026-05-19 v1.7 — 배경 이미지 등록·반복 배치 기능 PD 직접 발화 "배경 리소스는 맵 가장 하위 레이어에서 항상 반복해서 배치 + BgImg_1 + 스크롤 맵 배경으로 반복 이어붙여서 재생 + Resource Registry 수정" + client-team-lead Opus 단일 Task — (1) MapResourceCatalog.cs Sprite backgroundSprite 단일 슬롯 추가 (75→79L) (2) MapApplier.cs ApplyBackgroundLayer 헬퍼·BG_ROOT_NAME·SpriteRenderer drawMode=Tiled·tileMode=Continuous·sortingOrder=-1000·맵 전체 size (239→283L) (3) MapResourceRegistryWindow.cs F 섹션 신규 (Sprite ObjectField + Preview thumbnail + Set·Clear) (4) MapAssetBootstrap.cs (9) backgroundSprite ← Assets/Resources/Map/BgImg_1.png 자동 link (193→200L) · MCP 컴파일 0·Map 15/15 PASS 회귀 0 클라이언트팀장 (client-team-lead)
2026-05-19 v1.8 — 배경 위치 정렬 fix (PD "맵 선택 실행해도 배경 로테이션 안 됨") 진단: sprite pivot=(0.5,0.5) center + 기존 position.x=0·z=10 → 배경 영역 x ∈ [-60,+60]·지면 영역 x ∈ [0,120] 불일치·맵 우측 절반 빈 화면. fix: position=(lengthTiles/2, (maxY+minY)/2+6, 0) 맵 중심 정합·heightUnits=(maxY-minY)+6+24 점프 H_max 11.24m×2 여유 반영·z=0 (sortingOrder=-1000 우선)·MCP refresh_unity OK·컴파일 0·Map 15/15 PASS 회귀 0 총괄PM (pm-general)
2026-05-19 v1.9 — BgImg_1 import 설정 fix (PD "이미지가 여러장 겹쳐서 보임") 진단: BgImg_1.png import 영역 spriteMode=2 (Multiple)·spriteMeshType=1 (Tight) → drawMode=Tiled + tileMode=Continuous 영역 정합 X (Tight mesh 영역 trapezoidal 정렬·우측 줄무늬). fix: MCP execute_code 영역 TextureImporter API — spriteImportMode=Single·spriteMeshType=FullRect·spriteExtrude=0·spritesheet 정리·SaveAndReimport. 결과: sprite 1999×845 px·pivot center·PPU=100·Continuous Tiled 정합·Stage1 lengthTiles=120 기준 ≈6장 반복 정상 총괄PM (pm-general)
2026-05-19 v1.10 — Registry 단순화·9방향 지면·발판 2종·monsters 리스트 (PD 명세 4건) PD 직접 발화 4건 (A↔F 순서 변경·8방향 sprite 등록·게임 오브젝트 단순화·기존 폐기 섹션 제거 + ground.png/AirGround_Type1/Type2 자산 사용 + 임시 center 1종 일관 배치) + client-team-lead Opus 단일 Task. 8 파일 변경 + Tile asset 3종 자동 생성: (1) MapResourceCatalog.cs 전면 재작성 (79→49L·14필드 폐기·11신규 — 9 ground TileBase + platformShort/Long + List monsters) (2) MapResourceRegistryWindow.cs 전면 재작성 (587→357L·4신규 섹션 A 배경/B 9방향 3x3 grid/C 발판 2종/D 게임 오브젝트 ReorderableList) (3) MapAssetBootstrap.cs (200→240L·6 자동 link + EnsureSingleSpriteTile TextureImporter 자동 정정) (4) GroundPlanner.cs (73→74L·groundCenter 일관) (5) PlatformPlanner.cs (160→124L·platformShort/Long 폭 기준 선택) (6) SpawnPointPlanner.cs (228→175L·§15-8 폐기·monsters random) (7) MapApplier.cs (287→280L·goalPrefab 제거) (8) MapGeneratorTests.cs (292→280L·MakeCatalog 정합) + Tile asset 3종 자동 생성 (Tiles/Auto/Ground_Center·Platform_Short·Platform_Long) + ground/AirGround_Type1/Type2 png meta spriteMode 2→1 자동 정정. MCP refresh_unity OK·컴파일 0건·warning 0건·Map 15/15 PASS 회귀 0. 백업 9건 (공유/개발팀_백업/EerieVillage/BT13-Dev-Map_20260519/). 잔여: 9-slice 자동 분기 후속 안건·bossPrefab/goalPrefab 후속 PD 등록 클라이언트팀장 (client-team-lead)

§18. 종결

본 Phase 1 설계 완료. Phase 2 진입 전 §15 PD 결정 대기 안건 11건 영역 결정 의무. 결정 후 클라이언트팀장 영역 Phase 2-A 위임 진입.

관련 SOT 영역:

  • 본 문서 = 맵 시스템 설계 SOT (단일)
  • 스킬 시스템 영역 영향 X (BT12 정합)
  • BT5-Dev 발판 시스템 영역 완전 계승 (GameOptimizer·KinematicObject·EnemyController)
  • 기획 §5 자동 생성 vs 수동 배치 영역 PD OVERRIDE 후속 정합 의무 (§15-1)

작성: 개발팀장 (dev-team-lead) 근거: PD 직접 지시 BT13-Dev-Map (2026-05-18) + PD 추가 결정 4건 (시각 시뮬레이터·리소스 단위·랜덤성·거점 밀도) 관련 규칙: C39-10 사전 실측 의무·C49 표준 프로세스·C50 토큰 사전 승인·P18 설계 문서화·P32 맥락 분할 순차 진행