extends Node3D
# Game settings
const GRID_SIZE = 3 # 3x3 grid
const TILE_WIDTH = 2.0 # 增加到2倍大小
const TILE_HEIGHT = 2.0 # 增加到2倍大小
const BOARD_WIDTH = TILE_WIDTH * GRID_SIZE
const BOARD_HEIGHT = TILE_HEIGHT * GRID_SIZE
const TIME_LIMIT = 60 # Time limit per level (seconds)
# Game state
var tiles = [] # Stores all puzzle pieces
var selected_tile = null # Currently selected tile
var is_solved = false
var current_level = 1 # Current level
var camera_angle = 0.0
var camera_target_angle = 0.0
# Preload all images
var level_textures = [
preload("res://puzzle_image.png"), # Level 1
preload("res://puzzle2_image.png"), # Level 2
preload("res://puzzle3_image.png"), # Level 3
preload("res://puzzle4_image.png"), # Level 4
preload("res://puzzle5_image.png"), # Level 5
preload("res://puzzle6_image.png"), # Level 6
preload("res://puzzle7_image.png") # Level 7
]
# 添加自定义图片支持
var custom_level_textures = [] # 存储用户上传的图片
var custom_level_count = 0 # 自定义关卡计数
# Menu state
var in_menu = true
var menu_container = null
# Timer state
var time_left = TIME_LIMIT
var timer_running = false
var timer_label = null
func _ready():
# Create camera
var camera = Camera3D.new()
camera.position = Vector3(0, 0, 8) # 增加Z轴距离以便看到整个拼图板
camera.fov = 70
camera.current = true
add_child(camera)
# Create directional light
var light = DirectionalLight3D.new()
light.rotation_degrees = Vector3(-45, 45, 0)
add_child(light)
# 创建文件对话框(初始不可见)
var file_dialog = FileDialog.new()
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
file_dialog.filters = PackedStringArray(["*.png ; PNG Images", "*.jpg ; JPEG Images", "*.jpeg ; JPEG Images"])
file_dialog.size = Vector2(800, 600)
file_dialog.position = Vector2(100, 100)
file_dialog.title = "选择拼图图片 (PNG/JPG)"
file_dialog.file_selected.connect(_on_custom_image_selected)
file_dialog.visible = false
add_child(file_dialog)
file_dialog.name = "FileDialog" # 给个名字方便获取
# Show start menu
show_main_menu()
# Show main menu
func show_main_menu():
in_menu = true
timer_running = false
# Clear all existing UI and tiles
clear_ui_elements()
clear_tiles()
# Create menu container
menu_container = VBoxContainer.new()
menu_container.position = Vector2(200, 200)
menu_container.size = Vector2(400, 300)
menu_container.alignment = BoxContainer.ALIGNMENT_CENTER
menu_container.name = "MainMenu"
add_child(menu_container)
# Add game title
var title = Label.new()
title.text = "3D Puzzle Challenge"
title.add_theme_font_size_override("font_size", 48)
title.add_theme_color_override("font_color", Color(0.9, 0.9, 0.1))
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
menu_container.add_child(title)
# Add spacer
menu_container.add_child(Control.new())
# Add start button
var start_button = Button.new()
start_button.text = "开始游戏"
start_button.custom_minimum_size = Vector2(200, 60)
start_button.add_theme_font_size_override("font_size", 32)
start_button.pressed.connect(_on_start_button_pressed)
menu_container.add_child(start_button)
# Add spacer
menu_container.add_child(Control.new())
# Add game instructions
var instructions = Label.new()
instructions.text = "每关60秒时间\n点击拼图块进行交换\n使用方向键旋转视角"
instructions.add_theme_font_size_override("font_size", 20)
instructions.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
menu_container.add_child(instructions)
# Add spacer
menu_container.add_child(Control.new())
# Add custom image button
var custom_image_button = Button.new()
custom_image_button.text = "上传自定义图片"
custom_image_button.custom_minimum_size = Vector2(200, 50)
custom_image_button.add_theme_font_size_override("font_size", 28)
custom_image_button.pressed.connect(_on_custom_image_button_pressed)
menu_container.add_child(custom_image_button)
# Add spacer
menu_container.add_child(Control.new())
# Add quit button
var quit_button = Button.new()
quit_button.text = "退出游戏"
quit_button.custom_minimum_size = Vector2(200, 50)
quit_button.add_theme_font_size_override("font_size", 28)
quit_button.pressed.connect(_on_quit_button_pressed)
menu_container.add_child(quit_button)
# Start game button event
func _on_start_button_pressed():
start_level(1)
# 自定义图片按钮事件
func _on_custom_image_button_pressed():
var file_dialog = get_node("FileDialog")
file_dialog.popup_centered()
# 当用户选择图片文件时
func _on_custom_image_selected(path):
# 加载图片
var image = Image.new()
var error = image.load(path)
if error == OK:
# 创建纹理
var texture = ImageTexture.create_from_image(image)
# 添加到自定义关卡
custom_level_textures.append(texture)
custom_level_count += 1
# 启动自定义关卡
start_custom_level(texture)
else:
print("Failed to load image: ", path)
# 显示错误提示
show_hint("无法加载图片!请选择PNG或JPG格式")
# Quit game button event
func _on_quit_button_pressed():
get_tree().quit()
# Start new level
func start_level(level: int):
in_menu = false
timer_running = true
time_left = TIME_LIMIT
# Remove menu
if menu_container:
menu_container.queue_free()
# Reset game state
is_solved = false
selected_tile = null
current_level = level
camera_angle = 0.0
camera_target_angle = 0.0
print("Starting level: ", current_level)
# Clear UI elements
clear_ui_elements()
# Clear existing tiles
clear_tiles()
# Create game board
create_game_board()
# Shuffle tiles
shuffle_tiles()
# Show level info
show_level_info()
# Create timer label
timer_label = Label.new()
timer_label.text = "时间: %02d:%02d" % [floor(time_left / 60), int(time_left) % 60]
timer_label.position = Vector2(10, 10)
timer_label.add_theme_font_size_override("font_size", 24)
timer_label.add_theme_color_override("font_color", Color(1, 1, 0))
timer_label.name = "TimerLabel"
add_child(timer_label)
# 启动自定义关卡
func start_custom_level(texture: Texture2D):
in_menu = false
timer_running = true
time_left = TIME_LIMIT
# 移除菜单
if menu_container:
menu_container.queue_free()
# 重置游戏状态
is_solved = false
selected_tile = null
current_level = level_textures.size() + 1 # 设置为预设关卡之后
camera_angle = 0.0
camera_target_angle = 0.0
print("Starting custom puzzle")
# 清除UI元素
clear_ui_elements()
# 清除现有拼图块
clear_tiles()
# 创建游戏板
create_custom_game_board(texture)
# 打乱拼图块
shuffle_tiles()
# 显示关卡信息
show_custom_level_info()
# 创建计时器标签
timer_label = Label.new()
timer_label.text = "时间: %02d:%02d" % [floor(time_left / 60), int(time_left) % 60]
timer_label.position = Vector2(10, 10)
timer_label.add_theme_font_size_override("font_size", 24)
timer_label.add_theme_color_override("font_color", Color(1, 1, 0))
timer_label.name = "TimerLabel"
add_child(timer_label)
# 创建自定义游戏板
func create_custom_game_board(texture: Texture2D):
# 创建图像
var image = texture.get_image()
var texture_width = texture.get_width()
var texture_height = texture.get_height()
# 计算像素块大小
var tile_width_px = texture_width / GRID_SIZE
var tile_height_px = texture_height / GRID_SIZE
# 创建所有拼图块
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
# 计算索引
var index = y * GRID_SIZE + x
# 创建拼图块纹理
var tile_image = image.get_region(Rect2i(
x * tile_width_px,
y * tile_height_px,
tile_width_px,
tile_height_px
))
var tile_texture = ImageTexture.create_from_image(tile_image)
# 创建拼图块平面
var mesh = PlaneMesh.new()
mesh.size = Vector2(TILE_WIDTH, TILE_HEIGHT)
mesh.orientation = PlaneMesh.FACE_Z
var material = StandardMaterial3D.new()
material.albedo_texture = tile_texture
material.cull_mode = StandardMaterial3D.CULL_DISABLED
var tile = MeshInstance3D.new()
tile.mesh = mesh
tile.material_override = material
# 设置位置
tile.position = Vector3(
x * TILE_WIDTH - (BOARD_WIDTH - TILE_WIDTH) / 2.0,
(BOARD_HEIGHT - TILE_HEIGHT) / 2.0 - y * TILE_HEIGHT,
0.0
)
# 存储位置信息
tile.set_meta("grid_pos", Vector2(x, y))
tile.set_meta("index", index)
# 添加可点击区域
var area = Area3D.new()
var collision = CollisionShape3D.new()
collision.shape = BoxShape3D.new()
collision.shape.size = Vector3(TILE_WIDTH, TILE_HEIGHT, 0.2)
area.add_child(collision)
tile.add_child(area)
area.input_event.connect(_on_tile_input_event.bind(tile))
add_child(tile)
tiles.append(tile)
# Clear UI elements
func clear_ui_elements():
for child in get_children():
if child is Label or child is Button or child is VBoxContainer:
child.queue_free()
# Clear tiles
func clear_tiles():
for tile in tiles:
if is_instance_valid(tile):
tile.queue_free()
tiles.clear()
# Show level info
func show_level_info():
var level_label = Label.new()
level_label.text = "关卡: %d/%d" % [current_level, level_textures.size()]
level_label.position = Vector2(20, 80) # 调整位置避免重叠
level_label.add_theme_font_size_override("font_size", 24)
level_label.add_theme_color_override("font_color", Color.WHITE)
level_label.name = "LevelLabel"
add_child(level_label)
# 显示自定义关卡信息
func show_custom_level_info():
var level_label = Label.new()
level_label.text = "自定义拼图"
level_label.position = Vector2(20, 80)
level_label.add_theme_font_size_override("font_size", 24)
level_label.add_theme_color_override("font_color", Color(1, 0.8, 0.3)) # 橙色显示自定义关卡
level_label.name = "LevelLabel"
add_child(level_label)
# Create game board
func create_game_board():
# Get current level texture
var texture = level_textures[current_level - 1]
# Create image from texture
var image = texture.get_image()
var texture_width = texture.get_width()
var texture_height = texture.get_height()
# Calculate tile size in pixels
var tile_width_px = texture_width / GRID_SIZE
var tile_height_px = texture_height / GRID_SIZE
# Create all puzzle pieces as flat planes
for y in range(GRID_SIZE):
for x in range(GRID_SIZE):
# Calculate index
var index = y * GRID_SIZE + x
# Create tile texture
var tile_image = image.get_region(Rect2i(
x * tile_width_px,
y * tile_height_px,
tile_width_px,
tile_height_px
))
var tile_texture = ImageTexture.create_from_image(tile_image)
# Create flat plane for puzzle piece
var mesh = PlaneMesh.new()
mesh.size = Vector2(TILE_WIDTH, TILE_HEIGHT)
mesh.orientation = PlaneMesh.FACE_Z # Make it face the camera
var material = StandardMaterial3D.new()
material.albedo_texture = tile_texture
material.cull_mode = StandardMaterial3D.CULL_DISABLED # Make both sides visible
var tile = MeshInstance3D.new()
tile.mesh = mesh
tile.material_override = material
# Set position (no Z-offset needed for planes)
tile.position = Vector3(
x * TILE_WIDTH - (BOARD_WIDTH - TILE_WIDTH) / 2.0,
(BOARD_HEIGHT - TILE_HEIGHT) / 2.0 - y * TILE_HEIGHT,
0.0
)
# Store position info
tile.set_meta("grid_pos", Vector2(x, y))
tile.set_meta("index", index)
# Add clickable area
var area = Area3D.new()
var collision = CollisionShape3D.new()
collision.shape = BoxShape3D.new()
collision.shape.size = Vector3(TILE_WIDTH, TILE_HEIGHT, 0.2) # 增加深度为0.2
area.add_child(collision)
# 修复: 先将 area 添加到 tile
tile.add_child(area)
# Connect input event
area.input_event.connect(_on_tile_input_event.bind(tile))
add_child(tile)
tiles.append(tile)
# Shuffle tiles
func shuffle_tiles():
# Swap only with adjacent tiles
for i in range(100):
var random_tile = tiles[randi() % tiles.size()]
var neighbors = get_adjacent_tiles(random_tile)
if neighbors.size() > 0:
var neighbor_tile = neighbors[randi() % neighbors.size()]
swap_tiles(random_tile, neighbor_tile)
# Get adjacent tiles
func get_adjacent_tiles(tile: MeshInstance3D) -> Array:
var adjacent_tiles = []
var grid_pos = tile.get_meta("grid_pos")
# Check four directions: up, down, left, right
var directions = [
Vector2(0, -1), # Up
Vector2(0, 1), # Down
Vector2(-1, 0), # Left
Vector2(1, 0) # Right
]
for dir in directions:
var neighbor_pos = grid_pos + dir
# Check if position is valid
if neighbor_pos.x >= 0 and neighbor_pos.x < GRID_SIZE and \
neighbor_pos.y >= 0 and neighbor_pos.y < GRID_SIZE:
# Find tile at position
for neighbor in tiles:
if neighbor.get_meta("grid_pos") == neighbor_pos:
adjacent_tiles.append(neighbor)
break
return adjacent_tiles
# Swap two tiles
func swap_tiles(tile1: MeshInstance3D, tile2: MeshInstance3D):
# Swap positions
var temp_pos = tile1.position
tile1.position = tile2.position
tile2.position = temp_pos
# Swap grid position metadata
var temp_grid_pos = tile1.get_meta("grid_pos")
tile1.set_meta("grid_pos", tile2.get_meta("grid_pos"))
tile2.set_meta("grid_pos", temp_grid_pos)
# Add animation effect
animate_swap(tile1, tile2)
# Animate swap with 3D effect
func animate_swap(tile1: MeshInstance3D, tile2: MeshInstance3D):
# Create tween for pop effect
var tween = create_tween()
tween.set_parallel(true)
# Tile1 animation
tween.tween_property(tile1, "position:y", tile1.position.y + 1.0, 0.2) # 增加到1.0
tween.tween_property(tile1, "position:y", tile1.position.y, 0.2).set_delay(0.2)
# Tile2 animation
tween.tween_property(tile2, "position:y", tile2.position.y + 1.0, 0.2) # 增加到1.0
tween.tween_property(tile2, "position:y", tile2.position.y, 0.2).set_delay(0.2)
# Check if puzzle is solved
func check_completion() -> bool:
# Check if all tiles are in correct position
for tile in tiles:
var current_pos = tile.get_meta("grid_pos")
var index = tile.get_meta("index")
var correct_x = int(index) % GRID_SIZE
var correct_y = int(index) / GRID_SIZE
var correct_pos = Vector2(correct_x, correct_y)
if current_pos != correct_pos:
return false
return true
# Tile input event
func _on_tile_input_event(camera, event, position, normal, shape_idx, tile: MeshInstance3D):
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
if is_solved || in_menu || !timer_running:
return
# If no tile selected, select this tile
if selected_tile == null:
selected_tile = tile
# Highlight selected tile
tile.material_override.albedo_color = Color(0.8, 0.8, 1.0)
else:
# If same tile clicked, deselect
if selected_tile == tile:
selected_tile.material_override.albedo_color = Color(1, 1, 1)
selected_tile = null
return
# Get positions
var selected_pos = selected_tile.get_meta("grid_pos")
var tile_pos = tile.get_meta("grid_pos")
# Check if adjacent
var is_adjacent = false
if selected_pos.x == tile_pos.x: # Same column
if abs(selected_pos.y - tile_pos.y) == 1:
is_adjacent = true
elif selected_pos.y == tile_pos.y: # Same row
if abs(selected_pos.x - tile_pos.x) == 1:
is_adjacent = true
if is_adjacent:
# Swap the two tiles
swap_tiles(selected_tile, tile)
# Check if solved
if check_completion():
print("Puzzle solved!")
is_solved = true
timer_running = false
show_completion_message()
else:
# Show hint message
show_hint("只能交换相邻的拼图块")
# Restore colors
selected_tile.material_override.albedo_color = Color(1, 1, 1)
tile.material_override.albedo_color = Color(1, 1, 1)
# Deselect
selected_tile = null
# Show hint message
func show_hint(message: String):
# Create hint label
var hint = Label.new()
hint.text = message
hint.position = Vector2(200, 70)
hint.add_theme_font_size_override("font_size", 24)
hint.add_theme_color_override("font_color", Color.YELLOW)
hint.name = "HintLabel"
add_child(hint)
# Set timer to remove hint
var timer = Timer.new()
timer.wait_time = 1.5
timer.one_shot = true
timer.timeout.connect(
func():
if is_instance_valid(hint) and hint.is_inside_tree():
hint.queue_free()
if is_instance_valid(timer) and timer.is_inside_tree():
timer.queue_free()
)
add_child(timer)
timer.start()
# Show completion message and proceed to next level
func show_completion_message():
print("Showing completion message")
# Create completion message
var message = Label.new()
message.text = "关卡完成!"
message.position = Vector2(300, 100)
message.add_theme_font_size_override("font_size", 36)
message.add_theme_color_override("font_color", Color.GREEN)
message.name = "CompletionMessage"
add_child(message)
# Set timer for next level
var next_level_timer = Timer.new()
next_level_timer.wait_time = 2.0 # Proceed after 2 seconds
next_level_timer.one_shot = true
next_level_timer.timeout.connect(_on_next_level_timer_timeout)
add_child(next_level_timer)
next_level_timer.start()
# Timer timeout - proceed to next level
func _on_next_level_timer_timeout():
# 如果是自定义关卡,返回主菜单
if current_level > level_textures.size():
print("Custom puzzle completed")
show_main_menu()
return
# Check if more levels
if current_level < level_textures.size():
print("Proceeding to next level")
start_level(current_level + 1)
else:
print("All levels completed")
# Clear UI
clear_ui_elements()
# Show final message
var final_message = Label.new()
final_message.text = "恭喜!你完成了全部%d个关卡!" % level_textures.size()
final_message.position = Vector2(200, 200)
final_message.add_theme_font_size_override("font_size", 36)
final_message.add_theme_color_override("font_color", Color.GOLD)
final_message.name = "FinalMessage"
add_child(final_message)
# Add custom image button
var custom_button = Button.new()
custom_button.text = "玩自定义图片"
custom_button.position = Vector2(300, 280)
custom_button.custom_minimum_size = Vector2(200, 50)
custom_button.add_theme_font_size_override("font_size", 28)
custom_button.pressed.connect(_on_custom_image_button_pressed)
add_child(custom_button)
# Add menu button
var menu_button = Button.new()
menu_button.text = "返回主菜单"
menu_button.position = Vector2(300, 350)
menu_button.custom_minimum_size = Vector2(160, 50)
menu_button.add_theme_font_size_override("font_size", 28)
menu_button.pressed.connect(show_main_menu)
add_child(menu_button)
# Handle time updates and camera rotation
func _process(delta):
# Camera rotation
if Input.is_action_pressed("ui_left"):
camera_target_angle += 60 * delta
elif Input.is_action_pressed("ui_right"):
camera_target_angle -= 60 * delta
camera_angle = lerp(camera_angle, camera_target_angle, 5 * delta)
rotation_degrees.y = camera_angle
# Timer logic
if timer_running && !is_solved && !in_menu:
time_left -= delta
# Update timer label
if timer_label:
timer_label.text = "时间: %02d:%02d" % [floor(time_left / 60), int(time_left) % 60]
# Change color when time is low
if time_left < 10:
timer_label.add_theme_color_override("font_color", Color(1, 0, 0))
elif time_left < 20:
timer_label.add_theme_color_override("font_color", Color(1, 0.5, 0))
else:
timer_label.add_theme_color_override("font_color", Color(1, 1, 0))
# Check if time ran out
if time_left <= 0:
time_left = 0
timer_running = false
show_game_over()
# Show game over message
func show_game_over():
# Clear UI
clear_ui_elements()
# Create game over message
var game_over = Label.new()
game_over.text = "时间到!游戏结束"
game_over.position = Vector2(250, 200)
game_over.add_theme_font_size_override("font_size", 48)
game_over.add_theme_color_override("font_color", Color(1, 0, 0))
game_over.name = "GameOver"
add_child(game_over)
# Add menu button
var menu_button = Button.new()
menu_button.text = "返回主菜单"
menu_button.position = Vector2(300, 300)
menu_button.custom_minimum_size = Vector2(160, 60)
menu_button.add_theme_font_size_override("font_size", 32)
menu_button.pressed.connect(show_main_menu)
add_child(menu_button)