磊之茶 发表于 2026-8-28 08:15:47

一个简单的塔防游戏

# -*- coding: utf-8 -*-
"""
================================================================================
塔防游戏
================================================================================
操作说明:
方向键 / WASD : 移动主角色(红色正方形)
鼠标左键      : 在地图上放置红色三角标,小兵会围绕三角标行动
鼠标右键      : 取消三角标
T 键          : 在主角色当前位置建造一座塔(红色正方形,自动攻击怪物)
1 键          : 生成一支近战小兵(白色正方形)
2 键          : 生成一支远程小兵(白色圆形)
R 键          : 游戏结束后重新开始
ESC 键      : 退出游戏
================================================================================
"""

# 导入 pygame 库,用于创建窗口、绘制图形、处理事件等
import pygame
# 从 pygame.locals 导入常用常量(如按键、事件类型等)
from pygame.locals import *
# 导入 random 模块,用于生成随机数(如怪物生成间隔、随机偏移等)
import random
# 导入 math 模块,用于数学计算(如两点间距离、三角函数等)
import math
# 导入 sys 模块,用于系统相关操作(如退出程序)
import sys


# ==============================================================================
# 全局常量定义
# ==============================================================================

# 屏幕宽度(像素)
SCREEN_WIDTH = 1000
# 屏幕高度(像素)
SCREEN_HEIGHT = 700

# 游戏刷新帧率(每秒多少帧)
FPS = 60

# 颜色定义(RGB 格式)
COLOR_WHITE = (255, 255, 255)   # 白色 - 近战小兵(正方形) / 远程小兵(圆形)
COLOR_BLACK = (0, 0, 0)         # 黑色 - 近战怪物(正方形) / 远程怪物(圆形)
COLOR_RED = (220, 30, 30)       # 红色 - 主角色(正方形) / 塔(正方形)
COLOR_RED_DARK = (160, 20, 20)# 深红色 - 主角色子弹(小正方形)
COLOR_GRAY = (120, 120, 120)    # 灰色 - UI 文字、路径辅助线
COLOR_GREEN = (40, 200, 40)   # 绿色 - 生命条(健康)
COLOR_YELLOW = (230, 210, 30)   # 黄色 - 三角标
COLOR_BG = (30, 30, 40)         # 背景色 - 深蓝灰

# 主角色移动速度(像素/秒)
HERO_SPEED = 220
# 塔的攻击范围(像素)
TOWER_RANGE = 220
# 塔的射击冷却时间(毫秒)
TOWER_COOLDOWN = 700

# 小兵围绕目标(主角色/三角标)的半径范围
SOLDIER_ORBIT_MIN = 60
SOLDIER_ORBIT_MAX = 130

# 远程单位射击冷却(毫秒)
RANGED_COOLDOWN = 1200


# ==============================================================================
# 工具函数
# ==============================================================================

def distance(p1, p2):
    """
    计算两点之间的欧几里得距离。
    参数:
      p1 : (x, y) 元组,第一个点
      p2 : (x, y) 元组,第二个点
    返回:
      float : 两点间的距离
    """
    # 用勾股定理计算距离:sqrt((x2-x1)^2 + (y2-y1)^2)
    return math.hypot(p2 - p1, p2 - p1)


def clamp(value, min_val, max_val):
    """
    将数值限制在 区间内。
    参数:
      value   : 要限制的数值
      min_val : 下限
      max_val : 上限
    返回:
      限制后的数值
    """
    # 如果 value 小于下限,返回下限;大于上限,返回上限;否则返回原值
    if value < min_val:
      return min_val
    if value > max_val:
      return max_val
    return value


# ==============================================================================
# 子弹类(所有子弹统一用此类,通过 color / is_square 区分外观)
# ==============================================================================

