【游戏开发】坦克大战

目录

一、引言

二、开发前期:需求分析与技术选型

[1. 核心需求定义](#1. 核心需求定义)

[2. 技术选型与理由](#2. 技术选型与理由)

[3. 项目目录规划](#3. 项目目录规划)

三、架构设计:全局常量与核心分层

[1. 全局常量设计(可配置化)](#1. 全局常量设计(可配置化))

[2. 精灵分层设计(解决绘制层级问题)](#2. 精灵分层设计(解决绘制层级问题))

四、核心模块开发(按功能拆解)

[模块 1:资源加载(兼容与容错)](#模块 1:资源加载(兼容与容错))

实现方案:

[模块 2:基础游戏类(面向对象设计)](#模块 2:基础游戏类(面向对象设计))

[模块 3:按键配置系统(可自定义 + 冲突检测)](#模块 3:按键配置系统(可自定义 + 冲突检测))

实现步骤:

[模块 4:存档系统(高分榜 + 进度保存)](#模块 4:存档系统(高分榜 + 进度保存))

实现方案:

[模块 5:联机功能(Socket + 线程)](#模块 5:联机功能(Socket + 线程))

核心设计:

关键实现细节:

[模块 6:关卡与游戏状态管理](#模块 6:关卡与游戏状态管理)

实现方案:

[模块 7:UI 绘制与菜单系统](#模块 7:UI 绘制与菜单系统)

[1. 游戏内 UI](#1. 游戏内 UI)

[2. 主菜单(Tkinter)](#2. 主菜单(Tkinter))

[模块 8:游戏主循环(核心驱动)](#模块 8:游戏主循环(核心驱动))

五、优化与调试(关键问题解决)

[1. 性能优化](#1. 性能优化)

[2. 兼容性优化](#2. 兼容性优化)

[3. 核心 BUG 修复](#3. 核心 BUG 修复)

六、测试与验证

[1. 功能测试](#1. 功能测试)

[2. 性能测试](#2. 性能测试)

七、坦克大战的Python代码完整实现

八、程序运行部分截图展示

九、发布阶段

十、总结


一、引言

本文介绍的坦克大战游戏是基于 Pygame (2D 游戏核心)+ Tkinter (UI 菜单)+ Socket(联机功能)开发的跨平台 2D 射击游戏,涵盖单人 / 联机模式、动态关卡、道具系统、BOSS 战、存档 / 按键配置等核心功能。下面将详细讲解该坦克大战游戏的开发过程以及Python代码完整实现。

二、开发前期:需求分析与技术选型

1. 核心需求定义

开发初期先明确游戏的核心目标和功能边界,避免范围蔓延:

核心维度 具体需求
游戏模式 单人模式(支持双人本地)、联机模式(服务器 / 客户端)
核心玩法 坦克移动 / 射击、敌人 AI、障碍物碰撞、道具拾取、BOSS 战、关卡升级
辅助功能 自定义按键、存档(高分榜 / 进度)、难度调节、跨系统兼容(Windows/macOS/Linux)
性能要求 稳定 60FPS、低延迟联机、资源加载容错

2. 技术选型与理由

技术 / 库 用途 选型理由
Pygame 游戏核心(渲染、音效、输入) 专为 2D 游戏设计,封装了 SDL 底层,支持精灵、碰撞、帧率控制,上手成本低
Tkinter 菜单 / 设置 UI Python 内置库,无需额外安装,适合快速开发配置窗口(按键设置、难度选择)
Socket + Threading 联机功能 原生 Socket 实现 TCP 通信,线程处理异步消息(避免阻塞游戏主循环)
JSON 配置 / 存档存储 轻量、易读,支持中文,适配跨平台文件读写
Math/Platform 辅助计算 / 系统适配 原生库,用于碰撞距离计算、跨系统字体适配

3. 项目目录规划

提前规划目录结构,保证代码可维护性:

bash 复制代码
tank_game/
├── assets/          # 资源目录
│   ├── fonts/       # 字体文件(simhei.ttf)
│   └── images/      # 图片资源(动态生成,无需外部文件)
├── configs/         # 配置目录
│   └── key_config.json  # 按键配置
├── saves/           # 存档目录
│   └── game_save.json   # 高分榜/进度
└── tank_game.py     # 主程序

三、架构设计:全局常量与核心分层

1. 全局常量设计(可配置化)

将所有可变参数抽离为常量,支持动态调整(如分辨率缩放、难度系数):

  • 基础分辨率BASE_WIDTH/HEIGHT = 800/600,通过SCALE_FACTOR动态缩放(适配不同屏幕);
  • 难度配置DIFFICULTY_CONFIG 定义不同难度的敌人速度、生成频率、BOSS 血量;
  • 关卡 / 道具配置LEVEL_CONFIG(等级成长)、STAGE_CONFIG(关卡障碍物)、ITEM_TYPES(道具类型);
  • 联机配置:心跳包间隔、缓冲区大小、服务器 IP / 端口等。

2. 精灵分层设计(解决绘制层级问题)

游戏内元素按 "绘制优先级" 分层,避免子弹覆盖坦克、UI 被特效遮挡等问题:

python 复制代码
sprite_layers = {
    "background": 背景层(最底层),
    "obstacles": 障碍物层(墙壁/砖块),
    "tanks": 坦克层(玩家/敌人/BOSS),
    "bullets": 子弹层,
    "items": 道具层,
    "effects": 特效层(爆炸),
    "ui": UI层(最顶层,分数/血量)
}

四、核心模块开发(按功能拆解)

模块 1:资源加载(兼容与容错)

核心痛点:Pygame 默认字体不支持中文、跨系统字体差异、资源加载失败导致崩溃。

实现方案:
  1. 字体适配
    • 定义get_system_font_list(),按系统(Windows/macOS/Linux)返回适配中文字体列表;
    • load_game_font() 优先级:自定义字体文件 → 系统中文字体 → Pygame 默认字体;
    • render_text_safely() 确保 UTF-8 编码,避免中文乱码。
  2. 图片资源
    • 无需外部图片文件,通过 Pygame 绘图 API 动态生成坦克 / 子弹 / 障碍物(填充、描边、渐变);
    • 精灵表(SpriteSheet)管理爆炸特效帧,支持缩放。

模块 2:基础游戏类(面向对象设计)

所有游戏元素封装为 Pygame Sprite 子类,复用碰撞、绘制、更新逻辑:

类名 核心职责 关键实现细节
Tank 坦克(玩家 / 敌人 / BOSS)的移动 / 旋转 / 射击 / 受击 1. 旋转冷却(避免频繁旋转);2. 分步碰撞检测(X/Y 轴分离,防止卡住);3. BOSS AI(寻路 + 避障);4. 道具效果计时
Bullet 子弹移动 / 碰撞 / 穿透 1. 生命周期限制(避免无限飞行);2. 穿透逻辑(记录已碰撞目标,不重复伤害);3. 友军不伤害
Item 道具生成 / 拾取 / 时效 1. 超时消失(10 秒);2. 联机同步拾取消息;3. 道具效果绑定到坦克实例
Explosion 爆炸特效帧动画 帧速率控制,播放完成后自动销毁
Obstacle 墙壁 / 砖块障碍物 砖块可破坏、墙壁无敌,受击后生成爆炸特效

模块 3:按键配置系统(可自定义 + 冲突检测)

核心需求:支持玩家自定义按键、检测按键冲突、恢复默认、保存配置。

实现步骤:
  1. 配置存储 :默认按键配置DEFAULT_KEY_CONFIG,保存到key_config.json
  2. TK 窗口开发KeyConfigWindow类创建按键设置界面,分玩家 1 / 玩家 2 配置区域;
  3. 按键绑定
    • 点击按键按钮后进入 "待输入" 状态,监听 TK 按键事件;
    • TK 按键码映射为 Pygame 按键码(tk_to_pygame字典);
    • 检测按键冲突(避免多个动作绑定同一按键);
  4. 持久化:保存 / 恢复配置时更新 JSON 文件,同步 UI 显示。

模块 4:存档系统(高分榜 + 进度保存)

核心需求:自动保存高分榜、游戏进度,支持读取历史记录。

实现方案:
  1. 存档结构DEFAULT_SAVE_DATA 包含high_scores(高分榜)和last_progress(最后进度);
  2. 高分榜逻辑update_high_score() 去重 + 降序排序,只保留前 10 名;
  3. 进度保存:游戏结束时自动保存分数、等级、关卡、难度;
  4. 容错处理load_json() 捕获文件不存在 / 解析错误,返回默认配置并自动创建文件。

模块 5:联机功能(Socket + 线程)

核心痛点:联机数据同步、线程安全、断线重连、心跳保活。

核心设计:
角色 核心职责
服务器 1. 监听客户端连接;2. 维护在线玩家列表;3. 广播坦克 / 子弹 / 道具数据;4. 心跳检测超时玩家
客户端 1. 连接服务器;2. 发送本地坦克位置 / 射击数据;3. 接收并同步远程玩家状态;4. 心跳保活
关键实现细节:
  1. 线程安全 :使用online_players_lock(互斥锁)保护共享数据(在线玩家列表),避免多线程竞争;
  2. 心跳包机制
    • 服务器:每 1 秒检查玩家最后心跳时间,超时(5 秒)则清理连接;
    • 客户端:每 1 秒发送心跳包,维持连接;
  3. 数据同步
    • 坦克位置:客户端实时发送坐标 / 方向,服务器广播给其他玩家;
    • 子弹 / 道具:生成 / 拾取时发送同步消息,保证多端一致;
  4. 容错处理:Socket 设置超时、捕获断连异常,自动清理无效连接 / 坦克。

模块 6:关卡与游戏状态管理

核心需求:动态生成关卡、等级成长、BOSS 触发、游戏结束判断。

实现方案:
  1. 关卡生成generate_stage() 按关卡配置生成障碍物,避开玩家出生区;
  2. 敌人生成spawn_enemy() 限制最大敌人数量,随机生成位置(避开障碍物 / 玩家),适配等级速度;
  3. BOSS 战 :分数达到BOSS_SPAWN_SCORE时生成 BOSS,BOSS AI 自动寻路 + 射击玩家;
  4. 游戏状态更新update_game_state() 检测等级 / 关卡升级、游戏结束(单人模式无存活玩家)。

模块 7:UI 绘制与菜单系统

1. 游戏内 UI

draw_ui() 绘制分数、等级、血量条、道具效果、暂停 / 游戏结束提示:

  • 血量条:圆角矩形 + 渐变,根据血量比例显示绿 / 黄 / 红;
  • 道具效果:显示道具名称 + 剩余时间;
  • 暂停 / 结束:半透明遮罩 + 居中文字,提升视觉层级。
2. 主菜单(Tkinter)

create_main_menu() 实现功能:

  • 单人 / 联机游戏启动;
  • 难度选择(简单 / 普通 / 困难);
  • 按键设置窗口调用;
  • 高分榜显示;
  • 退出游戏。

模块 8:游戏主循环(核心驱动)

game_loop() 是游戏的核心入口,按固定流程执行:

  1. 帧率控制clock.tick(FPS) 保证 60FPS,dt 计算帧间隔(适配不同性能设备);
  2. 事件处理:监听退出、暂停、射击、重新开始等按键;
  3. 输入处理:读取按键状态,控制坦克移动 / 旋转 / 射击(联机模式仅控制本地玩家);
  4. 生成逻辑:定时生成敌人 / 道具;
  5. 精灵更新:按分层更新坦克 / 子弹 / 道具 / 特效;
  6. 状态更新:等级 / 关卡 / 游戏结束判断;
  7. 绘制渲染:按分层绘制精灵 + UI,刷新屏幕。

五、优化与调试(关键问题解决)

1. 性能优化

  • 分层绘制 :按sprite_layers顺序绘制,避免重复渲染;
  • 精灵清理:爆炸 / 子弹 / 道具播放完成后自动销毁,避免内存泄漏;
  • 帧率稳定:主循环限制 60FPS,避免过度占用 CPU。

2. 兼容性优化

  • 字体适配:按系统自动选择中文字体,兜底 Pygame 默认字体;
  • 分辨率缩放 :所有尺寸按SCALE_FACTOR缩放,适配不同屏幕;
  • 文件读写:使用 UTF-8 编码,支持中文路径 / 内容。

3. 核心 BUG 修复

问题现象 修复方案
坦克移动卡住 分步检测 X/Y 轴碰撞,只移动未碰撞的轴
子弹穿透无效 记录已碰撞的障碍物 / 坦克,避免重复检测
联机数据不同步 加线程锁、广播数据、超时重发
中文乱码 强制 UTF-8 编码,适配系统字体
道具计时异常 使用pygame.time.get_ticks()(毫秒级)计时,避免帧计数误差

六、测试与验证

1. 功能测试

测试项 测试方法
单人模式 移动 / 射击、敌人生成、道具拾取、BOSS 战、游戏结束 / 重新开始
联机模式 服务器启动、多客户端连接、坦克 / 子弹 / 道具同步、断连重连
按键配置 自定义按键、冲突检测、恢复默认、保存生效
存档功能 游戏结束后查看高分榜、进度保存 / 读取
跨系统兼容 在 Windows/macOS/Linux 分别运行,验证字体 / 按键 / 文件读写

2. 性能测试

  • 监控 CPU / 内存占用:运行 30 分钟,无内存泄漏;
  • 联机延迟:本地测试延迟 < 50ms,保证操作流畅;
  • 帧率稳定性:全程稳定 60FPS,无明显掉帧。

七、坦克大战的Python代码完整实现

python 复制代码
import pygame
import random
import sys
import os
import json
import socket
import threading
import time
import tkinter as tk
from tkinter import ttk, messagebox
import math
from typing import Dict, List, Tuple, Optional, Any
import platform  # 系统识别,用于字体适配

# -------------------------- 全局初始化 --------------------------
pygame.init()
pygame.mixer.init()  # 音频初始化(确保不报错)
os.environ['SDL_VIDEO_CENTERED'] = '1'  # 窗口居中
os.environ['SDL_VIDEO_WINDOW_POS'] = "100,100"  # TK兼容

# -------------------------- 核心常量(支持动态缩放) --------------------------
# 基础分辨率
BASE_WIDTH = 800
BASE_HEIGHT = 600
# 缩放因子(可自定义)
SCALE_FACTOR = 1.0
# 动态计算缩放后的分辨率
SCREEN_WIDTH = int(BASE_WIDTH * SCALE_FACTOR)
SCREEN_HEIGHT = int(BASE_HEIGHT * SCALE_FACTOR)

FPS = 60
TARGET_FPS = 60

# 基础属性(按缩放因子调整)
BASE_TANK_SPEED = int(3 * SCALE_FACTOR)
BASE_BULLET_SPEED = int(8 * SCALE_FACTOR)
BASE_ENEMY_SPAWN_RATE = 60
BASE_MAX_ENEMIES = 5

# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 60, 60)
GREEN = (60, 255, 60)
BLUE = (60, 60, 255)
YELLOW = (255, 255, 80)
GRAY = (80, 80, 80)
DARK_GRAY = (50, 50, 50)
LIGHT_GRAY = (120, 120, 120)
PURPLE = (150, 60, 200)
ORANGE = (255, 140, 0)
CYAN = (60, 200, 200)
TRANSPARENT_BLUE = (0, 0, 255, 80)
TRANSPARENT_RED = (255, 0, 0, 80)
TRANSPARENT_GREEN = (0, 255, 0, 80)

# 资源路径
ASSETS_DIR = "assets"
IMAGE_DIR = os.path.join(ASSETS_DIR, "images")
FONT_DIR = os.path.join(ASSETS_DIR, "fonts")
CONFIG_DIR = "configs"
SAVE_DIR = "saves"
# 确保目录存在
for dir_path in [IMAGE_DIR, FONT_DIR, CONFIG_DIR, SAVE_DIR]:
    os.makedirs(dir_path, exist_ok=True)

DEFAULT_FONT_PATH = os.path.join(FONT_DIR, "simhei.ttf")

# 难度配置
DIFFICULTY_CONFIG = {
    "easy": {"speed_coeff": 0.8, "spawn_rate": 70, "max_enemies": 4, "boss_hp": 8},
    "normal": {"speed_coeff": 1.0, "spawn_rate": 60, "max_enemies": 5, "boss_hp": 10},
    "hard": {"speed_coeff": 1.2, "spawn_rate": 50, "max_enemies": 6, "boss_hp": 12}
}

# 等级/关卡配置
LEVEL_CONFIG = {
    0: (1.0, 60, 5),
    50: (1.2, 50, 6),
    100: (1.4, 40, 7),
    200: (1.6, 30, 8),
    300: (1.8, 20, 9)
}

STAGE_CONFIG = {
    1: {"obstacles": 15, "wall_density": 1, "boss_score": 500},
    2: {"obstacles": 20, "wall_density": 1.5, "boss_score": 800},
    3: {"obstacles": 25, "wall_density": 2, "boss_score": 1000}
}

# 道具配置
ITEM_TYPES = {"speed": "双倍射速", "shield": "护盾", "penetrate": "穿透子弹", "hp": "生命恢复"}
ITEM_SPAWN_RATE = 120
ITEM_DURATION = 5000  # 毫秒

# BOSS配置
BOSS_BASE_HP = 10
BOSS_SPEED_COEFF = 1.5
BOSS_SHOOT_COOLDOWN = 20
BOSS_SPAWN_SCORE = 500

# 联机配置
SERVER_IP = "127.0.0.1"
SERVER_PORT = 12345
BUFFER_SIZE = 1024
HEARTBEAT_INTERVAL = 1  # 心跳包间隔(秒)
online_mode = False
is_server = False
client_socket: Optional[socket.socket] = None
server_socket: Optional[socket.socket] = None
# 服务器端:{player_id: {"socket": sock, "addr": addr, "last_heartbeat": time, "tank": Tank}}
server_online_players: Dict[int, Dict[str, Any]] = {}
# 客户端:{player_id: Tank}
client_online_players: Dict[int, 'Tank'] = {}
online_players_lock = threading.Lock()
online_bullets: Dict[int, int] = {}
online_items: Dict[int, Any] = {}
online_boss: Dict[int, 'Tank'] = {}
network_latency = 0

# 全局游戏状态
game_state = {
    "score": 0,
    "level": 0,
    "stage": 1,
    "difficulty": "normal",
    "game_over": False,
    "paused": False,
    "online_players": 0,
    "player_id": 1,
    "menu_selected": 0,
    "online_selected": 0
}

# 精灵分层(严格控制绘制顺序)
sprite_layers = {
    "background": pygame.sprite.Group(),
    "obstacles": pygame.sprite.Group(),
    "tanks": pygame.sprite.Group(),
    "bullets": pygame.sprite.Group(),
    "items": pygame.sprite.Group(),
    "effects": pygame.sprite.Group(),
    "ui": pygame.sprite.Group()
}


# -------------------------- 字体加载核心优化 --------------------------
def get_system_font_list() -> list:
    """根据系统返回适配的中文字体列表"""
    system = platform.system().lower()
    if "windows" in system:
        return ["SimHei", "Microsoft YaHei", "SimSun", "Microsoft JhengHei"]
    elif "darwin" in system:  # macOS
        return ["Heiti TC", "PingFang SC", "STHeiti", "Arial Unicode MS"]
    elif "linux" in system:  # Linux
        return ["WenQuanYi Micro Hei", "Noto Sans CJK SC", "DejaVu Sans"]
    else:
        return ["SimHei", "DejaVu Sans"]


def load_game_font(scale_factor: float = 1.0, base_size: int = 24) -> pygame.font.Font:
    """
    安全加载字体,优先级:自定义字体文件 > 系统内置中文字体 > Pygame默认字体
    解决中文乱码、字体加载失败、跨系统兼容问题
    """
    font_size = max(12, int(base_size * scale_factor))  # 强制整数化,避免模糊

    # 1. 尝试加载自定义字体文件
    if os.path.exists(DEFAULT_FONT_PATH):
        try:
            font = pygame.font.Font(DEFAULT_FONT_PATH, font_size)
            font.render("测试中文", True, WHITE)  # 验证中文支持
            return font
        except (pygame.error, FileNotFoundError):
            pass

    # 2. 尝试加载系统内置中文字体
    for font_name in get_system_font_list():
        try:
            font = pygame.font.SysFont(font_name, font_size)
            font.render("测试中文", True, WHITE)  # 验证中文支持
            return font
        except (pygame.error, ValueError):
            continue

    # 3. 终极兜底:Pygame默认字体
    return pygame.font.Font(None, font_size)


def render_text_safely(font: pygame.font.Font, text: str, color: Tuple[int, ...],
                       antialias: bool = True) -> pygame.Surface:
    """安全渲染文字,解决中文编码/显示问题"""
    text = text.encode('utf-8').decode('utf-8')  # 确保UTF-8编码
    return font.render(text, antialias, color)


# -------------------------- 工具函数 --------------------------
def load_json(file_path: str, default: Any = None) -> Any:
    """安全加载JSON文件"""
    try:
        with open(file_path, "r", encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError, PermissionError):
        if default is not None:
            save_json(file_path, default)
            return default
        return {}


def save_json(file_path: str, data: Any) -> None:
    """安全保存JSON文件"""
    try:
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)
    except Exception as e:
        print(f"保存文件失败: {e}")


def get_key_name(key_code: int) -> str:
    """将pygame按键码转换为可读名称"""
    key_map = {
        pygame.K_w: "W", pygame.K_s: "S", pygame.K_a: "A", pygame.K_d: "D",
        pygame.K_SPACE: "空格", pygame.K_UP: "上方向键", pygame.K_DOWN: "下方向键",
        pygame.K_LEFT: "左方向键", pygame.K_RIGHT: "右方向键", pygame.K_RETURN: "回车",
        pygame.K_ESCAPE: "ESC", pygame.K_LSHIFT: "左Shift", pygame.K_RSHIFT: "右Shift",
        pygame.K_LCTRL: "左Ctrl", pygame.K_RCTRL: "右Ctrl", pygame.K_TAB: "Tab",
        pygame.K_k: "K", pygame.K_j: "J", pygame.K_l: "L", pygame.K_i: "I"
    }
    return key_map.get(key_code, f"按键({key_code})")


def calculate_distance(pos1: Tuple[int, int], pos2: Tuple[int, int]) -> float:
    """计算两点间距离"""
    return math.hypot(pos1[0] - pos2[0], pos1[1] - pos2[1])


def draw_rounded_rect(surface: pygame.Surface, color: Tuple[int, ...], rect: Tuple[int, ...],
                      radius: int = 10, border_width: int = 0) -> None:
    """绘制圆角矩形"""
    x, y, w, h = map(int, rect)
    radius = int(radius)
    border_width = int(border_width)

    # 创建临时表面处理透明度
    temp_surface = pygame.Surface((w, h), pygame.SRCALPHA)
    temp_rect = temp_surface.get_rect()

    # 绘制边框
    if border_width > 0:
        pygame.draw.rect(temp_surface, color, temp_rect, border_width, border_radius=radius)
        inner_rect = (border_width, border_width, w - 2 * border_width, h - 2 * border_width)
        radius = max(0, radius - border_width)
    else:
        inner_rect = temp_rect

    # 绘制内部圆角矩形
    pygame.draw.rect(temp_surface, color, inner_rect, 0, border_radius=radius)

    # 绘制到目标表面
    surface.blit(temp_surface, (x, y))


def draw_gradient_rect(surface: pygame.Surface, color1: Tuple[int, ...], color2: Tuple[int, ...],
                       rect: Tuple[int, ...]) -> None:
    """绘制渐变矩形"""
    x, y, w, h = map(int, rect)
    for i in range(h):
        ratio = i / h
        color = (
            int(color1[0] * (1 - ratio) + color2[0] * ratio),
            int(color1[1] * (1 - ratio) + color2[1] * ratio),
            int(color1[2] * (1 - ratio) + color2[2] * ratio)
        )
        pygame.draw.line(surface, color, (x, y + i), (x + w, y + i))


# -------------------------- 资源加载优化(支持动态缩放) --------------------------
class SpriteSheet:
    """精灵表管理类"""

    def __init__(self, frame_width: int, frame_height: int):
        self.frame_width = int(frame_width)
        self.frame_height = int(frame_height)
        self.frames: List[pygame.Surface] = []
        self.load_frames()

    def load_frames(self) -> None:
        """生成爆炸特效帧"""
        colors = [(255, 100, 0), (255, 150, 0), (255, 200, 0), (255, 255, 0), (200, 200, 0)]
        sizes = [16, 24, 32, 40, 32, 24, 16]

        for i, size in enumerate(sizes):
            frame = pygame.Surface((self.frame_width, self.frame_height), pygame.SRCALPHA)
            color = colors[i % len(colors)]
            # 绘制爆炸圆
            pygame.draw.circle(frame, color, (self.frame_width // 2, self.frame_height // 2), size // 2)
            # 添加光晕
            halo_surface = pygame.Surface((self.frame_width, self.frame_height), pygame.SRCALPHA)
            pygame.draw.circle(halo_surface, (*color, 50), (self.frame_width // 2, self.frame_height // 2), size)
            frame.blit(halo_surface, (0, 0))
            self.frames.append(frame)

    def get_frame(self, index: int, scale: Optional[Tuple[int, int]] = None) -> pygame.Surface:
        """获取指定帧"""
        if index < 0 or index >= len(self.frames):
            return pygame.Surface((self.frame_width, self.frame_height), pygame.SRCALPHA)
        frame = self.frames[index]
        if scale:
            scale = (int(scale[0]), int(scale[1]))
            frame = pygame.transform.scale(frame, scale)
        return frame


def load_resources(scale_factor: float = 1.0) -> Dict[str, Any]:
    """优化的资源加载函数(支持动态缩放)"""
    resources = {
        "images": {},
        "sprite_sheets": {}
    }

    # 加载精灵表
    resources["sprite_sheets"]["explosion"] = SpriteSheet(int(64 * scale_factor), int(64 * scale_factor))

    # 基础图片加载
    image_map = {
        "player1": (int(40 * scale_factor), int(40 * scale_factor)),
        "player2": (int(40 * scale_factor), int(40 * scale_factor)),
        "enemy": (int(40 * scale_factor), int(40 * scale_factor)),
        "boss": (int(60 * scale_factor), int(60 * scale_factor)),
        "bullet": (int(8 * scale_factor), int(8 * scale_factor)),
        "wall": (int(50 * scale_factor), int(50 * scale_factor)),
        "brick": (int(50 * scale_factor), int(50 * scale_factor)),
        "item_speed": (int(30 * scale_factor), int(30 * scale_factor)),
        "item_shield": (int(30 * scale_factor), int(30 * scale_factor)),
        "item_penetrate": (int(30 * scale_factor), int(30 * scale_factor)),
        "item_hp": (int(30 * scale_factor), int(30 * scale_factor)),
        "shield_effect": (int(50 * scale_factor), int(50 * scale_factor)),
        "bg": (int(BASE_WIDTH * scale_factor), int(BASE_HEIGHT * scale_factor))
    }

    for name, size in image_map.items():
        img = pygame.Surface(size, pygame.SRCALPHA if name != "bg" else 0)

        if name == "player1":
            img.fill(GREEN)
            pygame.draw.rect(img, DARK_GRAY, (0, 0, size[0], size[1]), 2)
            pygame.draw.rect(img, LIGHT_GRAY, (size[0] // 2 - 2, 0, 4, size[1] // 2), 0)
            pygame.draw.rect(img, (*GREEN, 100), (5, 5, size[0] - 10, size[1] - 10), 0)

        elif name == "player2":
            img.fill(BLUE)
            pygame.draw.rect(img, DARK_GRAY, (0, 0, size[0], size[1]), 2)
            pygame.draw.rect(img, LIGHT_GRAY, (size[0] // 2 - 2, 0, 4, size[1] // 2), 0)
            pygame.draw.rect(img, (*BLUE, 100), (5, 5, size[0] - 10, size[1] - 10), 0)

        elif name == "enemy":
            img.fill(RED)
            pygame.draw.rect(img, DARK_GRAY, (0, 0, size[0], size[1]), 2)
            pygame.draw.rect(img, LIGHT_GRAY, (size[0] // 2 - 2, 0, 4, size[1] // 2), 0)
            pygame.draw.rect(img, (*RED, 100), (5, 5, size[0] - 10, size[1] - 10), 0)

        elif name == "boss":
            img.fill(PURPLE)
            pygame.draw.rect(img, DARK_GRAY, (0, 0, size[0], size[1]), 3)
            pygame.draw.rect(img, LIGHT_GRAY, (size[0] // 2 - 3, 0, 6, size[1] // 2), 0)
            pygame.draw.rect(img, (*PURPLE, 100), (8, 8, size[0] - 16, size[1] - 16), 0)

        elif name == "bullet":
            draw_gradient_rect(img, YELLOW, ORANGE, (0, 0, size[0], size[1]))
            pygame.draw.circle(img, WHITE, (size[0] // 2, size[1] // 2), size[0] // 4)

        elif name == "wall":
            img.fill(GRAY)
            pygame.draw.rect(img, DARK_GRAY, (0, 0, size[0], size[1]), 2)
            for i in range(0, int(size[0]), 8):
                pygame.draw.line(img, DARK_GRAY, (i, 0), (i, size[1]), 1)
                pygame.draw.line(img, DARK_GRAY, (0, i), (size[0], i), 1)

        elif name == "brick":
            img.fill(ORANGE)
            pygame.draw.rect(img, DARK_GRAY, (0, 0, size[0], size[1]), 2)
            pygame.draw.line(img, DARK_GRAY, (0, size[1] // 2), (size[0], size[1] // 2), 2)
            pygame.draw.line(img, DARK_GRAY, (size[0] // 2, 0), (size[0] // 2, size[1]), 2)

        elif name.startswith("item"):
            if "speed" in name:
                draw_gradient_rect(img, YELLOW, ORANGE, (0, 0, size[0], size[1]))
                pygame.draw.rect(img, WHITE, (size[0] // 4, size[1] // 4, size[0] // 2, size[1] // 2), 2)
            elif "shield" in name:
                draw_gradient_rect(img, BLUE, CYAN, (0, 0, size[0], size[1]))
                pygame.draw.circle(img, WHITE, (size[0] // 2, size[1] // 2), size[0] // 4, 2)
            elif "penetrate" in name:
                draw_gradient_rect(img, RED, ORANGE, (0, 0, size[0], size[1]))
                pygame.draw.line(img, WHITE, (size[0] // 4, size[1] // 4), (size[0] * 3 // 4, size[1] * 3 // 4), 2)
            elif "hp" in name:
                draw_gradient_rect(img, GREEN, CYAN, (0, 0, size[0], size[1]))
                pygame.draw.polygon(img, WHITE, [(size[0] // 2, size[1] // 4),
                                                 (size[0] * 3 // 4, size[1] * 3 // 4),
                                                 (size[0] // 4, size[1] * 3 // 4)])
            pygame.draw.rect(img, WHITE, (0, 0, size[0], size[1]), 2)

        elif name == "shield_effect":
            pygame.draw.circle(img, TRANSPARENT_BLUE, (size[0] // 2, size[1] // 2), size[0] // 2)
            pygame.draw.circle(img, WHITE, (size[0] // 2, size[1] // 2), size[0] // 2, 1)

        elif name == "bg":
            draw_gradient_rect(img, (10, 10, 30), (30, 30, 60), (0, 0, size[0], size[1]))
            grid_color = (20, 20, 40)
            grid_size = int(40 * scale_factor)
            for x in range(0, int(size[0]), grid_size):
                pygame.draw.line(img, grid_color, (x, 0), (x, size[1]), 1)
            for y in range(0, int(size[1]), grid_size):
                pygame.draw.line(img, grid_color, (0, y), (size[0], y), 1)

        resources["images"][name] = img

    return resources


# 初始加载资源
resources = load_resources(SCALE_FACTOR)

# -------------------------- 按键配置系统 --------------------------
KEY_CONFIG_PATH = os.path.join(CONFIG_DIR, "key_config.json")
DEFAULT_KEY_CONFIG = {
    "player1": {
        "up": pygame.K_w,
        "down": pygame.K_s,
        "left": pygame.K_a,
        "right": pygame.K_d,
        "shoot": pygame.K_SPACE
    },
    "player2": {
        "up": pygame.K_UP,
        "down": pygame.K_DOWN,
        "left": pygame.K_LEFT,
        "right": pygame.K_RIGHT,
        "shoot": pygame.K_RETURN
    }
}
key_config = load_json(KEY_CONFIG_PATH, DEFAULT_KEY_CONFIG)


class KeyConfigWindow:
    """完整按键设置窗口"""

    def __init__(self, parent: tk.Tk):
        self.window = tk.Toplevel(parent)
        self.window.title("按键设置")
        self.window.geometry("500x400")
        self.window.resizable(False, False)
        self.window.grab_set()

        # 当前正在设置的按键
        self.current_setting: Optional[Tuple[str, str, tk.StringVar]] = None
        self.key_vars = {
            "player1_up": tk.StringVar(value=get_key_name(key_config["player1"]["up"])),
            "player1_down": tk.StringVar(value=get_key_name(key_config["player1"]["down"])),
            "player1_left": tk.StringVar(value=get_key_name(key_config["player1"]["left"])),
            "player1_right": tk.StringVar(value=get_key_name(key_config["player1"]["right"])),
            "player1_shoot": tk.StringVar(value=get_key_name(key_config["player1"]["shoot"])),
            "player2_up": tk.StringVar(value=get_key_name(key_config["player2"]["up"])),
            "player2_down": tk.StringVar(value=get_key_name(key_config["player2"]["down"])),
            "player2_left": tk.StringVar(value=get_key_name(key_config["player2"]["left"])),
            "player2_right": tk.StringVar(value=get_key_name(key_config["player2"]["right"])),
            "player2_shoot": tk.StringVar(value=get_key_name(key_config["player2"]["shoot"]))
        }

        # 绑定按键事件
        self.window.bind("<KeyPress>", self.on_key_press)
        self.window.bind("<Escape>", self.cancel_setting)

        # 创建UI
        self.create_widgets()

    def create_widgets(self) -> None:
        """创建按键设置UI"""
        # 玩家1设置区域
        frame1 = ttk.LabelFrame(self.window, text="玩家1")
        frame1.pack(padx=10, pady=5, fill="x")

        ttk.Label(frame1, text="上移:").grid(row=0, column=0, padx=5, pady=5)
        self.btn_p1_up = ttk.Button(frame1, textvariable=self.key_vars["player1_up"],
                                    command=lambda: self.start_setting("player1", "up", self.key_vars["player1_up"]))
        self.btn_p1_up.grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(frame1, text="下移:").grid(row=0, column=2, padx=5, pady=5)
        self.btn_p1_down = ttk.Button(frame1, textvariable=self.key_vars["player1_down"],
                                      command=lambda: self.start_setting("player1", "down",
                                                                         self.key_vars["player1_down"]))
        self.btn_p1_down.grid(row=0, column=3, padx=5, pady=5)

        ttk.Label(frame1, text="左移:").grid(row=1, column=0, padx=5, pady=5)
        self.btn_p1_left = ttk.Button(frame1, textvariable=self.key_vars["player1_left"],
                                      command=lambda: self.start_setting("player1", "left",
                                                                         self.key_vars["player1_left"]))
        self.btn_p1_left.grid(row=1, column=1, padx=5, pady=5)

        ttk.Label(frame1, text="右移:").grid(row=1, column=2, padx=5, pady=5)
        self.btn_p1_right = ttk.Button(frame1, textvariable=self.key_vars["player1_right"],
                                       command=lambda: self.start_setting("player1", "right",
                                                                          self.key_vars["player1_right"]))
        self.btn_p1_right.grid(row=1, column=3, padx=5, pady=5)

        ttk.Label(frame1, text="射击:").grid(row=2, column=0, padx=5, pady=5)
        self.btn_p1_shoot = ttk.Button(frame1, textvariable=self.key_vars["player1_shoot"],
                                       command=lambda: self.start_setting("player1", "shoot",
                                                                          self.key_vars["player1_shoot"]))
        self.btn_p1_shoot.grid(row=2, column=1, padx=5, pady=5)

        # 玩家2设置区域
        frame2 = ttk.LabelFrame(self.window, text="玩家2")
        frame2.pack(padx=10, pady=5, fill="x")

        ttk.Label(frame2, text="上移:").grid(row=0, column=0, padx=5, pady=5)
        self.btn_p2_up = ttk.Button(frame2, textvariable=self.key_vars["player2_up"],
                                    command=lambda: self.start_setting("player2", "up", self.key_vars["player2_up"]))
        self.btn_p2_up.grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(frame2, text="下移:").grid(row=0, column=2, padx=5, pady=5)
        self.btn_p2_down = ttk.Button(frame2, textvariable=self.key_vars["player2_down"],
                                      command=lambda: self.start_setting("player2", "down",
                                                                         self.key_vars["player2_down"]))
        self.btn_p2_down.grid(row=0, column=3, padx=5, pady=5)

        ttk.Label(frame2, text="左移:").grid(row=1, column=0, padx=5, pady=5)
        self.btn_p2_left = ttk.Button(frame2, textvariable=self.key_vars["player2_left"],
                                      command=lambda: self.start_setting("player2", "left",
                                                                         self.key_vars["player2_left"]))
        self.btn_p2_left.grid(row=1, column=1, padx=5, pady=5)

        ttk.Label(frame2, text="右移:").grid(row=1, column=2, padx=5, pady=5)
        self.btn_p2_right = ttk.Button(frame2, textvariable=self.key_vars["player2_right"],
                                       command=lambda: self.start_setting("player2", "right",
                                                                          self.key_vars["player2_right"]))
        self.btn_p2_right.grid(row=1, column=3, padx=5, pady=5)

        ttk.Label(frame2, text="射击:").grid(row=2, column=0, padx=5, pady=5)
        self.btn_p2_shoot = ttk.Button(frame2, textvariable=self.key_vars["player2_shoot"],
                                       command=lambda: self.start_setting("player2", "shoot",
                                                                          self.key_vars["player2_shoot"]))
        self.btn_p2_shoot.grid(row=2, column=1, padx=5, pady=5)

        # 按钮区域
        btn_frame = ttk.Frame(self.window)
        btn_frame.pack(padx=10, pady=10, fill="x")

        ttk.Button(btn_frame, text="恢复默认", command=self.reset_default).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="保存设置", command=self.save_settings).pack(side="right", padx=5)

    def start_setting(self, player: str, action: str, var: tk.StringVar) -> None:
        """开始设置指定按键"""
        self.current_setting = (player, action, var)
        var.set("请按下按键...(ESC取消)")
        self.window.focus_set()

    def cancel_setting(self, event: tk.Event) -> None:
        """取消当前按键设置"""
        if self.current_setting:
            player, action, var = self.current_setting
            var.set(get_key_name(key_config[player][action]))
            self.current_setting = None

    def on_key_press(self, event: tk.Event) -> None:
        """处理tkinter按键事件"""
        if not self.current_setting:
            return

        # TK按键转Pygame按键码
        tk_to_pygame = {
            # 字母键
            "w": pygame.K_w, "s": pygame.K_s, "a": pygame.K_a, "d": pygame.K_d,
            "a": pygame.K_a, "b": pygame.K_b, "c": pygame.K_c, "d": pygame.K_d,
            "e": pygame.K_e, "f": pygame.K_f, "g": pygame.K_g, "h": pygame.K_h,
            "i": pygame.K_i, "j": pygame.K_j, "k": pygame.K_k, "l": pygame.K_l,
            "m": pygame.K_m, "n": pygame.K_n, "o": pygame.K_o, "p": pygame.K_p,
            "q": pygame.K_q, "r": pygame.K_r, "t": pygame.K_t, "u": pygame.K_u,
            "v": pygame.K_v, "w": pygame.K_w, "x": pygame.K_x, "y": pygame.K_y, "z": pygame.K_z,
            # 功能键
            "space": pygame.K_SPACE, "Up": pygame.K_UP, "Down": pygame.K_DOWN,
            "Left": pygame.K_LEFT, "Right": pygame.K_RIGHT, "Return": pygame.K_RETURN,
            "Escape": pygame.K_ESCAPE, "Shift_L": pygame.K_LSHIFT, "Shift_R": pygame.K_RSHIFT,
            "Control_L": pygame.K_LCTRL, "Control_R": pygame.K_RCTRL, "Tab": pygame.K_TAB,
            # 数字键
            "0": pygame.K_0, "1": pygame.K_1, "2": pygame.K_2, "3": pygame.K_3,
            "4": pygame.K_4, "5": pygame.K_5, "6": pygame.K_6, "7": pygame.K_7,
            "8": pygame.K_8, "9": pygame.K_9,
        }

        # 跳过ESC(用于取消)
        if event.keysym == "Escape":
            self.cancel_setting(event)
            return

        key_code = tk_to_pygame.get(event.keysym)
        if not key_code:
            self.current_setting[2].set("不支持的按键")
            self.current_setting = None
            return

        # 检查按键冲突
        player, action, var = self.current_setting
        conflict = False
        for p in key_config:
            for a in key_config[p]:
                if p == player and a == action:
                    continue
                if key_config[p][a] == key_code:
                    conflict = True
                    break
            if conflict:
                break

        if conflict:
            var.set("按键已被占用")
            self.current_setting = None
            return

        # 更新按键配置
        key_config[player][action] = key_code
        var.set(get_key_name(key_code))
        self.current_setting = None

    def reset_default(self) -> None:
        """恢复默认设置"""
        global key_config
        key_config = DEFAULT_KEY_CONFIG.copy()

        # 更新UI显示
        self.key_vars["player1_up"].set(get_key_name(DEFAULT_KEY_CONFIG["player1"]["up"]))
        self.key_vars["player1_down"].set(get_key_name(DEFAULT_KEY_CONFIG["player1"]["down"]))
        self.key_vars["player1_left"].set(get_key_name(DEFAULT_KEY_CONFIG["player1"]["left"]))
        self.key_vars["player1_right"].set(get_key_name(DEFAULT_KEY_CONFIG["player1"]["right"]))
        self.key_vars["player1_shoot"].set(get_key_name(DEFAULT_KEY_CONFIG["player1"]["shoot"]))
        self.key_vars["player2_up"].set(get_key_name(DEFAULT_KEY_CONFIG["player2"]["up"]))
        self.key_vars["player2_down"].set(get_key_name(DEFAULT_KEY_CONFIG["player2"]["down"]))
        self.key_vars["player2_left"].set(get_key_name(DEFAULT_KEY_CONFIG["player2"]["left"]))
        self.key_vars["player2_right"].set(get_key_name(DEFAULT_KEY_CONFIG["player2"]["right"]))
        self.key_vars["player2_shoot"].set(get_key_name(DEFAULT_KEY_CONFIG["player2"]["shoot"]))

    def save_settings(self) -> None:
        """保存按键设置"""
        save_json(KEY_CONFIG_PATH, key_config)
        messagebox.showinfo("成功", "按键设置已保存!")
        self.window.destroy()


# -------------------------- 存档系统 --------------------------
SAVE_FILE_PATH = os.path.join(SAVE_DIR, "game_save.json")
DEFAULT_SAVE_DATA = {
    "high_scores": [],
    "last_progress": {
        "score": 0,
        "level": 0,
        "stage": 1,
        "difficulty": "normal"
    }
}
save_data = load_json(SAVE_FILE_PATH, DEFAULT_SAVE_DATA)


def update_high_score(new_score: int) -> None:
    """更新高分榜(去重+排序)"""
    if new_score not in save_data["high_scores"]:
        save_data["high_scores"].append(new_score)
    save_data["high_scores"] = sorted(save_data["high_scores"], reverse=True)[:10]
    save_game_data()


def save_game_progress(score: int, level: int, stage: int, difficulty: str) -> None:
    """保存游戏进度"""
    save_data["last_progress"] = {
        "score": score,
        "level": level,
        "stage": stage,
        "difficulty": difficulty
    }
    save_game_data()


def save_game_data() -> None:
    """保存所有游戏数据"""
    save_json(SAVE_FILE_PATH, save_data)


def load_game_progress() -> Dict[str, Any]:
    """加载游戏进度"""
    return save_data["last_progress"]


# -------------------------- 游戏类 --------------------------
class Tank(pygame.sprite.Sprite):
    """优化的坦克类"""

    def __init__(self, x: int, y: int, color: Tuple[int, ...], is_player: bool = False,
                 player_id: int = 1, is_boss: bool = False, remote: bool = False):
        super().__init__()
        self.layer = "tanks"
        sprite_layers[self.layer].add(self)

        # 基础属性
        self.is_player = is_player
        self.player_id = player_id
        self.is_boss = is_boss
        self.remote = remote
        self.width = int(60 * SCALE_FACTOR) if is_boss else int(40 * SCALE_FACTOR)
        self.height = int(60 * SCALE_FACTOR) if is_boss else int(40 * SCALE_FACTOR)

        # 图形
        if is_boss:
            self.image = resources["images"]["boss"]
        elif is_player:
            self.image = resources["images"][f"player{player_id}"]
        else:
            self.image = resources["images"]["enemy"]

        self.rect = self.image.get_rect(center=(int(x), int(y)))
        self.original_image = self.image.copy()

        # 移动属性
        speed_coeff = DIFFICULTY_CONFIG[game_state["difficulty"]]["speed_coeff"]
        self.speed = BASE_TANK_SPEED * (BOSS_SPEED_COEFF if is_boss else speed_coeff)
        self.direction = "up"
        self.movement = {"up": False, "down": False, "left": False, "right": False}

        # 战斗属性
        self.hp = 3 if is_player else 1
        self.max_hp = 3 if is_player else 1
        if is_boss:
            self.hp = DIFFICULTY_CONFIG[game_state["difficulty"]]["boss_hp"]
            self.max_hp = self.hp
        self.shoot_cooldown = 15
        self.current_cooldown = 0
        self.invulnerable = False
        self.invulnerable_end_time = 0  # 无敌状态结束时间

        # 道具效果
        self.item_effects = {
            "speed": False, "shield": False, "penetrate": False, "hp": False
        }
        self.item_timers: Dict[str, int] = {}

        # BOSS专属属性
        self.boss_attack_pattern = 0
        self.boss_attack_cooldown = 0
        self.last_move_time = pygame.time.get_ticks()
        self.last_rotate_time = pygame.time.get_ticks()  # 防止频繁旋转

    def rotate(self, direction: str) -> None:
        """坦克旋转(增加冷却,防止频繁旋转)"""
        current_time = pygame.time.get_ticks()
        if direction == self.direction or current_time - self.last_rotate_time < 50:
            return

        self.direction = direction
        self.last_rotate_time = current_time
        angle_map = {"up": 0, "right": 90, "down": 180, "left": 270}
        angle = angle_map.get(direction, 0)

        self.image = pygame.transform.rotate(self.original_image, angle)
        self.rect = self.image.get_rect(center=self.rect.center)

    def move(self, obstacles: List[Any]) -> None:
        """坦克移动"""
        if self.remote:
            return

        # 计算移动向量
        dx, dy = 0, 0
        if self.movement["up"]: dy -= self.speed
        if self.movement["down"]: dy += self.speed
        if self.movement["left"]: dx -= self.speed
        if self.movement["right"]: dx += self.speed

        # 边界检测
        new_rect = self.rect.copy()
        new_rect.x += dx
        new_rect.y += dy

        # 防止出界
        new_rect.left = max(0, new_rect.left)
        new_rect.right = min(SCREEN_WIDTH, new_rect.right)
        new_rect.top = max(0, new_rect.top)
        new_rect.bottom = min(SCREEN_HEIGHT, new_rect.bottom)

        # 碰撞检测(分步检测,避免卡住)
        collision_x = False
        collision_y = False

        # 先检测X轴碰撞
        temp_rect = self.rect.copy()
        temp_rect.x = new_rect.x
        for obstacle in obstacles:
            if temp_rect.colliderect(obstacle.rect):
                collision_x = True
                break

        # 再检测Y轴碰撞
        temp_rect = self.rect.copy()
        temp_rect.y = new_rect.y
        for obstacle in obstacles:
            if temp_rect.colliderect(obstacle.rect):
                collision_y = True
                break

        # 只移动未碰撞的轴
        if not collision_x:
            self.rect.x = new_rect.x
        if not collision_y:
            self.rect.y = new_rect.y

    def shoot(self) -> Optional["Bullet"]:
        """射击(支持道具效果)"""
        if self.current_cooldown > 0:
            self.current_cooldown -= 1
            return None

        # 双倍射速效果
        cooldown = self.shoot_cooldown // 2 if self.item_effects["speed"] else self.shoot_cooldown
        self.current_cooldown = cooldown

        # 计算子弹起始位置
        bullet_x = self.rect.centerx
        bullet_y = self.rect.centery
        bullet_dir = self.direction

        # 调整子弹位置到炮口
        offset = self.width // 2
        if bullet_dir == "up":
            bullet_y -= offset
        elif bullet_dir == "down":
            bullet_y += offset
        elif bullet_dir == "left":
            bullet_x -= offset
        elif bullet_dir == "right":
            bullet_x += offset

        # 创建子弹
        bullet = Bullet(bullet_x, bullet_y, bullet_dir,
                        is_player=self.is_player,
                        penetrate=self.item_effects["penetrate"])

        # 联机模式同步
        if online_mode and self.is_player and not self.remote:
            send_bullet_data(bullet)

        return bullet

    def update_item_effects(self) -> None:
        """更新道具效果(修复计时逻辑)"""
        current_time = pygame.time.get_ticks()
        for item_type in list(self.item_effects.keys()):
            if self.item_effects[item_type]:
                if current_time > self.item_timers.get(item_type, 0):
                    self.item_effects[item_type] = False
                    if item_type == "shield":
                        self.invulnerable = False
                        self.invulnerable_end_time = 0

    def take_damage(self, damage: int = 1) -> bool:
        """受到伤害(支持护盾)"""
        current_time = pygame.time.get_ticks()
        if self.invulnerable or self.item_effects["shield"] or current_time < self.invulnerable_end_time:
            return False

        self.hp -= damage
        if self.hp <= 0:
            # 增加分数
            if not self.is_player:
                game_state["score"] += 100 if self.is_boss else 10
            self.kill()
            Explosion(self.rect.centerx, self.rect.centery)
            return True

        # 受伤后短暂无敌
        self.invulnerable_end_time = current_time + 500
        return False

    def apply_item(self, item_type: str) -> None:
        """应用道具效果"""
        self.item_effects[item_type] = True
        self.item_timers[item_type] = pygame.time.get_ticks() + ITEM_DURATION

        if item_type == "shield":
            self.invulnerable = True
            self.invulnerable_end_time = pygame.time.get_ticks() + ITEM_DURATION
        elif item_type == "hp":
            self.hp = min(self.max_hp, self.hp + 1)

    def update_boss_ai(self, obstacles: List[Any]) -> None:
        """优化的BOSS AI"""
        current_time = pygame.time.get_ticks()

        # 寻找最近的玩家
        target = None
        min_dist = float("inf")
        for tank in sprite_layers["tanks"]:
            if tank.is_player and not tank.remote and tank.hp > 0:
                dist = calculate_distance(self.rect.center, tank.rect.center)
                if dist < min_dist:
                    min_dist = dist
                    target = tank

        if not target:
            # 随机移动(防止卡住)
            if current_time - self.last_move_time > 1000:
                self.direction = random.choice(["up", "down", "left", "right"])
                self.rotate(self.direction)
                self.movement = {
                    "up": self.direction == "up",
                    "down": self.direction == "down",
                    "left": self.direction == "left",
                    "right": self.direction == "right"
                }
                self.last_move_time = current_time
            self.move(obstacles)
            return

        # 朝向玩家
        dx = target.rect.centerx - self.rect.centerx
        dy = target.rect.centery - self.rect.centery

        if abs(dx) > abs(dy):
            new_dir = "right" if dx > 0 else "left"
        else:
            new_dir = "down" if dy > 0 else "up"

        self.rotate(new_dir)

        # 移动(带避障和冷却)
        if current_time - self.last_move_time > 500:
            self.movement = {
                "up": new_dir == "up",
                "down": new_dir == "down",
                "left": new_dir == "left",
                "right": new_dir == "right"
            }
            self.last_move_time = current_time

        self.move(obstacles)

        # BOSS射击
        if self.boss_attack_cooldown <= 0 and min_dist < 300 * SCALE_FACTOR:
            self.shoot()
            self.boss_attack_cooldown = BOSS_SHOOT_COOLDOWN

    def update(self, obstacles: List[Any]) -> None:
        """坦克主更新逻辑"""
        if self.is_boss:
            self.update_boss_ai(obstacles)
        else:
            self.move(obstacles)

        self.update_item_effects()

        # BOSS攻击冷却
        if self.is_boss and self.boss_attack_cooldown > 0:
            self.boss_attack_cooldown -= 1


class Bullet(pygame.sprite.Sprite):
    """子弹类"""

    def __init__(self, x: int, y: int, direction: str, is_player: bool = False, penetrate: bool = False):
        super().__init__()
        self.layer = "bullets"
        sprite_layers[self.layer].add(self)

        self.image = resources["images"]["bullet"]
        self.rect = self.image.get_rect(center=(int(x), int(y)))
        self.direction = direction
        self.speed = BASE_BULLET_SPEED
        self.is_player = is_player
        self.penetrate = penetrate
        self.hit_obstacles: List[Any] = []
        self.hit_tanks: List[Any] = []
        self.lifetime = 0  # 子弹生命周期,防止无限飞行
        self.max_lifetime = int(1000 / (self.speed / 2))  # 最大飞行帧数

    def update(self, obstacles: List[Any]) -> None:
        """子弹移动和碰撞检测(修复穿透逻辑)"""
        self.lifetime += 1
        if self.lifetime > self.max_lifetime:
            self.kill()
            return

        # 移动
        move_map = {
            "up": (0, -self.speed),
            "down": (0, self.speed),
            "left": (-self.speed, 0),
            "right": (self.speed, 0)
        }
        dx, dy = move_map.get(self.direction, (0, 0))
        self.rect.x += dx
        self.rect.y += dy

        # 边界检测
        if (self.rect.bottom < 0 or self.rect.top > SCREEN_HEIGHT or
                self.rect.right < 0 or self.rect.left > SCREEN_WIDTH):
            self.kill()
            return

        # 1. 障碍物碰撞
        for obstacle in obstacles:
            if obstacle in self.hit_obstacles:
                continue
            if self.rect.colliderect(obstacle.rect):
                obstacle.take_damage()
                self.hit_obstacles.append(obstacle)
                if not self.penetrate:
                    self.kill()
                    return

        # 2. 坦克碰撞
        for tank in sprite_layers["tanks"]:
            if tank in self.hit_tanks:
                continue
            # 友军不伤害
            if (tank.is_player and self.is_player) or (not tank.is_player and not self.is_player):
                continue

            if self.rect.colliderect(tank.rect):
                tank.take_damage()
                self.hit_tanks.append(tank)
                if not self.penetrate:
                    self.kill()
                    return


class Item(pygame.sprite.Sprite):
    """道具类"""

    def __init__(self, x: int, y: int, item_type: str):
        super().__init__()
        self.layer = "items"
        sprite_layers[self.layer].add(self)

        self.item_type = item_type
        self.image = resources["images"][f"item_{item_type}"]
        self.rect = self.image.get_rect(center=(int(x), int(y)))
        self.spawn_time = pygame.time.get_ticks()
        self.lifetime = 10000  # 10秒

    def update(self, players: List[Tank]) -> None:
        """道具更新和拾取检测"""
        # 超时消失
        if pygame.time.get_ticks() - self.spawn_time > self.lifetime:
            self.kill()
            return

        # 玩家拾取
        for player in players:
            if player.hp > 0 and self.rect.colliderect(player.rect):
                player.apply_item(self.item_type)
                # 联机同步
                if online_mode:
                    if is_server:
                        # 服务器端广播拾取消息
                        broadcast_data(json.dumps({
                            "type": "item_pickup",
                            "item_type": self.item_type,
                            "x": self.rect.centerx,
                            "y": self.rect.centery
                        }).encode())
                    else:
                        # 客户端发送拾取消息
                        send_item_pickup(self)
                self.kill()
                break


class Explosion(pygame.sprite.Sprite):
    """爆炸特效类"""

    def __init__(self, x: int, y: int):
        super().__init__()
        self.layer = "effects"
        sprite_layers[self.layer].add(self)

        self.x = int(x)
        self.y = int(y)
        self.frame_index = 0
        self.frame_speed = 5
        self.current_frame = 0
        self.sprite_sheet = resources["sprite_sheets"]["explosion"]
        self.image = self.sprite_sheet.get_frame(0, (int(64 * SCALE_FACTOR), int(64 * SCALE_FACTOR)))
        self.rect = self.image.get_rect(center=(self.x, self.y))

    def update(self) -> None:
        """更新爆炸帧"""
        self.current_frame += 1
        if self.current_frame >= self.frame_speed:
            self.frame_index += 1
            self.current_frame = 0
            if self.frame_index >= len(self.sprite_sheet.frames):
                self.kill()
                return
            self.image = self.sprite_sheet.get_frame(self.frame_index, (int(64 * SCALE_FACTOR), int(64 * SCALE_FACTOR)))
            self.rect = self.image.get_rect(center=(self.x, self.y))


class Obstacle(pygame.sprite.Sprite):
    """障碍物类(墙壁/砖块)"""

    def __init__(self, x: int, y: int, obstacle_type: str = "wall"):
        super().__init__()
        self.layer = "obstacles"
        sprite_layers[self.layer].add(self)

        self.obstacle_type = obstacle_type
        self.image = resources["images"][obstacle_type]
        self.rect = self.image.get_rect(topleft=(int(x), int(y)))
        self.hp = 1 if obstacle_type == "brick" else float("inf")

    def take_damage(self, damage: int = 1) -> None:
        """受到伤害"""
        if self.obstacle_type == "wall":
            return

        self.hp -= damage
        if self.hp <= 0:
            Explosion(self.rect.centerx, self.rect.centery)
            self.kill()


# -------------------------- 联机功能实现 --------------------------
def start_server() -> None:
    """启动服务器(线程安全)"""
    global server_socket, online_mode, is_server
    is_server = True
    online_mode = True

    try:
        # 创建服务器套接字
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind((SERVER_IP, SERVER_PORT))
        server_socket.listen(5)
        server_socket.settimeout(1.0)  # 设置超时,避免阻塞

        # 启动客户端监听线程
        threading.Thread(target=accept_clients, daemon=True).start()
        # 启动心跳包线程
        threading.Thread(target=server_heartbeat, daemon=True).start()

        print(f"服务器启动成功: {SERVER_IP}:{SERVER_PORT}")
    except Exception as e:
        print(f"服务器启动失败: {e}")
        online_mode = False
        is_server = False
        if server_socket:
            server_socket.close()
            server_socket = None


def accept_clients() -> None:
    """接受客户端连接(修复线程安全)"""
    global online_mode, is_server, server_socket
    while online_mode and is_server and server_socket:
        try:
            client_sock, addr = server_socket.accept()
            client_sock.settimeout(5.0)

            with online_players_lock:
                # 生成唯一玩家ID
                player_id = 1
                while player_id in server_online_players:
                    player_id += 1

                # 创建远程玩家坦克
                spawn_x = random.randint(100, SCREEN_WIDTH - 100)
                spawn_y = random.randint(100, SCREEN_HEIGHT - 100)
                tank = Tank(spawn_x, spawn_y, BLUE if player_id == 2 else GREEN,
                            is_player=True, player_id=player_id, remote=True)

                server_online_players[player_id] = {
                    "socket": client_sock,
                    "addr": addr,
                    "last_heartbeat": time.time(),
                    "tank": tank
                }

            # 发送玩家ID
            client_sock.send(json.dumps({
                "type": "player_id",
                "id": player_id,
                "spawn_x": spawn_x,
                "spawn_y": spawn_y
            }).encode())

            # 启动客户端处理线程
            threading.Thread(target=handle_client, args=(client_sock, player_id), daemon=True).start()
            print(f"玩家 {player_id} 连接: {addr}")
        except socket.timeout:
            continue
        except Exception as e:
            if online_mode and is_server:
                print(f"接受客户端失败: {e}")
            break


def handle_client(client_sock: socket.socket, player_id: int) -> None:
    """处理客户端数据(修复数据解析和同步)"""
    global online_mode, is_server
    while online_mode and is_server:
        try:
            data = client_sock.recv(BUFFER_SIZE)
            if not data:
                break

            # 解析数据
            msg = json.loads(data.decode())

            with online_players_lock:
                if player_id not in server_online_players:
                    break

                if msg["type"] == "heartbeat":
                    server_online_players[player_id]["last_heartbeat"] = time.time()

                elif msg["type"] == "tank_pos":
                    # 更新坦克位置
                    tank = server_online_players[player_id]["tank"]
                    if tank and tank.alive():
                        tank.rect.x = int(msg["x"])
                        tank.rect.y = int(msg["y"])
                        tank.direction = msg["dir"]
                        tank.rotate(msg["dir"])

                elif msg["type"] == "bullet":
                    # 创建同步子弹
                    bullet = Bullet(int(msg["x"]), int(msg["y"]), msg["dir"], is_player=True)
                    online_bullets[id(bullet)] = player_id

                elif msg["type"] == "item_pickup":
                    # 同步道具拾取
                    for item in sprite_layers["items"]:
                        if (item.item_type == msg["item_type"] and
                                abs(item.rect.centerx - int(msg["x"])) < 5 and
                                abs(item.rect.centery - int(msg["y"])) < 5):
                            item.kill()
                            break

            # 广播数据给其他玩家
            broadcast_data(data, exclude=client_sock)
        except json.JSONDecodeError:
            print(f"玩家 {player_id} 数据格式错误")
            break
        except Exception as e:
            print(f"处理客户端 {player_id} 数据失败: {e}")
            break

    # 清理断开的客户端
    with online_players_lock:
        if player_id in server_online_players:
            # 清理坦克
            tank = server_online_players[player_id]["tank"]
            if tank and tank.alive():
                tank.kill()
            del server_online_players[player_id]

    try:
        client_sock.close()
    except:
        pass
    print(f"玩家 {player_id} 断开连接")


def broadcast_data(data: bytes, exclude: Optional[socket.socket] = None) -> None:
    """广播数据给所有客户端(线程安全,修复失效连接)"""
    with online_players_lock:
        # 遍历副本,避免迭代时修改
        players_copy = list(server_online_players.values())
        for player in players_copy:
            sock = player["socket"]
            if sock != exclude and sock:
                try:
                    sock.send(data)
                except:
                    # 移除失效连接
                    continue


def server_heartbeat() -> None:
    """服务器心跳包(修复超时清理)"""
    global online_mode, is_server
    while online_mode and is_server:
        try:
            current_time = time.time()
            to_remove = []

            # 清理超时玩家
            with online_players_lock:
                for pid, player in server_online_players.items():
                    if current_time - player["last_heartbeat"] > 5:
                        to_remove.append(pid)

                for pid in to_remove:
                    try:
                        server_online_players[pid]["socket"].close()
                    except:
                        pass
                    tank = server_online_players[pid]["tank"]
                    if tank and tank.alive():
                        tank.kill()
                    del server_online_players[pid]

            # 发送心跳包
            heartbeat_msg = json.dumps({
                "type": "heartbeat",
                "players": len(server_online_players)
            }).encode()
            broadcast_data(heartbeat_msg)

            game_state["online_players"] = len(server_online_players)
            time.sleep(HEARTBEAT_INTERVAL)
        except Exception as e:
            print(f"心跳包错误: {e}")
            time.sleep(HEARTBEAT_INTERVAL)


def connect_to_server() -> bool:
    """连接到服务器(修复连接稳定性)"""
    global client_socket, online_mode, is_server
    is_server = False
    online_mode = True

    try:
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.settimeout(5.0)
        client_socket.connect((SERVER_IP, SERVER_PORT))

        # 启动接收线程
        threading.Thread(target=receive_server_data, daemon=True).start()
        # 启动心跳包线程
        threading.Thread(target=client_heartbeat, daemon=True).start()

        print(f"连接服务器成功: {SERVER_IP}:{SERVER_PORT}")
        return True
    except Exception as e:
        print(f"连接服务器失败: {e}")
        online_mode = False
        if client_socket:
            client_socket.close()
            client_socket = None
        return False


def receive_server_data() -> None:
    """接收服务器数据(修复道具同步和远程玩家)"""
    global online_mode, is_server, client_socket, client_online_players
    while online_mode and not is_server and client_socket:
        try:
            data = client_socket.recv(BUFFER_SIZE)
            if not data:
                break

            msg = json.loads(data.decode())

            if msg["type"] == "player_id":
                game_state["player_id"] = msg["id"]
                # 创建本地玩家坦克
                Tank(msg["spawn_x"], msg["spawn_y"],
                     GREEN if msg["id"] == 1 else BLUE,
                     is_player=True, player_id=msg["id"])

            elif msg["type"] == "tank_pos":
                # 更新远程玩家位置
                pid = msg["pid"]
                if pid == game_state.get("player_id", 1):
                    continue  # 跳过自己

                if pid not in client_online_players:
                    # 创建远程玩家坦克
                    tank = Tank(int(msg["x"]), int(msg["y"]),
                                BLUE if pid == 2 else GREEN,
                                is_player=True, player_id=pid, remote=True)
                    client_online_players[pid] = tank
                else:
                    tank = client_online_players[pid]
                    if tank.alive():
                        tank.rect.x = int(msg["x"])
                        tank.rect.y = int(msg["y"])
                        tank.direction = msg["dir"]
                        tank.rotate(msg["dir"])

            elif msg["type"] == "bullet":
                # 创建远程子弹
                Bullet(int(msg["x"]), int(msg["y"]), msg["dir"], is_player=True)

            elif msg["type"] == "item_pickup":
                # 同步道具拾取
                for item in sprite_layers["items"]:
                    if (item.item_type == msg["item_type"] and
                            abs(item.rect.centerx - int(msg["x"])) < 5 and
                            abs(item.rect.centery - int(msg["y"])) < 5):
                        item.kill()
                        break

            elif msg["type"] == "heartbeat":
                # 更新在线玩家数
                game_state["online_players"] = msg["players"]
        except json.JSONDecodeError:
            print("服务器数据格式错误")
            continue
        except Exception as e:
            print(f"接收服务器数据失败: {e}")
            break

    # 断开连接清理
    online_mode = False
    if client_socket:
        try:
            client_socket.close()
        except:
            pass
        client_socket = None

    # 清理远程玩家
    with online_players_lock:
        for tank in client_online_players.values():
            if tank.alive():
                tank.kill()
        client_online_players.clear()

    print("与服务器断开连接")


def client_heartbeat() -> None:
    """客户端心跳包(修复稳定性)"""
    global online_mode, is_server, client_socket
    while online_mode and not is_server and client_socket:
        try:
            heartbeat_msg = json.dumps({"type": "heartbeat"}).encode()
            client_socket.send(heartbeat_msg)
            time.sleep(HEARTBEAT_INTERVAL)
        except:
            break


def send_tank_data(tank: Tank) -> None:
    """发送坦克位置数据(线程安全)"""
    if not online_mode or is_server or tank.remote:
        return

    try:
        data = json.dumps({
            "type": "tank_pos",
            "x": tank.rect.x,
            "y": tank.rect.y,
            "dir": tank.direction,
            "pid": game_state.get("player_id", 1)
        }).encode()
        client_socket.send(data)
    except:
        pass


def send_bullet_data(bullet: Bullet) -> None:
    """发送子弹数据"""
    if not online_mode or is_server:
        return

    try:
        data = json.dumps({
            "type": "bullet",
            "x": bullet.rect.x,
            "y": bullet.rect.y,
            "dir": bullet.direction
        }).encode()
        client_socket.send(data)
    except:
        pass


def send_item_pickup(item: Item) -> None:
    """发送道具拾取数据(修复服务器端判断)"""
    if not online_mode or is_server:
        return

    try:
        data = json.dumps({
            "type": "item_pickup",
            "item_type": item.item_type,
            "x": item.rect.centerx,
            "y": item.rect.centery
        }).encode()
        client_socket.send(data)
    except Exception as e:
        print(f"发送道具拾取消息失败: {e}")


# -------------------------- 游戏初始化与关卡生成 --------------------------
def init_game() -> List[Tank]:
    """初始化游戏状态(完全重置,修复精灵清理)"""
    global game_state, server_online_players, client_online_players, online_bullets, online_items, online_boss, resources

    # 重新加载资源(适配新的缩放因子)
    resources = load_resources(SCALE_FACTOR)

    # 重置游戏状态
    game_state.update({
        "score": 0,
        "level": 0,
        "stage": 1,
        "game_over": False,
        "paused": False,
        "online_players": 0
    })

    # 清空所有精灵组
    for layer in sprite_layers.values():
        for sprite in layer:
            sprite.kill()
        layer.empty()

    # 重置联机数据
    with online_players_lock:
        server_online_players.clear()
        client_online_players.clear()
        online_bullets.clear()
        online_items.clear()
        online_boss.clear()

    # 创建背景精灵(优化分层绘制)
    bg_sprite = pygame.sprite.Sprite()
    bg_sprite.layer = "background"
    bg_sprite.image = resources["images"]["bg"]
    bg_sprite.rect = bg_sprite.image.get_rect()
    sprite_layers["background"].add(bg_sprite)

    # 生成关卡障碍物
    generate_stage(game_state["stage"])

    # 创建玩家坦克
    players = []
    if not online_mode:
        # 单机模式:创建玩家1(支持双人)
        player1 = Tank(SCREEN_WIDTH // 4, SCREEN_HEIGHT // 2, GREEN, is_player=True, player_id=1)
        players.append(player1)
        # 可选双人模式(按配置)
        player2 = Tank(SCREEN_WIDTH * 3 // 4, SCREEN_HEIGHT // 2, BLUE, is_player=True, player_id=2)
        players.append(player2)
    else:
        # 联机模式:由服务器/客户端动态创建玩家
        pass

    return players


def generate_stage(stage: int) -> None:
    """生成关卡障碍物"""
    stage_config = STAGE_CONFIG.get(stage, STAGE_CONFIG[1])
    obstacle_count = int(stage_config["obstacles"] * SCALE_FACTOR)
    wall_density = stage_config["wall_density"]

    # 障碍物尺寸(取整)
    wall_size = int(50 * SCALE_FACTOR)
    brick_size = int(50 * SCALE_FACTOR)

    # 避免生成在玩家出生区
    safe_zones = [
        (SCREEN_WIDTH // 4 - 100, SCREEN_HEIGHT // 2 - 100, 200, 200),
        (SCREEN_WIDTH * 3 // 4 - 100, SCREEN_HEIGHT // 2 - 100, 200, 200)
    ]

    # 生成墙壁(固定位置,边缘)
    for x in range(0, SCREEN_WIDTH, wall_size):
        # 上下边缘
        Obstacle(x, 0, "wall")
        Obstacle(x, SCREEN_HEIGHT - wall_size, "wall")
    for y in range(wall_size, SCREEN_HEIGHT - wall_size, wall_size):
        # 左右边缘
        Obstacle(0, y, "wall")
        Obstacle(SCREEN_WIDTH - wall_size, y, "wall")

    # 生成随机障碍物(墙壁+砖块)
    generated_pos = set()
    for _ in range(obstacle_count):
        # 随机位置(取整,避免浮点数)
        x = random.randint(wall_size, SCREEN_WIDTH - 2 * wall_size)
        y = random.randint(wall_size, SCREEN_HEIGHT - 2 * wall_size)
        x = x - (x % wall_size)  # 对齐网格
        y = y - (y % wall_size)
        pos_key = (x, y)

        # 跳过安全区和已生成位置
        in_safe_zone = False
        for (sx, sy, sw, sh) in safe_zones:
            if sx <= x <= sx + sw and sy <= y <= sy + sh:
                in_safe_zone = True
                break
        if in_safe_zone or pos_key in generated_pos:
            continue

        # 随机选择障碍物类型
        obstacle_type = "wall" if random.random() < (0.2 * wall_density) else "brick"
        Obstacle(x, y, obstacle_type)
        generated_pos.add(pos_key)


def spawn_enemy() -> None:
    """生成敌人坦克"""
    # 获取当前难度/等级配置
    difficulty = DIFFICULTY_CONFIG[game_state["difficulty"]]
    level_coeff = 1.0
    for score_threshold, (coeff, _, _) in LEVEL_CONFIG.items():
        if game_state["score"] >= score_threshold:
            level_coeff = coeff

    # 计算当前最大敌人数量
    max_enemies = int(difficulty["max_enemies"] * level_coeff)
    current_enemies = len([t for t in sprite_layers["tanks"] if not t.is_player and not t.is_boss])

    # 限制敌人数量
    if current_enemies >= max_enemies:
        return

    # 生成位置(避开玩家和障碍物)
    spawn_attempts = 0
    while spawn_attempts < 10:
        x = random.randint(int(100 * SCALE_FACTOR), SCREEN_WIDTH - int(100 * SCALE_FACTOR))
        y = random.randint(int(100 * SCALE_FACTOR), SCREEN_HEIGHT - int(100 * SCALE_FACTOR))

        # 检测碰撞
        collide = False
        # 检测障碍物
        for obstacle in sprite_layers["obstacles"]:
            if pygame.Rect(x, y, int(40 * SCALE_FACTOR), int(40 * SCALE_FACTOR)).colliderect(obstacle.rect):
                collide = True
                break
        # 检测玩家
        for tank in sprite_layers["tanks"]:
            if tank.is_player and pygame.Rect(x, y, int(40 * SCALE_FACTOR), int(40 * SCALE_FACTOR)).colliderect(
                    tank.rect):
                collide = True
                break
        if not collide:
            # 创建敌人坦克
            enemy = Tank(x, y, RED, is_player=False)
            # 适配等级速度
            enemy.speed *= level_coeff
            return
        spawn_attempts += 1


def spawn_item() -> None:
    """生成道具"""
    # 限制道具数量
    current_items = len(sprite_layers["items"])
    if current_items >= 3:
        return

    # 生成位置(避开障碍物和坦克)
    spawn_attempts = 0
    while spawn_attempts < 10:
        x = random.randint(int(50 * SCALE_FACTOR), SCREEN_WIDTH - int(50 * SCALE_FACTOR))
        y = random.randint(int(50 * SCALE_FACTOR), SCREEN_HEIGHT - int(50 * SCALE_FACTOR))

        # 检测碰撞
        collide = False
        for sprite in sprite_layers["obstacles"]:
            if pygame.Rect(x, y, int(30 * SCALE_FACTOR), int(30 * SCALE_FACTOR)).colliderect(sprite.rect):
                collide = True
                break
        for sprite in sprite_layers["tanks"]:
            if pygame.Rect(x, y, int(30 * SCALE_FACTOR), int(30 * SCALE_FACTOR)).colliderect(sprite.rect):
                collide = True
                break
        if not collide:
            # 随机道具类型
            item_type = random.choice(list(ITEM_TYPES.keys()))
            Item(x, y, item_type)
            return
        spawn_attempts += 1


def spawn_boss() -> None:
    """生成BOSS坦克"""
    # 检查是否已存在BOSS
    if len([t for t in sprite_layers["tanks"] if t.is_boss]) > 0:
        return

    # BOSS生成位置(中心区域)
    x = SCREEN_WIDTH // 2
    y = SCREEN_HEIGHT // 2
    boss = Tank(x, y, PURPLE, is_player=False, is_boss=True)

    # 适配难度的血量和速度
    difficulty = DIFFICULTY_CONFIG[game_state["difficulty"]]
    boss.hp = difficulty["boss_hp"]
    boss.max_hp = boss.hp
    boss.speed = BASE_TANK_SPEED * BOSS_SPEED_COEFF


def update_game_state() -> None:
    """更新游戏状态(等级、关卡、BOSS生成)"""
    # 更新等级
    new_level = 0
    for score_threshold, (_, _, _) in LEVEL_CONFIG.items():
        if game_state["score"] >= score_threshold:
            new_level = score_threshold // 50  # 每50分升一级
    game_state["level"] = new_level

    # 检测关卡升级
    stage_config = STAGE_CONFIG.get(game_state["stage"], STAGE_CONFIG[3])
    if game_state["score"] >= stage_config["boss_score"]:
        # 生成BOSS
        spawn_boss()
        # 关卡升级(最多3关)
        if game_state["stage"] < 3:
            game_state["stage"] += 1
            # 生成新关卡障碍物
            generate_stage(game_state["stage"])

    # 检查游戏结束(单机模式)
    if not online_mode:
        alive_players = [t for t in sprite_layers["tanks"] if t.is_player and t.hp > 0]
        if len(alive_players) == 0:
            game_state["game_over"] = True
            # 更新高分榜
            update_high_score(game_state["score"])
            # 保存游戏进度
            save_game_progress(
                game_state["score"],
                game_state["level"],
                game_state["stage"],
                game_state["difficulty"]
            )


# -------------------------- UI绘制 --------------------------
def draw_ui(screen: pygame.Surface, players: List[Tank]) -> None:
    """绘制游戏UI(分数、血量、等级、道具、联机状态)"""
    # 加载适配的字体
    font_small = load_game_font(SCALE_FACTOR, 16)
    font_medium = load_game_font(SCALE_FACTOR, 20)
    font_large = load_game_font(SCALE_FACTOR, 24)

    # 1. 分数和等级
    score_text = render_text_safely(font_large, f"分数: {game_state['score']}", WHITE)
    level_text = render_text_safely(font_medium, f"等级: {game_state['level']}", YELLOW)
    stage_text = render_text_safely(font_medium, f"关卡: {game_state['stage']}", ORANGE)
    screen.blit(score_text, (int(10 * SCALE_FACTOR), int(10 * SCALE_FACTOR)))
    screen.blit(level_text, (int(10 * SCALE_FACTOR), int(40 * SCALE_FACTOR)))
    screen.blit(stage_text, (int(10 * SCALE_FACTOR), int(70 * SCALE_FACTOR)))

    # 2. 联机状态
    if online_mode:
        mode_text = "服务器模式" if is_server else "客户端模式"
        online_text = render_text_safely(font_medium, f"联机: {mode_text} | 玩家数: {game_state['online_players']}",
                                         CYAN)
        screen.blit(online_text,
                    (SCREEN_WIDTH - online_text.get_width() - int(10 * SCALE_FACTOR), int(10 * SCALE_FACTOR)))

    # 3. 玩家血量和道具
    player_y = int(10 * SCALE_FACTOR)
    for player in players:
        if not player.is_player or player.remote:
            continue

        # 血量条背景
        hp_bar_bg = (int(SCREEN_WIDTH - 200 * SCALE_FACTOR), player_y, int(180 * SCALE_FACTOR), int(20 * SCALE_FACTOR))
        draw_rounded_rect(screen, DARK_GRAY, hp_bar_bg, radius=int(5 * SCALE_FACTOR))

        # 血量条进度
        hp_ratio = player.hp / player.max_hp
        hp_bar_fg = (int(SCREEN_WIDTH - 200 * SCALE_FACTOR), player_y, int(180 * SCALE_FACTOR * hp_ratio),
                     int(20 * SCALE_FACTOR))
        draw_rounded_rect(screen, GREEN if hp_ratio > 0.5 else YELLOW if hp_ratio > 0.2 else RED, hp_bar_fg,
                          radius=int(5 * SCALE_FACTOR))

        # 玩家标识和血量数值
        player_text = render_text_safely(font_medium, f"玩家{player.player_id} (HP: {player.hp}/{player.max_hp})",
                                         WHITE)
        screen.blit(player_text, (int(SCREEN_WIDTH - 350 * SCALE_FACTOR), player_y))

        # 道具效果显示
        item_texts = []
        for item_type, active in player.item_effects.items():
            if active:
                remaining = (player.item_timers[item_type] - pygame.time.get_ticks()) // 1000
                item_texts.append(f"{ITEM_TYPES[item_type]}({remaining}s)")

        if item_texts:
            item_text = render_text_safely(font_small, " | ".join(item_texts), CYAN)
            screen.blit(item_text, (int(SCREEN_WIDTH - 200 * SCALE_FACTOR), player_y + int(25 * SCALE_FACTOR)))

        player_y += int(50 * SCALE_FACTOR)

    # 4. 暂停/游戏结束提示
    if game_state["paused"]:
        # 半透明遮罩
        mask = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
        mask.fill((0, 0, 0, 150))
        screen.blit(mask, (0, 0))
        # 暂停文字
        pause_text = render_text_safely(font_large, "游戏暂停 (按ESC继续)", YELLOW)
        text_rect = pause_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
        screen.blit(pause_text, text_rect)

    if game_state["game_over"]:
        # 半透明遮罩
        mask = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
        mask.fill((0, 0, 0, 180))
        screen.blit(mask, (0, 0))
        # 游戏结束文字
        game_over_text = render_text_safely(font_large, "游戏结束", RED)
        score_text = render_text_safely(font_medium, f"最终分数: {game_state['score']}", WHITE)
        restart_text = render_text_safely(font_small, "按R重新开始 | 按Q退出", YELLOW)

        game_over_rect = game_over_text.get_rect(
            center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - int(50 * SCALE_FACTOR)))
        score_rect = score_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2))
        restart_rect = restart_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + int(50 * SCALE_FACTOR)))

        screen.blit(game_over_text, game_over_rect)
        screen.blit(score_text, score_rect)
        screen.blit(restart_text, restart_rect)


# -------------------------- 游戏主循环 --------------------------
def game_loop(screen: pygame.Surface, clock: pygame.time.Clock, difficulty: str) -> None:
    """游戏主循环(适配联机、帧率稳定)"""
    game_state["difficulty"] = difficulty
    players = init_game()
    spawn_timer = 0
    item_timer = 0

    while True:
        # 帧率控制
        dt = clock.tick(FPS) / 1000.0
        current_time = pygame.time.get_ticks()

        # 事件处理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                # 清理联机资源
                global online_mode, client_socket, server_socket
                online_mode = False
                if client_socket:
                    client_socket.close()
                if server_socket:
                    server_socket.close()
                pygame.quit()
                sys.exit()

            # 键盘事件
            if event.type == pygame.KEYDOWN:
                # 暂停/继续
                if event.key == pygame.K_ESCAPE:
                    game_state["paused"] = not game_state["paused"]

                # 游戏结束后重新开始/退出
                if game_state["game_over"]:
                    if event.key == pygame.K_r:
                        players = init_game()
                    elif event.key == pygame.K_q:
                        return

                # 单机模式射击
                if not online_mode and not game_state["paused"] and not game_state["game_over"]:
                    for player in players:
                        if player.player_id == 1 and event.key == key_config["player1"]["shoot"]:
                            player.shoot()
                        if player.player_id == 2 and event.key == key_config["player2"]["shoot"]:
                            player.shoot()

        # 暂停/游戏结束时跳过更新
        if game_state["paused"] or game_state["game_over"]:
            # 绘制UI和精灵
            screen.fill(BLACK)
            # 分层绘制精灵(保证层级正确)
            for layer in ["background", "obstacles", "tanks", "bullets", "items", "effects", "ui"]:
                sprite_layers[layer].draw(screen)
            draw_ui(screen, players)
            pygame.display.flip()
            continue

        # 1. 输入处理(坦克移动)
        keys = pygame.key.get_pressed()
        if not online_mode:
            # 单机模式玩家控制
            for player in players:
                if player.player_id == 1:
                    player.movement["up"] = keys[key_config["player1"]["up"]]
                    player.movement["down"] = keys[key_config["player1"]["down"]]
                    player.movement["left"] = keys[key_config["player1"]["left"]]
                    player.movement["right"] = keys[key_config["player1"]["right"]]
                    # 自动射击(按住)
                    if keys[key_config["player1"]["shoot"]]:
                        player.shoot()
                if player.player_id == 2:
                    player.movement["up"] = keys[key_config["player2"]["up"]]
                    player.movement["down"] = keys[key_config["player2"]["down"]]
                    player.movement["left"] = keys[key_config["player2"]["left"]]
                    player.movement["right"] = keys[key_config["player2"]["right"]]
                    # 自动射击(按住)
                    if keys[key_config["player2"]["shoot"]]:
                        player.shoot()

                # 旋转坦克(根据移动方向)
                if player.movement["up"]:
                    player.rotate("up")
                elif player.movement["down"]:
                    player.rotate("down")
                elif player.movement["left"]:
                    player.rotate("left")
                elif player.movement["right"]:
                    player.rotate("right")

                # 联机模式发送坦克位置
                if online_mode and not player.remote:
                    send_tank_data(player)
        else:
            # 联机模式:仅控制本地玩家
            local_player = None
            for tank in sprite_layers["tanks"]:
                if tank.is_player and tank.player_id == game_state["player_id"] and not tank.remote:
                    local_player = tank
                    break
            if local_player:
                local_player.movement["up"] = keys[key_config["player1"]["up"]]
                local_player.movement["down"] = keys[key_config["player1"]["down"]]
                local_player.movement["left"] = keys[key_config["player1"]["left"]]
                local_player.movement["right"] = keys[key_config["player1"]["right"]]

                if local_player.movement["up"]:
                    local_player.rotate("up")
                elif local_player.movement["down"]:
                    local_player.rotate("down")
                elif local_player.movement["left"]:
                    local_player.rotate("left")
                elif local_player.movement["right"]:
                    local_player.rotate("right")

                # 射击
                if keys[key_config["player1"]["shoot"]]:
                    local_player.shoot()

                # 发送位置数据
                send_tank_data(local_player)

        # 2. 生成敌人和道具
        spawn_timer += 1
        item_timer += 1
        difficulty_config = DIFFICULTY_CONFIG[game_state["difficulty"]]
        spawn_rate = difficulty_config["spawn_rate"]

        # 适配等级的生成频率
        for score_threshold, (_, rate, _) in LEVEL_CONFIG.items():
            if game_state["score"] >= score_threshold:
                spawn_rate = rate

        if spawn_timer >= spawn_rate:
            spawn_enemy()
            spawn_timer = 0
        if item_timer >= ITEM_SPAWN_RATE:
            spawn_item()
            item_timer = 0

        # 3. 更新精灵
        obstacles = list(sprite_layers["obstacles"])
        # 更新坦克
        for tank in sprite_layers["tanks"]:
            tank.update(obstacles)
        # 更新子弹
        for bullet in sprite_layers["bullets"]:
            bullet.update(obstacles)
        # 更新道具
        for item in sprite_layers["items"]:
            item.update(players if not online_mode else [t for t in sprite_layers["tanks"] if t.is_player])
        # 更新爆炸特效
        for effect in sprite_layers["effects"]:
            effect.update()

        # 4. 更新游戏状态(等级、关卡)
        update_game_state()

        # 5. 绘制
        screen.fill(BLACK)
        # 分层绘制精灵(严格控制层级)
        for layer_name in ["background", "obstacles", "tanks", "bullets", "items", "effects", "ui"]:
            sprite_layers[layer_name].draw(screen)
        # 绘制UI
        draw_ui(screen, players if not online_mode else [t for t in sprite_layers["tanks"] if t.is_player])

        # 刷新屏幕
        pygame.display.flip()


# -------------------------- 菜单系统(TK+Pygame结合) --------------------------
def create_main_menu() -> None:
    """创建主菜单(TKinter)"""
    root = tk.Tk()
    root.title("坦克大战")
    root.geometry(f"{int(600 * SCALE_FACTOR)}x{int(500 * SCALE_FACTOR)}")
    root.resizable(False, False)
    root.configure(bg="#222222")

    # 加载高分榜
    high_scores = save_data["high_scores"]

    # 难度变量
    difficulty_var = tk.StringVar(value="normal")
    online_mode_var = tk.BooleanVar(value=False)
    server_mode_var = tk.BooleanVar(value=False)

    # 标题
    title_label = ttk.Label(root, text="坦克大战", font=("SimHei", int(30 * SCALE_FACTOR)))
    title_label.pack(pady=int(20 * SCALE_FACTOR))

    # 单人游戏按钮
    def start_single_player():
        root.withdraw()  # 隐藏主菜单(不销毁)
        screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("坦克大战 - 双人模式")
        clock = pygame.time.Clock()
        game_loop(screen, clock, difficulty_var.get())  # 游戏结束后执行后续代码

        # 游戏结束后清理Pygame窗口,恢复主菜单
        pygame.display.quit()
        root.deiconify()  # 重新显示主菜单

    single_btn = ttk.Button(root, text="双人游戏", command=start_single_player, width=int(20 * SCALE_FACTOR))
    single_btn.pack(pady=int(10 * SCALE_FACTOR))

    # 联机游戏按钮
    def start_online_game():
        root.withdraw()  # 隐藏主菜单
        screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("坦克大战 - 联机模式")
        clock = pygame.time.Clock()

        if server_mode_var.get():
            start_server()
        else:
            connect_to_server()

        game_loop(screen, clock, difficulty_var.get())

        # 联机模式额外清理网络资源
        global online_mode, client_socket, server_socket
        online_mode = False
        if client_socket:
            client_socket.close()
            client_socket = None
        if server_socket:
            server_socket.close()
            server_socket = None

        # 恢复主菜单
        pygame.display.quit()
        root.deiconify()

    online_btn = ttk.Button(root, text="联机游戏", command=start_online_game, width=int(20 * SCALE_FACTOR))
    online_btn.pack(pady=int(10 * SCALE_FACTOR))

    # 难度选择
    difficulty_frame = ttk.LabelFrame(root, text="难度选择")
    difficulty_frame.pack(pady=int(10 * SCALE_FACTOR), padx=int(20 * SCALE_FACTOR), fill="x")

    ttk.Radiobutton(difficulty_frame, text="简单", variable=difficulty_var, value="easy").pack(side="left", padx=int(
        10 * SCALE_FACTOR), pady=int(5 * SCALE_FACTOR))
    ttk.Radiobutton(difficulty_frame, text="普通", variable=difficulty_var, value="normal").pack(side="left", padx=int(
        10 * SCALE_FACTOR), pady=int(5 * SCALE_FACTOR))
    ttk.Radiobutton(difficulty_frame, text="困难", variable=difficulty_var, value="hard").pack(side="left", padx=int(
        10 * SCALE_FACTOR), pady=int(5 * SCALE_FACTOR))

    # 联机模式选择
    online_frame = ttk.LabelFrame(root, text="联机设置")
    online_frame.pack(pady=int(10 * SCALE_FACTOR), padx=int(20 * SCALE_FACTOR), fill="x")

    ttk.Checkbutton(online_frame, text="作为服务器", variable=server_mode_var).pack(side="left",
                                                                                    padx=int(10 * SCALE_FACTOR),
                                                                                    pady=int(5 * SCALE_FACTOR))

    # 按键设置按钮
    def open_key_config():
        KeyConfigWindow(root)

    key_config_btn = ttk.Button(root, text="按键设置", command=open_key_config, width=int(20 * SCALE_FACTOR))
    key_config_btn.pack(pady=int(10 * SCALE_FACTOR))

    # 高分榜显示
    score_frame = ttk.LabelFrame(root, text="高分榜")
    score_frame.pack(pady=int(10 * SCALE_FACTOR), padx=int(20 * SCALE_FACTOR), fill="x")

    score_text = tk.Text(score_frame, height=5, width=int(30 * SCALE_FACTOR))
    score_text.pack(padx=int(10 * SCALE_FACTOR), pady=int(5 * SCALE_FACTOR))
    score_text.insert("1.0", "\n".join(
        [f"{i + 1}. {score}" for i, score in enumerate(high_scores)]) if high_scores else "暂无高分记录")
    score_text.config(state="disabled")

    # 退出按钮
    exit_btn = ttk.Button(root, text="退出游戏", command=root.quit, width=int(20 * SCALE_FACTOR))
    exit_btn.pack(pady=int(10 * SCALE_FACTOR))

    # 运行TK主循环
    root.mainloop()


# -------------------------- 主函数 --------------------------
def main() -> None:
    """程序入口"""
    try:
        # 初始化Pygame
        pygame.init()
        pygame.mixer.init()
        # 设置缩放因子(可自定义)
        global SCALE_FACTOR, SCREEN_WIDTH, SCREEN_HEIGHT
        SCALE_FACTOR = 1.2  # 1.0=基础分辨率,1.2=放大20%
        SCREEN_WIDTH = int(BASE_WIDTH * SCALE_FACTOR)
        SCREEN_HEIGHT = int(BASE_HEIGHT * SCALE_FACTOR)
        # 创建主菜单
        create_main_menu()
    except Exception as e:
        print(f"程序异常: {e}")
    finally:
        # 清理资源
        pygame.quit()
        sys.exit()


if __name__ == "__main__":
    main()

八、程序运行部分截图展示

九、发布阶段

通过pyinstaller将 Python 代码打包为可执行文件(如 Windows 的.exe),步骤:

  1. 安装pyinstallerpip install pyinstaller
  2. 打包命令:

Windows 平台

bash 复制代码
pyinstaller --onefile --windowed --name 坦克大战 --add-data "assets;assets" --icon=icon.ico tank_game.py

macOS 平台

bash 复制代码
pyinstaller --onefolder --windowed --name 坦克大战 --add-data "assets:assets"  --icon=icon.icns tank_game.py

Linux 平台

bash 复制代码
pyinstaller --onefolder --name 坦克大战 --add-data "assets:assets"  tank_game.py

命令参数说明

参数 作用
--onefolder 打包为文件夹(包含所有依赖 + 可执行文件),推荐调试 / 发布使用
--onefile 打包为单个可执行文件(方便分发,启动时会解压到临时目录)
--windowed 隐藏控制台窗口(TK/Pygame 图形程序必须,否则会弹出黑窗口)
--name 指定生成的程序名称
--add-data 添加非代码资源(如 assets 文件夹),格式:源路径;目标路径(Windows)/源路径:目标路径(Linux/macOS)
--icon 自定义程序图标(Windows 用 ICO,macOS 用 ICNS,Linux 可选)
--clean 清理打包缓存(解决重复打包的冲突问题)

打包完成后,项目根目录会生成 3 个文件夹:

  • build/:打包临时文件,可删除;
  • dist/:最终发布文件(核心):
    • 文件夹模式:dist/坦克大战/ 包含所有运行文件,双击坦克大战.exe(Windows)/坦克大战(Linux/macOS)即可运行;
    • 单文件模式:dist/坦克大战.exe(Windows)/dist/坦克大战(Linux/macOS)为单个可执行文件;
  • xxx.spec:打包配置文件(可自定义打包规则,一般无需修改)。

十、总结

本文介绍了一个基于Python开发的跨平台2D坦克大战游戏,采用Pygame作为游戏核心引擎,结合Tkinter实现UI菜单,并支持Socket联机功能。游戏包含单人/双人本地模式、联机对战、动态关卡生成、道具系统、BOSS战等核心玩法,以及存档、按键配置等辅助功能。项目采用模块化设计,包括资源加载、精灵分层、联机同步、关卡管理等模块,并通过优化解决了中文显示、跨平台兼容等关键技术问题。最后通过pyinstaller打包为可执行文件,方便分发运行。该游戏展示了Python在2D游戏开发中的完整应用,可作为游戏编程的学习案例。

相关推荐
liu****2 小时前
04_Pandas数据分析入门
python·jupyter·数据挖掘·数据分析·numpy·pandas·python常用工具
2501_918126912 小时前
用Python开发一个三进制程序开发工具
开发语言·汇编·python·个人开发
顾安r2 小时前
1.1 脚本网页 战推棋
java·前端·游戏·html·virtualenv
草莓熊Lotso3 小时前
Python 进阶核心:字典 / 文件操作 + 上下文管理器实战指南
数据结构·c++·人工智能·经验分享·笔记·git·python
天远Date Lab3 小时前
Python实现用户消费潜力评估:天远个人消费能力等级API对接全攻略
java·大数据·网络·python
秃了也弱了。11 小时前
python实现定时任务:schedule库、APScheduler库
开发语言·python
Dfreedom.11 小时前
从 model(x) 到__call__:解密深度学习框架的设计基石
人工智能·pytorch·python·深度学习·call
weixin_4250230011 小时前
Spring Boot 配置文件优先级详解
spring boot·后端·python
lxysbly12 小时前
安卓玩MRP冒泡游戏:模拟器下载与使用方法
android·游戏