diff --git a/.workbuddy/_proc.txt b/.workbuddy/_proc.txt new file mode 100644 index 0000000..e0ef87b --- /dev/null +++ b/.workbuddy/_proc.txt @@ -0,0 +1,4 @@ +pid=10416 name=Godot_v4.7.2-stable_mono_win64 title='' responding=True +pid=12060 name=Godot_v4.7.2-stable_mono_win64 title='main.tscn - agame - Godot Engine' responding=True +pid=26164 name=godot-ai title='' responding=True +pid=27664 name=godot-ai title='' responding=True diff --git a/.workbuddy/memory/2026-09-21.md b/.workbuddy/memory/2026-09-21.md new file mode 100644 index 0000000..bf9f061 --- /dev/null +++ b/.workbuddy/memory/2026-09-21.md @@ -0,0 +1,43 @@ +# 2026-09-21 + +## 接入 godot-ai MCP(已完成并验证) + +- 把 Claude Desktop 的 `godot-ai` MCP 迁入 WorkBuddy 配置 `C:\Users\DHXY\.workbuddy\mcp.json`,与原有 `unityMCP` 并存。 +- 启动方式:`uvx.exe` + `--isolated --no-config ... --from godot-ai==4.1.0 godot-ai attach --port 8001 --ws-port 8002`。 + - 去掉了原配置中 `pythonw.exe` 包一层隐藏控制台的写法,直接走 uvx stdio。 +- 环境核实:`uvx.exe` 存在;Godot v4.7.2 mono 运行中,8001/8002 均 LISTEN;`addons/godot_ai/plugin.cfg` 版本 4.1.0 与服务端 pin 版本一致。 +- **已信任并实测通过**:`project_manage(op="settings_get", key="application/config/name")` → `agame`。 + +## 经验:MCP 信任入口位置 + +- 「自定义连接器 / 信任 MCP」不在侧边栏连接器列表里,而在 **「连接器」页顶栏 →「MCP 服务管理」弹窗 →「我的 MCP」列表**,每个服务右侧是启用开关(绿色圆点=已连接)。 +- 相关 i18n key(从 app.asar 反查得到):`connectorPanel.customConnector`=自定义连接器、`unifiedMarket.customConnector`=自定义连接器(市场页顶部「+」下拉里)。 + +## 本机环境补充(重要) + +- `.workbuddy/memory` 下的文件在本会话中被外部清理过一次,写入后需复核是否仍在。 +- Bash 工具完全不可用(`mkdir`/`dirname` 都不存在),PowerShell 可用但需把输出写文件再 Read;Glob/Grep 对 `.workbuddy` 这类点目录不可靠,用 Read 直接读或 PowerShell 列目录。 +- PowerShell 里 `Add-Type` 被安全策略拦截,无法用 .NET API 激活窗口。 + +## 新建:第三人称控制器(胶囊体占位) + +- 文件:`res://player/player.gd`、`res://player/player.tscn`、`res://world/main.tscn`(测试场地,已设为 main scene)。 +- 结构:Player(CharacterBody3D,不旋转) / Collision + Body(Node3D,视觉层朝移动方向转) / Mesh + CameraRig / SpringArm / Camera。 + - **关键教训**:相机臂不要挂在会旋转的本体下,否则按方向键会产生「相机跟着转」的螺旋轨迹(实测偏航 90°、直行无横向位移)。 +- 输入动作(已写入 project.godot):move_forward/back/left/right(WASD+方向键)、jump(Space)、sprint(Shift)。 +- 参数:走 5.0 / 跑 8.5 m/s,加速度 45(地面)/12(空中),跳跃初速 5.0(实测最高点 1.318 m)。 + +## 本机/工程改造记录 + +- `autoload/steamworks.gd`:Steam 初始化失败时原来直接 `get_tree().quit()`,导致没开 Steam 客户端时游戏秒退;已加 `OS.has_feature("editor")` 豁免(编辑器内返回不退出,发布版行为不变)。 + +## godot-ai MCP 使用要点(踩坑记录) + +1. **路径是相对「当前编辑场景根」**,且要带根节点名,如 `/Player/CameraRig/SpringArm`;`parent_path: ""` 表示当前场景根。切场景后所有路径的含义会变,写操作前先确认 `editor_state.current_scene`。 +2. **`is_action_just_pressed()` 在 `_physics_process` 里对调试器注入的输入无效**(注入发生在渲染帧,物理帧号已 +1)。要么用 `_unhandled_input` 收事件 + 输入缓冲,要么只能读 `is_action_pressed()`。 +3. `game_eval` 里 `var x := Engine.get_main_loop().current_scene` 会编译不过(类型推断),要 `var tree := Engine.get_main_loop() as SceneTree` 并且给节点变量显式类型。 +4. `game_eval` 里的语法错误会把游戏卡进调试器断点(`game_status.status == "break"`),此后工具全部失效,必须 `project_manage(op="stop")` 后重启。 +5. 布尔属性写入会报 `WRONG_TYPE: Cannot write String to a bool property`(`Camera3D.current` 试过),改用其他方式或接受默认行为(只有一台相机时 Godot 会自动启用)。 +6. `SpringArm3D` 只在运行时摆放子节点;编辑器里预览要在场景里把相机 position.z 写成 spring_length 的值(已写入 player.tscn = 4.0)。 +7. 验证玩法用 `game_manage(op="input_sequence")`;涉及 `just_pressed` 的场景改用 `game_eval` + `Input.parse_input_event` + `await tree.physics_frame` 循环采样。 + diff --git a/autoload/steamworks.gd b/autoload/steamworks.gd index 7a4a4ec..8e36fe4 100644 --- a/autoload/steamworks.gd +++ b/autoload/steamworks.gd @@ -14,5 +14,9 @@ func initialize_steam() -> void: print("Did Steam initialize?: %s" % initialize_response) if initialize_response['status'] > Steam.STEAM_API_INIT_RESULT_OK: + print("Failed to initialize Steam: %s" % initialize_response) + if OS.has_feature("editor"): + # 编辑器内调试时 Steam 未必在运行,不退出,方便单独调试玩法 + return print("Failed to initialize Steam, shutting down: %s" % initialize_response) get_tree().quit() diff --git a/player/player.gd b/player/player.gd new file mode 100644 index 0000000..d9bec9d --- /dev/null +++ b/player/player.gd @@ -0,0 +1,117 @@ +extends CharacterBody3D +## 第三人称控制器(胶囊体占位) +## +## 场景结构: +## Player (CharacterBody3D) ← 只负责移动与碰撞,自身不旋转 +## ├─ Collision (CollisionShape3D, CapsuleShape3D) +## ├─ Body (Node3D) ← 视觉层,朝移动方向转身;换角色模型时挂在它下面 +## │ └─ Mesh (MeshInstance3D) +## └─ CameraRig (Node3D) ← 承担偏航 yaw,独立于本体旋转 +## └─ SpringArm (SpringArm3D) ← 承担俯仰 pitch,自动避障 +## └─ Camera (Camera3D) +## +## 操作:WASD/方向键移动,鼠标转视角,Shift 疾跑,空格跳跃,Esc 释放/锁定鼠标 + +## 跳跃输入缓冲帧数(输入事件在渲染帧到达,物理帧读取,避免漏跳) +const JUMP_BUFFER_FRAMES: int = 6 + +@export_group("移动") +## 常规移动速度(米/秒) +@export var walk_speed: float = 5.0 +## 疾跑速度(米/秒) +@export var sprint_speed: float = 8.5 +## 地面加速度(米/秒²) +@export var acceleration: float = 45.0 +## 空中加速度(米/秒²) +@export var air_acceleration: float = 12.0 +## 起跳初速度(米/秒) +@export var jump_velocity: float = 5.0 +## 视觉层转身平滑速度 +@export var turn_speed: float = 10.0 + +@export_group("相机") +## 鼠标灵敏度(弧度/像素) +@export var mouse_sensitivity: float = 0.0025 +## 最低俯角(向下看) +@export var pitch_min_deg: float = -80.0 +## 最高仰角(向上看) +@export var pitch_max_deg: float = 45.0 + +@onready var body: Node3D = $Body +@onready var camera_rig: Node3D = $CameraRig +@onready var spring_arm: SpringArm3D = $CameraRig/SpringArm + +var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8)) +var _yaw: float = 0.0 +var _pitch: float = 0.0 +var _jump_buffer: int = 0 + + +func _ready() -> void: + _yaw = camera_rig.rotation.y + _pitch = clampf(spring_arm.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg)) + spring_arm.rotation.x = _pitch + set_mouse_captured(true) + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + _yaw -= event.relative.x * mouse_sensitivity + _pitch = clampf( + _pitch - event.relative.y * mouse_sensitivity, + deg_to_rad(pitch_min_deg), + deg_to_rad(pitch_max_deg) + ) + camera_rig.rotation.y = _yaw + spring_arm.rotation.x = _pitch + elif event.is_action_pressed("jump"): + _jump_buffer = JUMP_BUFFER_FRAMES + elif event.is_action_pressed("ui_cancel"): + set_mouse_captured(Input.mouse_mode != Input.MOUSE_MODE_CAPTURED) + elif event is InputEventMouseButton and event.pressed and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: + set_mouse_captured(true) + + +func set_mouse_captured(captured: bool) -> void: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE + + +func _physics_process(delta: float) -> void: + # --- 重力 --- + if is_on_floor(): + if velocity.y < 0.0: + velocity.y = 0.0 + else: + velocity.y -= _gravity * delta + + # --- 跳跃(读取输入缓冲)--- + if _jump_buffer > 0: + _jump_buffer -= 1 + if is_on_floor(): + velocity.y = jump_velocity + _jump_buffer = 0 + + # --- 相对相机的水平输入 --- + var input_dir := Input.get_vector("move_left", "move_right", "move_back", "move_forward") + var cam_basis := camera_rig.global_transform.basis + var wish := cam_basis.x * input_dir.x + (-cam_basis.z) * input_dir.y + wish.y = 0.0 + + var wants_move := wish.length() > 0.001 + if wants_move: + wish = wish.normalized() + # 只转视觉层,不动本体,也不影响相机(Godot 前进方向为 -Z) + var target_yaw := atan2(-wish.x, -wish.z) + body.rotation.y = lerp_angle(body.rotation.y, target_yaw, clampf(turn_speed * delta, 0.0, 1.0)) + + var speed := sprint_speed if Input.is_action_pressed("sprint") else walk_speed + if not wants_move: + speed = 0.0 + var accel := acceleration if is_on_floor() else air_acceleration + + var horizontal := Vector2(velocity.x, velocity.z) + horizontal = horizontal.move_toward(Vector2(wish.x, wish.z) * speed, accel * delta) + velocity.x = horizontal.x + velocity.z = horizontal.y + + move_and_slide() diff --git a/player/player.gd.uid b/player/player.gd.uid new file mode 100644 index 0000000..d04549c --- /dev/null +++ b/player/player.gd.uid @@ -0,0 +1 @@ +uid://f5ptp3is4agf diff --git a/player/player.tscn b/player/player.tscn new file mode 100644 index 0000000..40adcfe --- /dev/null +++ b/player/player.tscn @@ -0,0 +1,41 @@ +[gd_scene format=3 uid="uid://bekuuw153mv03"] + +[ext_resource type="Script" uid="uid://f5ptp3is4agf" path="res://player/player.gd" id="1_oul6g"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_rkbax"] +radius = 0.4 +height = 1.8 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_g1dw6"] +radius = 0.4 +height = 1.8 +radial_segments = 24 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_yw30f"] +albedo_color = Color(0.23137255, 0.50980395, 0.9647059, 1) +roughness = 0.6 + +[node name="Player" type="CharacterBody3D" unique_id=869687860] +script = ExtResource("1_oul6g") + +[node name="Collision" type="CollisionShape3D" parent="." unique_id=215132582] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0) +shape = SubResource("CapsuleShape3D_rkbax") + +[node name="CameraRig" type="Node3D" parent="." unique_id=1774966304] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.45, 0) + +[node name="SpringArm" type="SpringArm3D" parent="CameraRig" unique_id=917938711] +transform = Transform3D(1, 0, 0, 0, 0.9659257, 0.25881964, 0, -0.25881964, 0.9659257, 0, 0, 0) +spring_length = 4.0 + +[node name="Camera" type="Camera3D" parent="CameraRig/SpringArm" unique_id=52294557] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 4) +fov = 70.0 + +[node name="Body" type="Node3D" parent="." unique_id=394283421] + +[node name="Mesh" type="MeshInstance3D" parent="Body" unique_id=1230094944] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0) +mesh = SubResource("CapsuleMesh_g1dw6") +surface_material_override/0 = SubResource("StandardMaterial3D_yw30f") diff --git a/project.godot b/project.godot index 84c0789..c4f5a41 100644 --- a/project.godot +++ b/project.godot @@ -11,6 +11,7 @@ config_version=5 [application] config/name="agame" +run/main_scene="res://world/main.tscn" config/features=PackedStringArray("4.7", "Forward Plus") config/icon="res://icon.svg" @@ -36,6 +37,43 @@ project/assembly_name="agame" enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg") +[input] + +move_forward={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_back={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +jump={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +sprint={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194325,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} + [physics] 3d/physics_engine="Jolt Physics" diff --git a/world/main.tscn b/world/main.tscn new file mode 100644 index 0000000..06d1fe9 --- /dev/null +++ b/world/main.tscn @@ -0,0 +1,50 @@ +[gd_scene format=3 uid="uid://bp8l1uwgshc86"] + +[ext_resource type="PackedScene" uid="uid://bekuuw153mv03" path="res://player/player.tscn" id="1_gjxf3"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_hyim0"] +size = Vector3(40, 0.5, 40) + +[sub_resource type="PlaneMesh" id="PlaneMesh_4f7fj"] +size = Vector2(40, 40) +subdivide_width = 8 +subdivide_depth = 8 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_spvjk"] +albedo_color = Color(0.5529412, 0.5803922, 0.61960787, 1) +roughness = 0.9 + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_cmo1i"] +sky_top_color = Color(0.38, 0.45, 0.55, 1) +sky_horizon_color = Color(0.65, 0.67, 0.7, 1) +ground_bottom_color = Color(0.2, 0.17, 0.13, 1) +ground_horizon_color = Color(0.65, 0.67, 0.7, 1) + +[sub_resource type="Sky" id="Sky_i1eo7"] +sky_material = SubResource("ProceduralSkyMaterial_cmo1i") + +[sub_resource type="Environment" id="Environment_gu74f"] +background_mode = 2 +sky = SubResource("Sky_i1eo7") +ambient_light_source = 3 + +[node name="Main" type="Node3D" unique_id=58155864] + +[node name="Ground" type="StaticBody3D" parent="." unique_id=305091665] + +[node name="GroundShape" type="CollisionShape3D" parent="Ground" unique_id=8315700] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0) +shape = SubResource("BoxShape3D_hyim0") + +[node name="GroundMesh" type="MeshInstance3D" parent="Ground" unique_id=51344137] +mesh = SubResource("PlaneMesh_4f7fj") +surface_material_override/0 = SubResource("StandardMaterial3D_spvjk") + +[node name="Sun" type="DirectionalLight3D" parent="." unique_id=910400852] +transform = Transform3D(0.8660248, 0.3830344, -0.32138094, 0, 0.6427605, 0.76606715, 0.5000011, -0.66343313, 0.5566465, 0, 0, 0) + +[node name="Player" parent="." unique_id=869687860 instance=ExtResource("1_gjxf3")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0) + +[node name="Environment" type="WorldEnvironment" parent="." unique_id=794103784] +environment = SubResource("Environment_gu74f")