class Bullet:
    """
    子弹类:代表从塔/远程小兵/远程怪物/主角色射出的飞行道具。
    所有阵营的子弹共用此类,通过参数区分外观和伤害归属。
    """

    def __init__(self, x, y, target_x, target_y, speed, damage, color, is_square=False, size=6):
      """
      初始化一颗子弹。
      参数:
            x, y       : 子弹出生位置(像素坐标)
            target_x, y : 子弹飞向的目标位置(像素坐标)
            speed       : 飞行速度(像素/秒)
            damage      : 命中时造成的伤害值
            color       : 子弹颜色 (R,G,B)
            is_square   : True=正方形外观(塔/主角色子弹),False=圆形外观
            size      : 子弹边长/直径(像素)
      """
      # 记录出生坐标
      self.x = x
      self.y = y
      # 计算子弹到目标的单位方向向量
      dx = target_x - x
      dy = target_y - y
      dist = math.hypot(dx, dy)
      # 避免除零(目标与出生点重合时)
      if dist == 0:
            self.vx = 0
            self.vy = 0
      else:
            # 单位向量乘以速度得到每帧位移分量
            self.vx = dx / dist * speed
            self.vy = dy / dist * speed
      # 飞行速度(像素/秒)
      self.speed = speed
      # 伤害值
      self.damage = damage
      # 子弹颜色
      self.color = color
      # 是否为正方形外观
      self.is_square = is_square
      # 子弹尺寸(边长或直径)
      self.size = size
      # 存活标记,False 时会被从列表中移除
      self.alive = True

    def update(self, dt):
      """
      更新子弹位置。
      参数:
            dt : 上一帧到这一帧的时间间隔(秒)
      """
      # 根据速度和时间增量移动子弹
      self.x += self.vx * dt
      self.y += self.vy * dt
      # 如果子弹飞出屏幕范围,标记为死亡
      if (self.x < -20 or self.x > SCREEN_WIDTH + 20 or
                self.y < -20 or self.y > SCREEN_HEIGHT + 20):
            self.alive = False

    def draw(self, screen):
      """
      在屏幕上绘制子弹。
      参数:
            screen : pygame 的 Surface 对象(游戏窗口)
      """
      if self.is_square:
            # 正方形子弹:用 pygame.draw.rect 绘制
            rect = pygame.Rect(self.x - self.size / 2, self.y - self.size / 2,
                               self.size, self.size)
            pygame.draw.rect(screen, self.color, rect)
      else:
            # 圆形子弹:用 pygame.draw.circle 绘制
            pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)),
                               self.size // 2)

    def get_rect(self):
      """
      获取子弹的碰撞矩形(用于检测是否击中目标)。
      返回:
            pygame.Rect 对象
      """
      # 以子弹中心为基准,生成一个边长=size 的正方形碰撞框
      return pygame.Rect(self.x - self.size / 2, self.y - self.size / 2,
                           self.size, self.size)


# ==============================================================================
# 小兵基类(近战小兵 & 远程小兵的公共属性和行为)
# ==============================================================================

