feat(val-blog): add 2026-04-30 dream journey post

This commit is contained in:
Chen Gu
2026-08-13 17:00:22 +08:00
committed by Chen Gu
parent 691f96e8a6
commit 81978704fc
941 changed files with 195468 additions and 755 deletions
+125
View File
@@ -0,0 +1,125 @@
extends Node2D
# 公寓管理器 - 处理房间、家具和交互
@onready var val: CharacterBody2D = $Val
@onready var rooms: Node2D = $Rooms
@onready var furniture: Node2D = $Furniture
signal room_changed(room_name: String)
signal furniture_clicked(furniture: Node)
var bridge: Node = null
func _ready():
# 连接输入事件
_connect_furniture_clicks()
_connect_room_detection()
# 获取 BridgeClient 引用(从父场景)
_find_bridge()
print("Apartment ready, furniture count: ", furniture.get_child_count())
func _find_bridge():
# 尝试多种路径找到 BridgeClient
bridge = get_node_or_null("../BridgeClient")
if not bridge:
bridge = get_node_or_null("/root/Main/BridgeClient")
if not bridge:
# 延迟查找
await get_tree().process_frame
bridge = get_node_or_null("../BridgeClient")
if bridge:
print("BridgeClient found")
else:
print("BridgeClient not found - running in standalone mode")
func _connect_furniture_clicks():
for item in furniture.get_children():
if item is Area2D:
item.input_event.connect(_on_furniture_clicked.bind(item))
print("Connected click for: ", item.get_meta("furniture_name", "未知"))
func _connect_room_detection():
for room in rooms.get_children():
if room is Area2D:
room.body_entered.connect(_on_room_entered.bind(room))
func _on_furniture_clicked(viewport: Node, event: InputEvent, shape_idx: int, furniture: Node):
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
var furniture_name = furniture.get_meta("furniture_name", "未知")
print("Clicked on furniture: ", furniture_name)
emit_signal("furniture_clicked", furniture)
# 让Val走过去交互
if val:
val.interact_with(furniture)
else:
print("Val not found!")
# 发送事件到Bridge
_send_to_bridge({
"type": "furniture_clicked",
"furniture": furniture_name,
"position": [furniture.global_position.x, furniture.global_position.y]
})
func _on_room_entered(body: Node2D, room: Area2D):
if body.name == "Val":
var room_name = room.get_meta("room_name", "未知")
print("Val entered room: ", room_name)
emit_signal("room_changed", room_name)
# 更新Val的当前房间
if val and "current_room" in val:
val.current_room = room_name
if val and "agent_state" in val:
val.agent_state["location"] = room_name
# 发送事件到Bridge
_send_to_bridge({
"type": "room_changed",
"room": room_name,
"agent_id": "val"
})
func get_furniture_list() -> Array:
var list = []
for item in furniture.get_children():
if item is Area2D:
list.append({
"name": item.get_meta("furniture_name", "未知"),
"interaction": item.get_meta("interaction", "use"),
"position": [item.global_position.x, item.global_position.y]
})
return list
func get_room_list() -> Array:
var list = []
for room in rooms.get_children():
if room is Area2D:
list.append(room.get_meta("room_name", "未知"))
return list
func _send_to_bridge(data: Dictionary):
if bridge and "send_command" in bridge:
bridge.send_command(data)
# 公共API:让Val移动到指定房间
func move_val_to_room(room_name: String):
for room in rooms.get_children():
if room is Area2D and room.get_meta("room_name", "") == room_name:
var target_pos = room.global_position + Vector2(0, 50)
if val:
val.move_to(target_pos)
return true
return false
# 公共API:获取指定家具
func get_furniture(furniture_name: String) -> Node:
for item in furniture.get_children():
if item is Area2D and item.get_meta("furniture_name", "") == furniture_name:
return item
return null
@@ -0,0 +1 @@
uid://cljtq5ip65le0
@@ -0,0 +1,65 @@
extends Node2D
const GRID_SIZE = 64
const ROOM_WIDTH = 10
const ROOM_HEIGHT = 8
const MAIN_CHANNEL_WIDTH = 2
var grid_cells: Array = []
var occupied_positions: Array = []
func _ready():
initialize_grid()
func initialize_grid():
grid_cells.resize(ROOM_WIDTH * ROOM_HEIGHT)
for y in range(ROOM_HEIGHT):
for x in range(ROOM_WIDTH):
var index = y * ROOM_WIDTH + x
grid_cells[index] = {
"position": Vector2(x * GRID_SIZE, y * GRID_SIZE),
"occupied": false,
"type": "empty"
}
# Reserve main channel (2 cells wide, can be horizontal or vertical, here we assume vertical left side)
if x == 0 and y < ROOM_HEIGHT - 2: # Keep 2 cells at bottom free
grid_cells[index]["occupied"] = true
grid_cells[index]["type"] = "channel"
func is_position_valid(pos: Vector2) -> bool:
var x = int(pos.x / GRID_SIZE)
var y = int(pos.y / GRID_SIZE)
if x < 0 or x >= ROOM_WIDTH or y < 0 or y >= ROOM_HEIGHT:
return false
var index = y * ROOM_WIDTH + x
return not grid_cells[index]["occupied"]
func get_random_valid_position() -> Vector2:
var attempts = 0
while attempts < 50:
var x = randi() % (ROOM_WIDTH - 1) # Avoid blocking channel
var y = randi() % ROOM_HEIGHT
var pos = Vector2(x * GRID_SIZE + GRID_SIZE / 2, y * GRID_SIZE + GRID_SIZE / 2)
if is_position_valid(pos):
return pos
attempts += 1
return Vector2.ZERO
func reserve_position(pos: Vector2, obj_type: String = "object"):
var x = int(pos.x / GRID_SIZE)
var y = int(pos.y / GRID_SIZE)
if x >= 0 and x < ROOM_WIDTH and y >= 0 and y < ROOM_HEIGHT:
var index = y * ROOM_WIDTH + x
if not grid_cells[index]["occupied"]:
grid_cells[index]["occupied"] = true
grid_cells[index]["type"] = obj_type
occupied_positions.append(pos)
func free_position(pos: Vector2):
var x = int(pos.x / GRID_SIZE)
var y = int(pos.y / GRID_SIZE)
if x >= 0 and x < ROOM_WIDTH and y >= 0 and y < ROOM_HEIGHT:
var index = y * ROOM_WIDTH + x
grid_cells[index]["occupied"] = false
grid_cells[index]["type"] = "empty"
occupied_positions.erase(pos)
@@ -0,0 +1 @@
uid://q1m7nq7xin5m
@@ -0,0 +1,152 @@
extends Node
# Bridge 客户端 - 连接 Godot 和 OpenClaw
signal action_received(action: Dictionary)
signal state_synced(state: Dictionary)
signal connected_to_bridge()
signal disconnected_from_bridge()
@export var bridge_url: String = "ws://127.0.0.1:8765"
@export var reconnect_interval: float = 5.0
@export var auto_connect: bool = false # 默认不自动连接
var socket: WebSocketPeer
var is_connected: bool = false
var reconnect_timer: float = 0.0
func _ready():
# 作为全局单例
name = "BridgeClient"
print("BridgeClient initialized, auto_connect = ", auto_connect)
if auto_connect:
_connect_to_bridge()
func _process(delta):
if socket:
_socket_poll()
elif reconnect_timer > 0:
reconnect_timer -= delta
if reconnect_timer <= 0:
_connect_to_bridge()
func _connect_to_bridge():
print("Connecting to OpenClaw Bridge at ", bridge_url)
socket = WebSocketPeer.new()
var err = socket.connect_to_url(bridge_url)
if err != OK:
print("Failed to connect: ", err)
_schedule_reconnect()
return
# 设置状态
is_connected = false
func _socket_poll():
socket.poll()
var state = socket.get_ready_state()
match state:
WebSocketPeer.STATE_CONNECTING:
pass # 正在连接
WebSocketPeer.STATE_OPEN:
if not is_connected:
is_connected = true
print("Connected to OpenClaw Bridge!")
emit_signal("connected_to_bridge")
_send_hello()
# 处理收到的消息
while socket.get_available_packets() > 0:
var packet = socket.get_packet()
var message_str = packet.get_string_from_utf8()
_parse_message(message_str)
WebSocketPeer.STATE_CLOSING:
pass # 正在关闭
WebSocketPeer.STATE_CLOSED:
if is_connected:
is_connected = false
print("Disconnected from Bridge")
emit_signal("disconnected_from_bridge")
socket = null
_schedule_reconnect()
func _schedule_reconnect():
reconnect_timer = reconnect_interval
print("Will reconnect in ", reconnect_interval, " seconds...")
func _send_hello():
send_command({
"type": "hello",
"client": "godot",
"agent_id": "val",
"timestamp": Time.get_unix_time_from_system()
})
func _parse_message(message_str: String):
var json = JSON.new()
var err = json.parse(message_str)
if err != OK:
print("Failed to parse JSON: ", message_str)
return
var message = json.get_data()
if typeof(message) != TYPE_DICTIONARY:
print("Invalid message format")
return
var msg_type = message.get("type", "")
match msg_type:
"action_sequence":
# 解析动作序列
var actions = message.get("actions", [])
for action in actions:
emit_signal("action_received", action)
"state_sync":
# 同步Agent状态
emit_signal("state_synced", message.get("state", {}))
"heartbeat":
# 响应心跳
send_command({"type": "heartbeat_ack", "timestamp": Time.get_unix_time_from_system()})
"error":
print("Bridge error: ", message.get("error", "Unknown error"))
_:
print("Unknown message type: ", msg_type)
func send_command(command: Dictionary) -> bool:
if not is_connected or not socket:
# 静默处理,不打印警告
return false
var json_str = JSON.stringify(command)
var err = socket.send_text(json_str)
if err != OK:
print("Failed to send command: ", err)
return false
return true
func request_state_sync():
send_command({
"type": "request_state_sync",
"agent_id": "val"
})
func close_connection():
if socket:
socket.close()
is_connected = false
@@ -0,0 +1 @@
uid://v3b2e7ifiw6v
+76
View File
@@ -0,0 +1,76 @@
extends Node2D
# 主场景管理器
@onready var apartment: Node2D = $Apartment
@onready var bridge: Node = $BridgeClient
@onready var command_input: LineEdit = $UI/Panel/VBoxContainer/CommandInput
@onready var send_button: Button = $UI/Panel/VBoxContainer/SendButton
func _ready():
# 连接UI事件
send_button.pressed.connect(_on_send_command)
command_input.text_submitted.connect(_on_command_submitted)
# 连接公寓事件
apartment.room_changed.connect(_on_room_changed)
apartment.furniture_clicked.connect(_on_furniture_clicked)
# 连接Bridge事件
if bridge:
bridge.connected_to_bridge.connect(_on_bridge_connected)
bridge.disconnected_from_bridge.connect(_on_bridge_disconnected)
print("AgentLiving started!")
print("Click on furniture to make Val interact with it!")
func _on_send_command():
var command = command_input.text.strip_edges()
if command.is_empty():
return
_send_command_to_val(command)
command_input.clear()
func _on_command_submitted(text: String):
_on_send_command()
func _send_command_to_val(command: String):
print("User command: ", command)
var val = apartment.get_node_or_null("Val")
if val:
val.receive_command(command)
# 同时发送到Bridge
if bridge and bridge.is_connected:
bridge.send_command({
"type": "user_command",
"command": command,
"timestamp": Time.get_unix_time_from_system()
})
func _on_room_changed(room_name: String):
print("Val entered: ", room_name)
func _on_furniture_clicked(furniture: Node):
print("Furniture clicked: ", furniture.get_meta("furniture_name", "未知"))
func _on_bridge_connected():
print("Bridge connected!")
# 请求初始状态同步
if bridge:
bridge.request_state_sync()
func _on_bridge_disconnected():
print("Bridge disconnected!")
func _input(event):
# 按ESC退出
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
get_tree().quit()
# 按R重新连接Bridge
if event is InputEventKey and event.pressed and event.keycode == KEY_R:
if bridge:
bridge._connect_to_bridge()
@@ -0,0 +1 @@
uid://wo22tl1oknt8
+229
View File
@@ -0,0 +1,229 @@
extends CharacterBody2D
class_name Val
# 移动速度
@export var speed: float = 100.0
# 节点引用
@onready var body_placeholder: ColorRect = $BodyPlaceholder
@onready var head_placeholder: ColorRect = $HeadPlaceholder
@onready var thought_bubble: Label = $ThoughtBubble
@onready var state_indicator: Label = $StateIndicator
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
@onready var interaction_timer: Timer = $InteractionTimer
@onready var think_timer: Timer = $ThinkTimer
# 状态
enum State { IDLE, WALK, INTERACT, THINK }
var current_state: State = State.IDLE
var current_room: String = "客厅"
var agent_state: Dictionary = {
"location": "客厅",
"energy": 80,
"mood": "开心",
"activity": "idle"
}
# 目标
var target_position: Vector2 = Vector2.ZERO
var target_furniture: Node = null
var bridge: Node = null
# 动画颜色
var normal_color = Color(0.36, 0.61, 0.84, 1) # 蓝色
var interact_color = Color(0.5, 0.8, 0.36, 1) # 绿色
var think_color = Color(0.8, 0.5, 0.36, 1) # 橙色
signal state_changed(new_state: String)
signal action_completed(action: String)
func _ready():
# 初始化
thought_bubble.hide()
update_state_display()
# 连接导航完成信号
nav_agent.navigation_finished.connect(_on_navigation_finished)
# 查找 BridgeClient
_find_bridge()
print("Val ready at position: ", global_position)
func _find_bridge():
# 尝试多种路径
bridge = get_node_or_null("../../BridgeClient")
if not bridge:
bridge = get_node_or_null("/root/Main/BridgeClient")
if not bridge:
await get_tree().process_frame
bridge = get_node_or_null("../../BridgeClient")
if bridge:
print("Val: BridgeClient found")
if bridge.has_signal("action_received"):
bridge.action_received.connect(_on_bridge_action)
if bridge.has_signal("state_synced"):
bridge.state_synced.connect(_on_state_synced)
else:
print("Val: BridgeClient not found - running in standalone mode")
func _physics_process(delta):
match current_state:
State.WALK:
_handle_walk(delta)
State.INTERACT:
_handle_interact()
State.THINK:
_handle_think()
_:
_handle_idle()
func _handle_walk(delta):
if nav_agent.is_navigation_finished():
current_state = State.IDLE
update_state_display()
emit_signal("action_completed", "walk")
return
var next_pos = nav_agent.get_next_path_position()
var direction = (next_pos - global_position).normalized()
velocity = direction * speed
# 翻转精灵
if direction.x < 0:
body_placeholder.scale.x = -1
head_placeholder.scale.x = -1
elif direction.x > 0:
body_placeholder.scale.x = 1
head_placeholder.scale.x = 1
# 行走时颜色闪烁
body_placeholder.color = normal_color.lerp(Color.WHITE, sin(Time.get_ticks_msec() * 0.01) * 0.1)
move_and_slide()
func _handle_idle():
velocity = Vector2.ZERO
body_placeholder.color = normal_color
head_placeholder.color = normal_color
state_indicator.text = "idle"
func _handle_interact():
velocity = Vector2.ZERO
body_placeholder.color = interact_color
head_placeholder.color = interact_color
state_indicator.text = "interact"
func _handle_think():
velocity = Vector2.ZERO
body_placeholder.color = think_color
head_placeholder.color = think_color
state_indicator.text = "think"
# 移动到指定位置
func move_to(pos: Vector2):
target_position = pos
nav_agent.target_position = pos
current_state = State.WALK
update_state_display()
emit_signal("state_changed", "walk")
print("Val moving to: ", pos)
# 移动到家具并交互
func interact_with(furniture: Node):
target_furniture = furniture
var interact_pos = furniture.global_position + Vector2(0, 60)
print("Val will interact with: ", furniture.get_meta("furniture_name", "未知"), " at ", interact_pos)
move_to(interact_pos)
# 等待移动完成后再交互
await action_completed
_start_interaction(furniture)
func _start_interaction(furniture: Node):
current_state = State.INTERACT
update_state_display()
var furniture_name = furniture.get_meta("furniture_name", "物品")
var interaction = furniture.get_meta("interaction", "使用")
show_thought("正在" + interaction + furniture_name + "...")
# 发送交互事件到Bridge
_send_to_bridge({
"type": "furniture_interacted",
"furniture": furniture_name,
"interaction": interaction,
"position": [global_position.x, global_position.y]
})
# 交互持续2秒
interaction_timer.start(2.0)
await interaction_timer.timeout
current_state = State.IDLE
thought_bubble.hide()
update_state_display()
emit_signal("action_completed", "interact")
print("Val finished interacting with: ", furniture_name)
# 显示思考气泡
func show_thought(text: String, duration: float = 3.0):
thought_bubble.text = text
thought_bubble.show()
if duration > 0:
think_timer.start(duration)
await think_timer.timeout
thought_bubble.hide()
# 处理Bridge发来的动作
func _on_bridge_action(action: Dictionary):
match action.get("cmd", ""):
"walk_to":
move_to(Vector2(action.pos[0], action.pos[1]))
"show_thought":
show_thought(action.get("text", ""), action.get("duration", 3.0))
"interact":
var furniture_path = action.get("furniture_path", "")
if furniture_path:
var furniture = get_node_or_null(furniture_path)
if furniture:
interact_with(furniture)
"update_state":
_update_internal_state(action.get("key", ""), action.get("value"))
# 同步OpenClaw状态
func _on_state_synced(state: Dictionary):
agent_state = state
current_room = state.get("location", current_room)
update_state_display()
func _update_internal_state(key: String, value):
agent_state[key] = value
update_state_display()
func update_state_display():
state_indicator.text = State.keys()[current_state].to_lower()
func _on_navigation_finished():
current_state = State.IDLE
update_state_display()
print("Val navigation finished")
func _send_to_bridge(data: Dictionary):
if bridge and "send_command" in bridge:
bridge.send_command(data)
# 公共API:接收用户指令
func receive_command(command: String):
show_thought("思考中...", 0)
current_state = State.THINK
update_state_display()
_send_to_bridge({
"type": "user_command",
"command": command,
"current_state": agent_state
})
+1
View File
@@ -0,0 +1 @@
uid://buw2rqtgbwl0l
+59
View File
@@ -0,0 +1,59 @@
extends Node
enum State { IDLE, WALKING, INTERACTING, ANIMATING }
var current_state: State = State.IDLE
var target_position: Vector2 = Vector2.ZERO
var move_speed: float = 100.0
var is_moving: bool = false
func _ready():
pass
func _process(delta):
match current_state:
State.IDLE:
pass
State.WALKING:
walk_towards_target(delta)
State.INTERACTING:
pass
State.ANIMATING:
pass
func set_state(new_state: State):
current_state = new_state
match new_state:
State.IDLE:
pass
State.WALKING:
start_walking()
State.INTERACTING:
start_interacting()
State.ANIMATING:
start_animating()
func start_walking():
is_moving = true
func walk_towards_target(delta):
if not is_moving:
return
var direction = (target_position - position).normalized()
position += direction * move_speed * delta
if position.distance_to(target_position) < 5.0:
position = target_position
is_moving = false
set_state(State.IDLE)
func start_interacting():
# Trigger interaction animation or logic
pass
func start_animating():
# Handle any animations
pass
func move_to(pos: Vector2):
target_position = pos
set_state(State.WALKING)
@@ -0,0 +1 @@
uid://cei8m7rqa7a2r