Files
Sprout-Farm/Scene/SmallGame/SnakeGame.gd
2025-08-28 10:23:13 +08:00

562 lines
15 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Panel
# 游戏常量
const GRID_SIZE = 20
const GRID_WIDTH = 30
const GRID_HEIGHT = 30
const DATA_FILE_PATH = "user://playergamedata.json"
# 方向枚举
enum Direction {
UP,
DOWN,
LEFT,
RIGHT
}
# 食物类型枚举
enum FoodType {
NORMAL, # 普通食物 +10分
GOLDEN, # 金色食物 +50分
SPEED, # 加速食物 +20分临时加速
SLOW, # 减速食物 +30分临时减速
BONUS # 奖励食物 +100分
}
# 游戏变量
var snake_body = []
var snake_direction = Direction.RIGHT
var next_direction = Direction.RIGHT
var food_position = Vector2()
var food_type = FoodType.NORMAL
var score = 0
var best_score = 0
var level = 1
var speed_multiplier = 1.0
var speed_effect_timer = 0.0
var game_over = false
var game_started = false # 添加游戏开始状态
var obstacles = []
var games_played = 0
var total_food_eaten = 0
var player_data = {}
# 节点引用
@onready var game_area = $GameArea
@onready var score_label = $ScoreLabel
@onready var game_over_label = $GameOverLabel
@onready var game_timer = $GameTimer
@onready var virtual_controls = $VirtualControls
func _ready():
# 连接定时器信号
game_timer.timeout.connect(_on_game_timer_timeout)
# 设置游戏区域样式
game_area.modulate = Color(0.1, 0.1, 0.15, 0.9)
# 连接虚拟按键信号
setup_virtual_controls()
# 加载玩家数据
load_player_data()
# 显示游戏开始界面
show_start_screen()
func init_game():
# 重置游戏状态
game_over = false
game_started = true
score = 0
level = 1
speed_multiplier = 1.0
speed_effect_timer = 0.0
snake_direction = Direction.RIGHT
next_direction = Direction.RIGHT
games_played += 1
# 初始化蛇身
snake_body.clear()
snake_body.append(Vector2(5, 5))
snake_body.append(Vector2(4, 5))
snake_body.append(Vector2(3, 5))
# 清空障碍物
obstacles.clear()
# 生成食物和障碍物
generate_food()
generate_obstacles()
# 更新UI
update_score()
game_over_label.visible = false
# 设置定时器速度
game_timer.wait_time = 0.2 / speed_multiplier
game_timer.start()
func _input(event):
if event is InputEventKey and event.pressed:
# 游戏未开始时按Q键开始游戏
if not game_started:
if event.keycode == KEY_Q:
init_game()
return
if game_over:
if event.keycode == KEY_SPACE:
init_game()
elif event.keycode == KEY_Q:
init_game()
return
# 控制蛇的方向
match event.keycode:
KEY_UP, KEY_W:
change_direction(Direction.UP)
KEY_DOWN, KEY_S:
change_direction(Direction.DOWN)
KEY_LEFT, KEY_A:
change_direction(Direction.LEFT)
KEY_RIGHT, KEY_D:
change_direction(Direction.RIGHT)
func change_direction(new_direction: Direction):
# 防止蛇反向移动
match new_direction:
Direction.UP:
if snake_direction != Direction.DOWN:
next_direction = Direction.UP
Direction.DOWN:
if snake_direction != Direction.UP:
next_direction = Direction.DOWN
Direction.LEFT:
if snake_direction != Direction.RIGHT:
next_direction = Direction.LEFT
Direction.RIGHT:
if snake_direction != Direction.LEFT:
next_direction = Direction.RIGHT
func _on_game_timer_timeout():
if not game_started or game_over:
return
# 处理速度效果
if speed_effect_timer > 0:
speed_effect_timer -= game_timer.wait_time
if speed_effect_timer <= 0:
speed_multiplier = 1.0
game_timer.wait_time = 0.2 / speed_multiplier
# 更新方向
snake_direction = next_direction
# 移动蛇
move_snake()
# 检查碰撞
check_collisions()
# 重绘游戏
queue_redraw()
func move_snake():
var head = snake_body[0]
var new_head = head
# 根据方向计算新的头部位置
match snake_direction:
Direction.UP:
new_head = Vector2(head.x, head.y - 1)
Direction.DOWN:
new_head = Vector2(head.x, head.y + 1)
Direction.LEFT:
new_head = Vector2(head.x - 1, head.y)
Direction.RIGHT:
new_head = Vector2(head.x + 1, head.y)
# 添加新头部
snake_body.insert(0, new_head)
# 检查是否吃到食物
if new_head == food_position:
# 根据食物类型增加分数和效果
eat_food()
# 生成新食物
generate_food()
# 检查等级提升
check_level_up()
else:
# 移除尾部
snake_body.pop_back()
func check_collisions():
var head = snake_body[0]
# 检查边界碰撞
if head.x < 0 or head.x >= GRID_WIDTH or head.y < 0 or head.y >= GRID_HEIGHT:
game_over = true
show_game_over()
return
# 检查自身碰撞
for i in range(1, snake_body.size()):
if head == snake_body[i]:
game_over = true
show_game_over()
return
# 检查障碍物碰撞
for obstacle in obstacles:
if head == obstacle:
game_over = true
show_game_over()
return
func generate_food():
var attempts = 0
while attempts < 100: # 防止无限循环
food_position = Vector2(
randi() % GRID_WIDTH,
randi() % GRID_HEIGHT
)
# 确保食物不在蛇身上和障碍物上
var food_blocked = false
for segment in snake_body:
if segment == food_position:
food_blocked = true
break
if not food_blocked:
for obstacle in obstacles:
if obstacle == food_position:
food_blocked = true
break
if not food_blocked:
break
attempts += 1
# 随机生成食物类型
var rand = randf()
if rand < 0.6: # 60% 普通食物
food_type = FoodType.NORMAL
elif rand < 0.75: # 15% 金色食物
food_type = FoodType.GOLDEN
elif rand < 0.85: # 10% 加速食物
food_type = FoodType.SPEED
elif rand < 0.95: # 10% 减速食物
food_type = FoodType.SLOW
else: # 5% 奖励食物
food_type = FoodType.BONUS
func update_score():
score_label.text = "🐍 分数: " + str(score) + "\n🏆 最高分: " + str(best_score) + "\n⭐ 等级: " + str(level) + "\n🎮 游戏次数: " + str(games_played)
func show_game_over():
game_timer.stop()
game_started = false
if score > best_score:
best_score = score
update_score()
save_player_data()
game_over_label.text = "🎮 游戏结束\n🏆 分数: " + str(score) + "\n⭐ 等级: " + str(level) + "\n\n🔄 按Q键或空格重新开始"
game_over_label.visible = true
func eat_food():
total_food_eaten += 1
match food_type:
FoodType.NORMAL:
score += 10
FoodType.GOLDEN:
score += 50
FoodType.SPEED:
score += 20
speed_multiplier = 1.5
speed_effect_timer = 5.0
game_timer.wait_time = 0.2 / speed_multiplier
FoodType.SLOW:
score += 30
speed_multiplier = 0.7
speed_effect_timer = 5.0
game_timer.wait_time = 0.2 / speed_multiplier
FoodType.BONUS:
score += 100
update_score()
func check_level_up():
var new_level = (total_food_eaten / 10) + 1
if new_level > level:
level = new_level
generate_obstacles() # 每升级增加障碍物
func generate_obstacles():
# 根据等级生成障碍物
var obstacle_count = min(level - 1, 10) # 最多10个障碍物
obstacles.clear()
for i in range(obstacle_count):
var attempts = 0
while attempts < 50:
var obstacle_pos = Vector2(
randi() % GRID_WIDTH,
randi() % GRID_HEIGHT
)
# 确保障碍物不在蛇身、食物或其他障碍物上
var blocked = false
for segment in snake_body:
if segment == obstacle_pos:
blocked = true
break
if not blocked and obstacle_pos == food_position:
blocked = true
if not blocked:
for existing_obstacle in obstacles:
if existing_obstacle == obstacle_pos:
blocked = true
break
if not blocked:
obstacles.append(obstacle_pos)
break
attempts += 1
func setup_virtual_controls():
if not virtual_controls:
return
var up_btn = virtual_controls.get_node("UpButton")
var down_btn = virtual_controls.get_node("DownButton")
var left_btn = virtual_controls.get_node("LeftButton")
var right_btn = virtual_controls.get_node("RightButton")
var restart_btn = virtual_controls.get_node("RestartButton")
if up_btn:
up_btn.pressed.connect(_on_virtual_button_pressed.bind(Direction.UP))
if down_btn:
down_btn.pressed.connect(_on_virtual_button_pressed.bind(Direction.DOWN))
if left_btn:
left_btn.pressed.connect(_on_virtual_button_pressed.bind(Direction.LEFT))
if right_btn:
right_btn.pressed.connect(_on_virtual_button_pressed.bind(Direction.RIGHT))
if restart_btn:
restart_btn.pressed.connect(_on_restart_button_pressed)
func _on_virtual_button_pressed(direction: Direction):
if game_started and not game_over:
change_direction(direction)
func _on_restart_button_pressed():
if not game_started or game_over:
init_game()
func load_player_data():
if FileAccess.file_exists(DATA_FILE_PATH):
var file = FileAccess.open(DATA_FILE_PATH, FileAccess.READ)
if file:
var json_string = file.get_as_text()
file.close()
var json = JSON.new()
var parse_result = json.parse(json_string)
if parse_result == OK:
player_data = json.data
if player_data.has("snake"):
var game_data = player_data["snake"]
best_score = game_data.get("best_score", 0)
games_played = game_data.get("games_played", 0)
total_food_eaten = game_data.get("total_food_eaten", 0)
func show_start_screen():
# 重置状态
game_started = false
game_over = false
# 初始化蛇身用于显示
snake_body.clear()
snake_body.append(Vector2(5, 5))
snake_body.append(Vector2(4, 5))
snake_body.append(Vector2(3, 5))
# 清空障碍物
obstacles.clear()
# 生成初始食物
food_position = Vector2(10, 10)
food_type = FoodType.NORMAL
# 显示开始提示
game_over_label.text = "🐍 贪吃蛇游戏 🐍\n\n🏆 最高分数: " + str(best_score) + "\n🎯 游戏次数: " + str(games_played) + "\n\n🎮 按Q键开始游戏\n\n🎯 操作说明:\n方向键/WASD - 控制方向\n\n🍎 食物类型:\n🔴 普通食物 +10分\n🟡 金色食物 +50分\n🔵 加速食物 +20分\n🟣 减速食物 +30分\n🌈 奖励食物 +100分"
game_over_label.visible = true
# 停止定时器
game_timer.stop()
update_score()
queue_redraw()
func save_player_data():
if not player_data.has("snake"):
player_data["snake"] = {}
player_data["snake"]["best_score"] = best_score
player_data["snake"]["current_score"] = score
player_data["snake"]["games_played"] = games_played
player_data["snake"]["total_food_eaten"] = total_food_eaten
player_data["snake"]["max_level_reached"] = level
# 更新全局数据
if not player_data.has("global"):
player_data["global"] = {}
player_data["global"]["last_played"] = Time.get_datetime_string_from_system()
var file = FileAccess.open(DATA_FILE_PATH, FileAccess.WRITE)
if file:
var json_string = JSON.stringify(player_data)
file.store_string(json_string)
file.close()
func _draw():
if not game_area:
return
# 绘制背景渐变
var gradient = Gradient.new()
gradient.add_point(0.0, Color(0.05, 0.1, 0.2, 0.95))
gradient.add_point(0.5, Color(0.1, 0.15, 0.25, 0.9))
gradient.add_point(1.0, Color(0.15, 0.2, 0.3, 0.95))
draw_rect(Rect2(Vector2.ZERO, size), gradient.sample(0.5), true)
# 获取游戏区域的位置和大小
var area_pos = game_area.position
var area_size = game_area.size
# 绘制游戏区域阴影
var shadow_offset = Vector2(4, 4)
var area_rect = Rect2(area_pos + shadow_offset, area_size)
draw_rect(area_rect, Color(0, 0, 0, 0.3), true)
# 绘制游戏区域背景
area_rect = Rect2(area_pos, area_size)
draw_rect(area_rect, Color(0.08, 0.12, 0.18, 0.9), true)
# 计算网格大小
var cell_width = area_size.x / GRID_WIDTH
var cell_height = area_size.y / GRID_HEIGHT
# 绘制网格线(淡色)
for x in range(GRID_WIDTH + 1):
var start_pos = Vector2(area_pos.x + x * cell_width, area_pos.y)
var end_pos = Vector2(area_pos.x + x * cell_width, area_pos.y + area_size.y)
draw_line(start_pos, end_pos, Color(0.3, 0.3, 0.4, 0.3), 1)
for y in range(GRID_HEIGHT + 1):
var start_pos = Vector2(area_pos.x, area_pos.y + y * cell_height)
var end_pos = Vector2(area_pos.x + area_size.x, area_pos.y + y * cell_height)
draw_line(start_pos, end_pos, Color(0.3, 0.3, 0.4, 0.3), 1)
# 绘制障碍物
for obstacle in obstacles:
var rect = Rect2(
area_pos.x + obstacle.x * cell_width,
area_pos.y + obstacle.y * cell_height,
cell_width - 2,
cell_height - 2
)
# 绘制立体障碍物效果
draw_rect(rect, Color(0.4, 0.2, 0.1), true)
# 高光
var highlight_rect = Rect2(rect.position + Vector2(1, 1), Vector2(rect.size.x - 2, rect.size.y * 0.3))
draw_rect(highlight_rect, Color(0.6, 0.4, 0.2, 0.8), true)
# 绘制蛇身
for i in range(snake_body.size()):
var segment = snake_body[i]
var rect = Rect2(
area_pos.x + segment.x * cell_width,
area_pos.y + segment.y * cell_height,
cell_width - 2,
cell_height - 2
)
if i == 0: # 头部
# 绘制蛇头(圆形,带渐变)
var center = rect.get_center()
var radius = min(rect.size.x, rect.size.y) * 0.4
# 阴影
draw_circle(center + Vector2(1, 1), radius, Color(0, 0, 0, 0.3))
# 主体
draw_circle(center, radius, Color(0.2, 0.8, 0.2))
# 高光
draw_circle(center - Vector2(2, 2), radius * 0.6, Color(0.4, 1.0, 0.4, 0.7))
# 眼睛
var eye_size = radius * 0.2
draw_circle(center + Vector2(-eye_size, -eye_size), eye_size * 0.5, Color.BLACK)
draw_circle(center + Vector2(eye_size, -eye_size), eye_size * 0.5, Color.BLACK)
else: # 身体
# 绘制蛇身(渐变色)
var body_color = Color.LIME_GREEN.lerp(Color.DARK_GREEN, float(i) / snake_body.size())
draw_rect(rect, body_color, true)
# 高光
var highlight_rect = Rect2(rect.position + Vector2(1, 1), Vector2(rect.size.x - 2, rect.size.y * 0.3))
draw_rect(highlight_rect, Color(1, 1, 1, 0.3), true)
# 绘制食物
var food_rect = Rect2(
area_pos.x + food_position.x * cell_width,
area_pos.y + food_position.y * cell_height,
cell_width - 2,
cell_height - 2
)
# 根据食物类型绘制不同效果
var food_center = food_rect.get_center()
var food_radius = min(food_rect.size.x, food_rect.size.y) * 0.4
match food_type:
FoodType.NORMAL:
# 普通红色食物
draw_circle(food_center + Vector2(1, 1), food_radius, Color(0, 0, 0, 0.3)) # 阴影
draw_circle(food_center, food_radius, Color.RED)
draw_circle(food_center - Vector2(1, 1), food_radius * 0.6, Color(1, 0.5, 0.5, 0.8)) # 高光
FoodType.GOLDEN:
# 金色食物(闪烁效果)
var pulse = sin(Time.get_ticks_msec() * 0.008) * 0.2 + 0.8
draw_circle(food_center + Vector2(1, 1), food_radius, Color(0, 0, 0, 0.3)) # 阴影
draw_circle(food_center, food_radius, Color.GOLD * pulse)
draw_circle(food_center - Vector2(1, 1), food_radius * 0.6, Color.YELLOW) # 高光
FoodType.SPEED:
# 蓝色加速食物
draw_circle(food_center + Vector2(1, 1), food_radius, Color(0, 0, 0, 0.3)) # 阴影
draw_circle(food_center, food_radius, Color.CYAN)
draw_circle(food_center - Vector2(1, 1), food_radius * 0.6, Color.LIGHT_BLUE) # 高光
FoodType.SLOW:
# 紫色减速食物
draw_circle(food_center + Vector2(1, 1), food_radius, Color(0, 0, 0, 0.3)) # 阴影
draw_circle(food_center, food_radius, Color.PURPLE)
draw_circle(food_center - Vector2(1, 1), food_radius * 0.6, Color.MAGENTA) # 高光
FoodType.BONUS:
# 彩虹奖励食物(旋转彩虹效果)
var rainbow_time = Time.get_ticks_msec() * 0.003
var rainbow_color = Color.from_hsv(fmod(rainbow_time, 1.0), 1.0, 1.0)
draw_circle(food_center + Vector2(1, 1), food_radius, Color(0, 0, 0, 0.3)) # 阴影
draw_circle(food_center, food_radius, rainbow_color)
draw_circle(food_center - Vector2(1, 1), food_radius * 0.6, Color.WHITE) # 高光
func _on_quit_button_pressed() -> void:
self.hide()
get_parent().remove_child(self)
queue_free()
pass