diff --git a/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md b/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md index 745c2dd..c0d9ed8 100644 --- a/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md +++ b/공유/PD_지시_트래킹/개발팀_PD_지시_로그.md @@ -33,6 +33,9 @@ C3·C13 위반에 해당. **즉시 자진 보고 후 소급 등록**. | # | 일시 | 지시 요지 | 처리 상태 | 산출물 경로 | 중단 사유 | 사후 조치 | |---|------|----------|----------|-----------|----------|----------| +| BT41-FirstRising | 2026-09-28 19:0x | **옆 창 독립 — 좌 · 우 함께 보이기** — PD "각각의 UI는 개별적으로 열 수 있어야하고, 개별적으로 닫을 수도 있는 독립적인 구조여야 해 좌, 우측 영역의 UI가 함께 보여져야하는데 지금은 한쪽이 켜지면 한쪽이 닫히는 현상이 있는거 같아. 체크해보고 수정해줘." | **완료(09-28) · PD 확인("의도한 것이 맞아") · FR `bt/work` `2d510d24c4` push** — 원인 [실측 + 코드]: ① 컨텐츠 ↔ 성장 · 가방을 서로 닫는 코드 ② 장비 · 컨텐츠 · 성장(강화)이 게임의 로비 페이지 전환(한 번에 한 페이지)을 빌려 씀 ③ 성장(강화)과 컨텐츠가 같은 페이지를 통째로 옮겨 씀. 조치: 자리(왼쪽 · 오른쪽)별 관리(`TVSideSlots`) · 창마다 자기 페이지만 켜고 끔 · 성장(강화)은 진화 창만 빌려 옴. 좌 · 우 조합 17 단계 통과 · 에디터 로그 오류 0 [실측] | FR `Assets/BurningTimes/Visual/Scripts/TVSideSlots.cs` · 옆 창 5종 · `공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md` §4 | — | 같은 자리의 창 둘은 함께 열리지 않는다(겹침) — PD 확인 | +| BT40-FirstRising | 2026-09-28 17:20 | **게임 화면 기본 크기 · 세 영역** — PD "매번 에디터 화면이 너무 작아서 불편해. → 따라서 에디터 기준 화면 크기를 2배로 확대한 상태가 기본 크기면 좋겠어. (※추후 옵션에서 기본 화면 크기를 조절할 수 있도록 할 예정이야.)" · (보완) "옆에 발생되는 창도 게임 화면과 동일한 크기로 … 왼쪽 중앙 오른쪽 3 영역의 좌/우길이의 총합이 모니터 화면이 되도록" · (보완) "게임 뷰의 크기를 조절할 경우 잘리는 화면이 존재해. scale 1.0 기준으로 화면을 조절하면 맞춰서 유동적으로 그려지게 해줘" | **완료(09-28) · PD 확인("의도한 것이 맞아") · PC 빌드 확인은 PD 보류 · FR `bt/work` `2d510d24c4` push** — 1차(고정 크기 2배)는 보완 지시로 폐기. 최종: 세 영역의 배치(2200×1280)가 화면에 꽉 차게 배율을 잡음(`TVScreenScale`) · 옆 창 틈/여백 10 · 10 으로 세 영역 같은 크기 · 게임 화면 텍스처를 그려지는 픽셀 수로 · PC 창은 큰 모니터에서 키움(게임 원본 2곳 · 날짜/이유 주석). 게임 뷰 크기 8종에서 세 영역 모두 화면 안 [실측]. PD 에디터 게임 뷰를 자유 비율 · 제 해상도로 맞춤 | FR `TVScreenScale.cs` · `ViewportLayoutController.cs` · `DesktopWidgetManager.cs` · `Editor/TVLobbyScreenSetup.cs` · `공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md` §3 | — | PC 빌드로 창 크기 확인(PD 보류 09-28) · Entry/로딩/스테이지 화면 배율은 게임 원래대로 | +| BT39-FirstRising | 2026-09-28 17:00 | **PC 화면 영역 질문 · 테스트 맵/보스전 화면 구성 통일** — PD "1) 에디터에서 Entry 씬 실행 시 보여지는 화면이 실제 PC에서 게임 실행 시 보여지는 영역이 되는거야? (현재 내 에디터 화면의 검은 부분이 PC 화면이고, 게임이 보여지는 영역은 게임 화면이 떠있는 영역인가에 대한 질문이야.) 2) 테스트 맵(숲맵을 제외한 네가 새로 만든 맵)에 진입 시 캐릭터 상단에 떠있는 창은 제외해줘. (최초 Entry 씬 진입 시 보여지는 화면이 맵을 바꾸거나 보스 전에서도 동일하게 보여져야 해. 따라서 하단의 HP 정보가 보이는 메뉴 등은 맵이 바뀌어도 보여져야 한다는 의미야. 3) 단, 게임 화면 최하단의 "성장, 장비, 특성, 컨텐츠, 길드, 포탈" 메뉴는 보스전에서는 보여지지 않아야 해." | **완료(09-28) · PD 확인("의도한 것이 맞아") · FR `bt/work` `2d510d24c4` push** — ① 답: 에디터의 검은 부분은 PC 화면이 아니라 게임 창의 빈 자리(빌드에서 투명) [코드 + 실측] ② 필드 이동을 "로비 씬은 그대로 · 맵만 바꿔 끼우기"로 바꿈(`TVFieldTravel` · 필드 꾸러미 6종) → 로비 + 맵 6종 같은 화면 구성 · 스테이지용 화면과 머리 위 HP 막대 가림 ③ 보스전 중 하단 메뉴 숨김(7곳). 전체 순회 4회 에디터 로그 오류 0 [실측] | FR `TVFieldTravel.cs` · `TVBottomMenu.cs` · `TVBossArenaUI.cs` · `Editor/Maps/MapFieldKitBuilder.cs` · `Maps/Resources/FieldKits/` · `공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md` §1~2 | — | 빌드에서의 필드 이동은 맵 서브씬을 빌드에 넣어야 함(미반영) | | BT38-FirstRising | 2026-09-28 15:09 | **오염된 던전 바닥 텍스처 개선** — PD "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 다른 맵에 비해 떨어지는 느낌이야. 체크해보고 좀 더 자연스러운 던전 타일처럼 수정해줘." | **완료(09-28 15:37) · PD 확인 대기** — 원인 [실측]: ① 안쪽 장식 띠의 바닥 판 46장이 높이 기준 맞춤 탓에 2~4.9 배(폭 21~69 m)로 커져 바닥 전체를 덮음(BT31 PM 실수) ② 지형 층 텍스처가 512 픽셀(1 m 당 64~109) ③ 돌판 다리도 같은 큰 판. 조치: 띠 제거 · 지형 층을 던전 타일 1024 픽셀 4 m(1 m 당 256 · 돌 한 장 0.5~1 m) + 흙 얼룩 · 다리 판 교체 · 뼈 무더기 네모 판 오려내기. 빌드 OK · 보스 자리 막힘 0 ㎡ · Play 와 전용 필드 R4 에디터 로그 오류 0. PD 답변(09-28 16:17)으로 FR `bt/work` `5f9c8dd297` · 조직 레포 `main` 커밋 · push, 쓰이지 않는 에셋은 지우지 않고 목록으로 정리(맵 폴더 35개 · 10.0 MB), 커밋 규칙은 '지시할 때만' 유지 | FR `Assets/BurningTimes/Maps/PlagueCatacomb/`(정의 · Textures · Terrain · Deck) · `Editor/Maps/MapPropMaterials.cs` · 빌드결과 문서 §9 · `맵/tools/texderive_dungeon.py` · `map_tune5.py` · `맵/미사용_에셋_목록.md` | — | 쓰이지 않는 에셋 삭제는 PD 결정 대기(목록 문서) · 항구 널빤지(1.5~2 배)는 손대지 않음 | | BT37-FirstRising | 2026-09-28 14:53 | **남은 백그라운드 작업 확인 · 작업물 푸시(FR · 조직 레포)** — PD "백그라운드 작업은 왜 남아있지? 작업이 끝났으면 푸시하도록 해" · (후속) "조직 레포도 커밋해야하지 않아?" | **완료(09-28 14:57)** — ① 남아 있던 백그라운드 작업 = 12:03 에 에디터를 다시 띄운 셸(`unity open … | head`). 띄우는 명령은 끝났는데 출력 통로를 에디터가 물고 있어 `head` 가 끝나지 않았다(하는 일 없음) → `head` 만 종료, 에디터는 그대로 [실측] ② BT34~BT36 작업물을 `bt/work` 에 커밋 `1ce5df5b2e`(507개 파일) → push 완료, 원격 = 로컬 [실측]. 에디터가 덧붙인 변경 7개는 넣지 않음 ③ 조직 레포: 커밋 3건(`705986b` 조직 규칙 BT20-Org · `89505fa` 이번 세션 문서/도구/로그 · `c693d2e` 앞선 세션 기록 BT16~BT22) → `main` push 완료(`5698f2c..c693d2e`), 원격 = 로컬 [실측]. 첫 push 는 인증 실패, 재시도에서 성공 | FR `bt/work` `1ce5df5b2e` · 조직 레포 `main` `c693d2e` | — | FR 에셋 백업 폴더(1.2 GB)는 레포에 올리지 않음 — 무시 목록에 넣을지 PD 판단 | | BT36-FirstRising | 2026-09-28 12:5x | **블렌더로 테마별 소품 제작 · 전 맵 꾸미기** — PD "지금까지 만들어진 맵에 오브젝트나 프랍들이 너무 반복적으로 재활용된 느낌이야. 나는 블렌더를 활용해 네가 맵 테마나 컨셉에 맞게 오브젝트나 프랍들을 제작하면 좋겠어. 이번에 추가한 사막맵을 포함해 비주얼적인 완성도를 높이기 위해 자연스럽게 꾸미기 좋은 프랍이나 오브젝트를 블랜더로 추가 제작한 후 배치해서 맵을 꾸며줘. 그리고 모든 맵에 꾸미기 작업이 완성되면 보고해. (나는 상업적인 성공을 위해 비주얼과 완성도를 신경쓰고 싶어.) * 각 맵의 특징과 컨셉을 고려해 자연스럽고 비주얼적인 완성도를 높이는 것이 목표야"(BT35 진행 중 추가 지시) | **완료(09-28) · PD 확인 대기** — 블렌더로 7 테마 67종 제작, 맵 6종에 28~67개 + 숲(로비) 42개 배치. 화면 확인 뒤 6건 수정(검은 풀 · 성채 돌 밝기 · 줄무늬 · 알 발광 · 사막 분위기/바위 · 항구 보스 자리). Play 순회 에디터 로그 오류 0 · 물리 점검 통과 [실측]. **모델 품질은 구매 에셋보다 단순하다(PM 판단)**. `bt/work` `1ce5df5b2e` push(BT37) | FR `Assets/BurningTimes/Maps/_Shared/Props/` · `Editor/Maps/MapPropImporter.cs` · `공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md` §8 · `맵/tools/blender_props/` | — | 소품 품질 PD 판단 대기 · 기기 빌드 미검증 | diff --git a/공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md b/공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md new file mode 100644 index 0000000..a90bca6 --- /dev/null +++ b/공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md @@ -0,0 +1,153 @@ +# FR 화면 구성 — 세 영역 · 필드 이동 · 옆 창 (BT39 · BT40 · BT41) + +> 2026-09-28 · 총괄PM 직접 작업 · FR `bt/work` 작업 트리(미커밋 — 커밋은 PD 지시 시) +> 표기: **[실측]** = 에디터에서 직접 재거나 Play 로 확인 · **[코드]** = 코드 · 문서를 읽고 판단(실행 확인 없음) · **[미검증]** = 확인하지 못함 + +## 0. 상태 한눈에 + +| 안건 | PD 지시 요지 | 상태 | +|---|---|---| +| BT39 ① | 에디터의 검은 부분 = PC 화면인가 | 답변(§1) — 검은 부분은 'PC 화면'이 아니라 **게임 창의 빈 자리** [코드 + 실측] | +| BT39 ② | 테스트 맵에서도 로비와 같은 화면(HP 메뉴 등 유지) · 캐릭터 위 창 제외 | 완료 [실측] — 맵 6종 | +| BT39 ③ | 보스전에서는 최하단 메뉴(성장 · 장비 · 특성 · 컨텐츠 · 길드 · 포탈) 숨김 | 완료 [실측] — 로비 + 맵 6종 | +| BT40 | 화면이 작다 → 기본 크기 키움 · 옆 창도 게임 화면과 같은 크기 · 세 영역의 폭 합 = 모니터 화면 · 게임 뷰 크기에 맞춰 유동 배치 | 에디터 완료 [실측] · **PC 빌드는 [미검증]** | +| BT41 | 옆 창은 각각 열고 닫는 독립 구조 · 좌 · 우 함께 보이기 | 완료 [실측] | + +## 1. 질문 답변 — 에디터의 검은 부분은 무엇인가 (BT39 ①) + +- **검은 부분은 PC 화면(바탕화면)이 아니라 '게임 창' 안의 빈 자리**다. PC 빌드에서는 이 부분이 **투명**해져 바탕화면이 비쳐 보인다. + - 근거 [코드]: `Assets/Scripts/System/Windows/README_위젯모드.md` — "에디터에서는 투명 합성이 되지 않는다 … 게임 뷰가 검게 보이는 건 정상이고, 그 검은 영역이 빌드에서 뚫릴 부분이다." · `Entry.unity` 의 `UniWindowController`(`_isTransparent: 1`) · Win32 코드는 빌드에서만 돈다(`UNITY_STANDALONE_WIN && !UNITY_EDITOR`). +- 게임 창은 **세 영역**으로 짜여 있다 — 왼쪽 창(장비 · 성장 · 특성) · 가운데 게임 화면 · 오른쪽 창(컨텐츠 · 가방). 각 720×1280, 사이 틈과 가장자리 여백을 더해 2200×1280(게임의 기준 배치). +- 에디터의 게임 뷰 = 게임 창이다(모니터가 아니다). 게임 뷰의 해상도가 창의 크기 노릇을 한다. + - 앞서 PD님 게임 뷰는 세로 1080×1920(모바일용 설정)이었다 → 세 영역을 폭 1080 안에 넣느라 0.49 배로 줄어 게임 화면이 353×628 픽셀로 보였다 [실측]. 위아래로 넓은 검은 띠는 이 세로 해상도 때문에 생긴 것이다. +- PC 빌드의 창 크기 [코드 · 미검증]: 기준 2200×1280(`DesktopWidgetManager.m_NormalWindowSize`), 모니터 작업 영역보다 크면 같은 비율로 줄인다. BT40 으로 **기준보다 큰 모니터에서는 같은 비율로 키우게** 바꿨다(§3-3). + +## 2. 필드를 옮겨도 같은 화면 (BT39 ② · ③) + +### 2-1. 원인 [실측] +- 테마 맵 6종은 맵마다 따로 있는 **시험용 메인 씬**(`Assets/Scenes/Maps/<이름>.unity` — 시험 씬 TestVisual 의 사본)을 불러와 보여 줬다. +- 시험용 씬에는 로비의 화면 구성(게임 화면 칸 · HP/스킬 칸 · 하단 메뉴 · 옆 창)이 없고, 스테이지용 화면(시간 · 골드 · 처치 수 · 머리 위 HP 막대)이 그대로 떴다. + +### 2-2. 한 일 +- **어느 필드로 가든 로비 씬을 다시 읽고, 읽히는 순간 숲 맵 대신 고른 필드를 끼운다**(`TVFieldTravel`). 로비 씬 · 로비 프리팹의 내용은 실행 중에만 바꾼다. + 1. 숲 맵의 서브씬 컴포넌트를 끈다(서브씬보다 먼저 실행 — 실행 순서 −790 < 0) → 숲이 읽히지 않는다 + 2. 숲 전용 오브젝트(등불 `TV2_Lights` · `TV2_Diablo_Lights` · 후처리 `Volume_Post` · 캐릭터 조명 `KeyLight_Player` · 몬스터 구역 `MonsterZones`)를 끄고, 그 필드의 **꾸러미 프리팹**을 놓는다 + 3. 그 필드의 맵 서브씬(엔티티 씬)을 GUID 로 읽는다 — 게임이 스테이지 맵을 읽는 방식과 같다 + 4. 안개 · 환경광 · 반사를 그 필드의 값으로 바꾼다(시험용 메인 씬에 적혀 있던 값) +- **필드 꾸러미**(`Assets/BurningTimes/Maps/Resources/FieldKits/FieldKit_<이름>.prefab`) = 시험용 메인 씬에서 맵마다 다른 것만 뜬 것 — 빛 루트(광원 장치 · 점광원 · 바닥 안개 · 빛 입자) · 후처리 볼륨 · 캐릭터 조명 · 몬스터 구역. 맵의 지형 · 소품 · 벽 · 달빛은 맵 서브씬에 있다. +- 맵의 모습은 바꾸지 않았다(조명 · 후처리 · 몬스터 구역은 시험용 씬의 값 그대로). 달라지는 것은 화면 구성(UI)과 BT_Visual 의 공통 설정(로비 값)이다. +- 보스전 중 하단 메뉴 숨김: 보스전 화면(`TVBossArenaUI`)이 보스전에 들어가고 나올 때 하단 메뉴(`TVBottomMenu.SetBattleHidden`)에 알린다. HP · 스킬 칸은 그대로 보인다. +- 캐릭터 머리 위 HP 막대(`GameCanvas/CenterHolder/Health`)를 로비에서 가리는 목록에 보탰다 — 스테이지용이라 로비에서는 게임 화면 왼쪽 밖에 **빨간 줄**로 보이던 것이다 [실측]. + +### 2-3. 결과 [실측] +![로비 · 묘지 · 오염된 던전 · 보스전](img/bt39_lobby_map_boss.jpg) +![폐허 성채 · 거미 동굴 · 항구 · 사막](img/bt39_maps_same_hud.jpg) + +| 확인 | 결과 | +|---|---| +| 로비 + 맵 6종에서 HP/스킬 칸 · 하단 메뉴 · 보스전 버튼 | 모두 보임 | +| 스테이지용 화면(시간 · 골드 · 처치 수 · 스테이지 정보 · 보스 정보 · 머리 위 HP 막대) | 모두 가림 | +| 보스전(전용 필드) 중 하단 메뉴 | 숨김 — 나오면 다시 보임(7곳 모두) | +| 필드 이동 시간 | 6~10초 | +| 에디터 로그 오류 | 0 (전체 순회 4회) | + +### 2-4. 맵을 다시 지었을 때 +1. 맵 빌드(`BurningTimes/Maps/…/Build`) → 2. **필드 꾸러미 만들기**(`BurningTimes/Maps/필드 꾸러미 만들기` — `MapFieldKitBuilder.BuildAll()`) → 3. 보스전 전용 필드(`BurningTimes/Boss/4.`) → 4. 붙이기(`BurningTimes/Boss/3.` — 로비 프리팹의 필드 목록 · 분위기 값이 다시 채워진다) + +## 3. 화면 크기 — 세 영역 (BT40) + +### 3-1. PD 지시(시간순) +1. "매번 에디터 화면이 너무 작아서 불편해. → 에디터 기준 화면 크기를 2배로 확대한 상태가 기본 크기면 좋겠어. (※추후 옵션에서 기본 화면 크기를 조절할 수 있도록 할 예정이야.)" +2. "옆에 발생되는 창도 게임 화면과 동일한 크기로 보여져야 해. 메인 화면만 커지고 좌/우 영역에 뜨는 창이 너무 작아지지 않도록 기본 창 크기를 조절해줘. 내가 원하는건 왼쪽 중앙 오른쪽 3 영역의 좌/우길이의 총합이 모니터 화면이 되도록 하고 싶은거야" +3. "화면이 재생 될 해상도 크기에 맞게 조절되지 않고 넘어서는 것 같아. (예를들어 게임 뷰의 크기를 조절할 경우 잘리는 화면이 존재해. scale 1.0 기준으로 화면을 조절하면 맞춰서 유동적으로 그려지게 해줘." + +### 3-2. 경과 +- 1차(지시 1): 게임 화면을 고정 크기(720×1280 픽셀)로 그리게 했다 → 게임 뷰가 좁으면 옆 창이 남는 폭에 맞춰 0.45 배로 줄었고, 게임 뷰를 줄이면 옆 영역이 화면을 넘었다. **지시 2 · 3 으로 폐기.** +- 최종: **세 영역의 배치(2200×1280)가 화면에 꽉 차게** 배율을 잡는다 — 세 영역은 늘 같은 크기이고, 화면(게임 뷰 · 창)의 크기가 바뀌면 따라 바뀐다. + +### 3-3. 한 일 +| 무엇 | 어디 | 내용 | +|---|---|---| +| 화면 배율 | `TVScreenScale`(BT_Visual · 새 컴포넌트) | 배율 = min(화면 폭 ÷ 2200, 화면 높이 ÷ 1280) × 화면 크기 값(기본 1). 모든 뿌리 캔버스에 같은 값. 창 크기가 바뀔 때만 다시 계산(매 프레임 일 없음). 화면 크기 값은 `TVScreenScale.Instance.scale` 로 바꾼다(추후 옵션용 · 기기에 남음) | +| 옆 창 크기 | 장비 · 성장 · 특성 · 컨텐츠 · 가방 | 틈 24 · 여백 12 → **10 · 10**, 가방 폭 760 → 720. 기준 배치에서 옆 창이 게임 화면과 같은 720×1280 이 된다(앞서는 0.978 배) | +| 게임 화면 텍스처 | `ViewportLayoutController`(게임 원본 · 날짜/이유 주석) | 캔버스 단위(720×1280) 그대로 만들던 것을 **화면에 그려지는 픽셀 수**로 만든다 — 창을 키워도 게임 화면이 흐려지지 않는다 | +| PC 창 기본 크기 | `DesktopWidgetManager`(게임 원본 · 날짜/이유 주석) | 기준(2200×1280)보다 큰 모니터에서는 같은 비율로 키워 작업 영역을 채운다(`m_ExpandToWorkArea` — 끄면 예전대로). **빌드에서만 도는 코드라 [미검증]** | +| 에디터 게임 뷰 | PD님 에디터 설정 | **자유 비율(Free Aspect) · 제 해상도**로 맞춤 — 게임 뷰 크기가 곧 창 크기가 되고 Scale 1x 로 그려진다. 메뉴 `BurningTimes/게임 뷰/…` 로 다시 맞출 수 있다 | + +### 3-4. 결과 [실측] +![게임 뷰 크기에 맞춰지는 배치](img/bt40_fluid_fit.jpg) + +옆 창 둘(장비 · 컨텐츠)을 연 채로 게임 뷰 크기를 바꿔 가며 잰 값 — 모두 세 영역이 화면 안에 들어갔다. + +| 게임 뷰(화면) | 배율 | 영역 하나 | 세 영역이 차지한 폭 | +|---|---|---|---| +| 자유 비율 — 지금 게임 뷰 창(1438×1386) | 0.654 | 471×837 | 7 ~ 1431 (화면 폭 1438) | +| 1280×720 | 0.563 | 405×720 | 27 ~ 1253 | +| 1920×1080 | 0.844 | 608×1080 | 40 ~ 1880 | +| 2560×1440 | 1.125 | 810×1440 | 54 ~ 2506 | +| 3840×2160 | 1.688 | 1215×2160 | 81 ~ 3759 | +| 2503×2256 | 1.138 | 819×1456 | 11 ~ 2492 | +| 1080×1920(앞서 쓰던 설정) | 0.491 | 353×628 | 5 ~ 1075 | +| 720×1280 | 0.327 | 236×419 | 3 ~ 717 | + +- 가로로 넓은 화면(16:9 등)에서는 **높이**가 먼저 닿는다 → 세 영역이 화면 높이를 채우고 폭은 약 96 % 를 쓴다. 세로로 길거나 정사각에 가까운 화면에서는 **폭**이 먼저 닿는다 → 세 영역의 폭 합이 화면 폭이 된다. +- 게임 화면 텍스처는 영역의 픽셀 크기와 같게 만들어진다(예: 3840×2160 에서 1215×2160). + +### 3-5. 에디터에서 크게 보려면 +- 게임 뷰가 곧 창이다 — **게임 뷰를 넓히거나 최대화(Shift+Space)** 하면 그만큼 크게 그려진다. 지금 게임 뷰 창(가로 959 포인트)에서는 영역 하나가 471×837 픽셀이다. +- 고정 해상도(예: 4K UHD)를 고르면 Unity 가 그 해상도로 그린 그림을 게임 뷰에 맞춰 **줄여서**(Scale < 1) 보여 주고, Scale 을 1 로 올리면 **잘라서** 보여 준다(Unity 게임 뷰의 동작 · 게임 쪽에서 바꿀 수 없다). Scale 1 로 잘리지 않게 보려면 자유 비율을 쓴다. + +## 4. 옆 창 — 좌 · 우 독립 (BT41) + +### 4-1. 원인 [실측 + 코드] +1. 컨텐츠 창이 열릴 때 성장 · 가방을 닫고, 성장 · 가방이 열릴 때 컨텐츠를 닫는 코드가 있었다. +2. 장비 · 컨텐츠 · 성장(강화 탭)은 **게임의 로비 페이지**를 빌려 와 옆 자리에 띄운다. 페이지는 로비의 페이지 전환(`MainPageUI.ShowPage`)으로 열었는데, 그것은 **한 번에 한 페이지**만 켠다 → 컨텐츠를 열면 장비 페이지가 꺼지고(장비 창이 닫힘) 그 반대도 같았다. +3. 성장(강화 탭)과 컨텐츠는 같은 페이지(`TrainingPage`)를 통째로 옮겨 썼다 → 한 오브젝트가 두 자리에 있을 수 없었다. + +### 4-2. 한 일 +- **자리별 관리**(`TVSideSlots`): 자리는 왼쪽 · 오른쪽 둘. 창을 열면 **자기 자리에 있던 창만** 닫는다. 반대쪽은 건드리지 않는다. (왼쪽 = 성장 · 장비 · 특성 / 오른쪽 = 컨텐츠 · 가방) +- 페이지는 로비의 페이지 전환을 쓰지 않고 **창마다 자기 페이지만** 켜고 끈다. +- 성장(강화 탭)은 진화 창(`EvolutionPage`)만 빌려 온다 — 그것이 들어 있는 페이지는 컨텐츠 창이 오른쪽에 띄울 수 있다. 컨텐츠 창은 빌려 간 진화 창을 닫지 않는다. + +### 4-3. 결과 [실측] +![왼쪽 · 오른쪽 창이 함께](img/bt41_left_right_together.jpg) + +| 차례로 누른 것 | 왼쪽 자리 | 오른쪽 자리 | +|---|---|---| +| 장비 → 컨텐츠 | 장비 | 컨텐츠 | +| 장비 닫기 → 성장(강화) | 성장 | 컨텐츠(그대로) | +| 컨텐츠 닫기 → 가방 | 성장(그대로) | 가방 | +| 특성(왼쪽 같은 자리) | 특성(성장이 닫힘) | 가방(그대로) | +| 컨텐츠(오른쪽 같은 자리) | 특성(그대로) | 컨텐츠(가방이 닫힘) | +| 성장 → 컨텐츠 닫기 → 컨텐츠 열기 → 성장 닫기 | 열고 닫은 쪽만 바뀜 | 열고 닫은 쪽만 바뀜 | + +- 같은 자리의 창 둘은 겹치므로 함께 열리지 않는다(나중에 연 것이 놓인다). 좌 · 우는 함께 열린다. +- 에디터 로그 오류 0. + +## 5. 남은 것 · 미검증(정직 표기) + +| 항목 | 내용 | +|---|---| +| **PC 빌드 [미검증]** | 창 크기 키우기(`DesktopWidgetManager`)는 빌드에서만 도는 코드다 — 에디터에서 실행되지 않아 확인하지 못했다. 빌드로 확인이 필요하다 | +| **PC 빌드의 필드 이동 [미검증]** | 테마 맵의 서브씬이 빌드에 들어 있어야 한다. 지금은 넣지 않았다 → 빌드에서는 "이 빌드에는 … 필드가 들어 있지 않습니다" 안내만 뜬다(`TVFieldTravel.mapsInBuild` 꺼짐). 앞서(BT34)와 같은 제약이다 | +| Entry · 로딩 · 스테이지 화면 | 화면 배율 컴포넌트는 로비에만 있다. 그 밖의 화면은 게임 원래 배율이다(창 비율이 2200:1280 이면 같고, 에디터처럼 다르면 크기가 조금 다를 수 있다) | +| 가방 창 | 시작할 때 한 번 만든다 — Play 중에 게임 뷰의 가로세로 비율을 크게 바꿔도 다시 만들지 않는다(다른 옆 창은 열 때마다 만든다) | +| 게임 화면 오른쪽 위 흰 네모 둘 | 게임의 버튼(`PlayerInfoButton` · `WidgetModeButton`)에 그림이 없어 흰 네모로 보인다 [실측] — 게임 원본 쪽이라 손대지 않았다 | +| Play 종료 시 예외 1회 | 씬을 읽는 도중에 Play 를 끄면 Unity Physics 패키지의 종료 통계 코드가 예외를 낸다(`AsyncLoadSceneJob …`) [실측 1회 — 도착 3초 뒤 종료]. 게임 코드가 아니다 | +| 게임 화면 텍스처 크기 | 창이 크면 그리는 픽셀이 늘어난다(4K 모니터에서 720×1280 → 약 1150×2050). 성능 영향은 재지 않았다 [미검증] | +| 시험용 메인 씬 | 맵 빌더가 계속 쓴다(지우지 않았다). 그 씬에서 바로 Play 하면 예전 화면 구성으로 보인다 — 필드 이동을 누르면 로비 구성으로 옮겨 간다 | + +## 6. 바뀐 파일(FR · 미커밋) + +| 구분 | 파일 | +|---|---| +| 새 런타임 | `Assets/BurningTimes/Visual/Scripts/TVScreenScale.cs` · `TVSideSlots.cs` | +| 고친 런타임 | `TVFieldTravel.cs` · `TVBottomMenu.cs` · `TVEquipmentPanel.cs` · `TVGrowthPanel.cs` · `TVTraitPanel.cs` · `TVContentsPanel.cs` · `TVInventoryPanel.cs` · `Boss/Scripts/TVBossArenaUI.cs` | +| 게임 원본(날짜/이유 주석) | `Assets/Scripts/UI/Lobby/ViewportLayoutController.cs` · `Assets/Scripts/Manager/DesktopWidgetManager.cs` | +| 새 에디터 도구 | `Assets/BurningTimes/Editor/Maps/MapFieldKitBuilder.cs` · `MapCloneUtility.cs` · `Assets/BurningTimes/Editor/TVLobbyScreenSetup.cs` | +| 고친 에디터 도구 | `Editor/Maps/MapFieldTravelSetup.cs` · `Editor/Boss/BossFieldBuilder.cs`(옮기기 · 충돌체 끄기를 공용 도구로 — 만든 프리팹은 내용이 같음을 확인) | +| 에셋 | `Assets/BurningTimes/Maps/Resources/FieldKits/FieldKit_*.prefab` 6개(새로) · `Assets/GameAssets/LobbyPlayScene.prefab` · `Assets/Scenes/Maps/*.unity` 6개 · `Assets/Scenes/TestVisual.unity`(필드 목록 다시 채움) | + +## 7. 검증 도구 +- `tools/hud_tour.sh <꼬리표> [맵 …]` — 로비 → 보스전 → 맵마다 이동 · 보스전 → 로비 복귀를 돌며 상태와 화면을 남긴다(에디터 로그 기준 오류 집계) +- `tools/unity_eval/` — `hud_probe`(화면 구성) · `panels_probe`(옆 창 열고 닫기 · 세 영역이 화면 안인지) · `layout3_probe`(세 영역 크기) · `scale_probe`(화면 크기 값) · `gameview_*`(게임 뷰 설정 읽기 · 바꾸기) · `hier_dump` · `comp_dump` + `comp_diff.py`(씬 · 프리팹 구성 비교) · `prefab_same.py`(프리팹이 내용으로 같은지) diff --git a/공유/기획/FirstRising/UI/img/bt39_lobby_map_boss.jpg b/공유/기획/FirstRising/UI/img/bt39_lobby_map_boss.jpg new file mode 100644 index 0000000..0d2f8e5 Binary files /dev/null and b/공유/기획/FirstRising/UI/img/bt39_lobby_map_boss.jpg differ diff --git a/공유/기획/FirstRising/UI/img/bt39_maps_same_hud.jpg b/공유/기획/FirstRising/UI/img/bt39_maps_same_hud.jpg new file mode 100644 index 0000000..be4bab3 Binary files /dev/null and b/공유/기획/FirstRising/UI/img/bt39_maps_same_hud.jpg differ diff --git a/공유/기획/FirstRising/UI/img/bt40_fluid_fit.jpg b/공유/기획/FirstRising/UI/img/bt40_fluid_fit.jpg new file mode 100644 index 0000000..d683855 Binary files /dev/null and b/공유/기획/FirstRising/UI/img/bt40_fluid_fit.jpg differ diff --git a/공유/기획/FirstRising/UI/img/bt41_left_right_together.jpg b/공유/기획/FirstRising/UI/img/bt41_left_right_together.jpg new file mode 100644 index 0000000..706a3cd Binary files /dev/null and b/공유/기획/FirstRising/UI/img/bt41_left_right_together.jpg differ diff --git a/공유/기획/FirstRising/UI/tools/bt.sh b/공유/기획/FirstRising/UI/tools/bt.sh new file mode 100644 index 0000000..e2d8f39 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/bt.sh @@ -0,0 +1,149 @@ +#!/bin/bash +# BT27 검증용 도우미 — source 해서 쓴다 +export PYTHONIOENCODING=utf-8 +P="C:/BurningTimes/FirstRising" +SP="C:/Users/sw/AppData/Local/Temp/claude/E--BurningTimes/3562a2c1-e88a-4143-b7ec-e74c317778d8/scratchpad" + +# ev <스크립트 이름> [인수...] — eval_file 실행 후 결과 글자만 출력 +ev() { + local f="$1"; shift + if [ $# -gt 0 ]; then printf "%s" "$*" > "$SP/eval/args.txt"; fi + unity command eval_file --file "$SP/eval/$f.cs" --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json +raw = sys.stdin.read() +try: + d = json.loads(raw) +except Exception as ex: + print('PARSE FAIL', raw[:600]); sys.exit(0) +r = (d.get('data') or {}).get('result') +if isinstance(r, dict): + print(r.get('result')) + if r.get('error'): print('EVAL ERROR', r.get('error'), r.get('errorDetails')) + for x in (r.get('diagnostics') or []): print('DIAG', x) +else: + print(r) +if d.get('errors'): print('ERR', d.get('errors')) +" +} + +# st — Play 여부 단독 확인 +st() { + unity command editor_status --project-path "$P" --format json --no-banner 2>&1 | grep -E '"playMode"|"compiling"|"status"|domainReload' | tr -d ' \n'; echo +} + +# errs — 콘솔 오류 목록 +errs() { + unity command get_console_logs --severity error --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json +d = json.loads(sys.stdin.read()) +r = (d.get('data') or {}).get('result') or {} +print('errors total', r.get('total')) +seen = {} +for l in (r.get('logs') or []): + m = (l.get('message') or '')[:300] + seen[m] = seen.get(m, 0) + 1 +for m, c in seen.items(): + print(' x%d %s' % (c, m.replace('\n', ' | '))) +" +} + +# warns — 콘솔 경고 목록 +warns() { + unity command get_console_logs --severity warning --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json +d = json.loads(sys.stdin.read()) +r = (d.get('data') or {}).get('result') or {} +print('warnings total', r.get('total')) +seen = {} +for l in (r.get('logs') or []): + m = (l.get('message') or '')[:300] + seen[m] = seen.get(m, 0) + 1 +for m, c in seen.items(): + print(' x%d %s' % (c, m.replace('\n', ' | '))) +" +} + +# cap <이름> [너비] [높이] — 게임 화면을 찍어 스크래치패드에 저장(검정 여백은 잘라낸다) +cap() { + local name="$1"; local w="${2:-1080}"; local h="${3:-1920}" + unity command capture_game_view --source screen --width "$w" --height "$h" --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json, base64, io +d = json.loads(sys.stdin.read()) +r = (d.get('data') or {}).get('result') or {} +def find(o): + if isinstance(o, dict): + for k, v in o.items(): + if isinstance(v, str) and len(v) > 1000: + return v + f = find(v) + if f: return f + if isinstance(o, list): + for v in o: + f = find(v) + if f: return f + return None +b = find(r) +if not b: + print('NO IMAGE', json.dumps(d)[:500]); sys.exit(0) +data = base64.b64decode(b) +out = r'$SP/cap/$name.png' +try: + from PIL import Image + im = Image.open(io.BytesIO(data)).convert('RGB') + gray = im.convert('L').point(lambda p: 255 if p > 10 else 0) + box = gray.getbbox() + if box: im = im.crop(box) + mx = 900 + if max(im.size) > mx: + k = mx / float(max(im.size)) + im = im.resize((int(im.size[0] * k), int(im.size[1] * k)), Image.LANCZOS) + im.save(out) + print('saved', out, im.size) +except ImportError: + open(out, 'wb').write(data) + print('saved raw', out, len(data)) +" +} + +# rc — 재컴파일하고 끝날 때까지 기다린 뒤 결과와 콘솔 오류를 보여 준다(Play 여부는 먼저 st 로 따로 확인할 것) +rc() { + unity command recompile --project-path "$P" --format json --no-banner 2>&1 | grep -E '"status"' | head -1 + local s="" + for i in $(seq 1 100); do + sleep 3 + s=$(unity command recompile_status --project-path "$P" --format json --no-banner 2>&1 | grep -E '"result"' | head -1) + case "$s" in *'status\":\"completed'*|*'status\":\"up_to_date'*|*'status\":\"idle'*|*'failed\":true'*) break;; esac + done + echo "recompile: $s" + errs +} + +# bld — 보스 패턴 빌드(분리 실행) 후 보고서 머리·꼬리와 콘솔 오류 +bld() { + local J=$(unity command eval_file --file "$SP/eval/boss_build.cs" --project-path "$P" --detach --format json --no-banner 2>&1 | grep -o '"jobId": *"[a-f0-9]*"' | grep -o '[a-f0-9]\{32\}') + unity job wait "$J" --project-path "$P" --format json --no-banner 2>&1 | grep -E '"state"|"result": "|"executionTimeMs"|"error": "' + head -3 "$P/Assets/BurningTimes/Boss/Data/BuildReport.txt" + grep -n "소환물\|\[주의\]" "$P/Assets/BurningTimes/Boss/Data/BuildReport.txt" + tail -3 "$P/Assets/BurningTimes/Boss/Data/BuildReport.txt"; echo + errs +} + +# play — Play 진입 후 월드 준비까지 기다린다 +play() { + unity command clear_console --project-path "$P" --format json --no-banner >/dev/null 2>&1 + unity command editor_play --project-path "$P" --format json --no-banner 2>&1 | grep -E '"success"' | head -1 + for i in $(seq 1 60); do + sleep 3 + local r=$(ev probe 2>&1 | head -1) + case "$r" in *"config=True"*"player=True"*"world=True"*) echo "$i $r"; break;; esac + done + sleep 5 +} + +# stop — 감시자를 빼고 Play 를 끝낸다 +stop() { + ev monitor_remove + unity command editor_stop --project-path "$P" --format json --no-banner 2>&1 | grep -E '"success"' | head -1 + sleep 4 + st +} diff --git a/공유/기획/FirstRising/UI/tools/capfull.sh b/공유/기획/FirstRising/UI/tools/capfull.sh new file mode 100644 index 0000000..8a322ce --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/capfull.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# 게임 화면을 검정 여백까지 그대로 찍는다(UI 포함). source capfull.sh 뒤 capfull <이름> +# bt.sh 의 cap 은 검정 여백을 잘라 내므로, 화면 전체의 구성(어디가 비고 어디에 게임이 그려지는지)을 볼 때는 이것을 쓴다 +capfull() { + local name="$1" + unity command capture_game_view --source screen --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json, base64, io +d = json.loads(sys.stdin.read()) +r = (d.get('data') or {}).get('result') or {} +def find(o): + if isinstance(o, dict): + for k, v in o.items(): + if isinstance(v, str) and len(v) > 1000: + return v + f = find(v) + if f: return f + if isinstance(o, list): + for v in o: + f = find(v) + if f: return f + return None +def meta(o, keys=('width', 'height', 'source')): + out = {} + if isinstance(o, dict): + for k, v in o.items(): + if k in keys and not isinstance(v, (dict, list)): out[k] = v + elif isinstance(v, dict): out.update(meta(v, keys)) + return out +b = find(r) +if not b: + print('NO IMAGE', json.dumps(d)[:500]); sys.exit(0) +data = base64.b64decode(b) +out = r'$SP/cap/$name.png' +from PIL import Image +im = Image.open(io.BytesIO(data)).convert('RGB') +full = im.size +gray = im.convert('L').point(lambda p: 255 if p > 10 else 0) +box = gray.getbbox() +im.save(out) +print('saved', out, 'size', full, 'meta', meta(r), '검정이 아닌 영역', box) +" +} diff --git a/공유/기획/FirstRising/UI/tools/comp_diff.py b/공유/기획/FirstRising/UI/tools/comp_diff.py new file mode 100644 index 0000000..dd0add9 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/comp_diff.py @@ -0,0 +1,48 @@ +# 두 구성 요소 값 기록(comp_dump 의 출력)을 견준다. python comp_diff.py <왼쪽 파일> <오른쪽 파일> [--only-common] +# · 같은 열쇠(오브젝트 | 구성 요소 | 속성)의 값이 다르면 적고, 한쪽에만 있는 구성 요소는 이름만 적는다 +import sys, io, collections + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + + +def load(path): + rows = collections.OrderedDict() + objects = [] + for line in io.open(path, encoding='utf-8'): + line = line.rstrip('\n') + if line.startswith('## '): + objects.append(line[3:]) + continue + parts = line.split(' | ', 2) + if len(parts) < 3 or ' = ' not in parts[2]: + continue + prop, value = parts[2].split(' = ', 1) + rows[(parts[0], parts[1], prop)] = value + return rows, objects + + +left, left_objects = load(sys.argv[1]) +right, right_objects = load(sys.argv[2]) +left_comps = set((k[0], k[1]) for k in left) +right_comps = set((k[0], k[1]) for k in right) + +print('== 왼쪽에만 있는 구성 요소') +for c in sorted(left_comps - right_comps): + print(' ', c[0], '|', c[1]) +print('== 오른쪽에만 있는 구성 요소') +for c in sorted(right_comps - left_comps): + print(' ', c[0], '|', c[1]) +print('== 값이 다른 속성(왼쪽 → 오른쪽)') +count = 0 +for k, v in left.items(): + if k in right and right[k] != v: + print(' ', k[0], '|', k[1], '|', k[2], ':', v, '→', right[k]) + count += 1 +print('다른 값', count) +print('== 한쪽에만 있는 속성(같은 구성 요소 안)') +for k in left: + if k not in right and (k[0], k[1]) in right_comps: + print(' 왼쪽만', k[0], '|', k[1], '|', k[2], '=', left[k]) +for k in right: + if k not in left and (k[0], k[1]) in left_comps: + print(' 오른쪽만', k[0], '|', k[1], '|', k[2], '=', right[k]) diff --git a/공유/기획/FirstRising/UI/tools/evd.sh b/공유/기획/FirstRising/UI/tools/evd.sh new file mode 100644 index 0000000..e508cf1 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/evd.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# 오래 걸리는 eval 을 분리 실행(--detach)으로 돌리고 끝날 때까지 기다린다. source evd.sh 뒤 evd <스크립트 이름> [인수...] +# CLI 는 5초를 넘기면 에디터 콘솔에 빨간 오류를 남기므로, 씬을 여는 일처럼 긴 것은 이것으로 돌린다 +evd() { + local f="$1"; shift + if [ $# -gt 0 ]; then printf "%s" "$*" > "$SP/eval/args.txt"; fi + local J=$(unity command eval_file --file "$SP/eval/$f.cs" --project-path "$P" --detach --format json --no-banner 2>&1 | grep -o '"jobId": *"[a-f0-9]*"' | grep -o '[a-f0-9]\{32\}') + if [ -z "$J" ]; then echo "작업 번호를 받지 못함"; return 1; fi + unity job wait "$J" --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json +raw = sys.stdin.read() +try: + d = json.loads(raw) +except Exception: + print('PARSE FAIL', raw[:800]); sys.exit(0) +data = d.get('data') or {} +job = data.get('job') or data +res = job.get('result') +if isinstance(res, dict): + inner = res.get('result') + print(inner if inner is not None else json.dumps(res, ensure_ascii=False)[:1500]) + if res.get('error'): print('EVAL ERROR', res.get('error'), res.get('errorDetails')) +else: + print('state', job.get('state'), '| result', res, '| error', job.get('error')) +if d.get('errors'): print('ERR', d.get('errors')) +" +} diff --git a/공유/기획/FirstRising/UI/tools/hud_tour.sh b/공유/기획/FirstRising/UI/tools/hud_tour.sh new file mode 100644 index 0000000..0b8721f --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/hud_tour.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# BT39 검증 — 로비 → 보스전 → 필드 이동(맵마다) → 보스전 → 로비 복귀를 돌며 화면 구성을 찍고 상태를 적는다. +# bash hud_tour.sh <꼬리표> [맵 이름 ...] 맵 이름을 주지 않으면 여섯 맵 모두 +# Entry 씬에서 Play 를 띄우고, 끝나면 바로 끈다(내가 띄운 Play 를 남기지 않는다). 오류는 에디터 로그 기준으로 센다. +SP="C:/Users/sw/AppData/Local/Temp/claude/E--BurningTimes/3562a2c1-e88a-4143-b7ec-e74c317778d8/scratchpad" +source "$SP/bt.sh" +source "$SP/shot.sh" +export PYTHONIOENCODING=utf-8 +TAG="${1:-v}"; shift +MAPS="$*" +[ -z "$MAPS" ] && MAPS="Graveyard RuinedKeep SpiderCave PlagueCatacomb HarborExecution ScorchedDesert" +# 필드마다 상대할 보스(전용 필드의 짝과 같은 테마) +boss_of() { case "$1" in Graveyard) echo R1;; RuinedKeep) echo R2;; SpiderCave) echo R3;; PlagueCatacomb) echo R4;; HarborExecution) echo R5;; ScorchedDesert) echo C1;; *) echo R1;; esac; } + +# 옮겨 가기가 끝날 때까지 기다린다(최대 60초) +wait_travel() { + for k in $(seq 1 30); do + sleep 2 + local s=$(ev travel state 2>&1 | head -1) + case "$s" in *"traveling=False"*"player=True"*) echo " 도착 ${k}회째: $(echo "$s" | cut -c1-150)"; return 0;; esac + done + echo " (주의) 옮겨 가기가 60초 안에 끝나지 않음: $s"; return 1 +} + +# 보스전에 들어가 화면을 찍고 나온다 +boss_round() { + local name="$1" boss="$2" + echo " 보스전 입장 $boss: $(ev arena enter $boss | head -1)" + sleep 6 + ev hud_probe | head -3 + shot "hud_${name}_boss_${TAG}" + echo " 보스전 퇴장: $(ev arena exit | head -1)" + sleep 3 + ev hud_probe | sed -n '3p' +} + +st +python "$SP/logscan.py" mark +unity command clear_console --project-path "$P" --format json --no-banner >/dev/null 2>&1 +unity command editor_play --project-path "$P" --format json --no-banner 2>&1 | grep -E '"success"' | head -1 +for i in $(seq 1 40); do + sleep 3 + r=$(ev lobby_ready 2>&1 | head -1) + case "$r" in *"scene=Lobby"*"player=True"*"config=True"*) echo "ready $i"; break;; esac +done +sleep 6 + +echo "== 로비(숲)" +ev hud_probe +shot "hud_Lobby_${TAG}" +boss_round Lobby R1 + +for m in $MAPS; do + echo "== $m" + echo " 이동: $(ev travel go $m | head -1 | cut -c1-160)" + wait_travel + sleep 4 + ev hud_probe + shot "hud_${m}_${TAG}" + boss_round "$m" "$(boss_of $m)" +done + +echo "== 로비로 복귀" +echo " 이동: $(ev travel go Lobby | head -1 | cut -c1-160)" +wait_travel +sleep 4 +ev hud_probe +shot "hud_LobbyBack_${TAG}" +sleep 12 + +unity command editor_stop --project-path "$P" --format json --no-banner 2>&1 | grep -E '"success"' | head -1 +sleep 5 +st +python "$SP/logscan.py" scan "hud-tour-$TAG" diff --git a/공유/기획/FirstRising/UI/tools/logscan.py b/공유/기획/FirstRising/UI/tools/logscan.py new file mode 100644 index 0000000..b99798c --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/logscan.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# 에디터 로그(Editor.log)를 기준으로 오류를 센다. 콘솔 조회(get_console_logs)는 엔진이 직접 찍는 오류(BatchDrawCommand 등)를 놓쳤다. +# python logscan.py mark 지금 로그 크기를 적어 둔다 +# python logscan.py scan [이름] 적어 둔 자리부터 끝까지에서 오류 머리줄을 센다 +import io, os, re, sys, collections + +LOG = r"C:\Users\sw\AppData\Local\Unity\Editor\Editor.log" +MARK = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logscan.mark") + +# 도구가 내는 메시지(게임 오류 아님) +IGNORE = ("Failed to handle /api/exec request",) + +def read_from(offset): + with open(LOG, "rb") as f: + f.seek(offset) + return f.read().decode("utf-8", errors="replace") + +def main(): + cmd = sys.argv[1] if len(sys.argv) > 1 else "scan" + if cmd == "mark": + size = os.path.getsize(LOG) + io.open(MARK, "w").write(str(size)) + print("mark", size) + return + offset = int(io.open(MARK).read().strip()) if os.path.exists(MARK) else 0 + size = os.path.getsize(LOG) + if offset > size: + offset = 0 # 에디터가 다시 떠서 로그가 새로 시작됨 + text = read_from(offset) + lines = text.split("\n") + heads = collections.Counter() + unloads = 0 + for i, l in enumerate(lines): + l = l.rstrip("\r") + if l.startswith("Unloading ") and "unused Assets" in l: + unloads += 1 + head = None + if l.startswith("A BatchDrawCommand was submitted"): + head = "A BatchDrawCommand was submitted with an invalid Batch, Mesh, or Material ID" + elif l.startswith(("UnityEngine.Debug:LogError", "UnityEngine.Debug:LogException", "UnityEngine.Debug:LogAssertion", "UnityEngine.Debug:LogErrorFormat")): + j = i + while j > 0 and not lines[j].startswith("UnityEngine.Debug:ExtractStackTraceNoAlloc"): + j -= 1 + head = lines[j - 1].strip()[:200] if j > 0 else "?" + elif re.match(r"^[A-Za-z_\.]*Exception: ", l) or re.match(r"^[A-Za-z_\.]+Exception$", l.strip()): + head = l.strip()[:200] + elif l.startswith(("Assertion failed", "Some objects were not cleaned up", "Can't destroy", "Destroying GameObjects immediately")): + head = l.strip()[:200] + elif "error CS" in l: + head = l.strip()[:200] + if head is None: + continue + if any(x in head for x in IGNORE): + continue + heads[re.sub(r"\d+", "#", head)] += 1 + name = sys.argv[2] if len(sys.argv) > 2 else "" + total = sum(heads.values()) + print("LOGSCAN %s bytes=%d lines=%d unloadUnused=%d errors=%d" % (name, size - offset, len(lines), unloads, total)) + for h, c in heads.most_common(20): + print(" x%d %s" % (c, h)) + +main() diff --git a/공유/기획/FirstRising/UI/tools/prefab_same.py b/공유/기획/FirstRising/UI/tools/prefab_same.py new file mode 100644 index 0000000..b99cf3e --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/prefab_same.py @@ -0,0 +1,33 @@ +# 프리팹 파일 두 판(커밋된 것 · 작업 트리의 것)이 '내용으로' 같은지 본다 — 저장할 때마다 바뀌는 파일 안 번호(fileID)는 빼고 견준다. +# python prefab_same.py <저장소 폴더> <프리팹 경로> [...] +# · 오브젝트 종류별 수, 이름 · 값 줄의 묶음(번호를 # 로 바꾼 뒤 정렬)이 같으면 같은 내용으로 본다 +import sys, io, re, subprocess, collections + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +repo = sys.argv[1] +ID = re.compile(r'(fileID: |&)-?\d+') + + +def norm(text): + kinds = collections.Counter() + lines = collections.Counter() + for line in text.split('\n'): + line = line.rstrip('\r') + if line.startswith('--- !u!'): + kinds[line.split(' ')[1]] += 1 + continue + lines[ID.sub(r'\1#', line)] += 1 + return kinds, lines + + +for path in sys.argv[2:]: + old = subprocess.run(['git', '-C', repo, 'show', 'HEAD:' + path], capture_output=True).stdout.decode('utf-8', errors='replace') + new = io.open(repo + '/' + path, encoding='utf-8', errors='replace').read() + ok, ol = norm(old) + nk, nl = norm(new) + same_kinds = ok == nk + diff = (ol - nl) + (nl - ol) + print(path.split('/')[-1], '| 오브젝트 종류별 수 같음' if same_kinds else '| 오브젝트 수 다름', '| 다른 줄', sum(diff.values())) + for line, count in list(diff.most_common(12)): + side = '커밋' if ol[line] > nl[line] else '작업' + print(' x%d [%s쪽이 많음] %s' % (count, side, line.strip()[:150])) diff --git a/공유/기획/FirstRising/UI/tools/shot.sh b/공유/기획/FirstRising/UI/tools/shot.sh new file mode 100644 index 0000000..30508eb --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/shot.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# 게임 화면을 지금 게임 뷰의 크기 그대로 찍는다(UI 포함 · 검정 여백도 그대로). source shot.sh 뒤 shot <이름> [너비 높이] +# 너비 · 높이를 주지 않으면 실행 중인 게임의 화면 크기(Screen)를 물어서 쓴다 — 다른 크기로 찍으면 화면 구성이 달라진다 +shot() { + local name="$1"; local w="$2"; local h="$3" + if [ -z "$w" ] || [ -z "$h" ]; then + local s=$(ev screen_size 2>&1 | head -1) + w=$(echo "$s" | cut -d' ' -f1); h=$(echo "$s" | cut -d' ' -f2) + fi + unity command capture_game_view --source screen --width "$w" --height "$h" --project-path "$P" --format json --no-banner 2>&1 | python -c " +import sys, json, base64, io +d = json.loads(sys.stdin.read()) +r = (d.get('data') or {}).get('result') or {} +def find(o): + if isinstance(o, dict): + for k, v in o.items(): + if isinstance(v, str) and len(v) > 1000: return v + f = find(v) + if f: return f + if isinstance(o, list): + for v in o: + f = find(v) + if f: return f + return None +b = find(r) +if not b: + print('NO IMAGE', json.dumps(d)[:400]); sys.exit(0) +from PIL import Image +im = Image.open(io.BytesIO(base64.b64decode(b))).convert('RGB') +gray = im.convert('L').point(lambda p: 255 if p > 10 else 0) +im.save(r'$SP/cap/$name.png') +print('saved $name', im.size, '검정이 아닌 영역', gray.getbbox()) +" +} diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/comp_dump.cs b/공유/기획/FirstRising/UI/tools/unity_eval/comp_dump.cs new file mode 100644 index 0000000..a13fe89 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/comp_dump.cs @@ -0,0 +1,141 @@ +// 오브젝트(와 그 아래)의 구성 요소 값을 파일로 적는다(편집 상태 · 읽기 전용) — 두 씬의 설정을 견줄 때 쓴다. +// args.txt = " <에셋 경로> <오브젝트 경로(루트부터 / 로 이음)> <깊이> <출력 파일 이름>" +var args = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().Split(' '); +string kind = args[0], path = args[1], objectPath = args[2]; +int maxDepth = int.Parse(args[3]); +string outPath = @"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\out\" + args[4] + ".txt"; +System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outPath)); +if (UnityEditor.EditorApplication.isPlaying) + return "Play 중 — 편집 상태에서만 쓴다"; +var sb = new System.Text.StringBuilder(); +int lines = 0; +System.Func refName = (o) => +{ + if (o == null) return "null"; + string asset = UnityEditor.AssetDatabase.GetAssetPath(o); + if (!string.IsNullOrEmpty(asset) && !asset.EndsWith(".unity") && asset != path) return "[" + asset + "]" + o.name; + var c = o as UnityEngine.Component; + var g = o as UnityEngine.GameObject; + var t = c != null ? c.transform : (g != null ? g.transform : null); + if (t == null) return o.name + "(" + o.GetType().Name + ")"; + string p = t.name; + var q = t.parent; + while (q != null) { p = q.name + "/" + p; q = q.parent; } + return "{" + p + "}" + (c != null ? ":" + c.GetType().Name : ""); +}; +System.Func valueOf = (p) => +{ + switch (p.propertyType) + { + case UnityEditor.SerializedPropertyType.Integer: return p.longValue.ToString(); + case UnityEditor.SerializedPropertyType.Boolean: return p.boolValue ? "true" : "false"; + case UnityEditor.SerializedPropertyType.Float: return p.floatValue.ToString("0.####"); + case UnityEditor.SerializedPropertyType.String: return "\"" + p.stringValue + "\""; + case UnityEditor.SerializedPropertyType.Color: return p.colorValue.ToString("0.###"); + case UnityEditor.SerializedPropertyType.ObjectReference: return refName(p.objectReferenceValue); + case UnityEditor.SerializedPropertyType.LayerMask: return "mask " + p.intValue; + case UnityEditor.SerializedPropertyType.Enum: return "enum " + p.intValue; + case UnityEditor.SerializedPropertyType.Vector2: return p.vector2Value.ToString("0.###"); + case UnityEditor.SerializedPropertyType.Vector3: return p.vector3Value.ToString("0.###"); + case UnityEditor.SerializedPropertyType.Vector4: return p.vector4Value.ToString("0.###"); + case UnityEditor.SerializedPropertyType.Quaternion: return p.quaternionValue.eulerAngles.ToString("0.#"); + case UnityEditor.SerializedPropertyType.Rect: return p.rectValue.ToString(); + case UnityEditor.SerializedPropertyType.ArraySize: return "size " + p.intValue; + case UnityEditor.SerializedPropertyType.AnimationCurve: return "curve keys " + (p.animationCurveValue != null ? p.animationCurveValue.length : 0); + case UnityEditor.SerializedPropertyType.Vector2Int: return p.vector2IntValue.ToString(); + case UnityEditor.SerializedPropertyType.Bounds: return p.boundsValue.ToString(); + default: return null; + } +}; +System.Action walk = null; +walk = (t, rel, depth) => +{ + sb.Append("## ").Append(rel).Append(t.gameObject.activeSelf ? "" : " [꺼짐]").Append(" layer ").Append(t.gameObject.layer).Append('\n'); + var seen = new System.Collections.Generic.Dictionary(); + foreach (var c in t.GetComponents()) + { + if (c == null) { sb.Append(rel).Append(" | (없는 스크립트)\n"); continue; } + string cn = c.GetType().Name; + int n; + seen.TryGetValue(cn, out n); + seen[cn] = n + 1; + if (n > 0) cn += "#" + n; + var so = new UnityEditor.SerializedObject(c); + var p = so.GetIterator(); + bool enter = true; + while (p.Next(enter)) + { + enter = true; + if (p.propertyPath == "m_Script" || p.propertyPath.StartsWith("m_ObjectHideFlags") || p.propertyPath.StartsWith("m_CorrespondingSourceObject") + || p.propertyPath.StartsWith("m_PrefabInstance") || p.propertyPath.StartsWith("m_PrefabAsset") || p.propertyPath == "m_GameObject" + || p.propertyPath.StartsWith("m_EditorHideFlags") || p.propertyPath.StartsWith("m_EditorClassIdentifier") || p.propertyPath == "m_Name") + { enter = false; continue; } + if (c is UnityEngine.Transform && (p.propertyPath.StartsWith("m_Children") || p.propertyPath == "m_Father" || p.propertyPath.StartsWith("m_LocalEulerAnglesHint"))) + { enter = false; continue; } + if (p.propertyType == UnityEditor.SerializedPropertyType.String || p.propertyType == UnityEditor.SerializedPropertyType.AnimationCurve || p.propertyType == UnityEditor.SerializedPropertyType.Gradient) + enter = false; + string v = valueOf(p); + if (v == null) continue; + if (p.propertyType != UnityEditor.SerializedPropertyType.ArraySize && p.hasChildren && p.propertyType != UnityEditor.SerializedPropertyType.ObjectReference) + enter = false; + sb.Append(rel).Append(" | ").Append(cn).Append(" | ").Append(p.propertyPath).Append(" = ").Append(v).Append('\n'); + lines++; + } + } + if (depth >= maxDepth) return; + var names = new System.Collections.Generic.Dictionary(); + foreach (UnityEngine.Transform c in t) + { + int k; + names.TryGetValue(c.name, out k); + names[c.name] = k + 1; + walk(c, rel + "/" + c.name + (k > 0 ? "~" + k : ""), depth + 1); + } +}; +System.Func descend = null; +descend = (t, parts, index) => +{ + if (index >= parts.Length) return t; + foreach (UnityEngine.Transform c in t) + if (c.name == parts[index]) + { + var hit = descend(c, parts, index + 1); + if (hit != null) return hit; + } + return null; +}; +var partsAll = objectPath.Split('/'); +UnityEngine.Transform target = null; +UnityEngine.SceneManagement.Scene scene = default(UnityEngine.SceneManagement.Scene); +bool opened = false; +if (kind == "prefab") +{ + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) return "프리팹 없음: " + path; + if (go.name == partsAll[0]) target = descend(go.transform, partsAll, 1); +} +else +{ + scene = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(path); + opened = !(scene.IsValid() && scene.isLoaded); + if (opened) + scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, UnityEditor.SceneManagement.OpenSceneMode.Additive); + foreach (var root in scene.GetRootGameObjects()) + if (root.name == partsAll[0]) + { + target = descend(root.transform, partsAll, 1); + if (target != null) break; + } +} +string result; +if (target == null) + result = "오브젝트 없음: " + objectPath; +else +{ + walk(target, target.name, 0); + System.IO.File.WriteAllText(outPath, sb.ToString()); + result = "값 " + lines + "줄 → " + outPath; +} +if (opened) + UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true); +return result; diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/fieldkit_build.cs b/공유/기획/FirstRising/UI/tools/unity_eval/fieldkit_build.cs new file mode 100644 index 0000000..ef6c3d6 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/fieldkit_build.cs @@ -0,0 +1,7 @@ +// 필드 꾸러미 프리팹을 만든다(편집 모드 · 분리 실행). args: 맵 이름 하나 또는 all +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) + return "SKIP playing"; +string only = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim(); +if (only == "" || only == "all") + return BurningTimes.EditorTools.Maps.MapFieldKitBuilder.BuildAll(); +return BurningTimes.EditorTools.Maps.MapFieldKitBuilder.Build(only); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/gameview_like_window.cs b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_like_window.cs new file mode 100644 index 0000000..93f08b9 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_like_window.cs @@ -0,0 +1,3 @@ +// 게임 뷰를 PC 창처럼 보게 맞춘다(에디터 설정). args.txt = "max" | "docked" +var arg = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim(); +return BurningTimes.EditorTools.TVLobbyScreenSetup.SetGameViewLikeWindow(arg == "max"); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/gameview_lowres.cs b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_lowres.cs new file mode 100644 index 0000000..f40dc94 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_lowres.cs @@ -0,0 +1,28 @@ +// 게임 뷰의 '낮은 해상도로 그리기(Low Resolution Aspect Ratios)' 설정을 읽거나 바꾼다(에디터 설정). args.txt = "get" | "on" | "off" +var arg = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim(); +var asm = typeof(UnityEditor.Editor).Assembly; +var gvType = asm.GetType("UnityEditor.GameView"); +var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; +var sb = new System.Text.StringBuilder(); +sb.Append("pixelsPerPoint ").Append(UnityEditor.EditorGUIUtility.pixelsPerPoint).Append(" | "); +foreach (var o in UnityEngine.Resources.FindObjectsOfTypeAll(gvType)) +{ + var w = (UnityEditor.EditorWindow)o; + var prop = gvType.GetProperty("lowResolutionForAspectRatios", flags); + var forced = gvType.GetProperty("forceLowResolutionAspectRatios", flags); + sb.Append("지금 ").Append(prop.GetValue(o)).Append(" 강제 ").Append(forced != null ? forced.GetValue(o).ToString() : "?"); + if (arg == "on" || arg == "off") + { + prop.SetValue(o, arg == "on"); + var update = gvType.GetMethod("UpdateZoomAreaAndParent", flags); + if (update != null) update.Invoke(o, null); + w.Repaint(); + sb.Append(" → ").Append(prop.GetValue(o)); + } + var target = gvType.GetProperty("targetSize", flags); + if (target != null) sb.Append(" 그리는 크기 ").Append(target.GetValue(o)); + var zf = gvType.GetField("m_ZoomArea", flags); + var z2 = zf.GetValue(o); + sb.Append(" 확대 ").Append(z2.GetType().GetProperty("scale", flags).GetValue(z2)).Append(" | "); +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/gameview_members.cs b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_members.cs new file mode 100644 index 0000000..0fc05dd --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_members.cs @@ -0,0 +1,32 @@ +// 게임 뷰(와 그 부모 형)의 속성 · 필드 가운데 이름에 찾는 말이 든 것을 적는다(읽기 전용). args.txt = "<찾는 말(소문자)> ..." +var words = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().ToLowerInvariant().Split(' '); +var asm = typeof(UnityEditor.Editor).Assembly; +var gvType = asm.GetType("UnityEditor.GameView"); +var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly; +var sb = new System.Text.StringBuilder(); +UnityEngine.Object view = null; +foreach (var o in UnityEngine.Resources.FindObjectsOfTypeAll(gvType)) { view = o; break; } +for (var t = gvType; t != null && t != typeof(UnityEditor.EditorWindow); t = t.BaseType) +{ + sb.Append("== ").Append(t.FullName).Append('\n'); + foreach (var m in t.GetMembers(flags)) + { + string n = m.Name.ToLowerInvariant(); + bool hit = false; + foreach (var w in words) if (n.Contains(w)) hit = true; + if (!hit) continue; + sb.Append(" ").Append(m.MemberType).Append(' ').Append(m.Name); + var p = m as System.Reflection.PropertyInfo; + var f = m as System.Reflection.FieldInfo; + try + { + if (p != null && p.GetIndexParameters().Length == 0 && p.CanRead) + sb.Append(" : ").Append(p.PropertyType.Name).Append(" = ").Append(p.GetValue(p.GetGetMethod(true).IsStatic ? null : view)).Append(p.CanWrite ? " (쓸 수 있음)" : ""); + else if (f != null) + sb.Append(" : ").Append(f.FieldType.Name).Append(" = ").Append(f.GetValue(f.IsStatic ? null : view)); + } + catch (System.Exception e) { sb.Append(" (읽기 실패 ").Append(e.GetType().Name).Append(')'); } + sb.Append('\n'); + } +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/gameview_playmode.cs b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_playmode.cs new file mode 100644 index 0000000..7e5b3b8 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_playmode.cs @@ -0,0 +1,19 @@ +// 게임 뷰의 'Play 할 때 동작'을 읽거나 바꾼다(에디터 설정). args.txt = "get" | "PlayFocused" | "PlayMaximized" | "PlayUnfocused" +var arg = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim(); +var asm = typeof(UnityEditor.Editor).Assembly; +var gvType = asm.GetType("UnityEditor.GameView"); +var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; +var prop = gvType.GetProperty("enterPlayModeBehavior", flags); +var sb = new System.Text.StringBuilder(); +foreach (var o in UnityEngine.Resources.FindObjectsOfTypeAll(gvType)) +{ + sb.Append("지금 ").Append(prop.GetValue(o)); + if (arg != "get") + { + prop.SetValue(o, System.Enum.Parse(prop.PropertyType, arg)); + ((UnityEditor.EditorWindow)o).Repaint(); + sb.Append(" → ").Append(prop.GetValue(o)); + } + sb.Append(" | "); +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/gameview_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_probe.cs new file mode 100644 index 0000000..8d9eb0d --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_probe.cs @@ -0,0 +1,44 @@ +// 게임 뷰의 해상도 설정을 본다(읽기 전용) — 지금 고른 크기 · 고를 수 있는 크기 목록 · 창의 실제 크기 · 확대 배율 +var sb = new System.Text.StringBuilder(); +var asm = typeof(UnityEditor.Editor).Assembly; +var gvType = asm.GetType("UnityEditor.GameView"); +var sizesType = asm.GetType("UnityEditor.GameViewSizes"); +var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; +var singleton = typeof(UnityEditor.ScriptableSingleton<>).MakeGenericType(sizesType); +var sizes = singleton.GetProperty("instance", flags).GetValue(null); +var groupType = sizesType.GetProperty("currentGroupType", flags).GetValue(sizes); +var group = sizesType.GetProperty("currentGroup", flags).GetValue(sizes); +sb.Append("그룹 ").Append(groupType).Append('\n'); +var gType = group.GetType(); +int builtin = (int)gType.GetMethod("GetBuiltinCount", flags).Invoke(group, null); +int total = (int)gType.GetMethod("GetTotalCount", flags).Invoke(group, null); +var getSize = gType.GetMethod("GetGameViewSize", flags); +for (int i = 0; i < total; i++) +{ + var s = getSize.Invoke(group, new object[] { i }); + var st = s.GetType(); + sb.Append(i < builtin ? " [기본] " : " [사용자] ").Append(i).Append(' ') + .Append(st.GetProperty("baseText", flags).GetValue(s)).Append(" | ") + .Append(st.GetProperty("sizeType", flags).GetValue(s)).Append(' ') + .Append(st.GetProperty("width", flags).GetValue(s)).Append('x').Append(st.GetProperty("height", flags).GetValue(s)).Append('\n'); +} +foreach (var o in UnityEngine.Resources.FindObjectsOfTypeAll(gvType)) +{ + var w = (UnityEditor.EditorWindow)o; + sb.Append("게임 뷰 '").Append(w.titleContent.text).Append("' 창 ").Append(w.position.width.ToString("0")).Append('x').Append(w.position.height.ToString("0")) + .Append(" 고른 번호 ").Append(gvType.GetProperty("selectedSizeIndex", flags).GetValue(o)); + var target = gvType.GetProperty("targetSize", flags); + if (target != null) sb.Append(" 그리는 크기 ").Append(target.GetValue(o)); + var zoom = gvType.GetField("m_ZoomArea", flags); + if (zoom != null) + { + var z = zoom.GetValue(o); + var scale = z.GetType().GetProperty("scale", flags); + if (scale != null) sb.Append(" 확대 ").Append(scale.GetValue(z)); + } + var low = gvType.GetProperty("lowResolutionForAspectRatios", flags); + if (low != null) sb.Append(" lowRes ").Append(low.GetValue(o)); + sb.Append(" 떠 있음 ").Append(w.hasFocus).Append('\n'); +} +sb.Append("모니터 ").Append(UnityEngine.Screen.currentResolution.width).Append('x').Append(UnityEngine.Screen.currentResolution.height).Append(" · 에디터 주 창 ").Append(UnityEditor.EditorGUIUtility.GetMainWindowPosition()).Append('\n'); +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/gameview_set.cs b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_set.cs new file mode 100644 index 0000000..744c857 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/gameview_set.cs @@ -0,0 +1,50 @@ +// 게임 뷰의 해상도 선택 · 확대 배율을 바꾼다(에디터 설정). args.txt = "index <번호>" | "free" | "zoom <배율>" | "index <번호> zoom <배율>" +// · 번호는 gameview_probe 가 보여 주는 목록의 번호다. free = Free Aspect(0번) +var args = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().Split(' '); +var asm = typeof(UnityEditor.Editor).Assembly; +var gvType = asm.GetType("UnityEditor.GameView"); +var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; +var sb = new System.Text.StringBuilder(); +foreach (var o in UnityEngine.Resources.FindObjectsOfTypeAll(gvType)) +{ + var w = (UnityEditor.EditorWindow)o; + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "free" || args[i] == "index") + { + int index = args[i] == "free" ? 0 : int.Parse(args[++i]); + var select = gvType.GetMethod("SizeSelectionCallback", flags); + if (select != null) + select.Invoke(o, new object[] { index, null }); + else + gvType.GetProperty("selectedSizeIndex", flags).SetValue(o, index); + sb.Append("해상도 번호 ").Append(index).Append(' '); + } + else if (args[i] == "zoom") + { + float z = float.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture); + var zoomField = gvType.GetField("m_ZoomArea", flags); + var area = zoomField.GetValue(o); + var setTransform = area.GetType().GetMethod("SetTransform", flags); + if (setTransform != null) + setTransform.Invoke(area, new object[] { UnityEngine.Vector2.zero, new UnityEngine.Vector2(z, z) }); + else + area.GetType().GetProperty("scale", flags).SetValue(area, new UnityEngine.Vector2(z, z)); + var enforce = gvType.GetMethod("EnforceZoomAreaConstraints", flags); + if (enforce != null) enforce.Invoke(o, null); + sb.Append("확대 ").Append(z).Append(' '); + } + } + w.Repaint(); + sb.Append("→ 고른 번호 ").Append(gvType.GetProperty("selectedSizeIndex", flags).GetValue(o)); + var target = gvType.GetProperty("targetSize", flags); + if (target != null) sb.Append(" 그리는 크기 ").Append(target.GetValue(o)); + var zf = gvType.GetField("m_ZoomArea", flags); + if (zf != null) + { + var z2 = zf.GetValue(o); + sb.Append(" 확대 ").Append(z2.GetType().GetProperty("scale", flags).GetValue(z2)); + } + sb.Append(" | "); +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/hier_dump.cs b/공유/기획/FirstRising/UI/tools/unity_eval/hier_dump.cs new file mode 100644 index 0000000..a47a3d6 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/hier_dump.cs @@ -0,0 +1,75 @@ +// 프리팹 또는 씬의 계층을 파일로 적는다(편집 상태 · 읽기 전용). args.txt = " <에셋 경로> <최대 깊이> <출력 파일 이름>" +// · 씬은 열려 있지 않으면 덧붙여 열었다가, 다 적은 뒤 저장 없이 닫는다 +var args = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().Split(' '); +string kind = args[0], path = args[1]; +int maxDepth = int.Parse(args[2]); +string outPath = @"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\out\" + args[3] + ".txt"; +System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outPath)); +if (UnityEditor.EditorApplication.isPlaying) + return "Play 중 — 편집 상태에서만 쓴다"; +var sb = new System.Text.StringBuilder(); +int count = 0; +System.Action walk = null; +walk = (t, depth) => +{ + count++; + sb.Append(new string(' ', depth * 2)).Append(t.name); + if (!t.gameObject.activeSelf) sb.Append(" [꺼짐]"); + var rt = t as UnityEngine.RectTransform; + if (rt != null) + sb.Append(" <").Append(rt.anchorMin.x.ToString("0.##")).Append(',').Append(rt.anchorMin.y.ToString("0.##")).Append('~').Append(rt.anchorMax.x.ToString("0.##")).Append(',').Append(rt.anchorMax.y.ToString("0.##")) + .Append(" pos ").Append(rt.anchoredPosition.x.ToString("0")).Append(',').Append(rt.anchoredPosition.y.ToString("0")) + .Append(" size ").Append(rt.sizeDelta.x.ToString("0")).Append(',').Append(rt.sizeDelta.y.ToString("0")) + .Append(" piv ").Append(rt.pivot.x.ToString("0.##")).Append(',').Append(rt.pivot.y.ToString("0.##")).Append('>'); + else if (t.localPosition != UnityEngine.Vector3.zero) + sb.Append(" @").Append(t.localPosition.x.ToString("0.#")).Append(',').Append(t.localPosition.y.ToString("0.#")).Append(',').Append(t.localPosition.z.ToString("0.#")); + var names = new System.Collections.Generic.List(); + foreach (var c in t.GetComponents()) + { + if (c == null) { names.Add("(없는 스크립트)"); continue; } + if (c is UnityEngine.Transform || c is UnityEngine.CanvasRenderer) continue; + string n = c.GetType().Name; + var beh = c as UnityEngine.Behaviour; + if (beh != null && !beh.enabled) n += "(꺼짐)"; + var cv = c as UnityEngine.Canvas; + if (cv != null) n += "{order " + cv.sortingOrder + " " + cv.renderMode + (cv.overrideSorting ? " override" : "") + "}"; + var cs = c as UnityEngine.UI.CanvasScaler; + if (cs != null) n += "{" + cs.uiScaleMode + " ref " + cs.referenceResolution.x + "x" + cs.referenceResolution.y + " match " + cs.matchWidthOrHeight + " factor " + cs.scaleFactor + "}"; + var cam = c as UnityEngine.Camera; + if (cam != null) n += "{depth " + cam.depth + " target " + (cam.targetTexture != null ? cam.targetTexture.name : "화면") + "}"; + var li = c as UnityEngine.Light; + if (li != null) n += "{" + li.type + " " + li.intensity.ToString("0.##") + "}"; + var sub = c as Unity.Scenes.SubScene; + if (sub != null) n += "{" + sub.EditableScenePath + " auto " + sub.AutoLoadScene + "}"; + var vol = c as UnityEngine.Rendering.Volume; + if (vol != null) n += "{" + (vol.sharedProfile != null ? vol.sharedProfile.name : "-") + " pri " + vol.priority + (vol.isGlobal ? " global" : "") + "}"; + names.Add(n); + } + if (names.Count > 0) sb.Append(" : ").Append(string.Join(", ", names)); + if (depth >= maxDepth && t.childCount > 0) + { + sb.Append(" … 자식 ").Append(t.childCount).Append('\n'); + return; + } + sb.Append('\n'); + foreach (UnityEngine.Transform c in t) walk(c, depth + 1); +}; +if (kind == "prefab") +{ + var go = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (go == null) return "프리팹 없음: " + path; + walk(go.transform, 0); +} +else +{ + var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByPath(path); + bool opened = !(scene.IsValid() && scene.isLoaded); + if (opened) + scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(path, UnityEditor.SceneManagement.OpenSceneMode.Additive); + sb.Append("# 씬 ").Append(path).Append(opened ? " (열었다 닫음)" : " (열려 있던 것)").Append('\n'); + foreach (var root in scene.GetRootGameObjects()) walk(root.transform, 0); + if (opened) + UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true); +} +System.IO.File.WriteAllText(outPath, sb.ToString()); +return "오브젝트 " + count + " → " + outPath; diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/hud_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/hud_probe.cs new file mode 100644 index 0000000..53b3f4d --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/hud_probe.cs @@ -0,0 +1,91 @@ +// 화면 구성 확인(Play 중 · 읽기 전용) — 지금 필드 · HUD 묶음의 보임 · 스테이지용 화면의 가림 · 끼운 필드의 내용(서브씬 · 꾸러미 · 분위기) +var sb = new System.Text.StringBuilder(); +sb.Append("scene=").Append(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name); +var travel = BurningTimes.Visual.TVFieldTravel.Instance; +sb.Append(" field=").Append(travel != null ? travel.currentKey + "/" + travel.currentTitle : "-"); +sb.Append(" traveling=").Append(BurningTimes.Visual.TVFieldTravel.traveling); +var arena = BurningTimes.Boss.TVBossArena.Instance; +sb.Append(" boss=").Append(arena != null ? arena.state.ToString() : "-").Append(arena != null && arena.inField ? "(전용 필드)" : ""); +sb.Append(" player=").Append(Fx.Player.Instance != null ? Fx.Player.Instance.GetPosition().ToString("0.0") : "-"); +sb.Append(" Screen=").Append(UnityEngine.Screen.width).Append('x').Append(UnityEngine.Screen.height).Append('\n'); + +// HUD 묶음(위젯 영역 아래 TV_…)의 보임 +var area = BurningTimes.Visual.TVViewportHost.WidgetArea; +if (area == null) + sb.Append("위젯 영역 없음(로비 화면 구성이 아님)\n"); +else +{ + var corners = new UnityEngine.Vector3[4]; + area.GetWorldCorners(corners); + sb.Append("위젯 영역 ").Append(area.name).Append(" 화면 자리 (").Append(corners[0].x.ToString("0")).Append(',').Append(corners[0].y.ToString("0")).Append(")~(") + .Append(corners[2].x.ToString("0")).Append(',').Append(corners[2].y.ToString("0")).Append(") 캔버스 배율 ").Append(area.GetComponentInParent().rootCanvas.scaleFactor.ToString("0.000")).Append('\n'); + sb.Append("HUD:"); + foreach (UnityEngine.Transform c in area) + if (c.name.StartsWith("TV_")) + sb.Append(' ').Append(c.name).Append(c.gameObject.activeInHierarchy ? "=보임" : "=숨김"); + sb.Append('\n'); +} + +// 스테이지용 화면(시간 · 골드 · 처치 수 · 스테이지 정보)이 가려져 있는가 +sb.Append("스테이지 화면:"); +foreach (var name in new[] { "PlayInfoRoot", "Gold", "MonsterCountTitleText", "StageInfo", "BossInfoRoot" }) +{ + UnityEngine.Transform hit = null; + foreach (var ui in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None)) + foreach (var t in ui.GetComponentsInChildren(true)) + if (t.name == name && t.parent != null && (t.parent.name == "CenterHolder" || t.parent.name == "GameCanvas")) { hit = t; break; } + if (hit == null) { sb.Append(' ').Append(name).Append("=없음"); continue; } + var cg = hit.GetComponent(); + bool shown = hit.gameObject.activeInHierarchy && (cg == null || cg.alpha > 0.01f); + sb.Append(' ').Append(name).Append(shown ? "=보임" : "=가림"); +} +sb.Append('\n'); + +// 끼운 필드의 내용 +var world = Unity.Entities.World.DefaultGameObjectInjectionWorld; +if (world != null && world.IsCreated) +{ + var em = world.EntityManager; + using (var q = em.CreateEntityQuery(Unity.Entities.ComponentType.ReadOnly())) + sb.Append("엔티티 씬 ").Append(q.CalculateEntityCount()); + using (var q = em.CreateEntityQuery(Unity.Entities.ComponentType.ReadOnly())) + sb.Append(" · 플레이어 엔티티 ").Append(q.CalculateEntityCount()); + using (var q = em.CreateEntityQuery(Unity.Entities.ComponentType.ReadOnly())) + sb.Append(" · 캐릭터 ").Append(q.CalculateEntityCount()); +} +foreach (var sub in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None)) + sb.Append(" · 서브씬 컴포넌트 ").Append(sub.name).Append(sub.isActiveAndEnabled ? "(켜짐)" : "(꺼짐)"); +sb.Append('\n'); +if (travel != null) +{ + sb.Append("BT_Visual 아래:"); + foreach (UnityEngine.Transform c in travel.transform) + sb.Append(' ').Append(c.name).Append(c.gameObject.activeSelf ? "" : "(꺼짐)"); + sb.Append('\n'); + if (travel.transform.parent != null) + { + sb.Append("같은 자리:"); + foreach (UnityEngine.Transform c in travel.transform.parent) + sb.Append(' ').Append(c.name).Append(c.gameObject.activeSelf ? "" : "(꺼짐)"); + sb.Append('\n'); + } +} +sb.Append("안개 ").Append(UnityEngine.RenderSettings.fog ? UnityEngine.RenderSettings.fogColor.ToString("0.00") + " " + UnityEngine.RenderSettings.fogStartDistance + "~" + UnityEngine.RenderSettings.fogEndDistance : "끔") + .Append(" · 환경광 ").Append(UnityEngine.RenderSettings.ambientMode).Append(' ').Append(UnityEngine.RenderSettings.ambientSkyColor.ToString("0.00")) + .Append(" · 반사 ").Append(UnityEngine.RenderSettings.customReflectionTexture != null ? UnityEngine.RenderSettings.customReflectionTexture.name : "없음").Append('\n'); +sb.Append("후처리:"); +foreach (var v in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None)) + sb.Append(' ').Append(v.name).Append('[').Append(v.sharedProfile != null ? v.sharedProfile.name : "-").Append(" pri ").Append(v.priority).Append(']'); +sb.Append('\n'); +int lights = 0, zones = 0; +foreach (var l in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None)) + if (l.type == UnityEngine.LightType.Point) lights++; +foreach (var z in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None)) +{ + zones++; + sb.Append("몬스터 구역 컴포넌트 ").Append(z.transform.parent != null ? z.transform.parent.name + "/" : "").Append(z.name).Append(z.paused ? "(멈춤)" : "").Append(' '); +} +sb.Append("· 켜진 점광원 ").Append(lights); +foreach (var l in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None)) + if (l.name == "KeyLight_Player") sb.Append(" · 캐릭터 조명 ").Append(l.intensity.ToString("0.0")).Append(" 범위 ").Append(l.range.ToString("0")); +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/layout3_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/layout3_probe.cs new file mode 100644 index 0000000..a3816fa --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/layout3_probe.cs @@ -0,0 +1,41 @@ +// 세 영역(왼쪽 창 · 게임 화면 · 오른쪽 창)의 크기를 본다(Play 중 · 읽기 전용) — 화면 · 캔버스 배율 · 게임 화면 칸 · 그리는 텍스처 · 열려 있는 옆 창 +var sb = new System.Text.StringBuilder(); +var area = BurningTimes.Visual.TVViewportHost.WidgetArea; +if (area == null) + return "위젯 영역 없음"; +var root = area.GetComponentInParent().rootCanvas; +var corners = new UnityEngine.Vector3[4]; +System.Func px = (rt) => +{ + rt.GetWorldCorners(corners); + return "(" + corners[0].x.ToString("0") + "," + corners[0].y.ToString("0") + ")~(" + corners[2].x.ToString("0") + "," + corners[2].y.ToString("0") + ") = " + + (corners[2].x - corners[0].x).ToString("0") + "x" + (corners[2].y - corners[0].y).ToString("0"); +}; +sb.Append("화면(캔버스 기준) ").Append(root.pixelRect.width.ToString("0")).Append('x').Append(root.pixelRect.height.ToString("0")) + .Append(" · 캔버스 ").Append(((UnityEngine.RectTransform)root.transform).rect.width.ToString("0")).Append('x').Append(((UnityEngine.RectTransform)root.transform).rect.height.ToString("0")) + .Append(" 단위 · 배율 ").Append(root.scaleFactor.ToString("0.000")); +var scale = BurningTimes.Visual.TVScreenScale.Instance; +sb.Append(" · 화면 크기 값 ").Append(scale != null ? scale.scale.ToString("0.00") : "-").Append('\n'); +sb.Append("게임 화면 칸 ").Append(px(area)).Append(" 픽셀\n"); +var cam = BurningTimes.Visual.TVViewportHost.GameCamera; +if (cam != null && cam.targetTexture != null) + sb.Append("게임 화면 텍스처 ").Append(cam.targetTexture.width).Append('x').Append(cam.targetTexture.height).Append('\n'); +foreach (UnityEngine.Transform c in area.parent) +{ + if (!c.gameObject.activeInHierarchy || c == area || !c.name.StartsWith("TV_")) + continue; + var rt = c as UnityEngine.RectTransform; + sb.Append("옆 창 ").Append(c.name).Append(' ').Append(px(rt)).Append(" 픽셀"); + // 안에 옮겨 놓은 쪽(페이지)의 배율 + foreach (UnityEngine.Transform d in c) + if (d.gameObject.activeSelf) + { + sb.Append(" · 안쪽 ").Append(d.name).Append(" 배율 ").Append(d.localScale.x.ToString("0.000")); + break; + } + sb.Append('\n'); +} +foreach (var cv in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsSortMode.None)) + if (cv.isRootCanvas && cv.renderMode == UnityEngine.RenderMode.ScreenSpaceOverlay) + sb.Append(" 캔버스 ").Append(cv.name).Append(" 배율 ").Append(cv.scaleFactor.ToString("0.000")).Append('\n'); +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/lobby_screen_apply.cs b/공유/기획/FirstRising/UI/tools/unity_eval/lobby_screen_apply.cs new file mode 100644 index 0000000..20dc26e --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/lobby_screen_apply.cs @@ -0,0 +1,4 @@ +// 로비 프리팹에 화면 크기 컴포넌트를 붙이고 머리 위 HP 막대를 가릴 목록에 보탠다(편집 모드 · 분리 실행) +if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) + return "SKIP playing"; +return BurningTimes.EditorTools.TVLobbyScreenSetup.Apply(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/panel_toggle.cs b/공유/기획/FirstRising/UI/tools/unity_eval/panel_toggle.cs new file mode 100644 index 0000000..1f8d98f --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/panel_toggle.cs @@ -0,0 +1,15 @@ +// 하단 메뉴가 여는 옆 창을 열고 닫는다(Play 중). args.txt = "equip" | "growth" | "trait" | "contents" | "inventory" +var arg = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim(); +UnityEngine.MonoBehaviour panel = null; +if (arg == "equip") panel = UnityEngine.Object.FindFirstObjectByType(); +else if (arg == "growth") panel = UnityEngine.Object.FindFirstObjectByType(); +else if (arg == "trait") panel = UnityEngine.Object.FindFirstObjectByType(); +else if (arg == "contents") panel = UnityEngine.Object.FindFirstObjectByType(); +else if (arg == "inventory") panel = UnityEngine.Object.FindFirstObjectByType(); +if (panel == null) + return "창 컴포넌트 없음: " + arg; +var toggle = panel.GetType().GetMethod("Toggle", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); +if (toggle == null) + return "Toggle 없음: " + panel.GetType().Name; +toggle.Invoke(panel, null); +return "toggled " + panel.GetType().Name; diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/panels_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/panels_probe.cs new file mode 100644 index 0000000..30b404c --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/panels_probe.cs @@ -0,0 +1,56 @@ +// 옆 창의 열림 상태와 자리를 본다(Play 중). args.txt = "state" | "<창 이름> ..."(차례로 열고 닫기를 뒤집는다: equip growth trait contents inventory) | "closeall" +// · 뒤집은 뒤의 상태를 한 줄로 돌려준다 — 왼쪽 · 오른쪽 자리에 있는 창, 창마다 열림 여부, 세 영역이 화면 안에 들어 있는지 +var args = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().Split(' '); +var equip = UnityEngine.Object.FindFirstObjectByType(); +var growth = UnityEngine.Object.FindFirstObjectByType(); +var trait = UnityEngine.Object.FindFirstObjectByType(); +var contents = UnityEngine.Object.FindFirstObjectByType(); +var inventory = UnityEngine.Object.FindFirstObjectByType(); +if (equip == null || growth == null || trait == null || contents == null || inventory == null) + return "옆 창 컴포넌트를 찾지 못함"; +foreach (var a in args) +{ + if (a == "equip") equip.Toggle(); + else if (a == "growth") growth.Toggle(); + else if (a == "trait") trait.Toggle(); + else if (a == "contents") contents.Toggle(); + else if (a == "inventory") inventory.Toggle(); + else if (a == "closeall") { equip.Close(); growth.Close(); trait.Close(); contents.Close(); inventory.Close(); } +} +var sb = new System.Text.StringBuilder(); +System.Func nameOf = (p) => p == null ? "비어 있음" : p.GetType().Name.Replace("TV", "").Replace("Panel", ""); +sb.Append("왼쪽 자리 ").Append(nameOf(BurningTimes.Visual.TVSideSlots.Current(BurningTimes.Visual.TVSideSlots.Side.Left))) + .Append(" · 오른쪽 자리 ").Append(nameOf(BurningTimes.Visual.TVSideSlots.Current(BurningTimes.Visual.TVSideSlots.Side.Right))) + .Append(" | 열림: 장비 ").Append(equip.isOpen).Append(" 성장 ").Append(growth.isOpen).Append(" 특성 ").Append(trait.isOpen) + .Append(" 컨텐츠 ").Append(contents.isOpen).Append(" 가방 ").Append(inventory.isOpen); +var area = BurningTimes.Visual.TVViewportHost.WidgetArea; +if (area != null) +{ + var root = area.GetComponentInParent().rootCanvas; + float w = root.pixelRect.width, h = root.pixelRect.height; + var c = new UnityEngine.Vector3[4]; + sb.Append(" | 화면 ").Append(w.ToString("0")).Append('x').Append(h.ToString("0")).Append(" 배율 ").Append(root.scaleFactor.ToString("0.000")); + bool inside = true; + foreach (UnityEngine.Transform t in area.parent) + { + if (!t.gameObject.activeInHierarchy || !(t == area || t.name.EndsWith("Panel"))) + continue; + ((UnityEngine.RectTransform)t).GetWorldCorners(c); + bool ok = c[0].x >= -1f && c[0].y >= -1f && c[2].x <= w + 1f && c[2].y <= h + 1f; + inside &= ok; + sb.Append(" · ").Append(t == area ? "게임 화면" : t.name.Replace("TV_", "").Replace("Panel", "")).Append(' ') + .Append((c[2].x - c[0].x).ToString("0")).Append('x').Append((c[2].y - c[0].y).ToString("0")).Append(" x=").Append(c[0].x.ToString("0")).Append('~').Append(c[2].x.ToString("0")) + .Append(ok ? "" : " (화면 밖!)"); + } + sb.Append(inside ? " → 모두 화면 안" : " → 화면을 벗어난 것 있음"); + var cam = BurningTimes.Visual.TVViewportHost.GameCamera; + if (cam != null && cam.targetTexture != null) + sb.Append(" · 텍스처 ").Append(cam.targetTexture.width).Append('x').Append(cam.targetTexture.height); +} +// 게임 페이지의 켜짐 +var item = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); +var training = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); +var evolution = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); +sb.Append(" | 페이지: 장비 ").Append(item != null && item.gameObject.activeInHierarchy).Append(" 훈련 ").Append(training != null && training.gameObject.activeInHierarchy) + .Append(" 진화 ").Append(evolution != null && evolution.gameObject.activeInHierarchy).Append(evolution != null ? "(부모 " + evolution.transform.parent.name + ")" : ""); +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/scale_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/scale_probe.cs new file mode 100644 index 0000000..da8e493 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/scale_probe.cs @@ -0,0 +1,40 @@ +// 화면 크기 컴포넌트의 상태(Play 중). args.txt = "get" | "set <배>" | "reset"(기기에 남긴 값을 지우고 기본값으로) | "widget on|off"(최소화 모드 흉내) +var args = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().Split(' '); +var sb = new System.Text.StringBuilder(); +var scale = BurningTimes.Visual.TVScreenScale.Instance; +if (scale == null) + return "TVScreenScale 없음"; +if (args[0] == "set") + scale.scale = float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture); +else if (args[0] == "reset") +{ + scale.scale = scale.defaultValue; + UnityEngine.PlayerPrefs.DeleteKey("BT_ScreenScale"); + sb.Append("(남긴 값 지움) "); +} +else if (args[0] == "widget" && Fx.DesktopWidgetManager.HasInstance()) +{ + if (args[1] == "on") Fx.DesktopWidgetManager.Instance.EnterWidget(); + else Fx.DesktopWidgetManager.Instance.ExitWidget(); + sb.Append("(위젯 ").Append(Fx.DesktopWidgetManager.Instance.isWidget).Append(") "); +} +sb.Append("Screen ").Append(UnityEngine.Screen.width).Append('x').Append(UnityEngine.Screen.height) + .Append(" · 화면 크기 ").Append(scale.scale.ToString("0.00")).Append("배(기본 ").Append(scale.defaultValue.ToString("0.00")).Append(") · 단위당 픽셀 ").Append(scale.pixelsPerUnit.ToString("0.000")) + .Append(" · 남긴 값 ").Append(UnityEngine.PlayerPrefs.HasKey("BT_ScreenScale") ? UnityEngine.PlayerPrefs.GetFloat("BT_ScreenScale").ToString("0.00") : "없음").Append('\n'); +foreach (var scaler in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None)) +{ + var canvas = scaler.GetComponent(); + if (canvas == null || !canvas.isRootCanvas) + continue; + sb.Append(" ").Append(scaler.transform.parent != null ? scaler.transform.parent.name + "/" : "").Append(scaler.name).Append(canvas.gameObject.activeInHierarchy ? "" : "(꺼짐)") + .Append(' ').Append(scaler.uiScaleMode).Append(" 값 ").Append(scaler.scaleFactor.ToString("0.000")).Append(" 실제 ").Append(canvas.scaleFactor.ToString("0.000")).Append('\n'); +} +var area = BurningTimes.Visual.TVViewportHost.WidgetArea; +if (area != null) +{ + var c = new UnityEngine.Vector3[4]; + area.GetWorldCorners(c); + sb.Append("게임 화면 칸 (").Append(c[0].x.ToString("0")).Append(',').Append(c[0].y.ToString("0")).Append(")~(").Append(c[2].x.ToString("0")).Append(',').Append(c[2].y.ToString("0")) + .Append(") = ").Append((c[2].x - c[0].x).ToString("0")).Append('x').Append((c[2].y - c[0].y).ToString("0")).Append(" 픽셀"); +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/screen_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/screen_probe.cs new file mode 100644 index 0000000..4061483 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/screen_probe.cs @@ -0,0 +1,24 @@ +// 화면 구성을 본다(Play 중 · 읽기 전용) — 화면 크기 · 카메라마다 그리는 영역 · 캔버스 +var sb = new System.Text.StringBuilder(); +sb.Append("Screen ").Append(UnityEngine.Screen.width).Append("x").Append(UnityEngine.Screen.height) + .Append(" · 씬 ").Append(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name).Append('\n'); +foreach (var c in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None)) +{ + var data = c.GetComponent(); + sb.Append("카메라 ").Append(c.name).Append(c.isActiveAndEnabled ? "" : "(꺼짐)").Append(" rect=").Append(c.rect).Append(" pixelRect=").Append(c.pixelRect) + .Append(" depth=").Append(c.depth).Append(" clear=").Append(c.clearFlags).Append(" bg=").Append(c.backgroundColor) + .Append(" target=").Append(c.targetTexture != null ? c.targetTexture.name + " " + c.targetTexture.width + "x" + c.targetTexture.height : "화면") + .Append(" type=").Append(data != null ? data.renderType.ToString() : "-").Append(" 부모=").Append(c.transform.parent != null ? c.transform.parent.name : "-").Append('\n'); +} +foreach (var cv in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None)) +{ + if (!cv.isRootCanvas) continue; + var rt = cv.GetComponent(); + var scaler = cv.GetComponent(); + sb.Append("캔버스 ").Append(cv.name).Append(" mode=").Append(cv.renderMode).Append(" cam=").Append(cv.worldCamera != null ? cv.worldCamera.name : "-") + .Append(" order=").Append(cv.sortingOrder).Append(" pixelRect=").Append(cv.pixelRect).Append(" 크기=").Append(rt.rect.size).Append(" scale=").Append(cv.scaleFactor.ToString("0.00")); + if (scaler != null) + sb.Append(" scaler=").Append(scaler.uiScaleMode).Append(" ref=").Append(scaler.referenceResolution).Append(" match=").Append(scaler.matchWidthOrHeight.ToString("0.0")); + sb.Append(" 씬=").Append(cv.gameObject.scene.name).Append('\n'); +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/screen_size.cs b/공유/기획/FirstRising/UI/tools/unity_eval/screen_size.cs new file mode 100644 index 0000000..b4f184e --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/screen_size.cs @@ -0,0 +1,8 @@ +// 실행 중인 게임의 화면 크기(읽기 전용) — "너비 높이". 게임 화면 칸이 있는 뿌리 캔버스에서 읽는다(에디터에서는 Screen 값이 다른 창의 것일 수 있다) +var area = BurningTimes.Visual.TVViewportHost.WidgetArea; +if (area != null) +{ + var root = area.GetComponentInParent().rootCanvas; + return UnityEngine.Mathf.RoundToInt(root.pixelRect.width) + " " + UnityEngine.Mathf.RoundToInt(root.pixelRect.height); +} +return UnityEngine.Screen.width + " " + UnityEngine.Screen.height; diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/tmc_probe.cs b/공유/기획/FirstRising/UI/tools/unity_eval/tmc_probe.cs new file mode 100644 index 0000000..536bae5 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/tmc_probe.cs @@ -0,0 +1,26 @@ +// 게임의 시험용 스폰 컴포넌트(TestMonstersController)의 상태(Play 중 · 읽기 전용) — 켜짐 여부 · 스폰 끝남 · 엔티티를 찾은 수 +var sb = new System.Text.StringBuilder(); +var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; +var world = Unity.Entities.World.DefaultGameObjectInjectionWorld; +foreach (var c in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None)) +{ + var t = c.GetType(); + sb.Append(c.gameObject.scene.name).Append('/').Append(c.name).Append(c.isActiveAndEnabled ? " 켜짐" : " 꺼짐"); + sb.Append(" spawned=").Append(t.GetField("monsterSpawned", flags).GetValue(c)); + sb.Append(" foundAll=").Append(t.GetField("findAllEntities", flags).GetValue(c)); + var list = t.GetField("testMonsterSpawnInfos", flags).GetValue(c) as System.Collections.IList; + int found = 0, alive = 0; + if (list != null) + foreach (var info in list) + { + var e = (Unity.Entities.Entity)info.GetType().GetField("entity").GetValue(info); + if (e != Unity.Entities.Entity.Null) + { + found++; + if (world != null && world.IsCreated && world.EntityManager.Exists(e)) alive++; + } + } + sb.Append(" infos=").Append(list != null ? list.Count : -1).Append(" 엔티티 찾음=").Append(found).Append(" 살아 있음=").Append(alive).Append('\n'); +} +sb.Append("Player=").Append(Fx.Player.Instance != null).Append(" PlayerManager.player=").Append(Fx.PlayerManager.HasInstance() && Fx.PlayerManager.Instance.player != null); +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/ui_at.cs b/공유/기획/FirstRising/UI/tools/unity_eval/ui_at.cs new file mode 100644 index 0000000..4c3533c --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/ui_at.cs @@ -0,0 +1,28 @@ +// 화면의 한 점에 걸쳐 있는 UI 그림을 찾는다(Play 중 · 읽기 전용). args.txt = " " — 캡처 그림에서 읽은 좌표 그대로 +var args = System.IO.File.ReadAllText(@"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\eval\args.txt").Trim().Split(' '); +float x = float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture); +float y = UnityEngine.Screen.height - float.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture); +var point = new UnityEngine.Vector2(x, y); +var sb = new System.Text.StringBuilder(); +sb.Append("점 (").Append(x).Append(", ").Append(y).Append(") 아래 왼쪽 기준\n"); +foreach (var g in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None)) +{ + if (!g.enabled || g.canvas == null || g.color.a <= 0.01f) + continue; + var cam = g.canvas.renderMode == UnityEngine.RenderMode.ScreenSpaceOverlay ? null : g.canvas.worldCamera; + if (!UnityEngine.RectTransformUtility.RectangleContainsScreenPoint(g.rectTransform, point, cam)) + continue; + // 부모의 CanvasGroup 으로 가려져 있으면 뺀다 + float alpha = 1f; + foreach (var cg in g.GetComponentsInParent()) + alpha *= cg.alpha; + string p = g.name; + var q = g.transform.parent; + int n = 0; + while (q != null && n++ < 12) { p = q.name + "/" + p; q = q.parent; } + var c = new UnityEngine.Vector3[4]; + g.rectTransform.GetWorldCorners(c); + sb.Append(g.GetType().Name).Append(' ').Append(p).Append(" 색 ").Append(g.color.ToString("0.00")).Append(" 가림배율 ").Append(alpha.ToString("0.00")) + .Append(" 자리 (").Append(c[0].x.ToString("0")).Append(',').Append(c[0].y.ToString("0")).Append(")~(").Append(c[2].x.ToString("0")).Append(',').Append(c[2].y.ToString("0")).Append(")\n"); +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/UI/tools/unity_eval/ui_layout.cs b/공유/기획/FirstRising/UI/tools/unity_eval/ui_layout.cs new file mode 100644 index 0000000..230f505 --- /dev/null +++ b/공유/기획/FirstRising/UI/tools/unity_eval/ui_layout.cs @@ -0,0 +1,43 @@ +// 화면에 떠 있는 UI 의 배치를 본다(Play 중 · 읽기 전용) — 게임 그림을 보여 주는 칸(RawImage)과 캔버스 바로 아래 묶음들의 자리(화면 픽셀) +var sb = new System.Text.StringBuilder(); +sb.Append("Screen ").Append(UnityEngine.Screen.width).Append("x").Append(UnityEngine.Screen.height).Append(" · 씬 ").Append(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name).Append('\n'); +System.Func pathOf = (t) => +{ + string p = t.name; + var q = t.parent; + int n = 0; + while (q != null && n++ < 8) { p = q.name + "/" + p; q = q.parent; } + return p; +}; +System.Func rectOf = (rt) => +{ + var c = new UnityEngine.Vector3[4]; + rt.GetWorldCorners(c); + return "(" + c[0].x.ToString("0") + "," + c[0].y.ToString("0") + ")~(" + c[2].x.ToString("0") + "," + c[2].y.ToString("0") + ") " + (c[2].x - c[0].x).ToString("0") + "x" + (c[2].y - c[0].y).ToString("0"); +}; +foreach (var ri in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None)) +{ + if (ri.texture == null || !(ri.texture is UnityEngine.RenderTexture)) continue; + sb.Append("그림 칸 ").Append(pathOf(ri.transform)).Append(" tex=").Append(ri.texture.name).Append(" ").Append(ri.texture.width).Append("x").Append(ri.texture.height) + .Append(" 화면 자리 ").Append(rectOf(ri.rectTransform)).Append(" 씬=").Append(ri.gameObject.scene.name).Append('\n'); +} +foreach (var cv in UnityEngine.Object.FindObjectsByType(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None)) +{ + if (!cv.isRootCanvas) continue; + sb.Append("== 캔버스 ").Append(pathOf(cv.transform)).Append(" order=").Append(cv.sortingOrder).Append(" mode=").Append(cv.renderMode).Append(" 씬=").Append(cv.gameObject.scene.name).Append('\n'); + foreach (UnityEngine.Transform c in cv.transform) + { + if (!c.gameObject.activeInHierarchy) continue; + var rt = c as UnityEngine.RectTransform; + int graphics = c.GetComponentsInChildren(false).Length; + sb.Append(" ").Append(c.name).Append(" 자식 ").Append(c.childCount).Append(" 그림 ").Append(graphics).Append(rt != null ? " 자리 " + rectOf(rt) : "").Append('\n'); + if (c.childCount <= 14) + foreach (UnityEngine.Transform d in c) + { + if (!d.gameObject.activeInHierarchy) continue; + var drt = d as UnityEngine.RectTransform; + sb.Append(" ").Append(d.name).Append(" 자식 ").Append(d.childCount).Append(drt != null ? " 자리 " + rectOf(drt) : "").Append('\n'); + } + } +} +return sb.ToString(); diff --git a/공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md b/공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md index de0011f..422d27f 100644 --- a/공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md +++ b/공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md @@ -250,3 +250,14 @@ PD 지시: "오염된 던전의 바닥 텍스쳐가 너무 크고 퀄리티가 - 기기 빌드에서의 모습 · 성능은 확인하지 못했다. - FR `bt/work` `5f9c8dd297` 로 push 완료(PD 지시 2026-09-28). + +## 10. 필드 이동 방식 변경 — 로비 화면 그대로 · 필드 꾸러미 (BT39 · 2026-09-28) + +> PD 지시 — "테스트 맵(숲맵을 제외한 네가 새로 만든 맵)에 진입 시 캐릭터 상단에 떠있는 창은 제외해줘. (최초 Entry 씬 진입 시 보여지는 화면이 맵을 바꾸거나 보스 전에서도 동일하게 보여져야 해 …)" + +- 필드 이동은 이제 **시험용 메인 씬을 불러오지 않는다.** 로비 씬을 다시 읽고 숲 맵 대신 고른 맵을 끼운다(맵 서브씬은 GUID 로 읽는다) → 로비와 같은 화면 구성이 유지된다. +- 맵마다 **필드 꾸러미** 프리팹이 필요하다 — `Assets/BurningTimes/Maps/Resources/FieldKits/FieldKit_<이름>.prefab`(빛 루트 · 후처리 · 캐릭터 조명 · 몬스터 구역). 시험용 메인 씬에서 떠서 만든다. +- **맵을 다시 지었으면** 맵 빌드 → 필드 꾸러미(`BurningTimes/Maps/필드 꾸러미 만들기`) → 보스전 전용 필드(`BurningTimes/Boss/4.`) → 붙이기(`BurningTimes/Boss/3.`) 순서로 돌린다. +- 맵의 모습(조명 · 후처리 · 안개 · 환경광 · 반사 · 몬스터 구역)은 시험용 씬의 값 그대로 옮겼다 [실측 — 맵 6종 Play · 에디터 로그 오류 0]. +- 시험용 메인 씬(`Assets/Scenes/Maps/<이름>.unity`)은 맵 빌더가 계속 쓴다(지우지 않는다). +- 자세한 내용 · 검증 결과 · 남은 것: `공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md` §2. diff --git a/공유/대화로그/FirstRising/2026-09-28.md b/공유/대화로그/FirstRising/2026-09-28.md index e0a648b..2faaca0 100644 --- a/공유/대화로그/FirstRising/2026-09-28.md +++ b/공유/대화로그/FirstRising/2026-09-28.md @@ -203,3 +203,44 @@ - 4 m 마다의 무늬 되풀이는 약해졌지만 남아 있다. 기기 빌드 미검증. - FR 작업 트리: 에디터가 덧붙인 변경 6개만 남아 있다. + +--- + +## 7. BT39 화면 구성 통일 · BT40 화면 크기(세 영역) · BT41 옆 창 독립 (PM 직접 · 같은 세션) + +### PD 원문 +- (BT39) "현재 프로젝트 관련 질문이야. 1) 에디터에서 Entry 씬 실행 시 보여지는 화면이 실제 PC에서 게임 실행 시 보여지는 영역이 되는거야? (현재 내 에디터 화면의 검은 부분이 PC 화면이고, 게임이 보여지는 영역은 게임 화면이 떠있는 영역인가에 대한 질문이야.) 2) 테스트 맵(숲맵을 제외한 네가 새로 만든 맵)에 진입 시 캐릭터 상단에 떠있는 창은 제외해줘. (최초 Entry 씬 진입 시 보여지는 화면이 맵을 바꾸거나 보스 전에서도 동일하게 보여져야 해. 따라서 하단의 HP 정보가 보이는 메뉴 등은 맵이 바뀌어도 보여져야 한다는 의미야. 3) 단, 게임 화면 최하단의 "성장, 장비, 특성, 컨텐츠, 길드, 포탈" 메뉴는 보스전에서는 보여지지 않아야 해." +- (BT40) "매번 에디터 화면이 너무 작아서 불편해. → 따라서 에디터 기준 화면 크기를 2배로 확대한 상태가 기본 크기면 좋겠어. (※추후 옵션에서 기본 화면 크기를 조절할 수 있도록 할 예정이야.)" +- (BT40 보완 1) "옆에 발생되는 창도 게임 화면과 동일한 크기로 보여져야 해. 메인 화면만 커지고 좌/우 영역에 뜨는 창이 너무 작아지지 않도록 기본 창 크기를 조절해줘. 내가 원하는건 왼쪽 중앙 오른쪽 3 영역의 좌/우길이의 총합이 모니터 화면이 되도록 하고 싶은거야" +- (BT40 보완 2) "화면이 재생 될 해상도 크기에 맞게 조절되지 않고 넘어서는 것 같아. (예를들어 게임 뷰의 크기를 조절할 경우 잘리는 화면이 존재해. scale 1.0 기준으로 화면을 조절하면 맞춰서 유동적으로 그려지게 해줘." +- (BT41) "각각의 UI는 개별적으로 열 수 있어야하고, 개별적으로 닫을 수도 있는 독립적인 구조여야 해 좌, 우측 영역의 UI가 함께 보여져야하는데 지금은 한쪽이 켜지면 한쪽이 닫히는 현상이 있는거 같아. 체크해보고 수정해줘." + +### 결정 +- BT39: 맵 씬에 로비 화면을 덧붙이지 않고, **로비 씬을 그대로 둔 채 맵만 바꿔 끼운다**(숲 서브씬을 끄고 맵 서브씬을 GUID 로 읽기 · 맵마다 다른 오브젝트는 필드 꾸러미 프리팹으로). 맵의 모습은 시험용 씬의 값 그대로 옮긴다. +- BT40: 1차로 만든 '고정 크기(2배)'는 PD 보완 지시로 폐기 → **세 영역의 배치(2200×1280)가 화면에 꽉 차게** 배율을 잡는다. 옆 창의 틈 · 여백을 게임 기준(10 · 10)에 맞춰 세 영역을 같은 크기로. 게임 화면 텍스처는 그려지는 픽셀 수로. PC 창은 큰 모니터에서 키운다(게임 원본 2곳 수정 · 날짜/이유 주석). +- 에디터 게임 뷰: 자유 비율 · 제 해상도로 맞춤. 'Play 할 때 최대화'는 넣었다가 되돌렸다(PD 가 게임 뷰 크기를 직접 조절하고 있어 강제하지 않는다 · 메뉴만 둠). +- BT41: 창끼리 서로 찾아 닫던 것을 **자리(왼쪽 · 오른쪽)별 관리**로 바꾸고, 로비의 페이지 전환을 쓰지 않고 창마다 자기 페이지만 켜고 끈다. 성장(강화)은 진화 창만 빌려 온다. + +### 결과 +- BT39: 로비 + 맵 6종에서 같은 화면 구성 · 스테이지용 화면 모두 가림 · 보스전 중 하단 메뉴 숨김(7곳) · 필드 이동 6~10초 · 전체 순회 4회 에디터 로그 오류 0 [실측]. +- BT40: 게임 뷰 크기 8종에서 세 영역이 모두 화면 안 · 같은 크기(예: 3840×2160 에서 영역 하나 1215×2160, 지금 게임 뷰 창 1438×1386 에서 471×837) [실측]. PC 빌드의 창 크기는 미검증. +- BT41: 장비+컨텐츠 · 성장(강화)+컨텐츠 · 특성+가방 등 좌 · 우 함께 열림, 한쪽을 열고 닫아도 반대쪽 유지(17 단계) · 에디터 로그 오류 0 [실측]. +- 문서 `공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md` · 그림 `UI/img/bt39_*.jpg` · `bt40_fluid_fit.jpg` · `bt41_left_right_together.jpg` · 도구 `UI/tools/`. + +### 사고·조치 +- BT40 1차 구현(고정 크기)은 PD 의 뜻(세 영역이 함께 · 화면에 맞게)과 달랐다 — 게임 뷰가 좁으면 옆 창이 0.45 배로 줄고 화면을 넘었다. PD 보완 지시 2건으로 다시 만들었다. +- PD 게임 뷰 설정을 작업 중 여러 번 바꿨다(세로 1080×1920 확대 2 → 자유 비율 → PD 가 4K UHD 로 → 자유 비율). 끝 상태와 되돌리는 법을 보고에 적는다. +- PD Play 중에 게임 뷰 설정을 바꾸는 명령을 1회 실행했다(Play 여부를 그 명령과 함께 확인해 늦게 알았다) — 읽기 전용이 아닌 명령 앞에는 Play 여부를 단독으로 먼저 확인한다. +- 맵 시험용 씬에서 바로 Play 한 뒤 필드를 옮기면 게임의 시험용 스폰 컴포넌트가 지워진 엔티티를 건드려 예외 98회 → 옮기기 전에 그 컴포넌트를 끄도록 고침(재검증 0). +- 검색 명령 하나가 프로젝트 전체를 훑느라 백그라운드로 넘어감 → 바로 중지(남은 작업 없음). + +### 남은 이슈 +- **PD 확인 필요**: 화면 구성 · 세 영역 크기 · 옆 창 동작. PC 빌드에서의 창 크기(모니터 채우기)는 빌드로 확인해야 한다. +- 테마 맵의 서브씬을 빌드에 넣는 일(빌드에서 필드 이동) · Entry/로딩/스테이지 화면의 배율 통일 · 가방 창의 다시 만들기는 하지 않았다. +- FR 작업 트리에 미커밋으로 둠(커밋 · push 는 PD 지시 시). + +### PD 답변 · 커밋(2026-09-28 저녁) +- PD 원문: "1. 화면 구성, 세 영역 크기, 옆 창 동작은 의도한 것이 맞아. 2. PC 빌드로 창 크기 확인은 보류해도 돼. 3. 커밋 및 푸시하고, 다음 세션에서 작업할 수 있도록 세션 공유하도록 해. (본 세션 컨텍스트 한계 임박함)" +- FR `bt/work` `5f9c8dd297..2d510d24c4`(44개 파일) push 완료 · 원격 = 로컬 [실측]. 에디터가 덧붙인 변경 8개(재질 4 · 의존성 에셋 1 · 글자체 3)는 넣지 않았다. 커밋은 PD Play 중에 했다(git 은 작업 트리를 읽기만 한다 · 에디터 조작 없음). +- 조직 레포 `main`: 화면 구성 문서 · 그림 · 도구 · 대화로그 §7 · PD 지시 로그(BT39~BT41) · 세션 인수인계서를 커밋 · push. +- 세션 인수인계서: `공유/대화로그/FirstRising/2026-09-28_FR_맵_보스_화면구성_세션인수인계.md`(첫 프롬프트 · 이번에 열 문서 · 미검증 · 결정 대기). diff --git a/공유/대화로그/FirstRising/2026-09-28_FR_맵_보스_화면구성_세션인수인계.md b/공유/대화로그/FirstRising/2026-09-28_FR_맵_보스_화면구성_세션인수인계.md new file mode 100644 index 0000000..8e82498 --- /dev/null +++ b/공유/대화로그/FirstRising/2026-09-28_FR_맵_보스_화면구성_세션인수인계.md @@ -0,0 +1,105 @@ +# FR 맵 · 보스 · 화면 구성 — 세션 인수인계서 (2026-09-28 · BT27~BT41) + +> 이 문서 하나로 다음 세션이 이어갈 수 있게 적었다. 원문 · 과정은 `공유/대화로그/FirstRising/2026-09-28.md`(§1~§7), 지시 로그는 `공유/PD_지시_트래킹/개발팀_PD_지시_로그.md`(BT27~BT41). +> 세션 종료 사유: PD 지시(2026-09-28 "커밋 및 푸시하고, 다음 세션에서 작업할 수 있도록 세션 공유하도록 해. 본 세션 컨텍스트 한계 임박함"). + +## 0. 다음 세션 첫 프롬프트 (그대로 붙여넣기) + +``` +FR(FirstRising) 작업을 이어서 진행한다. +인수인계서: 공유/대화로그/FirstRising/2026-09-28_FR_맵_보스_화면구성_세션인수인계.md 의 §0-1 · §1 · §2 를 먼저 읽고, +현황을 3줄로 보고한 뒤 내 지시를 기다려라. (로그 전문 읽기 금지, 필요한 절만 Grep) + +고정 사항: +- 커밋 · push 는 PD 지시 시만(FR · 조직 레포 모두). 작업을 마치면 작업 트리에 두고 보고한다 +- 재컴파일 · Play 조작 · 에디터 설정을 바꾸는 명령 앞에는 Play 여부를 단독 명령으로 먼저 확인한다. + PD Play 중에는 코드 수정 · Play 조작 · 무거운 스크립트 금지(읽기 전용 확인만) +- 검증 Play 는 끝나면 즉시 종료 · 임시로 바꾼 값은 즉시 원복 · 백그라운드 작업을 남기지 않는다 +- 오류 확인은 에디터 로그 기준(logscan.py). 5초 넘는 eval 은 분리 실행(--detach) +- 게임 원본 코드 수정 시 날짜 · 이유 한글 주석. 새 코드 · 고친 코드에는 선언 단위 한글 주석. 스크립트는 BT_Visual 노드에만 +- 파일 삭제는 PD 지시 시만(쓰이지 않게 된 것은 목록으로 보고) +- 허위 · 추정 단정 금지(실측 · 코드 · 미검증을 구분해 보고). 감사 호출 생략, 무거운 에이전트 호출은 사전 승인, 세션 전환 요구 금지 +``` + +## 0-1. 이번 세션에서 열 문서(필요한 절만) + +| 하려는 일 | 열 문서 · 절 | +|---|---| +| 화면 구성(세 영역) · 옆 창 · 화면 크기 · 필드 이동 | `공유/기획/FirstRising/UI/2026-09-28_FR_화면구성_세영역_필드이동.md` — §0 상태 · §5 미검증 · §6 바뀐 파일 | +| 맵을 고치거나 다시 짓기 | `공유/기획/FirstRising/맵/2026-09-28_FR_신규맵5종_빌드결과.md` — §0 · §2(조정 근거) · §10(다시 짓는 순서) | +| 보스 패턴 · 보스전 모드 · 전용 필드 | `공유/기획/FirstRising/몬스터보스/2026-09-28_FR_보스패턴_구현노트_v2.md` — §0 · §12 | +| 쓰이지 않는 에셋을 묻는 경우 | `공유/기획/FirstRising/맵/미사용_에셋_목록.md` | +| 조직 레포 커밋 · push | 메모리 `reference_org_repo_commit_push.md`(게이트 · 인증 재시도 · 제외 목록) | +| PC 창 · 위젯(빌드 전용 코드) | FR `Assets/Scripts/System/Windows/README_위젯모드.md` | + +그 밖의 문서는 필요할 때만 연다. 메모리 색인(`MEMORY.md`)의 FR 항목이 위 문서들을 가리킨다. + +## 1. 현황 한눈에 + +| 구분 | 상태 | +|---|---| +| **BT39 화면 구성 통일** | 완료 · **PD 확인(09-28 "의도한 것이 맞아")** — 로비 + 맵 6종 같은 화면, 보스전 중 하단 메뉴 숨김 | +| **BT40 화면 크기(세 영역)** | 에디터 완료 · PD 확인. **PC 빌드 확인은 PD 보류(09-28)** | +| **BT41 옆 창 독립** | 완료 · PD 확인 | +| BT35 · BT36 · BT38 (맵 이동 영역 · 사막 맵 · 블렌더 소품 · 던전 바닥) | 완료 보고 · **PD 확인 대기**(§3) | +| BT27~BT34 (보스 패턴 10종 · 보스전 모드 · 전용 필드 · 신규 맵 5종 · 바닥 덧칠 · 필드 이동) | 완료 보고 · PD 확인 대기 항목 있음(§3) | +| 커밋 | FR `bt/work` **`2d510d24c4`** push 완료(원격 = 로컬). 조직 레포 `main` 은 이 문서가 든 커밋까지 push | +| 에디터 | Unity 6000.3.9f1 열려 있음. 열린 씬 Entry. 09-28 세션 끝에 PD 가 Play 중이었다 | +| FR 작업 트리 | 에디터가 덧붙인 변경 8개만 남아 있다(Hovl Studio 재질 4 · `GlobalEntitySceneDependency.asset` · 글자체 SDF 3) — 커밋에 넣지 않는다 | + +## 2. 이어가기 — 알아야 할 것 + +### 2-1. 화면 구성(BT39~41) +- 게임 창 = **세 영역**(왼쪽 창 · 게임 화면 · 오른쪽 창, 각 720×1280, 배치 2200×1280). 에디터 게임 뷰의 검은 부분은 창의 빈 자리(빌드에서 투명). +- 배율: `TVScreenScale`(BT_Visual) — 세 영역이 화면에 꽉 차게. 값 `scale`(기본 1)은 추후 옵션용. +- 옆 창: `TVSideSlots`(자리별 관리). 왼쪽 = 성장 · 장비 · 특성 / 오른쪽 = 컨텐츠 · 가방. 같은 자리는 하나만, 좌 · 우는 함께. +- 필드 이동: 로비 씬을 다시 읽고 맵만 바꿔 끼운다(`TVFieldTravel`). 맵마다 필드 꾸러미(`Assets/BurningTimes/Maps/Resources/FieldKits/`). +- **맵을 다시 지으면**: 맵 빌드 → 필드 꾸러미(`BurningTimes/Maps/필드 꾸러미 만들기`) → 보스전 전용 필드(`BurningTimes/Boss/4.`) → 붙이기(`BurningTimes/Boss/3.`). +- 로비 프리팹 값 적용은 `TVLobbyScreenSetup.Apply()`. **`TVLobbySetup.Apply()`(메뉴 `Lobby 메뉴/Apply`)는 돌리지 않는다** — 하단 메뉴 동작 · 숨김 경로가 옛 값으로 되돌아간다. + +### 2-2. 에디터 게임 뷰(PD 에디터의 설정 · 프로젝트에 저장되지 않는다) +- 09-28 끝 상태: **자유 비율(Free Aspect) · 제 해상도 · Play Unfocused**. PD 는 게임 뷰 크기 · 해상도를 직접 조절한다(작업 중 4K UHD 로 바꾸기도 했다). +- 게임 뷰 설정을 바꾸기 전에 지금 값을 적어 두고(`UI/tools/unity_eval/gameview_probe.cs`), 끝 상태를 보고한다. 'Play 할 때 최대화'를 강제하지 않는다. +- 캡처는 지금 화면 크기 그대로 찍는다(`UI/tools/shot.sh`) — 다른 크기를 주면 그림이 찌그러진다. + +### 2-3. 미검증 · 하지 않은 것 +- **PC 빌드**: 창 크기 키우기(`DesktopWidgetManager.m_ExpandToWorkArea`) · 빌드에서의 필드 이동(맵 서브씬이 빌드에 없다 → 안내만 뜸) · 큰 창에서의 성능. PD 가 빌드 확인을 보류했다. +- Entry · 로딩 · 스테이지 화면은 게임 원래 배율(화면 크기 컴포넌트는 로비에만 있다). +- 가방 창은 시작할 때 한 번 만든다(Play 중 게임 뷰 비율을 크게 바꿔도 다시 만들지 않는다). +- 게임 화면 오른쪽 위 흰 네모 둘 = 게임의 버튼(`PlayerInfoButton` · `WidgetModeButton`)에 그림이 없음. 손대지 않았다. +- 맵: 4 m 마다의 바닥 무늬 되풀이 · 배경 밀도(상업 수준 미달 판단) · 몰이 중 몬스터 끼임 · 기기 성능. + +### 2-4. 다음에 할 만한 일(PD 지시가 있을 때) +1. PC 빌드로 창 크기 · 투명 창 · 필드 이동 확인(맵 서브씬을 빌드에 넣는 방법 조사 포함) +2. 화면 크기 옵션 화면(값은 `TVScreenScale.Instance.scale`) +3. Entry · 로딩 · 스테이지 화면의 배율 통일 +4. §3 의 PD 확인 · 결정 대기 항목 + +## 3. PD 확인 · 결정 대기 + +| 안건 | 기다리는 것 | +|---|---| +| BT38 | 오염된 던전 바닥 모습 확인 · **쓰이지 않는 에셋 35개(10 MB)를 지울지**(`맵/미사용_에셋_목록.md`) | +| BT37 | 조직 레포의 FR 백업 폴더(`공유/개발팀_백업/FirstRising/` 1.2 GB)를 무시 목록에 넣을지 | +| BT36 | 블렌더 소품 품질(구매 에셋보다 단순하다는 PM 판단) | +| BT35 | 사막 맵 분위기 · 전용 필드의 보스 짝 C1(PM 선택) | +| BT34 | 바닥 자연스러움 · 테마 맵을 빌드에 넣을지 | +| BT27~BT33 | 보스 패턴 · 보스전 모드 확인(구현 노트 v2) | +| 기획(BT22~BT26) | 앞선 인수인계서 `2026-09-24_FR_시스템기획_세션인수인계.md` §3 그대로 | + +## 4. 산출물 · 도구 + +- `공유/기획/FirstRising/UI/` — 화면 구성 문서 · `img/bt39_*.jpg` · `bt40_fluid_fit.jpg` · `bt41_left_right_together.jpg` · `tools/`(`bt.sh` · `logscan.py` · `evd.sh` · `shot.sh` · `capfull.sh` · `hud_tour.sh` · `comp_diff.py` · `prefab_same.py` · `unity_eval/` 20개) +- `공유/기획/FirstRising/맵/` — 빌드결과(§0~§10) · 설계 · 에셋조사 · 실행순서 · 미사용 에셋 목록 · `tools/`(맵 정의 조정 · 촬영 순회 · 블렌더 소품 · `unity_eval/`) +- `공유/기획/FirstRising/몬스터보스/` — 보스 구현 노트 v2 · `tools/unity_eval/`(`comment_scan.py` — 한글 주석 점검 포함) +- FR 준비 폴더(레포 밖): `C:\BurningTimes\FR_maps_staging\`(맵 정의 원본 · 블렌더 소품 작업물) +- 스크래치패드의 도우미는 세션과 함께 사라진다 — 같은 것을 `UI/tools/` 에 넣어 두었다. 새 세션에서는 `bt.sh` 의 `SP`(스크래치패드 경로)만 그 세션의 경로로 바꾸고, `unity_eval/*.cs` 를 `$SP/eval/` 에 복사해 쓴다(`ev <이름>` · `evd <이름>` · `st` · `rc` · `shot <이름>`). **스크립트 안에 인수 파일(`args.txt`) · 출력 폴더의 경로가 이 세션의 스크래치패드(`…\3562a2c1-e88a-4143-b7ec-e74c317778d8\scratchpad\`)로 박혀 있다 — 복사한 뒤 새 경로로 한꺼번에 바꾼다.** + +## 5. 실측으로 알게 된 것(함정) + +- 에디터에서 `Screen.width/height` 는 게임 뷰가 아닌 다른 창의 크기가 나올 때가 있다 → 화면 크기는 뿌리 캔버스의 `pixelRect` 로 읽는다. +- 게임의 로비 페이지 전환(`MainPageUI.ShowPage`)은 한 번에 한 페이지만 켠다 → 옆 창이 페이지를 빌릴 때는 그 페이지의 `Show(true/false)` 를 직접 부른다. +- 프리팹은 저장할 때마다 파일 안 번호가 바뀐다 → 다시 만든 프리팹이 내용으로 같은지는 `prefab_same.py` 로 본다(같으면 되돌려 변경 기록을 남기지 않는다). +- 맵 시험용 씬에서 바로 Play 하면 게임의 시험용 스폰 컴포넌트가 엔티티를 쥔다 → 엔티티를 지우기 전에 그 컴포넌트를 끈다(`TVFieldTravel` 에 반영). +- 씬을 읽는 도중에 Play 를 끄면 Unity Physics 패키지가 예외를 1회 낸다(게임 코드 아님) → 검증 Play 는 도착 뒤 10초쯤 두고 끈다. +- 프로젝트 전체를 훑는 검색(`grep -r … Assets`)은 2분을 넘겨 백그라운드로 넘어간다 → 검색 도구(Grep)에 폴더 · 확장자를 좁혀서 쓴다.