--- 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. 입력 ```csharp [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) ```csharp public class MapBlueprint { public int lengthTiles; public int height; // 청크 영역 최대 높이 public List terrain; // 지면 Tile (Layer 0) public List platforms; // 공중 발판 Tile (Layer 16) public List spawnPoints; // Enemy spawn 거점 public Vector2 startPoint; // Player 시작 좌표 public Vector2 goalPoint; // VictoryZone 좌표 public List backgroundProps; // fence·building 등 배경 Prefab public List 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 정합) ```csharp 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) ```csharp [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 자동 충돌 영역 표준 패턴 ```csharp // 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(); if (tc == null) tc = levelTilemap.gameObject.AddComponent(); tc.compositeOperation = Collider2D.CompositeOperation.Merge; // CompositeCollider2D 통합 (지면 영역 단일 collider — 성능·정합) var cc = levelTilemap.GetComponent(); if (cc == null) cc = levelTilemap.gameObject.AddComponent(); var rb = levelTilemap.GetComponent(); if (rb == null) rb = levelTilemap.gameObject.AddComponent(); 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 영역) ```csharp 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) ```csharp [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 확장) ```csharp 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 폴더) ```csharp // E:/EerieVillage/Assets/Editor/Map/MapPreviewWindow.cs (신규) public class MapPreviewWindow : EditorWindow { [MenuItem("EerieVillage/Map/Preview")] public static void Open() => GetWindow("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) ```csharp [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) ```csharp [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 X~~ → **OVERRIDE 추가** | > | **15-8** | **안 2 — Enemy.prefab + MonsterRandomizer 활용** | ~~Enemy_M00N 직접~~ → **OVERRIDE 안 2** | > | **15-9** | **보스 방 자동 생성 (맵 가장 우측 끝)** | ~~별도 수동 Scene~~ → **OVERRIDE 자동 생성** | > | 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) | --- ## §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 맥락 분할 순차 진행