一个简单的塔防游戏
# -*- 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()
你好鱼油! 你贴出了一个塔防游戏的代码,看起来是用 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 小助理,如未能正确解答您的问题,请继续追问。 支持哇!!! 小时候玩的王国保卫战,猴子气球,还有一些造建筑产兵对战的游戏,都很好玩很有意思,当时在4399玩得不亦乐乎 {:5_108:}{:5_108:}{:5_108:}{:5_108:} 大制作呢 cjjJasonchen 发表于 2026-8-30 10:48
大制作呢
大佬我就一准大一,趁着高考暑假写一下,而且上次写代码还是小学买小甲鱼的书,捡起来磕磕绊绊的,实在是功力不足,其实后面还有个尝试加开始界面的,结果疯狂报错,我现在还在尝试调试{:10_285:} 磊之茶 发表于 2026-8-30 18:51
大佬我就一准大一,趁着高考暑假写一下,而且上次写代码还是小学买小甲鱼的书,捡起来磕磕绊绊的,实在是 ...
{:10_254:}我也是小萌新捏,比你大一岁哈哈哈{:10_264:}
页:
[1]