godot制作的3D拼图


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)


发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注