class Soldier:
    """
    小兵基类(我方单位)。
    近战小兵 = 白色正方形;远程小兵 = 白色圆形。
    小兵会围绕主角色(或玩家放置的红色三角标)行动,并自动攻击范围内的怪物。
    """

    def __init__(self, x, y, soldier_type):
      """
      初始化一个小兵。
      参数:
            x, y         : 出生位置
            soldier_type : 'melee' 近战(正方形) / 'ranged' 远程(圆形)
      """
      # 出生坐标
      self.x = x
      self.y = y
      # 小兵类型
      self.soldier_type = soldier_type
      # 近战用正方形,远程用圆形
      if soldier_type == 'melee':
            self.is_square = True   # 外观为正方形
            self.size = 20          # 边长
            self.max_hp = 60      # 最大生命值
            self.speed = 90         # 移动速度(像素/秒)
            self.attack_range = 28# 近战攻击距离
            self.attack_damage = 12 # 每次攻击伤害
            self.attack_cooldown = 800# 攻击冷却(毫秒)
      else:
            self.is_square = False# 外观为圆形
            self.size = 18          # 直径
            self.max_hp = 40
            self.speed = 75
            self.attack_range = 160# 远程攻击距离
            self.attack_damage = 8
            self.attack_cooldown = RANGED_COOLDOWN
      # 当前生命值
      self.hp = self.max_hp
      # 是否存活
      self.alive = True
      # 攻击冷却计时器(毫秒),大于 0 时不能攻击
      self.cooldown_timer = 0
      # 当前锁定的攻击目标(怪物对象),None 表示无目标
      self.target = None
      # 围绕目标(主角色/三角标)的当前角度(弧度),用于圆周运动
      self.orbit_angle = random.uniform(0, 2 * math.pi)
      # 围绕半径(随机一个值,让小兵分散开)
      self.orbit_radius = random.uniform(SOLDIER_ORBIT_MIN, SOLDIER_ORBIT_MAX)

    def update(self, dt, monsters, marker_pos, hero_pos, bullets_soldier):
      """
      更新小兵状态:移动 + 索敌 + 攻击。
      参数:
            dt             : 时间增量(秒)
            monsters       : 怪物列表(用于索敌)
            marker_pos   : 三角标位置 (x,y) 或 None
            hero_pos       : 主角色位置 (x,y)
            bullets_soldier: 我方子弹列表(远程小兵射击时往里添加)
      """
      # 冷却计时器随时间递减
      if self.cooldown_timer > 0:
            self.cooldown_timer -= dt * 1000

      # 确定围绕中心:优先三角标,其次主角色
      if marker_pos is not None:
            cx, cy = marker_pos
      else:
            cx, cy = hero_pos

      # ---- 索敌:找射程内最近的怪物 ----
      closest = None
      closest_dist = self.attack_range
      for m in monsters:
            if not m.alive:
                continue
            d = distance((self.x, self.y), (m.x, m.y))
            if d < closest_dist:
                closest_dist = d
                closest = m
      self.target = closest

      if self.target is not None:
            # 有目标且在射程内:停下进行攻击
            if self.soldier_type == 'ranged':
                # 远程:发射子弹
                if self.cooldown_timer <= 0:
                  b = Bullet(self.x, self.y, self.target.x, self.target.y,
                               280, self.attack_damage, COLOR_WHITE,
                               is_square=False, size=6)
                  bullets_soldier.append(b)
                  self.cooldown_timer = self.attack_cooldown
            else:
                # 近战:直接扣血
                if self.cooldown_timer <= 0:
                  self.target.take_damage(self.attack_damage)
                  self.cooldown_timer = self.attack_cooldown
      else:
            # 无目标:围绕中心做圆周运动
            self.orbit_angle += dt * 0.6# 角速度
            # 计算目标位置(圆周上的点)
            tx = cx + math.cos(self.orbit_angle) * self.orbit_radius
            ty = cy + math.sin(self.orbit_angle) * self.orbit_radius
            # 平滑移向目标点
            dx = tx - self.x
            dy = ty - self.y
            dist = math.hypot(dx, dy)
            if dist > 2:
                self.x += dx / dist * self.speed * dt
                self.y += dy / dist * self.speed * dt
            # 限制在屏幕内
            self.x = clamp(self.x, 10, SCREEN_WIDTH - 10)
            self.y = clamp(self.y, 10, SCREEN_HEIGHT - 10)

    def take_damage(self, dmg):
      """
      受到伤害。
      参数:
            dmg : 伤害值
      """
      self.hp -= dmg
      if self.hp <= 0:
            self.alive = False

    def draw(self, screen):
      """
      绘制小兵。
      参数:
            screen : 游戏窗口 Surface
      """
      if self.is_square:
            # 近战小兵:白色正方形
            rect = pygame.Rect(self.x - self.size / 2, self.y - self.size / 2,
                               self.size, self.size)
            pygame.draw.rect(screen, COLOR_WHITE, rect)
            # 加一个浅灰边框便于辨识
            pygame.draw.rect(screen, (200, 200, 200), rect, 1)
      else:
            # 远程小兵:白色圆形
            pygame.draw.circle(screen, COLOR_WHITE, (int(self.x), int(self.y)),
                               self.size // 2)
            pygame.draw.circle(screen, (200, 200, 200),
                               (int(self.x), int(self.y)), self.size // 2, 1)
      # 绘制血条(在头顶)
      bar_w = 22
      bar_h = 3
      bx = self.x - bar_w / 2
      by = self.y - self.size / 2 - 8
      pygame.draw.rect(screen, (60, 60, 60), (bx, by, bar_w, bar_h))
      hp_ratio = max(0, self.hp / self.max_hp)
      pygame.draw.rect(screen, COLOR_GREEN, (bx, by, bar_w * hp_ratio, bar_h))


# ==============================================================================
# 怪物类(近战怪物 & 远程怪物)
# ==============================================================================

class Monster:
    """
    怪物类(敌方单位)。
    近战怪物 = 黑色正方形;远程怪物 = 黑色圆形。
    怪物沿一条固定的、拐几个弯的路径从屏幕顶部向底部(或左向右)行进。
    """

    def __init__(self, x, y, monster_type, path_points):
      """
      初始化一个怪物。
      参数:
            x, y         : 出生位置
            monster_type : 'melee' 近战(正方形) / 'ranged' 远程(圆形)
            path_points: 路径点列表 [(x1,y1),(x2,y2),...],怪物依次走过
      """
      self.x = x
      self.y = y
      self.monster_type = monster_type
      # 路径点副本
      self.path = list(path_points)
      # 当前目标路径点索引
      self.path_index = 0
      if monster_type == 'melee':
            self.is_square = True
            self.size = 22
            self.max_hp = 50
            self.speed = 60
            self.attack_range = 30
            self.attack_damage = 10
            self.attack_cooldown = 900
      else:
            self.is_square = False
            self.size = 20
            self.max_hp = 35
            self.speed = 50
            self.attack_range = 150
            self.attack_damage = 7
            self.attack_cooldown = RANGED_COOLDOWN
      self.hp = self.max_hp
      self.alive = True
      self.cooldown_timer = 0
      self.target = None# 锁定的小兵目标

    def update(self, dt, soldiers, bullets_monster):
      """
      更新怪物:沿路径移动 + 索敌 + 攻击。
      参数:
            dt            : 时间增量(秒)
            soldiers      : 我方小兵列表(用于索敌)
            bullets_monster : 敌方子弹列表(远程怪物射击时添加)
      """
      if self.cooldown_timer > 0:
            self.cooldown_timer -= dt * 1000

      # ---- 索敌:找射程内最近的小兵 ----
      closest = None
      closest_dist = self.attack_range
      for s in soldiers:
            if not s.alive:
                continue
            d = distance((self.x, self.y), (s.x, s.y))
            if d < closest_dist:
                closest_dist = d
                closest = s
      self.target = closest

      if self.target is not None:
            # 有目标:停下攻击
            if self.monster_type == 'ranged':
                if self.cooldown_timer <= 0:
                  b = Bullet(self.x, self.y, self.target.x, self.target.y,
                               240, self.attack_damage, COLOR_BLACK,
                               is_square=False, size=6)
                  bullets_monster.append(b)
                  self.cooldown_timer = self.attack_cooldown
            else:
                if self.cooldown_timer <= 0:
                  self.target.take_damage(self.attack_damage)
                  self.cooldown_timer = self.attack_cooldown
      else:
            # 无目标:沿路径前进
            if self.path_index < len(self.path):
                tx, ty = self.path
                dx = tx - self.x
                dy = ty - self.y
                dist = math.hypot(dx, dy)
                if dist < 5:
                  # 到达当前路径点,前往下一个
                  self.path_index += 1
                else:
                  self.x += dx / dist * self.speed * dt
                  self.y += dy / dist * self.speed * dt

    def take_damage(self, dmg):
      """受到伤害。"""
      self.hp -= dmg
      if self.hp <= 0:
            self.alive = False

    def draw(self, screen):
      """绘制怪物。"""
      if self.is_square:
            # 近战怪物:黑色正方形
            rect = pygame.Rect(self.x - self.size / 2, self.y - self.size / 2,
                               self.size, self.size)
            pygame.draw.rect(screen, COLOR_BLACK, rect)
            pygame.draw.rect(screen, (50, 50, 50), rect, 1)
      else:
            # 远程怪物:黑色圆形
            pygame.draw.circle(screen, COLOR_BLACK, (int(self.x), int(self.y)),
                               self.size // 2)
            pygame.draw.circle(screen, (50, 50, 50),
                               (int(self.x), int(self.y)), self.size // 2, 1)
      # 血条
      bar_w = 24
      bar_h = 3
      bx = self.x - bar_w / 2
      by = self.y - self.size / 2 - 8
      pygame.draw.rect(screen, (60, 60, 60), (bx, by, bar_w, bar_h))
      hp_ratio = max(0, self.hp / self.max_hp)
      pygame.draw.rect(screen, (220, 60, 60), (bx, by, bar_w * hp_ratio, bar_h))

    def reached_end(self):
      """
      判断怪物是否已走完全部路径点(到达终点)。
      返回:
            True/False
      """
      return self.path_index >= len(self.path)


# ==============================================================================
# 塔类(玩家按 T 键在主角色位置建造)
# ==============================================================================

class Tower:
    """
    塔类:玩家按 T 键在主角色当前位置建造。
    外观:红色正方形。自动攻击射程内最近的怪物,发射红色小正方形子弹。
    """

    def __init__(self, x, y):
      """初始化一座塔。"""
      self.x = x
      self.y = y
      self.size = 26                # 塔的边长
      self.range = TOWER_RANGE      # 攻击范围
      self.damage = 18            # 每次命中伤害
      self.cooldown_max = TOWER_COOLDOWN# 射击冷却(毫秒)
      self.cooldown_timer = 0       # 当前冷却计时
      self.alive = True             # 塔始终存活(本验证版不可被攻击)

    def update(self, dt, monsters, bullets_tower):
      """
      塔的逻辑更新:索敌 + 射击。
      参数:
            dt         : 时间增量(秒)
            monsters   : 怪物列表
            bullets_tower: 塔子弹列表(射击时添加)
      """
      if self.cooldown_timer > 0:
            self.cooldown_timer -= dt * 1000
      # 找射程内最近的怪物
      closest = None
      closest_dist = self.range
      for m in monsters:
            if not m.alive:
                continue
            d = distance((self.x, self.y), (m.x, m.y))
            if d < closest_dist:
                closest_dist = d
                closest = m
      if closest is not None and self.cooldown_timer <= 0:
            # 发射红色小正方形子弹
            b = Bullet(self.x, self.y, closest.x, closest.y,
                     320, self.damage, COLOR_RED, is_square=True, size=8)
            bullets_tower.append(b)
            self.cooldown_timer = self.cooldown_max

    def draw(self, screen):
      """绘制塔(红色正方形 + 攻击范围圈)。"""
      # 先画半透明的攻击范围圈(淡红色)
      surf = pygame.Surface((self.range * 2, self.range * 2), pygame.SRCALPHA)
      pygame.draw.circle(surf, (220, 30, 30, 25),
                           (self.range, self.range), self.range)
      screen.blit(surf, (self.x - self.range, self.y - self.range))
      # 画塔主体(红色正方形)
      rect = pygame.Rect(self.x - self.size / 2, self.y - self.size / 2,
                           self.size, self.size)
      pygame.draw.rect(screen, COLOR_RED, rect)
      pygame.draw.rect(screen, (255, 120, 120), rect, 2)


# ==============================================================================
# 主角色类(玩家操控的红色正方形)
# ==============================================================================

class Hero:
    """
    主角色类:玩家用方向键/WASD 操控的红色正方形。
    小兵默认围绕主角色行动;玩家可在地图上放置红色三角标,
    放置后小兵改为围绕三角标行动。
    """

    def __init__(self, x, y):
      """初始化主角色。"""
      self.x = x
      self.y = y
      self.size = 24          # 边长
      self.speed = HERO_SPEED# 移动速度
      self.hp = 100         # 生命值
      self.max_hp = 100
      self.alive = True

    def update(self, dt, keys):
      """
      根据按键更新主角色位置。
      参数:
            dt    : 时间增量(秒)
            keys: pygame 按键状态字典
      """
      dx = 0
      dy = 0
      # 检测方向键和 WASD
      if keys or keys:
            dx -= 1
      if keys or keys:
            dx += 1
      if keys or keys:
            dy -= 1
      if keys or keys:
            dy += 1
      # 归一化对角线移动(防止斜向速度过快)
      if dx != 0 and dy != 0:
            dx *= 0.7071
            dy *= 0.7071
      self.x += dx * self.speed * dt
      self.y += dy * self.speed * dt
      # 限制在屏幕内
      self.x = clamp(self.x, self.size / 2, SCREEN_WIDTH - self.size / 2)
      self.y = clamp(self.y, self.size / 2, SCREEN_HEIGHT - self.size / 2)

    def draw(self, screen):
      """绘制主角色(红色正方形 + 血条)。"""
      rect = pygame.Rect(self.x - self.size / 2, self.y - self.size / 2,
                           self.size, self.size)
      pygame.draw.rect(screen, COLOR_RED, rect)
      pygame.draw.rect(screen, (255, 160, 160), rect, 2)
      # 血条
      bar_w = 30
      bar_h = 4
      bx = self.x - bar_w / 2
      by = self.y - self.size / 2 - 10
      pygame.draw.rect(screen, (60, 60, 60), (bx, by, bar_w, bar_h))
      hp_ratio = max(0, self.hp / self.max_hp)
      pygame.draw.rect(screen, COLOR_GREEN, (bx, by, bar_w * hp_ratio, bar_h))


# ==============================================================================
# 红色三角标类(玩家鼠标左键放置的集结点)
# ==============================================================================

class Marker:
    """
    红色三角标:玩家鼠标左键点击地图后生成。
    小兵检测到三角标存在时,改为围绕三角标而非主角色行动。
    """

    def __init__(self, x, y):
      """初始化三角标。"""
      self.x = x
      self.y = y
      self.size = 16# 三角标尺寸
      self.alive = True
      # 脉冲动画计时(让三角标有呼吸效果)
      self.pulse = 0

    def update(self, dt):
      """更新脉冲动画计时。"""
      self.pulse += dt * 4

    def draw(self, screen):
      """绘制红色三角标(向下指的三角形 + 外圈脉冲)。"""
      # 脉冲外圈
      pulse_r = self.size + math.sin(self.pulse) * 6 + 6
      pygame.draw.circle(screen, (230, 210, 30, 60),
                           (int(self.x), int(self.y)), int(pulse_r), 1)
      # 三角形三个顶点(指向下方)
      s = self.size
      p1 = (self.x, self.y - s * 0.7)   # 上顶点
      p2 = (self.x - s * 0.6, self.y + s * 0.5)# 左下
      p3 = (self.x + s * 0.6, self.y + s * 0.5)# 右下
      pygame.draw.polygon(screen, COLOR_YELLOW, )
      pygame.draw.polygon(screen, (180, 160, 20), , 2)


# ==============================================================================
# 游戏主类(管理所有状态和主循环)
# ==============================================================================

class Game:
    """游戏主类,负责初始化、主循环、事件处理、碰撞检测等。"""

    def __init__(self):
      """初始化 pygame 和游戏状态。"""
      # 初始化 pygame 视频模块
      pygame.init()
      pygame.display.set_caption(u"塔防游戏")
      # 创建游戏窗口
      self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
      # 创建时钟对象,用于限制帧率
      self.clock = pygame.time.Clock()
      # 创建字体对象(默认字体,支持中文需系统有中文字体)
      # 使用 pygame.font.SysFont 并指定中文字体名称列表,避免中文乱码
      self.font = pygame.font.SysFont(
            "noto sans cjk sc,wenquanyi micro hei,microsoft yahei,simhei,arial",
            20)
      self.font_small = pygame.font.SysFont(
            "noto sans cjk sc,wenquanyi micro hei,microsoft yahei,simhei,arial",
            16)
      self.font_big = pygame.font.SysFont(
            "noto sans cjk sc,wenquanyi micro hei,microsoft yahei,simhei,arial",
            48)

      # ---- 游戏状态变量 ----
      self.running = True          # 主循环是否继续
      self.game_over = False       # 是否游戏结束
      self.score = 0               # 当前得分
      self.leaked = 0            # 漏掉的怪物数
      self.max_leaked = 10         # 允许漏掉的最大值,超过则游戏结束
      self.wave = 1                # 当前波次
      self.wave_timer = 0          # 波次内计时(毫秒)
      self.spawn_timer = 0         # 怪物生成计时(毫秒)
      self.monsters_to_spawn = 0   # 当前波次剩余待生成怪物数

      # ---- 游戏对象列表 ----
      self.hero = Hero(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
      self.marker = None            # 红色三角标(None=未放置)
      self.soldiers = []         # 我方小兵列表
      self.monsters = []         # 敌方怪物列表
      self.towers = []             # 塔列表
      self.bullets_tower = []      # 塔发射的子弹
      self.bullets_soldier = []    # 小兵发射的子弹
      self.bullets_monster = []    # 怪物发射的子弹

      # ---- 固定路径定义(怪物走这条路径,拐几个弯)----
      # 路径:从屏幕上方中间出发,先向右下,再向左,再向下出屏
      self.monster_path = [
            (SCREEN_WIDTH // 2, -20),       # 起点(屏外上方)
            (SCREEN_WIDTH // 2, 120),       # 第一段:向下
            (SCREEN_WIDTH - 120, 120),      # 第二段:向右
            (SCREEN_WIDTH - 120, 380),      # 第三段:向下
            (140, 380),                     # 第四段:向左
            (140, SCREEN_HEIGHT - 60),      # 第五段:向下
            (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 60),# 第六段:向右
            (SCREEN_WIDTH // 2, SCREEN_HEIGHT + 30),# 终点(屏外下方)
      ]

      # 启动第一波
      self._start_wave()

    # --------------------------------------------------------------------------
    # 内部方法
    # --------------------------------------------------------------------------

    def _start_wave(self):
      """开始新的一波:计算本波要生成的怪物数量和间隔。"""
      # 每波怪物数量 = 5 + 波次*2
      self.monsters_to_spawn = 5 + self.wave * 2
      # 生成间隔随波次递减(最快 600ms)
      self.spawn_interval = max(600, 1800 - self.wave * 150)
      self.spawn_timer = 0

    def _spawn_monster(self):
      """生成一只怪物(从路径起点出发)。"""
      # 起点为路径第一个点
      sx, sy = self.monster_path
      # 70% 近战 + 30% 远程,随波次提高远程比例
      ranged_chance = min(0.4, 0.2 + self.wave * 0.03)
      mtype = 'ranged' if random.random() < ranged_chance else 'melee'
      # 怪物的生命值随波次提升
      m = Monster(sx, sy, mtype, self.monster_path)
      m.max_hp = int(m.max_hp * (1 + self.wave * 0.15))
      m.hp = m.max_hp
      self.monsters.append(m)

    def _handle_events(self):
      """处理 pygame 事件队列(键盘/鼠标/退出)。"""
      # 遍历所有待处理事件
      for event in pygame.event.get():
            if event.type == pygame.QUIT:
                # 点击关闭按钮
                self.running = False
            elif event.type == pygame.KEYDOWN:
                # 按键按下
                self._handle_keydown(event.key)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                # 鼠标按键
                self._handle_mouse(event)

    def _handle_keydown(self, key):
      """
      处理按键事件。
      参数:
            key : 被按下的键(pygame 常量)
      """
      if self.game_over:
            # 游戏结束后只响应 R 键重新开始
            if key == pygame.K_r:
                self._restart()
            elif key == pygame.K_ESCAPE:
                self.running = False
            return

      if key == pygame.K_1:
            # 1 键:在主角色附近生成一支近战小兵
            self._spawn_soldier('melee')
      elif key == pygame.K_2:
            # 2 键:在主角色附近生成一支远程小兵
            self._spawn_soldier('ranged')
      elif key == pygame.K_t:
            # T 键:在主角色当前位置建造一座塔
            print(0)
            self._build_tower()
      elif key == pygame.K_ESCAPE:
            self.running = False

    def _handle_mouse(self, event):
      """
      处理鼠标事件。
      参数:
            event : pygame 鼠标事件对象
      """
      if self.game_over:
            return
      if event.button == 1:
            # 鼠标左键:在点击位置放置/移动红色三角标
            mx, my = event.pos
            self.marker = Marker(mx, my)
      elif event.button == 3:
            # 鼠标右键:取消三角标
            self.marker = None

    def _spawn_soldier(self, stype):
      """
      在主角色附近随机偏移位置生成一支小兵。
      参数:
            stype : 'melee' 或 'ranged'
      """
      # 在主角位置附近随机偏移,避免重叠
      ox = random.uniform(-30, 30)
      oy = random.uniform(-30, 30)
      s = Soldier(self.hero.x + ox, self.hero.y + oy, stype)
      self.soldiers.append(s)

    def _build_tower(self):
      """在主角色当前位置建造一座塔(最多允许 8 座)。"""
      if len(self.towers) >= 8:
            return# 达到上限
      # 检查该位置是否已有塔(避免重叠)
      print(1)
      for t in self.towers:
            if distance((t.x, t.y), (self.hero.x, self.hero.y)) < 30:
                return
      print(2)
      self.towers.append(Tower(self.hero.x, self.hero.y))

    def _update(self, dt):
      """
      更新所有游戏对象(每帧调用一次)。
      参数:
            dt : 时间增量(秒)
      """
      if self.game_over:
            return

      # 获取当前按键状态
      keys = pygame.key.get_pressed()

      # 更新主角色
      self.hero.update(dt, keys)
      # 更新三角标(脉冲动画)
      if self.marker:
            self.marker.update(dt)

      # ---- 更新塔 ----
      for t in self.towers:
            t.update(dt, self.monsters, self.bullets_tower)

      # ---- 更新小兵 ----
      marker_pos = (self.marker.x, self.marker.y) if self.marker else None
      hero_pos = (self.hero.x, self.hero.y)
      for s in self.soldiers:
            s.update(dt, self.monsters, marker_pos, hero_pos,
                     self.bullets_soldier)

      # ---- 更新怪物(生成 + 行为)----
      self._update_monster_spawning(dt)
      for m in self.monsters:
            m.update(dt, self.soldiers, self.bullets_monster)
            # 检测是否走完全程(漏怪)
            if m.reached_end() and m.alive:
                m.alive = False
                self.leaked += 1
                self.score = max(0, self.score - 5)# 漏怪扣分
                if self.leaked >= self.max_leaked:
                  self.game_over = True

      # ---- 更新所有子弹 ----
      self._update_bullets(dt)

      # ---- 清理死亡对象 ----
      self._cleanup_dead()

      # ---- 波次推进 ----
      if self.monsters_to_spawn == 0 and len(self.monsters) == 0:
            # 本波清空,进入下一波
            self.wave += 1
            self.score += 50 * self.wave# 过关奖励
            self._start_wave()

      # 得分随时间小幅增长
      self.score += dt * 2

    def _update_monster_spawning(self, dt):
      """管理怪物按间隔逐只生成。"""
      if self.monsters_to_spawn > 0:
            self.spawn_timer += dt * 1000
            if self.spawn_timer >= self.spawn_interval:
                self._spawn_monster()
                self.monsters_to_spawn -= 1
                self.spawn_timer = 0

    def _update_bullets(self, dt):
      """更新子弹位置并检测碰撞。"""
      # --- 塔子弹(打怪物)---
      for b in self.bullets_tower:
            b.update(dt)
            if not b.alive:
                continue
            # 检测是否击中某只怪物
            for m in self.monsters:
                if not m.alive:
                  continue
                if b.get_rect().colliderect(m.x - m.size/2, m.y - m.size/2,
                                          m.size, m.size):
                  m.take_damage(b.damage)
                  b.alive = False
                  if not m.alive:
                        self.score += 10# 击杀得分
                  break

      # --- 小兵子弹(打怪物)---
      for b in self.bullets_soldier:
            b.update(dt)
            if not b.alive:
                continue
            for m in self.monsters:
                if not m.alive:
                  continue
                if b.get_rect().colliderect(m.x - m.size/2, m.y - m.size/2,
                                          m.size, m.size):
                  m.take_damage(b.damage)
                  b.alive = False
                  if not m.alive:
                        self.score += 5
                  break

      # --- 怪物子弹(打小兵)---
      for b in self.bullets_monster:
            b.update(dt)
            if not b.alive:
                continue
            for s in self.soldiers:
                if not s.alive:
                  continue
                if b.get_rect().colliderect(s.x - s.size/2, s.y - s.size/2,
                                          s.size, s.size):
                  s.take_damage(b.damage)
                  b.alive = False
                  break

    def _cleanup_dead(self):
      """移除所有标记为死亡的子弹/小兵/怪物,释放内存。"""
      self.bullets_tower =
      self.bullets_soldier =
      self.bullets_monster =
      self.soldiers =
      self.monsters =

    def _draw(self):
      """绘制当前帧的所有内容到屏幕。"""
      # 填充背景色
      self.screen.fill(COLOR_BG)

      # 绘制路径辅助线(淡灰色虚线,便于观察怪物行进路线)
      self._draw_path()

      # 绘制三角标
      if self.marker:
            self.marker.draw(self.screen)

      # 绘制塔
      for t in self.towers:
            t.draw(self.screen)
      # 绘制小兵
      for s in self.soldiers:
            s.draw(self.screen)
      # 绘制怪物
      for m in self.monsters:
            m.draw(self.screen)
      # 绘制主角色(最后绘制,显示在最上层)
      self.hero.draw(self.screen)

      # 绘制所有子弹
      for b in self.bullets_tower:
            b.draw(self.screen)
      for b in self.bullets_soldier:
            b.draw(self.screen)
      for b in self.bullets_monster:
            b.draw(self.screen)

      # 绘制 UI(得分/波次/漏怪数/操作提示)
      self._draw_ui()

      # 如果游戏结束,绘制半透明遮罩 + 结束文字
      if self.game_over:
            self._draw_game_over()

      # 将绘制内容刷新到屏幕
      pygame.display.flip()

    def _draw_path(self):
      """绘制怪物路径的辅助线(浅灰虚线)。"""
      pts = [(int(p), int(p)) for p in self.monster_path]
      if len(pts) < 2:
            return
      for i in range(len(pts) - 1):
            # 用浅灰色画路径线段
            pygame.draw.line(self.screen, (60, 60, 72), pts, pts, 2)
      # 在路径拐点画小圆点标记
      for p in pts:
            pygame.draw.circle(self.screen, (80, 80, 95), p, 4)

    def _draw_ui(self):
      """绘制顶部状态栏和操作提示。"""
      # ---- 顶部状态信息 ----
      info_lines = [
            u"得分: %d" % int(self.score),
            u"波次: %d" % self.wave,
            u"漏怪: %d/%d" % (self.leaked, self.max_leaked),
            u"小兵: %d" % len(self.soldiers),
            u"塔: %d/8" % len(self.towers),
      ]
      x0 = 10
      y0 = 8
      for i, line in enumerate(info_lines):
            surf = self.font_small.render(line, True, (220, 220, 220))
            self.screen.blit(surf, (x0, y0 + i * 20))

      # ---- 右下角操作提示 ----
      tips = [
            u"[方向键/WASD] 移动主角",
            u"[鼠标左键] 放置红色三角标",
            u"[鼠标右键] 取消三角标",
            u" 生成近战小兵(白方块)",
            u" 生成远程小兵(白圆)",
            u" 在主角位置建塔",
            u" 重新开始 退出",
      ]
      for i, tip in enumerate(tips):
            surf = self.font_small.render(tip, True, (170, 170, 170))
            self.screen.blit(surf, (SCREEN_WIDTH - 280,
                                    SCREEN_HEIGHT - 140 + i * 20))

      # 三角标状态提示
      if self.marker:
            surf = self.font_small.render(u"小兵围绕三角标行动中",
                                          True, COLOR_YELLOW)
            self.screen.blit(surf, (SCREEN_WIDTH // 2 - 80, 10))

    def _draw_game_over(self):
      """绘制游戏结束画面(半透明遮罩 + 文字)。"""
      # 半透明黑色遮罩
      overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
                                 pygame.SRCALPHA)
      overlay.fill((0, 0, 0, 160))
      self.screen.blit(overlay, (0, 0))
      # 游戏结束大字
      text = self.font_big.render(u"游戏结束", True, (230, 60, 60))
      rect = text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 40))
      self.screen.blit(text, rect)
      # 得分信息
      sub = self.font.render(u"最终得分: %d   到达波次: %d" %
                               (int(self.score), self.wave), True,
                               (220, 220, 220))
      rect2 = sub.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 10))
      self.screen.blit(sub, rect2)
      # 提示按键
      tip = self.font.render(u"按 重新开始   按 退出",
                               True, (180, 180, 180))
      rect3 = tip.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 50))
      self.screen.blit(tip, rect3)

    def _restart(self):
      print("restart!!")
      """重置所有游戏状态,开始新一局。"""
      self.__init__()

    # --------------------------------------------------------------------------
    # 主循环
    # --------------------------------------------------------------------------

    def run(self):
      """游戏主循环:持续处理事件、更新状态、绘制画面。"""
      while self.running:
            # 计算上一帧到这一帧的时间间隔(秒),用于帧率无关运动
            dt = self.clock.tick(FPS) / 1000.0
            # 限制 dt 上限,防止切到后台后回来出现巨大跳跃
            if dt > 0.1:
                dt = 0.1
            # 处理事件
            self._handle_events()
            # 更新游戏状态
            self._update(dt)
            # 绘制当前帧
            self._draw()
      # 退出 pygame
      pygame.quit()
      sys.exit()


# ==============================================================================
# 程序入口
# ==============================================================================

if __name__ == "__main__":
    # 创建游戏实例并启动主循环
    game = Game()
    game.run()

FishC 发表于 2026-8-28 08:15:58

你好鱼油! 你贴出了一个塔防游戏的代码,看起来是用 pygame 写的一个简单版本。我帮你梳理一下,并指出几个需要注意的地方。

1. 代码结构
代码分为几个核心部分:

[*] 全局常量(屏幕大小、颜色、速度等)
[*] 工具函数(距离计算、数值限制)
[*] 子弹类(Bullet)
[*] 小兵类(Soldier,目前只看到近战和远程小兵的定义)


操作上支持移动、放置三角标、建塔、生成小兵、重新开始和退出,功能还挺完整的。

2. 发现一个明显的 Bug
在 distance 函数里:

def distance(p1, p2):
    return math.hypot(p2 - p1, p2 - p1)

这里的 p1 和 p2 应该是坐标元组(如 (x1, y1)),不能直接相减。应该写成:

def distance(p1, p2):
    dx = p2 - p1
    dy = p2 - p1
    return math.hypot(dx, dy)

否则运行时会报错,或者算出来的距离是错的。

3. 代码不完整
你目前只贴到了 Soldier.update 的一部分,后续应该还有怪物类、塔类、主角色类、以及主循环等。如果直接运行会报“缩进错误”或“意外结束”。需要把完整代码发出来,或者补全后再测试。

4. 运行环境
确保已经安装了 pygame:
pip install pygame

5. 小建议
建议先修正 distance 函数,再把代码补完整,如果遇到问题随时发到论坛,我们一起排查。

加油,期待你的完整作品!

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。

小甲鱼的二师兄 发表于 2026-8-28 08:29:24

支持哇!!!

yinda_peng 发表于 2026-8-28 09:42:29

小时候玩的王国保卫战,猴子气球,还有一些造建筑产兵对战的游戏,都很好玩很有意思,当时在4399玩得不亦乐乎

羽风亦雨 发表于 2026-8-29 15:15:06

{:5_108:}{:5_108:}{:5_108:}{:5_108:}

cjjJasonchen 发表于 2026-8-30 10:48:11

大制作呢

磊之茶 发表于 2026-8-30 18:51:42

cjjJasonchen 发表于 2026-8-30 10:48
大制作呢

大佬我就一准大一,趁着高考暑假写一下,而且上次写代码还是小学买小甲鱼的书,捡起来磕磕绊绊的,实在是功力不足,其实后面还有个尝试加开始界面的,结果疯狂报错,我现在还在尝试调试{:10_285:}

cjjJasonchen 发表于 2026-8-31 08:23:04

磊之茶 发表于 2026-8-30 18:51
大佬我就一准大一,趁着高考暑假写一下,而且上次写代码还是小学买小甲鱼的书,捡起来磕磕绊绊的,实在是 ...

{:10_254:}我也是小萌新捏,比你大一岁哈哈哈{:10_264:}
页: [1]
查看完整版本: 一个简单的塔防游戏