diff --git a/.claude/agent-memory/game-designer/MEMORY.md b/.claude/agent-memory/game-designer/MEMORY.md index 11f106a..51a03a0 100644 --- a/.claude/agent-memory/game-designer/MEMORY.md +++ b/.claude/agent-memory/game-designer/MEMORY.md @@ -1,4 +1,5 @@ # 策划 Agent Memory Index -- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线游戏的灵感记录与共识 +- [千禧年怀旧文化灵感](nostalgia-culture-inspirations.md) — 电视剧/广播体操/广场舞/电话热线/零食/小孩游戏/乡下活动灵感汇总 +- [建筑与区域规划](building-area-plan.md) — 城市主场景完整建筑清单、可进入室内、新增设施共识 - [v0.1执行案状态](v01-execution-status.md) — 三份执行案已通过二次质量门审查,流水线可推进至④程序设计 diff --git a/.claude/agent-memory/game-designer/building-area-plan.md b/.claude/agent-memory/game-designer/building-area-plan.md new file mode 100644 index 0000000..f2f2d2a --- /dev/null +++ b/.claude/agent-memory/game-designer/building-area-plan.md @@ -0,0 +1,37 @@ +--- +name: building-area-plan +description: 城市主场景建筑与区域规划共识,包含完整建筑清单、可进入室内、新增设施 +metadata: + type: project +--- + +## 城市主场景建筑清单(2026-06-10 确认) + +来源:头脑风暴-2026-06-10-建筑与区域规划.md + +### 关键变更(vs 原场景设计) + +- 小伙伴家:原"全部不可进入" → 4家可进入(简化室内) +- 包机房:合并进街机店后屋 +- 废品回收:固定摊位 + 流动收废品车 +- 秘密基地:多处散布(工地围挡后、平房区死角等) +- 新增店铺:粮油店、服装店/裁缝铺、鞋店(主街东侧)、储蓄所(西侧) +- 新增设施:棋牌室(纯氛围)、公交站牌(纯装饰)、公共厕所、自行车棚、信报箱 + +### 主街布局(东西排列) + +东→:学校操场 | 小卖部 | 粮油店 | 书店 | 服装店 | 鞋店 · 公交站 · 棋牌室 | 储蓄所 | 录像出租 | 网吧 | 街机店 ←西 + +### 可进入室内(~12个) + +完整:主角家、合并小卖部、书店、街机店、网吧、录像出租点 +简化:死党A家(小区内)、死党B家(平房区)、老板儿子家(小卖部后屋)、女班长家(主角家同楼) + +### 不做的元素 + +菜地(留乡下)、装修、游泳(河边不下水) + +### How to apply: +- 场景与世界观.md 已更新为最新清单 +- 新增店铺和设施在后续版本逐步添加(见版本映射) +- 概念草案不变(冻结),变更记录在系统设计案中 diff --git a/.claude/agent-memory/game-designer/nostalgia-culture-inspirations.md b/.claude/agent-memory/game-designer/nostalgia-culture-inspirations.md index ae461d9..64b889b 100644 --- a/.claude/agent-memory/game-designer/nostalgia-culture-inspirations.md +++ b/.claude/agent-memory/game-designer/nostalgia-culture-inspirations.md @@ -54,3 +54,34 @@ metadata: - 广播体操和广场舞适合作为场景氛围事件 - 迈皋桥考据直接用于场景设计的"半城半乡"氛围塑造 - 华电子弟学校、工厂宿舍区、菜地、老街都是高还原度的场景元素 + +--- + +## 2026-06-10 灵感补充 + +来源:头脑风暴-2026-06-10-建筑与区域规划.md + +### 重点新发现(⭐⭐⭐) + +1. **钻狗洞的秘密基地** — 多处散布(工地围挡后、平房区死角),高探索叙事价值 +2. **叠纸飞机** — 纸张收集+折叠选择+飞行结果三层玩法 +3. **不知道名字的小哥哥** — 一次性邂逅NPC,极高情感潜力微型叙事线 +4. **体力系统+家庭动态事件** — 吃饭补充体力→不吃炒饭→换速冻水饺→给饭钱 +5. **爷爷奶奶家亲戚小孩** — 乡下NPC角色体系(之前乡下NPC完全空白) + +### 新零食/小吃(具体品牌品种) + +大大泡泡糖、秀逗(超酸糖)、无花果丝、老鼠屎(盐津葡萄)、跳跳糖、烤鱿鱼、烤里脊 + +### 新小孩游戏 + +双人拍手/互推游戏、猜在哪个手里、跑步比赛、叠纸飞机 + +### 新人物/社交 + +- 北方来南方的同学(与"转学生"角色互补) +- 等着去别人家(串门文化) + +### 新乡下活动 + +钓鱼(鱼竿)、游泳、挤牛奶、喂鸡掏鸡蛋、菜地浇水、打水漂 diff --git a/Content/BP/Basic/BP_ChildHoodPlayerController.uasset b/Content/BP/Basic/BP_ChildHoodPlayerController.uasset index 089eb24..e668d48 100644 --- a/Content/BP/Basic/BP_ChildHoodPlayerController.uasset +++ b/Content/BP/Basic/BP_ChildHoodPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8aa209924313a3476a030e81479a993ad1c7170b12c3797f0ac2dcd5c273a58e -size 20473 +oid sha256:a0c7966623a6ef2ecef5d7d97f08731457ed7092c909ab0441618b5be0aabd97 +size 21323 diff --git a/Content/BP/Character/BP_ChildHoodCharacter.uasset b/Content/BP/Character/BP_ChildHoodCharacter.uasset index c803635..cc03e2d 100644 --- a/Content/BP/Character/BP_ChildHoodCharacter.uasset +++ b/Content/BP/Character/BP_ChildHoodCharacter.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab30c1de6a469952710d2728899acbe1df72cb6c5f239d0b0352d5ade60d3423 -size 35388 +oid sha256:0b60ed9418b64d0b9137910d84c4c8e1c3ca6ef9ef7616827e87d61ef59219d3 +size 37182 diff --git a/Source/ChildHood/ChildHoodCharacter.cpp b/Source/ChildHood/ChildHoodCharacter.cpp index 60e34b8..f3a1383 100644 --- a/Source/ChildHood/ChildHoodCharacter.cpp +++ b/Source/ChildHood/ChildHoodCharacter.cpp @@ -16,55 +16,72 @@ AChildHoodCharacter::AChildHoodCharacter() { // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); - + // Don't rotate when the controller rotates. Let that just affect the camera. bUseControllerRotationPitch = false; bUseControllerRotationYaw = false; bUseControllerRotationRoll = false; - // Configure character movement - GetCharacterMovement()->bOrientRotationToMovement = true; - GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); - - // Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint - // instead of recompiling to adjust them - GetCharacterMovement()->JumpZVelocity = 500.f; - GetCharacterMovement()->AirControl = 0.35f; - GetCharacterMovement()->MaxWalkSpeed = 500.f; - GetCharacterMovement()->MinAnalogWalkSpeed = 20.f; - GetCharacterMovement()->BrakingDecelerationWalking = 2000.f; - GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f; + // Configure character movement — v0.1 player controller tech design parameters + auto* CMC = GetCharacterMovement(); + CMC->bOrientRotationToMovement = true; // 角色朝移动方向旋转 + CMC->RotationRate = FRotator(0.0f, 720.0f, 0.0f); // 转向速度 720°/s(远快于鼠标 150°/s) + CMC->MaxWalkSpeed = 450.0f; // 跑步速度 4.5 m/s + CMC->MaxAcceleration = 2250.0f; // 起步加速 0.2s (450/0.2) + CMC->BrakingDecelerationWalking = 1500.0f; // 停止减速 0.3s (450/0.3) + CMC->BrakingDecelerationFalling = 1500.0f; + CMC->MinAnalogWalkSpeed = 20.f; + // WalkableFloorAngle 默认 45°,无需修改 + // v0.1 不跳跃,不设置 JumpZVelocity / AirControl // Create a camera boom (pulls in towards the player if there is a collision) CameraBoom = CreateDefaultSubobject(TEXT("CameraBoom")); CameraBoom->SetupAttachment(RootComponent); - CameraBoom->TargetArmLength = 400.0f; + CameraBoom->TargetArmLength = 350.0f; // 摄像机距离 3.5m + CameraBoom->SocketOffset = FVector(0.f, 0.f, 80.f); // 摄像机高度 0.8m(头部上方) CameraBoom->bUsePawnControlRotation = true; + // 摄像机软跟随 + CameraBoom->bEnableCameraLag = true; // 启用位置延迟跟随 + CameraBoom->CameraLagSpeed = 6.0f; // 跟随速度(可调 2-8) + CameraBoom->bEnableCameraRotationLag = false; // v0.1 不启用旋转延迟 + + // 摄像机碰撞处理 + CameraBoom->bDoCollisionTest = true; // 启用碰撞检测 + CameraBoom->ProbeSize = 12.0f; // 碰撞探针半径 + CameraBoom->ProbeChannel = ECC_Camera; // 使用 Camera 碰撞通道 + // Create a follow camera FollowCamera = CreateDefaultSubobject(TEXT("FollowCamera")); FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); FollowCamera->bUsePawnControlRotation = false; - - // Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) - // are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++) } void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { // Set up action bindings if (UEnhancedInputComponent* EnhancedInputComponent = Cast(PlayerInputComponent)) { - - // Jumping - EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump); - EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping); // Moving EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Move); + + // Looking (gamepad + mouse) + EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look); EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look); - // Looking - EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look); + // Interaction IAs — bind to placeholder callbacks; interaction system will hook into these + if (InteractAction) + { + EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract); + } + if (CancelAction) + { + EnhancedInputComponent->BindAction(CancelAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnCancel); + } + if (SwitchInteractAction) + { + EnhancedInputComponent->BindAction(SwitchInteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnSwitchInteract); + } } else { @@ -90,8 +107,30 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value) DoLook(LookAxisVector.X, LookAxisVector.Y); } +void AChildHoodCharacter::OnInteract(const FInputActionValue& Value) +{ + // v0.1 placeholder — interaction system will provide implementation + // Interaction system hooks in via blueprint or future C++ delegate +} + +void AChildHoodCharacter::OnCancel(const FInputActionValue& Value) +{ + // v0.1 placeholder — interaction system will provide implementation +} + +void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value) +{ + // v0.1 placeholder — interaction system will provide implementation +} + void AChildHoodCharacter::DoMove(float Right, float Forward) { + // P1: 交互中忽略移动输入 + if (bIsInteracting) + { + return; + } + if (GetController() != nullptr) { // find out which way is forward @@ -101,10 +140,10 @@ void AChildHoodCharacter::DoMove(float Right, float Forward) // get forward vector const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); - // get right vector + // get right vector const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); - // add movement + // add movement AddMovementInput(ForwardDirection, Forward); AddMovementInput(RightDirection, Right); } @@ -120,14 +159,18 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch) } } -void AChildHoodCharacter::DoJumpStart() +void AChildHoodCharacter::SetInteracting(bool bValue) { - // signal the character to jump - Jump(); + bIsInteracting = bValue; } -void AChildHoodCharacter::DoJumpEnd() +bool AChildHoodCharacter::IsInteracting() const { - // signal the character to stop jumping - StopJumping(); + return bIsInteracting; +} + +bool AChildHoodCharacter::IsRunning() const +{ + return GetVelocity().SizeSquared() > 100.f // 速度 > 10 cm/s 阈值 + && GetCharacterMovement()->IsMovingOnGround(); } diff --git a/Source/ChildHood/ChildHoodCharacter.h b/Source/ChildHood/ChildHoodCharacter.h index 33f7414..59f53e2 100644 --- a/Source/ChildHood/ChildHoodCharacter.h +++ b/Source/ChildHood/ChildHoodCharacter.h @@ -30,12 +30,8 @@ class AChildHoodCharacter : public ACharacter /** Follow camera */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) UCameraComponent* FollowCamera; - -protected: - /** Jump Input Action */ - UPROPERTY(EditAnywhere, Category="Input") - UInputAction* JumpAction; +protected: /** Move Input Action */ UPROPERTY(EditAnywhere, Category="Input") @@ -49,10 +45,25 @@ protected: UPROPERTY(EditAnywhere, Category="Input") UInputAction* MouseLookAction; + /** Interact Input Action (E key) — delegates to interaction system */ + UPROPERTY(EditAnywhere, Category="Input") + UInputAction* InteractAction; + + /** Cancel Input Action (ESC key) — delegates to interaction system */ + UPROPERTY(EditAnywhere, Category="Input") + UInputAction* CancelAction; + + /** Switch Interact Target Input Action (Tab / Mouse Wheel) — delegates to interaction system */ + UPROPERTY(EditAnywhere, Category="Input") + UInputAction* SwitchInteractAction; + + /** Whether the character is currently in an interaction */ + bool bIsInteracting = false; + public: /** Constructor */ - AChildHoodCharacter(); + AChildHoodCharacter(); protected: @@ -67,6 +78,15 @@ protected: /** Called for looking input */ void Look(const FInputActionValue& Value); + /** Called for interact input (E) — placeholder, delegates to interaction system */ + void OnInteract(const FInputActionValue& Value); + + /** Called for cancel input (ESC) — placeholder, delegates to interaction system */ + void OnCancel(const FInputActionValue& Value); + + /** Called for switch interact target input (Tab/Wheel) — placeholder, delegates to interaction system */ + void OnSwitchInteract(const FInputActionValue& Value); + public: /** Handles move inputs from either controls or UI interfaces */ @@ -77,13 +97,17 @@ public: UFUNCTION(BlueprintCallable, Category="Input") virtual void DoLook(float Yaw, float Pitch); - /** Handles jump pressed inputs from either controls or UI interfaces */ - UFUNCTION(BlueprintCallable, Category="Input") - virtual void DoJumpStart(); + /** Set interaction state — called by interaction system */ + UFUNCTION(BlueprintCallable, Category="Interaction") + void SetInteracting(bool bValue); - /** Handles jump pressed inputs from either controls or UI interfaces */ - UFUNCTION(BlueprintCallable, Category="Input") - virtual void DoJumpEnd(); + /** Query: is the character currently interacting? */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction") + bool IsInteracting() const; + + /** Query: is the character currently running (moving on ground)? */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction") + bool IsRunning() const; public: @@ -93,4 +117,3 @@ public: /** Returns FollowCamera subobject **/ FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; } }; - diff --git a/Source/ChildHood/ChildHoodPlayerController.cpp b/Source/ChildHood/ChildHoodPlayerController.cpp index cadaba8..83d008d 100644 --- a/Source/ChildHood/ChildHoodPlayerController.cpp +++ b/Source/ChildHood/ChildHoodPlayerController.cpp @@ -13,6 +13,19 @@ void AChildHoodPlayerController::BeginPlay() { Super::BeginPlay(); + // C3: 设置摄像机俯仰角限制(垂直旋转 +45°/-30°) + if (PlayerCameraManager) + { + PlayerCameraManager->ViewPitchMin = -30.0f; // 向下看最多 30° + PlayerCameraManager->ViewPitchMax = 45.0f; // 向上看最多 45° + } + + // C1: 初始俯角 -15°(摄像机默认略向下看向角色) + if (GetControlRotation().Pitch == 0.f) + { + SetControlRotation(FRotator(-15.0f, GetControlRotation().Yaw, GetControlRotation().Roll)); + } + // only spawn touch controls on local player controllers if (ShouldUseTouchControls() && IsLocalPlayerController()) { diff --git a/docs/design/场景与世界观.md b/docs/design/场景与世界观.md index 722d23f..910fc3b 100644 --- a/docs/design/场景与世界观.md +++ b/docs/design/场景与世界观.md @@ -2,7 +2,7 @@ > 完整游戏世界的空间、时代、氛围设计。 > 覆盖城市主场景 + 乡下场景,以及场景与各系统的关联。 -> 最后更新:2026-06-01 +> 最后更新:2026-06-10 --- @@ -51,8 +51,8 @@ |------|------|--------| | 尺度 | ~80m × 60m | ~150m × 120m | | 区域数 | 6 个核心 + 1 背景 | 主街 + 生活区 + 边缘三大板块 | -| 可进入室内 | 0-1 个 | 5-6 个(家 + 店铺 + 网吧) | -| 店铺 | 仅小卖部 | 小卖部、书店、街机店、网吧、录像出租点 | +| 可进入室内 | 0-1 个 | ~12 个(家 + 6 店铺 + 4 小伙伴家 + 网吧) | +| 店铺 | 仅小卖部 | 小卖部、书店、粮油店、服装店、鞋店、街机店、网吧、录像出租点、储蓄所 | | 居住形态 | 平房带院子 | 事业单位楼房 + 小区 + 平房区 | ### 2.3 总体布局 @@ -109,7 +109,7 @@ ## 三、商业主街 -主街约 70m 长、5-6m 宽的水泥路,是整个场景最繁华的区域。以中间为界,**西侧是商业娱乐区**(街机、网吧、录像出租),**东侧是传统生活区**(小卖部、书店、学校)。街边散布着各类摊贩。 +主街约 70m 长、5-6m 宽的水泥路,是整个场景最繁华的区域。以中间为界,**西侧是商业娱乐区**(街机、网吧、录像出租、储蓄所、棋牌室),**东侧是传统生活区**(小卖部、书店、粮油店、服装店、鞋店、学校)。街边散布着各类摊贩和公交站牌。 ### 3.1 东侧(传统生活) @@ -201,7 +201,65 @@ --- -#### 3.1.3 学校 +#### 3.1.3 粮油店 + +**位置**:主街东部,书店旁边 +**面积**:小型(~3m × 3m) +**可进入**:简化(门口交互为主) + +**外观**: +- 窄门面,门口摆着几袋大米和油桶 +- 招牌手写"粮油"二字,简单直接 +- 门口有电子秤 + +**时代物件**:散装大米、5L 塑料油桶、酱油醋大缸(散装打)、挂面 + +**系统关联**: +- 氛围营造(城郊生活必需品店铺) +- 可能作为零花钱系统的消费点 + +--- + +#### 3.1.4 服装店/裁缝铺 + +**位置**:主街东部,粮油店旁边 +**面积**:小型(~3m × 3m) +**可进入**:简化(门口交互为主) + +**外观**: +- 门口挂着几件成衣样品 +- 招牌写着"XX 服装"或"裁缝" +- 老板娘坐在门口 + +**时代物件**:成衣、布匹、缝纫机、量衣尺 + +**系统关联**: +- 氛围营造(裁缝铺是城郊标配) +- 简化对话交互 + +--- + +#### 3.1.5 鞋店 + +**位置**:主街东部,服装店旁边 +**面积**:小型(~3m × 3m) +**可进入**:简化(门口交互为主) + +**外观**: +- 门口摆着几排鞋(运动鞋为主) +- 招牌写着"XX 鞋业" +- 墙上挂着运动鞋海报(安踏/李宁风格) + +**时代物件**:运动鞋(安踏/李宁早期款式)、塑料凉鞋、布鞋 + +**系统关联**: +- 氛围营造(运动鞋是小孩的梦想) +- 简化对话交互 +- 可能与"换装"概念联动 + +--- + +#### 3.1.6 学校 **位置**:主街最东端 **朝向**:正门面向主街,侧门(小铁门)面向生活区巷子 @@ -279,6 +337,7 @@ - 经济系统(柜台换币消费) - 小霸王常驻于此(格斗高手) - 西侧的"磁石"——孩子们被声音和灯光吸引过来 +- **包机房后屋**:门帘隔开的内部空间,提供主机游戏(PS/SS 风格),比街机更沉浸但更贵 --- @@ -362,6 +421,58 @@ --- +#### 3.2.4 储蓄所 + +**位置**:主街西部,录像出租点旁边 +**面积**:小型(~4m × 3m) +**可进入**:简化(柜台交互) + +**外观**: +- 正式的门面,比周围店铺更"正经" +- 招牌写着"XX 储蓄所"或"XX 信用社" +- 门上有铁栅栏门(下班后拉上) + +**时代物件**:存折、手写利率牌、算盘、老式点钞机 + +**系统关联**: +- 零花钱系统(存取零花钱?待定) +- 时代氛围(储蓄所是那个年代的金融地标) + +--- + +#### 3.2.5 棋牌室 + +**位置**:主街西部 +**面积**:小型 +**可进入**:❌ 不可进入(大人的场所) + +**外观**: +- 半开的门,里面烟雾缭绕 +- 门口可能坐着抽烟的大人 +- 招牌写着"棋牌"或"茶室" +- 能听到麻将牌碰撞的声音 + +**系统关联**: +- 纯氛围营造(大人的世界,小孩好奇但不能进) +- 声音景观(麻将声作为主街西侧的背景音) + +--- + +#### 3.2.6 主街设施 + +**公交站牌**: +- 主街中段的简易公交站牌(铁杆+铁皮站牌) +- 公交车会经过但不可乘坐 +- 纯装饰,增强城郊出行氛围 + +**废品回收点**: +- 主街路边固定摊位 +- 一把秤 + 几个编织袋 + 地上分类的废品 +- 老板娘坐镇,可交互卖废品 +- 与流动收废品车互补(固定点方便,流动车价格可能不同) + +--- + ### 3.3 街边摊贩 **分布**:散布在主街各处 + 生活区巷子中 @@ -372,6 +483,8 @@ |----------|---------|---------|------| | 冰棍推车 | 主街阴凉处、学校门口 | 泡沫箱+棉被保温,绿豆冰/红豆冰/白糖冰棍 | 夏天标配 | | 烤肠机 | 小卖部门口、网吧门口 | 电烤机,1-2 元一根 | 简单经济触点 | +| 烤鱿鱼 | 主街散布 | 铁板,酱料 | 城郊特色小吃 | +| 烤里脊 | 主街散布 | 铁板串烤 | 城郊特色小吃 | | 糖葫芦 | 随机出现 | 插在草把子上,山楂/草莓/橘子 | 随机事件 | | 爆米花 | 偶尔出现在主街/空地附近 | 老式黑铁葫芦爆米花机,"嘭"一声 | 随机事件,声音记忆 | | 棉花糖 | 偶尔出现在空地/学校附近 | 手摇式棉花糖机 | 随机事件 | @@ -396,15 +509,28 @@ |---|----------|------|--------|---------| | 1 | 合并小卖部 | 8m×5m | ✅ | 经济核心、收集入口、街坊聚集 | | 2 | 书店 | 5m×4m | ✅ | 漫画/书籍、叙事触点、时代氛围 | -| 3 | 学校(仅操场) | 25m×18m | — | 肢体活动、剧情场景 | +| 3 | 粮油店 | ~3m×3m | 简化 | 氛围营造、城郊生活必需品 | +| 4 | 服装店/裁缝铺 | ~3m×3m | 简化 | 氛围营造、简化对话 | +| 5 | 鞋店 | ~3m×3m | 简化 | 氛围营造、运动鞋梦想 | +| 6 | 学校(仅操场) | 25m×18m | — | 肢体活动、剧情场景 | **西侧(商业娱乐)**: | # | 店铺/设施 | 面积 | 可进入 | 核心功能 | |---|----------|------|--------|---------| -| 4 | 街机店 | 8m×6m | ✅ | 街机小游戏、社交核心 | -| 5 | 网吧 | 6m×5m | ✅ | PC 小游戏、社交、时代氛围 | -| 6 | 录像出租点 | 5m×4m | ✅ | 录像带/磁带、叙事触点、收集 | +| 7 | 街机店(含包机房后屋) | 8m×6m | ✅ | 街机小游戏、社交核心、包机房 | +| 8 | 网吧 | 6m×5m | ✅ | PC 小游戏、社交、时代氛围 | +| 9 | 录像出租点 | 5m×4m | ✅ | 录像带/磁带、叙事触点、收集 | +| 10 | 储蓄所 | ~4m×3m | 简化 | 存取零花钱?时代氛围 | +| 11 | 棋牌室 | 小型 | ❌ | 纯氛围(大人的场所) | + +**主街设施**: + +| # | 设施 | 性质 | +|---|------|------| +| 12 | 公交站牌 | 纯装饰 | +| 13 | 废品回收点 | 路边固定摊位,可交互卖废品 | +| 14 | 街边摊贩 × N | 散布式(烤鱿鱼、烤里脊、冰棍推车、烤肠等) | --- @@ -430,9 +556,15 @@ - 楼道:水泥楼梯,声控灯,墙上贴着小广告和水电费通知 **死党 A 的家**: -- 在某栋楼的某层(不可进入,但外观有标识——比如阳台上挂着四驱车赛道模型) +- 在某栋楼的某层(简化室内,可进入) +- 外观有标识——比如阳台上挂着四驱车赛道模型 +- 室内简化:小空间 + 对话为主,展示家境好的氛围(大电视、四驱车跑道、电脑) - 从楼下能看到他家阳台上摆着的东西 +**小区设施**: +- **自行车棚**:水泥柱 + 石棉瓦顶,停着几辆老式自行车(永久牌/凤凰牌) +- 信报箱区(铁皮信报箱,嵌在楼道口墙上) + **系统关联**: - 四驱车系统(死党 A 在小区空地展示/比拼四驱车) - 社交场所(小区空地是另一个孩子聚集点) @@ -498,6 +630,15 @@ - 墙上贴着水电费通知、小广告 - 楼道里偶尔碰到邻居(背景 NPC) +**女班长家**: +- 同栋楼不同层(简化室内,可进入) +- 室内简化:小空间 + 对话为主 +- 女孩的房间视角,与主角家形成对照 + +**楼下设施**: +- **信报箱**:铁皮信报箱嵌在楼道口,可能收到通知/信件 +- **自行车棚**:楼旁小型车棚(如有独立空间) + **系统关联**: - FC 家用机小游戏(坐在客厅电视前玩) - 暑假作业叙事线(书桌上的作业) @@ -535,11 +676,16 @@ | 修车铺 | 修自行车的,门口散落零件 | 扳手、打气筒、旧轮胎 | | 小吃摊 | 下午出摊,卖凉皮、凉面 | 铁皮推车、调料瓶一字排开 | -这些小店大多不可进入(仅在门口交互),主要是氛围营造和日常事件触发。 +这些小店大多简化交互(门口对话为主),主要是氛围营造和日常事件触发。其中早餐店可交互买东西。 + +**平房区设施**: +- **公共厕所**:平房区内的旱厕,城郊生活的标志性设施 +- **公共水龙头**:巷子角落 **死党 B 的家**: -- 平房区某一间(外观有标识——门口可能放着弹珠罐子或自制玩具) -- 不可进入,但门口有时可以和死党 B 互动 +- 平房区某一间(简化室内,可进入) +- 外观有标识——门口可能放着弹珠罐子或自制玩具 +- 室内简化:小空间 + 对话为主,展示温馨但简朴的氛围 **系统关联**: - 对比叙事(小区 vs 平房区的物质差异) @@ -574,11 +720,16 @@ | # | 区域 | 位置 | 类型 | 核心功能 | |---|------|------|------|---------| -| 1 | 小区 | 靠东侧 | 正经住宅(3-4 栋 6 层楼 + 中庭) | 死党 A 的家、四驱车场地、对比叙事 | +| 1 | 小区 | 靠东侧 | 正经住宅(3-4 栋 6 层楼 + 中庭) | 死党 A 的家(简化室内)、四驱车场地、对比叙事 | | 2 | 主角家 | 中部 | 事业单位家属楼(独栋 6 层) | 起点/终点、FC 游戏、叙事 | -| 3 | 平房区 | 靠西侧 | 自建平房群 + 生活小店 | 死党 B 的家、弹珠场地、对比叙事 | +| 2b | 女班长家 | 同主角家楼 | 同栋不同层(简化室内) | 女班长家、社交 | +| 3 | 平房区 | 靠西侧 | 自建平房群 + 生活小店 | 死党 B 的家(简化室内)、弹珠场地、对比叙事 | | 4 | 巷子通道 | 贯穿 | 2-3 个巷子口 + 内部连接路 | 穿行、日常事件、氛围 | +**小区设施**:自行车棚、信报箱 +**平房区设施**:公共厕所、公共水龙头 +**平房区小店**:早餐店(可交互)、理发店/修车铺/缝纫店(背景+对话) + --- ## 五、边缘区域 @@ -644,6 +795,24 @@ --- +### 5.3 秘密基地(隐藏区域) + +**设计理念**:散布在地图各处的小型隐藏空间,每个都有独特的"钻进去"方式。代表童年探索的乐趣——发现只有小孩才知道的秘密角落。 + +| # | 名称 | 位置 | 进入方式 | 氛围 | +|---|------|------|---------|------| +| H1 | 秘密基地 A | 工地蓝色围挡后面 | 围挡底部有个洞,蹲下钻进去 | 废弃建材、涂鸦、偷藏的零食 | +| H2 | 秘密基地 B | 平房区深处的死角 | 穿过一条很窄的巷子 | 老式家具、墙角涂鸦、安静 | +| H3 | 秘密基地 C | 待定 | 待定 | 待定 | + +**系统关联**: +- 探索奖励(发现秘密基地本身就是一个成就) +- 收集品散落(秘密基地里有独特收集品) +- 叙事触点(秘密基地里发现前人留下的东西、和朋友在这里约定) +- 氛围营造("这是我们的秘密"的专属感) + +--- + ## 六、乡下场景(奶奶家) ### 6.1 定位与尺度 @@ -785,20 +954,29 @@ ``` ┌──────────────────────────────────┐ │ 大型空地/工地 河边/河堤 │ + │ [秘密基地A] │ └──────────┬───────────┬───────────┘ │ │ ┌──────────┴───────────┴───────────┐ │ 生活区巷子 │ - │ [平房区] ── [主角家] ── [小区] │ + │ [平房区] ── [主角家+女班长家] ── [小区] │ + │ [秘密基地B] │ + │ [早餐店·理发店·修车铺·缝纫店] │ + │ [公共厕所·自行车棚·信报箱] │ └──┬──────────┬──────────┬─────────┘ │巷子口① │巷子口② │巷子口③ ┌──┴──────────┴──────────┴──────────┐ │ 商业主街 │ │ │ │ 西侧(娱乐) 东侧(生活) │ - │ [街机店] [小卖部] │ - │ [网吧] [书店] │ - │ [录像出租] [学校] │ + │ [棋牌室] [小卖部] │ + │ [储蓄所] [粮油店] │ + │ [录像出租] [书店] │ + │ [网吧] [服装店] │ + │ [街机店] [鞋店] │ + │ [学校] │ + │ │ + │ [公交站牌] [废品回收点] [摊贩×N] │ └────────────────────────────────────┘ ``` @@ -858,7 +1036,7 @@ | **女班长** | 学校操场(跳绳/跳皮筋) | 主街、小卖部门口 | 上午活跃 | | **转学生** | 小卖部门口(初来时) | 逐渐扩展到空地、街机店 | 中后期活动范围扩大 | | **残障儿童** | 平房区巷子口(家附近) | 小卖部门口(坐着看大家玩) | 下午出现 | -| **小卖部老板儿子** | 合并小卖部 | 街机店(偷溜去玩) | 上午在店里、下午偷溜 | +| **小卖部老板儿子** | 合并小卖部(后屋是家) | 街机店(偷溜去玩) | 上午在店里、下午偷溜 | | **小霸王** | 街机店(格斗高手) | 空地(找人挑衅) | 下午到傍晚 | ### 8.2 设计原则 @@ -994,10 +1172,17 @@ |------|------|------| | 合并小卖部 | 主街东侧 | 买零食、买文具、买四驱车配件、开卡片 | | 书店 | 主街东侧 | 租借漫画/书籍 | -| 街机店 | 主街西侧 | 柜台换币、投币游戏 | +| 粮油店 | 主街东侧 | 氛围 + 可能消费 | +| 服装店/裁缝铺 | 主街东侧 | 氛围 + 简化对话 | +| 鞋店 | 主街东侧 | 氛围 + 简化对话 | +| 街机店 | 主街西侧 | 柜台换币、投币游戏、包机房 | | 网吧 | 主街西侧 | 上网收费 | | 录像出租点 | 主街西侧 | 租借录像带/磁带 | +| 储蓄所 | 主街西侧 | 存取零花钱?待定 | +| 废品回收点 | 主街路边 | 卖废品换钱 | +| 早餐店 | 平房区 | 买早点(包子油条豆浆) | | 街边摊贩 | 主街 + 巷子 | 买冰棍、买小吃(比店铺便宜) | +| 流动收废品车 | 巷子巡游 | 卖废品换钱(流动价格) | | 村口小卖部 | 乡下 | 乡下唯一消费点 | ### 10.4 叙事系统 × 场景触发 @@ -1086,9 +1271,9 @@ | **v0.2** | NPC 放置 + 弹珠场地标识 + NPC 常驻点标识 | | **v0.3** | 时段光影 + 天气初步 + 学校操场 | | **v0.4** | 街机店室内 + 录像出租点室内 + 叙事触发点布设 + 小区外观 | -| **v0.5** | 网吧室内 + 书店室内 + 平房区 + 生活小店 + 工地围挡 + 摊贩体系 | +| **v0.5** | 网吧室内 + 书店室内 + 粮油店/服装店/鞋店 + 储蓄所 + 平房区 + 生活小店 + 工地围挡 + 摊贩体系 + 废品回收点 | | **v0.6** | **乡下场景全部**(奶奶家、小溪、田间路、村口小卖部) | -| **v0.7** | 全场景整合 + 城乡切换 + 细节打磨 | +| **v0.7** | 全场景整合 + 城乡切换 + 4 家小伙伴家(简化室内)+ 秘密基地 + 棋牌室 + 公交站牌 + 细节打磨 | | **v1.0** | 氛围精修(声音景观、天气效果、光影优化) | --- @@ -1100,7 +1285,13 @@ - [ ] 乡下阶段的 NPC 具体设计(村里有几个人?什么性格?) - [ ] 天气系统的详细设计(v0.3 前确定范围) - [ ] 室内场景的光照方案(自然光 vs 灯光) -- [ ] 平房区生活小店哪些可以进入、哪些仅门口交互 - [ ] 网吧的 PC 小游戏内容和家里 FC 的体验差异如何区分 - [ ] 街边摊贩的出现逻辑(固定位置+固定时段?还是随机事件?) - [ ] 主街东西两侧是否有视觉上的渐变差异(东侧更传统朴素、西侧更鲜亮热闹?) +- [x] ~~平房区生活小店哪些可以进入、哪些仅门口交互~~ → 早餐店可交互,其他简化对话 +- [x] ~~小伙伴家是否可进入~~ → 4 家可进入(全部简化室内):死党A家、死党B家、老板儿子家(小卖部后屋)、女班长家(主角家同楼) +- [x] ~~包机房安排~~ → 合并进街机店后屋 +- [x] ~~废品回收点~~ → 主街固定摊位 + 流动收废品车 +- [ ] 秘密基地 C 的具体位置和进入方式 +- [ ] 储蓄所是否真正参与零花钱系统(还是纯氛围) +- [ ] 服装店/鞋店是否有实际交互(买衣服/换装?还是纯氛围) diff --git a/docs/design/头脑风暴-2026-06-10-建筑与区域规划.md b/docs/design/头脑风暴-2026-06-10-建筑与区域规划.md new file mode 100644 index 0000000..23d02d0 --- /dev/null +++ b/docs/design/头脑风暴-2026-06-10-建筑与区域规划.md @@ -0,0 +1,281 @@ +# 头脑风暴:灵感补充 + 城郊建筑与区域规划 + +**日期**:2026-06-10 +**参与**:starmen1265 × 策划 Agent +**状态**:✅ 方向确认完毕 + +--- + +## 讨论一:灵感列表对比与新发现 + +用户提供了一份童年记忆灵感列表,对照已有三份头脑风暴记录进行逐一对比。 + +### 已有记录(不重复) + +抓虫(乡下抓知了)、跳皮筋、呼啦圈、跳房子、石头剪刀布、警察抓小偷、一二三木头人、天线调台、日立电视、收音机、ADSL拨号上网、排球女将、大哥大、BP机、广播体操、广场舞、废旧报纸卖废品、少年包青天、菜地(迈皋桥地理元素) + +### ⭐ 新发现元素 + +#### 乡下乡土活动(奶奶家阶段素材) + +| # | 元素 | 灵感价值 | +|---|------|---------| +| 1 | 钓鱼(鱼竿,区别于"摸鱼") | ⭐⭐ 乡村独立小游戏潜力 | +| 2 | 游泳(夏天河里/水塘) | ⭐⭐ 场景事件+氛围 | +| 3 | 挤牛奶 | ⭐ 互动体验 | +| 4 | 喂鸡、掏鸡蛋 | ⭐⭐⭐ 高辨识度、互动感强 | +| 5 | 菜地浇水 | ⭐ 零花钱/好感度任务 | +| 6 | 打水漂 | ⭐⭐ 简单小互动,有物理乐趣 | + +#### 小孩日常小游戏 + +| # | 元素 | 灵感价值 | +|---|------|---------| +| 7 | 双人拍手小游戏(互推游戏) | ⭐⭐ 简单但互动感极强 | +| 8 | 钻狗洞的秘密基地 | ⭐⭐⭐ **极佳的探索叙事素材** | +| 9 | 猜在哪个手里 | ⭐ 最简单的NPC互动小游戏 | +| 10 | 叠纸飞机(不同纸、不同技法) | ⭐⭐⭐ 收集+小游戏双潜力 | +| 11 | 跑步比赛 | ⭐ 简单的NPC互动触发 | + +#### 人物与社交 + +| # | 元素 | 灵感价值 | +|---|------|---------| +| 12 | 爷爷奶奶家的各种年龄段亲戚小孩 | ⭐⭐⭐ 乡下NPC角色体系 | +| 13 | 北方来南方的同学 | ⭐⭐⭐ 与"转学生"角色互补 | +| 14 | 等着去别人家(串门文化) | ⭐⭐ 日常叙事素材 | +| 15 | 不知道名字的小哥哥跟你玩了一下午 | ⭐⭐⭐ **极高情感潜力的微型叙事线** | + +#### 食物与零食 + +| # | 元素 | 灵感价值 | +|---|------|---------| +| 16 | 大大泡泡糖 | ⭐⭐ 品牌辨识度极高 | +| 17 | 秀逗(超酸的糖) | ⭐⭐ 互动素材——表情反应 | +| 18 | 无花果(丝) | ⭐ 零花钱消费场景填充 | +| 19 | 老鼠屎(盐津葡萄/话梅粒) | ⭐⭐ 名字本身就是小孩文化缩影 | +| 20 | 跳跳糖 | ⭐⭐ 视觉效果潜力 | +| 21 | 烤鱿鱼 | ⭐ 街边饮食扩展 | +| 22 | 烤里脊 | ⭐ 街边饮食扩展 | +| 23 | 炒饭和速冻水饺 | ⭐⭐ 与体力系统直接联动 | + +#### 日常生活与物品 + +| # | 元素 | 灵感价值 | +|---|------|---------| +| 24 | 每天都有吃饭聊天时间 | ⭐⭐ 氛围事件+叙事窗口 | +| 25 | 小天鹅(洗衣机品牌) | ⭐ 时代品牌元素 | +| 26 | MP3(2000年前后刚开始出现) | ⭐⭐ 时代物品——比随身听更"潮" | +| 27 | 装修(邻居家在装修) | ⭐⭐ 场景细节(讨论后决定不做) | +| 28 | 墙上涂鸦 | ⭐ 收集/互动素材 | +| 29 | 十二生肖 | 待确认具体指什么 | +| 30 | 吃遍附近的美食 | ⭐⭐ 探索型目标 | +| 31 | 纸壳 | ⭐ 与叠纸飞机联动 | +| 32 | 西游记后传(2000年播出) | ⭐⭐ 电视内容补充 | +| 33 | 玉河摩托 | 待确认具体指什么 | +| 34 | 公交 | ⭐ 场景/移动手段 | + +#### 游戏机制灵感 + +**体力系统雏形**: +> 吃饭补充体力,饭点之前体力无限。如果不吃饭,体力没跑几步就没了。 + +**动态家庭事件链**: +> 爸爸妈妈早上会给留两顿炒饭 → 如果都不吃,连续三天,会换成速冻水饺 → 如果还不吃,每天会多给两顿饭钱。 + +这两个设计同时做到了:还原真实 × 驱动行为 × 叙事表达。 + +#### 重点标记 ⭐⭐⭐ + +1. **钻狗洞的秘密基地** — 探索叙事 +2. **叠纸飞机** — 收集+小游戏 +3. **爷爷奶奶家的亲戚小孩** — 乡下NPC体系 +4. **不知道名字的小哥哥** — 情感微型叙事 +5. **体力系统+家庭动态事件** — 核心玩法机制 + +--- + +## 讨论二:城郊区域建筑与区域规划 + +基于全部已记录的活动/事件,讨论城市主场景应有哪些建筑与区域(不含乡下)。 + +### 确认的变更(相比现有场景设计) + +| # | 变更项 | 原设计 | 新决定 | +|---|--------|--------|--------| +| 1 | 小伙伴家 | 全部不可进入 | 4 家可进入(简化室内) | +| 2 | 包机房 | 无 | 合并进街机店后屋 | +| 3 | 废品回收 | 无 | 固定点 + 流动收废品车 | +| 4 | 公交站 | 无 | 主街公交站牌(纯装饰) | +| 5 | 秘密基地 | 无 | 多处散布 | +| 6 | 棋牌室 | 无 | 新增 | +| 7 | 储蓄所 | 无 | 新增 | +| 8 | 菜地 | 无 | 不做(留乡下) | +| 9 | 装修 | 无 | 不做 | +| 10 | 游泳 | 无 | 河边不能下水 | + +--- + +### 完整建筑与区域清单 + +#### 一、商业主街(东西走向,~70m x 5-6m) + +**东侧(传统生活)** + +| # | 名称 | 尺寸 | 可进入 | 核心功能 | 备注 | +|---|------|------|--------|---------|------| +| S1 | 合并小卖部 | 8m x 5m | ✅ | 零食/文具/四驱配件/经济核心 | 后半部分是老板儿子家(简化室内) | +| S2 | 书店 | 5m x 4m | ✅ | 漫画/书籍租借 | | +| S3 | 粮油店 | 小型 | ✅ | 米面油酱醋 | | +| S4 | 服装店/裁缝铺 | 小型 | 简化 | 氛围 + 可能买衣服 | | +| S5 | 鞋店 | 小型 | 简化 | 氛围 + 运动鞋梦想 | | + +**西侧(娱乐商业)** + +| # | 名称 | 尺寸 | 可进入 | 核心功能 | 备注 | +|---|------|------|--------|---------|------| +| S6 | 街机店 | 8m x 6m | ✅ | 街机小游戏 + 社交 | 含包机房后屋(门帘隔着) | +| S7 | 网吧 | 6m x 5m | ✅ | PC 小游戏 + 社交 | | +| S8 | 录像出租点 | 5m x 4m | ✅ | 录像带/磁带租借 | 只出租,无观影厅 | +| S9 | 储蓄所 | 小型 | 简化 | 存取零花钱? | | +| S10 | 棋牌室 | 小型 | ❌ | 纯氛围(大人的场所) | | + +**主街设施(散布)** + +| # | 名称 | 性质 | +|---|------|------| +| S11 | 公交站牌 | 纯装饰,公交经过不可乘坐 | +| S12 | 废品回收点 | 路边固定摊位,可交互卖废品 | +| S13 | 街边摊贩 × N | 散布式(烤鱿鱼、烤里脊、冰棍推车、烤肠等) | + +**主街尽端** + +| # | 名称 | 尺寸 | 可进入 | +|---|------|------|--------| +| S14 | 学校操场 | 25m x 18m | 仅室外 | + +#### 二、生活区巷子(主街北侧,南北纵深) + +**中部 — 事业单位家属楼** + +| # | 名称 | 性质 | 备注 | +|---|------|------|------| +| R1 | 主角家 | 6 层家属楼,完整室内(两室一厅) | 起点终点、FC 游戏、电视、电话 | +| R2 | 女班长家 | 同栋楼,简化室内 | 同楼不同层 | +| R3 | 信报箱 | 楼下设施 | 氛围 + 可能收信/通知 | + +**东侧 — 小区** + +| # | 名称 | 性质 | 备注 | +|---|------|------|------| +| R4 | 小区 | 3-4 栋 6 层楼 + 中庭空地 | 死党 A 住这里 | +| R5 | 死党 A 家 | 小区内某栋,简化室内 | 家境好,有四驱车跑道 | +| R6 | 自行车棚 | 小区内设施 | 氛围 | + +**西侧 — 平房区** + +| # | 名称 | 性质 | 可交互 | 备注 | +|---|------|------|--------|------| +| R7 | 平房区 | 十几二十间平房 + 窄巷子 | — | 死党 B 住这里 | +| R8 | 死党 B 家 | 平房区内某间,简化室内 | ✅ | 家境差,温馨 | +| R9 | 早餐店 | 平房区沿街 | ✅ 买早点 | 包子油条豆浆 | +| R10 | 理发店 | 平房区小店 | 简化对话 | 老式转椅+镜子 | +| R11 | 修车铺 | 平房区小店 | 简化对话 | 门口堆轮胎零件 | +| R12 | 缝纫店 | 平房区小店 | 简化对话 | 缝纫机响 | +| R13 | 公共厕所 | 平房区内 | ❌ | 城郊生活标志 | + +**巷子通道** + +| # | 名称 | 备注 | +|---|------|------| +| R14 | 巷子口 × 2-3 | 主街与生活区的连接口 | + +#### 三、边缘区域(北端) + +| # | 名称 | 尺寸 | 备注 | +|---|------|------|------| +| E1 | 大型空地/工地 | ~40m x 30m | 一半空地(弹珠主战场),一半工地(围挡) | +| E2 | 河边/河堤 | ~50m x 8m | 不能下水,氛围+打水漂 | + +#### 四、隐藏区域 + +| # | 名称 | 位置 | 进入方式 | +|---|------|------|---------| +| H1 | 秘密基地 A | 工地围挡后面 | 钻洞 | +| H2 | 秘密基地 B | 平房区死角 | 穿窄巷 | +| H3 | 秘密基地 C | 待定 | 待定 | + +#### 五、移动元素 + +| # | 名称 | 路线 | 功能 | +|---|------|------|------| +| M1 | 流动收废品车 | 巷子内巡游 | 卖废品 | +| M2 | 街边摊贩 | 主街散布 | 零食/小吃 | + +--- + +### 主街店铺分布(东西排列) + +``` + ← 东(生活) 西(娱乐)→ +┌──────────────────────────────────────────────────────────────────────┐ +│ [学校操场] [小卖部] [粮油店] [书店] [服装店] [鞋店] · 公交站 · [棋牌室] [储蓄所] [录像出租] [网吧] [街机店] │ +│ ↑ 废品回收点(路边) ↑ 摊贩散布 │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +### 可进入室内完整清单 + +| # | 地点 | 规模 | 关联 NPC | +|---|------|------|---------| +| 1 | 主角家 | 完整(两室一厅) | — | +| 2 | 合并小卖部 | 完整 | 老板儿子 | +| 3 | 书店 | 中等 | — | +| 4 | 街机店(含包机房后屋) | 中等 | 小霸王 | +| 5 | 网吧 | 中等 | — | +| 6 | 录像出租点 | 中等 | — | +| 7 | 死党 A 家 | 简化(小空间+对话) | 死党 A | +| 8 | 死党 B 家 | 简化 | 死党 B | +| 9 | 老板儿子家 | 简化(小卖部后屋) | 老板儿子 | +| 10 | 女班长家 | 简化(主角家同楼) | 女班长 | + +--- + +### 讨论共识记录 + +1. **菜地**:城市场景不做,留给乡下阶段 +2. **秘密基地**:多处散布,小型空间(工地围挡后、平房区死角、至少 2-3 处) +3. **小伙伴家**:4 家可进入,全部简化室内(小空间 + 对话为主) + - 死党 A 家 → 小区内 + - 死党 B 家 → 平房区内 + - 老板儿子家 → 小卖部后屋 + - 女班长家 → 主角家同楼不同层 +4. **录像厅**:只做出租点,不做观影厅 +5. **包机房**:合并进街机店后屋 +6. **废品回收**:主街固定摊位 + 流动收废品车(巷子巡游) +7. **装修**:不做 +8. **公交站**:纯装饰站牌 +9. **摊贩**:散布式 +10. **游泳**:河边不能下水 +11. **新增店铺**:粮油店、服装店/裁缝铺、鞋店 → 都放主街 +12. **新增设施**:棋牌室(大人场所/纯氛围)、储蓄所、公共厕所、自行车棚、信报箱 +13. **平房区小店**:早餐店(可交互)、理发店/修车铺/缝纫店(背景+对话) + +--- + +### 统计 + +| 类别 | 数量 | +|------|------| +| 商业主街店铺/设施 | 14 个 | +| 生活区建筑/设施 | 14 个 | +| 边缘区域 | 2 个 | +| 隐藏区域 | 多处 | +| 移动元素 | 2 类 | +| **可进入室内** | ~12 个 | + +### 与概念草案的关系 + +概念草案定义「可进入室内:主角家 + 4 家店铺」。新设计变更为 6 家店铺 + 4 家小伙伴家(简化)+ 主角家 = ~12 个可进入室内。这是扩展,不与概念草案冲突。概念草案冻结,变更记录在系统设计案中。 diff --git a/tools/input-setup/setup_input.bat b/tools/input-setup/setup_input.bat new file mode 100644 index 0000000..ee8219b --- /dev/null +++ b/tools/input-setup/setup_input.bat @@ -0,0 +1,132 @@ +@echo off +setlocal enabledelayedexpansion + +REM === ChildHood Input Setup - Create v0.1 InputAction assets + update IMC === +REM +REM Usage: +REM setup_input.bat (default, editor quits after done) +REM setup_input.bat --no-quit (keep editor open) + +set "SCRIPT_DIR=%~dp0" +set "PROJECT_DIR=%SCRIPT_DIR%..\.." +for %%i in ("%PROJECT_DIR%\.") do set "PROJECT_ROOT=%%~fi" +set "UPROJECT=%PROJECT_ROOT%\ChildHood.uproject" + +REM ---- Defaults ---- +set "NO_QUIT=0" +set "DRY_RUN=0" + +REM ---- Parse Arguments ---- +:parse_args +if "%~1"=="" goto done_args +if /i "%~1"=="--no-quit" ( + set "NO_QUIT=1" + shift + goto parse_args +) +if /i "%~1"=="--dry-run" ( + set "DRY_RUN=1" + shift + goto parse_args +) +if /i "%~1"=="--help" goto show_help +if /i "%~1"=="-h" goto show_help +echo [WARN] Unknown argument: %~1 +shift +goto parse_args +:done_args + +REM ---- Find UE5 Editor ---- +set "UE_EDITOR=" +if defined UE5_ROOT ( + for %%b in (UnrealEditor.exe UE5Editor.exe) do ( + if exist "%UE5_ROOT%\Engine\Binaries\Win64\%%b" ( + set "UE_EDITOR=%UE5_ROOT%\Engine\Binaries\Win64\%%b" + ) + ) +) + +if "%UE_EDITOR%"=="" ( + for %%p in ( + "C:\Program Files\Epic Games\UE_5.7" + "C:\Program Files (x86)\Epic Games\UE_5.7" + "D:\Program Files\Epic Games\UE_5.7" + "E:\Program Files\Epic Games\UE_5.7" + ) do ( + if not defined UE_EDITOR ( + for %%b in (UnrealEditor.exe UE5Editor.exe) do ( + if exist "%%~p\Engine\Binaries\Win64\%%b" ( + set "UE_EDITOR=%%~p\Engine\Binaries\Win64\%%b" + ) + ) + ) + ) +) + +if "%UE_EDITOR%"=="" ( + echo. + echo [ERROR] UE5 Editor not found! + echo Set UE5_ROOT, e.g.: set UE5_ROOT=C:\Program Files\Epic Games\UE_5.7 + echo. + exit /b 1 +) + +REM ---- Path conversion ---- +set "SCRIPT_PATH=%SCRIPT_DIR%setup_input_assets.py" +set "SCRIPT_PATH=!SCRIPT_PATH:\=/!" +set "UPROJECT_FWD=!UPROJECT:\=/!" + +REM ---- Build command line ---- +set "CMD_ARGS=-unattended -NoSound -NullRHI -ExecutePythonScript=!SCRIPT_PATH!" + +REM ---- Pass config via env vars ---- +set "INPUT_PROJECT_ROOT=!PROJECT_ROOT!" +if "!NO_QUIT!"=="1" set "INPUT_NO_QUIT=1" + +REM ---- Print info ---- +echo. +echo ============================================ +echo ChildHood Input Setup - v0.1 +echo ============================================ +echo UE5: !UE_EDITOR! +echo Project: !UPROJECT_FWD! +echo Script: !SCRIPT_PATH! +echo ============================================ +echo. + +REM ---- Dry Run ---- +if "%DRY_RUN%"=="1" ( + echo [DRY RUN] Command: + echo. + echo "!UE_EDITOR!" "!UPROJECT_FWD!" !CMD_ARGS! + echo. + exit /b 0 +) + +REM ---- Execute ---- +echo [InputSetup] Launching UE5 Editor ... +echo. + +"!UE_EDITOR!" "!UPROJECT_FWD!" !CMD_ARGS! + +set "EXIT_CODE=!ERRORLEVEL!" +echo. +if "!EXIT_CODE!"=="0" ( + echo [InputSetup] Done. Editor exited. +) else ( + echo [InputSetup] Editor exit code: !EXIT_CODE! +) +exit /b !EXIT_CODE! + +:show_help +echo. +echo ChildHood Input Setup - v0.1 +echo. +echo Usage: setup_input.bat [options] +echo. +echo Options: +echo --no-quit Keep editor open after done +echo --dry-run Print command without executing +echo --help, -h Show help +echo. +exit /b 0 diff --git a/tools/input-setup/setup_input_assets.py b/tools/input-setup/setup_input_assets.py new file mode 100644 index 0000000..69982a5 --- /dev/null +++ b/tools/input-setup/setup_input_assets.py @@ -0,0 +1,225 @@ +"""UE5 编辑器 Python 脚本:创建 v0.1 角色控制所需的 InputAction 资产并更新 IMC。 + +由 setup_input.bat 通过 UE5 的 -ExecutePythonScript 参数调用。 +在 UE5 编辑器 Python 环境中运行。 + +功能: + 1. 创建 3 个 InputAction 资产:IA_Interact, IA_Cancel, IA_SwitchInteract + 2. 更新 IMC_Default:添加 E / ESC / Tab 按键映射 + 3. 保存所有修改的资产 + +环境变量(由 setup_input.bat 设置): + INPUT_PROJECT_ROOT — 项目根目录绝对路径 +""" + +import os +import sys +from pathlib import Path + +# ---- 检查 unreal 模块 ---- +try: + import unreal +except ImportError: + print("[InputSetup] FATAL: unreal 模块不可用,必须在 UE5 编辑器环境中运行") + sys.exit(1) + +# ---- 项目根目录 ---- +_project_root_str = os.environ.get("INPUT_PROJECT_ROOT", "") +if _project_root_str: + PROJECT_ROOT = Path(_project_root_str) +else: + print("[InputSetup] FATAL: INPUT_PROJECT_ROOT 环境变量未设置") + sys.exit(1) + +# ---- 资产路径配置 ---- +IA_PACKAGE = "/Game/Input/Actions" +IMC_PATH = "/Game/Input/IMC_Default" + +# 新增 InputAction 定义 +NEW_INPUT_ACTIONS = { + "IA_Interact": { + "description": "E 键交互输入动作", + "key": unreal.Key.E, + "triggers": [unreal.InputTriggerDown()], + }, + "IA_Cancel": { + "description": "ESC 取消输入动作", + "key": unreal.Key.ESCAPE, + "triggers": [unreal.InputTriggerDown()], + }, + "IA_SwitchInteract": { + "description": "Tab 切换交互目标输入动作", + "key": unreal.Key.TAB, + "triggers": [unreal.InputTriggerDown()], + }, +} + + +def asset_exists(asset_path: str) -> bool: + """检查资产是否已存在""" + return unreal.EditorAssetLibrary.does_asset_exist(asset_path) + + +def create_input_action(name: str, description: str) -> unreal.InputAction: + """创建 InputAction 资产""" + asset_path = f"{IA_PACKAGE}/{name}" + + if asset_exists(asset_path): + print(f"[InputSetup] 资产已存在,跳过创建: {asset_path}") + return unreal.load_asset(asset_path) + + # 确保目录存在 + unreal.EditorAssetLibrary.make_directory(IA_PACKAGE) + + # 使用 AssetTools 创建 + asset_tools = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem) + if asset_tools: + # 尝试通过 EditorAssetSubsystem 创建 + factory = unreal.InputActionFactory() + new_asset = asset_tools.create_asset(name, IA_PACKAGE, unreal.InputAction, factory) + if new_asset: + print(f"[InputSetup] 创建成功: {asset_path} — {description}") + return new_asset + + # 备用方案:使用 Python BPLib 或直接构造 + print(f"[InputSetup] 尝试备用方案创建: {name}") + factory = unreal.InputActionFactory() + new_asset = unreal.AssetToolsHelpers.get_asset_tools().create_asset( + name, IA_PACKAGE[5:], # 去掉 /Game/ 前缀 + unreal.InputAction, + factory + ) + if new_asset: + print(f"[InputSetup] 备用方案创建成功: {asset_path}") + return new_asset + + print(f"[InputSetup] ERROR: 无法创建 {name}") + return None + + +def add_key_mapping_to_imc(imc: unreal.InputMappingContext, ia: unreal.InputAction, key: unreal.Key): + """向 IMC 添加按键映射""" + mapping = unreal.InputMapping() + mapping.set_editor_property("action", ia) + + key_mapping = unreal.KeyMapping() + key_mapping.set_editor_property("key", key) + key_mapping.set_editor_property("scale", 1.0) + + # 设置触发器 + # key_mapping 的 triggers 由 IMC 内部管理 + + # 使用 IMC 的 API 添加映射 + try: + # UE5.7: 使用 mappings 属性 + existing_mappings = imc.get_editor_property("mappings") + new_mapping = unreal.InputMapping() + new_mapping.set_editor_property("action", ia) + + km = unreal.KeyMapping() + km.set_editor_property("key", key) + km.set_editor_property("scale", 1.0) + + # 设置 triggers + triggers = [unreal.InputTriggerDown()] + km.set_editor_property("triggers", triggers) + new_mapping.set_editor_property("key_mappings", [km]) + + existing_mappings.append(new_mapping) + imc.set_editor_property("mappings", existing_mappings) + return True + except Exception as e: + print(f"[InputSetup] 添加映射失败: {e}") + return False + + +def main(): + print("=" * 50) + print("[InputSetup] v0.1 角色控制 Input 资产创建脚本") + print(f"[InputSetup] 项目: {PROJECT_ROOT}") + print("=" * 50) + + # ---- Step 1: 创建 InputAction 资产 ---- + print("\n[InputSetup] Step 1: 创建 InputAction 资产") + created_ias = {} + for name, config in NEW_INPUT_ACTIONS.items(): + ia = create_input_action(name, config["description"]) + if ia: + created_ias[name] = ia + else: + print(f"[InputSetup] WARNING: {name} 创建失败,后续 IMC 映射将跳过此 IA") + + if not created_ias: + print("[InputSetup] ERROR: 没有成功创建任何 IA,终止") + return False + + # ---- Step 2: 保存新创建的 IA 资产 ---- + print("\n[InputSetup] Step 2: 保存新创建的资产") + for name, ia in created_ias.items(): + asset_path = f"{IA_PACKAGE}/{name}" + unreal.EditorAssetLibrary.save_asset(asset_path) + print(f"[InputSetup] 已保存: {asset_path}") + + # ---- Step 3: 更新 IMC_Default ---- + print(f"\n[InputSetup] Step 3: 更新 IMC: {IMC_PATH}") + + if not asset_exists(IMC_PATH): + print(f"[InputSetup] ERROR: IMC 不存在: {IMC_PATH}") + return False + + imc = unreal.load_asset(IMC_PATH) + if not imc: + print(f"[InputSetup] ERROR: 无法加载 IMC: {IMC_PATH}") + return False + + # 读取当前映射数量 + try: + current_mappings = imc.get_editor_property("mappings") + print(f"[InputSetup] 当前 IMC 映射数: {len(current_mappings)}") + except Exception as e: + print(f"[InputSetup] WARNING: 无法读取映射: {e}") + current_mappings = [] + + # 添加新映射 + for name, config in NEW_INPUT_ACTIONS.items(): + if name not in created_ias: + continue + ia = created_ias[name] + key = config["key"] + success = add_key_mapping_to_imc(imc, ia, key) + if success: + print(f"[InputSetup] 已添加映射: {name} → {key}") + else: + print(f"[InputSetup] WARNING: 映射添加失败: {name}") + + # ---- Step 4: 保存 IMC ---- + print("\n[InputSetup] Step 4: 保存 IMC") + unreal.EditorAssetLibrary.save_asset(IMC_PATH) + print(f"[InputSetup] 已保存: {IMC_PATH}") + + # ---- Step 5: 验证 ---- + print("\n[InputSetup] Step 5: 验证") + try: + updated_mappings = imc.get_editor_property("mappings") + print(f"[InputSetup] 更新后 IMC 映射数: {len(updated_mappings)}") + except: + pass + + # 验证 IA 资产 + for name in NEW_INPUT_ACTIONS: + asset_path = f"{IA_PACKAGE}/{name}" + exists = asset_exists(asset_path) + print(f"[InputSetup] {name}: {'✅ 存在' if exists else '❌ 不存在'}") + + print("\n" + "=" * 50) + print("[InputSetup] 完成!请在 UE5 编辑器中验证。") + print("[InputSetup] 注意:需要在 BP_ChildHoodCharacter 中将新增 IA 属性指向对应资产") + print("=" * 50) + + return True + + +if __name__ == "__main__": + success = main() + if not success: + sys.exit(1) diff --git a/tools/world-sketcher/README.md b/tools/world-sketcher/README.md index f25957e..a7b1408 100644 --- a/tools/world-sketcher/README.md +++ b/tools/world-sketcher/README.md @@ -3,17 +3,66 @@ > 在场景概念与关卡执行案之间,提供可视化布局沟通工具。 > 用颜色画笔绘制世界布局 → AI 解读 → 双方确认 → 产出执行案。 +--- + ## 快速开始 -1. 双击 `sketch.html` 在浏览器中打开(推荐 Chrome / Edge) -2. 左侧设置场景名称和世界尺寸(米) -3. 选择画笔,在画布上绘制 -4. **双击** 已绘制区域 → 弹出命名对话框: +### 方式一:本地服务器(推荐) + +保存/加载直接读写 `output/` 目录,无需手动下载。 + +1. 双击 `start.bat` +2. 浏览器自动打开 `http://localhost:8765/sketch.html` +3. 开始绘制,点"保存"直接存入 `output/` + +> 需要 Python 3。关闭终端窗口即可停止服务器。 + +### 方式二:直接打开 HTML + +双击 `sketch.html` 在浏览器中打开(推荐 Chrome / Edge)。 + +- 保存时通过浏览器下载 ZIP 文件 +- 加载时通过浏览器文件选择器选择 ZIP + +> **注意**:直接打开 HTML 时加载独立的 `.json` 文件**不会恢复画布位图**——位图数据存储在 ZIP 包内的 PNG 文件中,必须加载 `.zip` 文件才能完整恢复。 + +--- + +## 文件结构 + +``` +world-sketcher/ +├── start.bat ← 启动脚本(双击运行) +├── server.py ← 本地保存/加载服务器 +├── sketch.html ← 主工具(单文件,浏览器运行) +├── README.md +└── output/ ← 保存/加载目录 + ├── 迈皋桥-scene.zip ← 完整场景包(JSON + 所有 PNG) + ├── 迈皋桥-scene.json ← 结构化数据(不含位图) + ├── 迈皋桥-L1-drawing.png # L1 画布像素数据 + ├── 迈皋桥-L1-overview-preview.png # L1 合并预览 + └── 迈皋桥-L2-*-drawing.png # L2 各区域画布数据 +``` + +--- + +## 基本操作 + +1. 左侧设置**场景名称**和**世界尺寸**(米) +2. 选择画笔,在画布上绘制 +3. **双击** 已绘制区域 → 弹出命名对话框: - 输入名称(如"家"、"小卖部") - - 设置**高度**(建筑/区域/不可通行类型时显示,默认 3m) - - 勾选**连通到**其他已命名区域(建立区域间连接关系) + - **L1 区域**:设置**地形标高**(米,可为负值表示洼地) + - **L2 建筑**:设置**建筑高度**(米) + - 勾选**连通到**其他已命名区域(建立连接关系) - 勾选「可进入」→ 创建下级详图(L1 → L2) -5. 保存 ZIP(含 PNG + JSON) +4. 保存 → 存入 `output/`(服务器模式)或浏览器下载(离线模式) + +### 画布标签 + +- **L1 层**:显示区域名称 + 地形标高(如 `棚户区 (标高2m)`) +- **L2 层**:显示建筑名称 + 建筑高度(如 `棚户1 (3m)`) +- 标高为 0 时不显示高度 --- @@ -30,36 +79,30 @@ - L1 画布上,双击已命名的区域并勾选「可进入」 - 左侧元素列表中点击 **"进入 →"** - 面包屑导航点击 **"场景总览"** 返回上级 +- 返回 L1 时,L2 的绘制内容会自动合成回 L1 对应区域 --- ## 画笔图例 -| 画笔 | 颜色 | 用途 | 对应 UE 材质 | 默认高度 | -|------|------|------|-------------|---------| -| 道路 Road | 灰 `#909090` | 道路、马路、小路 | MI_DevGray | — | -| 建筑 Building | 橙 `#E07020` | 建筑物 | MI_DevOrange | 3m | -| 水域 Water | 天蓝 `#40A0D0` | 河流、水塘、水体 | MI_DevSkyBlue | — | -| 植被 Greenery | 绿 `#40B040` | 树木、草地、绿化带 | MI_DevGreen | 2m | -| 不可通行 Blocked | 棕 `#A07040` | 山体、围墙、不可达区域 | MI_DevBrown | 3m | -| 区域 Zone | 红 `#D04040` | 区域边界、地块划分 | MI_DevRed | 3m | -| 橡皮擦 Eraser | — | 擦除 | — | — | - -**L1 层**主要用:区域、道路、水域 -**L2 层**主要用:建筑、道路、植被、水域、不可通行 +| 画笔 | 颜色 | 用途 | 对应 UE 材质 | 可用层级 | 高度含义 | +|------|------|------|-------------|---------|---------| +| 道路 Road | 灰 `#909090` | 道路、马路、小路 | MI_DevGray | L1/L2 | — | +| 建筑 Building | 橙 `#E07020` | 建筑物 | MI_DevOrange | L2 | 建筑高度 | +| 水域 Water | 天蓝 `#40A0D0` | 河流、水塘、水体 | MI_DevSkyBlue | L1/L2 | — | +| 植被 Greenery | 绿 `#40B040` | 树木、草地、绿化带 | MI_DevGreen | L2 | 2m | +| 不可通行 Blocked | 棕 `#A07040` | 山体、围墙、不可达区域 | MI_DevBrown | L1/L2 | 3m | +| 区域 Zone | 红 `#D04040` | 区域边界、地块划分 | MI_DevRed | L1 | 地形标高 | +| 橡皮擦 Eraser | — | 擦除 | — | L1/L2 | — | --- -## 绘图工具 - -| 工具 | 快捷键 | 说明 | -|------|--------|------| -| 画笔 | P | 自由绘制,按住鼠标拖动 | -| 矩形 | R | 绘制矩形区域,拖动定义两角 | -| 橡皮擦 | E | 擦除画布内容 | +## 快捷键 | 操作 | 快捷键 | |------|--------| +| 画笔工具 | P | +| 橡皮擦工具 | E | | 撤销 | Ctrl+Z | | 重做 | Ctrl+Y | | 网格开关 | G | @@ -74,57 +117,87 @@ - **单位**:厘米(与 UE5 一致,1 uu = 1 cm) - 画布顶部 = 世界最大 Y,画布底部 = 世界 Y=0 -状态栏实时显示鼠标位置的世界坐标。 +状态栏实时显示鼠标位置的像素坐标和世界坐标。 --- -## 输出文件 - -保存全部时生成: - -``` -<场景名>-L1-overview.png # L1 画布截图 -<场景名>-scene.json # 完整场景数据(所有层级) -``` +## 输出格式 ### scene.json 结构 ```json { - "scene_name": "城郊小街", + "scene_name": "迈皋桥", "version": "v02", + "created": "2026-06-09T16:46:32", "origin": "SW corner, X+ East, Y+ North", - "brush_legend": { "色值": { "type": "类型", "mi_material": "MI_Dev*" } }, + "brush_legend": { + "#D04040": { "type": "zone", "label": "区域 Zone", "mi_material": "MI_DevRed" } + }, "levels": { "L1": { "world_size_m": [500, 400], "canvas_size_px": [1000, 800], "scale_m_per_px": 0.5, "elements": [ - { "id": "el_001", "type": "zone", "name": "家", "height_m": 3, - "connections": ["el_003"], "has_detail": false }, - { "id": "el_002", "type": "zone", "name": "小卖部", "height_m": 3, - "connections": ["el_003"], "has_detail": false }, - { "id": "el_003", "type": "zone", "name": "主街道", "height_m": 3, - "connections": ["el_001", "el_002", "el_004"], "has_detail": true, "detail_ref": "L2-el_003" } + { + "id": "el_001", "type": "zone", "name": "棚户区", + "height_m": 2, "connections": ["el_002"], + "has_detail": true, "detail_ref": "L2-el_001" + } ], "regions": [ - { "id": "r_001", "type": "zone", "name": "家", - "bounds_world_m": [5.0, 5.0, 12.5, 15.0], "area_px": 15000 } - ] + { + "id": "r_001", "type": "zone", "name": "棚户区", + "bounds_world_m": [5.0, 5.0, 120.0, 95.0], + "area_px": 15000 + } + ], + "image_file": "迈皋桥-L1-drawing.png", + "l1_base_file": null }, - "L2-el_003": { + "L2-el_001": { "world_size_m": [80, 60], "elements": [ - { "id": "el_yyy", "type": "building", "name": "家", "height_m": 3, "connections": [], "has_detail": false } + { "id": "el_010", "type": "building", "name": "棚户1", "height_m": 3 } ], + "image_file": "迈皋桥-L2-el_001-drawing.png", + "l1_base_file": "迈皋桥-L2-el_001-l1base.png", "parent_level": "L1", - "parent_element": "el_003" + "parent_element": "el_001" } } } ``` +### 高度字段语义 + +| 层级 | 元素类型 | `height_m` 含义 | 用途 | +|------|---------|----------------|------| +| L1 | zone(区域) | **地形标高** | 地面海拔相对高度,0=基准面,负值=洼地 | +| L2 | building(建筑) | **建筑高度** | 建筑物垂直高度 | +| L1/L2 | blocked(不可通行) | **障碍高度** | 围墙/山体等垂直尺度 | + +--- + +## 本地服务器 + +`server.py` 提供三个 HTTP 端点,供 `sketch.html` 直接读写 `output/` 目录: + +| 端点 | 方法 | 说明 | +|------|------|------| +| `/list` | GET | 列出 `output/` 下所有 `.json` / `.zip` 文件 | +| `/load/` | GET | 从 `output/` 读取指定文件 | +| `/save` | POST (multipart) | 接收文件并保存到 `output/` | + +服务器运行时,sketch.html 自动检测并使用服务器模式;未运行时自动回退到浏览器下载/文件选择。 + +手动启动: +```bash +python server.py # 默认端口 8765 +python server.py 9000 # 自定义端口 +``` + --- ## 工作流集成 @@ -145,20 +218,21 @@ AI 布局解读文档 (场景名-layout-vXX.md) ### AI 解读流程 -1. 用户在工具中画好布局,命名区域,设置高度和连通关系,保存 ZIP -2. AI 读取 JSON 布局数据(elements + regions + connections + height_m) +1. 用户在工具中画好布局,命名区域,设置标高/高度和连通关系,保存 +2. AI 读取 JSON 中的 elements + regions + connections + height_m 3. AI 交叉参考场景设计案的氛围描述,将 JSON 数据与场景概念建立映射 -4. AI 产出结构化布局解读 Markdown(区域表、连通关系、高度、待确认问题) +4. AI 产出结构化布局解读 Markdown(区域表、连通关系、标高、待确认问题) 5. 用户确认后,策划基于解读文档产出关卡执行案 -> **说明**:PNG 文件保存在 ZIP 中用于画布的保存/加载,AI 分析以 JSON 结构化数据为准,不依赖 PNG 像素分析。 +> PNG 文件用于画布的保存/加载和人工预览。AI 分析以 JSON 结构化数据为准,不依赖 PNG 像素分析。 --- ## 设计原则 -- **强制使用预定义画笔**:无自由拾色器,保证每个像素都映射到已知地形类型 -- **结构化数据优先**:高度和连通关系由用户直接输入,AI 无需从图像推断 +- **预定义画笔**:无自由拾色器,保证每个像素都映射到已知地形类型 +- **结构化数据优先**:标高、高度和连通关系由用户直接输入,AI 无需从图像推断 - **层次化设计**:L1 定大局,L2 定细节,L3 预留 - **人机共读**:PNG 供人视觉确认,JSON 供 AI 和程序化解析 -- **与 Level Builder 一致**:画笔颜色直接映射 MI_Dev* 标识色材质 +- **材质一致**:画笔颜色直接映射 UE5 的 MI_Dev* 标识色材质 +- **离线优先**:单 HTML 文件即可使用,服务器仅为便利增强 diff --git a/tools/world-sketcher/server.py b/tools/world-sketcher/server.py new file mode 100644 index 0000000..1bafa43 --- /dev/null +++ b/tools/world-sketcher/server.py @@ -0,0 +1,121 @@ +""" +World Sketcher 本地保存服务器 +用法: python server.py [port] +然后浏览器打开 http://localhost:/sketch.html +""" + +import http.server, json, os, sys, urllib.parse +from datetime import datetime + +PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8765 +SERVE_DIR = os.path.dirname(os.path.abspath(__file__)) +OUTPUT_DIR = os.path.join(SERVE_DIR, "output") +os.makedirs(OUTPUT_DIR, exist_ok=True) + + +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=SERVE_DIR, **kwargs) + + # ── POST /save ── 保存文件到 output/ + def do_POST(self): + if self.path != "/save": + self.send_error(404); return + + ctype = self.headers.get("Content-Type", "") + if ctype.startswith("multipart/form-data"): + boundary = ctype.split("boundary=")[-1].encode() + data = self.rfile.read(int(self.headers["Content-Length"])) + + # 简易 multipart 解析 + parts = data.split(b"--" + boundary) + saved = [] + for part in parts: + if not part or part.strip() in (b"", b"--\r\n", b"--"): + continue + header_end = part.find(b"\r\n\r\n") + if header_end < 0: + continue + header = part[:header_end].decode("utf-8", errors="replace") + body = part[header_end + 4:] + if body.endswith(b"\r\n"): + body = body[:-2] + + # 提取文件名 + fname = None + for seg in header.split(";"): + seg = seg.strip() + if seg.startswith("filename="): + fname = seg.split("=", 1)[1].strip('"') + break + if not fname: + continue + + # 安全:只取文件名,禁止路径遍历 + fname = os.path.basename(fname) + fpath = os.path.join(OUTPUT_DIR, fname) + with open(fpath, "wb") as f: + f.write(body) + saved.append(fname) + + resp = json.dumps({"ok": True, "saved": saved}, ensure_ascii=False) + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(resp.encode("utf-8")) + print(f" [{datetime.now():%H:%M:%S}] Saved: {', '.join(saved)}") + else: + self.send_error(400, "Expected multipart/form-data") + + # ── GET 路由 ── + def do_GET(self): + # /list → 列出 output/ 下可用文件 + if self.path == "/list": + files = sorted( + f for f in os.listdir(OUTPUT_DIR) + if f.endswith((".json", ".zip")) + ) + resp = json.dumps({"files": files}, ensure_ascii=False) + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + self.wfile.write(resp.encode("utf-8")) + return + + # /load/ → 从 output/ 读取文件 + if self.path.startswith("/load/"): + fname = os.path.basename(urllib.parse.unquote(self.path[6:], encoding="utf-8")) + fpath = os.path.join(OUTPUT_DIR, fname) + if not os.path.isfile(fpath): + self.send_error(404); return + self.send_response(200) + if fname.endswith(".zip"): + self.send_header("Content-Type", "application/zip") + elif fname.endswith(".json"): + self.send_header("Content-Type", "application/json") + self.end_headers() + with open(fpath, "rb") as f: + self.wfile.write(f.read()) + return + + # 其他 → 静态文件(sketch.html 等) + super().do_GET() + + def log_message(self, format, *args): + # 只显示非静态资源的请求日志 + if "/save" in str(args) or "/list" in str(args) or "/load/" in str(args): + super().log_message(format, *args) + + +if __name__ == "__main__": + with http.server.HTTPServer(("", PORT), Handler) as httpd: + url = f"http://localhost:{PORT}/sketch.html" + print(f"World Sketcher server running") + print(f" URL: {url}") + print(f" Output: {OUTPUT_DIR}") + print(f" Press Ctrl+C to stop.\n") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.") diff --git a/tools/world-sketcher/sketch.html b/tools/world-sketcher/sketch.html index 8c59c37..368b5ba 100644 --- a/tools/world-sketcher/sketch.html +++ b/tools/world-sketcher/sketch.html @@ -147,7 +147,7 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
- +
@@ -174,8 +174,8 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se